Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
Commit graph pkg-proxy/docs
Author SHA1 Message Date
Andrew Nesbitt
866e559285
Add cooldown package pattern overrides (#407)
* Add cooldown package pattern overrides

* Expose full cooldown decisions through pattern policy

* Reject unsafe cooldown pattern syntax
2026-10-04 20:48:41 +01:00
PhantomPhoton
f60772d2d8
Support HTTP byte-range requests for local file cached artifact downloads (#404)
* feat(storage): expose efficient seek for local files

Open file-backed artifacts with os.Open so local readers support
efficient seeking. Leave cloud storage readers unchanged.

Keep the legacy sidecar mapping test focused on fileblob's reader:
Blob.Open now reads local files directly and no longer consults the
sidecar. Continue checking sidecar cleanup and post-store reads.

* feat(handler): preserve efficient seek through integrity checks

Keep the cache result's single Reader field and preserve io.Seeker
when the underlying storage reader supports efficient seeking.

Sequential reads continue through whole-object integrity verification.
A successful seek switches subsequent reads to the source for range
responses, where full-object verification is not possible.

Test both modes and verify the source is closed once.

* feat(handler): add request-aware byte range responses

Add a request-aware artifact serving helper for bounded, open-ended,
and suffix byte ranges, including If-Range handling, 206 responses,
and 416 responses for valid unsatisfiable ranges.

Ignore malformed and multi-range requests by serving the full artifact.
Advertise byte-range support only for seekable readers, preserving HEAD,
redirect, and non-seekable behavior. Clarify that net/http recovers
ErrAbortHandler per request and keeps the server running.

* feat(handler): enable byte ranges across artifact downloads

Forward each artifact request to the shared range-aware response helper,
including cached generic release assets and OCI blobs. This lets the
handler honor ranges only when its reader supports efficient seeking,
without changing direct-storage redirect behavior.

Add warm-cache endpoint tests that assert partial response headers and
bytes without an upstream refetch. Document supported range behavior,
unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 17:48:41 +01:00
montehurd
8cc63c40ee
Cache rewritten npm and Composer metadata (#402)
The npm and Composer handlers rewrite every metadata document they serve
so that download URLs point at the proxy: decode the whole document into
generic maps, change the URLs, encode it again, and for Composer expand
the minified format first. That ran on every request, cached metadata
included. With metadata caching on and upstream out of the picture, a
cached request still cost 7 ms for @babel/core, 17 ms for
symfony/console and 155 ms and 134 MB of allocations for typescript, and
an 8 vCPU VM serving cached npm packuments to 20 clients ran the proxy
at 450% CPU for about 1,100 requests a second.

Rewritten documents are now kept in memory, keyed by the ecosystem,
proxy URL, package and a SHA-256 of the raw document, so new bytes from
upstream are rewritten again and nothing is served stale. Requests that
arrive while a document is being rewritten wait for that rewrite rather
than running their own; a waiter leaves when its client does, and the
rewrite still completes and is cached. The denylist is fixed at startup,
so it needs no place in the key. Cooldown filtering depends on the
current time, so with cooldown on the cache is bypassed.

metadata_rewrite_cache_size bounds the cache (default "256MB", least
recently used out first, "0" to rewrite on every request). NewProxy
callers keep the old behaviour unless they set it.

Whole cached requests, measured locally:
  @babel/core      7.1 ms -> 0.38 ms, 54,489 -> 119 allocations
  typescript       155 ms -> 10.5 ms, 1.37M -> 133 allocations
  symfony/console  17.4 ms -> 0.48 ms, 165,909 -> 123 allocations
What remains is reading the raw document from storage and hashing it.
2026-10-03 12:09:15 +01:00
Andrew Nesbitt
e7817dc86c
Merge pull request #393 from wickedOne/analytics-6-source-attribution
[6/6] Attribute requests to a caller and a client tool
2026-10-02 10:38:16 +01:00
Andrew Nesbitt
7c129f9adb
Merge pull request #391 from wickedOne/analytics-4-stats-breakdown
[4/6] Report the per-ecosystem breakdown from GET /stats
2026-10-02 10:16:25 +01:00
Andrew Nesbitt
2124e8c8ca
Merge pull request #383 from montehurd/batch-hit-updates
Batch cache hit writes
2026-10-02 09:33:11 +01:00
Daniil Skripchenko
b40a50aad8
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them

With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.

Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.

Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address review: abort truncated streams, guard every cache read

- serveArtifact aborts the response on any body read error, and when fewer
  bytes than the declared size were written, not only on a digest mismatch.
  A failed or short upstream body in passthrough mode no longer reaches the
  client as a complete 200.
- Streamed upstream read failures are logged and counted as
  stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
  read path, including Swift archive HEAD requests, ignores entries stored
  before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
  PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.

Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rename Proxy.Passthrough to StreamArtifacts

Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:28:20 +01:00
Wicliff Wolda
3655000b16
Attribute requests to a caller and a client tool
- Track per-caller request and byte counts in a bounded table
- Export proxy_client_requests_total and
  proxy_client_response_bytes_total over a closed label set; addresses
  are never labels
- Gate the sources card behind ui_request_sources, off by default,
  since /ui carries no authentication of its own
- Move access_log.trust_forwarded_for to a top-level key
- Keep remote as the TCP peer and add remote_ip alongside it

Part 6 of 6 splitting #381 up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 16:15:03 +02:00
Wicliff Wolda
4b8c5a89fb
Report the per-ecosystem breakdown from GET /stats
- Add downloaded_bytes, downloads and an ecosystems array, served from
  the snapshot the gauges and the page already share
- Add stats_unavailable so a failed aggregation is distinguishable
  from an idle proxy
- Regenerate the OpenAPI spec

Part 4 of 6 splitting #381 up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 16:15:03 +02:00
montehurd
4827cb9d76 Batch cache hit writes
Every cached download wrote its hit in its own UPDATE. SQLite runs on
one connection, so each download was a write transaction the rest
queued behind, holding a warm npm cache near 1,000 requests a second
with the CPU two thirds idle.

Hits are now counted in memory and written in one transaction every
database.hit_flush_interval, "1s" by default, "0" for a write per hit.
A failed write keeps the hits for the next flush, and Close writes
what is pending. The same test then served about 2,200 requests a
second.

Closes #322.
2026-10-01 00:13:58 -07:00
montehurd
436155e50c Clear before deleting on eviction, and requeue failed reclaims
Eviction deleted an object before checking its record still pointed at
it. If a refetch had moved the record, that object was queued and a
request that read the record earlier could still be opening it. Eviction
now clears first, skips the delete if the record moved, and queues the
object when the delete fails after a clear.

Reclaim queues a failed delete again, which moves it behind the rest, so
objects the backend keeps refusing cannot fill every batch.

Also drop the clearArtifactCache passthrough and ClearCachedArtifact's
unused context, note that only tests call ArtifactPath, and document that
max_size does not count objects waiting to be reclaimed.
2026-09-29 10:24:22 -07:00
montehurd
4ce6c37bd9 Give each fetch its own storage path
Every fetch of an artifact wrote one path, so fetches with different URLs
or digests, which do not share a coalescing key, could overwrite each
other's bytes, serve the wrong ones, or delete them on a digest mismatch
while the other was about to open them.

storeArtifact now writes {ecosystem}/{name}/{version}/{fetch id}/{filename},
with a random id per fetch. Most ecosystems learn the digest only after
the fetch, and storage has no rename, so the id is the one rule that fits
all of them. Existing records keep their paths and stay readable. On a
file:// bucket, Delete now removes an emptied fetch directory, which
fileblob leaves behind, and no other directory, since another fetch may
be creating its own inside it.

A path a record stops pointing at is queued in pending_deletes rather
than deleted, since a request that read the record may still open it. A
loop deletes queued paths after max(1h, direct_serve_ttl), whether or not
max_size is set. UpsertArtifact and clears apply only if the record still
holds the path the caller read, so racing commits each queue the path
they replaced and a clear never orphans a newer commit. Eviction still
deletes inline, and counts space as freed only when the record still
pointed at what it deleted.
2026-09-23 16:14:43 -07:00
abhinavgautam01
c753a74666
docs: explain APT denylist limitations and version pinning 2026-09-22 12:47:37 +05:30
Andrew Nesbitt
e28745742c
Merge pull request #353 from andrewmrich/arich/debian-repositories
enable multiple debian repositories
2026-09-21 12:06:55 +01:00
Andrew Nesbitt
1da2dc0bf9
Merge pull request #359 from abhinavgautam01/fix/helm-oci-index-320
fix(helm): support OCI references in HTTP repository indexes
2026-09-21 11:59:04 +01:00
abhinavgautam01
548a50d56e
feat: add exact-version package denylist 2026-09-20 14:42:30 +05:30
abhinavgautam01
429e7458e4
fix(helm): support OCI references in HTTP repository indexes 2026-09-19 14:39:29 +05:30
Andrew Rich
8805cd9283
Merge branch 'git-pkgs:main' into arich/debian-repositories 2026-09-16 10:04:56 -07:00
pinguinfuss
be2a1f05d2
fix(server): tune the shared upstream transport defaults (#351)
* fix(server): tune the shared upstream transport defaults

server.serve builds the shared client with safehttp.New, which clones
Go's default transport: MaxIdleConnsPerHost stays 0 (an effective limit
of two idle connections per host) and ResponseHeaderTimeout stays 0.
Handing that client to fetch.NewFetcher via fetch.WithHTTPClient
replaces the fetcher's own defaults of 10 idle connections per host and
a 60-second response-header timeout.

Set both on the shared transport, matching the fetcher defaults: a
second burst of concurrent cache misses to one registry now reuses its
connections instead of re-dialling most of them, and an upstream that
accepts a request but stalls before sending headers is cut off after 60
seconds rather than only by the client's overall timeout.

Tests measure connection reuse across two concurrent bursts against a
TLS upstream that counts accepted connections (Go default: at most two
reused; tuned: all eight) and assert that a stall before headers fails
with the response-header timeout.

Fixes #327

* fix(server): reconcile http_timeout docs and tighten the transport tests

The http_timeout documentation and the config comment said "0" disables
the upstream timeout entirely. With a fixed 60-second
ResponseHeaderTimeout on the shared transport that is no longer the
whole story, so both now say that waiting for response headers stays
bounded independently of the setting.

Test cleanup from review: the 50ms settle between bursts was dead time
(the transport returns a connection to the idle pool before the body's
final Read returns, so burst returning already means the pool is
settled); the maxNewInBurst sentinel became explicit min/max bounds per
case; the stall test dropped the client.Timeout override and the
elapsed-time assertion, which was redundant with the error-text check
in any realistic run and whose failure message misattributed the cause,
and its comment now says plainly that the field assertions pin
production while the behavioural half runs at a lowered timeout.

* test(server): hold each burst at the upstream instead of sleeping

The reuse test kept a burst in flight with a 100ms handler sleep, so a
process stall longer than that between spawning the goroutines and
their dials let a request finish early and hand its connection to a
sibling. Review reproduced this with forced stalls: the default-transport
case then dialled 5 instead of 6 connections.

The handler now answers only once burstSize requests are waiting at the
same time. With HTTP/1.1 pinned that forces every burst onto burstSize
distinct connections regardless of scheduling, and the measured counts
stay exactly 6 new for Go's default and 0 for the tuned transport, also
under the same forced stalls.
2026-09-16 08:11:06 +01:00
Andrew Rich
8a51d04011 feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.

Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.

Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.

An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-15 15:35:04 -07:00
Andrew Nesbitt
b67cfb1014
Add Homebrew JSON API and bottle proxy support (#254)
* Add Homebrew JSON API and bottle proxy support

* Fix Homebrew HEAD offline fallback and non-sha256 OCI manifest handling

Route Homebrew API HEAD requests through ProxyCached so a warm cache
answers without an upstream call and stale entries are served when the
upstream is unreachable. HEAD still reaches upstream as HEAD when
metadata caching is disabled.

Limit OCI manifest digest verification to sha256 references and
Docker-Content-Digest headers so other digest algorithms are proxied
instead of rejected, and log the failing expected value.

* Reconcile with #280 and #301 after rebase

Compute real manifest digests in #280's fixture upstreams so the new
verification accepts them, and add headerETag / headerLastModified to

* Send fixed Accept for Homebrew API and match If-None-Match properly

The Homebrew API cache key does not include Accept, so replaying the
client header could serve one representation under another; the API
does not negotiate anyway. Compare If-None-Match with weak comparison,
list splitting and "*" per RFC 7232 instead of string equality, and
apply the same helper to the metadata and swift responders.

* Reconcile with #298 and #304 after rebase

Move the configureScanning doc comment back to its function after the
auto-merge stacked it on mountProtocolHandlers, and drop the second
ETag/Last-Modified set in writeMetadataCachedResponse now that the
pre-304 set covers both response paths.
2026-09-03 16:59:12 +01:00
Giles Westwood
30e54a1e0c
Add generic HTTP download proxy for GitHub release assets (mise/aqua) (#302)
* Add generic HTTP download proxy for GitHub release assets

Adds a /generic/{name}/ route backed by a new upstream.generic named-upstream
map, so tools that download from fixed URL shapes (mise's aqua backend
fetching GitHub release assets, and its tag lookups on api.github.com) can be
pointed at the proxy with client-side URL rewriting. Only configured
upstreams are reachable, so this is not an open HTTP proxy.

Paths shaped like {owner}/{repo}/releases/download/{tag}/{asset} are
version-pinned and go through the artifact cache: fetched once, hashed,
served without revalidation, and still served when the upstream is down.
Every other path goes through the metadata cache with the client's Accept
header and query string replayed, so API responses are fresh within
metadata_ttl, revalidated after that, and served stale when the upstream
fails or rate-limits the request.

Tests cover path classification, unknown upstreams and traversal, cache
hits with the upstream down, HEAD, 404 pass-through, Accept and query
forwarding, stale-on-429, cache isolation between upstreams, and that an
upstream token scoped to the release host is not sent to the object store
it redirects to.

Closes #183.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDjDeq27CKzEP2o3GWBY7F

* Use fixed Accept header for generic metadata

---------

Co-authored-by: Giles Westwood <giles@gileswestwood.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 16:44:54 +01:00
aarnaud
5499837084
Add pre-cache artifact scanning hook (trivy/ClamAV/Wiz/custom) (#298)
Runs fetched artifacts through pluggable external scanners after they're
staged in storage but before they're committed to the cache DB, so a
block verdict deletes the object instead of ever exposing it to a
client. Scanners pull the staged bytes themselves via a short-lived
HMAC-signed internal route rather than the proxy pushing bytes to them,
keeping the mechanism storage-backend-agnostic and avoiding uploading
potentially huge artifacts through the proxy's own egress.

Hardening baked in from the start: the internal scan-fetch route is
gated both at router-mount time and in the handler so it's inert
whenever scanning is disabled or unsigned; the signing key is mandatory
whenever scanning is enabled, enforced directly in scanner.NewGroup
rather than relying on callers to invoke config validation; the scan
call and the delete-on-block cleanup both run on a context detached
from the client's, so a client disconnecting mid-scan can't be mistaken
for a scanner failure, doesn't cause a legitimate artifact to be
deleted, and doesn't leave a genuinely blocked artifact's bytes
orphaned in storage; and scanner infrastructure errors (connection
failures, internal hostnames) are never forwarded verbatim to anonymous
clients, only a generic message. The scan-error metric also correctly
distinguishes a scanner's own timeout from being cancelled because a
sibling scanner already decided the verdict.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 10:06:44 +01:00
pinguinfuss
c188a54ea4
fix(handler): preserve upstream content-encoding for cached metadata (#304)
* fix(handler): preserve upstream content-encoding for cached metadata

Signed and hash-pinned index files (debian Release/Packages.gz, rpm
repomd.xml, helm index.yaml, conda repodata.json, apk APKINDEX.tar.gz)
were cached after Go's transport transparently decompressed any
Content-Encoding: gzip response, so the proxy served bytes that differ
from what the upstream signed and broke client verification.

Request the identity encoding on the shared metadata fetch so Go no
longer auto-decompresses, and persist the upstream Content-Encoding in
a new metadata_cache column so both the cached and offline responses
replay the exact bytes and header. The uncached streaming path now
forwards Content-Encoding too.

Fixes #300

* fix(handler): scope verbatim metadata fetch to the ProxyCached path

The first cut forced Accept-Encoding: identity in the shared
fetchUpstreamMetadata, which an adversarial review showed both missed
the bug and regressed unrelated ecosystems:

- proxyMetadataStream (the default path, since cache_metadata is off)
  never forced identity, so a client sending no Accept-Encoding still
  triggered Go's transparent gzip decompression and served altered bytes
  for apk/debian/rpm/conda indexes.
- Direct FetchOrCacheMetadata callers that parse or rewrite the body
  (npm, pypi, cargo, composer, pub, nuget, swift, maven, helm) were
  forced to identity too, losing wire compression and 502-ing against
  upstreams that ignore identity and gzip anyway (helm rewriteIndex).
- Rows cached before the fix kept serving decompressed bytes via ETag
  304 revalidation.

Scope the verbatim behavior to the ProxyCached code path, which serves
upstream bytes through unchanged (apk, debian, rpm, go, hex, conda,
cran, gem, conan, julia). That path now requests identity on both the
cached fetch and the uncached stream branch and replays Content-Encoding;
direct callers keep transparent compression, matching main. Migration
008 clears etag/fetched_at so legacy rows refetch once with identity.

Tests now exercise the stream path with no client Accept-Encoding and
pin that direct callers are not forced to identity.
2026-09-03 09:58:58 +01:00
Andrew Nesbitt
7743417c72
Add Swift package registry support (#259)
* Add Swift package registry support

* Fix Swift archive integrity handling

* Fix Swift registry pagination and archive HEAD requests

* Canonicalize Swift package identifiers

* Handle Swift registry cache identities

* Use stored PURLs for cache lookups

* Address review: upstream-hash refetch and cache PURL cleanup

- Re-fetch instead of 502 when a cached artifact's hash disagrees with
  the upstream-declared checksum; log and discard the stale entry.
- Rename expectedHash to upstreamHash and document how the check
  differs from checkCache's stream integrity verification.
- Drop the repository_url qualifier from swift cache PURLs so cache
  entries survive an upstream.swift change, matching other ecosystems.
- Pass name to handleSourceArchiveHead instead of re-deriving it.

* Drop BulkCheckVulnerabilities coverage after #279 removed it

The rebase over #279 (dead-code cleanup) drops BulkCheckVulnerabilities;
remove the tests and helper that exercised the swift-identity-filtering
path through it, and the imports they pulled in.
2026-09-02 13:06:34 +01:00
wickedOne
cad1a9226a
Report upstream circuit breaker state in /health and /metrics (#275)
* Report upstream circuit breaker state in /health and /metrics

* applied requested changes

* apply review changes

* Bumped github.com/git-pkgs/registries to v0.9.0
2026-09-02 12:52:32 +01:00
Andrew Nesbitt
b5ee6dd96c
Add ECR auto-refreshing upstream authentication (#278)
* Add ECR auto-refreshing upstream authentication

- Add "ecr" auth type to upstream.auth config with optional region
- Cache ecr:GetAuthorizationToken results per region and refresh
  shortly before expiry via the AWS SDK default credential chain
- Route type: ecr through the token cache in Server.authForURL
- Document the new type in config.example.yaml and docs/configuration.md

Fixes #276

* Collapse ecrTokens.header to a single return path

Drops the internal/server package below the goconst min-occurrences
threshold for the Authorization literal.

* Coalesce concurrent ECR token fetches with singleflight

Concurrent cache misses for the same region now share a single
GetAuthorizationToken call instead of each issuing their own, avoiding
a request burst against the ECR API at cold start and at each 12-hour
refresh. golang.org/x/sync is already a direct dependency.

* Improve ECR token refresh and region inference

* Back off failed ECR token refreshes
2026-09-02 12:43:43 +01:00
Abhinav Gautam
1e3369c959
fix(oci): cache tag lists and normalize manifest variants (#280)
* fix(oci): cache tag lists and normalize manifest variants

* fix(oci): refine manifest cache variants

* fix(oci): preserve manifest cache compatibility

* fix(oci): dual-write manifest cache variants

* fix(oci): preserve cached pagination links

* fix(oci): rewrite named registry pagination links
2026-09-02 12:40:35 +01:00
pinguinfuss
4b9b401d1f
Add Alpine APK repository proxy support (#293)
* Add Alpine APK repository proxy support

- Serve named APK repositories at /apk/{repository}/ with the official
  Alpine mirror as the default repository
- Cache v2 APKINDEX.tar.gz and v3 Packages.adb indexes and detached
  signatures via the metadata cache, serving stored bytes unchanged so
  apk signature verification keeps working
- Cache .apk packages in the shared artifact cache keyed by the full
  repository path, since APK filenames do not include the architecture
- Add configurable upstream repositories via upstream.apk with
  validation, plus dashboard registry instructions
- Add tests for index/signature byte fidelity, per-arch caching, cache
  hits, offline reads, upstream authentication, and 404 handling
- Document apk usage in README, config example, and configuration docs

* Serve APK package HEAD requests without a body

Use the method-aware serveArtifact helper (as container.go does) so HEAD
responses carry Content-Length but omit the body; add a regression test.

* Drop doubled blank line in docs/configuration.md

---------

Co-authored-by: Andrew Nesbitt <andrewnez@gmail.com>
2026-09-02 12:36:00 +01:00
Abhinav Gautam
7f8ccab96f
Accept inline SBOM documents in mirror API (#294)
* feat(mirror): accept inline SBOM API requests

* fix(mirror): address inline SBOM review feedback
2026-09-02 12:26:29 +01:00
Victor Chacon Codesseira
7e1cb68c7c
Add upstream.npm_full_metadata to serve publish times without cooldown (#297)
- Request application/json from the npm upstream when the option is set,
  independent of cooldown, so served packuments carry the "time" map
- Wire the option through the shared Proxy struct and the
  PROXY_UPSTREAM_NPM_FULL_METADATA environment override
- Document it in config.example.yaml and docs/configuration.md
- Test that the option forces full metadata with cooldown disabled
2026-09-02 12:13:12 +01:00
Andrew Nesbitt
f43aa9d13e
Make built-in upstream URLs configurable (#255)
Every built-in ecosystem upstream can now be set via the upstream
config block or PROXY_UPSTREAM_* env vars, with allow_private_hosts
and allow_loopback controlling access to non-public addresses.
2026-08-29 11:19:54 +01:00
Abhinav Gautam
088027cac3
feat: add Helm repository proxy support (#268)
* feat: add Helm repository proxy support

* fix(helm): address review feedback

* fix(helm): serve cached charts without index
2026-08-16 18:12:55 +01:00
Andrew Nesbitt
e4fbf3f277
Add JSONL access logging (#269)
* Add JSONL access logging

* Initialize access log before server dependencies
2026-08-16 18:07:39 +01:00
Andrew Nesbitt
538a15d9f8
fix(container): serve cached images when upstream is unavailable (#199)
* container: cache manifests for offline pulls

* Preserve direct-serve redirects for blob HEAD requests
2026-08-13 07:35:07 +01:00
Andrew Nesbitt
bbea63f046
fix(upstream): apply authentication through shared transport (#198)
* upstream: apply authentication through shared transport

* Address upstream authentication review findings
2026-08-13 07:08:23 +01:00
Andrew Nesbitt
cdbd1a9369 Canonicalize cooldown lookup PURLs to match config
NormalizedPackages runs config keys through purl.Parse().String(), which
applies per-type rules like lowercasing pypi names. The handler side was
building lookup keys with MakePURLString, which does not, so a config
entry for pkg:pypi/Django would be normalized to pkg:pypi/django and
never match the runtime key pkg:pypi/Django.

Route both sides through the same canonical form: a new
canonicalPackagePURL helper calls Normalize() on the constructed PURL
before stringifying, and all cooldown IsAllowed call sites use it.
2026-07-17 18:11:47 -07:00
Andrew Nesbitt
be15a0f826 Fix cooldown overrides for scoped package PURLs 2026-07-13 17:11:04 -07:00
Andrew Nesbitt
0ae63066e2
Make upstream HTTP timeout configurable
Add http_timeout config option (and PROXY_HTTP_TIMEOUT env var) to set
the per-request timeout on the shared HTTP client used by protocol
handlers for upstream metadata fetches and pass-through file requests.
Defaults to the previous hardcoded 30s; "0" disables the timeout.

Fixes #187
2026-07-07 17:48:36 +01:00
Andrew Nesbitt
da4fa2fc98
Add PROXY_UI_URL for separate UI/package URL advertisement (#161)
Adds UIBaseURL (env PROXY_UI_URL), advertised separately from BaseURL
for deployments where the UI is reached on a public domain while build
machines hit a Docker network alias for the package endpoints. Defaults
to BaseURL when unset.

Consumers:
- Logged alongside base_url at startup.
- <link rel="canonical"> and og:url / og:title / og:site_name in every
  UI page, omitted when UIBaseURL is empty.
- Banner on the install guide when UIBaseURL differs from BaseURL,
  clarifying that the URLs in the snippets are the package endpoint and
  that the UI itself lives elsewhere.

Docs call out that the proxy serves UI and package endpoints on the same
listener, so reverse proxies fronting the UI publicly must restrict the
public route to PathPrefix(/ui) to avoid exposing /npm, /pypi, etc.
nginx and Traefik examples both show the path-split pattern.
2026-06-07 16:12:43 +01:00
Andrew Nesbitt
08d975c397
Mount web UI under /ui (#148)
* Mount web UI under /ui (closes #123)

The UI now lives under /ui so reverse proxies can apply different
access rules to it (e.g. require auth) while leaving the package
endpoints (/npm, /pypi, /v2, ...) open to build machines.

- GET / redirects to /ui/
- /api/browse and /api/compare move to /ui/api/browse and
  /ui/api/compare since only the browser JS calls them
- /health, /stats, /metrics, /openapi.json and /api/* stay at root

* README: nginx example for splitting UI auth from package endpoints

The /ui prefix lets a reverse proxy apply different access rules to the
web UI than to the package endpoints. Adds the snippet that motivated
the prefix change so deployers don't have to derive it.
2026-06-07 15:47:29 +01:00
Andrew Nesbitt
0e7af4aed6
Make metadata size limit configurable (closes #149) (#150) 2026-06-02 07:59:00 +01:00
Mati Kepa
552c8ac4e5
Update Gradle cache with the configurable plugin support (#114)
* feat(gradle): add request metrics for build cache handler

* Implement fallback handling for Gradle plugin requests and update tests

* refactor(gradle): simplify response handling and remove unused metrics assertions

* Add tests for Gradle plugin metadata fallback and refactor metadata handling

---------

Co-authored-by: Mateusz (Mati) Kepa <m.kepa@sportradar.com>
2026-05-22 17:05:20 +01:00
Lars Wallenborn
76f41cf271
Add storage backend probe to /health (closes #73) (#119)
* config: add Health.StorageProbeInterval

* metrics: add proxy_health_probe_failures_total counter

* server: add storageProbe with happy-path test

* server: add storageProbe failure-mode tests

* server: add healthCache with TTL, single-flight, transition logging

* server: wire storage probe into /health

* server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer

Also fix Windows file-locking issue in storageProbe: close the reader
explicitly before Delete so the file handle is released prior to os.Remove.

* server: clean up stale comment in storageProbe

* docs: document storage health probe and new metric

* docs: regenerate Swagger for /health JSON response

* server: simplify rc.Close error handling in storageProbe

* server: defer probe cleanup so size/open/read/verify failures don't leak objects

Previously, storageProbe only called Delete on the success path. Any
failure between Store and the final Delete (size mismatch, Open error,
mid-stream read failure, content mismatch) left the probe object orphaned
in the storage backend. With caching disabled and Kubernetes-rate probing,
the leak could accumulate noticeably on backends like S3.

Use a named return + defer to attempt Delete after every successful Store.
The earlier-step failure remains the primary error; Delete failure only
surfaces as step="delete" when nothing else went wrong. Add a table-driven
test that asserts cleanup runs for each non-delete failure path.

Reported by Copilot on #119.

* config: validate health.storage_probe_interval in Config.Validate

The new duration field was only validated at use time in newHealthCache.
The existing codebase already validates other duration fields
(MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in
Config.Validate() so misconfiguration fails fast at startup with a
config-key-specific error.

Match that pattern. The parse-at-use code in newHealthCache stays as
a safety net, mirroring the MetadataTTL precedent.

Reported by Copilot on #119.

* docs: lowercase "counter" in metrics table for consistency

Other rows in the table use lowercase type names (counter/gauge/histogram).
Match that style.

Reported by Copilot on #119.

* docs: include size-check step in /health probe description

The probe is write → size-check → read → verify → delete; the
architecture note was missing the size-check step.

Reported by Copilot on #119.

* server: address andrew's review on #119

- Drop unused callerCtx parameter from healthCache.Check (Check is now
  parameter-less; the comment-only "accepted for symmetry" justification
  wasn't carrying its weight).
- Emit "storage": {"status": "skipped"} on DB short-circuit instead of
  omitting the key, so monitors expecting a fixed key set keep working.
- Reject negative storage_probe_interval at config validation time
  (previously parsed and silently behaved like "0").
- Extract HealthConfig.Validate to keep Config.Validate under the
  gocognit threshold and match the existing GradleBuildCacheConfig pattern.
- README Health Check section: note that /health is intended as a
  readiness probe rather than a liveness probe (Check holds a mutex
  for up to the 10s probe timeout).
- cmd/proxy/main.go godoc: column-align the new env var with the
  surrounding Gradle entries.

Reported by andrew on #119.
2026-05-22 12:14:01 +01:00
Andrew Nesbitt
f2a5b704f0
Add Julia Pkg server support (#117)
- Implement /julia/* handler for the Pkg server protocol
  (registries, registry, package, artifact, meta)
- Resolve package UUIDs to names by parsing Registry.toml from
  the General registry tarball, with a hash-guarded background
  refresh on registry updates
- Wire into router, ecosystem list, install page, badge styles
- Update README and architecture docs
2026-05-13 06:46:35 +01:00
Andrew Nesbitt
5315883c3b
Bump registries to v0.6.0 and replace internal/cooldown (#120)
- Bump github.com/git-pkgs/registries to v0.6.0: the fetcher now
  honours HTTP_PROXY, gates dialled IPs against the safehttp block
  list, and Version.Integrity is populated for pub, julia and nuget
- Replace internal/cooldown with github.com/git-pkgs/cooldown v0.1.1
  (identical surface, lifted from this repo)
- Update docs/architecture.md to point at the external package
2026-05-13 06:45:33 +01:00
Mati Kepa
31a9ca75b2
add Gradle Build Cache support with handler and tests (#87)
* add Gradle Build Cache support with handler and tests

* linting issue

* MR Suggestions: Add Gradle HTTP Build Cache configuration to README

* implement  minor stuff: Refactor Gradle handler to remove unnecessary URL parameter and update related tests

Co-authored-by: Copilot <copilot@github.com>

* Add Gradle build cache configuration and eviction support

- Introduced configuration options for Gradle build cache in config files and documentation.
- Implemented read-only mode and upload size limits for the Gradle build cache.
- Added cache eviction logic based on age and size, with corresponding tests.
- Enhanced storage interfaces to support listing objects by prefix.

* implement minor stuff: Refactor Gradle handler to remove unnecessary URL parameter and update related tests

* last finding fix

* fix tests and implement PR suggestions

Co-authored-by: Copilot <copilot@github.com>

* unify path

---------

Co-authored-by: Mateusz (Mati) Kepa <m.kepa@sportradar.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 11:15:16 +01:00
Andrew Nesbitt
8d2740624f
Structured JSON error responses for API endpoints (#110)
* Structured JSON error responses for API endpoints

API handlers returned errors via http.Error (text/plain) with ad-hoc
strings, while the mirror API used a different {"error": "..."} shape
and leaked internal err.Error() text to clients.

Add ErrorResponse{Code, Message} with stable codes (BAD_REQUEST,
NOT_FOUND, UPSTREAM_ERROR, INTERNAL_ERROR) and writeError/badRequest/
notFound/internalError helpers. Convert all JSON API handlers in
api.go, browse.go, mirror_api.go and the /stats endpoint. Enrichment
failures now report 502 UPSTREAM_ERROR rather than 500.

Protocol handlers in internal/handler/ are deliberately unchanged
since npm/pip/cargo clients expect their own response formats, not
JSON. HTML page handlers in server.go also keep text/plain.

Swagger @Failure annotations updated and docs regenerated.

Fixes #76

* Convert validatePackagePath errors to JSON in API handlers
2026-05-03 09:42:03 +01:00
Andrew Nesbitt
7346008aa5
Add metadata TTL and stale-while-revalidate support
Cached metadata is now served directly within a configurable TTL window
(default 5m) without contacting upstream, reducing latency and upstream
load. When upstream is unreachable and the cache is past its TTL, stale
content is served with a Warning: 110 header per RFC 7234.

New config: `metadata_ttl` (YAML) / `PROXY_METADATA_TTL` (env).
Set to "0" to always revalidate with upstream.
2026-04-13 09:01:05 +01:00
Andrew Nesbitt
c01f0a5c05
Fix metadata caching, 404 propagation, mirror progress, and registry stubs
- ProxyCached now stores upstream Last-Modified in the cache and uses it
  (along with ETag) for conditional request handling, returning 304 when
  client validators match. Adds Content-Length to cached responses.

- Handlers calling FetchOrCacheMetadata (pypi, composer, pub, nuget) now
  check for ErrUpstreamNotFound and return 404 instead of 502, matching
  the existing npm and cargo behavior.

- Mirror jobs report live progress via a periodic callback while running,
  so API polls return real counts instead of zeroed progress.

- Registry mirroring removed from CLI flags, API acceptance, README, and
  docs since every enumerator was a stub returning "not yet implemented".

- Added tests for the conditional metadata path (ETag/If-None-Match,
  Last-Modified/If-Modified-Since, 304 responses, header omission).
2026-04-13 09:01:05 +01:00