Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
Commit graph pkg-proxy/internal/handler
Author SHA1 Message Date
Giles Westwood
6051e20adf Cache Maven checksums and signatures as artifacts
Checksum and .asc files beside a versioned artifact are as immutable as the
artifact, but were routed through the metadata path: uncached until a
first successful fetch, with no retry or circuit breaker. A build pulling
hundreds of artifacts sent as many checksum requests straight to Maven
Central, which rate-limited the proxy, after which every further checksum
request still went upstream and failed.

Only maven-metadata.xml and its own checksums stay on the metadata path,
since the version listing changes with each release.
2026-10-14 21:34:57 +01:00
Simon Chrzanowski
ba5e6e84c5 Leave Composer dev branch archives unverified
A version's integrity is a single value shared by every archive cached
for it, but a dev branch caches one archive per commit. Recording the
listed head's shasum made a cache hit for an older pinned commit fail its
check, so its archive was discarded and fetched again, which cleared the
head's recorded shasum in turn. The listed shasum only ever describes the
current head, and Composer checks dist.shasum itself.

- Skip the shasum for dev versions
- Test a pinned commit, then the head, then both again from the cache
2026-10-14 18:06:01 +02:00
Simon Chrzanowski
bf74d982d7 Verify npm and Composer archives against their SHA-1 shasum
npm versions published before dist.integrity existed carry only a SHA-1
dist.shasum, and Composer metadata lists only a SHA-1 dist.shasum, so
neither could be checked.

- Accept native integrity given as an algorithm-prefixed hex checksum,
  verified with integrity's exact-digest check, so a recorded "sha1-<hex>"
  is also checked on cache hits
- npm: fall back to dist.shasum when dist.integrity is missing or cannot
  be parsed; a usable dist.integrity still takes precedence
- Composer: verify an archive against its dist.shasum, except another
  commit of a branch fetched in place of the listed one, which the listed
  shasum does not describe
- Fetch unverified as before when no shasum is listed, as Packagist does
  for GitHub archives, or it is not a SHA-1 digest
2026-10-14 18:04:31 +02:00
Andrew Nesbitt
e0fb8be4d9
Merge pull request #473 from simonchrz/fix/artifact-integrity
Verify npm tarballs against dist.integrity
2026-10-14 16:56:28 +01:00
Andrew Nesbitt
2a8717cb77
Merge pull request #477 from git-pkgs/fix/oci-referrers-tag-offline
Answer uncached referrers tags with 404 when the upstream is down
2026-10-14 16:52:18 +01:00
Andrew Nesbitt
0add4d7368
Merge pull request #476 from git-pkgs/fix/oci-multiple-accept-headers
Join every Accept header on container manifest requests
2026-10-14 16:51:38 +01:00
Simon Chrzanowski
2d258ddc4b Verify npm tarballs against dist.integrity
The proxy cached the first tarball upstream sent for a version without
checking it against the packument, so a tampered tarball was served to
every client after it.

- Pass the packument's dist.integrity to the existing integrity checks, so
  a fresh tarball is verified as it is stored or streamed; a mismatch is
  discarded and returns 502, or aborts the streamed response
- Record it with the cached version, so cache hits are verified against it
  through the same check as before
- Verify with integrity.Result.Verify, which accepts any digest of the
  strongest algorithm listed
- Fetch unverified as before when dist.integrity is missing or cannot be
  parsed, or the packument cannot be fetched
2026-10-14 15:40:36 +02:00
Andrew Nesbitt
27f65297c6
Merge pull request #464 from simonchrz/133_local-package-upload
Push npm and Composer packages into the proxy
2026-10-14 13:09:53 +01:00
Andrew Nesbitt
513687ce10
Answer uncached referrers tags with 404 when the upstream is down
Docker falls back to a sha256-<digest> tag when the referrers API fails
and aborts the pull on anything but a manifest or 404.
2026-10-14 12:50:03 +01:00
Andrew Nesbitt
dc809b0762
Join every Accept header on container manifest requests
Docker sends one Accept header per media type. Reading only the first
forwarded just that type upstream and ruled out cached manifests of the
others, so pulls failed when the upstream was down.
2026-10-14 12:43:14 +01:00
simon chrzanowski
c0ac418160
Serve pushed artifacts when streaming upstream artifacts
- GetLocalArtifact went through checkCache, which reports a miss when StreamArtifacts is set, so npm and Composer downloads fetched the upstream archive instead of the pushed one
- Split readStored out of lookupCache and read pushed artifacts through it regardless of StreamArtifacts
- Keep the denylist check and the 503/404 answers for unreadable or missing pushed artifacts
- Test npm and Composer downloads of pushed archives with StreamArtifacts set

