feat: make native TLS protocol boundary explicit

This commit is contained in:
2026-09-22 18:39:57 +08:00
parent 4a8feaca36
commit a523e6ba3f
6 changed files with 93 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
@@ -33,6 +34,7 @@ const (
defaultMaxBodyBytes = 8 << 20
defaultReadBufferSize = 4 << 10
maxPooledResponseBytes = 64 << 10
nativeALPNProtocol = "http/1.1"
)
// Server is a small HTTP/1.1 server with project-owned connection lifecycle.
@@ -194,9 +196,27 @@ func (s *Server) ServeTLS(ctx context.Context, listener net.Listener, config *tl
if config == nil {
return errors.New("go-omni/native: nil TLS config")
}
config, err := prepareTLSConfig(config)
if err != nil {
_ = listener.Close()
return err
}
return s.Serve(ctx, tls.NewListener(listener, config))
}
func prepareTLSConfig(config *tls.Config) (*tls.Config, error) {
clone := config.Clone()
if len(clone.NextProtos) == 0 {
clone.NextProtos = []string{nativeALPNProtocol}
}
for _, protocol := range clone.NextProtos {
if protocol != nativeALPNProtocol {
return nil, fmt.Errorf("go-omni/native: unsupported TLS ALPN protocol %q; only %q is supported", protocol, nativeALPNProtocol)
}
}
return clone, nil
}
// Serve accepts connections from listener until the context is canceled or an
// unrecoverable accept error occurs. The listener is owned by the Server.
func (s *Server) Serve(ctx context.Context, listener net.Listener) error {

View File

@@ -10,6 +10,7 @@ import (
"io"
"net"
"net/http"
"strings"
"sync/atomic"
"testing"
"time"
@@ -186,3 +187,69 @@ func TestNativeTLSRequestMetadata(t *testing.T) {
t.Fatalf("TLS metadata unavailable to handler: status=%d", response.StatusCode)
}
}
func TestNativeTLSDefaultsToHTTP11ALPN(t *testing.T) {
server := native.New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || r.TLS.NegotiatedProtocol != "http/1.1" {
http.Error(w, "missing HTTP/1.1 ALPN", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}))
certificate := testCertificate(t)
leaf, err := x509.ParseCertificate(certificate.Certificate[0])
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
roots.AddCert(leaf)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() {
done <- server.ServeTLS(context.Background(), listener, &tls.Config{
Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12,
})
}()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
t.Errorf("TLS shutdown: %v", err)
}
if err := <-done; err != nil {
t.Errorf("TLS serve: %v", err)
}
}()
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{
RootCAs: roots, ServerName: "localhost", MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1"},
}}, Timeout: 2 * time.Second}
defer client.CloseIdleConnections()
response, err := client.Get("https://" + listener.Addr().String() + "/")
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusNoContent {
t.Fatalf("unexpected status: %d", response.StatusCode)
}
}
func TestNativeTLSRejectsUnsupportedALPN(t *testing.T) {
server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
certificate := testCertificate(t)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
err = server.ServeTLS(context.Background(), listener, &tls.Config{
Certificates: []tls.Certificate{certificate}, NextProtos: []string{"h2"},
})
if err == nil || !strings.Contains(err.Error(), "unsupported TLS ALPN protocol") {
t.Fatalf("expected unsupported ALPN error, got %v", err)
}
}