mediaproxy_http_test.mx raw

   1  // The full request/response path of Fetch and fetchOnce, driven against a
   2  // scripted HTTP/1.1 origin on loopback. The package's other test file stops at
   3  // the dial; these reach the request construction, the status/header parse, all
   4  // three body framings, redirects and the error returns inside the connection.
   5  
   6  package mediaproxy
   7  
   8  import (
   9  	"errors"
  10  	"syscall"
  11  	"testing"
  12  )
  13  
  14  // mpListen binds a loopback origin. The receive timeout makes a blocked
  15  // accept() in the spawned server return instead of parking a domain forever.
  16  func mpListen() (fd int32, port int32, err error) {
  17  	fd, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
  18  	if err != nil {
  19  		return 0, 0, err
  20  	}
  21  	syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
  22  	tv := syscall.Timeval{Sec: 5}
  23  	syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv)
  24  	sa := &syscall.SockaddrInet4{Port: 0, Addr: [4]byte{127, 0, 0, 1}}
  25  	if err = syscall.Bind(fd, sa); err != nil {
  26  		syscall.Close(fd)
  27  		return 0, 0, err
  28  	}
  29  	if err = syscall.Listen(fd, 8); err != nil {
  30  		syscall.Close(fd)
  31  		return 0, 0, err
  32  	}
  33  	got, gerr := syscall.Getsockname(fd)
  34  	if gerr != nil {
  35  		syscall.Close(fd)
  36  		return 0, 0, gerr
  37  	}
  38  	sa4, ok := got.(*syscall.SockaddrInet4)
  39  	if !ok {
  40  		syscall.Close(fd)
  41  		return 0, 0, errors.New("mediaproxy: loopback listener is not inet4")
  42  	}
  43  	return fd, sa4.Port, nil
  44  }
  45  
  46  func mpPortStr(port int32) (s string) {
  47  	if port == 0 {
  48  		return "0"
  49  	}
  50  	buf := []byte{:6}
  51  	n := int32(6)
  52  	for port > 0 {
  53  		n--
  54  		buf[n] = byte('0' + port%10)
  55  		port = port / 10
  56  	}
  57  	return string(buf[n:])
  58  }
  59  
  60  // mpStartListener is mpListen with the test's failure handling.
  61  func mpStartListener(t *testing.T) (fd int32, base string) {
  62  	t.Helper()
  63  	fd, port, err := mpListen()
  64  	if err != nil {
  65  		t.Fatalf("listen: %v", err)
  66  		return 0, ""
  67  	}
  68  	return fd, "http://127.0.0.1:" | mpPortStr(port)
  69  }
  70  
  71  // mpPack length-prefixes each response so the whole script crosses the spawn
  72  // boundary as one byte slice. Presized: the cursor is the only writer.
  73  func mpPack(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  // mpReadHead consumes one request head (through the blank line).
  96  func mpReadHead(fd int32) (req []byte) {
  97  	buf := []byte{:2048}
  98  	for {
  99  		n, err := syscall.Read(fd, buf)
 100  		if n <= 0 || err != nil {
 101  			return req
 102  		}
 103  		req = req | buf[:n]
 104  		for i := 0; i+3 < len(req); i++ {
 105  			if req[i] == '\r' && req[i+1] == '\n' && req[i+2] == '\r' && req[i+3] == '\n' {
 106  				return req
 107  			}
 108  		}
 109  	}
 110  }
 111  
 112  // mpServe answers the packed script in order, one connection per response.
 113  func mpServe(fd int32, packed []byte) {
 114  	pos := 0
 115  	for pos+4 <= len(packed) {
 116  		n := int32(packed[pos])<<24 | int32(packed[pos+1])<<16 | int32(packed[pos+2])<<8 | int32(packed[pos+3])
 117  		pos += 4
 118  		if pos+n > len(packed) {
 119  			break
 120  		}
 121  		resp := packed[pos : pos+n]
 122  		pos += n
 123  		nfd, _, err := syscall.Accept(fd)
 124  		if err != nil {
 125  			break
 126  		}
 127  		mpReadHead(nfd)
 128  		syscall.Write(nfd, resp)
 129  		syscall.Close(nfd)
 130  	}
 131  	syscall.Close(fd)
 132  }
 133  
 134  func mpStart(t *testing.T, responses [][]byte) (base string, done chan struct{}) {
 135  	t.Helper()
 136  	var fd int32
 137  	fd, base = mpStartListener(t)
 138  	if fd == 0 {
 139  		return
 140  	}
 141  	done = spawn(mpServe, fd, mpPack(responses))
 142  	return
 143  }
 144  
 145  func TestFetchContentLengthBody(t *testing.T) {
 146  	resp := []byte("HTTP/1.1 200 OK\r\nContent-Length: 11\r\nContent-Type: image/png\r\n\r\nhello world")
 147  	base, done := mpStart(t, [][]byte{resp})
 148  	if done == nil {
 149  		return
 150  	}
 151  	// maxBytes 0 must fall back to the default rather than reject the body.
 152  	status, headers, body, err := Fetch(base|"/a.png", 0)
 153  	if err != nil {
 154  		t.Fatalf("fetch: %v", err)
 155  	}
 156  	if status != 200 {
 157  		t.Fatalf("status = %d", status)
 158  	}
 159  	if string(body) != "hello world" {
 160  		t.Fatalf("body = %s", body)
 161  	}
 162  	if headers["content-type"] != "image/png" {
 163  		t.Fatalf("header keys must be lowercased: %v", headers["content-type"])
 164  	}
 165  	if headers["content-length"] != "11" {
 166  		t.Fatalf("content-length = %s", headers["content-length"])
 167  	}
 168  	<-done
 169  }
 170  
 171  func TestFetchChunkedBody(t *testing.T) {
 172  	resp := []byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" |
 173  		"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n")
 174  	base, done := mpStart(t, [][]byte{resp})
 175  	if done == nil {
 176  		return
 177  	}
 178  	status, headers, body, err := Fetch(base|"/chunked", 1024)
 179  	if err != nil {
 180  		t.Fatalf("fetch: %v", err)
 181  	}
 182  	if status != 200 {
 183  		t.Fatalf("status = %d", status)
 184  	}
 185  	if string(body) != "hello world" {
 186  		t.Fatalf("body = %s", body)
 187  	}
 188  	if headers["transfer-encoding"] != "chunked" {
 189  		t.Fatalf("transfer-encoding = %s", headers["transfer-encoding"])
 190  	}
 191  	<-done
 192  }
 193  
 194  func TestFetchReadsToEOFWhenUnframed(t *testing.T) {
 195  	// No content-length and no transfer-encoding: the body ends at the peer's
 196  	// close, which is also the only path that returns an empty body cleanly.
 197  	resp := []byte("HTTP/1.1 200 OK\r\nX-Probe: yes\r\n\r\nstream to eof")
 198  	base, done := mpStart(t, [][]byte{resp})
 199  	if done == nil {
 200  		return
 201  	}
 202  	// No path in the URL at all: fetchOnce's empty-RequestURI default runs.
 203  	status, _, body, err := Fetch(base, 32)
 204  	if err != nil {
 205  		t.Fatalf("fetch: %v", err)
 206  	}
 207  	if status != 200 || string(body) != "stream to eof" {
 208  		t.Fatalf("status = %d body = %s", status, body)
 209  	}
 210  	<-done
 211  }
 212  
 213  func TestFetchFollowsRedirects(t *testing.T) {
 214  	fd, base := mpStartListener(t)
 215  	if fd == 0 {
 216  		return
 217  	}
 218  	// An absolute location, then a network-path reference, then a 200: three
 219  	// hops through the loop and both non-relative resolveRedirect branches.
 220  	first := []byte("HTTP/1.1 302 Found\r\nLocation: " | base | "/next\r\n\r\n")
 221  	second := []byte("HTTP/1.1 301 Moved\r\nLocation: //" | base[7:] | "/np\r\n\r\n")
 222  	third := []byte("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndone")
 223  	done := spawn(mpServe, fd, mpPack([][]byte{first, second, third}))
 224  
 225  	status, _, body, err := Fetch(base|"/start", 1024)
 226  	if err != nil {
 227  		t.Fatalf("fetch: %v", err)
 228  	}
 229  	if status != 200 || string(body) != "done" {
 230  		t.Fatalf("status = %d body = %s", status, body)
 231  	}
 232  	<-done
 233  }
 234  
 235  func TestFetchRelativeRedirect(t *testing.T) {
 236  	fd, base := mpStartListener(t)
 237  	if fd == 0 {
 238  		return
 239  	}
 240  	first := []byte("HTTP/1.1 307 Temporary Redirect\r\nLocation: /elsewhere/x\r\n\r\n")
 241  	second := []byte("HTTP/1.1 302 Found\r\nLocation: sibling\r\n\r\n")
 242  	third := []byte("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nrel")
 243  	done := spawn(mpServe, fd, mpPack([][]byte{first, second, third}))
 244  
 245  	status, _, body, err := Fetch(base|"/dir/page", 1024)
 246  	if err != nil {
 247  		t.Fatalf("fetch: %v", err)
 248  	}
 249  	if status != 200 || string(body) != "rel" {
 250  		t.Fatalf("status = %d body = %s", status, body)
 251  	}
 252  	<-done
 253  }
 254  
 255  func TestFetchRedirectWithoutLocation(t *testing.T) {
 256  	resp := []byte("HTTP/1.1 304 Not Modified\r\nX-Probe: yes\r\n\r\n")
 257  	base, done := mpStart(t, [][]byte{resp})
 258  	if done == nil {
 259  		return
 260  	}
 261  	status, headers, body, err := Fetch(base|"/nothing", 1024)
 262  	if err != nil {
 263  		t.Fatalf("fetch: %v", err)
 264  	}
 265  	if status != 304 {
 266  		t.Fatalf("status = %d", status)
 267  	}
 268  	if body != nil {
 269  		t.Fatalf("a redirect must not carry a body: %s", body)
 270  	}
 271  	if headers["x-probe"] != "yes" {
 272  		t.Fatal("headers must still be returned with the redirect")
 273  	}
 274  	<-done
 275  }
 276  
 277  func TestFetchTooManyRedirects(t *testing.T) {
 278  	fd, base := mpStartListener(t)
 279  	if fd == 0 {
 280  		return
 281  	}
 282  	hop := []byte("HTTP/1.1 302 Found\r\nLocation: " | base | "/loop\r\n\r\n")
 283  	done := spawn(mpServe, fd, mpPack([][]byte{hop, hop, hop, hop, hop}))
 284  
 285  	_, _, _, err := Fetch(base|"/loop", 1024)
 286  	if err == nil {
 287  		t.Fatal("a redirect cycle must fail after maxRedirects")
 288  	}
 289  	if !mpHas(err.Error(), "too many redirects") {
 290  		t.Fatalf("err = %s", err.Error())
 291  	}
 292  	<-done
 293  }
 294  
 295  func TestFetchBodyErrorBranches(t *testing.T) {
 296  	cases := []struct {
 297  		name string
 298  		resp []byte
 299  		lim  int64
 300  		want string
 301  	}{
 302  		{"bad-status-code", []byte("HTTP/1.1 teapot\r\n\r\n"), 1024, "bad status code"},
 303  		{"bad-content-length", []byte("HTTP/1.1 200 OK\r\nContent-Length: abc\r\n\r\n"), 1024, "bad content-length"},
 304  		{"negative-content-length", []byte("HTTP/1.1 200 OK\r\nContent-Length: -5\r\n\r\n"), 1024, "bad content-length"},
 305  		{"length-over-limit", []byte("HTTP/1.1 200 OK\r\nContent-Length: 9999\r\n\r\n"), 10, "response too large"},
 306  		{"short-body", []byte("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabc"), 1024, "read body"},
 307  		{"eof-over-limit", []byte("HTTP/1.1 200 OK\r\ny\r\n\r\n0123456789"), 4, "response too large"},
 308  		{"chunk-over-limit", []byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nff\r\n0123456789\r\n0\r\n\r\n"), 4, "response too large"},
 309  		{"chunk-bad-size", []byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n\r\n"), 1024, "chunked size parse"},
 310  		{"chunk-short-body", []byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nff\r\nabc"), 1024, "chunked body"},
 311  	}
 312  	for i := 0; i < len(cases); i++ {
 313  		tc := cases[i]
 314  		base, done := mpStart(t, [][]byte{tc.resp})
 315  		if done == nil {
 316  			return
 317  		}
 318  		_, _, _, err := Fetch(base|"/probe", tc.lim)
 319  		if err == nil {
 320  			t.Fatalf("%s: expected an error", tc.name)
 321  		}
 322  		if !mpHas(err.Error(), tc.want) {
 323  			t.Fatalf("%s: err = %s, want %s", tc.name, err.Error(), tc.want)
 324  		}
 325  		<-done
 326  	}
 327  }
 328  
 329  func TestFetchHeadersWithoutColonAreSkipped(t *testing.T) {
 330  	resp := []byte("HTTP/1.1 200 OK\r\nGarbage Header Line\r\nContent-Length: 2\r\n\r\nok")
 331  	base, done := mpStart(t, [][]byte{resp})
 332  	if done == nil {
 333  		return
 334  	}
 335  	status, _, body, err := Fetch(base|"/odd", 1024)
 336  	if err != nil {
 337  		t.Fatalf("fetch: %v", err)
 338  	}
 339  	if status != 200 || string(body) != "ok" {
 340  		t.Fatalf("status = %d body = %s", status, body)
 341  	}
 342  	<-done
 343  }
 344  
 345  func TestFetchEmptyReplyFails(t *testing.T) {
 346  	// The peer closes before the status line: the read error is reported.
 347  	base, done := mpStart(t, [][]byte{[]byte(nil)})
 348  	if done == nil {
 349  		return
 350  	}
 351  	_, _, _, err := Fetch(base|"/gone", 1024)
 352  	if err == nil {
 353  		t.Fatal("an empty reply must fail")
 354  	}
 355  	if !mpHas(err.Error(), "read status") {
 356  		t.Fatalf("err = %s", err.Error())
 357  	}
 358  	<-done
 359  }
 360