fix(http): populate Request.TLS for private HTTPS via httpConnCtx

net/http only sets Request.TLS for *tls.Conn or conns implementing ConnectionState(). Our listener wrapped tls.Server in httpConnCtx with an embedded net.Conn, so TLS was never surfaced and r.TLS stayed nil. That triggered the HTTP→HTTPS permanent redirect on every request for HTTPS rules.

Add ConnectionState() on httpConnCtx delegating to the underlying TLS conn.
Add tests for TLS forwarding and plain TCP.
This commit is contained in:
Laurence
2026-05-08 15:17:31 +01:00
parent 663e98af60
commit 146e7835eb
2 changed files with 63 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
package netstack2
import (
"crypto/tls"
"net"
"testing"
)
// tlsConnStub is a minimal net.Conn that also exposes TLS state, matching
// *tls.Conn's ConnectionState used by net/http.Server.
type tlsConnStub struct {
net.Conn
state tls.ConnectionState
}
func (t *tlsConnStub) ConnectionState() tls.ConnectionState {
return t.state
}
func TestHTTPConnCtxForwardsConnectionState(t *testing.T) {
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
inner := &tlsConnStub{
Conn: c1,
state: tls.ConnectionState{Version: tls.VersionTLS12, HandshakeComplete: true},
}
wrapped := &httpConnCtx{Conn: inner, rule: nil}
got := wrapped.ConnectionState()
if got.Version != tls.VersionTLS12 || !got.HandshakeComplete {
t.Fatalf("ConnectionState = %+v, want TLS 1.2 and HandshakeComplete", got)
}
}
func TestHTTPConnCtxConnectionStatePlainTCP(t *testing.T) {
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
wrapped := &httpConnCtx{Conn: c1, rule: nil}
got := wrapped.ConnectionState()
if got.Version != 0 {
t.Fatalf("expected zero ConnectionState for plain conn, got %+v", got)
}
_ = c2
}