package nghttp2 import ( "crypto/tls" "fmt" "github.com/bradfitz/http2" "github.com/bradfitz/http2/hpack" "io" "io/ioutil" "net/http" "strings" "syscall" "testing" ) // TestH2H1PlainGET tests whether simple HTTP/2 GET request works. func TestH2H1PlainGET(t *testing.T) { st := newServerTester(nil, t, noopHandler) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1PlainGET", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } want := 200 if res.status != want { t.Errorf("status = %v; want %v", res.status, want) } } // TestH2H1AddXff tests that server generates X-Forwarded-For header // field when forwarding request to backend. func TestH2H1AddXff(t *testing.T) { st := newServerTester([]string{"--add-x-forwarded-for"}, t, func(w http.ResponseWriter, r *http.Request) { xff := r.Header.Get("X-Forwarded-For") want := "127.0.0.1" if xff != want { t.Errorf("X-Forwarded-For = %v; want %v", xff, want) } }) defer st.Close() _, err := st.http2(requestParam{ name: "TestH2H1AddXff", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } } // TestH2H1AddXff2 tests that server appends X-Forwarded-For header // field to existing one when forwarding request to backend. func TestH2H1AddXff2(t *testing.T) { st := newServerTester([]string{"--add-x-forwarded-for"}, t, func(w http.ResponseWriter, r *http.Request) { xff := r.Header.Get("X-Forwarded-For") want := "host, 127.0.0.1" if xff != want { t.Errorf("X-Forwarded-For = %v; want %v", xff, want) } }) defer st.Close() _, err := st.http2(requestParam{ name: "TestH2H1AddXff2", header: []hpack.HeaderField{ pair("x-forwarded-for", "host"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } } // TestH2H1StripXff tests that --strip-incoming-x-forwarded-for // option. func TestH2H1StripXff(t *testing.T) { st := newServerTester([]string{"--strip-incoming-x-forwarded-for"}, t, func(w http.ResponseWriter, r *http.Request) { if xff, found := r.Header["X-Forwarded-For"]; found { t.Errorf("X-Forwarded-For = %v; want nothing", xff) } }) defer st.Close() _, err := st.http2(requestParam{ name: "TestH2H1StripXff1", header: []hpack.HeaderField{ pair("x-forwarded-for", "host"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } } // TestH2H1StripAddXff tests that --strip-incoming-x-forwarded-for and // --add-x-forwarded-for options. func TestH2H1StripAddXff(t *testing.T) { args := []string{ "--strip-incoming-x-forwarded-for", "--add-x-forwarded-for", } st := newServerTester(args, t, func(w http.ResponseWriter, r *http.Request) { xff := r.Header.Get("X-Forwarded-For") want := "127.0.0.1" if xff != want { t.Errorf("X-Forwarded-For = %v; want %v", xff, want) } }) defer st.Close() _, err := st.http2(requestParam{ name: "TestH2H1StripAddXff", header: []hpack.HeaderField{ pair("x-forwarded-for", "host"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } } // TestH2H1GenerateVia tests that server generates Via header field to and // from backend server. func TestH2H1GenerateVia(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Via"), "2 nghttpx"; got != want { t.Errorf("Via: %v; want %v", got, want) } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1GenerateVia", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.header.Get("Via"), "1.1 nghttpx"; got != want { t.Errorf("Via: %v; want %v", got, want) } } // TestH2H1AppendVia tests that server adds value to existing Via // header field to and from backend server. func TestH2H1AppendVia(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Via"), "foo, 2 nghttpx"; got != want { t.Errorf("Via: %v; want %v", got, want) } w.Header().Add("Via", "bar") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1AppendVia", header: []hpack.HeaderField{ pair("via", "foo"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.header.Get("Via"), "bar, 1.1 nghttpx"; got != want { t.Errorf("Via: %v; want %v", got, want) } } // TestH2H1NoVia tests that server does not add value to existing Via // header field to and from backend server. func TestH2H1NoVia(t *testing.T) { st := newServerTester([]string{"--no-via"}, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Via"), "foo"; got != want { t.Errorf("Via: %v; want %v", got, want) } w.Header().Add("Via", "bar") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1NoVia", header: []hpack.HeaderField{ pair("via", "foo"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.header.Get("Via"), "bar"; got != want { t.Errorf("Via: %v; want %v", got, want) } } // TestH2H1HostRewrite tests that server rewrites host header field func TestH2H1HostRewrite(t *testing.T) { st := newServerTester([]string{"--host-rewrite"}, t, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("request-host", r.Host) }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1HostRewrite", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status: %v; want %v", got, want) } if got, want := res.header.Get("request-host"), st.backendHost; got != want { t.Errorf("request-host: %v; want %v", got, want) } } // TestH2H1NoHostRewrite tests that server does not rewrite host // header field func TestH2H1NoHostRewrite(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("request-host", r.Host) }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1NoHostRewrite", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status: %v; want %v", got, want) } if got, want := res.header.Get("request-host"), st.frontendHost; got != want { t.Errorf("request-host: %v; want %v", got, want) } } // TestH2H1BadRequestCL tests that server rejects request whose // content-length header field value does not match its request body // size. func TestH2H1BadRequestCL(t *testing.T) { st := newServerTester(nil, t, noopHandler) defer st.Close() // we set content-length: 1024, but the actual request body is // 3 bytes. res, err := st.http2(requestParam{ name: "TestH2H1BadRequestCL", method: "POST", header: []hpack.HeaderField{ pair("content-length", "1024"), }, body: []byte("foo"), }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } want := http2.ErrCodeProtocol if res.errCode != want { t.Errorf("res.errCode = %v; want %v", res.errCode, want) } } // TestH2H1BadResponseCL tests that server returns error when // content-length response header field value does not match its // response body size. func TestH2H1BadResponseCL(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { // we set content-length: 1024, but only send 3 bytes. w.Header().Add("Content-Length", "1024") w.Write([]byte("foo")) }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1BadResponseCL", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } want := http2.ErrCodeProtocol if res.errCode != want { t.Errorf("res.errCode = %v; want %v", res.errCode, want) } } // TestH2H1LocationRewrite tests location header field rewriting // works. func TestH2H1LocationRewrite(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { // TODO we cannot get st.ts's port number here.. 8443 // is just a place holder. We ignore it on rewrite. w.Header().Add("Location", "http://127.0.0.1:8443/p/q?a=b#fragment") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1LocationRewrite", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } want := fmt.Sprintf("http://127.0.0.1:%v/p/q?a=b#fragment", serverPort) if got := res.header.Get("Location"); got != want { t.Errorf("Location: %v; want %v", got, want) } } // TestH2H1ChunkedRequestBody tests that chunked request body works. func TestH2H1ChunkedRequestBody(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { want := "[chunked]" if got := fmt.Sprint(r.TransferEncoding); got != want { t.Errorf("Transfer-Encoding: %v; want %v", got, want) } body, err := ioutil.ReadAll(r.Body) if err != nil { t.Fatalf("Error reading r.body: %v", err) } want = "foo" if got := string(body); got != want { t.Errorf("body: %v; want %v", got, want) } }) defer st.Close() _, err := st.http2(requestParam{ name: "TestH2H1ChunkedRequestBody", method: "POST", body: []byte("foo"), }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } } // TestH2H1MultipleRequestCL tests that server rejects request with // multiple Content-Length request header fields. func TestH2H1MultipleRequestCL(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { t.Errorf("server should not forward bad request") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1MultipleRequestCL", header: []hpack.HeaderField{ pair("content-length", "1"), pair("content-length", "1"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.errCode, http2.ErrCodeProtocol; got != want { t.Errorf("res.errCode: %v; want %v", got, want) } } // TestH2H1InvalidRequestCL tests that server rejects request with // Content-Length which cannot be parsed as a number. func TestH2H1InvalidRequestCL(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { t.Errorf("server should not forward bad request") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1InvalidRequestCL", header: []hpack.HeaderField{ pair("content-length", ""), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.errCode, http2.ErrCodeProtocol; got != want { t.Errorf("res.errCode: %v; want %v", got, want) } } // TestH2H1ConnectFailure tests that server handles the situation that // connection attempt to HTTP/1 backend failed. func TestH2H1ConnectFailure(t *testing.T) { st := newServerTester(nil, t, noopHandler) defer st.Close() // shutdown backend server to simulate backend connect failure st.ts.Close() res, err := st.http2(requestParam{ name: "TestH2H1ConnectFailure", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } want := 503 if got := res.status; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H1InvalidMethod tests that server rejects invalid method with // 501. func TestH2H1InvalidMethod(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { t.Errorf("server should not forward this request") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1InvalidMethod", method: "get", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 501; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H1AssembleCookies tests that crumbled cookies in HTTP/2 // request is assembled into 1 when forwarding to HTTP/1 backend link. func TestH2H1AssembleCookies(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Cookie"), "alpha; bravo; charlie"; got != want { t.Errorf("Cookie: %v; want %v", got, want) } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1AssembleCookies", header: []hpack.HeaderField{ pair("cookie", "alpha"), pair("cookie", "bravo"), pair("cookie", "charlie"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H1TETrailers tests that server accepts TE request header // field if it has trailers only. func TestH2H1TETrailers(t *testing.T) { st := newServerTester(nil, t, noopHandler) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1TETrailers", header: []hpack.HeaderField{ pair("te", "trailers"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H1TEGzip tests that server resets stream if TE request header // field contains gzip. func TestH2H1TEGzip(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { t.Error("server should not forward bad request") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1TEGzip", header: []hpack.HeaderField{ pair("te", "gzip"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.errCode, http2.ErrCodeProtocol; got != want { t.Errorf("res.errCode = %v; want %v", res.errCode, want) } } // TestH2H1SNI tests server's TLS SNI extension feature. It must // choose appropriate certificate depending on the indicated // server_name from client. func TestH2H1SNI(t *testing.T) { st := newServerTesterTLSConfig([]string{"--subcert=" + testDir + "/alt-server.key:" + testDir + "/alt-server.crt"}, t, noopHandler, &tls.Config{ ServerName: "alt-domain", }) defer st.Close() tlsConn := st.conn.(*tls.Conn) connState := tlsConn.ConnectionState() cert := connState.PeerCertificates[0] if got, want := cert.Subject.CommonName, "alt-domain"; got != want { t.Errorf("CommonName: %v; want %v", got, want) } } // TestH2H1TLSXfp tests nghttpx sends x-forwarded-proto header field // with http value since :scheme is http, even if the frontend // connection is encrypted. func TestH2H1TLSXfp(t *testing.T) { st := newServerTesterTLS(nil, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("x-forwarded-proto"), "http"; got != want { t.Errorf("x-forwarded-proto: want %v; got %v", want, got) } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1TLSXfp", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("res.status: %v; want %v", got, want) } } // TestH2H1ServerPush tests server push using Link header field from // backend server. func TestH2H1ServerPush(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { // only resources marked as rel=preload are pushed if !strings.HasPrefix(r.URL.Path, "/css/") { w.Header().Add("Link", "; rel=preload, , ; rel=preload") } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1ServerPush", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("res.status: %v; want %v", got, want) } if got, want := len(res.pushResponse), 2; got != want { t.Fatalf("len(res.pushResponse): %v; want %v", got, want) } mainCSS := res.pushResponse[0] if got, want := mainCSS.status, 200; got != want { t.Errorf("mainCSS.status: %v; want %v", got, want) } themeCSS := res.pushResponse[1] if got, want := themeCSS.status, 200; got != want { t.Errorf("themeCSS.status: %v; want %v", got, want) } } // TestH2H1RequestTrailer tests request trailer part is forwarded to // backend. func TestH2H1RequestTrailer(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 4096) for { _, err := r.Body.Read(buf) if err == io.EOF { break } if err != nil { t.Fatalf("r.Body.Read() = %v", err) } } if got, want := r.Trailer.Get("foo"), "bar"; got != want { t.Errorf("r.Trailer.Get(foo): %v; want %v", got, want) } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1RequestTrailer", body: []byte("1"), trailer: []hpack.HeaderField{ pair("foo", "bar"), }, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("res.status: %v; want %v", got, want) } } // TestH2H1HeaderFieldBuffer tests that request with header fields // larger than configured buffer size is rejected. func TestH2H1HeaderFieldBuffer(t *testing.T) { st := newServerTester([]string{"--header-field-buffer=10"}, t, func(w http.ResponseWriter, r *http.Request) { t.Fatal("execution path should not be here") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1HeaderFieldBuffer", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 431; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H1HeaderFields tests that request with header fields more // than configured number is rejected. func TestH2H1HeaderFields(t *testing.T) { st := newServerTester([]string{"--max-header-fields=1"}, t, func(w http.ResponseWriter, r *http.Request) { t.Fatal("execution path should not be here") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1HeaderFields", // we have at least 4 pseudo-header fields sent, and // that ensures that buffer limit exceeds. }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 431; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H1ReqPhaseSetHeader tests mruby request phase hook // modifies request header fields. func TestH2H1ReqPhaseSetHeader(t *testing.T) { st := newServerTester([]string{"--request-phase-file=" + testDir + "/req-set-header.rb"}, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("User-Agent"), "mruby"; got != want { t.Errorf("User-Agent = %v; want %v", got, want) } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1ReqPhaseSetHeader", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status = %v; want %v", got, want) } } // TestH2H1ReqPhaseReturn tests mruby request phase hook returns // custom response. func TestH2H1ReqPhaseReturn(t *testing.T) { st := newServerTester([]string{"--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { t.Fatalf("request should not be forwarded") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1ReqPhaseReturn", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 404; got != want { t.Errorf("status = %v; want %v", got, want) } hdtests := []struct { k, v string }{ {"content-length", "11"}, {"from", "mruby"}, } for _, tt := range hdtests { if got, want := res.header.Get(tt.k), tt.v; got != want { t.Errorf("%v = %v; want %v", tt.k, got, want) } } if got, want := string(res.body), "Hello World"; got != want { t.Errorf("body = %v; want %v", got, want) } } // TestH2H1RespPhaseSetHeader tests mruby response phase hook modifies // response header fields. func TestH2H1RespPhaseSetHeader(t *testing.T) { st := newServerTester([]string{"--response-phase-file=" + testDir + "/resp-set-header.rb"}, t, noopHandler) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1RespPhaseSetHeader", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status = %v; want %v", got, want) } if got, want := res.header.Get("alpha"), "bravo"; got != want { t.Errorf("alpha = %v; want %v", got, want) } } // TestH2H1RespPhaseReturn tests mruby response phase hook returns // custom response. func TestH2H1RespPhaseReturn(t *testing.T) { st := newServerTester([]string{"--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H1RespPhaseReturn", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 404; got != want { t.Errorf("status = %v; want %v", got, want) } hdtests := []struct { k, v string }{ {"content-length", "11"}, {"from", "mruby"}, } for _, tt := range hdtests { if got, want := res.header.Get(tt.k), tt.v; got != want { t.Errorf("%v = %v; want %v", tt.k, got, want) } } if got, want := string(res.body), "Hello World"; got != want { t.Errorf("body = %v; want %v", got, want) } } // TestH2H1Upgrade tests HTTP Upgrade to HTTP/2 func TestH2H1Upgrade(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) {}) defer st.Close() res, err := st.http1(requestParam{ name: "TestH2H1Upgrade", header: []hpack.HeaderField{ pair("Connection", "Upgrade, HTTP2-Settings"), pair("Upgrade", "h2c"), pair("HTTP2-Settings", "AAMAAABkAAQAAP__"), }, }) if err != nil { t.Fatalf("Error st.http1() = %v", err) } if got, want := res.status, 101; got != want { t.Errorf("res.status: %v; want %v", got, want) } res, err = st.http2(requestParam{ httpUpgrade: true, }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("res.status: %v; want %v", got, want) } } // TestH2H1GracefulShutdown tests graceful shutdown. func TestH2H1GracefulShutdown(t *testing.T) { st := newServerTester(nil, t, noopHandler) defer st.Close() fmt.Fprint(st.conn, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") if err := st.fr.WriteSettings(); err != nil { t.Fatalf("st.fr.WriteSettings(): %v", err) } header := []hpack.HeaderField{ pair(":method", "GET"), pair(":scheme", "http"), pair(":authority", st.authority), pair(":path", "/"), } for _, h := range header { _ = st.enc.WriteField(h) } if err := st.fr.WriteHeaders(http2.HeadersFrameParam{ StreamID: 1, EndStream: false, EndHeaders: true, BlockFragment: st.headerBlkBuf.Bytes(), }); err != nil { t.Fatalf("st.fr.WriteHeaders(): %v", err) } // send SIGQUIT signal to nghttpx to perform graceful shutdown st.cmd.Process.Signal(syscall.SIGQUIT) // after signal, finish request body if err := st.fr.WriteData(1, true, nil); err != nil { t.Fatalf("st.fr.WriteData(): %v", err) } numGoAway := 0 for { fr, err := st.readFrame() if err != nil { if err == io.EOF { want := 2 if got := numGoAway; got != want { t.Fatalf("numGoAway: %v; want %v", got, want) } return } t.Fatalf("st.readFrame(): %v", err) } switch f := fr.(type) { case *http2.GoAwayFrame: numGoAway += 1 want := http2.ErrCodeNo if got := f.ErrCode; got != want { t.Fatalf("f.ErrCode(%v): %v; want %v", numGoAway, got, want) } switch numGoAway { case 1: want := (uint32(1) << 31) - 1 if got := f.LastStreamID; got != want { t.Fatalf("f.LastStreamID(%v): %v; want %v", numGoAway, got, want) } case 2: want := uint32(1) if got := f.LastStreamID; got != want { t.Fatalf("f.LastStreamID(%v): %v; want %v", numGoAway, got, want) } case 3: t.Fatalf("too many GOAWAYs received") } } } } // TestH2H2MultipleResponseCL tests that server returns error if // multiple Content-Length response header fields are received. func TestH2H2MultipleResponseCL(t *testing.T) { st := newServerTester([]string{"--http2-bridge"}, t, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("content-length", "1") w.Header().Add("content-length", "1") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2MultipleResponseCL", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.errCode, http2.ErrCodeInternal; got != want { t.Errorf("res.errCode: %v; want %v", got, want) } } // TestH2H2InvalidResponseCL tests that server returns error if // Content-Length response header field value cannot be parsed as a // number. func TestH2H2InvalidResponseCL(t *testing.T) { st := newServerTester([]string{"--http2-bridge"}, t, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("content-length", "") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2InvalidResponseCL", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.errCode, http2.ErrCodeInternal; got != want { t.Errorf("res.errCode: %v; want %v", got, want) } } // TestH2H2ConnectFailure tests that server handles the situation that // connection attempt to HTTP/2 backend failed. func TestH2H2ConnectFailure(t *testing.T) { st := newServerTester([]string{"--http2-bridge"}, t, noopHandler) defer st.Close() // simulate backend connect attempt failure st.ts.Close() res, err := st.http2(requestParam{ name: "TestH2H2ConnectFailure", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } want := 503 if got := res.status; got != want { t.Errorf("status: %v; want %v", got, want) } } // TestH2H2HostRewrite tests that server rewrites host header field func TestH2H2HostRewrite(t *testing.T) { st := newServerTester([]string{"--http2-bridge", "--host-rewrite"}, t, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("request-host", r.Host) }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2HostRewrite", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status: %v; want %v", got, want) } if got, want := res.header.Get("request-host"), st.backendHost; got != want { t.Errorf("request-host: %v; want %v", got, want) } } // TestH2H2NoHostRewrite tests that server does not rewrite host // header field func TestH2H2NoHostRewrite(t *testing.T) { st := newServerTester([]string{"--http2-bridge"}, t, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("request-host", r.Host) }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2NoHostRewrite", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("status: %v; want %v", got, want) } if got, want := res.header.Get("request-host"), st.frontendHost; got != want { t.Errorf("request-host: %v; want %v", got, want) } } // TestH2H2TLSXfp tests nghttpx sends x-forwarded-proto header field // with http value since :scheme is http, even if the frontend // connection is encrypted. func TestH2H2TLSXfp(t *testing.T) { st := newServerTesterTLS([]string{"--http2-bridge"}, t, func(w http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("x-forwarded-proto"), "http"; got != want { t.Errorf("x-forwarded-proto: want %v; got %v", want, got) } }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2TLSXfp", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 200; got != want { t.Errorf("res.status: %v; want %v", got, want) } } // TestH2H2ReqPhaseReturn tests mruby request phase hook returns // custom response. func TestH2H2ReqPhaseReturn(t *testing.T) { st := newServerTester([]string{"--http2-bridge", "--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { t.Fatalf("request should not be forwarded") }) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2ReqPhaseReturn", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 404; got != want { t.Errorf("status = %v; want %v", got, want) } hdtests := []struct { k, v string }{ {"content-length", "11"}, {"from", "mruby"}, } for _, tt := range hdtests { if got, want := res.header.Get(tt.k), tt.v; got != want { t.Errorf("%v = %v; want %v", tt.k, got, want) } } if got, want := string(res.body), "Hello World"; got != want { t.Errorf("body = %v; want %v", got, want) } } // TestH2H2RespPhaseReturn tests mruby response phase hook returns // custom response. func TestH2H2RespPhaseReturn(t *testing.T) { st := newServerTester([]string{"--http2-bridge", "--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) defer st.Close() res, err := st.http2(requestParam{ name: "TestH2H2RespPhaseReturn", }) if err != nil { t.Fatalf("Error st.http2() = %v", err) } if got, want := res.status, 404; got != want { t.Errorf("status = %v; want %v", got, want) } hdtests := []struct { k, v string }{ {"content-length", "11"}, {"from", "mruby"}, } for _, tt := range hdtests { if got, want := res.header.Get(tt.k), tt.v; got != want { t.Errorf("%v = %v; want %v", tt.k, got, want) } } if got, want := string(res.body), "Hello World"; got != want { t.Errorf("body = %v; want %v", got, want) } }