| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Registry.Request
Description
Ecosystem-agnostic request mechanics shared by every registry adapter's request
layer: the redirect-pin finaliser every credential-bearing request must pass through,
conditional-GET validators, URL parsing into a typed UrlFormationError, the
empty-base-guarded path join, and the opaque-artifact request core that streams a body
byte-for-byte.
An adapter supplies only its ecosystem's protocol facts (its media types, its path
encoding, its credential scheme). The request formation itself, pinning the redirect
count, marking an artifact non-decompressing, relaying validators, parsing a URL, is
uniform across npm, PyPI, and RubyGems, so it lives here rather than in any one
ecosystem's namespace. This module reads no credential: the credential attach is an
injected Request transformation, so no ecosystem's scheme leaks into the shared home.
Synopsis
- finaliseRequest :: (Request -> Request) -> Request -> Request
- data Validators = Validators {}
- noValidators :: Validators
- addValidators :: Validators -> Request -> Request
- artifactRequestByUrl :: (Request -> Request) -> Text -> Either UrlFormationError Request
- joinPath :: Text -> Text -> Either UrlFormationError Text
- parseRequestEither :: Text -> Either UrlFormationError Request
Request finalisation
finaliseRequest :: (Request -> Request) -> Request -> Request Source #
Finalise a data-plane request: disable redirect following (redirectCount = 0)
on every request, then apply the ecosystem's injected credential attach.
This is the single request-finalisation point for the whole data plane: every adapter's
request builder funnels through it, so pinning redirectCount = 0 here makes one
invariant universal: Écluse never follows an upstream redirect, on the credentialed
and the anonymous plane alike. The credential attach is injected (a Request -> Request
function, an ecosystem's own scheme: npm's Bearer, another's Basic) rather than fixed
here, so no attach can reach the wire around the pin.
Two dangers it forecloses, one per plane:
* Credential leakage (credentialed plane). http-client's default (redirectCount =
10) re-sends the Authorization header to the redirect's Location, and its
shouldStripHeaderOnRedirect does not strip it cross-host, so a hostile or
misconfigured upstream could 302 a forwarded/minted credential to an attacker-chosen
host. That is especially dangerous on the trusted private manager, where a redirect
could exfiltrate the credential to an attacker-chosen target; pinning redirectCount = 0
removes the hop entirely rather than relying on the per-hop egress controls.
* SSRF via redirect (anonymous plane). The host allowlist is enforced when the URL is
built, not per redirect hop, so following a 302 would let an allowlisted upstream
steer an anonymous fetch to any host: an internal/cloud-metadata address or any
off-allowlist host: re-gated by nothing. Not following the redirect removes the hop
there is to gate.
The accepted consequence, symmetric across both planes: a read no longer follows an
upstream's CDN 302: it returns the 3xx to the serve path rather than chasing it. That
is the safer posture, and the proxy already honours the packument's dist.tarball
location explicitly, gated by the egress policy, rather than relying on redirects.
Redirect-following for a nonstandard upstream (a presigned/redirecting object store) is an
explicit, per-upstream opt-in, never the default.
Conditional-GET validators
data Validators Source #
The conditional-GET validators to relay on a metadata fetch. Replaying an
upstream's ETag as If-None-Match (or its Last-Modified as If-Modified-Since) lets
the upstream answer 304 Not Modified with no body: the cheap freshness check the proxy
uses on a cache revalidation. Both are forwarded only when present.
Constructors
| Validators | |
Fields
| |
Instances
| Show Validators Source # | |
Defined in Ecluse.Core.Registry.Request Methods showsPrec :: Int -> Validators -> ShowS # show :: Validators -> String # showList :: [Validators] -> ShowS # | |
| Eq Validators Source # | |
Defined in Ecluse.Core.Registry.Request | |
noValidators :: Validators Source #
No conditional-GET validators: an unconditional fetch.
addValidators :: Validators -> Request -> Request Source #
Request building
artifactRequestByUrl :: (Request -> Request) -> Text -> Either UrlFormationError Request Source #
Build the artifact GET request addressing a tarball at its __authoritative
upstream location__: the absolute url a projection preserved from the upstream's
dist.tarball, rather than reconstructing it from a (base, package, file) coordinate.
The ecosystem's credential attach is injected and the request is finalised through
finaliseRequest, so the redirect pin always applies.
The artifact location is server-chosen data, not a derivable fact: a registry may serve a version's tarball from a different host or a path a naming convention cannot rebuild. Honouring the preserved location is what lets Écluse front those registries; the URL it fetches is the same one the served metadata's integrity digest is paired with, so the bytes still verify.
The request is marked non-decompressing (decompress returns False): a tarball is
opaque binary that must reach the client byte-for-byte, so it is never gunzipped in flight
and its integrity digest stays valid. Fails with a UrlFormationError only when the url
cannot be parsed into a request.