// Blob serving: the stored-blob GET/HEAD paths, the missing-blob 404s, and the // CORS-proxy fallback against a scripted loopback origin (including the local // cache it writes on success). The package's other test file stops at the pure // request-routing surface. package blossom import ( "errors" "os" "syscall" "testing" ) const blHash = "abababababababababababababababababababababababababababababababab" func blListen() (fd int32, port int32, err error) { fd, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil { return 0, 0, err } syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) tv := syscall.Timeval{Sec: 5} syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv) sa := &syscall.SockaddrInet4{Port: 0, Addr: [4]byte{127, 0, 0, 1}} if err = syscall.Bind(fd, sa); err != nil { syscall.Close(fd) return 0, 0, err } if err = syscall.Listen(fd, 8); err != nil { syscall.Close(fd) return 0, 0, err } got, gerr := syscall.Getsockname(fd) if gerr != nil { syscall.Close(fd) return 0, 0, gerr } sa4, ok := got.(*syscall.SockaddrInet4) if !ok { syscall.Close(fd) return 0, 0, errors.New("blossom: loopback listener is not inet4") } return fd, sa4.Port, nil } func blPortStr(port int32) (s string) { if port == 0 { return "0" } buf := []byte{:6} n := int32(6) for port > 0 { n-- buf[n] = byte('0' + port%10) port = port / 10 } return string(buf[n:]) } func blStartListener(t *testing.T) (fd int32, base string) { t.Helper() fd, port, err := blListen() if err != nil { t.Fatalf("listen: %v", err) return 0, "" } return fd, "http://127.0.0.1:" | blPortStr(port) } // blPack length-prefixes each response so the script crosses the spawn // boundary as one slice: presized, cursor-written. func blPack(parts [][]byte) (out []byte) { total := 0 for i := 0; i < len(parts); i++ { total += 4 + len(parts[i]) } out = []byte{:total} pos := 0 for i := 0; i < len(parts); i++ { n := int32(len(parts[i])) out[pos] = byte(n >> 24) out[pos+1] = byte(n >> 16) out[pos+2] = byte(n >> 8) out[pos+3] = byte(n) pos += 4 for j := 0; j < len(parts[i]); j++ { out[pos+j] = parts[i][j] } pos += len(parts[i]) } return } func blServe(fd int32, packed []byte) { pos := 0 for pos+4 <= len(packed) { n := int32(packed[pos])<<24 | int32(packed[pos+1])<<16 | int32(packed[pos+2])<<8 | int32(packed[pos+3]) pos += 4 if pos+n > len(packed) { break } resp := packed[pos : pos+n] pos += n nfd, _, err := syscall.Accept(fd) if err != nil { break } buf := []byte{:2048} for { got, rerr := syscall.Read(nfd, buf) if got <= 0 || rerr != nil { break } if blHas(buf[:got], []byte("\r\n\r\n")) { break } } syscall.Write(nfd, resp) syscall.Close(nfd) } syscall.Close(fd) } func TestMkdirFailureIsReported(t *testing.T) { _, dir := blTmp(t) // A path through an existing regular file cannot become a directory. file := dir | "/notadir" if err := os.WriteFile(file, []byte("x"), 0644); err != nil { t.Fatal(err) } if _, err := New(file | "/sub"); err == nil { t.Fatal("New must report a directory it cannot create") } } func TestServeStoredBlobGetAndHead(t *testing.T) { s, dir := blTmp(t) data := []byte("png bytes here") if err := os.WriteFile(dir|"/"|blHash, data, 0644); err != nil { t.Fatal(err) } status, headers, body := s.HandleRaw("GET", "/"|blHash|".png", nil, nil) if status != 200 { t.Fatalf("GET status = %d", status) } if string(body) != "png bytes here" { t.Fatalf("GET body = %s", body) } if headers["Content-Type"] != "image/png" { t.Fatalf("GET content-type = %s", headers["Content-Type"]) } if headers["Access-Control-Allow-Origin"] != "*" { t.Fatal("GET must carry the CORS headers") } // The extension is absent: the blob is still served, as octet-stream. status2, headers2, body2 := s.HandleRaw("GET", "/"|blHash, nil, nil) if status2 != 200 || string(body2) != "png bytes here" { t.Fatalf("extensionless GET status = %d body = %s", status2, body2) } if headers2["Content-Type"] != "application/octet-stream" { t.Fatalf("extensionless content-type = %s", headers2["Content-Type"]) } // HEAD reports the size and type and carries no body. status3, headers3, body3 := s.HandleRaw("HEAD", "/"|blHash|".jpg", nil, nil) if status3 != 200 { t.Fatalf("HEAD status = %d", status3) } if body3 != nil { t.Fatalf("HEAD body = %s", body3) } if headers3["Content-Length"] != "14" { t.Fatalf("HEAD content-length = %s", headers3["Content-Length"]) } if headers3["Content-Type"] != "image/jpeg" { t.Fatalf("HEAD content-type = %s", headers3["Content-Type"]) } } func TestServeMissingBlobIs404(t *testing.T) { s, _ := blTmp(t) cases := []string{ "/" | blHash | ".png", // valid hash, nothing stored, no upstream "/" | blHash, // no extension "/short", // wrong length } for i := 0; i < len(cases); i++ { status, _, body := s.HandleRaw("GET", cases[i], nil, nil) if status != 404 || body != nil { t.Fatalf("GET %s = %d", cases[i], status) } } // HEAD of a missing blob is a 404 too, even with an upstream configured: // Fetch always GETs, so HEAD is never proxied. if status, _, _ := s.HandleRawWithUpstream("HEAD", "/"|blHash, nil, nil, "http://127.0.0.1:1"); status != 404 { t.Fatalf("missing HEAD = %d", status) } // A dot at index 0 leaves the whole path as the hash candidate. if status, _, _ := s.HandleRaw("GET", "/.png", nil, nil); status != 404 { t.Fatal("a leading dot must not be read as an extension") } // An unparseable upstream counts as self and is not dialed. if status, _, _ := s.HandleRawWithUpstream("GET", "/"|blHash, nil, nil, "://bad"); status != 404 { t.Fatal("an unparseable upstream must 404") } // A refused upstream connection is a 404, not an error. if status, _, _ := s.HandleRawWithUpstream("GET", "/"|blHash, nil, nil, "http://127.0.0.1:1"); status != 404 { t.Fatal("a refused upstream must 404") } } func TestOptionsPreflight(t *testing.T) { s, _ := blTmp(t) status, headers, body := s.HandleRaw("OPTIONS", "/anything", nil, nil) if status != 204 || body != nil { t.Fatalf("OPTIONS = %d", status) } if headers["Access-Control-Allow-Methods"] == "" { t.Fatal("preflight must advertise the methods") } } func TestUpstreamProxyServesAndCaches(t *testing.T) { s, dir := blTmp(t) fd, base := blStartListener(t) if fd == 0 { return } resp := []byte("HTTP/1.1 200 OK\r\nContent-Type: image/webp\r\nContent-Length: 11\r\n\r\nremote blob") done := spawn(blServe, fd, blPack([][]byte{resp})) // The upstream URL carries a trailing slash, which the target builder // trims before appending /blossom/.webp. status, headers, body := s.HandleRawWithUpstream("GET", "/"|blHash|".webp", nil, nil, base|"/") if status != 200 { t.Fatalf("proxied GET = %d", status) } if string(body) != "remote blob" { t.Fatalf("proxied body = %s", body) } if headers["Content-Type"] != "image/webp" { t.Fatalf("proxied content-type = %s", headers["Content-Type"]) } if headers["Access-Control-Allow-Origin"] != "*" { t.Fatal("the proxied response must carry our CORS headers") } <-done // The response was cached: a second request with a dead upstream is served // from disk. status2, _, body2 := s.HandleRawWithUpstream("GET", "/"|blHash|".webp", nil, nil, "http://127.0.0.1:1") if status2 != 200 || string(body2) != "remote blob" { t.Fatalf("cached GET = %d body = %s", status2, body2) } if _, err := os.Stat(dir | "/" | blHash); err != nil { t.Fatalf("the proxied blob must be cached on disk: %v", err) } } func TestUpstreamNonSuccessIs404(t *testing.T) { s, _ := blTmp(t) fd, base := blStartListener(t) if fd == 0 { return } resp := []byte("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n") done := spawn(blServe, fd, blPack([][]byte{resp})) status, _, _ := s.HandleRawWithUpstream("GET", "/"|blHash, nil, nil, base) if status != 404 { t.Fatalf("a 404 upstream must become our 404, got %d", status) } <-done } func TestUpstreamContentTypeFallsBackToExtension(t *testing.T) { s, _ := blTmp(t) fd, base := blStartListener(t) if fd == 0 { return } // No content-type upstream: the extension's mime type is used. resp := []byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi") done := spawn(blServe, fd, blPack([][]byte{resp})) status, headers, body := s.HandleRawWithUpstream("GET", "/"|blHash|".gif", nil, nil, base) if status != 200 || string(body) != "hi" { t.Fatalf("proxied GET = %d body = %s", status, body) } if headers["Content-Type"] != "image/gif" { t.Fatalf("content-type = %s", headers["Content-Type"]) } <-done } // blURL extracts the "url" field from an upload response. func blURL(body []byte) (u []byte) { pre := []byte("\"url\":\"") for i := 0; i+len(pre) <= len(body); i++ { if string(body[i:i+len(pre)]) == string(pre) { j := i + len(pre) for j < len(body) && body[j] != '"' { j++ } return body[i+len(pre) : j] } } return } // blPath strips the /blossom/ route prefix the relay removes before the // handler sees the path. func blPath(u []byte) []byte { pre := []byte("/blossom/") if len(u) > len(pre) && string(u[:len(pre)]) == string(pre) { return u[len(pre):] } return u } func TestUploadThenFetchRoundTrip(t *testing.T) { s, _ := blTmp(t) // Round trip an upload and fetch the URL it reports. up := []byte("PNG fake") status, _, body := s.HandleRaw("PUT", "/upload", map[string]string{"content-type": "image/png"}, up) if status != 200 { t.Fatalf("upload = %d", status) } if !blHas(body, []byte("\"type\":\"image/png\"")) { t.Fatalf("upload response = %s", body) } if !blHas(body, []byte("\"size\":8")) { t.Fatalf("upload response must report the size: %s", body) } blobURL := blPath(blURL(body)) if len(blobURL) == 0 { t.Fatalf("upload response has no url: %s", body) } status2, headers2, got := s.HandleRaw("GET", blobURL, nil, nil) if status2 != 200 || string(got) != string(up) { t.Fatalf("uploaded blob GET = %d body = %s", status2, got) } if headers2["Content-Type"] != "image/png" { t.Fatalf("uploaded blob content-type = %s", headers2["Content-Type"]) } // No content-type: the default type and no extension. up2 := []byte("raw") status3, _, body3 := s.HandleRaw("PUT", "/upload", nil, up2) if status3 != 200 { t.Fatalf("untyped upload = %d", status3) } if !blHas(body3, []byte("\"type\":\"application/octet-stream\"")) { t.Fatalf("untyped upload response = %s", body3) } if status4, _, got4 := s.HandleRaw("GET", blPath(blURL(body3)), nil, nil); status4 != 200 || string(got4) != "raw" { t.Fatalf("untyped blob GET = %d body = %s", status4, got4) } }