diff options
Diffstat (limited to '')
-rw-r--r-- | integration-tests/.gitignore | 3 | ||||
-rw-r--r-- | integration-tests/CMakeLists.txt | 46 | ||||
-rw-r--r-- | integration-tests/Makefile.am | 51 | ||||
-rw-r--r-- | integration-tests/alt-server.crt | 21 | ||||
-rw-r--r-- | integration-tests/alt-server.key | 28 | ||||
-rw-r--r-- | integration-tests/config.go.in | 6 | ||||
-rw-r--r-- | integration-tests/nghttpx_http1_test.go | 1374 | ||||
-rw-r--r-- | integration-tests/nghttpx_http2_test.go | 3740 | ||||
-rw-r--r-- | integration-tests/nghttpx_http3_test.go | 393 | ||||
-rw-r--r-- | integration-tests/req-return.rb | 12 | ||||
-rw-r--r-- | integration-tests/req-set-header.rb | 7 | ||||
-rw-r--r-- | integration-tests/resp-return.rb | 12 | ||||
-rw-r--r-- | integration-tests/resp-set-header.rb | 7 | ||||
-rw-r--r-- | integration-tests/server.crt | 21 | ||||
-rw-r--r-- | integration-tests/server.key | 28 | ||||
-rw-r--r-- | integration-tests/server_tester.go | 895 | ||||
-rw-r--r-- | integration-tests/setenv.in | 13 |
17 files changed, 6657 insertions, 0 deletions
diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore new file mode 100644 index 0000000..f40c109 --- /dev/null +++ b/integration-tests/.gitignore @@ -0,0 +1,3 @@ +# generated files +config.go +setenv diff --git a/integration-tests/CMakeLists.txt b/integration-tests/CMakeLists.txt new file mode 100644 index 0000000..d9385a5 --- /dev/null +++ b/integration-tests/CMakeLists.txt @@ -0,0 +1,46 @@ +set(GO_FILES + nghttpx_http1_test.go + nghttpx_http2_test.go + server_tester.go +) + +# XXX unused +set(EXTRA_DIST + ${GO_FILES} + server.key + server.crt + alt-server.key + alt-server.crt + setenv + req-set-header.rb + resp-set-header.rb + req-return.rb + resp-return.rb +) + +add_custom_target(itprep + COMMAND go get -d -v golang.org/x/net/http2 + COMMAND go get -d -v github.com/tatsuhiro-t/go-nghttp2 + COMMAND go get -d -v golang.org/x/net/websocket +) + +# 'go test' requires both config.go and the test files in the same directory. +# For out-of-tree builds, config.go is normally not placed next to the source +# files, so copy the tests to the build directory as a workaround. +set(GO_BUILD_FILES) +if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR) + foreach(gofile IN LISTS GO_FILES) + set(outfile "${CMAKE_CURRENT_BINARY_DIR}/${gofile}") + add_custom_command(OUTPUT "${outfile}" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/${gofile}" "${outfile}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${gofile}" + ) + list(APPEND GO_BUILD_FILES "${outfile}") + endforeach() +endif() + +add_custom_target(it + COMMAND sh setenv go test -v + DEPENDS ${GO_BUILD_FILES} +) diff --git a/integration-tests/Makefile.am b/integration-tests/Makefile.am new file mode 100644 index 0000000..38cd119 --- /dev/null +++ b/integration-tests/Makefile.am @@ -0,0 +1,51 @@ +# nghttp2 - HTTP/2 C Library + +# Copyright (c) 2015 Tatsuhiro Tsujikawa + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +GO_FILES = \ + nghttpx_http1_test.go \ + nghttpx_http2_test.go \ + nghttpx_http3_test.go \ + server_tester.go + +EXTRA_DIST = \ + CMakeLists.txt \ + $(GO_FILES) \ + server.key \ + server.crt \ + alt-server.key \ + alt-server.crt \ + setenv \ + req-set-header.rb \ + resp-set-header.rb \ + req-return.rb \ + resp-return.rb + +GO_TEST_TAGS = + +if ENABLE_HTTP3 +GO_TEST_TAGS += quic +endif # ENABLE_HTTP3 + +it: + for i in $(GO_FILES); do [ -e $(builddir)/$$i ] || cp $(srcdir)/$$i $(builddir); done + sh setenv go test -v --tags=${GO_TEST_TAGS} diff --git a/integration-tests/alt-server.crt b/integration-tests/alt-server.crt new file mode 100644 index 0000000..f003eb1 --- /dev/null +++ b/integration-tests/alt-server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhzCCAm+gAwIBAgIJANfuEldiquMNMA0GCSqGSIb3DQEBCwUAMFoxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEzARBgNVBAMMCmFsdC1kb21haW4wHhcNMTUwMTI1MDYy +NTQxWhcNMjUwMTIyMDYyNTQxWjBaMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29t +ZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRMwEQYD +VQQDDAphbHQtZG9tYWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +0IwhDOGDipGrJQ9IoRSzPdkU/Ii4aJgGKHlXminym42X0VI3IW61RLvOHRlHVmVH +JQjFuDo2x+y81t9NlDg3HGUbSpzOzpm6StiutB7c4hreT5G4r0YKya1ugiemN0+p +qjIPJWm2jVnf448eZvUKRKEQ9W0MLZjiNjVGKrKlwo7fIlXg4N3+YixLYffAT1NV +d1T6V5jzlbruj15gK2nGjMQ9D1h1t9vTbTxY+mtk72aX0Y64IE6pPBWLFSSH8ozU +idDoL3AZwz2Jker+ALKK8CM4uho/RPpyW1C06HH+HLdH2MqEjDOROde/Nzxm668O +gK/JWGIEyUqYiUXx0yhFxwIDAQABo1AwTjAdBgNVHQ4EFgQU/Y0GDN2uPjbyePcu +95ZvYEK/gHIwHwYDVR0jBBgwFoAU/Y0GDN2uPjbyePcu95ZvYEK/gHIwDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAodD6LVCzL3wfsZ6TxTzf9TfgIdbj +ilL3SEMT/xnfTXT3SLYScTRqQIAI29Y7dOLMq89p4hY2wmeUEhBUAz+y9G2JVr8o +6EbxXrQpWgNJogELqoNnMdrDxB5RsmDDKEJ/rLjDfSkjWbK7B2PZsqVTDgjekCFw +u6FqTIjn/O1O/L5tjwxwxjHmQod/maFCvXoDOVBuwdHnkp298tqlvsHfHO8m++Wj ++XYB8plMIjpeTh9v4w9Jc4QZ59lK/3Tt4qaENeQrMEubKSY/Zen7L2bzhk+cChWT +GSGz9uNXieoZaH79D0wnyZaSZ5Ds4ActMevnGg3iYXuzuFqx8Pungn74Vg== +-----END CERTIFICATE----- diff --git a/integration-tests/alt-server.key b/integration-tests/alt-server.key new file mode 100644 index 0000000..a977663 --- /dev/null +++ b/integration-tests/alt-server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDQjCEM4YOKkasl +D0ihFLM92RT8iLhomAYoeVeaKfKbjZfRUjchbrVEu84dGUdWZUclCMW4OjbH7LzW +302UODccZRtKnM7OmbpK2K60HtziGt5PkbivRgrJrW6CJ6Y3T6mqMg8labaNWd/j +jx5m9QpEoRD1bQwtmOI2NUYqsqXCjt8iVeDg3f5iLEth98BPU1V3VPpXmPOVuu6P +XmAracaMxD0PWHW329NtPFj6a2TvZpfRjrggTqk8FYsVJIfyjNSJ0OgvcBnDPYmR +6v4AsorwIzi6Gj9E+nJbULTocf4ct0fYyoSMM5E51783PGbrrw6Ar8lYYgTJSpiJ +RfHTKEXHAgMBAAECggEBALTrjFSXY72YB+h7rN+JjMIwDIPUvF6I3HbKZhQpJf6K +xNVkRM2tNHavku0tm/S4ohLf3F+pqRKiL2Udjjjy1+S7VgTRqpwTQ0lhV5aNW8SP +2KMg4R61XfB+k+s4KHu9kYxEJ12mqydPe+r3o0FgfYryTDsOYk1AX6b1aqzqFOGF +7GaqLALSbKU59tcJJ1SZNBbpIKFUrAT9nZt9dW02/foqP5bzUk43Yjw48xmLwegc +bMXXcpZhNZSktltvwRw7Q4Foc9kuRlMdTAnAD9PnMCcZwicS/YeVVF6Rz4fGviKv +7/kPHQ7g4YpFktVDzuZ5xw6GDVFeJ6uGMVUX8+EePvkCgYEA+/nrcn82nFHCxm8Q +0iiUhi/AoXjZg+O5Ytaje9O/YNoX+c4ywe13h0+TXKH79O0KfTwXeJyDgPZbAIFV +9oURellRYUzKDafnBHis2f+Ywn6GqHL5e2X30ZxIp1GK46pcvne1YuvJhgGmiVay +vd7sRx09OKU124dG22rIFCis6asCgYEA0+CsA6LrEwQ/aPJYASY3VHNO/WoAOnPg +Cwsg+02XWsPEwP//lNmpanz8TUm2URS063ZK8bx7t3ejvDgBdsRwwjiMlDp7XTUU +3Zk+mhCV2qkMi02aKemvz29bDhmh5JoH7W3IwsXtJYO0yZDYrDR3ioiKRccioPoE +b/Nq781sEFUCgYEA4xqx9xRpaCLY5nicNI6WrwrDF8YQZisNn+PMnYKP7v8itOgA +H4GkRbSXINpueKZc2dsbXH3UmJtyEdaAYBw3UIrIKmZHhl9afFE3mZQhXssjGxfl +fC6/WZD+eq+n+uJFjPXf6jSSAdHjA828dB1D4CSeVTuyexZF6uUnR+QRVNkCgYEA +i+pb7XLSpZYygY03zFp+Q0h6KyKqz+7hTqmkuA8/GfMZpRHop1UtaWLsAeXhfZ2c +87kEOKptUHSzLYIWhWWnyLorK1+LQ7vf8Y5XJso5C1KDNCKk4XSuYt94U9FddWa6 +QXI0F1s5BYL6Cfma++0R2+va08Vy+rbf40XtojoXWJkCgYEA0hMQSCvok7is27nQ +G80KXfmghU2eEB7zif3T00/fwJycxEbmnNeof+SKmhdY4ZgqTscfOxlQPflV/eqB +xs4GnFDDeM0F8KH0BimOXxr7sJPFCg22PCCQQcRtM/KoU+ip/kNmTfwrsC0xMFPU +HD8M1JCZF2eLMekXXP3cB0U4sUs= +-----END PRIVATE KEY----- diff --git a/integration-tests/config.go.in b/integration-tests/config.go.in new file mode 100644 index 0000000..3d79297 --- /dev/null +++ b/integration-tests/config.go.in @@ -0,0 +1,6 @@ +package nghttp2 + +const ( + buildDir = "@top_builddir@" + sourceDir = "@top_srcdir@" +) diff --git a/integration-tests/nghttpx_http1_test.go b/integration-tests/nghttpx_http1_test.go new file mode 100644 index 0000000..a083f0e --- /dev/null +++ b/integration-tests/nghttpx_http1_test.go @@ -0,0 +1,1374 @@ +package nghttp2 + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "syscall" + "testing" + "time" + + "golang.org/x/net/http2/hpack" + "golang.org/x/net/websocket" +) + +// TestH1H1PlainGET tests whether simple HTTP/1 GET request works. +func TestH1H1PlainGET(t *testing.T) { + st := newServerTester(t, options{}) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1PlainGET", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH1H1PlainGETClose tests whether simple HTTP/1 GET request with +// Connection: close request header field works. +func TestH1H1PlainGETClose(t *testing.T) { + st := newServerTester(t, options{}) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1PlainGETClose", + header: []hpack.HeaderField{ + pair("Connection", "close"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH1H1InvalidMethod tests that server rejects invalid method with +// 501 status code +func TestH1H1InvalidMethod(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1InvalidMethod", + method: "get", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusNotImplemented; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH1H1MultipleRequestCL tests that server rejects request which +// contains multiple Content-Length header fields. +func TestH1H1MultipleRequestCL(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward bad request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %v\r\nTest-Case: TestH1H1MultipleRequestCL\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", + st.authority)); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusBadRequest; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// // TestH1H1ConnectFailure tests that server handles the situation that +// // connection attempt to HTTP/1 backend failed. +// func TestH1H1ConnectFailure(t *testing.T) { +// st := newServerTester(t, options{}) +// defer st.Close() + +// // shutdown backend server to simulate backend connect failure +// st.ts.Close() + +// res, err := st.http1(requestParam{ +// name: "TestH1H1ConnectFailure", +// }) +// if err != nil { +// t.Fatalf("Error st.http1() = %v", err) +// } +// want := 503 +// if got := res.status; got != want { +// t.Errorf("status: %v; want %v", got, want) +// } +// } + +// TestH1H1AffinityCookie tests that affinity cookie is sent back in +// cleartext http. +func TestH1H1AffinityCookie(t *testing.T) { + opts := options{ + args: []string{"--affinity-cookie"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1AffinityCookie", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar` + validCookie := regexp.MustCompile(pattern) + if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) { + t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern) + } +} + +// TestH1H1AffinityCookieTLS tests that affinity cookie is sent back +// in https. +func TestH1H1AffinityCookieTLS(t *testing.T) { + opts := options{ + args: []string{"--alpn-h1", "--affinity-cookie"}, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1AffinityCookieTLS", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar; Secure` + validCookie := regexp.MustCompile(pattern) + if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) { + t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern) + } +} + +// TestH1H1GracefulShutdown tests graceful shutdown. +func TestH1H1GracefulShutdown(t *testing.T) { + st := newServerTester(t, options{}) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1GracefulShutdown-1", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } + + if err := st.cmd.Process.Signal(syscall.SIGQUIT); err != nil { + t.Fatalf("Error st.cmd.Process.Signal() = %v", err) + } + + time.Sleep(150 * time.Millisecond) + + res, err = st.http1(requestParam{ + name: "TestH1H1GracefulShutdown-2", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } + + if got, want := res.connClose, true; got != want { + t.Errorf("res.connClose: %v; want %v", got, want) + } + + want := io.EOF + b := make([]byte, 256) + if _, err := st.conn.Read(b); err == nil || err != want { + t.Errorf("st.conn.Read(): %v; want %v", err, want) + } +} + +// TestH1H1HostRewrite tests that server rewrites Host header field +func TestH1H1HostRewrite(t *testing.T) { + opts := options{ + args: []string{"--host-rewrite"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1HostRewrite", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; 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) + } +} + +// TestH1H1BadHost tests that server rejects request including bad +// characters in host header field. +func TestH1H1BadHost(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET / HTTP/1.1\r\nTest-Case: TestH1H1HBadHost\r\nHost: foo\"bar\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusBadRequest; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1BadAuthority tests that server rejects request including +// bad characters in authority component of requset URI. +func TestH1H1BadAuthority(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET http://foo\"bar/ HTTP/1.1\r\nTest-Case: TestH1H1HBadAuthority\r\nHost: foobar\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusBadRequest; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1BadScheme tests that server rejects request including +// bad characters in scheme component of requset URI. +func TestH1H1BadScheme(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET http*://example.com/ HTTP/1.1\r\nTest-Case: TestH1H1HBadScheme\r\nHost: example.com\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusBadRequest; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1HTTP10 tests that server can accept HTTP/1.0 request +// without Host header field +func TestH1H1HTTP10(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET / HTTP/1.0\r\nTest-Case: TestH1H1HTTP10\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } + if got, want := resp.Header.Get("request-host"), st.backendHost; got != want { + t.Errorf("request-host: %v; want %v", got, want) + } +} + +// TestH1H1HTTP10NoHostRewrite tests that server generates host header +// field using actual backend server even if --no-http-rewrite is +// used. +func TestH1H1HTTP10NoHostRewrite(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET / HTTP/1.0\r\nTest-Case: TestH1H1HTTP10NoHostRewrite\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } + if got, want := resp.Header.Get("request-host"), st.backendHost; got != want { + t.Errorf("request-host: %v; want %v", got, want) + } +} + +// TestH1H1RequestTrailer tests request trailer part is forwarded to +// backend. +func TestH1H1RequestTrailer(t *testing.T) { + opts := options{ + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1RequestTrailer", + body: []byte("1"), + trailer: []hpack.HeaderField{ + pair("foo", "bar"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH1H1HeaderFieldBufferPath tests that request with request path +// larger than configured buffer size is rejected. +func TestH1H1HeaderFieldBufferPath(t *testing.T) { + // The value 100 is chosen so that sum of header fields bytes + // does not exceed it. We use > 100 bytes URI to exceed this + // limit. + opts := options{ + args: []string{"--request-header-field-buffer=100"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("execution path should not be here") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1HeaderFieldBufferPath", + path: "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusRequestHeaderFieldsTooLarge; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1HeaderFieldBuffer tests that request with header fields +// larger than configured buffer size is rejected. +func TestH1H1HeaderFieldBuffer(t *testing.T) { + opts := options{ + args: []string{"--request-header-field-buffer=10"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("execution path should not be here") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1HeaderFieldBuffer", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusRequestHeaderFieldsTooLarge; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1HeaderFields tests that request with header fields more +// than configured number is rejected. +func TestH1H1HeaderFields(t *testing.T) { + opts := options{ + args: []string{"--max-request-header-fields=1"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("execution path should not be here") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1HeaderFields", + header: []hpack.HeaderField{ + // Add extra header field to ensure that + // header field limit exceeds + pair("Connection", "close"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusRequestHeaderFieldsTooLarge; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1Websocket tests that HTTP Upgrade to WebSocket works. +func TestH1H1Websocket(t *testing.T) { + opts := options{ + handler: websocket.Handler(func(ws *websocket.Conn) { + if _, err := io.Copy(ws, ws); err != nil { + t.Fatalf("Error io.Copy() = %v", err) + } + }).ServeHTTP, + } + st := newServerTester(t, opts) + defer st.Close() + + content := []byte("hello world") + res := st.websocket(requestParam{ + name: "TestH1H1Websocket", + body: content, + }) + if got, want := res.body, content; !bytes.Equal(got, want) { + t.Errorf("echo: %q; want %q", got, want) + } +} + +// TestH1H1ReqPhaseSetHeader tests mruby request phase hook +// modifies request header fields. +func TestH1H1ReqPhaseSetHeader(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-set-header.rb"}, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1ReqPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH1H1ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH1H1ReqPhaseReturn(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-return.rb"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "20"}, + {"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 from req"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH1H1RespPhaseSetHeader tests mruby response phase hook modifies +// response header fields. +func TestH1H1RespPhaseSetHeader(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/resp-set-header.rb"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1RespPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusOK; 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) + } +} + +// TestH1H1RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH1H1RespPhaseReturn(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/resp-return.rb"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "21"}, + {"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 from resp"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH1H1HTTPSRedirect tests that the request to the backend which +// requires TLS is redirected to https URI. +func TestH1H1HTTPSRedirect(t *testing.T) { + opts := options{ + args: []string{"--redirect-if-not-tls"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1HTTPSRedirect", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusPermanentRedirect; got != want { + t.Errorf("status = %v; want %v", got, want) + } + if got, want := res.header.Get("location"), "https://127.0.0.1/"; got != want { + t.Errorf("location: %v; want %v", got, want) + } +} + +// TestH1H1HTTPSRedirectPort tests that the request to the backend +// which requires TLS is redirected to https URI with given port. +func TestH1H1HTTPSRedirectPort(t *testing.T) { + opts := options{ + args: []string{ + "--redirect-if-not-tls", + "--redirect-https-port=8443", + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + path: "/foo?bar", + name: "TestH1H1HTTPSRedirectPort", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusPermanentRedirect; got != want { + t.Errorf("status = %v; want %v", got, want) + } + if got, want := res.header.Get("location"), "https://127.0.0.1:8443/foo?bar"; got != want { + t.Errorf("location: %v; want %v", got, want) + } +} + +// TestH1H1POSTRequests tests that server can handle 2 requests with +// request body. +func TestH1H1POSTRequests(t *testing.T) { + st := newServerTester(t, options{}) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1POSTRequestsNo1", + body: make([]byte, 1), + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + res, err = st.http1(requestParam{ + name: "TestH1H1POSTRequestsNo2", + body: make([]byte, 65536), + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// // TestH1H2ConnectFailure tests that server handles the situation that +// // connection attempt to HTTP/2 backend failed. +// func TestH1H2ConnectFailure(t *testing.T) { +// opts := options{ +// args: []string{"--http2-bridge"}, +// } +// st := newServerTester(t, opts) +// defer st.Close() + +// // simulate backend connect attempt failure +// st.ts.Close() + +// res, err := st.http1(requestParam{ +// name: "TestH1H2ConnectFailure", +// }) +// if err != nil { +// t.Fatalf("Error st.http1() = %v", err) +// } +// want := 503 +// if got := res.status; got != want { +// t.Errorf("status: %v; want %v", got, want) +// } +// } + +// TestH1H2NoHost tests that server rejects request without Host +// header field for HTTP/2 backend. +func TestH1H2NoHost(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward bad request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + // without Host header field, we expect 400 response + if _, err := io.WriteString(st.conn, "GET / HTTP/1.1\r\nTest-Case: TestH1H2NoHost\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusBadRequest; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H2HTTP10 tests that server can accept HTTP/1.0 request +// without Host header field +func TestH1H2HTTP10(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET / HTTP/1.0\r\nTest-Case: TestH1H2HTTP10\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } + if got, want := resp.Header.Get("request-host"), st.backendHost; got != want { + t.Errorf("request-host: %v; want %v", got, want) + } +} + +// TestH1H2HTTP10NoHostRewrite tests that server generates host header +// field using actual backend server even if --no-http-rewrite is +// used. +func TestH1H2HTTP10NoHostRewrite(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, "GET / HTTP/1.0\r\nTest-Case: TestH1H2HTTP10NoHostRewrite\r\n\r\n"); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } + if got, want := resp.Header.Get("request-host"), st.backendHost; got != want { + t.Errorf("request-host: %v; want %v", got, want) + } +} + +// TestH1H2CrumbleCookie tests that Cookies are crumbled and assembled +// when forwarding to HTTP/2 backend link. go-nghttp2 server +// concatenates crumbled Cookies automatically, so this test is not +// much effective now. +func TestH1H2CrumbleCookie(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2CrumbleCookie", + header: []hpack.HeaderField{ + pair("Cookie", "alpha; bravo; charlie"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H2GenerateVia tests that server generates Via header field to and +// from backend server. +func TestH1H2GenerateVia(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Via"), "1.1 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2GenerateVia", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.header.Get("Via"), "2 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } +} + +// TestH1H2AppendVia tests that server adds value to existing Via +// header field to and from backend server. +func TestH1H2AppendVia(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Via"), "foo, 1.1 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } + w.Header().Add("Via", "bar") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2AppendVia", + header: []hpack.HeaderField{ + pair("via", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.header.Get("Via"), "bar, 2 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } +} + +// TestH1H2NoVia tests that server does not add value to existing Via +// header field to and from backend server. +func TestH1H2NoVia(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--no-via"}, + handler: 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") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2NoVia", + header: []hpack.HeaderField{ + pair("via", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.header.Get("Via"), "bar"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } +} + +// TestH1H2ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH1H2ReqPhaseReturn(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--mruby-file=" + testDir + "/req-return.rb", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "20"}, + {"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 from req"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH1H2RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH1H2RespPhaseReturn(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--mruby-file=" + testDir + "/resp-return.rb", + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "21"}, + {"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 from resp"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH1H2TE tests that "te: trailers" header is forwarded to HTTP/2 +// backend server by stripping other encodings. +func TestH1H2TE(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("te"), "trailers"; got != want { + t.Errorf("te: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2TE", + header: []hpack.HeaderField{ + pair("te", "foo,trailers,bar"), + }, + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1APIBackendconfig exercise backendconfig API endpoint routine +// for successful case. +func TestH1APIBackendconfig(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1APIBackendconfig", + path: "/api/v1beta1/backendconfig", + method: "PUT", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Success"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 200; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH1APIBackendconfigQuery exercise backendconfig API endpoint +// routine with query. +func TestH1APIBackendconfigQuery(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1APIBackendconfigQuery", + path: "/api/v1beta1/backendconfig?foo=bar", + method: "PUT", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Success"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 200; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH1APIBackendconfigBadMethod exercise backendconfig API endpoint +// routine with bad method. +func TestH1APIBackendconfigBadMethod(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1APIBackendconfigBadMethod", + path: "/api/v1beta1/backendconfig", + method: "GET", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusMethodNotAllowed; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Failure"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 405; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH1APIConfigrevision tests configrevision API. +func TestH1APIConfigrevision(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1APIConfigrevision", + path: "/api/v1beta1/configrevision", + method: "GET", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want = %v", got, want) + } + + var apiResp APIResponse + d := json.NewDecoder(bytes.NewBuffer(res.body)) + d.UseNumber() + err = d.Decode(&apiResp) + if err != nil { + t.Fatalf("Error unmarshalling API response: %v", err) + } + if got, want := apiResp.Status, "Success"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 200; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Data["configRevision"], json.Number("0"); got != want { + t.Errorf(`apiResp.Data["configRevision"]: %v %t; want %v`, got, got, want) + } +} + +// TestH1APINotFound exercise backendconfig API endpoint routine when +// API endpoint is not found. +func TestH1APINotFound(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1APINotFound", + path: "/api/notfound", + method: "GET", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Failure"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 404; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH1Healthmon tests health monitor endpoint. +func TestH1Healthmon(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3011;healthmon;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3011, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1Healthmon", + path: "/alpha/bravo", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH1ResponseBeforeRequestEnd tests the situation where response +// ends before request body finishes. +func TestH1ResponseBeforeRequestEnd(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-return.rb"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("request should not be forwarded") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := io.WriteString(st.conn, fmt.Sprintf("POST / HTTP/1.1\r\nHost: %v\r\nTest-Case: TestH1ResponseBeforeRequestEnd\r\nContent-Length: 1000000\r\n\r\n", + st.authority)); err != nil { + t.Fatalf("Error io.WriteString() = %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(st.conn), nil) + if err != nil { + t.Fatalf("Error http.ReadResponse() = %v", err) + } + + defer resp.Body.Close() + + if got, want := resp.StatusCode, http.StatusNotFound; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH1H1ChunkedEndsPrematurely tests that an HTTP/1.1 request fails +// if the backend chunked encoded response ends prematurely. +func TestH1H1ChunkedEndsPrematurely(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Could not hijack the connection", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + if _, err := bufrw.WriteString("HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n"); err != nil { + t.Fatalf("Error bufrw.WriteString() = %v", err) + } + bufrw.Flush() + }, + } + st := newServerTester(t, opts) + defer st.Close() + + _, err := st.http1(requestParam{ + name: "TestH1H1ChunkedEndsPrematurely", + }) + if err == nil { + t.Fatal("st.http1() should fail") + } +} diff --git a/integration-tests/nghttpx_http2_test.go b/integration-tests/nghttpx_http2_test.go new file mode 100644 index 0000000..5324a18 --- /dev/null +++ b/integration-tests/nghttpx_http2_test.go @@ -0,0 +1,3740 @@ +package nghttp2 + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "regexp" + "strings" + "syscall" + "testing" + "time" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/hpack" +) + +// TestH2H1PlainGET tests whether simple HTTP/2 GET request works. +func TestH2H1PlainGET(t *testing.T) { + st := newServerTester(t, options{}) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1PlainGET", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1AddXfp tests that server appends :scheme to the existing +// x-forwarded-proto header field. +func TestH2H1AddXfp(t *testing.T) { + opts := options{ + args: []string{"--no-strip-incoming-x-forwarded-proto"}, + handler: func(w http.ResponseWriter, r *http.Request) { + xfp := r.Header.Get("X-Forwarded-Proto") + if got, want := xfp, "foo, http"; got != want { + t.Errorf("X-Forwarded-Proto = %q; want %q", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1NoAddXfp tests that server does not append :scheme to the +// existing x-forwarded-proto header field. +func TestH2H1NoAddXfp(t *testing.T) { + opts := options{ + args: []string{ + "--no-add-x-forwarded-proto", + "--no-strip-incoming-x-forwarded-proto", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + xfp := r.Header.Get("X-Forwarded-Proto") + if got, want := xfp, "foo"; got != want { + t.Errorf("X-Forwarded-Proto = %q; want %q", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1NoAddXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1StripXfp tests that server strips incoming +// x-forwarded-proto header field. +func TestH2H1StripXfp(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + xfp := r.Header.Get("X-Forwarded-Proto") + if got, want := xfp, "http"; got != want { + t.Errorf("X-Forwarded-Proto = %q; want %q", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1StripXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1StripNoAddXfp tests that server strips incoming +// x-forwarded-proto header field, and does not add another. +func TestH2H1StripNoAddXfp(t *testing.T) { + opts := options{ + args: []string{"--no-add-x-forwarded-proto"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, found := r.Header["X-Forwarded-Proto"]; found { + t.Errorf("X-Forwarded-Proto = %q; want nothing", got) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1StripNoAddXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1AddXff tests that server generates X-Forwarded-For header +// field when forwarding request to backend. +func TestH2H1AddXff(t *testing.T) { + opts := options{ + args: []string{"--add-x-forwarded-for"}, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddXff", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1AddXff2 tests that server appends X-Forwarded-For header +// field to existing one when forwarding request to backend. +func TestH2H1AddXff2(t *testing.T) { + opts := options{ + args: []string{"--add-x-forwarded-for"}, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddXff2", + header: []hpack.HeaderField{ + pair("x-forwarded-for", "host"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1StripXff tests that --strip-incoming-x-forwarded-for +// option. +func TestH2H1StripXff(t *testing.T) { + opts := options{ + args: []string{"--strip-incoming-x-forwarded-for"}, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1StripXff", + header: []hpack.HeaderField{ + pair("x-forwarded-for", "host"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1StripAddXff tests that --strip-incoming-x-forwarded-for and +// --add-x-forwarded-for options. +func TestH2H1StripAddXff(t *testing.T) { + opts := options{ + args: []string{ + "--strip-incoming-x-forwarded-for", + "--add-x-forwarded-for", + }, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1StripAddXff", + header: []hpack.HeaderField{ + pair("x-forwarded-for", "host"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1AddForwardedObfuscated tests that server generates +// Forwarded header field with obfuscated "by" and "for" parameters. +func TestH2H1AddForwardedObfuscated(t *testing.T) { + opts := options{ + args: []string{"--add-forwarded=by,for,host,proto"}, + handler: func(w http.ResponseWriter, r *http.Request) { + pattern := fmt.Sprintf(`by=_[^;]+;for=_[^;]+;host="127\.0\.0\.1:%v";proto=http`, serverPort) + validFwd := regexp.MustCompile(pattern) + got := r.Header.Get("Forwarded") + + if !validFwd.MatchString(got) { + t.Errorf("Forwarded = %v; want pattern %v", got, pattern) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddForwardedObfuscated", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1AddForwardedByIP tests that server generates Forwarded header +// field with IP address in "by" parameter. +func TestH2H1AddForwardedByIP(t *testing.T) { + opts := options{ + args: []string{"--add-forwarded=by,for", "--forwarded-by=ip"}, + handler: func(w http.ResponseWriter, r *http.Request) { + pattern := fmt.Sprintf(`by="127\.0\.0\.1:%v";for=_[^;]+`, serverPort) + validFwd := regexp.MustCompile(pattern) + if got := r.Header.Get("Forwarded"); !validFwd.MatchString(got) { + t.Errorf("Forwarded = %v; want pattern %v", got, pattern) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddForwardedByIP", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1AddForwardedForIP tests that server generates Forwarded header +// field with IP address in "for" parameters. +func TestH2H1AddForwardedForIP(t *testing.T) { + opts := options{ + args: []string{ + "--add-forwarded=by,for,host,proto", + "--forwarded-by=_alpha", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + want := fmt.Sprintf(`by=_alpha;for=127.0.0.1;host="127.0.0.1:%v";proto=http`, serverPort) + if got := r.Header.Get("Forwarded"); got != want { + t.Errorf("Forwarded = %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddForwardedForIP", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1AddForwardedMerge tests that server generates Forwarded +// header field with IP address in "by" and "for" parameters. The +// generated values must be appended to the existing value. +func TestH2H1AddForwardedMerge(t *testing.T) { + opts := options{ + args: []string{"--add-forwarded=proto"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Forwarded"), `host=foo, proto=http`; got != want { + t.Errorf("Forwarded = %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddForwardedMerge", + header: []hpack.HeaderField{ + pair("forwarded", "host=foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1AddForwardedStrip tests that server generates Forwarded +// header field with IP address in "by" and "for" parameters. The +// generated values must not include the existing value. +func TestH2H1AddForwardedStrip(t *testing.T) { + opts := options{ + args: []string{ + "--strip-incoming-forwarded", + "--add-forwarded=proto", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Forwarded"), `proto=http`; got != want { + t.Errorf("Forwarded = %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddForwardedStrip", + header: []hpack.HeaderField{ + pair("forwarded", "host=foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1StripForwarded tests that server strips incoming Forwarded +// header field. +func TestH2H1StripForwarded(t *testing.T) { + opts := options{ + args: []string{"--strip-incoming-forwarded"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, found := r.Header["Forwarded"]; found { + t.Errorf("Forwarded = %v; want nothing", got) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1StripForwarded", + header: []hpack.HeaderField{ + pair("forwarded", "host=foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1AddForwardedStatic tests that server generates Forwarded +// header field with the given static obfuscated string for "by" +// parameter. +func TestH2H1AddForwardedStatic(t *testing.T) { + opts := options{ + args: []string{ + "--add-forwarded=by,for", + "--forwarded-by=_alpha", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + pattern := `by=_alpha;for=_[^;]+` + validFwd := regexp.MustCompile(pattern) + if got := r.Header.Get("Forwarded"); !validFwd.MatchString(got) { + t.Errorf("Forwarded = %v; want pattern %v", got, pattern) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AddForwardedStatic", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1GenerateVia tests that server generates Via header field to and +// from backend server. +func TestH2H1GenerateVia(t *testing.T) { + opts := options{ + handler: 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) + } + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + handler: 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") + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + args: []string{"--no-via"}, + handler: 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") + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + args: []string{"--host-rewrite"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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(t, options{}) + 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) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + // we set content-length: 1024, but only send 3 bytes. + w.Header().Add("Content-Length", "1024") + if _, err := w.Write([]byte("foo")); err != nil { + t.Fatalf("Error w.Write() = %v", err) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1BadResponseCL", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + want := http2.ErrCodeInternal + 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) { + opts := options{ + handler: 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") + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + handler: 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 := io.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) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ChunkedRequestBody", + method: "POST", + body: []byte("foo"), + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1MultipleRequestCL tests that server rejects request with +// multiple Content-Length request header fields. +func TestH2H1MultipleRequestCL(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward bad request") + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward bad request") + }, + } + st := newServerTester(t, opts) + 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(t, options{}) +// 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) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + 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, http.StatusNotImplemented; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H1BadAuthority tests that server rejects request including +// bad characters in :authority header field. +func TestH2H1BadAuthority(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1BadAuthority", + authority: `foo\bar`, + }) + 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) + } +} + +// TestH2H1BadScheme tests that server rejects request including +// bad characters in :scheme header field. +func TestH2H1BadScheme(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1BadScheme", + scheme: "http*", + }) + 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) + } +} + +// 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) { + opts := options{ + handler: 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) + } + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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(t, options{}) + 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, http.StatusOK; 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) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + t.Error("server should not forward bad request") + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + args: []string{"--subcert=" + testDir + "/alt-server.key:" + testDir + "/alt-server.crt"}, + tls: true, + tlsConfig: &tls.Config{ + ServerName: "alt-domain", + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + handler: 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) + } + }, + tls: true, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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) { + opts := options{ + handler: 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", "</css/main.css>; rel=preload, </foo>, </css/theme.css>; rel=preload") + } + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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, http.StatusOK; got != want { + t.Errorf("mainCSS.status: %v; want %v", got, want) + } + themeCSS := res.pushResponse[1] + if got, want := themeCSS.status, http.StatusOK; 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) { + opts := options{ + handler: 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) + } + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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) { + opts := options{ + args: []string{"--request-header-field-buffer=10"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("execution path should not be here") + }, + } + st := newServerTester(t, opts) + 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, http.StatusRequestHeaderFieldsTooLarge; 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) { + opts := options{ + args: []string{"--max-request-header-fields=1"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("execution path should not be here") + }, + } + st := newServerTester(t, opts) + 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, http.StatusRequestHeaderFieldsTooLarge; 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) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-set-header.rb"}, + handler: 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) + } + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH2H1ReqPhaseReturn(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-return.rb"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + } + st := newServerTester(t, opts) + 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, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "20"}, + {"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 from req"; 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) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/resp-set-header.rb"}, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/resp-return.rb"}, + } + st := newServerTester(t, opts) + 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, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "21"}, + {"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 from resp"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH2H1Upgrade tests HTTP Upgrade to HTTP/2 +func TestH2H1Upgrade(t *testing.T) { + st := newServerTester(t, options{}) + 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, http.StatusSwitchingProtocols; 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, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1ForwardedForObfuscated tests that Forwarded +// header field includes obfuscated address even if PROXY protocol +// version 1 containing TCP4 entry is accepted. +func TestH2H1ProxyProtocolV1ForwardedForObfuscated(t *testing.T) { + pattern := `^for=_[^;]+$` + validFwd := regexp.MustCompile(pattern) + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=obfuscated", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Forwarded"); !validFwd.MatchString(got) { + t.Errorf("Forwarded: %v; want pattern %v", got, pattern) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP4 192.168.0.2 192.168.0.100 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1ForwardedForObfuscated", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1TCP4 tests PROXY protocol version 1 +// containing TCP4 entry is accepted and X-Forwarded-For contains +// advertised src address. +func TestH2H1ProxyProtocolV1TCP4(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "192.168.0.2"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=192.168.0.2"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP4 192.168.0.2 192.168.0.100 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TCP4", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1TCP6 tests PROXY protocol version 1 +// containing TCP6 entry is accepted and X-Forwarded-For contains +// advertised src address. +func TestH2H1ProxyProtocolV1TCP6(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), `for="[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"`; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 2001:0db8:85a3:0000:0000:8a2e:0370:7334 ::1 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TCP6", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1TCP4TLS tests PROXY protocol version 1 over +// TLS containing TCP4 entry is accepted and X-Forwarded-For contains +// advertised src address. +func TestH2H1ProxyProtocolV1TCP4TLS(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "192.168.0.2"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=192.168.0.2"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + tls: true, + tcpData: []byte("PROXY TCP4 192.168.0.2 192.168.0.100 12345 8080\r\n"), + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TCP4TLS", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1TCP6TLS tests PROXY protocol version 1 over +// TLS containing TCP6 entry is accepted and X-Forwarded-For contains +// advertised src address. +func TestH2H1ProxyProtocolV1TCP6TLS(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), `for="[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"`; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + tls: true, + tcpData: []byte("PROXY TCP6 2001:0db8:85a3:0000:0000:8a2e:0370:7334 ::1 12345 8080\r\n"), + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TCP6TLS", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1Unknown tests PROXY protocol version 1 +// containing UNKNOWN entry is accepted. +func TestH2H1ProxyProtocolV1Unknown(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, notWant := r.Header.Get("X-Forwarded-For"), "192.168.0.2"; got == notWant { + t.Errorf("X-Forwarded-For: %v; want something else", got) + } + if got, notWant := r.Header.Get("Forwarded"), "for=192.168.0.2"; got == notWant { + t.Errorf("Forwarded: %v; want something else", got) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY UNKNOWN 192.168.0.2 192.168.0.100 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1Unknown", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1JustUnknown tests PROXY protocol version 1 +// containing only "PROXY UNKNOWN" is accepted. +func TestH2H1ProxyProtocolV1JustUnknown(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY UNKNOWN\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1JustUnknown", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV1TooLongLine tests PROXY protocol version 1 +// line longer than 107 bytes must be rejected +func TestH2H1ProxyProtocolV1TooLongLine(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + }, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY UNKNOWN ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 655350\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TooLongLine", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1BadLineEnd tests that PROXY protocol version +// 1 line ending without \r\n should be rejected. +func TestH2H1ProxyProtocolV1BadLineEnd(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 12345 8080\r \n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1BadLineEnd", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1NoEnd tests that PROXY protocol version 1 +// line containing no \r\n should be rejected. +func TestH2H1ProxyProtocolV1NoEnd(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 12345 8080")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1NoEnd", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1EmbeddedNULL tests that PROXY protocol +// version 1 line containing NULL character should be rejected. +func TestH2H1ProxyProtocolV1EmbeddedNULL(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + b := []byte("PROXY TCP6 ::1*foo ::1 12345 8080\r\n") + b[14] = 0 + if _, err := st.conn.Write(b); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1EmbeddedNULL", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1MissingSrcPort tests that PROXY protocol +// version 1 line without src port should be rejected. +func TestH2H1ProxyProtocolV1MissingSrcPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1MissingSrcPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1MissingDstPort tests that PROXY protocol +// version 1 line without dst port should be rejected. +func TestH2H1ProxyProtocolV1MissingDstPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 12345 \r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1MissingDstPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1InvalidSrcPort tests that PROXY protocol +// containing invalid src port should be rejected. +func TestH2H1ProxyProtocolV1InvalidSrcPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 123x 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1InvalidSrcPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1InvalidDstPort tests that PROXY protocol +// containing invalid dst port should be rejected. +func TestH2H1ProxyProtocolV1InvalidDstPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 123456 80x\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1InvalidDstPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1LeadingZeroPort tests that PROXY protocol +// version 1 line with non zero port with leading zero should be +// rejected. +func TestH2H1ProxyProtocolV1LeadingZeroPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 03000 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1LeadingZeroPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1TooLargeSrcPort tests that PROXY protocol +// containing too large src port should be rejected. +func TestH2H1ProxyProtocolV1TooLargeSrcPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 65536 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TooLargeSrcPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1TooLargeDstPort tests that PROXY protocol +// containing too large dst port should be rejected. +func TestH2H1ProxyProtocolV1TooLargeDstPort(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 ::1 12345 65536\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1TooLargeDstPort", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1InvalidSrcAddr tests that PROXY protocol +// containing invalid src addr should be rejected. +func TestH2H1ProxyProtocolV1InvalidSrcAddr(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 192.168.0.1 ::1 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1InvalidSrcAddr", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1InvalidDstAddr tests that PROXY protocol +// containing invalid dst addr should be rejected. +func TestH2H1ProxyProtocolV1InvalidDstAddr(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY TCP6 ::1 192.168.0.1 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1InvalidDstAddr", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1InvalidProtoFamily tests that PROXY protocol +// containing invalid protocol family should be rejected. +func TestH2H1ProxyProtocolV1InvalidProtoFamily(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PROXY UNIX ::1 ::1 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1InvalidProtoFamily", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV1InvalidID tests that PROXY protocol +// containing invalid PROXY protocol version 1 ID should be rejected. +func TestH2H1ProxyProtocolV1InvalidID(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + if _, err := st.conn.Write([]byte("PR0XY TCP6 ::1 ::1 12345 8080\r\n")); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV1InvalidID", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV2TCP4 tests PROXY protocol version 2 +// containing AF_INET family is accepted and X-Forwarded-For contains +// advertised src address. +func TestH2H1ProxyProtocolV2TCP4(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "192.168.0.2"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=192.168.0.2"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + var b bytes.Buffer + if err := writeProxyProtocolV2(&b, proxyProtocolV2{ + command: proxyProtocolV2CommandProxy, + sourceAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.2").To4(), + Port: 12345, + }, + destinationAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.100").To4(), + Port: 8080, + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + if _, err := st.conn.Write(b.Bytes()); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2TCP4", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV2TCP6 tests PROXY protocol version 2 +// containing AF_INET6 family is accepted and X-Forwarded-For contains +// advertised src address. +func TestH2H1ProxyProtocolV2TCP6(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "2001:db8:85a3::8a2e:370:7334"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), `for="[2001:db8:85a3::8a2e:370:7334]"`; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + var b bytes.Buffer + if err := writeProxyProtocolV2(&b, proxyProtocolV2{ + command: proxyProtocolV2CommandProxy, + sourceAddress: &net.TCPAddr{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Port: 12345, + }, + destinationAddress: &net.TCPAddr{ + IP: net.ParseIP("::1"), + Port: 8080, + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + if _, err := st.conn.Write(b.Bytes()); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2TCP6", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV2TCP4TLS tests PROXY protocol version 2 over +// TLS containing AF_INET family is accepted and X-Forwarded-For +// contains advertised src address. +func TestH2H1ProxyProtocolV2TCP4TLS(t *testing.T) { + var v2Hdr bytes.Buffer + if err := writeProxyProtocolV2(&v2Hdr, proxyProtocolV2{ + command: proxyProtocolV2CommandProxy, + sourceAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.2").To4(), + Port: 12345, + }, + destinationAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.100").To4(), + Port: 8080, + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "192.168.0.2"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=192.168.0.2"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + tls: true, + tcpData: v2Hdr.Bytes(), + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2TCP4TLS", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV2TCP6TLS tests PROXY protocol version 2 over +// TLS containing AF_INET6 family is accepted and X-Forwarded-For +// contains advertised src address. +func TestH2H1ProxyProtocolV2TCP6TLS(t *testing.T) { + var v2Hdr bytes.Buffer + if err := writeProxyProtocolV2(&v2Hdr, proxyProtocolV2{ + command: proxyProtocolV2CommandProxy, + sourceAddress: &net.TCPAddr{ + IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Port: 12345, + }, + destinationAddress: &net.TCPAddr{ + IP: net.ParseIP("::1"), + Port: 8080, + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "2001:db8:85a3::8a2e:370:7334"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), `for="[2001:db8:85a3::8a2e:370:7334]"`; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + tls: true, + tcpData: v2Hdr.Bytes(), + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2TCP6TLS", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV2Local tests PROXY protocol version 2 +// containing cmd == Local is ignored. +func TestH2H1ProxyProtocolV2Local(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "127.0.0.1"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=127.0.0.1"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + var b bytes.Buffer + if err := writeProxyProtocolV2(&b, proxyProtocolV2{ + command: proxyProtocolV2CommandLocal, + sourceAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.2").To4(), + Port: 12345, + }, + destinationAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.100").To4(), + Port: 8080, + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + if _, err := st.conn.Write(b.Bytes()); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2Local", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV2UnknownCmd tests PROXY protocol version 2 +// containing unknown cmd should be rejected. +func TestH2H1ProxyProtocolV2UnknownCmd(t *testing.T) { + opts := options{ + args: []string{"--accept-proxy-protocol"}, + } + st := newServerTester(t, opts) + defer st.Close() + + var b bytes.Buffer + if err := writeProxyProtocolV2(&b, proxyProtocolV2{ + command: 0xf, + sourceAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.2").To4(), + Port: 12345, + }, + destinationAddress: &net.TCPAddr{ + IP: net.ParseIP("192.168.0.100").To4(), + Port: 8080, + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + if _, err := st.conn.Write(b.Bytes()); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + _, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2UnknownCmd", + }) + + if err == nil { + t.Fatalf("connection was not terminated") + } +} + +// TestH2H1ProxyProtocolV2Unix tests PROXY protocol version 2 +// containing AF_UNIX family is ignored. +func TestH2H1ProxyProtocolV2Unix(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "127.0.0.1"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=127.0.0.1"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + var b bytes.Buffer + if err := writeProxyProtocolV2(&b, proxyProtocolV2{ + command: proxyProtocolV2CommandProxy, + sourceAddress: &net.UnixAddr{ + Name: "/foo", + Net: "unix", + }, + destinationAddress: &net.UnixAddr{ + Name: "/bar", + Net: "unix", + }, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + if _, err := st.conn.Write(b.Bytes()); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2Unix", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ProxyProtocolV2Unspec tests PROXY protocol version 2 +// containing AF_UNSPEC family is ignored. +func TestH2H1ProxyProtocolV2Unspec(t *testing.T) { + opts := options{ + args: []string{ + "--accept-proxy-protocol", + "--add-x-forwarded-for", + "--add-forwarded=for", + "--forwarded-for=ip", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("X-Forwarded-For"), "127.0.0.1"; got != want { + t.Errorf("X-Forwarded-For: %v; want %v", got, want) + } + if got, want := r.Header.Get("Forwarded"), "for=127.0.0.1"; got != want { + t.Errorf("Forwarded: %v; want %v", got, want) + } + }, + } + st := newServerTester(t, opts) + defer st.Close() + + var b bytes.Buffer + if err := writeProxyProtocolV2(&b, proxyProtocolV2{ + command: proxyProtocolV2CommandProxy, + additionalData: []byte("foobar"), + }); err != nil { + t.Fatalf("Error writeProxyProtocolV2() = %v", err) + } + + if _, err := st.conn.Write(b.Bytes()); err != nil { + t.Fatalf("Error st.conn.Write() = %v", err) + } + + res, err := st.http2(requestParam{ + name: "TestH2H1ProxyProtocolV2Unspec", + }) + + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ExternalDNS tests that DNS resolution using external DNS +// with HTTP/1 backend works. +func TestH2H1ExternalDNS(t *testing.T) { + opts := options{ + args: []string{"--external-dns"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ExternalDNS", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1DNS tests that DNS resolution without external DNS with +// HTTP/1 backend works. +func TestH2H1DNS(t *testing.T) { + opts := options{ + args: []string{"--dns"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1DNS", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1HTTPSRedirect tests that the request to the backend which +// requires TLS is redirected to https URI. +func TestH2H1HTTPSRedirect(t *testing.T) { + opts := options{ + args: []string{"--redirect-if-not-tls"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1HTTPSRedirect", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusPermanentRedirect; got != want { + t.Errorf("status = %v; want %v", got, want) + } + if got, want := res.header.Get("location"), "https://127.0.0.1/"; got != want { + t.Errorf("location: %v; want %v", got, want) + } +} + +// TestH2H1HTTPSRedirectPort tests that the request to the backend +// which requires TLS is redirected to https URI with given port. +func TestH2H1HTTPSRedirectPort(t *testing.T) { + opts := options{ + args: []string{ + "--redirect-if-not-tls", + "--redirect-https-port=8443", + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + path: "/foo?bar", + name: "TestH2H1HTTPSRedirectPort", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusPermanentRedirect; got != want { + t.Errorf("status = %v; want %v", got, want) + } + if got, want := res.header.Get("location"), "https://127.0.0.1:8443/foo?bar"; got != want { + t.Errorf("location: %v; want %v", got, want) + } +} + +// TestH2H1Code204 tests that 204 response without content-length, and +// transfer-encoding is valid. +func TestH2H1Code204(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1Code204", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusNoContent; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1Code204CL0 tests that 204 response with content-length: 0 +// is allowed. +func TestH2H1Code204CL0(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Could not hijack the connection", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + if _, err := bufrw.WriteString("HTTP/1.1 204\r\nContent-Length: 0\r\n\r\n"); err != nil { + t.Fatalf("Error bufrw.WriteString() = %v", err) + } + bufrw.Flush() + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1Code204CL0", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusNoContent; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + if got, found := res.header["Content-Length"]; found { + t.Errorf("Content-Length = %v, want nothing", got) + } +} + +// TestH2H1Code204CLNonzero tests that 204 response with nonzero +// content-length is not allowed. +func TestH2H1Code204CLNonzero(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Could not hijack the connection", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + if _, err := bufrw.WriteString("HTTP/1.1 204\r\nContent-Length: 1\r\n\r\n"); err != nil { + t.Fatalf("Error bufrw.WriteString() = %v", err) + } + bufrw.Flush() + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1Code204CLNonzero", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusBadGateway; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1Code204TE tests that 204 response with transfer-encoding is +// not allowed. +func TestH2H1Code204TE(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Could not hijack the connection", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + if _, err := bufrw.WriteString("HTTP/1.1 204\r\nTransfer-Encoding: chunked\r\n\r\n"); err != nil { + t.Fatalf("Error bufrw.WriteString() = %v", err) + } + bufrw.Flush() + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1Code204TE", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusBadGateway; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1AffinityCookie tests that affinity cookie is sent back in +// cleartext http. +func TestH2H1AffinityCookie(t *testing.T) { + opts := options{ + args: []string{"--affinity-cookie"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AffinityCookie", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar` + validCookie := regexp.MustCompile(pattern) + if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) { + t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern) + } +} + +// TestH2H1AffinityCookieTLS tests that affinity cookie is sent back +// in https. +func TestH2H1AffinityCookieTLS(t *testing.T) { + opts := options{ + args: []string{"--affinity-cookie"}, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1AffinityCookieTLS", + scheme: "https", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar; Secure` + validCookie := regexp.MustCompile(pattern) + if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) { + t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern) + } +} + +// TestH2H1GracefulShutdown tests graceful shutdown. +func TestH2H1GracefulShutdown(t *testing.T) { + st := newServerTester(t, options{}) + 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 + if err := st.cmd.Process.Signal(syscall.SIGQUIT); err != nil { + t.Fatalf("Error st.cmd.Process.Signal() = %v", err) + } + + time.Sleep(150 * time.Millisecond) + + // 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++ + 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) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("content-length", "1") + w.Header().Add("content-length", "1") + }, + } + st := newServerTester(t, opts) + 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) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("content-length", "") + }, + } + st := newServerTester(t, opts) + 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) { +// opts := options{ +// args: []string{"--http2-bridge"}, +// } +// st := newServerTester(t, opts) +// 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) { + opts := options{ + args: []string{"--http2-bridge", "--host-rewrite"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("request-host", r.Host) + }, + } + st := newServerTester(t, opts) + 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, http.StatusOK; 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) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: 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) + } + }, + tls: true, + } + st := newServerTester(t, opts) + 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, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H2AddXfp tests that server appends :scheme to the existing +// x-forwarded-proto header field. +func TestH2H2AddXfp(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--no-strip-incoming-x-forwarded-proto", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + xfp := r.Header.Get("X-Forwarded-Proto") + if got, want := xfp, "foo, http"; got != want { + t.Errorf("X-Forwarded-Proto = %q; want %q", got, want) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2AddXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2NoAddXfp tests that server does not append :scheme to the +// existing x-forwarded-proto header field. +func TestH2H2NoAddXfp(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--no-add-x-forwarded-proto", + "--no-strip-incoming-x-forwarded-proto", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + xfp := r.Header.Get("X-Forwarded-Proto") + if got, want := xfp, "foo"; got != want { + t.Errorf("X-Forwarded-Proto = %q; want %q", got, want) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2NoAddXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2StripXfp tests that server strips incoming +// x-forwarded-proto header field. +func TestH2H2StripXfp(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + xfp := r.Header.Get("X-Forwarded-Proto") + if got, want := xfp, "http"; got != want { + t.Errorf("X-Forwarded-Proto = %q; want %q", got, want) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2StripXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2StripNoAddXfp tests that server strips incoming +// x-forwarded-proto header field, and does not add another. +func TestH2H2StripNoAddXfp(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--no-add-x-forwarded-proto"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, found := r.Header["X-Forwarded-Proto"]; found { + t.Errorf("X-Forwarded-Proto = %q; want nothing", got) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2StripNoAddXfp", + header: []hpack.HeaderField{ + pair("x-forwarded-proto", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2AddXff tests that server generates X-Forwarded-For header +// field when forwarding request to backend. +func TestH2H2AddXff(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--add-x-forwarded-for"}, + handler: 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) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2AddXff", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2AddXff2 tests that server appends X-Forwarded-For header +// field to existing one when forwarding request to backend. +func TestH2H2AddXff2(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--add-x-forwarded-for"}, + handler: 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) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2AddXff2", + header: []hpack.HeaderField{ + pair("x-forwarded-for", "host"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2StripXff tests that --strip-incoming-x-forwarded-for +// option. +func TestH2H2StripXff(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--strip-incoming-x-forwarded-for", + }, + handler: 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) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2StripXff", + header: []hpack.HeaderField{ + pair("x-forwarded-for", "host"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2StripAddXff tests that --strip-incoming-x-forwarded-for and +// --add-x-forwarded-for options. +func TestH2H2StripAddXff(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--strip-incoming-x-forwarded-for", + "--add-x-forwarded-for", + }, + handler: 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) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2StripAddXff", + header: []hpack.HeaderField{ + pair("x-forwarded-for", "host"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2AddForwarded tests that server generates Forwarded header +// field using static obfuscated "by" parameter. +func TestH2H2AddForwarded(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--add-forwarded=by,for,host,proto", + "--forwarded-by=_alpha", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + pattern := fmt.Sprintf(`by=_alpha;for=_[^;]+;host="127\.0\.0\.1:%v";proto=https`, serverPort) + validFwd := regexp.MustCompile(pattern) + if got := r.Header.Get("Forwarded"); !validFwd.MatchString(got) { + t.Errorf("Forwarded = %v; want pattern %v", got, pattern) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2AddForwarded", + scheme: "https", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H2AddForwardedMerge tests that server generates Forwarded +// header field using static obfuscated "by" parameter, and +// existing Forwarded header field. +func TestH2H2AddForwardedMerge(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--add-forwarded=by,host,proto", + "--forwarded-by=_alpha", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + want := fmt.Sprintf(`host=foo, by=_alpha;host="127.0.0.1:%v";proto=https`, serverPort) + if got := r.Header.Get("Forwarded"); got != want { + t.Errorf("Forwarded = %v; want %v", got, want) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2AddForwardedMerge", + scheme: "https", + header: []hpack.HeaderField{ + pair("forwarded", "host=foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H2AddForwardedStrip tests that server generates Forwarded +// header field using static obfuscated "by" parameter, and +// existing Forwarded header field stripped. +func TestH2H2AddForwardedStrip(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--strip-incoming-forwarded", + "--add-forwarded=by,host,proto", + "--forwarded-by=_alpha", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + want := fmt.Sprintf(`by=_alpha;host="127.0.0.1:%v";proto=https`, serverPort) + if got := r.Header.Get("Forwarded"); got != want { + t.Errorf("Forwarded = %v; want %v", got, want) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2AddForwardedStrip", + scheme: "https", + header: []hpack.HeaderField{ + pair("forwarded", "host=foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H2StripForwarded tests that server strips incoming Forwarded +// header field. +func TestH2H2StripForwarded(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--strip-incoming-forwarded"}, + handler: func(w http.ResponseWriter, r *http.Request) { + if got, found := r.Header["Forwarded"]; found { + t.Errorf("Forwarded = %v; want nothing", got) + } + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2StripForwarded", + scheme: "https", + header: []hpack.HeaderField{ + pair("forwarded", "host=foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status: %v; want %v", got, want) + } +} + +// TestH2H2ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH2H2ReqPhaseReturn(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--mruby-file=" + testDir + "/req-return.rb", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + } + st := newServerTester(t, opts) + 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, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "20"}, + {"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 from req"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH2H2RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH2H2RespPhaseReturn(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--mruby-file=" + testDir + "/resp-return.rb", + }, + } + st := newServerTester(t, opts) + 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, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "21"}, + {"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 from resp"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH2H2ExternalDNS tests that DNS resolution using external DNS +// with HTTP/2 backend works. +func TestH2H2ExternalDNS(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--external-dns"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2ExternalDNS", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2DNS tests that DNS resolution without external DNS with +// HTTP/2 backend works. +func TestH2H2DNS(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge", "--dns"}, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2DNS", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H2Code204 tests that 204 response without content-length, and +// transfer-encoding is valid. +func TestH2H2Code204(t *testing.T) { + opts := options{ + args: []string{"--http2-bridge"}, + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2Code204", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusNoContent; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2APIBackendconfig exercise backendconfig API endpoint routine +// for successful case. +func TestH2APIBackendconfig(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2APIBackendconfig", + path: "/api/v1beta1/backendconfig", + method: "PUT", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Success"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 200; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH2APIBackendconfigQuery exercise backendconfig API endpoint +// routine with query. +func TestH2APIBackendconfigQuery(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2APIBackendconfigQuery", + path: "/api/v1beta1/backendconfig?foo=bar", + method: "PUT", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Success"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 200; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH2APIBackendconfigBadMethod exercise backendconfig API endpoint +// routine with bad method. +func TestH2APIBackendconfigBadMethod(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2APIBackendconfigBadMethod", + path: "/api/v1beta1/backendconfig", + method: "GET", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusMethodNotAllowed; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Failure"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 405; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH2APIConfigrevision tests configrevision API. +func TestH2APIConfigrevision(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2APIConfigrevision", + path: "/api/v1beta1/configrevision", + method: "GET", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want = %v", got, want) + } + + var apiResp APIResponse + d := json.NewDecoder(bytes.NewBuffer(res.body)) + d.UseNumber() + err = d.Decode(&apiResp) + if err != nil { + t.Fatalf("Error unmarshalling API response: %v", err) + } + if got, want := apiResp.Status, "Success"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 200; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Data["configRevision"], json.Number("0"); got != want { + t.Errorf(`apiResp.Data["configRevision"]: %v %t; want %v`, got, got, want) + } +} + +// TestH2APINotFound exercise backendconfig API endpoint routine when +// API endpoint is not found. +func TestH2APINotFound(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3010;api;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3010, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2APINotFound", + path: "/api/notfound", + method: "GET", + body: []byte(`# comment +backend=127.0.0.1,3011 + +`), + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } + + var apiResp APIResponse + err = json.Unmarshal(res.body, &apiResp) + if err != nil { + t.Fatalf("Error unmarshaling API response: %v", err) + } + if got, want := apiResp.Status, "Failure"; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } + if got, want := apiResp.Code, 404; got != want { + t.Errorf("apiResp.Status: %v; want %v", got, want) + } +} + +// TestH2Healthmon tests health monitor endpoint. +func TestH2Healthmon(t *testing.T) { + opts := options{ + args: []string{"-f127.0.0.1,3011;healthmon;no-tls"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + connectPort: 3011, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2Healthmon", + path: "/alpha/bravo", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2ResponseBeforeRequestEnd tests the situation where response +// ends before request body finishes. +func TestH2ResponseBeforeRequestEnd(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-return.rb"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("request should not be forwarded") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2ResponseBeforeRequestEnd", + noEndStream: true, + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH2H1ChunkedEndsPrematurely tests that a stream is reset if the +// backend chunked encoded response ends prematurely. +func TestH2H1ChunkedEndsPrematurely(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Could not hijack the connection", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + if _, err := bufrw.WriteString("HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n"); err != nil { + t.Fatalf("Error bufrw.WriteString() = %v", err) + } + bufrw.Flush() + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ChunkedEndsPrematurely", + }) + 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) + } +} + +// TestH2H1RequireHTTPSchemeHTTPSWithoutEncryption verifies that https +// scheme in non-encrypted connection is treated as error. +func TestH2H1RequireHTTPSchemeHTTPSWithoutEncryption(t *testing.T) { + opts := options{ + args: []string{"--require-http-scheme"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1RequireHTTPSchemeHTTPSWithoutEncryption", + scheme: "https", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusBadRequest; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1RequireHTTPSchemeHTTPWithEncryption verifies that http +// scheme in encrypted connection is treated as error. +func TestH2H1RequireHTTPSchemeHTTPWithEncryption(t *testing.T) { + opts := options{ + args: []string{"--require-http-scheme"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1RequireHTTPSchemeHTTPWithEncryption", + scheme: "http", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusBadRequest; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1RequireHTTPSchemeUnknownSchemeWithoutEncryption verifies +// that unknown scheme in non-encrypted connection is treated as +// error. +func TestH2H1RequireHTTPSchemeUnknownSchemeWithoutEncryption(t *testing.T) { + opts := options{ + args: []string{"--require-http-scheme"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1RequireHTTPSchemeUnknownSchemeWithoutEncryption", + scheme: "unknown", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusBadRequest; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1RequireHTTPSchemeUnknownSchemeWithEncryption verifies that +// unknown scheme in encrypted connection is treated as error. +func TestH2H1RequireHTTPSchemeUnknownSchemeWithEncryption(t *testing.T) { + opts := options{ + args: []string{"--require-http-scheme"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Errorf("server should not forward this request") + }, + tls: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1RequireHTTPSchemeUnknownSchemeWithEncryption", + scheme: "unknown", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, http.StatusBadRequest; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} diff --git a/integration-tests/nghttpx_http3_test.go b/integration-tests/nghttpx_http3_test.go new file mode 100644 index 0000000..9ea85d7 --- /dev/null +++ b/integration-tests/nghttpx_http3_test.go @@ -0,0 +1,393 @@ +//go:build quic + +package nghttp2 + +import ( + "bytes" + "crypto/rand" + "io" + "net/http" + "regexp" + "testing" + + "golang.org/x/net/http2/hpack" +) + +// TestH3H1PlainGET tests whether simple HTTP/3 GET request works. +func TestH3H1PlainGET(t *testing.T) { + st := newServerTester(t, options{ + quic: true, + }) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1PlainGET", + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH3H1RequestBody tests HTTP/3 request with body works. +func TestH3H1RequestBody(t *testing.T) { + body := make([]byte, 3333) + _, err := rand.Read(body) + if err != nil { + t.Fatalf("Unable to create request body: %v", err) + } + + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + buf := make([]byte, 4096) + buflen := 0 + p := buf + + for { + if len(p) == 0 { + t.Fatal("Request body is too large") + } + + n, err := r.Body.Read(p) + + p = p[n:] + buflen += n + + if err != nil { + if err == io.EOF { + break + } + + t.Fatalf("r.Body.Read() = %v", err) + } + } + + buf = buf[:buflen] + + if got, want := buf, body; !bytes.Equal(got, want) { + t.Fatalf("buf = %v; want %v", got, want) + } + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1RequestBody", + body: body, + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH3H1GenerateVia tests that server generates Via header field to +// and from backend server. +func TestH3H1GenerateVia(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Via"), "3 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1GenerateVia", + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + if got, want := res.header.Get("Via"), "1.1 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } +} + +// TestH3H1AppendVia tests that server adds value to existing Via +// header field to and from backend server. +func TestH3H1AppendVia(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Via"), "foo, 3 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } + w.Header().Add("Via", "bar") + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1AppendVia", + header: []hpack.HeaderField{ + pair("via", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + if got, want := res.header.Get("Via"), "bar, 1.1 nghttpx"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } +} + +// TestH3H1NoVia tests that server does not add value to existing Via +// header field to and from backend server. +func TestH3H1NoVia(t *testing.T) { + opts := options{ + args: []string{"--no-via"}, + handler: 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") + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1NoVia", + header: []hpack.HeaderField{ + pair("via", "foo"), + }, + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + if got, want := res.header.Get("Via"), "bar"; got != want { + t.Errorf("Via: %v; want %v", got, want) + } +} + +// TestH3H1BadResponseCL tests that server returns error when +// content-length response header field value does not match its +// response body size. +func TestH3H1BadResponseCL(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + // we set content-length: 1024, but only send 3 bytes. + w.Header().Add("Content-Length", "1024") + if _, err := w.Write([]byte("foo")); err != nil { + t.Fatalf("Error w.Write() = %v", err) + } + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + _, err := st.http3(requestParam{ + name: "TestH3H1BadResponseCL", + }) + if err == nil { + t.Fatal("st.http3() should fail") + } +} + +// TestH3H1HTTPSRedirect tests that HTTPS redirect should not happen +// with HTTP/3. +func TestH3H1HTTPSRedirect(t *testing.T) { + opts := options{ + args: []string{"--redirect-if-not-tls"}, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1HTTPSRedirect", + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH3H1AffinityCookieTLS tests that affinity cookie is sent back +// in https. +func TestH3H1AffinityCookieTLS(t *testing.T) { + opts := options{ + args: []string{"--affinity-cookie"}, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H1AffinityCookieTLS", + scheme: "https", + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + + if got, want := res.status, http.StatusOK; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar; Secure` + validCookie := regexp.MustCompile(pattern) + if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) { + t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern) + } +} + +// TestH3H2ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH3H2ReqPhaseReturn(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--mruby-file=" + testDir + "/req-return.rb", + }, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H2ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "20"}, + {"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 from req"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH3H2RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH3H2RespPhaseReturn(t *testing.T) { + opts := options{ + args: []string{ + "--http2-bridge", + "--mruby-file=" + testDir + "/resp-return.rb", + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3H2RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "21"}, + {"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 from resp"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH3ResponseBeforeRequestEnd tests the situation where response +// ends before request body finishes. +func TestH3ResponseBeforeRequestEnd(t *testing.T) { + opts := options{ + args: []string{"--mruby-file=" + testDir + "/req-return.rb"}, + handler: func(w http.ResponseWriter, r *http.Request) { + t.Fatal("request should not be forwarded") + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + res, err := st.http3(requestParam{ + name: "TestH3ResponseBeforeRequestEnd", + noEndStream: true, + }) + if err != nil { + t.Fatalf("Error st.http3() = %v", err) + } + if got, want := res.status, http.StatusNotFound; got != want { + t.Errorf("res.status: %v; want %v", got, want) + } +} + +// TestH3H1ChunkedEndsPrematurely tests that a stream is reset if the +// backend chunked encoded response ends prematurely. +func TestH3H1ChunkedEndsPrematurely(t *testing.T) { + opts := options{ + handler: func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Could not hijack the connection", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + if _, err := bufrw.WriteString("HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n"); err != nil { + t.Fatalf("Error bufrw.WriteString() = %v", err) + } + bufrw.Flush() + }, + quic: true, + } + st := newServerTester(t, opts) + defer st.Close() + + _, err := st.http3(requestParam{ + name: "TestH3H1ChunkedEndsPrematurely", + }) + if err == nil { + t.Fatal("st.http3() should fail") + } +} diff --git a/integration-tests/req-return.rb b/integration-tests/req-return.rb new file mode 100644 index 0000000..51d315e --- /dev/null +++ b/integration-tests/req-return.rb @@ -0,0 +1,12 @@ +class App + def on_req(env) + resp = env.resp + + resp.clear_headers + resp.status = 404 + resp.add_header "from", "mruby" + resp.return "Hello World from req" + end +end + +App.new diff --git a/integration-tests/req-set-header.rb b/integration-tests/req-set-header.rb new file mode 100644 index 0000000..986f128 --- /dev/null +++ b/integration-tests/req-set-header.rb @@ -0,0 +1,7 @@ +class App + def on_req(env) + env.req.set_header "User-Agent", "mruby" + end +end + +App.new diff --git a/integration-tests/resp-return.rb b/integration-tests/resp-return.rb new file mode 100644 index 0000000..fbbd775 --- /dev/null +++ b/integration-tests/resp-return.rb @@ -0,0 +1,12 @@ +class App + def on_resp(env) + resp = env.resp + + resp.clear_headers + resp.status = 404 + resp.add_header "from", "mruby" + resp.return "Hello World from resp" + end +end + +App.new diff --git a/integration-tests/resp-set-header.rb b/integration-tests/resp-set-header.rb new file mode 100644 index 0000000..228837a --- /dev/null +++ b/integration-tests/resp-set-header.rb @@ -0,0 +1,7 @@ +class App + def on_resp(env) + env.resp.set_header "Alpha", "bravo" + end +end + +App.new diff --git a/integration-tests/server.crt b/integration-tests/server.crt new file mode 100644 index 0000000..c50fdaa --- /dev/null +++ b/integration-tests/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhTCCAm2gAwIBAgIJAOvIx8xIxgyOMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCTEyNy4wLjAuMTAeFw0xNTAxMjMxMjI0 +MjdaFw0yNTAxMjAxMjI0MjdaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l +LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV +BAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMuI +QZRI/iBaxPTjTWGemt8tCEfzZWxuIW3hY/gIhwJDfH2SbourBh1s9vqcqhBq5vmo +kdfVQXAnNLjIG1uhWmcHuNnKrE5hU82N6i9RsmuM5TQRvhsamHri4G+EXJMu9GqF +Mso8g7MWpRSGKf+8gfjAVNwfCHFiu8oBcMmy3l54MFHgRLSveAMhiPB0e3Xlnpr5 +2bS/oGTx5ynwPgBpEn2FrpT4Z/aLCLzJ/ysgNH8BXEh7n/v7xM3vd5grqB039rd5 +JoxlWvp+4XpzKp5upaqmOcVUq4pDSFUQ3w6C+v33Z3OK6Qaon7GMxLv3Us3b7PZ3 +1CLoWJR2o3OSnUfO/gUCAwEAAaNQME4wHQYDVR0OBBYEFLc5JWPUUVx4GJesogMV +w2Rz0L3yMB8GA1UdIwQYMBaAFLc5JWPUUVx4GJesogMVw2Rz0L3yMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAP/cJWpM+GEjmVYHFacKTdbXBMox2Xn +QY2NLm00WPOGvKnO7czMFfX/pEmiq71kD45rLLfbaJP205QpxqiAIvhFhuq50Co7 +sTDtwcDTPLX9H7Ugjt4sTMPiwC14uVXFfoT/J46zMjXwP00qKyfszc2tkIgHfrTl +h4M1hkdfmMximir/Ii7TdYYJ3oGS8tdcYb6D4DZwAljKmxF6iUOwFCUgpTmqDBT5 +irXY8D27DzuNN5Pg07rwAlwXLCzrJE10UtO4MmRVXwpzmoaRQD4/tna6bZzdetvs +gPdGP6W1o0q85gullieMJWeKyQA/wasoE7fypn4pHAdTZm/vH+v7GHg= +-----END CERTIFICATE----- diff --git a/integration-tests/server.key b/integration-tests/server.key new file mode 100644 index 0000000..0fc02bb --- /dev/null +++ b/integration-tests/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLiEGUSP4gWsT0 +401hnprfLQhH82VsbiFt4WP4CIcCQ3x9km6LqwYdbPb6nKoQaub5qJHX1UFwJzS4 +yBtboVpnB7jZyqxOYVPNjeovUbJrjOU0Eb4bGph64uBvhFyTLvRqhTLKPIOzFqUU +hin/vIH4wFTcHwhxYrvKAXDJst5eeDBR4ES0r3gDIYjwdHt15Z6a+dm0v6Bk8ecp +8D4AaRJ9ha6U+Gf2iwi8yf8rIDR/AVxIe5/7+8TN73eYK6gdN/a3eSaMZVr6fuF6 +cyqebqWqpjnFVKuKQ0hVEN8Ogvr992dziukGqJ+xjMS791LN2+z2d9Qi6FiUdqNz +kp1Hzv4FAgMBAAECggEACG26GYP0Ui6wHVwUZkiFLVzWDPS9bIIbDEvbMfhYbvWQ +gDrCLTKF7E4I5FP8jvV+XzRl5cRFE3nsKwLObzr9XWrqcsp73DsXl1mbKx58/ws0 +qrVZZBHz4pLmrHeUxduZ75dYhRuAcLgtWe48awTJdR2x5fO7C8cE89afbxrjLpJE +tVyiw6vVB0GfWTZodxtAFMTX1KVm4bTngXfg0NF1FBNHAX3Cm6t4YCE41hKSc0IQ +Jr3C4e9uj8poze1B17k79bGB8HNMbbc8Ws0sdbxi5xnY+HUA/mYQrmGXo8sdqiYC +EYCMqPm3iJrCmmpHukGf2Vt9k1aLlJ+lxOclSwFO+QKBgQDoRmoprfdmU20LyxYH +eVeVqggqmhNohwnuhIvOAyrWGUkbDsssqx2Vv82z0WHAAkwEvQ984UzaYWCCL3m3 ++JzpF2dz6aKhXIaYnXBlk3STMGUCDT5ysPvsin9z/unzkffh3vrbDBARGFYWG18x +eUyTDOVVeTZNHUJXGjRyiftCkwKBgQDgUkR6dHU4ciSt7Y0UkyAgtZ7POR41T05L +bcxbjJeqm6qlj+oP9WUk7JxeSEFUbrMiROABLPPqTwmGo4xrDRx/e7WrqN6QBKC+ +Y8CfalrKRb0np60x7Mxx0kbmHp5cwv9QDKznKViOYSgKxFrOFZyMAEXQdZ3FvjXF +OQWrw86kBwKBgQDXuxa9MWO3uUJtkqkaNfw/+FVvY/0kt09lJdxHci+l/IQmyl2w +Vhm7TRK7sXvtfvSl7gblgMgFiC2/nGKbmR/7ag5e3R98aVhlhMywuvyp/GfEORLI +KVNChfwMezVFUUx+j8BEFHcTuZuzGqcWZ0fUyER0V4k0pDlKdv9BZqBkWwKBgCdP +o3qGQCilMDJex/OMGPxCd9M+4kFbZZAobMC6cbXPU+dxwgYL7i67XGfVZ8WBJNlj +kpICK7irIzM6JBh6krzwlBTCIkbA2N6kopQNUl3SPOTfKKXwJp/nxs77HKuK7K09 +m2tjPoatFhRU9sjY1rdeMN3oTr7hp5CpfonsZaEvAoGAEPsZcDd4N9ap5bgaeDy9 +NOfLsIyaxT5k6moRIiy83QPihvCuECP16+r6M5tiSfgt/PtCimdjhRiqXzIHNRhh +Nfsv13vUtZgt8cYXuTdI4a8feKI7Q4876ME8Qp3WM5/UNZWq6/sWCuZFqbXUhqM0 +mwNEi5Zddzf8VsSL2gCraQg= +-----END PRIVATE KEY----- diff --git a/integration-tests/server_tester.go b/integration-tests/server_tester.go new file mode 100644 index 0000000..f98a04a --- /dev/null +++ b/integration-tests/server_tester.go @@ -0,0 +1,895 @@ +package nghttp2 + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/exec" + "sort" + "strconv" + "strings" + "syscall" + "testing" + "time" + + "github.com/lucas-clemente/quic-go/http3" + "github.com/tatsuhiro-t/go-nghttp2" + "golang.org/x/net/http2" + "golang.org/x/net/http2/hpack" + "golang.org/x/net/websocket" +) + +const ( + serverBin = buildDir + "/src/nghttpx" + serverPort = 3009 + testDir = sourceDir + "/integration-tests" + logDir = buildDir + "/integration-tests" +) + +func pair(name, value string) hpack.HeaderField { + return hpack.HeaderField{ + Name: name, + Value: value, + } +} + +type serverTester struct { + cmd *exec.Cmd // test frontend server process, which is test subject + url string // test frontend server URL + t *testing.T + ts *httptest.Server // backend server + frontendHost string // frontend server host + backendHost string // backend server host + conn net.Conn // connection to frontend server + h2PrefaceSent bool // HTTP/2 preface was sent in conn + nextStreamID uint32 // next stream ID + fr *http2.Framer // HTTP/2 framer + headerBlkBuf bytes.Buffer // buffer to store encoded header block + enc *hpack.Encoder // HTTP/2 HPACK encoder + header http.Header // received header fields + dec *hpack.Decoder // HTTP/2 HPACK decoder + authority string // server's host:port + frCh chan http2.Frame // used for incoming HTTP/2 frame + errCh chan error +} + +type options struct { + // args is the additional arguments to nghttpx. + args []string + // handler is the handler to handle the request. It defaults + // to noopHandler. + handler http.HandlerFunc + // connectPort is the server side port where client connection + // is made. It defaults to serverPort. + connectPort int + // tls, if set to true, sets up TLS frontend connection. + tls bool + // tlsConfig is the client side TLS configuration that is used + // when tls is true. + tlsConfig *tls.Config + // tcpData is additional data that are written to connection + // before TLS handshake starts. This field is ignored if tls + // is false. + tcpData []byte + // quic, if set to true, sets up QUIC frontend connection. + // quic implies tls = true. + quic bool +} + +// newServerTester creates test context. +func newServerTester(t *testing.T, opts options) *serverTester { + if opts.quic { + opts.tls = true + } + + if opts.handler == nil { + opts.handler = noopHandler + } + if opts.connectPort == 0 { + opts.connectPort = serverPort + } + + ts := httptest.NewUnstartedServer(opts.handler) + + var args []string + var backendTLS, dns, externalDNS, acceptProxyProtocol, redirectIfNotTLS, affinityCookie, alpnH1 bool + + for _, k := range opts.args { + switch k { + case "--http2-bridge": + backendTLS = true + case "--dns": + dns = true + case "--external-dns": + dns = true + externalDNS = true + case "--accept-proxy-protocol": + acceptProxyProtocol = true + case "--redirect-if-not-tls": + redirectIfNotTLS = true + case "--affinity-cookie": + affinityCookie = true + case "--alpn-h1": + alpnH1 = true + default: + args = append(args, k) + } + } + if backendTLS { + nghttp2.ConfigureServer(ts.Config, &nghttp2.Server{}) + // According to httptest/server.go, we have to set + // NextProtos separately for ts.TLS. NextProtos set + // in nghttp2.ConfigureServer is effectively ignored. + ts.TLS = new(tls.Config) + ts.TLS.NextProtos = append(ts.TLS.NextProtos, "h2") + ts.StartTLS() + args = append(args, "-k") + } else { + ts.Start() + } + scheme := "http" + if opts.tls { + scheme = "https" + args = append(args, testDir+"/server.key", testDir+"/server.crt") + } + + backendURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("Error parsing URL from httptest.Server: %v", err) + } + + // URL.Host looks like "127.0.0.1:8080", but we want + // "127.0.0.1,8080" + b := "-b" + if !externalDNS { + b += fmt.Sprintf("%v;", strings.Replace(backendURL.Host, ":", ",", -1)) + } else { + sep := strings.LastIndex(backendURL.Host, ":") + if sep == -1 { + t.Fatalf("backendURL.Host %v does not contain separator ':'", backendURL.Host) + } + // We use awesome service nip.io. + b += fmt.Sprintf("%v.nip.io,%v;", backendURL.Host[:sep], backendURL.Host[sep+1:]) + } + + if backendTLS { + b += ";proto=h2;tls" + } + if dns { + b += ";dns" + } + + if redirectIfNotTLS { + b += ";redirect-if-not-tls" + } + + if affinityCookie { + b += ";affinity=cookie;affinity-cookie-name=affinity;affinity-cookie-path=/foo/bar" + } + + noTLS := ";no-tls" + if opts.tls { + noTLS = "" + } + + var proxyProto string + if acceptProxyProtocol { + proxyProto = ";proxyproto" + } + + args = append(args, fmt.Sprintf("-f127.0.0.1,%v%v%v", serverPort, noTLS, proxyProto), b, + "--errorlog-file="+logDir+"/log.txt", "-LINFO") + + if opts.quic { + args = append(args, + fmt.Sprintf("-f127.0.0.1,%v;quic", serverPort), + "--no-quic-bpf") + } + + authority := fmt.Sprintf("127.0.0.1:%v", opts.connectPort) + + st := &serverTester{ + cmd: exec.Command(serverBin, args...), + t: t, + ts: ts, + url: fmt.Sprintf("%v://%v", scheme, authority), + frontendHost: fmt.Sprintf("127.0.0.1:%v", serverPort), + backendHost: backendURL.Host, + nextStreamID: 1, + authority: authority, + frCh: make(chan http2.Frame), + errCh: make(chan error), + } + + st.cmd.Stdout = os.Stdout + st.cmd.Stderr = os.Stderr + + if err := st.cmd.Start(); err != nil { + st.t.Fatalf("Error starting %v: %v", serverBin, err) + } + + retry := 0 + for { + time.Sleep(50 * time.Millisecond) + + conn, err := net.Dial("tcp", authority) + if err == nil && opts.tls { + if len(opts.tcpData) > 0 { + if _, err := conn.Write(opts.tcpData); err != nil { + st.Close() + st.t.Fatal("Error writing TCP data") + } + } + + var tlsConfig *tls.Config + if opts.tlsConfig == nil { + tlsConfig = new(tls.Config) + } else { + tlsConfig = opts.tlsConfig.Clone() + } + tlsConfig.InsecureSkipVerify = true + if alpnH1 { + tlsConfig.NextProtos = []string{"http/1.1"} + } else { + tlsConfig.NextProtos = []string{"h2"} + } + tlsConn := tls.Client(conn, tlsConfig) + err = tlsConn.Handshake() + if err == nil { + conn = tlsConn + } + } + if err != nil { + retry++ + if retry >= 100 { + st.Close() + st.t.Fatalf("Error server is not responding too long; server command-line arguments may be invalid") + } + continue + } + st.conn = conn + break + } + + st.fr = http2.NewFramer(st.conn, st.conn) + st.enc = hpack.NewEncoder(&st.headerBlkBuf) + st.dec = hpack.NewDecoder(4096, func(f hpack.HeaderField) { + st.header.Add(f.Name, f.Value) + }) + + return st +} + +func (st *serverTester) Close() { + if st.conn != nil { + st.conn.Close() + } + if st.cmd != nil { + done := make(chan struct{}) + go func() { + if err := st.cmd.Wait(); err != nil { + st.t.Errorf("Error st.cmd.Wait() = %v", err) + } + close(done) + }() + + if err := st.cmd.Process.Signal(syscall.SIGQUIT); err != nil { + st.t.Errorf("Error st.cmd.Process.Signal() = %v", err) + } + + select { + case <-done: + case <-time.After(10 * time.Second): + if err := st.cmd.Process.Kill(); err != nil { + st.t.Errorf("Error st.cmd.Process.Kill() = %v", err) + } + <-done + } + } + if st.ts != nil { + st.ts.Close() + } +} + +func (st *serverTester) readFrame() (http2.Frame, error) { + go func() { + f, err := st.fr.ReadFrame() + if err != nil { + st.errCh <- err + return + } + st.frCh <- f + }() + + select { + case f := <-st.frCh: + return f, nil + case err := <-st.errCh: + return nil, err + case <-time.After(5 * time.Second): + return nil, errors.New("timeout waiting for frame") + } +} + +type requestParam struct { + name string // name for this request to identify the request in log easily + streamID uint32 // stream ID, automatically assigned if 0 + method string // method, defaults to GET + scheme string // scheme, defaults to http + authority string // authority, defaults to backend server address + path string // path, defaults to / + header []hpack.HeaderField // additional request header fields + body []byte // request body + trailer []hpack.HeaderField // trailer part + httpUpgrade bool // true if upgraded to HTTP/2 through HTTP Upgrade + noEndStream bool // true if END_STREAM should not be sent +} + +// wrapper for request body to set trailer part +type chunkedBodyReader struct { + trailer []hpack.HeaderField + trailerWritten bool + body io.Reader + req *http.Request +} + +func (cbr *chunkedBodyReader) Read(p []byte) (n int, err error) { + // document says that we have to set http.Request.Trailer + // after request was sent and before body returns EOF. + if !cbr.trailerWritten { + cbr.trailerWritten = true + for _, h := range cbr.trailer { + cbr.req.Trailer.Set(h.Name, h.Value) + } + } + return cbr.body.Read(p) +} + +func (st *serverTester) websocket(rp requestParam) *serverResponse { + urlstring := st.url + "/echo" + + config, err := websocket.NewConfig(urlstring, st.url) + if err != nil { + st.t.Fatalf("websocket.NewConfig(%q, %q) returned error: %v", urlstring, st.url, err) + } + + config.Header.Add("Test-Case", rp.name) + for _, h := range rp.header { + config.Header.Add(h.Name, h.Value) + } + + ws, err := websocket.NewClient(config, st.conn) + if err != nil { + st.t.Fatalf("Error creating websocket client: %v", err) + } + + if _, err := ws.Write(rp.body); err != nil { + st.t.Fatalf("ws.Write() returned error: %v", err) + } + + msg := make([]byte, 1024) + var n int + if n, err = ws.Read(msg); err != nil { + st.t.Fatalf("ws.Read() returned error: %v", err) + } + + res := &serverResponse{ + body: msg[:n], + } + + return res +} + +func (st *serverTester) http3(rp requestParam) (*serverResponse, error) { + rt := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + defer rt.Close() + + c := &http.Client{ + Transport: rt, + } + + method := "GET" + if rp.method != "" { + method = rp.method + } + + var body io.Reader + + if rp.body != nil { + body = bytes.NewBuffer(rp.body) + } + + reqURL := st.url + + if rp.path != "" { + u, err := url.Parse(st.url) + if err != nil { + st.t.Fatalf("Error parsing URL from st.url %v: %v", st.url, err) + } + u.Path = "" + u.RawQuery = "" + reqURL = u.String() + rp.path + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, method, reqURL, body) + if err != nil { + return nil, err + } + + for _, h := range rp.header { + req.Header.Add(h.Name, h.Value) + } + + req.Header.Add("Test-Case", rp.name) + + // TODO http3 package does not support trailer at the time of + // this writing. + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + res := &serverResponse{ + status: resp.StatusCode, + header: resp.Header, + body: respBody, + connClose: resp.Close, + } + + return res, nil +} + +func (st *serverTester) http1(rp requestParam) (*serverResponse, error) { + method := "GET" + if rp.method != "" { + method = rp.method + } + + var body io.Reader + var cbr *chunkedBodyReader + if rp.body != nil { + body = bytes.NewBuffer(rp.body) + if len(rp.trailer) != 0 { + cbr = &chunkedBodyReader{ + trailer: rp.trailer, + body: body, + } + body = cbr + } + } + + reqURL := st.url + + if rp.path != "" { + u, err := url.Parse(st.url) + if err != nil { + st.t.Fatalf("Error parsing URL from st.url %v: %v", st.url, err) + } + u.Path = "" + u.RawQuery = "" + reqURL = u.String() + rp.path + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, method, reqURL, body) + if err != nil { + return nil, err + } + for _, h := range rp.header { + req.Header.Add(h.Name, h.Value) + } + req.Header.Add("Test-Case", rp.name) + if cbr != nil { + cbr.req = req + // this makes request use chunked encoding + req.ContentLength = -1 + req.Trailer = make(http.Header) + for _, h := range cbr.trailer { + req.Trailer.Set(h.Name, "") + } + } + if err := req.Write(st.conn); err != nil { + return nil, err + } + resp, err := http.ReadResponse(bufio.NewReader(st.conn), req) + if err != nil { + return nil, err + } + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body.Close() + + res := &serverResponse{ + status: resp.StatusCode, + header: resp.Header, + body: respBody, + connClose: resp.Close, + } + + return res, nil +} + +func (st *serverTester) http2(rp requestParam) (*serverResponse, error) { + st.headerBlkBuf.Reset() + st.header = make(http.Header) + + var id uint32 + if rp.streamID != 0 { + id = rp.streamID + if id >= st.nextStreamID && id%2 == 1 { + st.nextStreamID = id + 2 + } + } else { + id = st.nextStreamID + st.nextStreamID += 2 + } + + if !st.h2PrefaceSent { + st.h2PrefaceSent = true + fmt.Fprint(st.conn, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") + if err := st.fr.WriteSettings(); err != nil { + return nil, err + } + } + + res := &serverResponse{ + streamID: id, + } + + streams := make(map[uint32]*serverResponse) + streams[id] = res + + if !rp.httpUpgrade { + method := "GET" + if rp.method != "" { + method = rp.method + } + _ = st.enc.WriteField(pair(":method", method)) + + scheme := "http" + if rp.scheme != "" { + scheme = rp.scheme + } + _ = st.enc.WriteField(pair(":scheme", scheme)) + + authority := st.authority + if rp.authority != "" { + authority = rp.authority + } + _ = st.enc.WriteField(pair(":authority", authority)) + + path := "/" + if rp.path != "" { + path = rp.path + } + _ = st.enc.WriteField(pair(":path", path)) + + _ = st.enc.WriteField(pair("test-case", rp.name)) + + for _, h := range rp.header { + _ = st.enc.WriteField(h) + } + + err := st.fr.WriteHeaders(http2.HeadersFrameParam{ + StreamID: id, + EndStream: len(rp.body) == 0 && len(rp.trailer) == 0 && !rp.noEndStream, + EndHeaders: true, + BlockFragment: st.headerBlkBuf.Bytes(), + }) + if err != nil { + return nil, err + } + + if len(rp.body) != 0 { + // TODO we assume rp.body fits in 1 frame + if err := st.fr.WriteData(id, len(rp.trailer) == 0 && !rp.noEndStream, rp.body); err != nil { + return nil, err + } + } + + if len(rp.trailer) != 0 { + st.headerBlkBuf.Reset() + for _, h := range rp.trailer { + _ = st.enc.WriteField(h) + } + err := st.fr.WriteHeaders(http2.HeadersFrameParam{ + StreamID: id, + EndStream: true, + EndHeaders: true, + BlockFragment: st.headerBlkBuf.Bytes(), + }) + if err != nil { + return nil, err + } + } + } +loop: + for { + fr, err := st.readFrame() + if err != nil { + return res, err + } + switch f := fr.(type) { + case *http2.HeadersFrame: + _, err := st.dec.Write(f.HeaderBlockFragment()) + if err != nil { + return res, err + } + sr, ok := streams[f.FrameHeader.StreamID] + if !ok { + st.header = make(http.Header) + break + } + sr.header = cloneHeader(st.header) + var status int + status, err = strconv.Atoi(sr.header.Get(":status")) + if err != nil { + return res, fmt.Errorf("Error parsing status code: %w", err) + } + sr.status = status + if f.StreamEnded() { + if streamEnded(res, streams, sr) { + break loop + } + } + case *http2.PushPromiseFrame: + _, err := st.dec.Write(f.HeaderBlockFragment()) + if err != nil { + return res, err + } + sr := &serverResponse{ + streamID: f.PromiseID, + reqHeader: cloneHeader(st.header), + } + streams[sr.streamID] = sr + case *http2.DataFrame: + sr, ok := streams[f.FrameHeader.StreamID] + if !ok { + break + } + sr.body = append(sr.body, f.Data()...) + if f.StreamEnded() { + if streamEnded(res, streams, sr) { + break loop + } + } + case *http2.RSTStreamFrame: + sr, ok := streams[f.FrameHeader.StreamID] + if !ok { + break + } + sr.errCode = f.ErrCode + if streamEnded(res, streams, sr) { + break loop + } + case *http2.GoAwayFrame: + if f.ErrCode == http2.ErrCodeNo { + break + } + res.errCode = f.ErrCode + res.connErr = true + break loop + case *http2.SettingsFrame: + if f.IsAck() { + break + } + if err := st.fr.WriteSettingsAck(); err != nil { + return res, err + } + } + } + sort.Sort(ByStreamID(res.pushResponse)) + return res, nil +} + +func streamEnded(mainSr *serverResponse, streams map[uint32]*serverResponse, sr *serverResponse) bool { + delete(streams, sr.streamID) + if mainSr.streamID != sr.streamID { + mainSr.pushResponse = append(mainSr.pushResponse, sr) + } + return len(streams) == 0 +} + +type serverResponse struct { + status int // HTTP status code + header http.Header // response header fields + body []byte // response body + streamID uint32 // stream ID in HTTP/2 + errCode http2.ErrCode // error code received in HTTP/2 RST_STREAM or GOAWAY + connErr bool // true if HTTP/2 connection error + connClose bool // Connection: close is included in response header in HTTP/1 test + reqHeader http.Header // http request header, currently only sotres pushed request header + pushResponse []*serverResponse // pushed response +} + +type ByStreamID []*serverResponse + +func (b ByStreamID) Len() int { + return len(b) +} + +func (b ByStreamID) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +func (b ByStreamID) Less(i, j int) bool { + return b[i].streamID < b[j].streamID +} + +func cloneHeader(h http.Header) http.Header { + h2 := make(http.Header, len(h)) + for k, vv := range h { + vv2 := make([]string, len(vv)) + copy(vv2, vv) + h2[k] = vv2 + } + return h2 +} + +func noopHandler(w http.ResponseWriter, r *http.Request) { + if _, err := io.ReadAll(r.Body); err != nil { + http.Error(w, fmt.Sprintf("Error io.ReadAll() = %v", err), http.StatusInternalServerError) + } +} + +type APIResponse struct { + Status string `json:"status,omitempty"` + Code int `json:"code,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` +} + +type proxyProtocolV2 struct { + command proxyProtocolV2Command + sourceAddress net.Addr + destinationAddress net.Addr + additionalData []byte +} + +type proxyProtocolV2Command int + +const ( + proxyProtocolV2CommandLocal proxyProtocolV2Command = 0x0 + proxyProtocolV2CommandProxy proxyProtocolV2Command = 0x1 +) + +type proxyProtocolV2Family int + +const ( + proxyProtocolV2FamilyUnspec proxyProtocolV2Family = 0x0 + proxyProtocolV2FamilyInet proxyProtocolV2Family = 0x1 + proxyProtocolV2FamilyInet6 proxyProtocolV2Family = 0x2 + proxyProtocolV2FamilyUnix proxyProtocolV2Family = 0x3 +) + +type proxyProtocolV2Protocol int + +const ( + proxyProtocolV2ProtocolUnspec proxyProtocolV2Protocol = 0x0 + proxyProtocolV2ProtocolStream proxyProtocolV2Protocol = 0x1 + proxyProtocolV2ProtocolDgram proxyProtocolV2Protocol = 0x2 +) + +func writeProxyProtocolV2(w io.Writer, hdr proxyProtocolV2) error { + if _, err := w.Write([]byte{0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A}); err != nil { + return err + } + if _, err := w.Write([]byte{byte(0x20 | hdr.command)}); err != nil { + return err + } + + switch srcAddr := hdr.sourceAddress.(type) { + case *net.TCPAddr: + dstAddr := hdr.destinationAddress.(*net.TCPAddr) + if len(srcAddr.IP) != len(dstAddr.IP) { + panic("len(srcAddr.IP) != len(dstAddr.IP)") + } + var fam byte + if len(srcAddr.IP) == 4 { + fam = byte(proxyProtocolV2FamilyInet << 4) + } else { + fam = byte(proxyProtocolV2FamilyInet6 << 4) + } + fam |= byte(proxyProtocolV2ProtocolStream) + if _, err := w.Write([]byte{fam}); err != nil { + return err + } + length := uint16(len(srcAddr.IP)*2 + 4 + len(hdr.additionalData)) + if err := binary.Write(w, binary.BigEndian, length); err != nil { + return err + } + if _, err := w.Write(srcAddr.IP); err != nil { + return err + } + if _, err := w.Write(dstAddr.IP); err != nil { + return err + } + if err := binary.Write(w, binary.BigEndian, uint16(srcAddr.Port)); err != nil { + return err + } + if err := binary.Write(w, binary.BigEndian, uint16(dstAddr.Port)); err != nil { + return err + } + case *net.UnixAddr: + dstAddr := hdr.destinationAddress.(*net.UnixAddr) + if len(srcAddr.Name) > 108 { + panic("too long Unix source address") + } + if len(dstAddr.Name) > 108 { + panic("too long Unix destination address") + } + fam := byte(proxyProtocolV2FamilyUnix << 4) + switch srcAddr.Net { + case "unix": + fam |= byte(proxyProtocolV2ProtocolStream) + case "unixdgram": + fam |= byte(proxyProtocolV2ProtocolDgram) + default: + fam |= byte(proxyProtocolV2ProtocolUnspec) + } + if _, err := w.Write([]byte{fam}); err != nil { + return err + } + length := uint16(216 + len(hdr.additionalData)) + if err := binary.Write(w, binary.BigEndian, length); err != nil { + return err + } + zeros := make([]byte, 108) + if _, err := w.Write([]byte(srcAddr.Name)); err != nil { + return err + } + if _, err := w.Write(zeros[:108-len(srcAddr.Name)]); err != nil { + return err + } + if _, err := w.Write([]byte(dstAddr.Name)); err != nil { + return err + } + if _, err := w.Write(zeros[:108-len(dstAddr.Name)]); err != nil { + return err + } + default: + fam := byte(proxyProtocolV2FamilyUnspec<<4) | byte(proxyProtocolV2ProtocolUnspec) + if _, err := w.Write([]byte{fam}); err != nil { + return err + } + length := uint16(len(hdr.additionalData)) + if err := binary.Write(w, binary.BigEndian, length); err != nil { + return err + } + } + + if _, err := w.Write(hdr.additionalData); err != nil { + return err + } + + return nil +} diff --git a/integration-tests/setenv.in b/integration-tests/setenv.in new file mode 100644 index 0000000..7177200 --- /dev/null +++ b/integration-tests/setenv.in @@ -0,0 +1,13 @@ +#!/bin/sh -e + +libdir="@abs_top_builddir@/lib" +if [ -d "$libdir/.libs" ]; then + libdir="$libdir/.libs" +fi + +export CGO_CFLAGS="-I@abs_top_srcdir@/lib/includes -I@abs_top_builddir@/lib/includes @CFLAGS@" +export CGO_CPPFLAGS="@CPPFLAGS@" +export CGO_LDFLAGS="-L$libdir @LDFLAGS@" +export LD_LIBRARY_PATH="$libdir" +export GODEBUG=cgocheck=0 +"$@" |