ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Server.Pipeline

Description

The proxy's data-plane entry point for package and artifact routes.

This module re-exports the top-level handlers for packument merges (GET /{pkg}), artifact relays (GET /{pkg}/-/{file}.tgz), and first-party publishes (PUT /{pkg}).

Ecosystem coupling

This is the npm packument pipeline: it reaches for the npm registry client, projection, and structural filter directly, so it is the one serve-path module that depends on a concrete adapter. The coupling is expedient, not intended -- the agnostic handles that would let it dispatch through an adapter (a per-adapter router, and an ecosystem-neutral filter/projection) would let a second ecosystem reuse this orchestration unchanged.

Synopsis

The packument handler

servePackument :: PackageName -> Request -> (Response -> IO ResponseReceived) -> Handler ResponseReceived Source #

Serve a GET /{pkg} packument request end to end, over the request's RequestCtx.

The mount's PackumentDeps and error renderer are read from the matched MountBinding in context, not threaded as arguments. When the mount has no packument-serve dependencies wired, the route is recognised but not served -- a 501 in the mount's surface -- rather than fabricating a result.

With dependencies wired: the edge token, if configured, is validated before any upstream is touched. Then the private and public upstreams are fetched concurrently -- the client's credential forwarded to the private origin, the public origin anonymous -- each parse failure or unavailable upstream degrading to a missing contribution rather than an error. Private versions are trusted as-is; public versions are gated through the rules and the structural filter (the FilterPlan); the surviving sets are merged (mergePackuments) and the MergePlan assembled onto the raw upstream Values to build the served body, which is then answered against the client's conditional request with our own ETag. When nothing survives, the status follows the most recoverable cause via packumentStatus. An origin whose self-reported packument name disagrees with the route is validated out -- dropped as untrusted for this request and logged -- so a single misreporting upstream never denies a package another upstream serves; when that leaves no valid origin, the request is a 502 (a responding upstream returned an invalid response), distinct from a genuine absence. Every refusal -- the edge 401 and the no-survivors 403/503/502/500 -- is rendered through the mount's MountRenderer.

headPackument :: PackageName -> Request -> (Response -> IO ResponseReceived) -> Handler ResponseReceived Source #

Serve a HEAD /{pkg} packument request: the identical pipeline and gating as servePackument -- the same fetch, merge, filter, rule decision, and no-survivors status -- answered with the identical status and headers as the GET (the would-be merged body's Content-Length and the own ETag the conditional-request machinery computes), but with the body suppressed (bodiless), as HTTP semantics require of a HEAD reply.

A packument body is assembled locally (a metadata fetch plus the cross-upstream merge), so -- unlike the tarball HEAD (headTarball) -- answering it pumps __no artifact body__ and carries no egress-amplification risk: this is the HTTP-correctness half of the explicit-HEAD handling, not the DoS lever the tarball path closes. The merged body is still materialised, to size it and compute its ETag; only the bytes are withheld from the reply.

The tarball handler

serveTarball :: PackageName -> Version -> Filename -> Request -> (Response -> IO ResponseReceived) -> Handler ResponseReceived Source #

Serve a GET /{pkg}/-/{file}.tgz artifact request end to end, over the request's RequestCtx.

The mount's PackumentDeps and error renderer are read from the matched MountBinding; an unwired mount is the recognised-but-unserved 501 stub (as for servePackument). With dependencies wired and the edge token (if any) validated, the two legs locate the tarball by the trust of their origin:

  • the private leg is a conventional stable read: it fetches {pdPrivateBaseUrl}/{pkg}/-/{file} by the requested filename (artifactRequestByFile), forwarding the client's credential and __without a private-packument fetch__; a 2xx streams the bytes through with bounded memory and answers the request, any other status (or a connection failure) is a clean miss that falls through. It applies no serve-time integrity floor -- the bytes are still verified client-side and by the mirror worker (see the module header → "Artifact path");
  • on a private miss the public leg fetches that one version's metadata anonymously and gates it against the rules; an admit honours the gated dist.tarball, streaming the public bytes and enqueuing a MirrorJob (serve-then-enqueue, the enqueue best-effort and non-blocking), a reject renders the serve error model (403/503/500/404) through the mount's renderer.

The public-upstream fetch is always anonymous (the client credential is never sent to the public upstream); the mirror job carries no credential. The serve path does not verify dist.integrity (see the module header → "Artifact path").

headTarball :: PackageName -> Version -> Filename -> Request -> (Response -> IO ResponseReceived) -> Handler ResponseReceived Source #

Serve a HEAD /{pkg}/-/{file}.tgz artifact request end to end, over the request's RequestCtx.

A HEAD must never run the full-GET streaming pump: a bodiless HEAD would otherwise open the upstream artifact connection and pump a whole artifact body that the reply then discards -- wasted upstream egress and a DoS-amplification lever (a client forcing arbitrary full-artifact fetches with cheap HEADs). So this handler gates the artifact through the identical pipeline as serveTarball -- the same edge auth, host-allowlist, internal-range, and tarball-host policy, and the same upstream-request construction -- but issues the upstream request as a HEAD and relays its status and safe response headers (relayArtifact) with no body (probeUpstreamWhen). On an admit no MirrorJob is enqueued: a HEAD serves no bytes, so there is nothing to back-fill (mirroring stays demand-driven on the GET path). A refusal renders the same serve error model with an empty body.

The first-party publish handler