test(http2): 添加 HTTP/2 E2E 和 TLS 集成测试
- 添加 HTTP/2 协议协商、流多路复用、头部压缩 E2E 测试 - 添加 TLS 握手、ALPN 协商、HTTP/1.1 回退集成测试 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2ffcfd782b
commit
e145f1b080
476
internal/e2e/http2_e2e_test.go
Normal file
476
internal/e2e/http2_e2e_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
687
internal/http2/integration_tls_test.go
Normal file
687
internal/http2/integration_tls_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user