1
2
3
4
5 package fcgi
6
7 import (
8 "bytes"
9 "errors"
10 "io"
11 "net/http"
12 "strings"
13 "testing"
14 "time"
15 )
16
17 var sizeTests = []struct {
18 size uint32
19 bytes []byte
20 }{
21 {0, []byte{0x00}},
22 {127, []byte{0x7F}},
23 {128, []byte{0x80, 0x00, 0x00, 0x80}},
24 {1000, []byte{0x80, 0x00, 0x03, 0xE8}},
25 {33554431, []byte{0x81, 0xFF, 0xFF, 0xFF}},
26 }
27
28 func TestSize(t *testing.T) {
29 b := make([]byte, 4)
30 for i, test := range sizeTests {
31 n := encodeSize(b, test.size)
32 if !bytes.Equal(b[:n], test.bytes) {
33 t.Errorf("%d expected %x, encoded %x", i, test.bytes, b)
34 }
35 size, n := readSize(test.bytes)
36 if size != test.size {
37 t.Errorf("%d expected %d, read %d", i, test.size, size)
38 }
39 if len(test.bytes) != n {
40 t.Errorf("%d did not consume all the bytes", i)
41 }
42 }
43 }
44
45 var streamTests = []struct {
46 desc string
47 recType recType
48 reqId uint16
49 content []byte
50 raw []byte
51 }{
52 {"single record", typeStdout, 1, nil,
53 []byte{1, byte(typeStdout), 0, 1, 0, 0, 0, 0},
54 },
55
56 {"two records", typeStdin, 300, make([]byte, 66000),
57 bytes.Join([][]byte{
58
59 {1, byte(typeStdin), 0x01, 0x2C, 0xFF, 0xFF, 1, 0},
60 make([]byte, 65536),
61
62 {1, byte(typeStdin), 0x01, 0x2C, 0x01, 0xD1, 7, 0},
63 make([]byte, 472),
64
65 {1, byte(typeStdin), 0x01, 0x2C, 0, 0, 0, 0},
66 },
67 nil),
68 },
69 }
70
71 type nilCloser struct {
72 io.ReadWriter
73 }
74
75 func (c *nilCloser) Close() error { return nil }
76
77 func TestStreams(t *testing.T) {
78 var rec record
79 outer:
80 for _, test := range streamTests {
81 buf := bytes.NewBuffer(test.raw)
82 var content []byte
83 for buf.Len() > 0 {
84 if err := rec.read(buf); err != nil {
85 t.Errorf("%s: error reading record: %v", test.desc, err)
86 continue outer
87 }
88 content = append(content, rec.content()...)
89 }
90 if rec.h.Type != test.recType {
91 t.Errorf("%s: got type %d expected %d", test.desc, rec.h.Type, test.recType)
92 continue
93 }
94 if rec.h.Id != test.reqId {
95 t.Errorf("%s: got request ID %d expected %d", test.desc, rec.h.Id, test.reqId)
96 continue
97 }
98 if !bytes.Equal(content, test.content) {
99 t.Errorf("%s: read wrong content", test.desc)
100 continue
101 }
102 buf.Reset()
103 c := newConn(&nilCloser{buf})
104 w := newWriter(c, test.recType, test.reqId)
105 if _, err := w.Write(test.content); err != nil {
106 t.Errorf("%s: error writing record: %v", test.desc, err)
107 continue
108 }
109 if err := w.Close(); err != nil {
110 t.Errorf("%s: error closing stream: %v", test.desc, err)
111 continue
112 }
113 if !bytes.Equal(buf.Bytes(), test.raw) {
114 t.Errorf("%s: wrote wrong content", test.desc)
115 }
116 }
117 }
118
119 type writeOnlyConn struct {
120 buf []byte
121 }
122
123 func (c *writeOnlyConn) Write(p []byte) (int, error) {
124 c.buf = append(c.buf, p...)
125 return len(p), nil
126 }
127
128 func (c *writeOnlyConn) Read(p []byte) (int, error) {
129 return 0, errors.New("conn is write-only")
130 }
131
132 func (c *writeOnlyConn) Close() error {
133 return nil
134 }
135
136 func TestGetValues(t *testing.T) {
137 var rec record
138 rec.h.Type = typeGetValues
139
140 wc := new(writeOnlyConn)
141 c := newChild(wc, nil)
142 err := c.handleRecord(&rec)
143 if err != nil {
144 t.Fatalf("handleRecord: %v", err)
145 }
146
147 const want = "\x01\n\x00\x00\x00\x12\x06\x00" +
148 "\x0f\x01FCGI_MPXS_CONNS1" +
149 "\x00\x00\x00\x00\x00\x00\x01\n\x00\x00\x00\x00\x00\x00"
150 if got := string(wc.buf); got != want {
151 t.Errorf(" got: %q\nwant: %q\n", got, want)
152 }
153 }
154
155 func nameValuePair11(nameData, valueData string) []byte {
156 return bytes.Join(
157 [][]byte{
158 {byte(len(nameData)), byte(len(valueData))},
159 []byte(nameData),
160 []byte(valueData),
161 },
162 nil,
163 )
164 }
165
166 func makeRecord(
167 recordType recType,
168 requestId uint16,
169 contentData []byte,
170 ) []byte {
171 requestIdB1 := byte(requestId >> 8)
172 requestIdB0 := byte(requestId)
173
174 contentLength := len(contentData)
175 contentLengthB1 := byte(contentLength >> 8)
176 contentLengthB0 := byte(contentLength)
177 return bytes.Join([][]byte{
178 {1, byte(recordType), requestIdB1, requestIdB0, contentLengthB1,
179 contentLengthB0, 0, 0},
180 contentData,
181 },
182 nil)
183 }
184
185
186
187 var streamBeginTypeStdin = bytes.Join([][]byte{
188
189 makeRecord(typeBeginRequest, 1,
190 []byte{0, byte(roleResponder), 0, 0, 0, 0, 0, 0}),
191
192 makeRecord(typeParams, 1, nameValuePair11("REQUEST_METHOD", "GET")),
193 makeRecord(typeParams, 1, nameValuePair11("SERVER_PROTOCOL", "HTTP/1.1")),
194 makeRecord(typeParams, 1, nil),
195
196 makeRecord(typeStdin, 1, []byte("0123456789abcdef")),
197 },
198 nil)
199
200 var cleanUpTests = []struct {
201 input []byte
202 err error
203 }{
204
205 {
206 bytes.Join([][]byte{
207 streamBeginTypeStdin,
208 makeRecord(typeAbortRequest, 1, nil),
209 },
210 nil),
211 ErrRequestAborted,
212 },
213
214 {
215 bytes.Join([][]byte{
216 streamBeginTypeStdin,
217 nil,
218 },
219 nil),
220 ErrConnClosed,
221 },
222 }
223
224 type nopWriteCloser struct {
225 io.Reader
226 }
227
228 func (nopWriteCloser) Write(buf []byte) (int, error) {
229 return len(buf), nil
230 }
231
232 func (nopWriteCloser) Close() error {
233 return nil
234 }
235
236
237
238
239 func TestChildServeCleansUp(t *testing.T) {
240 for _, tt := range cleanUpTests {
241 input := make([]byte, len(tt.input))
242 copy(input, tt.input)
243 rc := nopWriteCloser{bytes.NewReader(input)}
244 done := make(chan struct{})
245 c := newChild(rc, http.HandlerFunc(func(
246 w http.ResponseWriter,
247 r *http.Request,
248 ) {
249
250 _, err := io.Copy(io.Discard, r.Body)
251 if err != tt.err {
252 t.Errorf("Expected %#v, got %#v", tt.err, err)
253 }
254
255 close(done)
256 }))
257 c.serve()
258
259 <-done
260 }
261 }
262
263 type rwNopCloser struct {
264 io.Reader
265 io.Writer
266 }
267
268 func (rwNopCloser) Close() error {
269 return nil
270 }
271
272
273 func TestMalformedParams(t *testing.T) {
274 input := []byte{
275
276 1, 1, 0, 1, 0, 8, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
277
278 1, 4, 0, 1, 0, 10, 0, 0, 50, 50, 3, 4, 5, 6, 7, 8, 9, 10,
279
280 1, 4, 0, 1, 0, 0, 0, 0,
281 }
282 rw := rwNopCloser{bytes.NewReader(input), io.Discard}
283 c := newChild(rw, http.DefaultServeMux)
284 c.serve()
285 }
286
287
288 var streamFullRequestStdin = bytes.Join([][]byte{
289
290 makeRecord(typeBeginRequest, 1,
291 []byte{0, byte(roleResponder), 0, 0, 0, 0, 0, 0}),
292
293 makeRecord(typeParams, 1, nameValuePair11("REQUEST_METHOD", "GET")),
294 makeRecord(typeParams, 1, nameValuePair11("SERVER_PROTOCOL", "HTTP/1.1")),
295
296 makeRecord(typeParams, 1, nameValuePair11("REMOTE_USER", "jane.doe")),
297 makeRecord(typeParams, 1, nameValuePair11("QUERY_STRING", "/foo/bar")),
298 makeRecord(typeParams, 1, nil),
299
300 makeRecord(typeStdin, 1, []byte("0123456789abcdef")),
301
302 makeRecord(typeEndRequest, 1, nil),
303 },
304 nil)
305
306 var envVarTests = []struct {
307 input []byte
308 envVar string
309 expectedVal string
310 expectedFilteredOut bool
311 }{
312 {
313 streamFullRequestStdin,
314 "REMOTE_USER",
315 "jane.doe",
316 false,
317 },
318 {
319 streamFullRequestStdin,
320 "QUERY_STRING",
321 "",
322 true,
323 },
324 }
325
326
327
328
329 func TestChildServeReadsEnvVars(t *testing.T) {
330 for _, tt := range envVarTests {
331 input := make([]byte, len(tt.input))
332 copy(input, tt.input)
333 rc := nopWriteCloser{bytes.NewReader(input)}
334 done := make(chan struct{})
335 c := newChild(rc, http.HandlerFunc(func(
336 w http.ResponseWriter,
337 r *http.Request,
338 ) {
339 env := ProcessEnv(r)
340 if _, ok := env[tt.envVar]; ok && tt.expectedFilteredOut {
341 t.Errorf("Expected environment variable %s to not be set, but set to %s",
342 tt.envVar, env[tt.envVar])
343 } else if env[tt.envVar] != tt.expectedVal {
344 t.Errorf("Expected %s, got %s", tt.expectedVal, env[tt.envVar])
345 }
346 close(done)
347 }))
348 c.serve()
349 <-done
350 }
351 }
352
353 func TestResponseWriterSniffsContentType(t *testing.T) {
354 var tests = []struct {
355 name string
356 body string
357 wantCT string
358 }{
359 {
360 name: "no body",
361 wantCT: "text/plain; charset=utf-8",
362 },
363 {
364 name: "html",
365 body: "<html><head><title>test page</title></head><body>This is a body</body></html>",
366 wantCT: "text/html; charset=utf-8",
367 },
368 {
369 name: "text",
370 body: strings.Repeat("gopher", 86),
371 wantCT: "text/plain; charset=utf-8",
372 },
373 {
374 name: "jpg",
375 body: "\xFF\xD8\xFF" + strings.Repeat("B", 1024),
376 wantCT: "image/jpeg",
377 },
378 }
379 for _, tt := range tests {
380 t.Run(tt.name, func(t *testing.T) {
381 input := make([]byte, len(streamFullRequestStdin))
382 copy(input, streamFullRequestStdin)
383 rc := nopWriteCloser{bytes.NewReader(input)}
384 done := make(chan struct{})
385 var resp *response
386 c := newChild(rc, http.HandlerFunc(func(
387 w http.ResponseWriter,
388 r *http.Request,
389 ) {
390 io.WriteString(w, tt.body)
391 resp = w.(*response)
392 close(done)
393 }))
394 c.serve()
395 <-done
396 if got := resp.Header().Get("Content-Type"); got != tt.wantCT {
397 t.Errorf("got a Content-Type of %q; expected it to start with %q", got, tt.wantCT)
398 }
399 })
400 }
401 }
402
403 type signalingNopWriteCloser struct {
404 io.ReadCloser
405 closed chan bool
406 }
407
408 func (*signalingNopWriteCloser) Write(buf []byte) (int, error) {
409 return len(buf), nil
410 }
411
412 func (rc *signalingNopWriteCloser) Close() error {
413 close(rc.closed)
414 return rc.ReadCloser.Close()
415 }
416
417
418
419 func TestSlowRequest(t *testing.T) {
420 pr, pw := io.Pipe()
421
422 writerDone := make(chan struct{})
423 go func() {
424 for _, buf := range [][]byte{
425 streamBeginTypeStdin,
426 makeRecord(typeStdin, 1, nil),
427 } {
428 pw.Write(buf)
429 time.Sleep(100 * time.Millisecond)
430 }
431 close(writerDone)
432 }()
433 defer func() {
434 <-writerDone
435 pw.Close()
436 }()
437
438 rc := &signalingNopWriteCloser{pr, make(chan bool)}
439 handlerDone := make(chan bool)
440
441 c := newChild(rc, http.HandlerFunc(func(
442 w http.ResponseWriter,
443 r *http.Request,
444 ) {
445 w.WriteHeader(200)
446 close(handlerDone)
447 }))
448 c.serve()
449
450 <-handlerDone
451 <-rc.closed
452 t.Log("FastCGI child closed connection")
453 }
454
View as plain text