Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289//go:build linux
package microvm
import ( "context" "errors" "io" "log/slog" "net" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time"
"tangled.org/core/spindle/netguard")
func TestCacheProxyFallsBackOnNotFound(t *testing.T) { first := httptest.NewServer(http.NotFoundHandler()) defer first.Close() second := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.URL.Path != "/00000000000000000000000000000000.narinfo" { t.Fatalf("path: got %q, want /00000000000000000000000000000000.narinfo", req.URL.Path) } _, _ = io.WriteString(w, "ok") })) defer second.Close()
upstreams, err := parseSubstituterUpstreams([]string{first.URL, second.URL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://guest/00000000000000000000000000000000.narinfo", nil) rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String()) } if got := rec.Body.String(); got != "ok" { t.Fatalf("body: got %q, want ok", got) }}
func TestCacheProxyServesNixCacheInfoItself(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { t.Errorf("upstream should not be hit, got request for %q", req.URL.Path) })) defer upstream.Close()
upstreams, err := parseSubstituterUpstreams([]string{upstream.URL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://guest/nix-cache-info", nil) rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String()) } if got := rec.Body.String(); got != nixCacheInfo { t.Fatalf("body: got %q, want %q", got, nixCacheInfo) }}
func TestCacheProxyErrorStatusDoesNotWinRace(t *testing.T) { erroring := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.Error(w, "misdirected", http.StatusMisdirectedRequest) })) defer erroring.Close() healthy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { time.Sleep(50 * time.Millisecond) // lose the race to the erroring upstream _, _ = io.WriteString(w, "ok") })) defer healthy.Close()
upstreams, err := parseSubstituterUpstreams([]string{erroring.URL, healthy.URL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://guest/00000000000000000000000000000000.narinfo", nil) rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String()) } if got := rec.Body.String(); got != "ok" { t.Fatalf("body: got %q, want ok", got) }}
func TestCacheProxyJoinsSubpathQueryAndAuth(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.URL.Path != "/sub/cache/00000000000000000000000000000000.narinfo" { t.Errorf("path: got %q, want /sub/cache/00000000000000000000000000000000.narinfo", req.URL.Path) } if got := req.URL.Query().Get("token"); got != "s3cret" { t.Errorf("token: got %q, want s3cret", got) } if user, pass, ok := req.BasicAuth(); !ok || user != "dawn" || pass != "woof" { t.Errorf("basic auth: got %q/%q/%v, want dawn/woof/true", user, pass, ok) } _, _ = io.WriteString(w, "ok") })) defer upstream.Close()
upstreamURL := "http://dawn:woof@" + strings.TrimPrefix(upstream.URL, "http://") + "/sub/cache/?token=s3cret" upstreams, err := parseSubstituterUpstreams([]string{upstreamURL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://guest/00000000000000000000000000000000.narinfo", nil) rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String()) } if got := rec.Body.String(); got != "ok" { t.Fatalf("body: got %q, want ok", got) }}
func TestCacheProxyGuardAllowsPublicIPv4(t *testing.T) { if err := netguard.RefuseSpecialPurposeAddrs("tcp", "104.26.13.82:443", nil); err != nil { t.Fatalf("public IPv4 address was blocked: %v", err) }}
func TestCacheProxyGuardedUpstreamCannotReachBlockedRanges(t *testing.T) { // httptest listens on 127.0.0.1, which is in the blocked ranges; reaching // it would mean a workflow-defined cache can hit the host's loopback upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { t.Errorf("guarded upstream on loopback should not be reachable, got request for %q", req.URL.Path) })) defer upstream.Close()
upstreams, err := parseSubstituterUpstreams([]string{upstream.URL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://guest/00000000000000000000000000000000.narinfo", nil) rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(nil, upstreams), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusBadGateway { t.Fatalf("status: got %d, want 502; body=%q", rec.Code, rec.Body.String()) }}
func TestCacheProxyRewritesHostHeader(t *testing.T) { var upstreamHost string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Host != upstreamHost { t.Errorf("host: got %q, want %q", req.Host, upstreamHost) } _, _ = io.WriteString(w, "ok") })) defer upstream.Close() upstreamHost = strings.TrimPrefix(upstream.URL, "http://")
upstreams, err := parseSubstituterUpstreams([]string{upstream.URL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:10500/00000000000000000000000000000000.narinfo", nil) req.Host = "127.0.0.1:10500" rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String()) }}
const ( readProxyNarHash = "0af4767hy0qvmdblnqmwzrvq84paml3bh9v3y1k1wcfymqnyr572" readProxyStoreHash = "a4pwv4icv48yjn31wp8drzxddzawvs14")
func TestCacheProxyForwardsNarHashQuery(t *testing.T) { var gotPath, gotQuery string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { gotPath, gotQuery = req.URL.Path, req.URL.RawQuery _, _ = io.WriteString(w, "ok") })) defer upstream.Close()
upstreams, err := parseSubstituterUpstreams([]string{upstream.URL}) if err != nil { t.Fatal(err) }
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:10500/nar/"+readProxyNarHash+".nar?hash="+readProxyStoreHash, nil) req.Host = "127.0.0.1:10500" rec := httptest.NewRecorder() cacheProxyHandler(mergeSubstituterUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String()) } if want := "/nar/" + readProxyNarHash + ".nar"; gotPath != want { t.Fatalf("upstream path: got %q, want %q", gotPath, want) } if want := "hash=" + readProxyStoreHash; gotQuery != want { t.Fatalf("upstream query: got %q, want %q", gotQuery, want) }}
func TestCacheProxyRejectsUnsafeRequestShapes(t *testing.T) { var hits int upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { hits++ })) defer upstream.Close() parsed, err := parseSubstituterUpstreams([]string{upstream.URL}) if err != nil { t.Fatal(err) } handler := cacheProxyHandler(mergeSubstituterUpstreams(parsed, nil), slog.Default()) tests := []struct { method string target string status int }{ {http.MethodPost, "http://guest/00000000000000000000000000000000.narinfo", http.StatusMethodNotAllowed}, {http.MethodGet, "http://guest/arbitrary", http.StatusNotFound}, {http.MethodGet, "http://guest/00000000000000000000000000000000.narinfo?delete=1", http.StatusNotFound}, {http.MethodGet, "http://guest/nar/not-a-hash.nar", http.StatusNotFound}, {http.MethodGet, "http://guest/nar/" + readProxyNarHash + ".nar?delete=1", http.StatusNotFound}, {http.MethodGet, "http://guest/nar/" + readProxyNarHash + ".nar?hash=not-a-store-hash", http.StatusNotFound}, {http.MethodGet, "http://guest/nar/" + readProxyNarHash + ".nar?hash=" + readProxyStoreHash + "&hash=" + readProxyStoreHash, http.StatusNotFound}, } for _, test := range tests { rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(test.method, test.target, nil)) if rec.Code != test.status { t.Errorf("%s %s: got %d, want %d", test.method, test.target, rec.Code, test.status) } } if hits != 0 { t.Fatalf("rejected requests reached upstream %d times", hits) }}
func TestCacheURLForLogRedactsCredentials(t *testing.T) { got := cacheURLForLog("https://user:password@cache.example/base?token=secret#fragment") if got != "https://cache.example/base" { t.Fatalf("redacted URL = %q", got) }}
func TestRedactURLsInTextStripsCredentialsAndSecrets(t *testing.T) { got := redactURLsInText(`Put "https://user:password@cache.example/nar/abc.nar.xz?token=secret#frag": write: broken pipe`) want := `Put "https://REDACTED@cache.example/nar/abc.nar.xz": write: broken pipe` if got != want { t.Fatalf("redacted text = %q, want %q", got, want) }}
func TestRedactingErrorKeepsErrorChain(t *testing.T) { wrapped := redactingError{err: &url.Error{ Op: "Put", URL: "https://user:password@cache.example/nar/abc.nar.xz?token=secret", Err: context.DeadlineExceeded, }}
message := wrapped.Error() if strings.Contains(message, "password") || strings.Contains(message, "token=secret") { t.Fatalf("redacted error still leaks credentials: %q", message) } if !errors.Is(wrapped, context.DeadlineExceeded) { t.Error("errors.Is(context.DeadlineExceeded) no longer matches") } var netErr net.Error if !errors.As(wrapped, &netErr) { t.Error("errors.As(net.Error) no longer matches") }}