@marcel91200 / caddy Mirror · read-only / commits / 1ce2a13a
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 19:45 UTC

caddyhttp: wrap accepted connection to suppress tls.ConnectionState (#7247)

WeidiDeng committed Oct 16, 2025 at 11:13 UTC 1ce2a13ad10eab5693b425056c571621f5d860e9
4 files changed +166 -11
caddytest/integration/h2listener_test.go new
+129
@@ -0,0 +1,129 @@
1 +package integration
2 +
3 +import (
4 + "fmt"
5 + "net/http"
6 + "slices"
7 + "strings"
8 + "testing"
9 +
10 + "github.com/caddyserver/caddy/v2/caddytest"
11 +)
12 +
13 +func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
14 + const baseConfig = `
15 + {
16 + skip_install_trust
17 + admin localhost:2999
18 + http_port 9080
19 + https_port 9443
20 + servers :9443 {
21 + protocols %s
22 + }
23 + }
24 + localhost {
25 + respond "{http.request.tls.proto} {http.request.proto}"
26 + }
27 + `
28 + tester := caddytest.NewTester(t)
29 + tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
30 +
31 + tr := tester.Client.Transport.(*http.Transport)
32 + tr.TLSClientConfig.NextProtos = clientVersions
33 + tr.Protocols = new(http.Protocols)
34 + if slices.Contains(clientVersions, "h2") {
35 + tr.ForceAttemptHTTP2 = true
36 + tr.Protocols.SetHTTP2(true)
37 + }
38 + if !slices.Contains(clientVersions, "http/1.1") {
39 + tr.Protocols.SetHTTP1(false)
40 + }
41 +
42 + return tester
43 +}
44 +
45 +func TestH2ListenerWithTLS(t *testing.T) {
46 + tests := []struct {
47 + serverVersions []string
48 + clientVersions []string
49 + expectedBody string
50 + failed bool
51 + }{
52 + {[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false},
53 + {[]string{"h2"}, []string{"http/1.1"}, "", true},
54 + {[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
55 + {[]string{"h1"}, []string{"h2"}, "", true},
56 + {[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false},
57 + {[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
58 + }
59 + for _, tc := range tests {
60 + tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions)
61 + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
62 + if tc.failed {
63 + resp, err := tester.Client.Get("https://localhost:9443")
64 + if err == nil {
65 + t.Errorf("unexpected response: %d", resp.StatusCode)
66 + }
67 + } else {
68 + tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody)
69 + }
70 + }
71 +}
72 +
73 +func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
74 + const baseConfig = `
75 + {
76 + skip_install_trust
77 + admin localhost:2999
78 + http_port 9080
79 + servers :9080 {
80 + protocols %s
81 + }
82 + }
83 + http://localhost {
84 + respond "{http.request.proto}"
85 + }
86 + `
87 + tester := caddytest.NewTester(t)
88 + tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
89 +
90 + tr := tester.Client.Transport.(*http.Transport)
91 + tr.Protocols = new(http.Protocols)
92 + if slices.Contains(clientVersions, "h2c") {
93 + tr.Protocols.SetHTTP1(false)
94 + tr.Protocols.SetUnencryptedHTTP2(true)
95 + } else if slices.Contains(clientVersions, "http/1.1") {
96 + tr.Protocols.SetHTTP1(true)
97 + tr.Protocols.SetUnencryptedHTTP2(false)
98 + }
99 +
100 + return tester
101 +}
102 +
103 +func TestH2ListenerWithoutTLS(t *testing.T) {
104 + tests := []struct {
105 + serverVersions []string
106 + clientVersions []string
107 + expectedBody string
108 + failed bool
109 + }{
110 + {[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false},
111 + {[]string{"h2c"}, []string{"http/1.1"}, "", true},
112 + {[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
113 + {[]string{"h1"}, []string{"h2c"}, "", true},
114 + {[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false},
115 + {[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
116 + }
117 + for _, tc := range tests {
118 + tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions)
119 + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
120 + if tc.failed {
121 + resp, err := tester.Client.Get("http://localhost:9080")
122 + if err == nil {
123 + t.Errorf("unexpected response: %d", resp.StatusCode)
124 + }
125 + } else {
126 + tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody)
127 + }
128 + }
129 +}
modules/caddyhttp/app.go
+8 -1
@@ -466,7 +466,14 @@ func (app *App) Start() error {
466 ErrorLog: serverLogger,
467 Protocols: new(http.Protocols),
468 ConnContext: func(ctx context.Context, c net.Conn) context.Context {
469 - return context.WithValue(ctx, ConnCtxKey, c)
469 + if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok {
470 + getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState {
471 + tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState()
472 + return &tlsConnState
473 + })
474 + ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc)
475 + }
476 + return ctx
477 },
478 }
479
modules/caddyhttp/http2listener.go
+22 -1
@@ -36,6 +36,12 @@ func (h *http2Listener) Accept() (net.Conn, error) {
36 return nil, err
37 }
38
39 + // *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns
40 + // and handshake won't succeed without mutually supported alpns
41 + if tlsConn, ok := conn.(*tls.Conn); ok {
42 + return tlsConn, nil
43 + }
44 +
45 _, isConnectionStater := conn.(connectionStater)
46 // emit a warning
47 if h.useTLS && !isConnectionStater {
@@ -46,6 +52,9 @@ func (h *http2Listener) Accept() (net.Conn, error) {
52
53 // if both h1 and h2 are enabled, we don't need to check the preface
54 if h.useH1 && h.useH2 {
55 + if isConnectionStater {
56 + return tlsStateConn{conn}, nil
57 + }
58 return conn, nil
59 }
60
@@ -53,14 +62,26 @@ func (h *http2Listener) Accept() (net.Conn, error) {
62 // or else the listener wouldn't be created
63 h2Conn := &http2Conn{
64 h2Expected: h.useH2,
65 + logger: h.logger,
66 Conn: conn,
67 }
68 if isConnectionStater {
59 - return http2StateConn{h2Conn}, nil
69 + return tlsStateConn{http2StateConn{h2Conn}}, nil
70 }
71 return h2Conn, nil
72 }
73
74 +// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method
75 +// we can call netConn to get the original net.Conn and get the tls connection state
76 +// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn
77 +type tlsStateConn struct {
78 + net.Conn
79 +}
80 +
81 +func (conn tlsStateConn) tlsNetConn() net.Conn {
82 + return conn.Conn
83 +}
84 +
85 type http2StateConn struct {
86 *http2Conn
87 }
modules/caddyhttp/server.go
+7 -9
@@ -288,14 +288,9 @@ type Server struct {
288 // ServeHTTP is the entry point for all HTTP requests.
289 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
290 // If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil.
291 - // TODO: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged.
291 if r.TLS == nil {
293 - // not all requests have a conn (like virtual requests) - see #5698
294 - if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok {
295 - if csc, ok := conn.(connectionStater); ok {
296 - r.TLS = new(tls.ConnectionState)
297 - *r.TLS = csc.ConnectionState()
298 - }
292 + if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok {
293 + r.TLS = tlsConnStateFunc()
294 }
295 }
296
@@ -1115,11 +1110,14 @@ const (
1110 // originally came into the server's entry handler
1111 OriginalRequestCtxKey caddy.CtxKey = "original_request"
1112
1118 - // For referencing underlying net.Conn
1119 - // This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin
1113 + // DEPRECATED: not used anymore.
1114 + // To refer to the underlying connection, implement a middleware plugin
1115 // that RegisterConnContext during provisioning.
1116 ConnCtxKey caddy.CtxKey = "conn"
1117
1118 + // used to get the tls connection state in the context, if available
1119 + tlsConnectionStateFuncCtxKey caddy.CtxKey = "tls_connection_state_func"
1120 +
1121 // For tracking whether the client is a trusted proxy
1122 TrustedProxyVarKey string = "trusted_proxy"
1123