mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
An uncached tarball is checked against the integrity its packument declares. When the packument couldn't be fetched or was refused, for example because it is larger than metadata_max_size or the registry answered 503, the download went ahead with no integrity at all, and the unverified tarball was cached and served to every later client. Without the packument the download now fails like the Composer one does: 502, or 404 when the registry says the package doesn't exist. Nothing is fetched or cached. Tarballs already in the cache are still served, since they were verified when they were stored, and a packument that lists the version without a usable checksum still falls back to an unverified fetch as before.
301 lines
12 KiB
Go
301 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
// npmUpstreamRecorder serves canned packuments and records which paths were
|
|
// requested.
|
|
type npmUpstreamRecorder struct {
|
|
*httptest.Server
|
|
mu sync.Mutex
|
|
requests []string
|
|
}
|
|
|
|
func newNPMUpstream(t *testing.T, packuments map[string]func(baseURL string) string, status int) *npmUpstreamRecorder {
|
|
t.Helper()
|
|
u := &npmUpstreamRecorder{}
|
|
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
u.mu.Lock()
|
|
u.requests = append(u.requests, r.URL.Path)
|
|
u.mu.Unlock()
|
|
if status != http.StatusOK {
|
|
w.WriteHeader(status)
|
|
return
|
|
}
|
|
packument, ok := packuments[r.URL.Path]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, packument(u.URL))
|
|
}))
|
|
t.Cleanup(u.Close)
|
|
return u
|
|
}
|
|
|
|
func (u *npmUpstreamRecorder) requested() []string {
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
return append([]string(nil), u.requests...)
|
|
}
|
|
|
|
func routedTestPackument(version, tarballBase string) func(string) string {
|
|
return func(baseURL string) string {
|
|
return `{"name":"@example/widgets","dist-tags":{"latest":"` + version + `"},"versions":{"` + version +
|
|
`":{"version":"` + version + `","dist":{"tarball":"` + baseURL + tarballBase + `/@example/widgets/-/widgets-` + version + `.tgz"}}}}`
|
|
}
|
|
}
|
|
|
|
const routedNPMPath = "/registry/@example/widgets"
|
|
|
|
func newRoutedNPMHandler(proxy *Proxy, public, private *npmUpstreamRecorder) *NPMHandler {
|
|
return NewNPMHandler(proxy, "http://proxy.test", public.URL).
|
|
WithPackageRoutes(map[string]string{"@example/*": private.URL + "/registry"})
|
|
}
|
|
|
|
func TestNPMRoutedMetadataComesFromRouteOnly(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/@example/widgets": routedTestPackument("9.9.9", "")}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{routedNPMPath: routedTestPackument("1.0.0", "/registry")}, http.StatusOK)
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
h := newRoutedNPMHandler(proxy, public, private)
|
|
|
|
w := serveNPM(t, h, http.MethodGet, "/@example%2fwidgets", nil, nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
if got := public.requested(); len(got) != 0 {
|
|
t.Errorf("default registry was queried for a routed package: %v", got)
|
|
}
|
|
if strings.Contains(w.Body.String(), "9.9.9") {
|
|
t.Errorf("routed package served the default registry's metadata: %s", w.Body.String())
|
|
}
|
|
wantTarball := "http://proxy.test/npm/@example%2Fwidgets/-/widgets-1.0.0.tgz"
|
|
if !strings.Contains(w.Body.String(), wantTarball) {
|
|
t.Errorf("tarball not rewritten to %q: %s", wantTarball, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestNPMRoutedMetadataDoesNotFallBack(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
privateStatus int
|
|
wantStatus int
|
|
}{
|
|
{"package missing on route", http.StatusOK, http.StatusNotFound},
|
|
{"route rejects credentials", http.StatusUnauthorized, http.StatusBadGateway},
|
|
{"route unavailable", http.StatusInternalServerError, http.StatusBadGateway},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/@example/widgets": routedTestPackument("9.9.9", "")}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{}, tt.privateStatus)
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
h := newRoutedNPMHandler(proxy, public, private)
|
|
|
|
w := serveNPM(t, h, http.MethodGet, "/@example%2fwidgets", nil, nil)
|
|
|
|
if w.Code != tt.wantStatus {
|
|
t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
|
|
}
|
|
if got := public.requested(); len(got) != 0 {
|
|
t.Errorf("default registry was queried for a routed package: %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNPMRoutedMetadataIgnoresDefaultRegistryCache(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/@example/widgets": routedTestPackument("9.9.9", "")}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{routedNPMPath: routedTestPackument("1.0.0", "/registry")}, http.StatusOK)
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
proxy.CacheMetadata = true
|
|
proxy.MetadataTTL = time.Hour
|
|
|
|
if w := serveNPM(t, NewNPMHandler(proxy, "http://proxy.test", public.URL), http.MethodGet, "/@example%2fwidgets", nil, nil); w.Code != http.StatusOK {
|
|
t.Fatalf("priming the default registry cache: status %d", w.Code)
|
|
}
|
|
|
|
w := serveNPM(t, newRoutedNPMHandler(proxy, public, private), http.MethodGet, "/@example%2fwidgets", nil, nil)
|
|
|
|
if strings.Contains(w.Body.String(), "9.9.9") {
|
|
t.Errorf("routed package served metadata cached from the default registry: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestNPMUnroutedMetadataUsesDefaultRegistry(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/lodash": routedTestPackument("4.17.21", "")}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{}, http.StatusOK)
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
h := newRoutedNPMHandler(proxy, public, private)
|
|
|
|
w := serveNPM(t, h, http.MethodGet, "/lodash", nil, nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
if got := private.requested(); len(got) != 0 {
|
|
t.Errorf("route was queried for an unrouted package: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestNPMRoutedDownloadResolvesFromRoute(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/@example/widgets": routedTestPackument("1.0.0", "")}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{routedNPMPath: routedTestPackument("1.0.0", "/registry")}, http.StatusOK)
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
h := newRoutedNPMHandler(proxy, public, private)
|
|
|
|
w := serveNPM(t, h, http.MethodGet, "/@example/widgets/-/widgets-1.0.0.tgz", nil, nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
if want := private.URL + "/registry/@example/widgets/-/widgets-1.0.0.tgz"; artifactFetcher.fetchedURL != want {
|
|
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
|
|
}
|
|
if got := public.requested(); len(got) != 0 {
|
|
t.Errorf("default registry was queried for a routed download: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestNPMRoutedDownloadWithoutMetadataStaysOnRoute(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{}, http.StatusInternalServerError)
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
h := newRoutedNPMHandler(proxy, public, private)
|
|
|
|
w := serveNPM(t, h, http.MethodGet, "/@example/widgets/-/widgets-1.0.0.tgz", nil, nil)
|
|
|
|
// Without the route's metadata the tarball can't be verified, so it is
|
|
// not fetched at all, and in particular not from the default registry.
|
|
if w.Code != http.StatusBadGateway {
|
|
t.Errorf("status = %d, want 502", w.Code)
|
|
}
|
|
if artifactFetcher.fetchedURL != "" {
|
|
t.Errorf("fetched URL = %q, want none", artifactFetcher.fetchedURL)
|
|
}
|
|
}
|
|
|
|
func TestNPMRoutedDownloadIgnoresDefaultRegistryArtifact(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/@example/widgets": routedTestPackument("1.0.0", "")}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{routedNPMPath: routedTestPackument("1.0.0", "/registry")}, http.StatusOK)
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("public package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
|
|
if w := serveNPM(t, NewNPMHandler(proxy, "http://proxy.test", public.URL), http.MethodGet, "/@example/widgets/-/widgets-1.0.0.tgz", nil, nil); w.Code != http.StatusOK {
|
|
t.Fatalf("priming the artifact cache: status %d; body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("private package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
w := serveNPM(t, newRoutedNPMHandler(proxy, public, private), http.MethodGet, "/@example/widgets/-/widgets-1.0.0.tgz", nil, nil)
|
|
|
|
if w.Body.String() != "private package" {
|
|
t.Errorf("routed download served %q, want the routed registry's package", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// timedTestPackument is routedTestPackument with a publish time for the
|
|
// version, published age ago.
|
|
func timedTestPackument(version, tarballBase string, age time.Duration) func(string) string {
|
|
published := time.Now().Add(-age).UTC().Format(time.RFC3339)
|
|
return func(baseURL string) string {
|
|
return `{"name":"@example/widgets","dist-tags":{"latest":"` + version + `"},"versions":{"` + version +
|
|
`":{"version":"` + version + `","dist":{"tarball":"` + baseURL + tarballBase + `/@example/widgets/-/widgets-` + version + `.tgz"}}},` +
|
|
`"time":{"` + version + `":"` + published + `"}}`
|
|
}
|
|
}
|
|
|
|
// TestNPMRoutedCooldownUsesRoutePublishTimes is a regression test: the
|
|
// download cooldown check stored publish times under the package's plain
|
|
// version PURL, so a time read from one registry was trusted for the same
|
|
// version from another. A route's version published an hour ago must stay in
|
|
// a seven-day cooldown even when the default registry's version of the same
|
|
// name is a month old, whichever was downloaded first, and changing a route's
|
|
// upstream must not carry over the old one's times.
|
|
func TestNPMRoutedCooldownUsesRoutePublishTimes(t *testing.T) {
|
|
const tarball = "/@example/widgets/-/widgets-1.0.0.tgz"
|
|
month, hour := 30*24*time.Hour, time.Hour
|
|
|
|
type step struct {
|
|
handler string // "public", "route" or "other route"
|
|
wantStatus int
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
steps []step
|
|
}{
|
|
{"route added after a public download", []step{
|
|
{"public", http.StatusOK},
|
|
{"route", http.StatusNotFound},
|
|
}},
|
|
{"public download after a routed one", []step{
|
|
{"route", http.StatusNotFound},
|
|
{"public", http.StatusOK},
|
|
}},
|
|
{"route moved to another upstream", []step{
|
|
{"other route", http.StatusOK},
|
|
{"route", http.StatusNotFound},
|
|
}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
public := newNPMUpstream(t, map[string]func(string) string{"/@example/widgets": timedTestPackument("1.0.0", "", month)}, http.StatusOK)
|
|
private := newNPMUpstream(t, map[string]func(string) string{routedNPMPath: timedTestPackument("1.0.0", "/registry", hour)}, http.StatusOK)
|
|
old := newNPMUpstream(t, map[string]func(string) string{routedNPMPath: timedTestPackument("1.0.0", "/registry", month)}, http.StatusOK)
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
proxy.HTTPClient = http.DefaultClient
|
|
proxy.CacheMetadata = true
|
|
proxy.MetadataTTL = time.Hour
|
|
proxy.Cooldown = &cooldown.Config{Default: "7d"}
|
|
handlers := map[string]*NPMHandler{
|
|
"public": NewNPMHandler(proxy, "http://proxy.test", public.URL),
|
|
"route": newRoutedNPMHandler(proxy, public, private),
|
|
"other route": newRoutedNPMHandler(proxy, public, old),
|
|
}
|
|
|
|
for _, s := range tt.steps {
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
w := serveNPM(t, handlers[s.handler], http.MethodGet, tarball, nil, nil)
|
|
if w.Code != s.wantStatus {
|
|
t.Errorf("%s download: status %d, want %d; body: %s", s.handler, w.Code, s.wantStatus, w.Body.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|