Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-08-01 01:58:09 -04:00

Compare commits

..
Author SHA1 Message Date
dependabot[bot]
9a9a82176d
Bump modernc.org/sqlite from 1.54.0 to 1.55.0 (#219)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.54.0 to 1.55.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.54.0...v1.55.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 09:32:38 +01:00
dependabot[bot]
31ecca8cf1
Bump github.com/prometheus/client_golang from 1.23.2 to 1.24.0 (#221)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.23.2 to 1.24.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.24.0/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.2...v1.24.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 09:31:21 +01:00
dependabot[bot]
1057ee926e
Bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1 (#217)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](6599ee8b7a...6fc4b00623)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 17:55:16 +01:00
dependabot[bot]
b56a1fed65
Bump docker/login-action from 4.4.0 to 4.5.0 (#220)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](af1e73f918...06fb636fac)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 17:52:35 +01:00
dependabot[bot]
a5d456790d
Bump actions/checkout from 7.0.0 to 7.0.1 (#218)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](9c091bb21b...3d3c42e5aa)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 17:14:36 +01:00
dependabot[bot]
90422697b8
Bump google.golang.org/grpc from 1.81.1 to 1.82.1 (#216)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 12:16:27 +01:00
Andrew Nesbitt
44041d07a9
Bump git-pkgs archives, enrichment, purl, registries, vulns (#215) 2026-07-27 12:15:57 +01:00
Andrew Nesbitt
cf3741162f
fix(upstream): honor npm and Cargo overrides (#200)
* fix(upstream): honor npm and cargo overrides

* Preserve npm scope separators in download URLs

* Apply upstream auth to metadata requests
2026-07-26 19:11:24 +01:00
Tilian Honig
532e4925fe
fix: proper handling of upstream registry 404s (#209)
* fix: proper handling of upstream registry 404s

* fix: consistently return 404s for all artifact types
2026-07-26 19:07:46 +01:00
Andrew Nesbitt
6b767d0128
Merge pull request #211 from git-pkgs/dependabot/go_modules/github.com/git-pkgs/vers-0.3.0
Bump github.com/git-pkgs/vers from 0.2.6 to 0.3.0
2026-07-23 17:01:32 +01:00
Andrew Nesbitt
4737777e80
Merge pull request #212 from git-pkgs/dependabot/github_actions/zizmorcore/zizmor-action-0.6.0
Bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0
2026-07-23 16:52:51 +01:00
Andrew Nesbitt
238c628b31
Merge pull request #213 from git-pkgs/dependabot/go_modules/modernc.org/sqlite-1.54.0
Bump modernc.org/sqlite from 1.53.0 to 1.54.0
2026-07-23 16:47:03 +01:00
Andrew Nesbitt
ced9c1198b
Merge pull request #210 from git-pkgs/dependabot/github_actions/actions/setup-go-7.0.0
Bump actions/setup-go from 6.5.0 to 7.0.0
2026-07-23 16:45:51 +01:00
dependabot[bot]
13ae3970e9
Bump github.com/git-pkgs/vers from 0.2.6 to 0.3.0
Bumps [github.com/git-pkgs/vers](https://github.com/git-pkgs/vers) from 0.2.6 to 0.3.0.
- [Commits](https://github.com/git-pkgs/vers/compare/v0.2.6...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/git-pkgs/vers
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 15:42:03 +00:00
Andrew Nesbitt
7071d4fb0e
Merge pull request #214 from git-pkgs/dependabot/go_modules/github.com/git-pkgs/vulns-0.2.0
Bump github.com/git-pkgs/vulns from 0.1.6 to 0.2.0
2026-07-23 16:39:43 +01:00
dependabot[bot]
5ae5eab031
Bump github.com/git-pkgs/vulns from 0.1.6 to 0.2.0
Bumps [github.com/git-pkgs/vulns](https://github.com/git-pkgs/vulns) from 0.1.6 to 0.2.0.
- [Commits](https://github.com/git-pkgs/vulns/compare/v0.1.6...v0.2.0)

---
updated-dependencies:
- dependency-name: github.com/git-pkgs/vulns
  dependency-version: 0.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 15:14:10 +00:00
dependabot[bot]
c3f1577017
Bump modernc.org/sqlite from 1.53.0 to 1.54.0
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.53.0 to 1.54.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.53.0...v1.54.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 15:14:01 +00:00
dependabot[bot]
d9b7a30294
Bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.7 to 0.6.0.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](192e21d79a...6599ee8b7a)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 15:13:53 +00:00
dependabot[bot]
60c72be65e
Bump actions/setup-go from 6.5.0 to 7.0.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](924ae3a1cd...b7ad1dad31)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 15:13:49 +00:00
Andrew Nesbitt
0a3b9e58f2
Merge pull request #201 from git-pkgs/fix-197-cooldown-scoped-purls
Normalize cooldown package PURL overrides
2026-07-17 18:18:54 -07: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
785c989f12
Merge pull request #206 from git-pkgs/dependabot/go_modules/github.com/git-pkgs/enrichment-0.5.0
Bump github.com/git-pkgs/enrichment from 0.4.1 to 0.6.0
2026-07-17 10:23:03 -07:00
dependabot[bot]
41ef27907e
Bump github.com/git-pkgs/enrichment from 0.4.1 to 0.6.0
Bumps [github.com/git-pkgs/enrichment](https://github.com/git-pkgs/enrichment) from 0.4.1 to 0.6.0.
- [Commits](https://github.com/git-pkgs/enrichment/compare/v0.4.1...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/git-pkgs/enrichment
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-17 15:20:18 +00:00
Andrew Nesbitt
95b4e34ebe
Merge pull request #204 from git-pkgs/dependabot/go_modules/github.com/git-pkgs/purl-0.1.14
Bump github.com/git-pkgs/purl from 0.1.13 to 0.1.14
2026-07-17 08:19:09 -07:00
Andrew Nesbitt
61b91fbebb
Merge pull request #203 from git-pkgs/dependabot/go_modules/github.com/go-chi/chi/v5-5.3.1
Bump github.com/go-chi/chi/v5 from 5.3.0 to 5.3.1
2026-07-17 08:18:12 -07:00
Andrew Nesbitt
4e0575a3ab
Merge pull request #202 from git-pkgs/dependabot/docker/golang-1.26.5-alpine
Bump golang from 1.26.4-alpine to 1.26.5-alpine
2026-07-17 08:15:36 -07:00
Andrew Nesbitt
a79ec25bc9
Merge pull request #205 from git-pkgs/dependabot/github_actions/docker/login-action-4.4.0
Bump docker/login-action from 4.3.0 to 4.4.0
2026-07-16 09:50:50 -07:00
dependabot[bot]
df997e5825
Bump docker/login-action from 4.3.0 to 4.4.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c99871dec2...af1e73f918)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 15:14:14 +00:00
dependabot[bot]
96e113213d
Bump github.com/git-pkgs/purl from 0.1.13 to 0.1.14
Bumps [github.com/git-pkgs/purl](https://github.com/git-pkgs/purl) from 0.1.13 to 0.1.14.
- [Commits](https://github.com/git-pkgs/purl/compare/v0.1.13...v0.1.14)

---
updated-dependencies:
- dependency-name: github.com/git-pkgs/purl
  dependency-version: 0.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 15:14:12 +00:00
dependabot[bot]
ec3cc35795
Bump github.com/go-chi/chi/v5 from 5.3.0 to 5.3.1
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.3.0...v5.3.1)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 15:14:06 +00:00
dependabot[bot]
cdf075695f
Bump golang from 1.26.4-alpine to 1.26.5-alpine
Bumps golang from 1.26.4-alpine to 1.26.5-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.5-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 15:14:00 +00:00
Andrew Nesbitt
be15a0f826 Fix cooldown overrides for scoped package PURLs 2026-07-13 17:11:04 -07:00
Andrew Nesbitt
23364bd44a
Merge pull request #196 from ychampion/fix-go-proxy-not-found-status
fix(go): return 404 for missing modules
2026-07-12 23:32:24 +01:00
ychampion
5b959d14b9 Preserve Go proxy fallback for missing modules
Return the repository-standard not-found response when an upstream module is absent.

Constraint: GOPROXY advances to direct only after 404 or 410 responses.
Rejected: Preserve a handler-specific response body | sibling handlers consistently use not found.
Confidence: high
Scope-risk: narrow
Directive: Keep missing-artifact responses consistent across registry handlers.
Tested: go test ./internal/handler -run TestGoModuleDownloadUpstreamErrors -count=1; prior go test -race ./...; go build ./...; golangci-lint; go vet.
2026-07-12 18:48:39 +00:00
Andrew Nesbitt
18c711b4d3
Merge pull request #195 from git-pkgs/fix-filesystem-storage-path-validation
Validate filesystem storage paths
2026-07-09 17:46:32 +01:00
Andrew Nesbitt
98150ea576
Name empty path validation subtest 2026-07-09 17:41:05 +01:00
Andrew Nesbitt
41465968cb
Validate filesystem storage paths 2026-07-09 17:36:18 +01:00
Andrew Nesbitt
95bb9d897a
Merge pull request #193 from git-pkgs/dependabot/github_actions/goreleaser/goreleaser-action-7.2.3
Bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3
2026-07-09 17:19:29 +01:00
Andrew Nesbitt
16e778970f
Merge pull request #192 from git-pkgs/dependabot/github_actions/docker/login-action-4.3.0
Bump docker/login-action from 4.2.0 to 4.3.0
2026-07-09 16:41:34 +01:00
Andrew Nesbitt
a13e3ecaf1
Merge pull request #191 from git-pkgs/dependabot/go_modules/golang.org/x/sync-0.22.0
Bump golang.org/x/sync from 0.21.0 to 0.22.0
2026-07-09 16:41:08 +01:00
Andrew Nesbitt
c4f2b89290
Merge pull request #190 from git-pkgs/dependabot/github_actions/docker/build-push-action-7.3.0
Bump docker/build-push-action from 7.2.0 to 7.3.0
2026-07-09 16:40:19 +01:00
Andrew Nesbitt
bb9e560b46
Merge pull request #194 from git-pkgs/dependabot/github_actions/docker/metadata-action-6.2.0
Bump docker/metadata-action from 6.1.0 to 6.2.0
2026-07-09 16:39:00 +01:00
dependabot[bot]
383d2089cf
Bump docker/metadata-action from 6.1.0 to 6.2.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](80c7e94dd9...dc80280410)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 15:14:26 +00:00
dependabot[bot]
defe1ec96d
Bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 7.2.2 to 7.2.3.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](5daf1e915a...f06c13b6b1)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 15:14:16 +00:00
dependabot[bot]
ea4eea4b11
Bump docker/login-action from 4.2.0 to 4.3.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](650006c6eb...c99871dec2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 15:14:07 +00:00
dependabot[bot]
f7b5dd2028
Bump golang.org/x/sync from 0.21.0 to 0.22.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/sync/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 15:14:04 +00:00
dependabot[bot]
df3ad91848
Bump docker/build-push-action from 7.2.0 to 7.3.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.2.0 to 7.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](f9f3042f7e...53b7df96c9)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 15:13:56 +00:00
Andrew Nesbitt
38e2ff4ae4
Merge pull request #188 from git-pkgs/http-timeout-config
Make upstream HTTP timeout configurable
2026-07-07 18:38:18 +01:00
Andrew Nesbitt
cb8caa81d2
Merge pull request #189 from git-pkgs/bump-x-net
Bump golang.org/x/net to v0.56.0
2026-07-07 18:38:03 +01: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
41 changed files with 1027 additions and 234 deletions

View file

@ -17,12 +17,12 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
@ -35,12 +35,12 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: '1.25' go-version: '1.25'

View file

@ -20,12 +20,12 @@ jobs:
contents: read contents: read
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with: with:
persist-credentials: false persist-credentials: false
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -33,12 +33,12 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
with: with:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with: with:
context: . context: .
push: true push: true

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
@ -22,12 +22,12 @@ jobs:
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version-file: go.mod go-version-file: go.mod
cache: false cache: false
- uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with: with:
version: "~> v2" version: "~> v2"
args: release --clean args: release --clean

View file

@ -12,12 +12,12 @@ jobs:
swagger: swagger:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: '1.25' go-version: '1.25'

View file

@ -21,9 +21,9 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1

View file

@ -1,4 +1,4 @@
FROM golang:1.26.4-alpine AS builder FROM golang:1.26.5-alpine AS builder
WORKDIR /src WORKDIR /src

View file

@ -9,6 +9,11 @@ listen: ":8080"
# so users know what to point their package manager at. # so users know what to point their package manager at.
base_url: "http://localhost:8080" base_url: "http://localhost:8080"
# Timeout for individual upstream HTTP requests made by protocol handlers
# (metadata fetches, pass-through file requests). Uses Go duration syntax.
# Set to "0" to disable the timeout. Default: "30s".
# http_timeout: "30s"
# Public URL where the web UI is reached. Defaults to base_url when unset. # Public URL where the web UI is reached. Defaults to base_url when unset.
# Set this separately when the UI is served on a different hostname than the # Set this separately when the UI is served on a different hostname than the
# package endpoints — for example, the UI on a public domain behind auth while # package endpoints — for example, the UI on a public domain behind auth while
@ -163,7 +168,8 @@ cooldown:
# npm: "7d" # npm: "7d"
# cargo: "0" # cargo: "0"
# Per-package overrides (keyed by PURL) # Per-package overrides (keyed by PURL). Keys are normalized, so npm scopes
# may use either @scope or the canonical %40scope form.
# packages: # packages:
# "pkg:npm/lodash": "0" # "pkg:npm/lodash": "0"
# "pkg:npm/@babel/core": "14d" # "pkg:npm/@babel/core": "14d"

View file

@ -234,6 +234,8 @@ cooldown:
Durations support days (`7d`), hours (`48h`), and minutes (`30m`). Set to `0` to disable. Durations support days (`7d`), hours (`48h`), and minutes (`30m`). Set to `0` to disable.
Package PURL keys are normalized to canonical form before matching, so `pkg:npm/@babel/core` and `pkg:npm/%40babel/core` are equivalent, as are `pkg:pypi/Django` and `pkg:pypi/django`. If both forms configure the same package, the canonical entry wins.
Resolution order: package override, then ecosystem override, then global default. This lets you set a conservative default while exempting trusted packages. Resolution order: package override, then ecosystem override, then global default. This lets you set a conservative default while exempting trusted packages.
Currently supported for npm, PyPI, pub.dev, Composer, Cargo, NuGet, Conda, RubyGems, and Hex. These ecosystems include publish timestamps in their metadata. Currently supported for npm, PyPI, pub.dev, Composer, Cargo, NuGet, Conda, RubyGems, and Hex. These ecosystems include publish timestamps in their metadata.
@ -276,6 +278,18 @@ metadata_max_size: "100MB" # default
Or via environment variable: `PROXY_METADATA_MAX_SIZE=250MB`. Or via environment variable: `PROXY_METADATA_MAX_SIZE=250MB`.
## Upstream HTTP timeout
Protocol handlers use a shared HTTP client for upstream requests such as metadata fetches and pass-through file downloads. `http_timeout` sets that client's per-request timeout. Raise it if slow upstreams or large metadata responses cause `context deadline exceeded` errors.
```yaml
http_timeout: "30s" # default
```
Or via environment variable: `PROXY_HTTP_TIMEOUT=2m`.
Set to `"0"` to disable the timeout entirely (requests then rely only on the server's write timeout).
## Mirror API ## Mirror API
The `/api/mirror` endpoints are disabled by default. Enable them to allow starting mirror jobs via HTTP: The `/api/mirror` endpoints are disabled by default. Enable them to allow starting mirror jobs via HTTP:

45
go.mod
View file

@ -5,26 +5,26 @@ go 1.25.6
require ( require (
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.11.0 github.com/CycloneDX/cyclonedx-go v0.11.0
github.com/git-pkgs/archives v0.3.0 github.com/git-pkgs/archives v0.3.1
github.com/git-pkgs/cooldown v0.1.1 github.com/git-pkgs/cooldown v0.1.1
github.com/git-pkgs/enrichment v0.4.1 github.com/git-pkgs/enrichment v0.6.4
github.com/git-pkgs/purl v0.1.13 github.com/git-pkgs/purl v0.1.15
github.com/git-pkgs/registries v0.6.2 github.com/git-pkgs/registries v0.6.4
github.com/git-pkgs/spdx v0.1.4 github.com/git-pkgs/spdx v0.1.4
github.com/git-pkgs/vers v0.2.6 github.com/git-pkgs/vers v0.3.0
github.com/git-pkgs/vulns v0.1.6 github.com/git-pkgs/vulns v0.2.1
github.com/go-chi/chi/v5 v5.3.0 github.com/go-chi/chi/v5 v5.3.1
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.24.0
github.com/prometheus/client_model v0.6.2 github.com/prometheus/client_model v0.6.2
github.com/spdx/tools-golang v0.5.7 github.com/spdx/tools-golang v0.5.7
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
gocloud.dev v0.46.0 gocloud.dev v0.46.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.22.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.53.0 modernc.org/sqlite v1.55.0
) )
require ( require (
@ -115,7 +115,7 @@ require (
github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ecosyste-ms/ecosystems-go v0.2.0 // indirect github.com/ecosyste-ms/ecosystems-go v0.4.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect github.com/ettle/strcase v0.2.0 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
@ -217,15 +217,16 @@ require (
github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
github.com/oapi-codegen/runtime v1.4.1 // indirect github.com/oapi-codegen/nullable v1.1.0 // indirect
github.com/oapi-codegen/runtime v1.6.0 // indirect
github.com/package-url/packageurl-go v0.1.6 // indirect github.com/package-url/packageurl-go v0.1.6 // indirect
github.com/pandatix/go-cvss v0.6.2 // indirect github.com/pandatix/go-cvss v0.6.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.70.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect
@ -265,7 +266,7 @@ require (
github.com/timonwong/loggercheck v0.11.0 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.16 // indirect
github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect
@ -289,25 +290,25 @@ require (
go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
golang.org/x/mod v0.36.0 // indirect golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.38.0 // indirect golang.org/x/text v0.38.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.47.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.272.0 // indirect google.golang.org/api v0.272.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.81.1 // indirect google.golang.org/grpc v1.82.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.7.0 // indirect honnef.co/go/tools v0.7.0 // indirect
modernc.org/libc v1.73.4 // indirect modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect mvdan.cc/gofumpt v0.9.2 // indirect

116
go.sum
View file

@ -66,8 +66,8 @@ github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3w
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
@ -217,8 +217,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ecosyste-ms/ecosystems-go v0.2.0 h1:Nhpg54C+St8Sd/mf8bNJmQqx35ZgHw33TfFoxaXMQI8= github.com/ecosyste-ms/ecosystems-go v0.4.0 h1:5A+zF+XWT8sYYYjlc2/tI1SmiDGzbHLyT9CapVc5dGA=
github.com/ecosyste-ms/ecosystems-go v0.2.0/go.mod h1:CCdzT1iAZirbEZAbFSnWpK88eKKaIWex7gjtZ0UudXA= github.com/ecosyste-ms/ecosystems-go v0.4.0/go.mod h1:FVswCrp3DQkur1HjVqfDF/gYrDSEmiFflntcB1G0DbA=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
@ -244,30 +244,30 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
github.com/git-pkgs/archives v0.3.0 h1:iXKyO83jEFub1PGEDlHmk2tQ7XeV5LySTc0sEkH3x78= github.com/git-pkgs/archives v0.3.1 h1:GKUuw++0YXAAElxweVHiR4AaSShKKYoVQmyxlF5blG4=
github.com/git-pkgs/archives v0.3.0/go.mod h1:LTJ1iQVFA7otizWMOyiI82NYVmyBWAPRzwu/e30rcXU= github.com/git-pkgs/archives v0.3.1/go.mod h1:408oQv3FxLCtePa33zp3sg3njXnwH74vnHZFxkRqoPo=
github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w= github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w=
github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E= github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E=
github.com/git-pkgs/enrichment v0.4.1 h1:A8BKs0XwvpF1sF5qviZy4fkJAe18qB9OgpbRnmwnT34= github.com/git-pkgs/enrichment v0.6.4 h1:mGrfenttwmcUfPXRkWpB0wBJiiGj55ltniUh66Pq4bU=
github.com/git-pkgs/enrichment v0.4.1/go.mod h1:stHqZUitV9ZkwACqHzBysLMSe6T4QZn81hxTdSroNhM= github.com/git-pkgs/enrichment v0.6.4/go.mod h1:zz1vPUak/w8Jhajll0KDRN2MjKaEYeCzQTxumWnVhqY=
github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M= github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M=
github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4= github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4=
github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY= github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
github.com/git-pkgs/pom v0.1.5/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk= github.com/git-pkgs/pom v0.1.5/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
github.com/git-pkgs/purl v0.1.13 h1:at8BU6vnP5oonHFHAPA064BzgRqij+SZcOUDgNT2DC8= github.com/git-pkgs/purl v0.1.15 h1:iQ3clh0Cw41rkM0rf24B7ShnN9Z+UtLMAFlNDUs+Qd4=
github.com/git-pkgs/purl v0.1.13/go.mod h1:8oCcdcYZA/e1B33e7Ylju6azboTKjdqf3ybcbQj6I/o= github.com/git-pkgs/purl v0.1.15/go.mod h1:PqCLVBDeZrZgHysR803/AntMELgIr2LFZVNCcwLH2m0=
github.com/git-pkgs/registries v0.6.2 h1:26G5zW6Q7x1CSfNkaEqEjRMJiA4JwfdKOCJ7Qm+u0a8= github.com/git-pkgs/registries v0.6.4 h1:Kq/KlStjaQyE83UXT/tKuzCrIzc4keGeBjtroMqgoHA=
github.com/git-pkgs/registries v0.6.2/go.mod h1:GR0Bu6nC3NQe6f7lfDoEVqAnoQkMocf4M98B12a7B3E= github.com/git-pkgs/registries v0.6.4/go.mod h1:YkGHbxHIe2Ha/ROH6zNkS5PJUUoa9g0Ti/s2XhZnrak=
github.com/git-pkgs/spdx v0.1.4 h1:eQ0waEV3uUeItpWAOvdN1K1rL9hTgsU7fF74r1mDXMs= github.com/git-pkgs/spdx v0.1.4 h1:eQ0waEV3uUeItpWAOvdN1K1rL9hTgsU7fF74r1mDXMs=
github.com/git-pkgs/spdx v0.1.4/go.mod h1:cqRoZcvl530s/W+oGNvwjt4ODN8T1W6D/20MUZEFdto= github.com/git-pkgs/spdx v0.1.4/go.mod h1:cqRoZcvl530s/W+oGNvwjt4ODN8T1W6D/20MUZEFdto=
github.com/git-pkgs/vers v0.2.6 h1:IelZd7BP/JhzTloUTDY67nehUgoYva3g9viqAMCHJg8= github.com/git-pkgs/vers v0.3.0 h1:xM4LLUCRmqzdDfe+/pVQUx4SRyFXRVth6tOsJ14wMKU=
github.com/git-pkgs/vers v0.2.6/go.mod h1:biTbSQK1qdbrsxDEKnqe3Jzclxz8vW6uDcwKjfUGcOo= github.com/git-pkgs/vers v0.3.0/go.mod h1:biTbSQK1qdbrsxDEKnqe3Jzclxz8vW6uDcwKjfUGcOo=
github.com/git-pkgs/vulns v0.1.6 h1:8RRSgdlxp4JMU0Zykr63XTOMo5CyZKwt/PwaQxrx9Yg= github.com/git-pkgs/vulns v0.2.1 h1:tWGhOfPVDZwkM2Y9vRkMpMR+gjtlu2jhERS5JeNBoKQ=
github.com/git-pkgs/vulns v0.1.6/go.mod h1:TsZC4MjoCkKJslgmbcmRCnytwnFcjESC2N8b0a2xDWc= github.com/git-pkgs/vulns v0.2.1/go.mod h1:/0gHKHQR5SWttZVEMqgOvCXssKFwAtbac/PfkhBax9o=
github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ= github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ=
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
@ -422,8 +422,8 @@ github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3
github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -512,8 +512,8 @@ github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr
github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo= github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU=
github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
@ -541,14 +541,14 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
@ -654,8 +654,8 @@ github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVF
github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
@ -702,8 +702,8 @@ go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDoj
go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
@ -726,8 +726,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q= gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q=
@ -755,8 +755,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -784,8 +784,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -807,8 +807,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -839,8 +839,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@ -857,12 +857,12 @@ google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -882,20 +882,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@ -904,8 +904,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View file

@ -54,10 +54,12 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -110,6 +112,12 @@ type Config struct {
// size return ErrMetadataTooLarge. Default: "100MB". // size return ErrMetadataTooLarge. Default: "100MB".
MetadataMaxSize string `json:"metadata_max_size" yaml:"metadata_max_size"` MetadataMaxSize string `json:"metadata_max_size" yaml:"metadata_max_size"`
// HTTPTimeout is the timeout for individual upstream HTTP requests made
// by protocol handlers (metadata fetches, pass-through file requests).
// Uses Go duration syntax (e.g. "30s", "2m"). Default: "30s".
// Set to "0" to disable the timeout entirely.
HTTPTimeout string `json:"http_timeout" yaml:"http_timeout"`
// MirrorAPI enables the /api/mirror endpoints for starting mirror jobs via HTTP. // MirrorAPI enables the /api/mirror endpoints for starting mirror jobs via HTTP.
// Disabled by default to prevent unauthenticated users from triggering downloads. // Disabled by default to prevent unauthenticated users from triggering downloads.
MirrorAPI bool `json:"mirror_api" yaml:"mirror_api"` MirrorAPI bool `json:"mirror_api" yaml:"mirror_api"`
@ -131,9 +139,38 @@ type CooldownConfig struct {
Ecosystems map[string]string `json:"ecosystems" yaml:"ecosystems"` Ecosystems map[string]string `json:"ecosystems" yaml:"ecosystems"`
// Packages overrides the cooldown for specific packages (keyed by PURL). // Packages overrides the cooldown for specific packages (keyed by PURL).
// Valid PURL keys are normalized to canonical form before use.
Packages map[string]string `json:"packages" yaml:"packages"` Packages map[string]string `json:"packages" yaml:"packages"`
} }
// NormalizedPackages returns a copy of the package overrides with valid PURL
// keys in canonical form. An explicitly canonical key wins over an equivalent
// noncanonical key, and invalid keys are preserved unchanged.
func (c *CooldownConfig) NormalizedPackages() map[string]string {
if c == nil || c.Packages == nil {
return nil
}
keys := make([]string, 0, len(c.Packages))
for key := range c.Packages {
keys = append(keys, key)
}
sort.Strings(keys)
normalized := make(map[string]string, len(c.Packages))
for _, key := range keys {
canonical := key
if parsed, err := purl.Parse(key); err == nil {
canonical = parsed.String()
}
if _, exists := normalized[canonical]; exists && key != canonical {
continue
}
normalized[canonical] = c.Packages[key]
}
return normalized
}
// StorageConfig configures artifact storage. // StorageConfig configures artifact storage.
type StorageConfig struct { type StorageConfig struct {
// URL is the storage backend URL. // URL is the storage backend URL.
@ -460,6 +497,9 @@ func (c *Config) LoadFromEnv() {
if v := os.Getenv("PROXY_METADATA_MAX_SIZE"); v != "" { if v := os.Getenv("PROXY_METADATA_MAX_SIZE"); v != "" {
c.MetadataMaxSize = v c.MetadataMaxSize = v
} }
if v := os.Getenv("PROXY_HTTP_TIMEOUT"); v != "" {
c.HTTPTimeout = v
}
if v := os.Getenv("PROXY_GRADLE_BUILD_CACHE_READ_ONLY"); v != "" { if v := os.Getenv("PROXY_GRADLE_BUILD_CACHE_READ_ONLY"); v != "" {
c.Gradle.BuildCache.ReadOnly = v == "true" || v == "1" c.Gradle.BuildCache.ReadOnly = v == "true" || v == "1"
} }
@ -567,6 +607,10 @@ func (c *Config) Validate() error {
return err return err
} }
if err := validateHTTPTimeout(c.HTTPTimeout); err != nil {
return err
}
if err := c.Health.Validate(); err != nil { if err := c.Health.Validate(); err != nil {
return err return err
} }
@ -636,6 +680,7 @@ func (g *GradleBuildCacheConfig) Validate() error {
const ( const (
defaultMetadataTTL = 5 * time.Minute //nolint:mnd // sensible default defaultMetadataTTL = 5 * time.Minute //nolint:mnd // sensible default
defaultDirectServeTTL = 15 * time.Minute //nolint:mnd // sensible default defaultDirectServeTTL = 15 * time.Minute //nolint:mnd // sensible default
defaultHTTPTimeout = 30 * time.Second //nolint:mnd // sensible default
defaultMetadataMaxSize = 100 << 20 defaultMetadataMaxSize = 100 << 20
defaultGradleBuildCacheMaxUploadSize = 100 << 20 defaultGradleBuildCacheMaxUploadSize = 100 << 20
defaultGradleBuildCacheSweepInterval = 10 * time.Minute defaultGradleBuildCacheSweepInterval = 10 * time.Minute
@ -656,6 +701,20 @@ func (c *Config) ParseMaxSize() int64 {
return size return size
} }
func validateHTTPTimeout(s string) error {
if s == "" || s == "0" {
return nil
}
d, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("invalid http_timeout %q: %w", s, err)
}
if d < 0 {
return fmt.Errorf("invalid http_timeout %q: must be non-negative", s)
}
return nil
}
func validateMetadataMaxSize(s string) error { func validateMetadataMaxSize(s string) error {
if s == "" { if s == "" {
return nil return nil
@ -683,6 +742,22 @@ func (c *Config) ParseMetadataMaxSize() int64 {
return size return size
} }
// ParseHTTPTimeout returns the upstream HTTP client timeout.
// Returns 30s if unset, 0 (no timeout) if explicitly set to "0".
func (c *Config) ParseHTTPTimeout() time.Duration {
if c.HTTPTimeout == "" {
return defaultHTTPTimeout
}
if c.HTTPTimeout == "0" {
return 0
}
d, err := time.ParseDuration(c.HTTPTimeout)
if err != nil || d < 0 {
return defaultHTTPTimeout
}
return d
}
// ParseMetadataTTL returns the metadata TTL duration. // ParseMetadataTTL returns the metadata TTL duration.
// Returns 5 minutes if unset, 0 if explicitly disabled. // Returns 5 minutes if unset, 0 if explicitly disabled.
func (c *Config) ParseMetadataTTL() time.Duration { func (c *Config) ParseMetadataTTL() time.Duration {

View file

@ -363,6 +363,34 @@ cooldown:
if cfg.Cooldown.Packages["pkg:npm/@babel/core"] != "14d" { if cfg.Cooldown.Packages["pkg:npm/@babel/core"] != "14d" {
t.Errorf("Cooldown.Packages[@babel/core] = %q, want %q", cfg.Cooldown.Packages["pkg:npm/@babel/core"], "14d") t.Errorf("Cooldown.Packages[@babel/core] = %q, want %q", cfg.Cooldown.Packages["pkg:npm/@babel/core"], "14d")
} }
if got := cfg.Cooldown.NormalizedPackages()["pkg:npm/%40babel/core"]; got != "14d" {
t.Errorf("normalized Cooldown.Packages[@babel/core] = %q, want %q", got, "14d")
}
}
func TestCooldownConfigNormalizedPackages(t *testing.T) {
rawScoped := "pkg:npm/@typescript/typescript-darwin-arm64"
canonicalScoped := "pkg:npm/%40typescript/typescript-darwin-arm64"
cfg := CooldownConfig{Packages: map[string]string{
rawScoped: "2d",
canonicalScoped: "3d",
"not-a-purl": "4d",
}}
got := cfg.NormalizedPackages()
if got[canonicalScoped] != "3d" {
t.Errorf("canonical scoped package duration = %q, want %q", got[canonicalScoped], "3d")
}
if _, exists := got[rawScoped]; exists {
t.Errorf("raw scoped package key %q was not canonicalized", rawScoped)
}
if got["not-a-purl"] != "4d" {
t.Errorf("invalid PURL duration = %q, want preserved value %q", got["not-a-purl"], "4d")
}
if cfg.Packages[rawScoped] != "2d" {
t.Error("NormalizedPackages mutated the source map")
}
} }
func TestLoadCooldownFromEnv(t *testing.T) { func TestLoadCooldownFromEnv(t *testing.T) {
@ -524,6 +552,69 @@ func TestValidateHealthStorageProbeInterval(t *testing.T) {
} }
} }
func TestParseHTTPTimeout(t *testing.T) {
tests := []struct {
name string
timeout string
want time.Duration
}{
{"empty defaults to 30s", "", 30 * time.Second},
{"explicit zero disables", "0", 0},
{"2 minutes", "2m", 2 * time.Minute},
{"90 seconds", "90s", 90 * time.Second},
{"invalid defaults to 30s", "not-a-duration", 30 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Default()
cfg.HTTPTimeout = tt.timeout
got := cfg.ParseHTTPTimeout()
if got != tt.want {
t.Errorf("ParseHTTPTimeout() = %v, want %v", got, tt.want)
}
})
}
}
func TestValidateHTTPTimeout(t *testing.T) {
cfg := Default()
cfg.HTTPTimeout = "not-a-duration"
if err := cfg.Validate(); err == nil {
t.Error("expected validation error for invalid http_timeout")
}
cfg.HTTPTimeout = "-5s"
if err := cfg.Validate(); err == nil {
t.Error("expected validation error for negative http_timeout")
}
cfg.HTTPTimeout = "2m"
if err := cfg.Validate(); err != nil {
t.Errorf("unexpected error for valid http_timeout: %v", err)
}
cfg.HTTPTimeout = "0"
if err := cfg.Validate(); err != nil {
t.Errorf("unexpected error for zero http_timeout: %v", err)
}
cfg.HTTPTimeout = ""
if err := cfg.Validate(); err != nil {
t.Errorf("unexpected error for empty http_timeout: %v", err)
}
}
func TestLoadHTTPTimeoutFromEnv(t *testing.T) {
cfg := Default()
t.Setenv("PROXY_HTTP_TIMEOUT", "90s")
cfg.LoadFromEnv()
if cfg.HTTPTimeout != "90s" {
t.Errorf("HTTPTimeout = %q, want %q", cfg.HTTPTimeout, "90s")
}
}
func TestLoadMetadataTTLFromEnv(t *testing.T) { func TestLoadMetadataTTLFromEnv(t *testing.T) {
cfg := Default() cfg := Default()
t.Setenv("PROXY_METADATA_TTL", "10m") t.Setenv("PROXY_METADATA_TTL", "10m")

View file

@ -6,10 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -30,11 +29,18 @@ type CargoHandler struct {
} }
// NewCargoHandler creates a new cargo protocol handler. // NewCargoHandler creates a new cargo protocol handler.
func NewCargoHandler(proxy *Proxy, proxyURL string) *CargoHandler { func NewCargoHandler(proxy *Proxy, proxyURL, indexURL, downloadURL string) *CargoHandler {
if strings.TrimSpace(indexURL) == "" {
indexURL = cargoUpstream
}
if strings.TrimSpace(downloadURL) == "" {
downloadURL = cargoDownloadBase
}
return &CargoHandler{ return &CargoHandler{
proxy: proxy, proxy: proxy,
indexURL: cargoUpstream, indexURL: strings.TrimSuffix(indexURL, "/"),
downloadURL: cargoDownloadBase, downloadURL: strings.TrimSuffix(downloadURL, "/"),
proxyURL: strings.TrimSuffix(proxyURL, "/"), proxyURL: strings.TrimSuffix(proxyURL, "/"),
} }
} }
@ -143,7 +149,7 @@ func (h *CargoHandler) applyCooldownFiltering(downstreamResponse http.ResponseWr
continue continue
} }
cratePURL := purl.MakePURLString("cargo", crate.Name, "") cratePURL := canonicalPackagePURL("cargo", crate.Name)
if !h.proxy.Cooldown.IsAllowed("cargo", cratePURL, publishedAt) { if !h.proxy.Cooldown.IsAllowed("cargo", cratePURL, publishedAt) {
h.proxy.Logger.Info("cooldown: filtering cargo version", h.proxy.Logger.Info("cooldown: filtering cargo version",
@ -191,10 +197,17 @@ func (h *CargoHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
h.proxy.Logger.Info("cargo download request", h.proxy.Logger.Info("cargo download request",
"crate", name, "version", version, "filename", filename) "crate", name, "version", version, "filename", filename)
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "cargo", name, version, filename) downloadURL := fmt.Sprintf(
"%s/%s/%s",
h.downloadURL,
url.PathEscape(name),
url.PathEscape(filename),
)
result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "cargo", name, version, filename, downloadURL,
)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch crate")
http.Error(w, "failed to fetch crate", http.StatusBadGateway)
return return
} }

View file

@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"io"
"log/slog" "log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -10,6 +11,7 @@ import (
"time" "time"
"github.com/git-pkgs/cooldown" "github.com/git-pkgs/cooldown"
"github.com/git-pkgs/registries/fetch"
) )
func cargoTestProxy() *Proxy { func cargoTestProxy() *Proxy {
@ -70,6 +72,75 @@ func TestCargoConfigEndpoint(t *testing.T) {
} }
} }
func TestCargoHandlerUsesConfiguredUpstreams(t *testing.T) {
t.Run("index", func(t *testing.T) {
var requestPath, authHeader string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestPath = r.URL.Path
authHeader = r.Header.Get("Authorization")
if authHeader != "Bearer cargo-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "text/plain")
_, _ = io.WriteString(w, `{"name":"serde","vers":"1.0.0"}`)
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.HTTPClient = upstream.Client()
proxy.AuthForURL = func(string) (string, string) {
return "Authorization", "Bearer cargo-token"
}
h := NewCargoHandler(
proxy,
"http://proxy.test",
upstream.URL+"/index/",
"https://crates.example.test/files/",
)
req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
if requestPath != "/index/se/rd/serde" {
t.Errorf("upstream path = %q, want %q", requestPath, "/index/se/rd/serde")
}
if authHeader != "Bearer cargo-token" {
t.Errorf("Authorization = %q, want %q", authHeader, "Bearer cargo-token")
}
})
t.Run("download", func(t *testing.T) {
proxy, _, _, artifactFetcher := setupTestProxy(t)
artifactFetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("crate")),
ContentType: "application/gzip",
}
h := NewCargoHandler(
proxy,
"http://proxy.test",
"https://index.example.test/root/",
"https://crates.example.test/files/",
)
req := httptest.NewRequest(http.MethodGet, "/crates/serde/1.0.0/download", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
want := "https://crates.example.test/files/serde/serde-1.0.0.crate"
if artifactFetcher.fetchedURL != want {
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
}
})
}
func TestCargoIndexProxy(t *testing.T) { func TestCargoIndexProxy(t *testing.T) {
// Create a mock upstream index server // Create a mock upstream index server
indexContent := `{"name":"serde","vers":"1.0.0","deps":[],"cksum":"abc123"} indexContent := `{"name":"serde","vers":"1.0.0","deps":[],"cksum":"abc123"}

View file

@ -10,8 +10,6 @@ import (
"path" "path"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -216,7 +214,7 @@ func deepCopyValue(v any) any {
// filterAndRewriteVersions applies cooldown filtering and rewrites dist URLs // filterAndRewriteVersions applies cooldown filtering and rewrites dist URLs
// for a single package's version list. // for a single package's version list.
func (h *ComposerHandler) filterAndRewriteVersions(packageName string, versionList []any) []any { func (h *ComposerHandler) filterAndRewriteVersions(packageName string, versionList []any) []any {
packagePURL := purl.MakePURLString("composer", packageName, "") packagePURL := canonicalPackagePURL("composer", packageName)
filtered := versionList[:0] filtered := versionList[:0]
for _, v := range versionList { for _, v := range versionList {
@ -348,8 +346,7 @@ func (h *ComposerHandler) handleDownload(w http.ResponseWriter, r *http.Request)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "composer", packageName, version, filename, downloadURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "composer", packageName, version, filename, downloadURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }

View file

@ -84,8 +84,7 @@ func (h *ConanHandler) handleRecipeFile(w http.ResponseWriter, r *http.Request)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch file")
http.Error(w, "failed to fetch file", http.StatusBadGateway)
return return
} }
@ -122,8 +121,7 @@ func (h *ConanHandler) handlePackageFile(w http.ResponseWriter, r *http.Request)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch file")
http.Error(w, "failed to fetch file", http.StatusBadGateway)
return return
} }

View file

@ -6,8 +6,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -74,8 +72,7 @@ func (h *CondaHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conda", packageName, version, filename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conda", packageName, version, filename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }
@ -218,7 +215,7 @@ func (h *CondaHandler) applyCooldownFiltering(body []byte) ([]byte, error) {
continue continue
} }
packagePURL := purl.MakePURLString("conda", name, "") packagePURL := canonicalPackagePURL("conda", name)
if !h.proxy.Cooldown.IsAllowed("conda", packagePURL, publishedAt) { if !h.proxy.Cooldown.IsAllowed("conda", packagePURL, publishedAt) {
version, _ := entryMap["version"].(string) version, _ := entryMap["version"].(string)

View file

@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -117,8 +118,12 @@ func (h *ContainerHandler) handleBlobDownload(w http.ResponseWriter, r *http.Req
) )
if err != nil { if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
h.containerError(w, http.StatusNotFound, "BLOB_UNKNOWN", "blob unknown to registry")
return
}
h.proxy.Logger.Error("failed to fetch blob", "error", err) h.proxy.Logger.Error("failed to fetch blob", "error", err)
h.containerError(w, http.StatusBadGateway, "BLOB_UNKNOWN", "failed to fetch blob") h.containerError(w, http.StatusBadGateway, "INTERNAL_ERROR", "failed to fetch blob")
return return
} }

View file

@ -72,8 +72,7 @@ func (h *CRANHandler) handleSourceDownload(w http.ResponseWriter, r *http.Reques
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, version, filename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, version, filename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }
@ -107,8 +106,7 @@ func (h *CRANHandler) handleBinaryDownload(w http.ResponseWriter, r *http.Reques
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, storageVersion, filename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, storageVersion, filename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }

View file

@ -81,8 +81,7 @@ func (h *DebianHandler) handlePackageDownload(w http.ResponseWriter, r *http.Req
result, err := h.proxy.GetOrFetchArtifactFromURL( result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "deb", name, version, filename, downloadURL) r.Context(), "deb", name, version, filename, downloadURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get debian package", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }

View file

@ -0,0 +1,39 @@
package handler
import (
"net/http"
"strings"
)
type filenameDownload struct {
ecosystem string
suffix string
parseErr string
fetchErr string
parse func(string) (name, version string)
}
func (p *Proxy) handleFilenameDownload(w http.ResponseWriter, r *http.Request, d filenameDownload) {
filename := r.PathValue("filename")
if filename == "" || !strings.HasSuffix(filename, d.suffix) {
http.Error(w, "invalid filename", http.StatusBadRequest)
return
}
name, version := d.parse(filename)
if name == "" || version == "" {
http.Error(w, d.parseErr, http.StatusBadRequest)
return
}
p.Logger.Info(d.ecosystem+" download request",
"name", name, "version", version, "filename", filename)
result, err := p.GetOrFetchArtifact(r.Context(), d.ecosystem, name, version, filename)
if err != nil {
p.serveArtifactError(w, err, d.fetchErr)
return
}
ServeArtifact(w, result)
}

View file

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -60,30 +58,13 @@ func (h *GemHandler) Routes() http.Handler {
// handleDownload serves a gem file, fetching and caching from upstream if needed. // handleDownload serves a gem file, fetching and caching from upstream if needed.
func (h *GemHandler) handleDownload(w http.ResponseWriter, r *http.Request) { func (h *GemHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename") h.proxy.handleFilenameDownload(w, r, filenameDownload{
if filename == "" || !strings.HasSuffix(filename, ".gem") { ecosystem: "gem",
http.Error(w, "invalid filename", http.StatusBadRequest) suffix: ".gem",
return parseErr: "could not parse gem filename",
} fetchErr: "failed to fetch gem",
parse: h.parseGemFilename,
// Extract name and version from filename (e.g., "rails-7.1.0.gem") })
name, version := h.parseGemFilename(filename)
if name == "" || version == "" {
http.Error(w, "could not parse gem filename", http.StatusBadRequest)
return
}
h.proxy.Logger.Info("gem download request",
"name", name, "version", version, "filename", filename)
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "gem", name, version, filename)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch gem", http.StatusBadGateway)
return
}
ServeArtifact(w, result)
} }
// parseGemFilename extracts name and version from a gem filename. // parseGemFilename extracts name and version from a gem filename.
@ -266,7 +247,7 @@ func (h *GemHandler) fetchFilteredVersions(r *http.Request, name string) (map[st
return nil, err return nil, err
} }
packagePURL := purl.MakePURLString("gem", name, "") packagePURL := canonicalPackagePURL("gem", name)
filtered := make(map[string]bool) filtered := make(map[string]bool)
for _, v := range versions { for _, v := range versions {

View file

@ -1,9 +1,12 @@
package handler package handler
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/git-pkgs/registries/fetch"
) )
const ( const (
@ -100,6 +103,10 @@ func (h *GoHandler) handleDownload(w http.ResponseWriter, r *http.Request, modul
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "golang", decodedModule, version, filename) result, err := h.proxy.GetOrFetchArtifact(r.Context(), "golang", decodedModule, version, filename)
if err != nil { if err != nil {
if errors.Is(err, fetch.ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch module", http.StatusBadGateway) http.Error(w, "failed to fetch module", http.StatusBadGateway)
return return

View file

@ -1,9 +1,49 @@
package handler package handler
import ( import (
"errors"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/git-pkgs/registries/fetch"
) )
func TestGoModuleDownloadUpstreamErrors(t *testing.T) {
tests := []struct {
name string
fetchErr error
wantStatus int
}{
{
name: "module not found",
fetchErr: fetch.ErrNotFound,
wantStatus: http.StatusNotFound,
},
{
name: "upstream failure",
fetchErr: errors.New("connection refused"),
wantStatus: http.StatusBadGateway,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErr = tt.fetchErr
handler := NewGoHandler(proxy, "http://localhost:8080")
req := httptest.NewRequest(http.MethodGet, "/example.com/mod/@v/v1.0.0.zip", nil)
resp := httptest.NewRecorder()
handler.Routes().ServeHTTP(resp, req)
if resp.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d", resp.Code, tt.wantStatus)
}
})
}
}
func TestDecodeGoModule(t *testing.T) { func TestDecodeGoModule(t *testing.T) {
tests := []struct { tests := []struct {
encoded string encoded string

View file

@ -48,6 +48,14 @@ func hasDotDotSegment(path string) bool {
const defaultHTTPTimeout = 30 * time.Second const defaultHTTPTimeout = 30 * time.Second
// canonicalPackagePURL returns a versionless PURL in canonical form so cooldown
// lookups match keys produced by config.CooldownConfig.NormalizedPackages.
func canonicalPackagePURL(ecosystem, name string) string {
p := purl.MakePURL(ecosystem, name, "")
_ = p.Normalize()
return p.String()
}
const contentTypeJSON = "application/json" const contentTypeJSON = "application/json"
const headerAcceptEncoding = "Accept-Encoding" const headerAcceptEncoding = "Accept-Encoding"
@ -96,6 +104,7 @@ type Proxy struct {
// storage at an internal one. // storage at an internal one.
DirectServeBaseURL string DirectServeBaseURL string
HTTPClient *http.Client HTTPClient *http.Client
AuthForURL func(string) (headerName, headerValue string)
} }
// NewProxy creates a new Proxy with the given dependencies. // NewProxy creates a new Proxy with the given dependencies.
@ -243,6 +252,9 @@ func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, fil
// Resolve download URL // Resolve download URL
info, err := p.Resolver.Resolve(ctx, ecosystem, name, version) info, err := p.Resolver.Resolve(ctx, ecosystem, name, version)
if err != nil { if err != nil {
if errors.Is(err, fetch.ErrNotFound) {
return nil, ErrUpstreamNotFound
}
return nil, fmt.Errorf("resolving download URL: %w", err) return nil, fmt.Errorf("resolving download URL: %w", err)
} }
@ -262,6 +274,9 @@ func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, fil
if err != nil { if err != nil {
metrics.RecordUpstreamFetch(ecosystem, fetchDuration) metrics.RecordUpstreamFetch(ecosystem, fetchDuration)
metrics.RecordUpstreamError(ecosystem, "fetch_failed") metrics.RecordUpstreamError(ecosystem, "fetch_failed")
if errors.Is(err, fetch.ErrNotFound) {
return nil, ErrUpstreamNotFound
}
return nil, fmt.Errorf("fetching from upstream: %w", err) return nil, fmt.Errorf("fetching from upstream: %w", err)
} }
metrics.RecordUpstreamFetch(ecosystem, fetchDuration) metrics.RecordUpstreamFetch(ecosystem, fetchDuration)
@ -391,6 +406,7 @@ func (p *Proxy) ProxyUpstream(w http.ResponseWriter, r *http.Request, upstreamUR
req.Header.Set(header, v) req.Header.Set(header, v)
} }
} }
p.applyUpstreamAuth(req)
resp, err := p.HTTPClient.Do(req) resp, err := p.HTTPClient.Do(req)
if err != nil { if err != nil {
@ -417,6 +433,7 @@ func (p *Proxy) ProxyFile(w http.ResponseWriter, r *http.Request, upstreamURL st
http.Error(w, "failed to create request", http.StatusInternalServerError) http.Error(w, "failed to create request", http.StatusInternalServerError)
return return
} }
p.applyUpstreamAuth(req)
resp, err := p.HTTPClient.Do(req) resp, err := p.HTTPClient.Do(req)
if err != nil { if err != nil {
@ -443,7 +460,18 @@ func JSONError(w http.ResponseWriter, status int, message string) {
} }
// ErrUpstreamNotFound indicates the upstream returned 404. // ErrUpstreamNotFound indicates the upstream returned 404.
var ErrUpstreamNotFound = fmt.Errorf("upstream: not found") var ErrUpstreamNotFound = fmt.Errorf("upstream: %w", fetch.ErrNotFound)
// serveArtifactError writes response for a failed fetch:
// 404 when upstream reports artifact missing, 502 otherwise.
func (p *Proxy) serveArtifactError(w http.ResponseWriter, err error, clientMsg string) {
if errors.Is(err, ErrUpstreamNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
p.Logger.Error("failed to get artifact", "error", err)
http.Error(w, clientMsg, http.StatusBadGateway)
}
// errStale304 is returned when upstream sends 304 but the cached file is missing. // errStale304 is returned when upstream sends 304 but the cached file is missing.
var errStale304 = fmt.Errorf("upstream returned 304 but cached file is missing") var errStale304 = fmt.Errorf("upstream returned 304 but cached file is missing")
@ -546,6 +574,7 @@ func (p *Proxy) fetchUpstreamMetadata(ctx context.Context, upstreamURL string, e
return nil, "", "", zeroTime, fmt.Errorf("creating request: %w", err) return nil, "", "", zeroTime, fmt.Errorf("creating request: %w", err)
} }
req.Header.Set("Accept", accept) req.Header.Set("Accept", accept)
p.applyUpstreamAuth(req)
if entry != nil && entry.ETag.Valid { if entry != nil && entry.ETag.Valid {
req.Header.Set("If-None-Match", entry.ETag.String) req.Header.Set("If-None-Match", entry.ETag.String)
@ -735,6 +764,7 @@ func (p *Proxy) proxyMetadataStream(w http.ResponseWriter, r *http.Request, upst
accept = acceptHeaders[0] accept = acceptHeaders[0]
} }
req.Header.Set("Accept", accept) req.Header.Set("Accept", accept)
p.applyUpstreamAuth(req)
for _, header := range []string{headerAcceptEncoding, "If-Modified-Since", "If-None-Match"} { for _, header := range []string{headerAcceptEncoding, "If-Modified-Since", "If-None-Match"} {
if v := r.Header.Get(header); v != "" { if v := r.Header.Get(header); v != "" {
@ -759,6 +789,17 @@ func (p *Proxy) proxyMetadataStream(w http.ResponseWriter, r *http.Request, upst
_, _ = io.Copy(w, resp.Body) _, _ = io.Copy(w, resp.Body)
} }
func (p *Proxy) applyUpstreamAuth(req *http.Request) {
if p.AuthForURL == nil {
return
}
headerName, headerValue := p.AuthForURL(req.URL.String())
if headerName != "" && headerValue != "" {
req.Header.Set(headerName, headerValue)
}
}
// GetOrFetchArtifactFromURL retrieves an artifact from cache or fetches from a specific URL. // GetOrFetchArtifactFromURL retrieves an artifact from cache or fetches from a specific URL.
// This is useful for registries where download URLs are determined from metadata. // This is useful for registries where download URLs are determined from metadata.
func (p *Proxy) GetOrFetchArtifactFromURL(ctx context.Context, ecosystem, name, version, filename, downloadURL string) (*CacheResult, error) { func (p *Proxy) GetOrFetchArtifactFromURL(ctx context.Context, ecosystem, name, version, filename, downloadURL string) (*CacheResult, error) {
@ -787,6 +828,9 @@ func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, versi
artifact, err := p.Fetcher.FetchWithHeaders(ctx, downloadURL, headers) artifact, err := p.Fetcher.FetchWithHeaders(ctx, downloadURL, headers)
if err != nil { if err != nil {
if errors.Is(err, fetch.ErrNotFound) {
return nil, ErrUpstreamNotFound
}
return nil, fmt.Errorf("fetching from upstream: %w", err) return nil, fmt.Errorf("fetching from upstream: %w", err)
} }

View file

@ -14,6 +14,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/storage" "github.com/git-pkgs/proxy/internal/storage"
"github.com/git-pkgs/registries/fetch" "github.com/git-pkgs/registries/fetch"
@ -1010,3 +1011,33 @@ func TestProxyCached_FreshResponse_NoWarningHeader(t *testing.T) {
t.Errorf("Warning should be empty for fresh response, got %q", got) t.Errorf("Warning should be empty for fresh response, got %q", got)
} }
} }
// TestCanonicalPackagePURLMatchesConfig ensures the runtime cooldown lookup key
// agrees with config.CooldownConfig.NormalizedPackages for the same package,
// so a configured override is actually found regardless of how the user wrote it.
func TestCanonicalPackagePURLMatchesConfig(t *testing.T) {
tests := []struct {
ecosystem string
requestName string
configKey string
}{
{"npm", "@babel/core", "pkg:npm/@babel/core"},
{"npm", "@babel/core", "pkg:npm/%40babel/core"},
{"npm", "@typescript/typescript-darwin-arm64", "pkg:npm/@typescript/typescript-darwin-arm64"},
{"pypi", "Django", "pkg:pypi/Django"},
{"pypi", "django", "pkg:pypi/Django"},
{"composer", "symfony/console", "pkg:composer/Symfony/Console"},
{"cargo", "serde", "pkg:cargo/serde"},
}
for _, tt := range tests {
t.Run(tt.ecosystem+"/"+tt.requestName+"<="+tt.configKey, func(t *testing.T) {
cfg := config.CooldownConfig{Packages: map[string]string{tt.configKey: "1d"}}
normalized := cfg.NormalizedPackages()
lookup := canonicalPackagePURL(tt.ecosystem, tt.requestName)
if _, ok := normalized[lookup]; !ok {
t.Errorf("lookup key %q not found in normalized config %v", lookup, normalized)
}
})
}
}

View file

@ -10,7 +10,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
"google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/encoding/protowire"
) )
@ -54,30 +53,13 @@ func (h *HexHandler) Routes() http.Handler {
// handleDownload serves a package tarball, fetching and caching from upstream if needed. // handleDownload serves a package tarball, fetching and caching from upstream if needed.
func (h *HexHandler) handleDownload(w http.ResponseWriter, r *http.Request) { func (h *HexHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename") h.proxy.handleFilenameDownload(w, r, filenameDownload{
if filename == "" || !strings.HasSuffix(filename, ".tar") { ecosystem: "hex",
http.Error(w, "invalid filename", http.StatusBadRequest) suffix: ".tar",
return parseErr: "could not parse tarball filename",
} fetchErr: "failed to fetch package",
parse: h.parseTarballFilename,
// Extract name and version from filename (e.g., "phoenix-1.7.10.tar") })
name, version := h.parseTarballFilename(filename)
if name == "" || version == "" {
http.Error(w, "could not parse tarball filename", http.StatusBadRequest)
return
}
h.proxy.Logger.Info("hex download request",
"name", name, "version", version, "filename", filename)
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "hex", name, version, filename)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return
}
ServeArtifact(w, result)
} }
// parseTarballFilename extracts name and version from a hex tarball filename. // parseTarballFilename extracts name and version from a hex tarball filename.
@ -237,7 +219,7 @@ func (h *HexHandler) fetchFilteredVersions(r *http.Request, name string) (map[st
return nil, err return nil, err
} }
packagePURL := purl.MakePURLString("hex", name, "") packagePURL := canonicalPackagePURL("hex", name)
filtered := make(map[string]bool) filtered := make(map[string]bool)
for _, release := range pkg.Releases { for _, release := range pkg.Releases {

View file

@ -90,8 +90,7 @@ func (h *JuliaHandler) handleRegistry(w http.ResponseWriter, r *http.Request) {
upstreamURL := h.upstreamURL + r.URL.Path upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaRegistryName, hash, hash+".tar.gz", upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaRegistryName, hash, hash+".tar.gz", upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get registry", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch registry")
http.Error(w, "failed to fetch registry", http.StatusBadGateway)
return return
} }
@ -119,8 +118,7 @@ func (h *JuliaHandler) handlePackage(w http.ResponseWriter, r *http.Request) {
upstreamURL := h.upstreamURL + r.URL.Path upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", name, hash, hash+".tar.gz", upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", name, hash, hash+".tar.gz", upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get package", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }
@ -141,8 +139,7 @@ func (h *JuliaHandler) handleArtifact(w http.ResponseWriter, r *http.Request) {
upstreamURL := h.upstreamURL + r.URL.Path upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaArtifactName, hash, hash+".tar.gz", upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaArtifactName, hash, hash+".tar.gz", upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch artifact")
http.Error(w, "failed to fetch artifact", http.StatusBadGateway)
return return
} }

View file

@ -130,12 +130,7 @@ func (h *MavenHandler) handleDownload(w http.ResponseWriter, r *http.Request, ur
} }
} }
if err != nil { if err != nil {
if errors.Is(err, ErrUpstreamNotFound) { h.proxy.serveArtifactError(w, err, "failed to fetch artifact")
http.Error(w, "not found", http.StatusNotFound)
return
}
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch artifact", http.StatusBadGateway)
return return
} }

View file

@ -0,0 +1,151 @@
package handler
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/git-pkgs/registries/fetch"
)
func TestArtifactDownloadUpstreamNotFoundReturns404(t *testing.T) {
tests := []struct {
name string
path string
handler func(p *Proxy) http.Handler
}{
{"debian", "/pool/main/n/nginx/nginx_1.18.0-6_amd64.deb",
func(p *Proxy) http.Handler { return NewDebianHandler(p, "http://localhost").Routes() }},
{"rpm", "/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm",
func(p *Proxy) http.Handler { return NewRPMHandler(p, "http://localhost").Routes() }},
{"nuget", "/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg",
func(p *Proxy) http.Handler { return NewNuGetHandler(p, "http://localhost").Routes() }},
{"pypi", "/packages/packages/ab/cd/ef0123456789/requests-2.31.0-py3-none-any.whl",
func(p *Proxy) http.Handler { return NewPyPIHandler(p, "http://localhost").Routes() }},
{"cran", "/src/contrib/ggplot2_3.4.4.tar.gz",
func(p *Proxy) http.Handler { return NewCRANHandler(p, "http://localhost").Routes() }},
{"conda", "/conda-forge/linux-64/numpy-1.26.0-py311_0.tar.bz2",
func(p *Proxy) http.Handler { return NewCondaHandler(p, "http://localhost").Routes() }},
{"conan", "/v1/files/zlib/1.3.1/_/_/0/recipe/conan_sources.tgz",
func(p *Proxy) http.Handler { return NewConanHandler(p, "http://localhost").Routes() }},
{"gem", "/gems/rails-7.1.0.gem",
func(p *Proxy) http.Handler { return NewGemHandler(p, "http://localhost").Routes() }},
{"hex", "/tarballs/phoenix-1.7.10.tar",
func(p *Proxy) http.Handler { return NewHexHandler(p, "http://localhost").Routes() }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErr = fetch.ErrNotFound
srv := httptest.NewServer(tt.handler(proxy))
defer srv.Close()
resp, err := http.Get(srv.URL + tt.path)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("want 404 for missing upstream artifact, got %d", resp.StatusCode)
}
})
}
}
func TestJuliaPackageUpstreamNotFoundReturns404(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErr = fetch.ErrNotFound
dead := httptest.NewServer(http.NotFoundHandler())
defer dead.Close()
h := NewJuliaHandler(proxy, "http://localhost")
h.upstreamURL = dead.URL
srv := httptest.NewServer(h.Routes())
defer srv.Close()
resp, err := http.Get(srv.URL +
"/package/7876af07-990d-54b4-ab0e-23690620f79a/0123456789abcdef0123456789abcdef01234567")
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("want 404 for missing upstream package, got %d", resp.StatusCode)
}
}
func TestComposerDownloadUpstreamNotFoundReturns404(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErr = fetch.ErrNotFound
meta := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/p2/monolog/monolog.json" {
_, _ = w.Write([]byte(`{
"packages": {
"monolog/monolog": [
{"version": "2.9.1", "dist": {"url": "https://example.com/monolog-2.9.1.zip", "type": "zip"}}
]
}
}`))
return
}
http.NotFound(w, r)
}))
defer meta.Close()
h := &ComposerHandler{proxy: proxy, repoURL: meta.URL, proxyURL: "http://localhost"}
srv := httptest.NewServer(h.Routes())
defer srv.Close()
resp, err := http.Get(srv.URL + "/files/monolog/monolog/2.9.1/monolog-2.9.1.zip")
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("want 404 for missing upstream dist, got %d", resp.StatusCode)
}
}
func TestContainerBlobUpstreamNotFoundReturns404(t *testing.T) {
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"token": "test-token-123"}`))
}))
defer authServer.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.Fetcher = &mockFetcherWithHeaders{
fetchFn: func(_ context.Context, _ string, _ http.Header) (*fetch.Artifact, error) {
return nil, fetch.ErrNotFound
},
}
h := &ContainerHandler{
proxy: proxy,
registryURL: "https://registry-1.docker.io",
authURL: authServer.URL,
proxyURL: "http://localhost:8080",
}
req := httptest.NewRequest(http.MethodGet,
"/library/nginx/blobs/sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("want 404 for missing upstream blob, got %d; body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "BLOB_UNKNOWN") {
t.Errorf("want BLOB_UNKNOWN error code in body, got: %s", w.Body.String())
}
}

View file

@ -0,0 +1,83 @@
package handler
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/git-pkgs/registries/fetch"
)
func TestErrUpstreamNotFoundWrapsFetchErrNotFound(t *testing.T) {
if !errors.Is(ErrUpstreamNotFound, fetch.ErrNotFound) {
t.Fatal("ErrUpstreamNotFound does not wrap fetch.ErrNotFound")
}
}
func TestGetOrFetchArtifactFromURL_NotFound(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErr = fetch.ErrNotFound
_, err := proxy.GetOrFetchArtifactFromURL(context.Background(),
"maven", "org.example:missing", "1.0", "missing-1.0.jar",
"http://upstream.test/org/example/missing/1.0/missing-1.0.jar")
if !errors.Is(err, ErrUpstreamNotFound) {
t.Fatalf("want ErrUpstreamNotFound, got %v", err)
}
}
func TestMavenHandler_UpstreamNotFoundReturns404(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErr = fetch.ErrNotFound
h := NewMavenHandler(proxy, "http://localhost", "http://upstream.test", "http://portal.test")
srv := httptest.NewServer(h.Routes())
defer srv.Close()
resp, err := http.Get(srv.URL + "/org/example/missing/1.0/missing-1.0.jar")
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("want 404 for missing upstream artifact, got %d", resp.StatusCode)
}
}
func TestMavenHandler_PluginPortalFallback(t *testing.T) {
proxy, _, _, fetcher := setupTestProxy(t)
fetcher.fetchErrByURL = map[string]error{
"http://upstream.test/org/example/plugin/1.0/plugin-1.0.jar": fetch.ErrNotFound,
}
fetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("portal artifact")),
ContentType: "application/java-archive",
}
h := NewMavenHandler(proxy, "http://localhost", "http://upstream.test", "http://portal.test")
srv := httptest.NewServer(h.Routes())
defer srv.Close()
resp, err := http.Get(srv.URL + "/org/example/plugin/1.0/plugin-1.0.jar")
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Fatalf("want 200 via plugin portal fallback, got %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != "portal artifact" {
t.Errorf("want portal artifact body, got %q", body)
}
if fetcher.fetchedURL != "http://portal.test/org/example/plugin/1.0/plugin-1.0.jar" {
t.Errorf("fallback did not hit plugin portal, last URL: %s", fetcher.fetchedURL)
}
}

View file

@ -9,8 +9,6 @@ import (
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -27,10 +25,14 @@ type NPMHandler struct {
} }
// NewNPMHandler creates a new npm protocol handler. // NewNPMHandler creates a new npm protocol handler.
func NewNPMHandler(proxy *Proxy, proxyURL string) *NPMHandler { func NewNPMHandler(proxy *Proxy, proxyURL, upstreamURL string) *NPMHandler {
if strings.TrimSpace(upstreamURL) == "" {
upstreamURL = npmUpstream
}
return &NPMHandler{ return &NPMHandler{
proxy: proxy, proxy: proxy,
upstreamURL: npmUpstream, upstreamURL: strings.TrimSuffix(upstreamURL, "/"),
proxyURL: strings.TrimSuffix(proxyURL, "/"), proxyURL: strings.TrimSuffix(proxyURL, "/"),
} }
} }
@ -134,7 +136,7 @@ func (h *NPMHandler) applyCooldownFiltering(metadata map[string]any, versions ma
return return
} }
packagePURL := purl.MakePURLString("npm", packageName, "") packagePURL := canonicalPackagePURL("npm", packageName)
for version := range versions { for version := range versions {
publishedStr, ok := timeMap[version].(string) publishedStr, ok := timeMap[version].(string)
@ -263,8 +265,20 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
h.proxy.Logger.Info("npm download request", h.proxy.Logger.Info("npm download request",
"package", packageName, "version", version, "filename", filename) "package", packageName, "version", version, "filename", filename)
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "npm", packageName, version, filename) downloadURL := fmt.Sprintf(
"%s/%s/-/%s",
h.upstreamURL,
escapeNPMDownloadPackage(packageName),
url.PathEscape(filename),
)
result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "npm", packageName, version, filename, downloadURL,
)
if err != nil { if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
JSONError(w, http.StatusNotFound, "package not found")
return
}
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.Logger.Error("failed to get artifact", "error", err)
JSONError(w, http.StatusBadGateway, "failed to fetch package") JSONError(w, http.StatusBadGateway, "failed to fetch package")
return return
@ -273,6 +287,14 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
ServeArtifact(w, result) ServeArtifact(w, result)
} }
func escapeNPMDownloadPackage(packageName string) string {
scope, name, scoped := strings.Cut(packageName, "/")
if scoped && strings.HasPrefix(scope, "@") && len(scope) > 1 && name != "" && !strings.Contains(name, "/") {
return url.PathEscape(scope) + "/" + url.PathEscape(name)
}
return url.PathEscape(packageName)
}
// extractPackageName extracts the package name from the request path. // extractPackageName extracts the package name from the request path.
// Handles both scoped (@scope/name) and unscoped (name) packages. // Handles both scoped (@scope/name) and unscoped (name) packages.
func (h *NPMHandler) extractPackageName(r *http.Request) string { func (h *NPMHandler) extractPackageName(r *http.Request) string {

View file

@ -2,13 +2,16 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"io"
"log/slog" "log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
"github.com/git-pkgs/cooldown" "github.com/git-pkgs/cooldown"
"github.com/git-pkgs/registries/fetch"
) )
const testVersion100 = "1.0.0" const testVersion100 = "1.0.0"
@ -46,6 +49,86 @@ func TestNPMExtractVersionFromFilename(t *testing.T) {
} }
} }
func TestNPMHandlerUsesConfiguredUpstream(t *testing.T) {
t.Run("metadata", func(t *testing.T) {
var requestPath, authHeader string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestPath = r.URL.Path
authHeader = r.Header.Get("Authorization")
if authHeader != "Bearer npm-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"versions":{}}`)
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.HTTPClient = upstream.Client()
proxy.AuthForURL = func(string) (string, string) {
return "Authorization", "Bearer npm-token"
}
h := NewNPMHandler(proxy, "http://proxy.test", upstream.URL+"/root/")
req := httptest.NewRequest(http.MethodGet, "/testpkg", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
if requestPath != "/root/testpkg" {
t.Errorf("upstream path = %q, want %q", requestPath, "/root/testpkg")
}
if authHeader != "Bearer npm-token" {
t.Errorf("Authorization = %q, want %q", authHeader, "Bearer npm-token")
}
})
t.Run("download", func(t *testing.T) {
proxy, _, _, artifactFetcher := setupTestProxy(t)
artifactFetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("package")),
ContentType: "application/gzip",
}
h := NewNPMHandler(proxy, "http://proxy.test", "https://npm.example.test/root/")
req := httptest.NewRequest(http.MethodGet, "/testpkg/-/testpkg-1.0.0.tgz", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
want := "https://npm.example.test/root/testpkg/-/testpkg-1.0.0.tgz"
if artifactFetcher.fetchedURL != want {
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
}
})
t.Run("scoped download", func(t *testing.T) {
proxy, _, _, artifactFetcher := setupTestProxy(t)
artifactFetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("package")),
ContentType: "application/gzip",
}
h := NewNPMHandler(proxy, "http://proxy.test", "https://npm.example.test/root/")
req := httptest.NewRequest(http.MethodGet, "/@scope/name/-/name-1.0.0.tgz", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
want := "https://npm.example.test/root/@scope/name/-/name-1.0.0.tgz"
if artifactFetcher.fetchedURL != want {
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
}
})
}
func TestNPMRewriteMetadata(t *testing.T) { func TestNPMRewriteMetadata(t *testing.T) {
h := &NPMHandler{ h := &NPMHandler{
proxy: testProxy(), proxy: testProxy(),

View file

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -271,7 +269,7 @@ func (h *NuGetHandler) applyCooldownFiltering(body []byte) ([]byte, error) {
} }
} }
packagePURL := purl.MakePURLString("nuget", strings.ToLower(id), "") packagePURL := canonicalPackagePURL("nuget", strings.ToLower(id))
if !h.proxy.Cooldown.IsAllowed("nuget", packagePURL, publishedAt) { if !h.proxy.Cooldown.IsAllowed("nuget", packagePURL, publishedAt) {
h.proxy.Logger.Info("cooldown: filtering nuget version", h.proxy.Logger.Info("cooldown: filtering nuget version",
@ -316,8 +314,7 @@ func (h *NuGetHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "nuget", name, version, filename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "nuget", name, version, filename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }

View file

@ -7,8 +7,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -69,8 +67,7 @@ func (h *PubHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "pub", name, version, filename) result, err := h.proxy.GetOrFetchArtifact(r.Context(), "pub", name, version, filename)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }
@ -127,7 +124,7 @@ func (h *PubHandler) rewriteMetadata(name string, body []byte) ([]byte, error) {
return body, nil return body, nil
} }
packagePURL := purl.MakePURLString("pub", name, "") packagePURL := canonicalPackagePURL("pub", name)
filtered := h.filterAndRewriteVersions(name, packagePURL, versions) filtered := h.filterAndRewriteVersions(name, packagePURL, versions)
metadata["versions"] = filtered metadata["versions"] = filtered

View file

@ -12,8 +12,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/git-pkgs/purl"
) )
const ( const (
@ -131,7 +129,7 @@ func (h *PyPIHandler) fetchFilteredVersions(r *http.Request, name string) map[st
return nil return nil
} }
packagePURL := purl.MakePURLString("pypi", name, "") packagePURL := canonicalPackagePURL("pypi", name)
filtered := make(map[string]bool) filtered := make(map[string]bool)
for version, files := range releases { for version, files := range releases {
@ -262,7 +260,7 @@ func (h *PyPIHandler) rewriteJSONMetadata(body []byte) ([]byte, error) {
packageName, _ := extractPyPIName(metadata) packageName, _ := extractPyPIName(metadata)
packagePURL := "" packagePURL := ""
if packageName != "" { if packageName != "" {
packagePURL = purl.MakePURLString("pypi", packageName, "") packagePURL = canonicalPackagePURL("pypi", packageName)
} }
h.filterAndRewriteReleases(metadata, packageName, packagePURL) h.filterAndRewriteReleases(metadata, packageName, packagePURL)
@ -428,8 +426,7 @@ func (h *PyPIHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "pypi", name, version, filename, upstreamURL) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "pypi", name, version, filename, upstreamURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }

View file

@ -83,8 +83,7 @@ func (h *RPMHandler) handlePackageDownload(w http.ResponseWriter, r *http.Reques
result, err := h.proxy.GetOrFetchArtifactFromURL( result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "rpm", name, version, filename, downloadURL) r.Context(), "rpm", name, version, filename, downloadURL)
if err != nil { if err != nil {
h.proxy.Logger.Error("failed to get rpm package", "error", err) h.proxy.serveArtifactError(w, err, "failed to fetch package")
http.Error(w, "failed to fetch package", http.StatusBadGateway)
return return
} }

View file

@ -163,9 +163,11 @@ func (s *Server) Start() error {
cd := &cooldown.Config{ cd := &cooldown.Config{
Default: s.cfg.Cooldown.Default, Default: s.cfg.Cooldown.Default,
Ecosystems: s.cfg.Cooldown.Ecosystems, Ecosystems: s.cfg.Cooldown.Ecosystems,
Packages: s.cfg.Cooldown.Packages, Packages: s.cfg.Cooldown.NormalizedPackages(),
} }
proxy := handler.NewProxy(s.db, s.storage, fetcher, resolver, s.logger) proxy := handler.NewProxy(s.db, s.storage, fetcher, resolver, s.logger)
proxy.HTTPClient.Timeout = s.cfg.ParseHTTPTimeout()
proxy.AuthForURL = s.authForURL
proxy.Cooldown = cd proxy.Cooldown = cd
proxy.CacheMetadata = s.cfg.CacheMetadata proxy.CacheMetadata = s.cfg.CacheMetadata
proxy.MetadataTTL = s.cfg.ParseMetadataTTL() proxy.MetadataTTL = s.cfg.ParseMetadataTTL()
@ -195,8 +197,13 @@ func (s *Server) Start() error {
}) })
// Mount protocol handlers // Mount protocol handlers
npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL) npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.NPM)
cargoHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL) cargoHandler := handler.NewCargoHandler(
proxy,
s.cfg.BaseURL,
s.cfg.Upstream.Cargo,
s.cfg.Upstream.CargoDownload,
)
gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL) gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL)
goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL) goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL)
hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL) hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL)

View file

@ -67,8 +67,13 @@ func newTestServer(t *testing.T) *testServer {
r := chi.NewRouter() r := chi.NewRouter()
// Mount handlers // Mount handlers
npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL) npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL, cfg.Upstream.NPM)
cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL) cargoHandler := handler.NewCargoHandler(
proxy,
cfg.BaseURL,
cfg.Upstream.Cargo,
cfg.Upstream.CargoDownload,
)
gemHandler := handler.NewGemHandler(proxy, cfg.BaseURL) gemHandler := handler.NewGemHandler(proxy, cfg.BaseURL)
goHandler := handler.NewGoHandler(proxy, cfg.BaseURL) goHandler := handler.NewGoHandler(proxy, cfg.BaseURL)
pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL) pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL)

