This repository has no description
1package microvm
2
3import (
4 "io"
5 "log/slog"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10 "time"
11
12 "tangled.org/core/spindle/netguard"
13)
14
15func TestCacheProxyFallsBackOnNotFound(t *testing.T) {
16 first := httptest.NewServer(http.NotFoundHandler())
17 defer first.Close()
18 second := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
19 if req.URL.Path != "/abc.narinfo" {
20 t.Fatalf("path: got %q, want /abc.narinfo", req.URL.Path)
21 }
22 _, _ = io.WriteString(w, "ok")
23 }))
24 defer second.Close()
25
26 upstreams, err := parseCacheUpstreams([]string{first.URL, second.URL})
27 if err != nil {
28 t.Fatal(err)
29 }
30
31 req := httptest.NewRequest(http.MethodGet, "http://guest/abc.narinfo", nil)
32 rec := httptest.NewRecorder()
33 cacheProxyHandler(mergeCacheUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
34
35 if rec.Code != http.StatusOK {
36 t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String())
37 }
38 if got := rec.Body.String(); got != "ok" {
39 t.Fatalf("body: got %q, want ok", got)
40 }
41}
42
43func TestCacheProxyServesNixCacheInfoItself(t *testing.T) {
44 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
45 t.Errorf("upstream should not be hit, got request for %q", req.URL.Path)
46 }))
47 defer upstream.Close()
48
49 upstreams, err := parseCacheUpstreams([]string{upstream.URL})
50 if err != nil {
51 t.Fatal(err)
52 }
53
54 req := httptest.NewRequest(http.MethodGet, "http://guest/nix-cache-info", nil)
55 rec := httptest.NewRecorder()
56 cacheProxyHandler(mergeCacheUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
57
58 if rec.Code != http.StatusOK {
59 t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String())
60 }
61 if got := rec.Body.String(); got != nixCacheInfo {
62 t.Fatalf("body: got %q, want %q", got, nixCacheInfo)
63 }
64}
65
66func TestCacheProxyErrorStatusDoesNotWinRace(t *testing.T) {
67 erroring := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
68 http.Error(w, "misdirected", http.StatusMisdirectedRequest)
69 }))
70 defer erroring.Close()
71 healthy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
72 time.Sleep(50 * time.Millisecond) // lose the race to the erroring upstream
73 _, _ = io.WriteString(w, "ok")
74 }))
75 defer healthy.Close()
76
77 upstreams, err := parseCacheUpstreams([]string{erroring.URL, healthy.URL})
78 if err != nil {
79 t.Fatal(err)
80 }
81
82 req := httptest.NewRequest(http.MethodGet, "http://guest/abc.narinfo", nil)
83 rec := httptest.NewRecorder()
84 cacheProxyHandler(mergeCacheUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
85
86 if rec.Code != http.StatusOK {
87 t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String())
88 }
89 if got := rec.Body.String(); got != "ok" {
90 t.Fatalf("body: got %q, want ok", got)
91 }
92}
93
94func TestCacheProxyJoinsSubpathQueryAndAuth(t *testing.T) {
95 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
96 if req.URL.Path != "/sub/cache/abc.narinfo" {
97 t.Errorf("path: got %q, want /sub/cache/abc.narinfo", req.URL.Path)
98 }
99 if got := req.URL.Query().Get("token"); got != "s3cret" {
100 t.Errorf("token: got %q, want s3cret", got)
101 }
102 if user, pass, ok := req.BasicAuth(); !ok || user != "dawn" || pass != "woof" {
103 t.Errorf("basic auth: got %q/%q/%v, want dawn/woof/true", user, pass, ok)
104 }
105 _, _ = io.WriteString(w, "ok")
106 }))
107 defer upstream.Close()
108
109 upstreamURL := "http://dawn:woof@" + strings.TrimPrefix(upstream.URL, "http://") + "/sub/cache/?token=s3cret"
110 upstreams, err := parseCacheUpstreams([]string{upstreamURL})
111 if err != nil {
112 t.Fatal(err)
113 }
114
115 req := httptest.NewRequest(http.MethodGet, "http://guest/abc.narinfo", nil)
116 rec := httptest.NewRecorder()
117 cacheProxyHandler(mergeCacheUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
118
119 if rec.Code != http.StatusOK {
120 t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String())
121 }
122 if got := rec.Body.String(); got != "ok" {
123 t.Fatalf("body: got %q, want ok", got)
124 }
125}
126
127func TestCacheProxyGuardAllowsPublicIPv4(t *testing.T) {
128 if err := netguard.RefuseSpecialPurposeAddrs("tcp", "104.26.13.82:443", nil); err != nil {
129 t.Fatalf("public IPv4 address was blocked: %v", err)
130 }
131}
132
133func TestCacheProxyGuardedUpstreamCannotReachBlockedRanges(t *testing.T) {
134 // httptest listens on 127.0.0.1, which is in the blocked ranges; reaching
135 // it would mean a workflow-defined cache can hit the host's loopback
136 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
137 t.Errorf("guarded upstream on loopback should not be reachable, got request for %q", req.URL.Path)
138 }))
139 defer upstream.Close()
140
141 upstreams, err := parseCacheUpstreams([]string{upstream.URL})
142 if err != nil {
143 t.Fatal(err)
144 }
145
146 req := httptest.NewRequest(http.MethodGet, "http://guest/abc.narinfo", nil)
147 rec := httptest.NewRecorder()
148 cacheProxyHandler(mergeCacheUpstreams(nil, upstreams), slog.Default()).ServeHTTP(rec, req)
149
150 if rec.Code != http.StatusBadGateway {
151 t.Fatalf("status: got %d, want 502; body=%q", rec.Code, rec.Body.String())
152 }
153}
154
155func TestCacheProxyRewritesHostHeader(t *testing.T) {
156 var upstreamHost string
157 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
158 if req.Host != upstreamHost {
159 t.Errorf("host: got %q, want %q", req.Host, upstreamHost)
160 }
161 _, _ = io.WriteString(w, "ok")
162 }))
163 defer upstream.Close()
164 upstreamHost = strings.TrimPrefix(upstream.URL, "http://")
165
166 upstreams, err := parseCacheUpstreams([]string{upstream.URL})
167 if err != nil {
168 t.Fatal(err)
169 }
170
171 req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:10500/abc.narinfo", nil)
172 req.Host = "127.0.0.1:10500"
173 rec := httptest.NewRecorder()
174 cacheProxyHandler(mergeCacheUpstreams(upstreams, nil), slog.Default()).ServeHTTP(rec, req)
175
176 if rec.Code != http.StatusOK {
177 t.Fatalf("status: got %d, want 200; body=%q", rec.Code, rec.Body.String())
178 }
179}