Refs #133
2026-10-14 13:23:19 +02:00
simon chrzanowski
eca224bb56
Leave denied pushed versions out of package metadata
Pushing and downloading refuse denied versions, but npm and Composer metadata listed every pushed version. A version denied after it was pushed stayed listed, so resolvers picked it and then got 403 on the download. localVersions now leaves denied versions out, which covers both ecosystems and the hosted-only namespaces; npm latest falls back to the highest allowed version. /api/hosted still lists them, so operators can see and delete them.

Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
2a957b92e9
Prefer the root composer.json of a pushed archive
The manifest lookup took the first composer.json at the root or inside a top directory, in zip order. Dist archives keep composer.json files for tests and examples, so tests/composer.json stored before the root file refused the push with a name mismatch. The root composer.json now always wins. Without one, the manifest inside a top directory is used only when there is exactly one; more than one refuses the archive as ambiguous.

Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
e0ab227f14
Never replace a pushed artifact with the upstream copy
checkCache answered nil both when a stored object couldn't be opened and after an integrity failure had cleared the record. Download handlers then fetched the upstream archive for the same version and filename, and recording it turned the pushed row into an upstream one without its manifest and queued the pushed object for deletion. One storage error while downloading a patched version served the unpatched upstream archive and dropped the push.

checkCache now looks at the record on a miss. For a pushed artifact it returns ErrHostedArtifactUnavailable when the object is recorded but couldn't be served, and ErrHostedArtifactMissing when no object is recorded any more. Every path that would fetch upstream goes through it, so none of them can replace the pushed artifact. Downloads answer 503 and 404 for the two.

Refs #133
2026-10-14 13:23:19 +02:00
simon chrzanowski
69313c26d9
Serve hosted-only npm scopes and Composer vendors from pushed packages alone
A package the upstream doesn't know is served from its pushed versions, but the upstream is still asked about it. Once someone publishes the same name there, their versions are listed next to the pushed ones and a version range can resolve to them.

hosted.npm_scopes and hosted.composer_vendors (PROXY_HOSTED_NPM_SCOPES, PROXY_HOSTED_COMPOSER_VENDORS) name namespaces whose packages are served from pushed versions alone. Their metadata is built from what was pushed, npm latest included, and anything not pushed answers 404. The upstream is never asked about them. Packages in other namespaces are proxied as before.

The npm metadata handler's decisions about pushed versions move into serveLocalMetadata.

Refs #133
Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
96241b910b
Serve pushed npm tarballs before the cooldown check
The packument lists pushed versions without cooldown, but the tarball download checked cooldown against the upstream's publish times before looking at the cache. A pushed override of a version the upstream published recently was listed and then withheld with 404. Pushed tarballs are now served right after the denylist check.

Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
112fc590b7
List pushed Composer versions in package metadata
With hosted packages enabled, a package's p2 metadata lists its pushed versions, built from composer.json without the root-only keys, with a zip dist at the proxy and the time they were pushed. dist.reference is the archive's sha256 so a re-push isn't served from Composer's cache, and shasum is left empty. Dev versions go to the ~dev file and tagged versions to the other.

A pushed version takes the place of an upstream entry with the same version, with or without a leading v. Pushed entries follow the upstream ones and go through the same minified writer, so fields an earlier upstream entry set and the pushed one lacks are written as __unset rather than inherited.

When the upstream doesn't know the package, the document holds only the pushed versions. Like npm, documents with pushed versions bypass the rewrite cache.

Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
c566949573
List pushed npm versions in package metadata
With hosted packages enabled, the packument of a package with pushed versions lists each of them, built from its package.json with dist pointing at the proxy and carrying the archive's sha512, and the time it was pushed. A pushed version takes the place of an upstream entry with the same version. Cooldown and the denylist still filter only upstream versions, and dist-tags stay as upstream has them.

