blossom_serve_test.mx raw
1 // Blob serving: the stored-blob GET/HEAD paths, the missing-blob 404s, and the
2 // CORS-proxy fallback against a scripted loopback origin (including the local
3 // cache it writes on success). The package's other test file stops at the pure
4 // request-routing surface.
5
6 package blossom
7
8 import (
9 "errors"
10 "os"
11 "syscall"
12 "testing"
13 )
14
15 const blHash = "abababababababababababababababababababababababababababababababab"
16
17 func blListen() (fd int32, port int32, err error) {
18 fd, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
19 if err != nil {
20 return 0, 0, err
21 }
22 syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
23 tv := syscall.Timeval{Sec: 5}
24 syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv)
25 sa := &syscall.SockaddrInet4{Port: 0, Addr: [4]byte{127, 0, 0, 1}}
26 if err = syscall.Bind(fd, sa); err != nil {
27 syscall.Close(fd)
28 return 0, 0, err
29 }
30 if err = syscall.Listen(fd, 8); err != nil {
31 syscall.Close(fd)
32 return 0, 0, err
33 }
34 got, gerr := syscall.Getsockname(fd)
35 if gerr != nil {
36 syscall.Close(fd)
37 return 0, 0, gerr
38 }
39 sa4, ok := got.(*syscall.SockaddrInet4)
40 if !ok {
41 syscall.Close(fd)
42 return 0, 0, errors.New("blossom: loopback listener is not inet4")
43 }
44 return fd, sa4.Port, nil
45 }
46
47 func blPortStr(port int32) (s string) {
48 if port == 0 {
49 return "0"
50 }
51 buf := []byte{:6}
52 n := int32(6)
53 for port > 0 {
54 n--
55 buf[n] = byte('0' + port%10)
56 port = port / 10
57 }
58 return string(buf[n:])
59 }
60
61 func blStartListener(t *testing.T) (fd int32, base string) {
62 t.Helper()
63 fd, port, err := blListen()
64 if err != nil {
65 t.Fatalf("listen: %v", err)
66 return 0, ""
67 }
68 return fd, "http://127.0.0.1:" | blPortStr(port)
69 }
70
71 // blPack length-prefixes each response so the script crosses the spawn
72 // boundary as one slice: presized, cursor-written.
73 func blPack(parts [][]byte) (out []byte) {
74 total := 0
75 for i := 0; i < len(parts); i++ {
76 total += 4 + len(parts[i])
77 }
78 out = []byte{:total}
79 pos := 0
80 for i := 0; i < len(parts); i++ {
81 n := int32(len(parts[i]))
82 out[pos] = byte(n >> 24)
83 out[pos+1] = byte(n >> 16)
84 out[pos+2] = byte(n >> 8)
85 out[pos+3] = byte(n)
86 pos += 4
87 for j := 0; j < len(parts[i]); j++ {
88 out[pos+j] = parts[i][j]
89 }
90 pos += len(parts[i])
91 }
92 return
93 }
94
95 func blServe(fd int32, packed []byte) {
96 pos := 0
97 for pos+4 <= len(packed) {
98 n := int32(packed[pos])<<24 | int32(packed[pos+1])<<16 | int32(packed[pos+2])<<8 | int32(packed[pos+3])
99 pos += 4
100 if pos+n > len(packed) {
101 break
102 }
103 resp := packed[pos : pos+n]
104 pos += n
105 nfd, _, err := syscall.Accept(fd)
106 if err != nil {
107 break
108 }
109 buf := []byte{:2048}
110 for {
111 got, rerr := syscall.Read(nfd, buf)
112 if got <= 0 || rerr != nil {
113 break
114 }
115 if blHas(buf[:got], []byte("\r\n\r\n")) {
116 break
117 }
118 }
119 syscall.Write(nfd, resp)
120 syscall.Close(nfd)
121 }
122 syscall.Close(fd)
123 }
124
125 func TestMkdirFailureIsReported(t *testing.T) {
126 _, dir := blTmp(t)
127 // A path through an existing regular file cannot become a directory.
128 file := dir | "/notadir"
129 if err := os.WriteFile(file, []byte("x"), 0644); err != nil {
130 t.Fatal(err)
131 }
132 if _, err := New(file | "/sub"); err == nil {
133 t.Fatal("New must report a directory it cannot create")
134 }
135 }
136
137 func TestServeStoredBlobGetAndHead(t *testing.T) {
138 s, dir := blTmp(t)
139 data := []byte("png bytes here")
140 if err := os.WriteFile(dir|"/"|blHash, data, 0644); err != nil {
141 t.Fatal(err)
142 }
143
144 status, headers, body := s.HandleRaw("GET", "/"|blHash|".png", nil, nil)
145 if status != 200 {
146 t.Fatalf("GET status = %d", status)
147 }
148 if string(body) != "png bytes here" {
149 t.Fatalf("GET body = %s", body)
150 }
151 if headers["Content-Type"] != "image/png" {
152 t.Fatalf("GET content-type = %s", headers["Content-Type"])
153 }
154 if headers["Access-Control-Allow-Origin"] != "*" {
155 t.Fatal("GET must carry the CORS headers")
156 }
157
158 // The extension is absent: the blob is still served, as octet-stream.
159 status2, headers2, body2 := s.HandleRaw("GET", "/"|blHash, nil, nil)
160 if status2 != 200 || string(body2) != "png bytes here" {
161 t.Fatalf("extensionless GET status = %d body = %s", status2, body2)
162 }
163 if headers2["Content-Type"] != "application/octet-stream" {
164 t.Fatalf("extensionless content-type = %s", headers2["Content-Type"])
165 }
166
167 // HEAD reports the size and type and carries no body.
168 status3, headers3, body3 := s.HandleRaw("HEAD", "/"|blHash|".jpg", nil, nil)
169 if status3 != 200 {
170 t.Fatalf("HEAD status = %d", status3)
171 }
172 if body3 != nil {
173 t.Fatalf("HEAD body = %s", body3)
174 }
175 if headers3["Content-Length"] != "14" {
176 t.Fatalf("HEAD content-length = %s", headers3["Content-Length"])
177 }
178 if headers3["Content-Type"] != "image/jpeg" {
179 t.Fatalf("HEAD content-type = %s", headers3["Content-Type"])
180 }
181 }
182
183 func TestServeMissingBlobIs404(t *testing.T) {
184 s, _ := blTmp(t)
185 cases := []string{
186 "/" | blHash | ".png", // valid hash, nothing stored, no upstream
187 "/" | blHash, // no extension
188 "/short", // wrong length
189 }
190 for i := 0; i < len(cases); i++ {
191 status, _, body := s.HandleRaw("GET", cases[i], nil, nil)
192 if status != 404 || body != nil {
193 t.Fatalf("GET %s = %d", cases[i], status)
194 }
195 }
196 // HEAD of a missing blob is a 404 too, even with an upstream configured:
197 // Fetch always GETs, so HEAD is never proxied.
198 if status, _, _ := s.HandleRawWithUpstream("HEAD", "/"|blHash, nil, nil, "http://127.0.0.1:1"); status != 404 {
199 t.Fatalf("missing HEAD = %d", status)
200 }
201 // A dot at index 0 leaves the whole path as the hash candidate.
202 if status, _, _ := s.HandleRaw("GET", "/.png", nil, nil); status != 404 {
203 t.Fatal("a leading dot must not be read as an extension")
204 }
205 // An unparseable upstream counts as self and is not dialed.
206 if status, _, _ := s.HandleRawWithUpstream("GET", "/"|blHash, nil, nil, "://bad"); status != 404 {
207 t.Fatal("an unparseable upstream must 404")
208 }
209 // A refused upstream connection is a 404, not an error.
210 if status, _, _ := s.HandleRawWithUpstream("GET", "/"|blHash, nil, nil, "http://127.0.0.1:1"); status != 404 {
211 t.Fatal("a refused upstream must 404")
212 }
213 }
214
215 func TestOptionsPreflight(t *testing.T) {
216 s, _ := blTmp(t)
217 status, headers, body := s.HandleRaw("OPTIONS", "/anything", nil, nil)
218 if status != 204 || body != nil {
219 t.Fatalf("OPTIONS = %d", status)
220 }
221 if headers["Access-Control-Allow-Methods"] == "" {
222 t.Fatal("preflight must advertise the methods")
223 }
224 }
225
226 func TestUpstreamProxyServesAndCaches(t *testing.T) {
227 s, dir := blTmp(t)
228 fd, base := blStartListener(t)
229 if fd == 0 {
230 return
231 }
232 resp := []byte("HTTP/1.1 200 OK\r\nContent-Type: image/webp\r\nContent-Length: 11\r\n\r\nremote blob")
233 done := spawn(blServe, fd, blPack([][]byte{resp}))
234
235 // The upstream URL carries a trailing slash, which the target builder
236 // trims before appending /blossom/<hash>.webp.
237 status, headers, body := s.HandleRawWithUpstream("GET", "/"|blHash|".webp", nil, nil, base|"/")
238 if status != 200 {
239 t.Fatalf("proxied GET = %d", status)
240 }
241 if string(body) != "remote blob" {
242 t.Fatalf("proxied body = %s", body)
243 }
244 if headers["Content-Type"] != "image/webp" {
245 t.Fatalf("proxied content-type = %s", headers["Content-Type"])
246 }
247 if headers["Access-Control-Allow-Origin"] != "*" {
248 t.Fatal("the proxied response must carry our CORS headers")
249 }
250 <-done
251
252 // The response was cached: a second request with a dead upstream is served
253 // from disk.
254 status2, _, body2 := s.HandleRawWithUpstream("GET", "/"|blHash|".webp", nil, nil, "http://127.0.0.1:1")
255 if status2 != 200 || string(body2) != "remote blob" {
256 t.Fatalf("cached GET = %d body = %s", status2, body2)
257 }
258 if _, err := os.Stat(dir | "/" | blHash); err != nil {
259 t.Fatalf("the proxied blob must be cached on disk: %v", err)
260 }
261 }
262
263 func TestUpstreamNonSuccessIs404(t *testing.T) {
264 s, _ := blTmp(t)
265 fd, base := blStartListener(t)
266 if fd == 0 {
267 return
268 }
269 resp := []byte("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
270 done := spawn(blServe, fd, blPack([][]byte{resp}))
271 status, _, _ := s.HandleRawWithUpstream("GET", "/"|blHash, nil, nil, base)
272 if status != 404 {
273 t.Fatalf("a 404 upstream must become our 404, got %d", status)
274 }
275 <-done
276 }
277
278 func TestUpstreamContentTypeFallsBackToExtension(t *testing.T) {
279 s, _ := blTmp(t)
280 fd, base := blStartListener(t)
281 if fd == 0 {
282 return
283 }
284 // No content-type upstream: the extension's mime type is used.
285 resp := []byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
286 done := spawn(blServe, fd, blPack([][]byte{resp}))
287 status, headers, body := s.HandleRawWithUpstream("GET", "/"|blHash|".gif", nil, nil, base)
288 if status != 200 || string(body) != "hi" {
289 t.Fatalf("proxied GET = %d body = %s", status, body)
290 }
291 if headers["Content-Type"] != "image/gif" {
292 t.Fatalf("content-type = %s", headers["Content-Type"])
293 }
294 <-done
295 }
296
297 // blURL extracts the "url" field from an upload response.
298 func blURL(body []byte) (u []byte) {
299 pre := []byte("\"url\":\"")
300 for i := 0; i+len(pre) <= len(body); i++ {
301 if string(body[i:i+len(pre)]) == string(pre) {
302 j := i + len(pre)
303 for j < len(body) && body[j] != '"' {
304 j++
305 }
306 return body[i+len(pre) : j]
307 }
308 }
309 return
310 }
311
312 // blPath strips the /blossom/ route prefix the relay removes before the
313 // handler sees the path.
314 func blPath(u []byte) []byte {
315 pre := []byte("/blossom/")
316 if len(u) > len(pre) && string(u[:len(pre)]) == string(pre) {
317 return u[len(pre):]
318 }
319 return u
320 }
321
322 func TestUploadThenFetchRoundTrip(t *testing.T) {
323 s, _ := blTmp(t)
324 // Round trip an upload and fetch the URL it reports.
325 up := []byte("PNG fake")
326 status, _, body := s.HandleRaw("PUT", "/upload", map[string]string{"content-type": "image/png"}, up)
327 if status != 200 {
328 t.Fatalf("upload = %d", status)
329 }
330 if !blHas(body, []byte("\"type\":\"image/png\"")) {
331 t.Fatalf("upload response = %s", body)
332 }
333 if !blHas(body, []byte("\"size\":8")) {
334 t.Fatalf("upload response must report the size: %s", body)
335 }
336 blobURL := blPath(blURL(body))
337 if len(blobURL) == 0 {
338 t.Fatalf("upload response has no url: %s", body)
339 }
340 status2, headers2, got := s.HandleRaw("GET", blobURL, nil, nil)
341 if status2 != 200 || string(got) != string(up) {
342 t.Fatalf("uploaded blob GET = %d body = %s", status2, got)
343 }
344 if headers2["Content-Type"] != "image/png" {
345 t.Fatalf("uploaded blob content-type = %s", headers2["Content-Type"])
346 }
347 // No content-type: the default type and no extension.
348 up2 := []byte("raw")
349 status3, _, body3 := s.HandleRaw("PUT", "/upload", nil, up2)
350 if status3 != 200 {
351 t.Fatalf("untyped upload = %d", status3)
352 }
353 if !blHas(body3, []byte("\"type\":\"application/octet-stream\"")) {
354 t.Fatalf("untyped upload response = %s", body3)
355 }
356 if status4, _, got4 := s.HandleRaw("GET", blPath(blURL(body3)), nil, nil); status4 != 200 || string(got4) != "raw" {
357 t.Fatalf("untyped blob GET = %d body = %s", status4, got4)
358 }
359 }
360