lolly/internal/e2e/http2_e2e_test.go
xfy e145f1b080 test(http2): 添加 HTTP/2 E2E 和 TLS 集成测试
- 添加 HTTP/2 协议协商、流多路复用、头部压缩 E2E 测试
- 添加 TLS 握手、ALPN 协商、HTTP/1.1 回退集成测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 17:20:21 +08:00

477 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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")
}