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