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