From e145f1b080370c1f7c7370704af63dc1d53fa8e6 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 23 Apr 2026 17:20:21 +0800 Subject: [PATCH] =?UTF-8?q?test(http2):=20=E6=B7=BB=E5=8A=A0=20HTTP/2=20E2?= =?UTF-8?q?E=20=E5=92=8C=20TLS=20=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 HTTP/2 协议协商、流多路复用、头部压缩 E2E 测试 - 添加 TLS 握手、ALPN 协商、HTTP/1.1 回退集成测试 Co-Authored-By: Claude Opus 4.7 --- internal/e2e/http2_e2e_test.go | 476 +++++++++++++++++ internal/http2/integration_tls_test.go | 687 +++++++++++++++++++++++++ 2 files changed, 1163 insertions(+) create mode 100644 internal/e2e/http2_e2e_test.go create mode 100644 internal/http2/integration_tls_test.go diff --git a/internal/e2e/http2_e2e_test.go b/internal/e2e/http2_e2e_test.go new file mode 100644 index 0000000..26018bc --- /dev/null +++ b/internal/e2e/http2_e2e_test.go @@ -0,0 +1,476 @@ +//go:build e2e + +// http2_e2e_test.go - HTTP/2 协议 E2E 测试 +// +// 测试 lolly HTTP/2 功能:协议协商、流多路复用、头部压缩等。 +// +// 作者:xfy +package e2e + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/http2" + + "rua.plus/lolly/internal/e2e/testutil" +) + +// TestE2EHTTP2ProtocolNegotiation 测试 HTTP/2 协议协商。 +// +// 验证 ALPN 协商成功选择 h2 协议。 +func TestE2EHTTP2ProtocolNegotiation(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建带 SSL 的配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithStatic("/", "/var/www/html") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 创建 HTTP/2 客户端 + client := &http.Client{ + Transport: &http2.Transport{ + AllowHTTP: false, + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, *tls.ConnectionState, error) { + dialer := &net.Dialer{} + conn, err := dialer.DialContext(ctx, network, addr) + if err != nil { + return nil, nil, err + } + tlsConn := tls.Client(conn, cfg) + if err := tlsConn.HandshakeContext(ctx); err != nil { + _ = conn.Close() + return nil, nil, err + } + return tlsConn, &tlsConn.ConnectionState{}, nil + }, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + // 发送请求 + resp, err := client.Get(lolly.HTTPSBaseURL()) + require.NoError(t, err, "HTTP/2 request failed") + defer resp.Body.Close() + + // 验证 HTTP/2 协议 + assert.Equal(t, 2, resp.ProtoMajor, "Expected HTTP/2 protocol") + t.Logf("HTTP/2 negotiation successful, status: %d", resp.StatusCode) +} + +// TestE2EHTTP2StreamMultiplexing 测试 HTTP/2 流多路复用。 +// +// 验证多个并发请求在单个连接上复用。 +func TestE2EHTTP2StreamMultiplexing(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithStatic("/", "/var/www/html") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 创建 HTTP/2 客户端 + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + transport := &http2.Transport{ + TLSClientConfig: tlsConfig, + } + client := &http.Client{Transport: transport} + + // 并发发送多个请求 + numRequests := 10 + var wg sync.WaitGroup + errors := make(chan error, numRequests) + start := time.Now() + + for i := 0; i < numRequests; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + resp, err := client.Get(fmt.Sprintf("%s/test%d", lolly.HTTPSBaseURL(), id)) + if err != nil { + errors <- fmt.Errorf("request %d failed: %w", id, err) + return + } + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }(i) + } + + wg.Wait() + close(errors) + + elapsed := time.Since(start) + t.Logf("Completed %d requests in %v", numRequests, elapsed) + + // 检查错误 + for err := range errors { + t.Errorf("Request error: %v", err) + } + + // 多路复用应该比串行请求快 + // 如果每个请求需要 100ms,串行需要 1s,多路复用应该更快 + assert.Less(t, elapsed, 2*time.Second, "Multiplexed requests should complete quickly") +} + +// TestE2EHTTP2HeaderCompression 测试 HTTP/2 头部压缩。 +// +// 验证 HPACK 压缩正常工作。 +func TestE2EHTTP2HeaderCompression(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithStatic("/", "/var/www/html") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 创建 HTTP/2 客户端 + transport := &http2.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{Transport: transport} + + // 发送多个请求,头部应该被压缩复用 + for i := 0; i < 5; i++ { + req, err := http.NewRequest("GET", lolly.HTTPSBaseURL(), nil) + require.NoError(t, err) + + // 添加自定义头部 + req.Header.Set("X-Custom-Header", "test-value-that-should-be-compressed") + + resp, err := client.Do(req) + if err != nil { + t.Logf("Request %d error: %v", i, err) + continue + } + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + + t.Logf("Request %d: status %d", i, resp.StatusCode) + } +} + +// TestE2EHTTP2ServerPush 测试 HTTP/2 服务器推送(如果支持)。 +// +// 验证服务器推送功能。 +func TestE2EHTTP2ServerPush(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithStatic("/", "/var/www/html") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 创建支持推送的客户端 + transport := &http2.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{Transport: transport} + + // 发送请求 + resp, err := client.Get(lolly.HTTPSBaseURL()) + require.NoError(t, err, "Request failed") + defer resp.Body.Close() + + // 检查是否支持推送(通过响应头) + pushSupported := resp.Header.Get("HTTP2-Settings") != "" + t.Logf("HTTP/2 response status: %d, push supported: %v", resp.StatusCode, pushSupported) +} + +// TestE2EHTTP2ConnectionPreface 测试 HTTP/2 连接前缀。 +// +// 验证服务器正确响应 HTTP/2 连接前缀。 +func TestE2EHTTP2ConnectionPreface(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithStatic("/", "/var/www/html") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 建立 TLS 连接 + dialer := &net.Dialer{Timeout: 5 * time.Second} + conn, err := dialer.DialContext(ctx, "tcp", lolly.HTTPSAddr()) + require.NoError(t, err, "Failed to connect") + defer conn.Close() + + tlsConn := tls.Client(conn, &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h2"}, + }) + require.NoError(t, tlsConn.HandshakeContext(ctx), "TLS handshake failed") + + // 验证协商的协议 + state := tlsConn.ConnectionState() + assert.Equal(t, "h2", state.NegotiatedProtocol, "Expected h2 protocol") + + // 发送 HTTP/2 连接前缀 + preface := "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + _, err = tlsConn.Write([]byte(preface)) + require.NoError(t, err, "Failed to send preface") + + // 读取响应(服务器应该发送 SETTINGS 帧) + buf := make([]byte, 1024) + _ = tlsConn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := tlsConn.Read(buf) + if err != nil { + t.Logf("Read error (expected SETTINGS frame): %v", err) + } else { + t.Logf("Received %d bytes after preface", n) + // 检查是否是 SETTINGS 帧(类型 0x04) + if n >= 9 && buf[3] == 0x04 { + t.Log("Received SETTINGS frame") + } + } +} + +// TestE2EHTTP2LargeRequest 测试 HTTP/2 大请求处理。 +// +// 验证大请求体的处理。 +func TestE2EHTTP2LargeRequest(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithProxy("/upload", "http://backend:8080") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 创建 HTTP/2 客户端 + transport := &http2.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{Transport: transport} + + // 发送大请求(1MB) + largeBody := strings.NewReader(strings.Repeat("x", 1024*1024)) + req, err := http.NewRequest("POST", lolly.HTTPSBaseURL()+"/upload", largeBody) + require.NoError(t, err) + + resp, err := client.Do(req) + if err != nil { + t.Logf("Large request error: %v", err) + } else { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + t.Logf("Large request status: %d", resp.StatusCode) + } +} + +// TestE2EHTTP2ConcurrentStreams 测试 HTTP/2 并发流限制。 +// +// 验证服务器正确处理大量并发流。 +func TestE2EHTTP2ConcurrentStreams(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout) + defer cancel() + + if !testutil.LollyImageAvailable(ctx) { + t.Skip("lolly:latest image not available, run 'make docker-build' first") + } + + // 生成自签名证书 + certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir()) + require.NoError(t, err, "Failed to generate certificate") + defer cleanup() + + // 构建配置 + cfg := testutil.NewConfigBuilder(). + WithServer(":8443"). + WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key"). + WithStatic("/", "/var/www/html") + + configYAML, err := cfg.Build() + require.NoError(t, err, "Failed to build config") + + // 启动 lolly + lolly, err := testutil.StartLolly(ctx, + testutil.WithConfigYAML(configYAML), + testutil.WithCert(certPath, keyPath), + ) + require.NoError(t, err, "Failed to start lolly") + defer lolly.Terminate(ctx) + + // 创建 HTTP/2 客户端 + transport := &http2.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{Transport: transport} + + // 发送大量并发请求 + numStreams := 100 + var wg sync.WaitGroup + successCount := 0 + mu := sync.Mutex{} + + for i := 0; i < numStreams; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + resp, err := client.Get(fmt.Sprintf("%s/stream%d", lolly.HTTPSBaseURL(), id)) + if err == nil { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + mu.Lock() + successCount++ + mu.Unlock() + } + }(i) + } + + wg.Wait() + + t.Logf("Successfully handled %d/%d concurrent streams", successCount, numStreams) + assert.Greater(t, successCount, numStreams/2, "Most streams should succeed") +} diff --git a/internal/http2/integration_tls_test.go b/internal/http2/integration_tls_test.go new file mode 100644 index 0000000..adaf99a --- /dev/null +++ b/internal/http2/integration_tls_test.go @@ -0,0 +1,687 @@ +// Package http2 提供 HTTP/2 TLS 连接集成测试。 +// +// 该文件测试 HTTP/2 服务器的 TLS 相关功能: +// - TLS 握手成功/失败 +// - ALPN 协商 h2/http1.1 +// - HTTP/1.1 回退路径 +// +// 作者:xfy +package http2 + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "net/http" + "sync" + "testing" + "time" + + "github.com/valyala/fasthttp" + "rua.plus/lolly/internal/config" +) + +// generateTestCert 生成测试用自签名证书。 +func generateTestCert(t *testing.T) (tls.Certificate, *x509.CertPool) { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate private key: %v", err) + } + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test Org"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey) + if err != nil { + t.Fatalf("Failed to create certificate: %v", err) + } + + cert := tls.Certificate{ + Certificate: [][]byte{certDER}, + PrivateKey: privateKey, + } + + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(certDER) + + return cert, certPool +} + +// TestTLSHandshakeSuccess 测试 TLS 握手成功。 +func TestTLSHandshakeSuccess(t *testing.T) { + cert, _ := generateTestCert(t) + + handler := func(ctx *fasthttp.RequestCtx) { + ctx.WriteString("Hello HTTP/2") + ctx.SetStatusCode(fasthttp.StatusOK) + } + + cfg := &config.HTTP2Config{ + Enabled: true, + MaxConcurrentStreams: 100, + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"h2", "http/1.1"}, + } + + server, err := NewServer(cfg, handler, tlsConfig) + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + // 创建管道连接 + serverConn, clientConn := net.Pipe() + defer func() { + _ = serverConn.Close() + _ = clientConn.Close() + }() + + // 包装服务器端连接为 TLS + tlsServerConn := tls.Server(serverConn, tlsConfig) + + // 需要先 Add(1) 因为 handleConnection 会调用 Done() + server.connWg.Add(1) + + // 在后台处理连接 + go func() { + server.handleConnection(tlsServerConn) + }() + + // 客户端 TLS 握手 + tlsClientConfig := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h2"}, + } + tlsClientConn := tls.Client(clientConn, tlsClientConfig) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := tlsClientConn.HandshakeContext(ctx); err != nil { + t.Fatalf("TLS handshake failed: %v", err) + } + + // 验证协商的协议 + state := tlsClientConn.ConnectionState() + if state.NegotiatedProtocol != "h2" { + t.Errorf("Expected negotiated protocol 'h2', got '%s'", state.NegotiatedProtocol) + } + + // 关闭连接 + _ = tlsClientConn.Close() + _ = tlsServerConn.Close() + + // 等待处理完成 + done := make(chan struct{}) + go func() { + server.connWg.Wait() + close(done) + }() + select { + case <-done: + t.Log("Connection handling completed") + case <-time.After(2 * time.Second): + t.Log("Timeout waiting for connection handling") + } +} + +// TestTLSHandshakeFailure 测试 TLS 握手失败。 +func TestTLSHandshakeFailure(t *testing.T) { + cert, _ := generateTestCert(t) + + handler := func(ctx *fasthttp.RequestCtx) { + ctx.WriteString("Hello") + } + + cfg := &config.HTTP2Config{ + Enabled: true, + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"h2"}, + } + + server, err := NewServer(cfg, handler, tlsConfig) + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + serverConn, clientConn := net.Pipe() + + // 包装服务器端连接为 TLS + tlsServerConn := tls.Server(serverConn, tlsConfig) + + // 需要先 Add(1) 因为 handleConnection 会调用 Done() + server.connWg.Add(1) + + // 在后台处理连接 + go func() { + server.handleConnection(tlsServerConn) + }() + + // 客户端不进行 TLS 握手,直接发送无效数据 + _, _ = clientConn.Write([]byte("INVALID DATA NOT TLS")) + + // 等待处理完成 + time.Sleep(200 * time.Millisecond) + + // 关闭连接 + _ = clientConn.Close() + _ = tlsServerConn.Close() + + // 等待处理完成 + done := make(chan struct{}) + go func() { + server.connWg.Wait() + close(done) + }() + select { + case <-done: + t.Log("Connection handling completed after handshake failure") + case <-time.After(2 * time.Second): + t.Log("Timeout waiting for connection handling") + } +} + +// TestALPNNegotiationH2 测试 ALPN 协商选择 h2。 +func TestALPNNegotiationH2(t *testing.T) { + cert, _ := generateTestCert(t) + + handler := func(ctx *fasthttp.RequestCtx) { + ctx.WriteString("OK") + } + + cfg := &config.HTTP2Config{ + Enabled: true, + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"h2", "http/1.1"}, + } + + server, err := NewServer(cfg, handler, tlsConfig) + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + // 验证 ALPN 配置 + alpnConfig := server.ALPNConfig() + if alpnConfig == nil { + t.Fatal("ALPN config should not be nil") + } + + foundH2 := false + for _, proto := range alpnConfig.NextProtos { + if proto == "h2" { + foundH2 = true + break + } + } + if !foundH2 { + t.Error("ALPN config should include h2 protocol") + } +} + +// TestALPNHTTP11Fallback 测试 ALPN 协商回退到 HTTP/1.1。 +func TestALPNHTTP11Fallback(t *testing.T) { + cert, _ := generateTestCert(t) + + handler := func(ctx *fasthttp.RequestCtx) { + ctx.WriteString("HTTP/1.1 response") + ctx.SetStatusCode(fasthttp.StatusOK) + } + + cfg := &config.HTTP2Config{ + Enabled: true, + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"h2", "http/1.1"}, + } + + server, err := NewServer(cfg, handler, tlsConfig) + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + serverConn, clientConn := net.Pipe() + defer func() { + _ = serverConn.Close() + _ = clientConn.Close() + }() + + tlsServerConn := tls.Server(serverConn, tlsConfig) + + // 需要先 Add(1) 因为 handleConnection 会调用 Done() + server.connWg.Add(1) + + go func() { + server.handleConnection(tlsServerConn) + }() + + // 客户端只支持 HTTP/1.1 + tlsClientConfig := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"http/1.1"}, + } + tlsClientConn := tls.Client(clientConn, tlsClientConfig) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := tlsClientConn.HandshakeContext(ctx); err != nil { + t.Fatalf("TLS handshake failed: %v", err) + } + + // 验证协商的协议是 http/1.1 + state := tlsClientConn.ConnectionState() + if state.NegotiatedProtocol != "http/1.1" { + t.Errorf("Expected negotiated protocol 'http/1.1', got '%s'", state.NegotiatedProtocol) + } + + _ = tlsClientConn.Close() + _ = tlsServerConn.Close() +} + +// TestTLSListenerWrapper 测试 TLS 监听器包装。 +func TestTLSListenerWrapper(t *testing.T) { + cert, _ := generateTestCert(t) + + // 创建底层监听器 + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Failed to create listener: %v", err) + } + defer func() { _ = ln.Close() }() + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + // 包装监听器 + wrappedLn := WrapTLSListener(ln, tlsConfig) + if wrappedLn == nil { + t.Fatal("WrapTLSListener returned nil") + } + + // 验证 NextProtos 已设置 + if len(tlsConfig.NextProtos) == 0 { + t.Error("NextProtos should be set after wrapping") + } + + foundH2 := false + foundHTTP11 := false + for _, proto := range tlsConfig.NextProtos { + if proto == "h2" { + foundH2 = true + } + if proto == "http/1.1" { + foundHTTP11 = true + } + } + if !foundH2 || !foundHTTP11 { + t.Error("NextProtos should include both h2 and http/1.1") + } +} + +// TestTLSListenerExistingProtos 测试已有 NextProtos 的情况。 +func TestTLSListenerExistingProtos(t *testing.T) { + cert, _ := generateTestCert(t) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Failed to create listener: %v", err) + } + defer func() { _ = ln.Close() }() + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"custom-proto"}, + } + + wrappedLn := WrapTLSListener(ln, tlsConfig) + if wrappedLn == nil { + t.Fatal("WrapTLSListener returned nil") + } + + // 已有 NextProtos 不应被覆盖 + if len(tlsConfig.NextProtos) != 1 || tlsConfig.NextProtos[0] != "custom-proto" { + t.Errorf("Existing NextProtos should not be overwritten, got %v", tlsConfig.NextProtos) + } +} + +// TestServeHTTP1Fallback 测试 HTTP/1.1 回退。 +func TestServeHTTP1Fallback(t *testing.T) { + handler := func(ctx *fasthttp.RequestCtx) { + ctx.WriteString("HTTP/1.1 response") + ctx.SetStatusCode(fasthttp.StatusOK) + ctx.Response.Header.Set("X-Test", "value") + } + + cfg := &config.HTTP2Config{ + Enabled: true, + } + + server, err := NewServer(cfg, handler, nil) + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + serverConn, clientConn := net.Pipe() + defer func() { + _ = serverConn.Close() + _ = clientConn.Close() + }() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + server.serveHTTP1(serverConn) + }() + + // 发送 HTTP/1.1 请求 + request := "GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n" + _, _ = clientConn.Write([]byte(request)) + + // 读取响应 + buf := make([]byte, 1024) + _ = clientConn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := clientConn.Read(buf) + if err != nil { + t.Fatalf("Failed to read response: %v", err) + } + + response := string(buf[:n]) + if response == "" { + t.Error("Expected non-empty response") + } + + // 关闭连接 + _ = clientConn.Close() + wg.Wait() +} + +// TestConnectionPoolOperations 测试连接池操作。 +func TestConnectionPoolOperations(t *testing.T) { + pool := newConnectionPool() + + // 创建模拟连接 + conn1 := &mockTestConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}} + conn2 := &mockTestConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12346}} + + // 添加连接 + pool.add("client1", conn1) + pool.add("client1", conn2) + + // 验证连接数 + if count := pool.count("client1"); count != 2 { + t.Errorf("Expected 2 connections, got %d", count) + } + + // 获取连接 + conns := pool.get("client1") + if len(conns) != 2 { + t.Errorf("Expected 2 connections, got %d", len(conns)) + } + + // 移除连接 + pool.remove("client1", conn1) + if count := pool.count("client1"); count != 1 { + t.Errorf("Expected 1 connection after removal, got %d", count) + } + + // 关闭所有连接 + pool.closeAll() + if count := pool.count("client1"); count != 0 { + t.Errorf("Expected 0 connections after closeAll, got %d", count) + } +} + +// mockTestConn 是用于测试的模拟连接。 +type mockTestConn struct { + remoteAddr net.Addr +} + +func (m *mockTestConn) Read(_ []byte) (n int, err error) { return 0, nil } +func (m *mockTestConn) Write(_ []byte) (n int, err error) { return 0, nil } +func (m *mockTestConn) Close() error { return nil } +func (m *mockTestConn) LocalAddr() net.Addr { return m.remoteAddr } +func (m *mockTestConn) RemoteAddr() net.Addr { return m.remoteAddr } +func (m *mockTestConn) SetDeadline(_ time.Time) error { return nil } +func (m *mockTestConn) SetReadDeadline(_ time.Time) error { return nil } +func (m *mockTestConn) SetWriteDeadline(_ time.Time) error { + return nil +} + +// TestIsHTTP2RequestMethod 测试 HTTP/2 请求检测。 +func TestIsHTTP2RequestMethod(t *testing.T) { + tests := []struct { + name string + method string + proto int + want bool + hasPseudoHeader bool + }{ + {"PRI method", "PRI", 1, true, false}, + {"HTTP/2 version", "GET", 2, true, false}, + {"HTTP/1.1", "GET", 1, false, false}, + {"With pseudo header", "GET", 1, true, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest(tt.method, "http://example.com/", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + if tt.proto == 2 { + req.ProtoMajor = 2 + } + if tt.hasPseudoHeader { + req.Header.Set(":method", "GET") + } + + if got := IsHTTP2Request(req); got != tt.want { + t.Errorf("IsHTTP2Request() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestGetALPNProtocolNonTLS 测试获取 ALPN 协议(非 TLS)。 +func TestGetALPNProtocolNonTLS(t *testing.T) { + // 非 TLS 连接 + plainConn := &mockTestConn{} + if proto := GetALPNProtocol(plainConn); proto != "" { + t.Errorf("Expected empty protocol for non-TLS connection, got '%s'", proto) + } +} + +// TestValidateSettingsFunc 测试设置验证。 +func TestValidateSettingsFunc(t *testing.T) { + tests := []struct { + name string + settings Settings + wantErr bool + }{ + { + name: "valid settings", + settings: DefaultSettings(), + wantErr: false, + }, + { + name: "zero max concurrent streams", + settings: Settings{ + MaxConcurrentStreams: 0, + MaxFrameSize: 16384, + MaxHeaderListSize: 4096, + }, + wantErr: true, + }, + { + name: "invalid max frame size - too small", + settings: Settings{ + MaxConcurrentStreams: 100, + MaxFrameSize: 1000, + MaxHeaderListSize: 4096, + }, + wantErr: true, + }, + { + name: "invalid max frame size - too large", + settings: Settings{ + MaxConcurrentStreams: 100, + MaxFrameSize: 20000000, + MaxHeaderListSize: 4096, + }, + wantErr: true, + }, + { + name: "invalid initial window size", + settings: Settings{ + MaxConcurrentStreams: 100, + MaxFrameSize: 16384, + InitialWindowSize: 3000000000, + MaxHeaderListSize: 4096, + }, + wantErr: true, + }, + { + name: "zero max header list size", + settings: Settings{ + MaxConcurrentStreams: 100, + MaxFrameSize: 16384, + MaxHeaderListSize: 0, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSettings(tt.settings) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateSettings() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// TestParseSettingsFunc 测试设置解析。 +func TestParseSettingsFunc(t *testing.T) { + cfg := &config.HTTP2Config{ + MaxConcurrentStreams: 200, + MaxHeaderListSize: 2048576, + PushEnabled: false, + } + + settings := ParseSettings(cfg) + + if settings.MaxConcurrentStreams != 200 { + t.Errorf("Expected MaxConcurrentStreams 200, got %d", settings.MaxConcurrentStreams) + } + if settings.MaxHeaderListSize != 2048576 { + t.Errorf("Expected MaxHeaderListSize 2048576, got %d", settings.MaxHeaderListSize) + } + if settings.EnablePush { + t.Error("Expected EnablePush to be false") + } +} + +// TestDefaultSettingsFunc 测试默认设置。 +func TestDefaultSettingsFunc(t *testing.T) { + settings := DefaultSettings() + + if settings.HeaderTableSize != 4096 { + t.Errorf("Expected HeaderTableSize 4096, got %d", settings.HeaderTableSize) + } + if !settings.EnablePush { + t.Error("Expected EnablePush to be true") + } + if settings.MaxConcurrentStreams != 250 { + t.Errorf("Expected MaxConcurrentStreams 250, got %d", settings.MaxConcurrentStreams) + } + if settings.InitialWindowSize != 65535 { + t.Errorf("Expected InitialWindowSize 65535, got %d", settings.InitialWindowSize) + } + if settings.MaxFrameSize != 16384 { + t.Errorf("Expected MaxFrameSize 16384, got %d", settings.MaxFrameSize) + } + if settings.MaxHeaderListSize != 1048576 { + t.Errorf("Expected MaxHeaderListSize 1048576, got %d", settings.MaxHeaderListSize) + } +} + +// TestSupportsHTTP2Func 测试 HTTP/2 支持检测。 +func TestSupportsHTTP2Func(t *testing.T) { + tests := []struct { + name string + setupReq func(*http.Request) + wantResult bool + }{ + { + name: "HTTP/2 request", + setupReq: func(r *http.Request) { + r.ProtoMajor = 2 + }, + wantResult: true, + }, + { + name: "h2c upgrade", + setupReq: func(r *http.Request) { + r.Header.Set("Upgrade", "h2c") + }, + wantResult: true, + }, + { + name: "HTTP2-Settings header", + setupReq: func(r *http.Request) { + r.Header.Set("HTTP2-Settings", "some-settings") + }, + wantResult: true, + }, + { + name: "HTTP/1.1 without upgrade", + setupReq: func(r *http.Request) {}, + wantResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com/", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + tt.setupReq(req) + + if got := SupportsHTTP2(req); got != tt.wantResult { + t.Errorf("SupportsHTTP2() = %v, want %v", got, tt.wantResult) + } + }) + } +}