feat(middleware,http3,config): 增强 gzip_static 支持 Brotli,新增 HTTP/3 0-RTT 与性能配置验证
- gzip_static: 支持 .br 和 .gz 预压缩文件,按 br > gzip 优先级选择 - http3/server: 新增 Enable0RTT 配置,启用时输出安全警告 - config: 废弃 FileCache.LRUEviction 字段,新增 validatePerformance 验证 - 新增 gzip_static 完整测试套件覆盖编码优先级、路径遍历防护等场景 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
20016ebb59
commit
038c0639fd
@ -244,7 +244,8 @@ type FileCacheConfig struct {
|
||||
MaxEntries int64 `yaml:"max_entries"` // 最大缓存条目数
|
||||
MaxSize int64 `yaml:"max_size"` // 内存上限(字节)
|
||||
Inactive time.Duration `yaml:"inactive"` // 未访问淘汰时间
|
||||
LRUEviction bool `yaml:"lru_eviction"` // 启用 LRU 淘汰
|
||||
// Deprecated: 该字段已废弃,将在未来版本中移除,请使用 MaxSize 代替
|
||||
LRUEviction bool `yaml:"lru_eviction"` // 启用 LRU 淘汰(已废弃)
|
||||
}
|
||||
|
||||
// TransportConfig HTTP Transport 配置。
|
||||
@ -444,5 +445,10 @@ func Validate(cfg *Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证性能配置
|
||||
if err := validatePerformance(&cfg.Performance); err != nil {
|
||||
return fmt.Errorf("performance: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ func DefaultConfig() *Config {
|
||||
Level: 6,
|
||||
MinSize: 1024,
|
||||
GzipStatic: false,
|
||||
GzipStaticExtensions: []string{".gz", ".br"},
|
||||
GzipStaticExtensions: []string{".br", ".gz"},
|
||||
Types: []string{
|
||||
"text/html",
|
||||
"text/css",
|
||||
@ -398,6 +398,7 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
||||
fmt.Fprintf(&buf, " max_entries: %d # 最大缓存条目\n", cfg.Performance.FileCache.MaxEntries)
|
||||
fmt.Fprintf(&buf, " max_size: %d # 内存上限(字节,%dMB)\n", cfg.Performance.FileCache.MaxSize, cfg.Performance.FileCache.MaxSize/1024/1024)
|
||||
fmt.Fprintf(&buf, " inactive: %ds # 未访问淘汰时间\n", int(cfg.Performance.FileCache.Inactive.Seconds()))
|
||||
buf.WriteString(" # Deprecated: lru_eviction 已废弃,将在未来版本中移除\n")
|
||||
fmt.Fprintf(&buf, " lru_eviction: %v # 启用 LRU 淘汰\n", cfg.Performance.FileCache.LRUEviction)
|
||||
buf.WriteString(" transport: # HTTP Transport 连接池\n")
|
||||
fmt.Fprintf(&buf, " max_idle_conns: %d # 最大空闲连接\n", cfg.Performance.Transport.MaxIdleConns)
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"rua.plus/lolly/internal/loadbalance"
|
||||
@ -634,3 +635,32 @@ func validateStream(s *StreamConfig) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePerformance 验证性能配置。
|
||||
//
|
||||
// 检查性能配置中的废弃选项和潜在问题,输出警告信息。
|
||||
//
|
||||
// 参数:
|
||||
// - p: 性能配置对象
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 验证失败时返回错误信息,成功返回 nil
|
||||
func validatePerformance(p *PerformanceConfig) error {
|
||||
// 检查废弃的 LRUEviction 字段
|
||||
if p.FileCache.LRUEviction {
|
||||
fmt.Fprintln(os.Stderr, "[警告] performance.file_cache.lru_eviction 已废弃,请使用 max_size 代替")
|
||||
}
|
||||
|
||||
// 检查 Transport 配置(可能导致性能问题)
|
||||
if p.Transport.MaxIdleConns < 0 {
|
||||
return errors.New("transport.max_idle_conns 不能为负数")
|
||||
}
|
||||
if p.Transport.MaxIdleConnsPerHost < 0 {
|
||||
return errors.New("transport.max_idle_conns_per_host 不能为负数")
|
||||
}
|
||||
if p.Transport.MaxConnsPerHost < 0 {
|
||||
return errors.New("transport.max_conns_per_host 不能为负数")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -906,3 +906,126 @@ func TestValidateStream(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePerformance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config PerformanceConfig
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "空配置有效",
|
||||
config: PerformanceConfig{
|
||||
FileCache: FileCacheConfig{},
|
||||
Transport: TransportConfig{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "有效的 file_cache 配置",
|
||||
config: PerformanceConfig{
|
||||
FileCache: FileCacheConfig{
|
||||
MaxEntries: 1000,
|
||||
MaxSize: 1024 * 1024 * 100,
|
||||
},
|
||||
GoroutinePool: GoroutinePoolConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "有效的 transport 配置(零值)",
|
||||
config: PerformanceConfig{
|
||||
Transport: TransportConfig{
|
||||
MaxIdleConns: 0,
|
||||
MaxIdleConnsPerHost: 0,
|
||||
MaxConnsPerHost: 0,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "有效的 transport 配置(正值)",
|
||||
config: PerformanceConfig{
|
||||
Transport: TransportConfig{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxConnsPerHost: 50,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "LRUEviction=true(废弃警告)",
|
||||
config: PerformanceConfig{
|
||||
FileCache: FileCacheConfig{
|
||||
LRUEviction: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "MaxIdleConns 负数",
|
||||
config: PerformanceConfig{
|
||||
Transport: TransportConfig{
|
||||
MaxIdleConns: -1,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "transport.max_idle_conns 不能为负数",
|
||||
},
|
||||
{
|
||||
name: "MaxIdleConnsPerHost 负数",
|
||||
config: PerformanceConfig{
|
||||
Transport: TransportConfig{
|
||||
MaxIdleConnsPerHost: -1,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "transport.max_idle_conns_per_host 不能为负数",
|
||||
},
|
||||
{
|
||||
name: "MaxConnsPerHost 负数",
|
||||
config: PerformanceConfig{
|
||||
Transport: TransportConfig{
|
||||
MaxConnsPerHost: -1,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "transport.max_conns_per_host 不能为负数",
|
||||
},
|
||||
{
|
||||
name: "多个 transport 字段为负",
|
||||
config: PerformanceConfig{
|
||||
Transport: TransportConfig{
|
||||
MaxIdleConns: -1,
|
||||
MaxIdleConnsPerHost: -2,
|
||||
MaxConnsPerHost: -3,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "transport.max_idle_conns 不能为负数",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validatePerformance(&tt.config)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("validatePerformance() 期望返回错误,但返回 nil")
|
||||
return
|
||||
}
|
||||
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("validatePerformance() 错误消息不匹配,期望包含 %q,实际 %q", tt.errMsg, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("validatePerformance() 期望返回 nil,但返回错误: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,6 +109,16 @@ func (s *Server) Start() error {
|
||||
MaxIncomingStreams: int64(s.config.MaxStreams),
|
||||
MaxIdleTimeout: s.config.IdleTimeout,
|
||||
KeepAlivePeriod: 30 * time.Second,
|
||||
Allow0RTT: s.config.Enable0RTT,
|
||||
}
|
||||
|
||||
// 如果启用了 0-RTT,输出安全警告
|
||||
if s.config.Enable0RTT {
|
||||
logging.Warn().
|
||||
Msg("HTTP/3 0-RTT is enabled. " +
|
||||
"For 0-RTT to work, TLS session tickets must be configured " +
|
||||
"(TLSConfig.ClientSessionCache and TLSConfig.SessionTicketKey). " +
|
||||
"See documentation for details.")
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -25,8 +24,8 @@ import (
|
||||
|
||||
// GzipStatic 预压缩文件支持。
|
||||
//
|
||||
// 检查是否存在预压缩的 .gz 文件,如果存在则直接发送,
|
||||
// 避免实时压缩的 CPU 开销。
|
||||
// 检查是否存在预压缩的 .gz 或 .br 文件,如果存在且客户端支持对应编码,
|
||||
// 则直接发送,避免实时压缩的 CPU 开销。
|
||||
type GzipStatic struct {
|
||||
// enabled 是否启用
|
||||
enabled bool
|
||||
@ -36,6 +35,9 @@ type GzipStatic struct {
|
||||
|
||||
// extensions 支持的扩展名
|
||||
extensions []string
|
||||
|
||||
// precompressedExtensions 预压缩扩展名,按优先级排序(默认 [".br", ".gz"])
|
||||
precompressedExtensions []string
|
||||
}
|
||||
|
||||
// NewGzipStatic 创建预压缩文件处理器。
|
||||
@ -49,16 +51,18 @@ func NewGzipStatic(enabled bool, root string, extensions []string) *GzipStatic {
|
||||
extensions = []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||||
}
|
||||
return &GzipStatic{
|
||||
enabled: enabled,
|
||||
root: root,
|
||||
extensions: extensions,
|
||||
enabled: enabled,
|
||||
root: root,
|
||||
extensions: extensions,
|
||||
precompressedExtensions: []string{".br", ".gz"},
|
||||
}
|
||||
}
|
||||
|
||||
// ServeFile 发送预压缩文件(如果存在)。
|
||||
//
|
||||
// 检查是否存在对应的 .gz 文件,如果存在且客户端支持 gzip,
|
||||
// 则发送预压缩文件。
|
||||
// 检查是否存在对应的 .br 或 .gz 文件,按优先级(br > gzip)选择:
|
||||
// 1. 如果客户端支持 br 且 .br 文件存在,返回 .br 文件
|
||||
// 2. 否则如果客户端支持 gzip 且 .gz 文件存在,返回 .gz 文件
|
||||
//
|
||||
// 参数:
|
||||
// - ctx: FastHTTP 请求上下文
|
||||
@ -71,36 +75,48 @@ func (g *GzipStatic) ServeFile(ctx *fasthttp.RequestCtx, filePath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查客户端是否支持 gzip
|
||||
acceptEncoding := ctx.Request.Header.Peek("Accept-Encoding")
|
||||
if !bytes.Contains(acceptEncoding, []byte("gzip")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
if !g.matchExtension(filePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查预压缩文件是否存在
|
||||
gzPath := filePath + ".gz"
|
||||
fullGzPath := filepath.Join(g.root, gzPath)
|
||||
|
||||
// 安全检查:防止目录遍历
|
||||
if strings.Contains(gzPath, "..") {
|
||||
if strings.Contains(filePath, "..") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(fullGzPath); err != nil {
|
||||
return false
|
||||
// 获取 Accept-Encoding 头
|
||||
acceptEncoding := ctx.Request.Header.Peek("Accept-Encoding")
|
||||
|
||||
// 按优先级检查预压缩文件
|
||||
for _, ext := range g.precompressedExtensions {
|
||||
// 检查客户端是否支持该编码
|
||||
if !supportsEncoding(acceptEncoding, ext) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建预压缩文件路径
|
||||
compressedPath := filePath + ext
|
||||
fullPath := filepath.Join(g.root, compressedPath)
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(fullPath); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 设置 Content-Encoding 头
|
||||
switch ext {
|
||||
case ".br":
|
||||
ctx.Response.Header.Set("Content-Encoding", "br")
|
||||
case ".gz":
|
||||
ctx.Response.Header.Set("Content-Encoding", "gzip")
|
||||
}
|
||||
ctx.Response.Header.Set("Vary", "Accept-Encoding")
|
||||
fasthttp.ServeFile(ctx, fullPath)
|
||||
return true
|
||||
}
|
||||
|
||||
// 发送预压缩文件
|
||||
ctx.Response.Header.Set("Content-Encoding", "gzip")
|
||||
ctx.Response.Header.Set("Vary", "Accept-Encoding")
|
||||
fasthttp.ServeFile(ctx, fullGzPath)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
// TryServeFile 尝试发送预压缩文件的静态方法。
|
||||
@ -145,3 +161,22 @@ func (g *GzipStatic) Extensions() []string {
|
||||
func DefaultExtensions() []string {
|
||||
return []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||||
}
|
||||
|
||||
// supportsEncoding 检查客户端是否支持指定编码。
|
||||
//
|
||||
// 简单检查,忽略 q-value,固定优先级由遍历顺序决定。
|
||||
func supportsEncoding(acceptEncoding []byte, ext string) bool {
|
||||
if len(acceptEncoding) == 0 {
|
||||
return false
|
||||
}
|
||||
enc := strings.ToLower(string(acceptEncoding))
|
||||
|
||||
switch ext {
|
||||
case ".br":
|
||||
return strings.Contains(enc, "br")
|
||||
case ".gz":
|
||||
return strings.Contains(enc, "gzip")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
481
internal/middleware/compression/gzip_static_test.go
Normal file
481
internal/middleware/compression/gzip_static_test.go
Normal file
@ -0,0 +1,481 @@
|
||||
// Package compression 提供 gzip_static 预压缩文件测试。
|
||||
package compression
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// TestGzipStaticServeFile_BrotliPriority 测试 .br 文件优先于 .gz 文件
|
||||
func TestGzipStaticServeFile_BrotliPriority(t *testing.T) {
|
||||
// 创建临时目录
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建 .br 和 .gz 文件
|
||||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||||
gzFile := filepath.Join(tmpDir, "test.js.gz")
|
||||
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(gzFile, []byte("gz content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .gz 文件失败: %v", err)
|
||||
}
|
||||
|
||||
g := NewGzipStatic(true, tmpDir, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptEncoding string
|
||||
wantServed bool
|
||||
wantEncoding string
|
||||
}{
|
||||
{
|
||||
name: "同时支持 br 和 gzip,优先返回 br",
|
||||
acceptEncoding: "br, gzip",
|
||||
wantServed: true,
|
||||
wantEncoding: "br",
|
||||
},
|
||||
{
|
||||
name: "只支持 br",
|
||||
acceptEncoding: "br",
|
||||
wantServed: true,
|
||||
wantEncoding: "br",
|
||||
},
|
||||
{
|
||||
name: "只支持 gzip",
|
||||
acceptEncoding: "gzip",
|
||||
wantServed: true,
|
||||
wantEncoding: "gzip",
|
||||
},
|
||||
{
|
||||
name: "支持 deflate 和 gzip,返回 gzip",
|
||||
acceptEncoding: "deflate, gzip",
|
||||
wantServed: true,
|
||||
wantEncoding: "gzip",
|
||||
},
|
||||
{
|
||||
name: "不支持任何编码",
|
||||
acceptEncoding: "",
|
||||
wantServed: false,
|
||||
wantEncoding: "",
|
||||
},
|
||||
{
|
||||
name: "支持 br(大小写不敏感)",
|
||||
acceptEncoding: "BR",
|
||||
wantServed: true,
|
||||
wantEncoding: "br",
|
||||
},
|
||||
{
|
||||
name: "支持 gzip(大小写不敏感)",
|
||||
acceptEncoding: "GZIP",
|
||||
wantServed: true,
|
||||
wantEncoding: "gzip",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
|
||||
|
||||
served := g.ServeFile(ctx, "test.js")
|
||||
|
||||
if served != tt.wantServed {
|
||||
t.Errorf("ServeFile() = %v, want %v", served, tt.wantServed)
|
||||
}
|
||||
|
||||
if tt.wantServed {
|
||||
encoding := ctx.Response.Header.Peek("Content-Encoding")
|
||||
if string(encoding) != tt.wantEncoding {
|
||||
t.Errorf("Content-Encoding = %q, want %q", string(encoding), tt.wantEncoding)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStaticServeFile_GzipFallback 测试仅 .gz 存在时的回退
|
||||
func TestGzipStaticServeFile_GzipFallback(t *testing.T) {
|
||||
// 创建临时目录
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 只创建 .gz 文件
|
||||
gzFile := filepath.Join(tmpDir, "test.css.gz")
|
||||
if err := os.WriteFile(gzFile, []byte("gz content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .gz 文件失败: %v", err)
|
||||
}
|
||||
|
||||
g := NewGzipStatic(true, tmpDir, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptEncoding string
|
||||
wantServed bool
|
||||
wantEncoding string
|
||||
}{
|
||||
{
|
||||
name: "支持 br 但没有 .br 文件,回退到 gzip",
|
||||
acceptEncoding: "br, gzip",
|
||||
wantServed: true,
|
||||
wantEncoding: "gzip",
|
||||
},
|
||||
{
|
||||
name: "只支持 gzip",
|
||||
acceptEncoding: "gzip",
|
||||
wantServed: true,
|
||||
wantEncoding: "gzip",
|
||||
},
|
||||
{
|
||||
name: "只支持 br,没有 .br 文件,不服务",
|
||||
acceptEncoding: "br",
|
||||
wantServed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
|
||||
|
||||
served := g.ServeFile(ctx, "test.css")
|
||||
|
||||
if served != tt.wantServed {
|
||||
t.Errorf("ServeFile() = %v, want %v", served, tt.wantServed)
|
||||
}
|
||||
|
||||
if tt.wantServed {
|
||||
encoding := ctx.Response.Header.Peek("Content-Encoding")
|
||||
if string(encoding) != tt.wantEncoding {
|
||||
t.Errorf("Content-Encoding = %q, want %q", string(encoding), tt.wantEncoding)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStaticServeFile_AcceptEncodingParsing 测试 Accept-Encoding 头解析
|
||||
func TestGzipStaticServeFile_AcceptEncodingParsing(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建测试文件
|
||||
brFile := filepath.Join(tmpDir, "test.html.br")
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
|
||||
g := NewGzipStatic(true, tmpDir, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptEncoding string
|
||||
wantSupported bool
|
||||
}{
|
||||
{
|
||||
name: "包含 br",
|
||||
acceptEncoding: "gzip, deflate, br",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "br 在中间",
|
||||
acceptEncoding: "gzip, br, deflate",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "br 在最后",
|
||||
acceptEncoding: "gzip, deflate,br",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "包含空格",
|
||||
acceptEncoding: "gzip, br , deflate",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "q-value 支持",
|
||||
acceptEncoding: "br;q=0.9, gzip;q=0.8",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "没有 br",
|
||||
acceptEncoding: "gzip, deflate",
|
||||
wantSupported: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
|
||||
|
||||
served := g.ServeFile(ctx, "test.html")
|
||||
|
||||
if served != tt.wantSupported {
|
||||
t.Errorf("ServeFile() = %v, want %v", served, tt.wantSupported)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStaticServeFile_Disabled 测试禁用时不服务
|
||||
func TestGzipStaticServeFile_Disabled(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建测试文件
|
||||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 禁用的 GzipStatic
|
||||
g := NewGzipStatic(false, tmpDir, nil)
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||||
|
||||
served := g.ServeFile(ctx, "test.js")
|
||||
|
||||
if served {
|
||||
t.Error("禁用时不应服务文件")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStaticServeFile_InvalidExtension 测试无效扩展名
|
||||
func TestGzipStaticServeFile_InvalidExtension(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建测试文件
|
||||
brFile := filepath.Join(tmpDir, "test.exe.br")
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
|
||||
g := NewGzipStatic(true, tmpDir, nil)
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||||
|
||||
served := g.ServeFile(ctx, "test.exe")
|
||||
|
||||
if served {
|
||||
t.Error("无效扩展名不应服务文件")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStaticServeFile_PathTraversal 测试路径遍历防护
|
||||
func TestGzipStaticServeFile_PathTraversal(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建测试文件
|
||||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
|
||||
g := NewGzipStatic(true, tmpDir, nil)
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||||
|
||||
// 尝试路径遍历
|
||||
served := g.ServeFile(ctx, "../test.js")
|
||||
|
||||
if served {
|
||||
t.Error("路径遍历应被阻止")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStaticServeFile_VaryHeader 测试 Vary 头设置
|
||||
func TestGzipStaticServeFile_VaryHeader(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建测试文件
|
||||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
|
||||
g := NewGzipStatic(true, tmpDir, nil)
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||||
|
||||
g.ServeFile(ctx, "test.js")
|
||||
|
||||
vary := ctx.Response.Header.Peek("Vary")
|
||||
if string(vary) != "Accept-Encoding" {
|
||||
t.Errorf("Vary 头 = %q, want %q", string(vary), "Accept-Encoding")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewGzipStatic_DefaultExtensions 测试默认扩展名
|
||||
func TestNewGzipStatic_DefaultExtensions(t *testing.T) {
|
||||
g := NewGzipStatic(true, "/tmp", nil)
|
||||
|
||||
expected := []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||||
got := g.Extensions()
|
||||
|
||||
if len(got) != len(expected) {
|
||||
t.Errorf("扩展名数量 = %d, want %d", len(got), len(expected))
|
||||
}
|
||||
|
||||
for i, ext := range expected {
|
||||
if got[i] != ext {
|
||||
t.Errorf("扩展名[%d] = %q, want %q", i, got[i], ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewGzipStatic_CustomExtensions 测试自定义扩展名
|
||||
func TestNewGzipStatic_CustomExtensions(t *testing.T) {
|
||||
custom := []string{".custom", ".ext"}
|
||||
g := NewGzipStatic(true, "/tmp", custom)
|
||||
|
||||
got := g.Extensions()
|
||||
if len(got) != 2 || got[0] != ".custom" || got[1] != ".ext" {
|
||||
t.Errorf("自定义扩展名 = %v, want %v", got, custom)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStatic_Enabled 测试 Enabled 方法
|
||||
func TestGzipStatic_Enabled(t *testing.T) {
|
||||
g1 := NewGzipStatic(true, "/tmp", nil)
|
||||
if !g1.Enabled() {
|
||||
t.Error("Enabled() = false, want true")
|
||||
}
|
||||
|
||||
g2 := NewGzipStatic(false, "/tmp", nil)
|
||||
if g2.Enabled() {
|
||||
t.Error("Enabled() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultExtensions 测试 DefaultExtensions 函数
|
||||
func TestDefaultExtensions(t *testing.T) {
|
||||
expected := []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||||
got := DefaultExtensions()
|
||||
|
||||
if len(got) != len(expected) {
|
||||
t.Errorf("默认扩展名数量 = %d, want %d", len(got), len(expected))
|
||||
}
|
||||
|
||||
for i, ext := range expected {
|
||||
if got[i] != ext {
|
||||
t.Errorf("默认扩展名[%d] = %q, want %q", i, got[i], ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupportsEncoding 测试 supportsEncoding 函数
|
||||
func TestSupportsEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptEncoding string
|
||||
wantEncoding string
|
||||
wantSupported bool
|
||||
}{
|
||||
{
|
||||
name: "支持 br",
|
||||
acceptEncoding: "br",
|
||||
wantEncoding: ".br",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "支持 gzip",
|
||||
acceptEncoding: "gzip",
|
||||
wantEncoding: ".gz",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "不支持",
|
||||
acceptEncoding: "deflate",
|
||||
wantEncoding: ".br",
|
||||
wantSupported: false,
|
||||
},
|
||||
{
|
||||
name: "空 Accept-Encoding",
|
||||
acceptEncoding: "",
|
||||
wantEncoding: ".br",
|
||||
wantSupported: false,
|
||||
},
|
||||
{
|
||||
name: "br 在中间",
|
||||
acceptEncoding: "gzip, br, deflate",
|
||||
wantEncoding: ".br",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "gzip 在中间",
|
||||
acceptEncoding: "br, gzip, deflate",
|
||||
wantEncoding: ".gz",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "大小写不敏感",
|
||||
acceptEncoding: "BR, GZIP",
|
||||
wantEncoding: ".br",
|
||||
wantSupported: true,
|
||||
},
|
||||
{
|
||||
name: "未知扩展名",
|
||||
acceptEncoding: "br, gzip",
|
||||
wantEncoding: ".unknown",
|
||||
wantSupported: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
supported := supportsEncoding([]byte(tt.acceptEncoding), tt.wantEncoding)
|
||||
if supported != tt.wantSupported {
|
||||
t.Errorf("supportsEncoding(%q, %q) = %v, want %v",
|
||||
tt.acceptEncoding, tt.wantEncoding, supported, tt.wantSupported)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTryServeFile 测试 TryServeFile 静态方法
|
||||
func TestTryServeFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 创建测试文件
|
||||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||||
}
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||||
|
||||
served := TryServeFile(ctx, tmpDir, "test.js", nil)
|
||||
|
||||
if !served {
|
||||
t.Error("TryServeFile() = false, want true")
|
||||
}
|
||||
|
||||
encoding := ctx.Response.Header.Peek("Content-Encoding")
|
||||
if string(encoding) != "br" {
|
||||
t.Errorf("Content-Encoding = %q, want %q", string(encoding), "br")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGzipStatic_PrecompressedExtensions 测试预压缩扩展名优先级
|
||||
func TestGzipStatic_PrecompressedExtensions(t *testing.T) {
|
||||
g := NewGzipStatic(true, "/tmp", nil)
|
||||
|
||||
// 验证默认预压缩扩展名顺序
|
||||
expected := []string{".br", ".gz"}
|
||||
if len(g.precompressedExtensions) != len(expected) {
|
||||
t.Errorf("预压缩扩展名数量 = %d, want %d", len(g.precompressedExtensions), len(expected))
|
||||
}
|
||||
|
||||
for i, ext := range expected {
|
||||
if g.precompressedExtensions[i] != ext {
|
||||
t.Errorf("预压缩扩展名[%d] = %q, want %q", i, g.precompressedExtensions[i], ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user