View file

@ -34,11 +34,31 @@ func NewFilesystem(root string) (*Filesystem, error) {
} }
func (fs *Filesystem) fullPath(path string) (string, error) { func (fs *Filesystem) fullPath(path string) (string, error) {
full := filepath.Clean(filepath.Join(fs.root, filepath.FromSlash(path))) localPath, err := cleanStoragePath(path)
if full != fs.root && !strings.HasPrefix(full, fs.root+string(filepath.Separator)) { if err != nil {
return "", fmt.Errorf("%w: path escapes storage root", ErrNotFound) return "", err
} }
return full, nil return filepath.Join(fs.root, localPath), nil
}
func (fs *Filesystem) prefixPath(prefix string) (string, error) {
if prefix == "" {
return fs.root, nil
}
return fs.fullPath(prefix)
}
func cleanStoragePath(path string) (string, error) {
if path == "." || strings.Contains(path, `\`) || !filepath.IsLocal(path) {
return "", fmt.Errorf("%w: invalid storage path", ErrNotFound)
}
localPath, err := filepath.Localize(path)
if err != nil || localPath == "." || !filepath.IsLocal(localPath) {
return "", fmt.Errorf("%w: invalid storage path", ErrNotFound)
}
return localPath, nil
} }
func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) { func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
@ -190,7 +210,7 @@ func (fs *Filesystem) UsedSpace(ctx context.Context) (int64, error) {
// ListPrefix returns object metadata for paths under a prefix. // ListPrefix returns object metadata for paths under a prefix.
func (fs *Filesystem) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) { func (fs *Filesystem) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) {
searchRoot, err := fs.fullPath(prefix) searchRoot, err := fs.prefixPath(prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -243,19 +243,70 @@ func TestFilesystemLargeFile(t *testing.T) {
assertLargeFileRoundTrip(t, createTestFilesystem(t)) assertLargeFileRoundTrip(t, createTestFilesystem(t))
} }
func TestFilesystemRejectsTraversal(t *testing.T) { func TestFilesystemRejectsInvalidPaths(t *testing.T) {
tmp := t.TempDir() tmp := t.TempDir()
fs, err := NewFilesystem(tmp) fs, err := NewFilesystem(tmp)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, p := range []string{"../etc/passwd", "../../etc/passwd", "a/../../etc/passwd"} { for _, p := range []string{
if _, err := fs.Open(context.Background(), p); err == nil { "",
t.Errorf("Open(%q) should reject traversal", p) ".",
"../etc/passwd",
"../../etc/passwd",
"a/../../etc/passwd",
"/etc/passwd",
"test//file.txt",
"test/./file.txt",
"test/../file.txt",
`test\..\file.txt`,
} {
name := p
if name == "" {
name = "empty"
} }
if _, _, err := fs.Store(context.Background(), p, strings.NewReader("x")); err == nil {
t.Errorf("Store(%q) should reject traversal", p) t.Run(name, func(t *testing.T) {
ctx := context.Background()
if _, err := fs.FullPath(p); !errors.Is(err, ErrNotFound) {
t.Errorf("FullPath(%q) = %v, want ErrNotFound", p, err)
} }
if _, err := fs.Open(ctx, p); err == nil {
t.Errorf("Open(%q) should reject invalid path", p)
}
if _, _, err := fs.Store(ctx, p, strings.NewReader("x")); err == nil {
t.Errorf("Store(%q) should reject invalid path", p)
}
if _, err := fs.Exists(ctx, p); err == nil {
t.Errorf("Exists(%q) should reject invalid path", p)
}
if err := fs.Delete(ctx, p); err == nil {
t.Errorf("Delete(%q) should reject invalid path", p)
}
if _, err := fs.Size(ctx, p); err == nil {
t.Errorf("Size(%q) should reject invalid path", p)
}
if _, err := fs.ListPrefix(ctx, p); p != "" && err == nil {
t.Errorf("ListPrefix(%q) should reject invalid path", p)
}
})
}
}
func TestFilesystemListPrefixAllowsEmptyPrefix(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
_, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa"))
_, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc"))
objects, err := fs.ListPrefix(ctx, "")
if err != nil {
t.Fatalf("ListPrefix empty prefix failed: %v", err)
}
if len(objects) != 2 {
t.Fatalf("ListPrefix empty prefix returned %d objects, want 2", len(objects))
} }
} }