When the upstream doesn't know the package, the packument holds only the pushed versions, with latest set to the highest of them. Requests for a single pushed version are answered from its entry.

Rewrites that include pushed versions bypass the rewrite cache, which is keyed by the upstream document alone, so a push or delete shows on the next request and changes the ETag. Packages without pushed versions take the cached path as before, and with hosted packages disabled there is no extra lookup.

Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
e7449d1540
Read the package manifest of pushed archives
Resolvers take a version's dependencies from the registry metadata, not from the archive, so listing a pushed version needs its manifest. A push now reads package.json from the top directory of an npm tarball, or composer.json from the root or the single top directory of a Composer zip, and keeps it in the new artifacts.manifest column (migration 013_add_artifact_manifest).

The manifest has to name the pushed package and version. A Composer manifest may leave the version out, and its name is compared case-insensitively. An archive that can't be read or whose manifest names something else is refused with 400 and removed from storage.

The version's integrity is now the archive's sha512, which npm clients expect in dist.integrity.

Refs #134
2026-10-14 13:23:19 +02:00
simon chrzanowski
5e66688873
Push, list and delete npm and Composer archives through /api/hosted
With hosted.enabled, PUT /api/hosted/{ecosystem}/{name}/{version} stores the request body as the archive for that version, marked local. It replaces any cached upstream copy and is served from the cache without contacting the upstream. GET /api/hosted/{ecosystem}/{name} lists what was pushed and DELETE removes a version again, after which clients get the upstream archive if there is one. Pushing and deleting need the bearer token; listing is open like the rest of the API.

Download handlers look archives up by the filename clients request, so a push is stored under the name its ecosystem derives from name and version: {name}-{version}.tgz for npm, without the scope, and {package}-{version}.zip for Composer. Other ecosystems are refused for now.

The Composer download handler resolves the upstream metadata before it looks at the cache and answers 404 for versions the upstream doesn't list. It now serves a pushed archive first. Only artifacts marked local take that path, so cached upstream archives still go through the metadata and commit checks.

The version's integrity is set to the pushed bytes, so the cache's integrity check verifies them rather than an upstream digest. Names and versions with path separators, dot segments or control characters are refused before anything is stored.

Refs #133
2026-10-14 13:23:19 +02:00
Andrew Nesbitt
02ae49d401
Merge remote-tracking branch 'origin/main' into issue-411-mirror-images
# Conflicts:
#	cmd/proxy/main.go
#	internal/server/server.go
2026-10-14 12:21:14 +01:00
Andrew Nesbitt
784402ed30
Merge pull request #472 from simonchrz/fix/artifact-size-limits
Harden against resource exhaustion: partial blobs, artifact size, concurrent diffs
2026-10-14 12:19:53 +01:00
Andrew Nesbitt
61eeaf59ad
Merge remote-tracking branch 'origin/main' into issue-411-mirror-images
# Conflicts:
#	internal/server/server.go
2026-10-14 12:19:25 +01:00
Andrew Nesbitt
167e6b8ce7
Merge pull request #462 from pinguinfuss/issue-409-go-sumdb
Serve the Go checksum database through the proxy
2026-10-14 12:10:03 +01:00
Andrew Nesbitt
53c03c9694
Merge pull request #465 from pinguinfuss/issue-306-retention-deb
Enable storage retention for deb
2026-10-14 12:05:48 +01:00
Andrew Nesbitt
ab413e8966
Merge pull request #469 from simonchrz/fix/composer-download-cooldown
Withhold Composer archives still in cooldown
2026-10-14 11:50:48 +01:00
simon chrzanowski
b7b7a64239
Add a size limit for upstream artifacts
Artifacts had no size limit. A fetch was bounded only by the server's write
timeout, and storage.max_size evicts only after the bytes are written, so
any client could fill storage or keep fetches busy by requesting huge
artifacts.

- Add artifact_max_size (PROXY_ARTIFACT_MAX_SIZE), unlimited by default
- Refuse an artifact whose declared size is over the limit before reading
  it, and fail one of unknown size once the download passes the limit
- Apply it in the fetcher so stored, streamed and mirrored artifacts all
  honour it; nothing is cached and clients get 502
