Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ upstream:

# npm registry URL
npm: "https://registry.npmjs.org"
# Always request full npm packuments so served metadata carries publish
# times ("time" map) even when cooldown is disabled. Needed by clients that
# gate on publish age, e.g. Yarn's npmMinimalAgeGate. Default: false.
# npm_full_metadata: true

# Cargo sparse index URL
cargo: "https://index.crates.io"
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ Each upstream used by a built-in package route can be set in YAML or JSON under
| `upstream.allow_private_hosts` | `PROXY_UPSTREAM_ALLOW_PRIVATE_HOSTS` | `[]` |
| `upstream.allow_loopback` | `PROXY_UPSTREAM_ALLOW_LOOPBACK` | `false` |
| `upstream.npm` | `PROXY_UPSTREAM_NPM` | `https://registry.npmjs.org` |
| `upstream.npm_full_metadata` | `PROXY_UPSTREAM_NPM_FULL_METADATA` | `false` |
| `upstream.cargo` | `PROXY_UPSTREAM_CARGO` | `https://index.crates.io` |
| `upstream.cargo_download` | `PROXY_UPSTREAM_CARGO_DOWNLOAD` | `https://static.crates.io/crates` |
| `upstream.gem` | `PROXY_UPSTREAM_GEM` | `https://rubygems.org` |
Expand Down
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@ type UpstreamConfig struct {
// Default: https://registry.npmjs.org
NPM string `json:"npm" yaml:"npm"`

// NPMFullMetadata always requests the full packument (application/json)
// from the npm upstream, so served metadata carries the "time" map even
// when cooldown is disabled. Clients that gate on publish age (for
// example Yarn's npmMinimalAgeGate) need this.
// Default: false (the abbreviated format is preferred).
NPMFullMetadata bool `json:"npm_full_metadata" yaml:"npm_full_metadata"`

// Cargo is the upstream cargo index URL.
// Default: https://index.crates.io
Cargo string `json:"cargo" yaml:"cargo"`
Expand Down Expand Up @@ -687,6 +694,7 @@ func (c *Config) LoadFromEnv() {
setEnvStringSlice(&c.Upstream.AllowPrivateHosts, "PROXY_UPSTREAM_ALLOW_PRIVATE_HOSTS")
setEnvBool(&c.Upstream.AllowLoopback, "PROXY_UPSTREAM_ALLOW_LOOPBACK")
setEnvString(&c.Upstream.NPM, "PROXY_UPSTREAM_NPM")
setEnvBool(&c.Upstream.NPMFullMetadata, "PROXY_UPSTREAM_NPM_FULL_METADATA")
setEnvString(&c.Upstream.Cargo, "PROXY_UPSTREAM_CARGO")
setEnvString(&c.Upstream.CargoDownload, "PROXY_UPSTREAM_CARGO_DOWNLOAD")
setEnvString(&c.Upstream.Gem, "PROXY_UPSTREAM_GEM")
Expand Down
7 changes: 5 additions & 2 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@ type Proxy struct {
MetadataMaxSize int64
GradleReadOnly bool
GradleMaxUploadSize int64
DirectServe bool
DirectServeTTL time.Duration
// NPMFullMetadata requests full npm packuments from upstream even when
// cooldown is disabled, so served metadata carries publish times.
NPMFullMetadata bool
DirectServe bool
DirectServeTTL time.Duration
// DirectServeBaseURL, if set, replaces the scheme and host of presigned
// URLs so clients receive a public address even when the proxy reaches
// storage at an internal one.
Expand Down
4 changes: 3 additions & 1 deletion internal/handler/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ func (h *NPMHandler) handlePackageMetadata(w http.ResponseWriter, r *http.Reques
// Artifactory, which returns 406) can still respond with full metadata.
// When cooldown is enabled we must use full metadata exclusively because the
// abbreviated format omits the "time" map required for version age filtering.
// Operators can also force full metadata so clients that gate on publish
// age (for example Yarn's npmMinimalAgeGate) keep working through the proxy.
accept := npmAcceptDefault
if h.proxy.Cooldown != nil && h.proxy.Cooldown.Enabled() {
if h.proxy.NPMFullMetadata || (h.proxy.Cooldown != nil && h.proxy.Cooldown.Enabled()) {
accept = contentTypeJSON
}

Expand Down
19 changes: 19 additions & 0 deletions internal/handler/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,25 @@ func TestNPMHandlerUsesAbbreviatedMetadata(t *testing.T) {
t.Errorf("Accept = %q, want %q (cooldown requires full metadata)", gotAccept, contentTypeJSON)
}
})

t.Run("full metadata option uses full metadata without cooldown", func(t *testing.T) {
proxy := testProxy()
proxy.NPMFullMetadata = true

h := &NPMHandler{
proxy: proxy,
upstreamURL: upstream.URL,
proxyURL: "http://proxy.local",
}

req := httptest.NewRequest(http.MethodGet, "/testpkg", nil)
w := httptest.NewRecorder()
h.handlePackageMetadata(w, req)

if gotAccept != contentTypeJSON {
t.Errorf("Accept = %q, want %q (npm_full_metadata requires full metadata)", gotAccept, contentTypeJSON)
}
})
}

func TestNPMHandlerMetadataNotFound(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func (s *Server) serve(listener net.Listener) error {
proxy.MetadataTTL = s.cfg.ParseMetadataTTL()
proxy.MetadataMaxSize = s.cfg.ParseMetadataMaxSize()
proxy.GradleReadOnly = s.cfg.Gradle.BuildCache.ReadOnly
proxy.NPMFullMetadata = s.cfg.Upstream.NPMFullMetadata
proxy.GradleMaxUploadSize = s.cfg.ParseGradleBuildCacheMaxUploadSize()
proxy.DirectServe = s.cfg.Storage.DirectServe
proxy.DirectServeTTL = s.cfg.ParseDirectServeTTL()
Expand Down