xfy 038c0639fd 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>
2026-04-07 14:25:28 +08:00

183 lines
4.8 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.

// Package compression 提供 gzip_static 预压缩文件支持。
//
// 该文件实现预压缩文件的检测和发送,避免实时压缩开销。
//
// 主要用途:
//
// 用于发送预压缩的 .gz 文件,减少服务器 CPU 开销,提升响应速度。
//
// 使用场景:
// - 静态资源预先压缩(如 CSS、JS、HTML 文件)
// - 构建时生成 .gz 文件
// - 运行时直接发送预压缩文件
//
// 作者xfy
package compression
import (
"os"
"path/filepath"
"strings"
"github.com/valyala/fasthttp"
)
// GzipStatic 预压缩文件支持。
//
// 检查是否存在预压缩的 .gz 或 .br 文件,如果存在且客户端支持对应编码,
// 则直接发送,避免实时压缩的 CPU 开销。
type GzipStatic struct {
// enabled 是否启用
enabled bool
// root 静态文件根目录
root string
// extensions 支持的扩展名
extensions []string
// precompressedExtensions 预压缩扩展名,按优先级排序(默认 [".br", ".gz"]
precompressedExtensions []string
}
// NewGzipStatic 创建预压缩文件处理器。
//
// 参数:
// - enabled: 是否启用预压缩支持
// - root: 静态文件根目录
// - extensions: 支持预压缩的文件扩展名,为空则使用默认值
func NewGzipStatic(enabled bool, root string, extensions []string) *GzipStatic {
if len(extensions) == 0 {
extensions = []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
}
return &GzipStatic{
enabled: enabled,
root: root,
extensions: extensions,
precompressedExtensions: []string{".br", ".gz"},
}
}
// ServeFile 发送预压缩文件(如果存在)。
//
// 检查是否存在对应的 .br 或 .gz 文件按优先级br > gzip选择
// 1. 如果客户端支持 br 且 .br 文件存在,返回 .br 文件
// 2. 否则如果客户端支持 gzip 且 .gz 文件存在,返回 .gz 文件
//
// 参数:
// - ctx: FastHTTP 请求上下文
// - filePath: 请求的文件路径
//
// 返回值:
// - bool: true 表示已发送预压缩文件false 表示未发送
func (g *GzipStatic) ServeFile(ctx *fasthttp.RequestCtx, filePath string) bool {
if !g.enabled {
return false
}
// 检查文件扩展名
if !g.matchExtension(filePath) {
return false
}
// 安全检查:防止目录遍历
if strings.Contains(filePath, "..") {
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
}
return false
}
// TryServeFile 尝试发送预压缩文件的静态方法。
//
// 用于在静态文件处理器中调用。
//
// 参数:
// - ctx: FastHTTP 请求上下文
// - root: 静态文件根目录
// - filePath: 请求的文件路径
// - extensions: 支持的扩展名
//
// 返回值:
// - bool: true 表示已发送预压缩文件
func TryServeFile(ctx *fasthttp.RequestCtx, root, filePath string, extensions []string) bool {
g := NewGzipStatic(true, root, extensions)
return g.ServeFile(ctx, filePath)
}
// matchExtension 检查文件扩展名是否匹配。
func (g *GzipStatic) matchExtension(filePath string) bool {
ext := strings.ToLower(filepath.Ext(filePath))
for _, e := range g.extensions {
if strings.ToLower(e) == ext {
return true
}
}
return false
}
// Enabled 返回是否启用预压缩。
func (g *GzipStatic) Enabled() bool {
return g.enabled
}
// Extensions 返回支持的扩展名列表。
func (g *GzipStatic) Extensions() []string {
return g.extensions
}
// DefaultExtensions 返回默认支持的扩展名。
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
}
}