2026-10-14 08:41:57 +02:00
simon chrzanowski
aa0b543ae8
Stop forwarding custom auth headers across redirects
Handlers set the configured upstream auth header on the request itself
before handing it to the client. When upstream redirected to another host,
Go's client copied that header to the next hop: it strips only
Authorization and Cookie, so a "header" type credential such as
X-JFrog-Art-Api reached the redirect target. The auth transport already
applies credentials per hop, scoped to each hop's URL.

- Remove Proxy.applyUpstreamAuth and Proxy.AuthForURL; auth comes only
  from the transport
- Add a server test that redirects a header-authenticated upstream to
  another host and asserts the header stays with the upstream
- Wire handler tests' auth through the transport, as the server does
2026-10-14 08:38:51 +02:00
simon chrzanowski
03c8e680cc
Withhold Composer archives still in cooldown
Cooldown only filtered the metadata. A composer.lock names the proxy's
/files URL directly, so `composer install` fetched versions still inside
the cooldown window without ever seeing the filtered metadata. npm already
checks the publish time on tarball downloads.

- Return the version's publish time with its dist URL, following the
  minified format's inheritance
- Answer 404 for a version in cooldown before fetching its archive
- Read the download lookup's metadata through ReadMetadata, so it has the
  same size limit as other metadata reads
2026-10-14 08:37:39 +02:00
Christian Heim
329780050f Enable storage retention for deb
Packages are stored as pkg:deb/{name}, without a vendor and whichever configured archive they came from. Package rules may name a vendor the way PURLs usually do (pkg:deb/debian/curl, pkg:deb/ubuntu/curl); it's dropped, so a rule covers the package from every archive. The docs row warns that Debian and Ubuntu archives only keep the current version per suite.

The mirror can't fetch deb packages, so there's no mirror test.

Refs #306
2026-10-13 20:08:24 +02:00
Christian Heim
b0570bdf9d Mirroring a tag now checks it with a HEAD first and reuses the cached manifest when the digest is unchanged, falls back to the cached tag when the upstream body is cut off, and keeps a tag that a client pull moved while the walk ran. The platform filter is checked across the whole image, so a nested index without the wanted platform no longer fails it, and a 304 for a per-Accept fallback entry only refreshes that client's own keys instead of overwriting the shared entry. 2026-10-13 17:02:32 +02:00
Christian Heim
42df87e035 Forward data tiles without storing them. The go command never fetches them, and kept for good they would add up to the whole log; the record count check from before is no longer needed. 2026-10-13 16:42:03 +02:00
Christian Heim
e3cdf73a89 Wait for the clock to move on in the fresh-tree test, since on Windows the first probe's success and the start of the lookup can get the same time. 2026-10-13 16:42:03 +02:00
Christian Heim
5332f1f5e0 Don't count a tree fresh in the cache as the database answering. The probe never asked it, and during an outage that kept lookups from remembering the failure, so offline mode waited for the upstream timeout on every request until the tree expired. 2026-10-13 16:42:03 +02:00
Christian Heim
943009c669 Check that a data tile holds W records before caching it. Complete tiles are never fetched again, so a block page cached as one stayed for good. 2026-10-13 16:42:03 +02:00
Christian Heim
9532c6a94e Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.

That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.

While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.

The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-13 16:42:03 +02:00
Christian Heim
1b20307e81 Test the probe timeout, the block pages and a damaged cached tile
Also the cases around a silent or slow database: a fresh cached tree still
gets a yes on supported, a stale one does not, a late answer from the
database clears the remembered failure, a fallback to the cached tree is
not mistaken for an answer, and an answer still counts when the cache
write fails afterwards. The damaged tile test guards the size check on the
cached copy of a complete tile: it shortens the stored blob and expects
the tile to be fetched again, also past the TTL from a database that sends
an ETag.

A block page in place of a lookup must not replace the cached record, not
even one that starts like a record. The lookup fixtures now look like real
records, since lookups are checked too.
2026-10-13 16:42:03 +02:00
Christian Heim
c1a597d2d8 Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.

The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.

And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.

Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-13 16:42:03 +02:00
Christian Heim
9a93ce05b4 Check tile sizes before caching them
Complete tiles are never fetched again once cached, so a bad response would
stick around until someone deletes it by hand. That could be an error page
from a proxy in between that comes back with 200.

