summaryrefslogtreecommitdiffstats
path: root/integration-tests
diff options
context:
space:
mode:
Diffstat (limited to 'integration-tests')
-rw-r--r--integration-tests/.gitignore3
-rw-r--r--integration-tests/CMakeLists.txt46
-rw-r--r--integration-tests/Makefile.am51
-rw-r--r--integration-tests/alt-server.crt21
-rw-r--r--integration-tests/alt-server.key28
-rw-r--r--integration-tests/config.go.in6
-rw-r--r--integration-tests/nghttpx_http1_test.go1374
-rw-r--r--integration-tests/nghttpx_http2_test.go3740
-rw-r--r--integration-tests/nghttpx_http3_test.go393
-rw-r--r--integration-tests/req-return.rb12
-rw-r--r--integration-tests/req-set-header.rb7
-rw-r--r--integration-tests/resp-return.rb12
-rw-r--r--integration-tests/resp-set-header.rb7
-rw-r--r--integration-tests/server.crt21
-rw-r--r--integration-tests/server.key28
-rw-r--r--integration-tests/server_tester.go895
-rw-r--r--integration-tests/setenv.in13
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
+"$@"