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