diff --git a/caddytest/integration/h2listener_test.go b/caddytest/integration/h2listener_test.go new file mode 100644 index 00000000000..451c925ba5d --- /dev/null +++ b/caddytest/integration/h2listener_test.go @@ -0,0 +1,129 @@ +package integration + +import ( + "fmt" + "net/http" + "slices" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester { + const baseConfig = ` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + servers :9443 { + protocols %s + } + } + localhost { + respond "{http.request.tls.proto} {http.request.proto}" + } + ` + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") + + tr := tester.Client.Transport.(*http.Transport) + tr.TLSClientConfig.NextProtos = clientVersions + tr.Protocols = new(http.Protocols) + if slices.Contains(clientVersions, "h2") { + tr.ForceAttemptHTTP2 = true + tr.Protocols.SetHTTP2(true) + } + if !slices.Contains(clientVersions, "http/1.1") { + tr.Protocols.SetHTTP1(false) + } + + return tester +} + +func TestH2ListenerWithTLS(t *testing.T) { + tests := []struct { + serverVersions []string + clientVersions []string + expectedBody string + failed bool + }{ + {[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false}, + {[]string{"h2"}, []string{"http/1.1"}, "", true}, + {[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false}, + {[]string{"h1"}, []string{"h2"}, "", true}, + {[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false}, + {[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false}, + } + for _, tc := range tests { + tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions) + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) + if tc.failed { + resp, err := tester.Client.Get("https://localhost:9443") + if err == nil { + t.Errorf("unexpected response: %d", resp.StatusCode) + } + } else { + tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody) + } + } +} + +func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester { + const baseConfig = ` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + servers :9080 { + protocols %s + } + } + http://localhost { + respond "{http.request.proto}" + } + ` + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") + + tr := tester.Client.Transport.(*http.Transport) + tr.Protocols = new(http.Protocols) + if slices.Contains(clientVersions, "h2c") { + tr.Protocols.SetHTTP1(false) + tr.Protocols.SetUnencryptedHTTP2(true) + } else if slices.Contains(clientVersions, "http/1.1") { + tr.Protocols.SetHTTP1(true) + tr.Protocols.SetUnencryptedHTTP2(false) + } + + return tester +} + +func TestH2ListenerWithoutTLS(t *testing.T) { + tests := []struct { + serverVersions []string + clientVersions []string + expectedBody string + failed bool + }{ + {[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false}, + {[]string{"h2c"}, []string{"http/1.1"}, "", true}, + {[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false}, + {[]string{"h1"}, []string{"h2c"}, "", true}, + {[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false}, + {[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false}, + } + for _, tc := range tests { + tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions) + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) + if tc.failed { + resp, err := tester.Client.Get("http://localhost:9080") + if err == nil { + t.Errorf("unexpected response: %d", resp.StatusCode) + } + } else { + tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody) + } + } +} diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 17fb4d0d5e0..7611285f720 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -466,7 +466,14 @@ func (app *App) Start() error { ErrorLog: serverLogger, Protocols: new(http.Protocols), ConnContext: func(ctx context.Context, c net.Conn) context.Context { - return context.WithValue(ctx, ConnCtxKey, c) + if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok { + getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState { + tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState() + return &tlsConnState + }) + ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc) + } + return ctx }, } diff --git a/modules/caddyhttp/http2listener.go b/modules/caddyhttp/http2listener.go index afc5e51ab75..ad5991790dd 100644 --- a/modules/caddyhttp/http2listener.go +++ b/modules/caddyhttp/http2listener.go @@ -36,6 +36,12 @@ func (h *http2Listener) Accept() (net.Conn, error) { return nil, err } + // *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns + // and handshake won't succeed without mutually supported alpns + if tlsConn, ok := conn.(*tls.Conn); ok { + return tlsConn, nil + } + _, isConnectionStater := conn.(connectionStater) // emit a warning if h.useTLS && !isConnectionStater { @@ -46,6 +52,9 @@ func (h *http2Listener) Accept() (net.Conn, error) { // if both h1 and h2 are enabled, we don't need to check the preface if h.useH1 && h.useH2 { + if isConnectionStater { + return tlsStateConn{conn}, nil + } return conn, nil } @@ -53,14 +62,26 @@ func (h *http2Listener) Accept() (net.Conn, error) { // or else the listener wouldn't be created h2Conn := &http2Conn{ h2Expected: h.useH2, + logger: h.logger, Conn: conn, } if isConnectionStater { - return http2StateConn{h2Conn}, nil + return tlsStateConn{http2StateConn{h2Conn}}, nil } return h2Conn, nil } +// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method +// we can call netConn to get the original net.Conn and get the tls connection state +// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn +type tlsStateConn struct { + net.Conn +} + +func (conn tlsStateConn) tlsNetConn() net.Conn { + return conn.Conn +} + type http2StateConn struct { *http2Conn } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index c195857bafd..ac30f40286e 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -288,14 +288,9 @@ type Server struct { // ServeHTTP is the entry point for all HTTP requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil. - // TODO: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged. if r.TLS == nil { - // not all requests have a conn (like virtual requests) - see #5698 - if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok { - if csc, ok := conn.(connectionStater); ok { - r.TLS = new(tls.ConnectionState) - *r.TLS = csc.ConnectionState() - } + if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok { + r.TLS = tlsConnStateFunc() } } @@ -1115,11 +1110,14 @@ const ( // originally came into the server's entry handler OriginalRequestCtxKey caddy.CtxKey = "original_request" - // For referencing underlying net.Conn - // This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin + // DEPRECATED: not used anymore. + // To refer to the underlying connection, implement a middleware plugin // that RegisterConnContext during provisioning. ConnCtxKey caddy.CtxKey = "conn" + // used to get the tls connection state in the context, if available + tlsConnectionStateFuncCtxKey caddy.CtxKey = "tls_connection_state_func" + // For tracking whether the client is a trusted proxy TrustedProxyVarKey string = "trusted_proxy"