// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fcgi import ( "bytes" "errors" "io" "net/http" "strings" "testing" "time" ) var sizeTests = []struct { size uint32 bytes []byte }{ {0, []byte{0x00}}, {127, []byte{0x7F}}, {128, []byte{0x80, 0x00, 0x00, 0x80}}, {1000, []byte{0x80, 0x00, 0x03, 0xE8}}, {33554431, []byte{0x81, 0xFF, 0xFF, 0xFF}}, } func TestSize(t *testing.T) { b := make([]byte, 4) for i, test := range sizeTests { n := encodeSize(b, test.size) if !bytes.Equal(b[:n], test.bytes) { t.Errorf("%d expected %x, encoded %x", i, test.bytes, b) } size, n := readSize(test.bytes) if size != test.size { t.Errorf("%d expected %d, read %d", i, test.size, size) } if len(test.bytes) != n { t.Errorf("%d did not consume all the bytes", i) } } } var streamTests = []struct { desc string recType recType reqId uint16 content []byte raw []byte }{ {"single record", typeStdout, 1, nil, []byte{1, byte(typeStdout), 0, 1, 0, 0, 0, 0}, }, // this data will have to be split into two records {"two records", typeStdin, 300, make([]byte, 66000), bytes.Join([][]byte{ // header for the first record {1, byte(typeStdin), 0x01, 0x2C, 0xFF, 0xFF, 1, 0}, make([]byte, 65536), // header for the second {1, byte(typeStdin), 0x01, 0x2C, 0x01, 0xD1, 7, 0}, make([]byte, 472), // header for the empty record {1, byte(typeStdin), 0x01, 0x2C, 0, 0, 0, 0}, }, nil), }, } type nilCloser struct { io.ReadWriter } func (c *nilCloser) Close() error { return nil } func TestStreams(t *testing.T) { var rec record outer: for _, test := range streamTests { buf := bytes.NewBuffer(test.raw) var content []byte for buf.Len() > 0 { if err := rec.read(buf); err != nil { t.Errorf("%s: error reading record: %v", test.desc, err) continue outer } content = append(content, rec.content()...) } if rec.h.Type != test.recType { t.Errorf("%s: got type %d expected %d", test.desc, rec.h.Type, test.recType) continue } if rec.h.Id != test.reqId { t.Errorf("%s: got request ID %d expected %d", test.desc, rec.h.Id, test.reqId) continue } if !bytes.Equal(content, test.content) { t.Errorf("%s: read wrong content", test.desc) continue } buf.Reset() c := newConn(&nilCloser{buf}) w := newWriter(c, test.recType, test.reqId) if _, err := w.Write(test.content); err != nil { t.Errorf("%s: error writing record: %v", test.desc, err) continue } if err := w.Close(); err != nil { t.Errorf("%s: error closing stream: %v", test.desc, err) continue } if !bytes.Equal(buf.Bytes(), test.raw) { t.Errorf("%s: wrote wrong content", test.desc) } } } type writeOnlyConn struct { buf []byte } func (c *writeOnlyConn) Write(p []byte) (int, error) { c.buf = append(c.buf, p...) return len(p), nil } func (c *writeOnlyConn) Read(p []byte) (int, error) { return 0, errors.New("conn is write-only") } func (c *writeOnlyConn) Close() error { return nil } func TestGetValues(t *testing.T) { var rec record rec.h.Type = typeGetValues wc := new(writeOnlyConn) c := newChild(wc, nil) err := c.handleRecord(&rec) if err != nil { t.Fatalf("handleRecord: %v", err) } const want = "\x01\n\x00\x00\x00\x12\x06\x00" + "\x0f\x01FCGI_MPXS_CONNS1" + "\x00\x00\x00\x00\x00\x00\x01\n\x00\x00\x00\x00\x00\x00" if got := string(wc.buf); got != want { t.Errorf(" got: %q\nwant: %q\n", got, want) } } func nameValuePair11(nameData, valueData string) []byte { return bytes.Join( [][]byte{ {byte(len(nameData)), byte(len(valueData))}, []byte(nameData), []byte(valueData), }, nil, ) } func makeRecord( recordType recType, requestId uint16, contentData []byte, ) []byte { requestIdB1 := byte(requestId >> 8) requestIdB0 := byte(requestId) contentLength := len(contentData) contentLengthB1 := byte(contentLength >> 8) contentLengthB0 := byte(contentLength) return bytes.Join([][]byte{ {1, byte(recordType), requestIdB1, requestIdB0, contentLengthB1, contentLengthB0, 0, 0}, contentData, }, nil) } // a series of FastCGI records that start a request and begin sending the // request body var streamBeginTypeStdin = bytes.Join([][]byte{ // set up request 1 makeRecord(typeBeginRequest, 1, []byte{0, byte(roleResponder), 0, 0, 0, 0, 0, 0}), // add required parameters to request 1 makeRecord(typeParams, 1, nameValuePair11("REQUEST_METHOD", "GET")), makeRecord(typeParams, 1, nameValuePair11("SERVER_PROTOCOL", "HTTP/1.1")), makeRecord(typeParams, 1, nil), // begin sending body of request 1 makeRecord(typeStdin, 1, []byte("0123456789abcdef")), }, nil) var cleanUpTests = []struct { input []byte err error }{ // confirm that child.handleRecord closes req.pw after aborting req { bytes.Join([][]byte{ streamBeginTypeStdin, makeRecord(typeAbortRequest, 1, nil), }, nil), ErrRequestAborted, }, // confirm that child.serve closes all pipes after error reading record { bytes.Join([][]byte{ streamBeginTypeStdin, nil, }, nil), ErrConnClosed, }, } type nopWriteCloser struct { io.Reader } func (nopWriteCloser) Write(buf []byte) (int, error) { return len(buf), nil } func (nopWriteCloser) Close() error { return nil } // Test that child.serve closes the bodies of aborted requests and closes the // bodies of all requests before returning. Causes deadlock if either condition // isn't met. See issue 6934. func TestChildServeCleansUp(t *testing.T) { for _, tt := range cleanUpTests { input := make([]byte, len(tt.input)) copy(input, tt.input) rc := nopWriteCloser{bytes.NewReader(input)} done := make(chan bool) c := newChild(rc, http.HandlerFunc(func( w http.ResponseWriter, r *http.Request, ) { // block on reading body of request _, err := io.Copy(io.Discard, r.Body) if err != tt.err { t.Errorf("Expected %#v, got %#v", tt.err, err) } // not reached if body of request isn't closed done <- true })) go c.serve() // wait for body of request to be closed or all goroutines to block <-done } } type rwNopCloser struct { io.Reader io.Writer } func (rwNopCloser) Close() error { return nil } // Verifies it doesn't crash. Issue 11824. func TestMalformedParams(t *testing.T) { input := []byte{ // beginRequest, requestId=1, contentLength=8, role=1, keepConn=1 1, 1, 0, 1, 0, 8, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, // params, requestId=1, contentLength=10, k1Len=50, v1Len=50 (malformed, wrong length) 1, 4, 0, 1, 0, 10, 0, 0, 50, 50, 3, 4, 5, 6, 7, 8, 9, 10, // end of params 1, 4, 0, 1, 0, 0, 0, 0, } rw := rwNopCloser{bytes.NewReader(input), io.Discard} c := newChild(rw, http.DefaultServeMux) c.serve() } // a series of FastCGI records that start and end a request var streamFullRequestStdin = bytes.Join([][]byte{ // set up request makeRecord(typeBeginRequest, 1, []byte{0, byte(roleResponder), 0, 0, 0, 0, 0, 0}), // add required parameters makeRecord(typeParams, 1, nameValuePair11("REQUEST_METHOD", "GET")), makeRecord(typeParams, 1, nameValuePair11("SERVER_PROTOCOL", "HTTP/1.1")), // set optional parameters makeRecord(typeParams, 1, nameValuePair11("REMOTE_USER", "jane.doe")), makeRecord(typeParams, 1, nameValuePair11("QUERY_STRING", "/foo/bar")), makeRecord(typeParams, 1, nil), // begin sending body of request makeRecord(typeStdin, 1, []byte("0123456789abcdef")), // end request makeRecord(typeEndRequest, 1, nil), }, nil) var envVarTests = []struct { input []byte envVar string expectedVal string expectedFilteredOut bool }{ { streamFullRequestStdin, "REMOTE_USER", "jane.doe", false, }, { streamFullRequestStdin, "QUERY_STRING", "", true, }, } // Test that environment variables set for a request can be // read by a handler. Ensures that variables not set will not // be exposed to a handler. func TestChildServeReadsEnvVars(t *testing.T) { for _, tt := range envVarTests { input := make([]byte, len(tt.input)) copy(input, tt.input) rc := nopWriteCloser{bytes.NewReader(input)} done := make(chan bool) c := newChild(rc, http.HandlerFunc(func( w http.ResponseWriter, r *http.Request, ) { env := ProcessEnv(r) if _, ok := env[tt.envVar]; ok && tt.expectedFilteredOut { t.Errorf("Expected environment variable %s to not be set, but set to %s", tt.envVar, env[tt.envVar]) } else if env[tt.envVar] != tt.expectedVal { t.Errorf("Expected %s, got %s", tt.expectedVal, env[tt.envVar]) } done <- true })) go c.serve() <-done } } func TestResponseWriterSniffsContentType(t *testing.T) { var tests = []struct { name string body string wantCT string }{ { name: "no body", wantCT: "text/plain; charset=utf-8", }, { name: "html", body: "test pageThis is a body", wantCT: "text/html; charset=utf-8", }, { name: "text", body: strings.Repeat("gopher", 86), wantCT: "text/plain; charset=utf-8", }, { name: "jpg", body: "\xFF\xD8\xFF" + strings.Repeat("B", 1024), wantCT: "image/jpeg", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { input := make([]byte, len(streamFullRequestStdin)) copy(input, streamFullRequestStdin) rc := nopWriteCloser{bytes.NewReader(input)} done := make(chan bool) var resp *response c := newChild(rc, http.HandlerFunc(func( w http.ResponseWriter, r *http.Request, ) { io.WriteString(w, tt.body) resp = w.(*response) done <- true })) defer c.cleanUp() go c.serve() <-done if got := resp.Header().Get("Content-Type"); got != tt.wantCT { t.Errorf("got a Content-Type of %q; expected it to start with %q", got, tt.wantCT) } }) } } type signallingNopCloser struct { io.Reader closed chan bool } func (*signallingNopCloser) Write(buf []byte) (int, error) { return len(buf), nil } func (rc *signallingNopCloser) Close() error { close(rc.closed) return nil } // Test whether server properly closes connection when processing slow // requests func TestSlowRequest(t *testing.T) { pr, pw := io.Pipe() go func(w io.Writer) { for _, buf := range [][]byte{ streamBeginTypeStdin, makeRecord(typeStdin, 1, nil), } { pw.Write(buf) time.Sleep(100 * time.Millisecond) } }(pw) rc := &signallingNopCloser{pr, make(chan bool)} handlerDone := make(chan bool) c := newChild(rc, http.HandlerFunc(func( w http.ResponseWriter, r *http.Request, ) { w.WriteHeader(200) close(handlerDone) })) go c.serve() defer c.cleanUp() timeout := time.After(2 * time.Second) <-handlerDone select { case <-rc.closed: t.Log("FastCGI child closed connection") case <-timeout: t.Error("FastCGI child did not close socket after handling request") } }