A hash tile is always W*32 bytes, so anything else is refused now and not
stored, and the same check runs when a cached tile is served. Data tiles
have no fixed size and only must not be empty.

A cached tile that fails the check is fetched again without asking the
database whether it changed. Past the TTL the cache would ask, and a
database that sends an ETag would answer 304 and hand back the damaged
copy every time.

The fake database in the tests now guards its maps with the mutex, since the
tests change them between requests.
2026-10-13 16:42:03 +02:00
Christian Heim
e6aadb297c Only claim checksum database support when the database is reachable
Once a proxy answers 200 on supported, the go command sends all checksum
requests there and never goes to the database directly. A proxy that cannot
reach sum.golang.org would therefore break every build that needs a lookup.

supported now fetches latest first, through the cache, and answers 404 if
that fails and nothing is cached, so clients fall back like before. A failed
check is remembered for a minute so an unreachable database doesn't make
each go command wait for an upstream timeout.
2026-10-13 16:42:03 +02:00
Christian Heim
1c6cfae813 Serve the Go checksum database from its own URL
Everything under /go/sumdb/ was passed on to the module upstream. But
proxy.golang.org doesn't serve the checksum database, so those requests
always came back 404 and every client had to talk to sum.golang.org itself.

The Go handler now serves /sumdb/{name}/ for the databases in
upstream.go_sumdbs, which defaults to sum.golang.org. Lookups, latest and
tiles come from the database through the metadata cache. Complete tiles
never change, so once they're cached we don't ask again. Unknown names and
paths that aren't part of the protocol get a 404 without going upstream.

If the database answers 404 we pass on up to 4 KiB of its text, because the
go command prints it and it tells you why a version is missing. For that the
not-found error from the metadata fetch now carries the start of the body.
Other handlers still just see ErrUpstreamNotFound.
2026-10-13 16:41:53 +02:00
Andrew Nesbitt
0ce007fdea
Merge pull request #436 from pinguinfuss/issue-263-arch-pacman
Add Arch Linux pacman repository support
2026-10-13 13:30:55 +01:00
Andrew Nesbitt
00e812421b
Merge pull request #439 from simonchrz/feature/failure-details
List recent failures under each failure count on /ui/analytics
2026-10-13 13:28:41 +01:00
Andrew Nesbitt
9a6b467412
Merge pull request #446 from pinguinfuss/issue-306-retention-nuget
Enable storage retention for nuget
2026-10-13 13:22:28 +01:00
Andrew Nesbitt
ff2c9cf37f
Merge pull request #450 from pinguinfuss/issue-306-retention-gem
Enable storage retention for gem
2026-10-13 13:21:32 +01:00
Andrew Nesbitt
c96b4a955b
Merge pull request #452 from pinguinfuss/issue-306-retention-hex
Enable storage retention for hex
2026-10-13 13:19:53 +01:00
Andrew Nesbitt
131ebc4b68
Merge pull request #454 from pinguinfuss/issue-306-retention-conda
Enable storage retention for conda
2026-10-13 13:19:19 +01:00
Andrew Nesbitt
a23ced2ef0
Merge pull request #459 from gilesw/feat/generic-builtin-artifacts
Build in version-pinned paths for well-known download hosts
2026-10-13 13:18:27 +01:00
Giles Westwood
9d0175b1e2 Try configured generic_artifacts before built-in patterns
The first matching pattern names the package, so an upstream's own
patterns now take precedence over overlapping built-ins and keep the
package names, cache entries and rules already keyed on them.
2026-10-12 19:22:42 +01:00
Christian Heim
854182fa28 Concurrent clients could still end up with a database and a signature that don't belong together. Databases are now stored under their digest like signatures, so a cache row and its bytes always agree, also with several replicas on shared storage. Writes for one database are serialized, and the signature recovery shares the refresh with database requests, so a slow fetch can no longer store an older database over a newer one. When the cache write fails, the proxy serves the database the cache still describes. Unchanged databases are no longer rewritten, and a remembered missing signature is asked for again after the metadata TTL. The new tests force these interleavings with storage and upstream hooks. 2026-10-12 20:06:59 +02:00