xfy 351f477822 feat(middleware): 增强 rewrite 安全性,添加 ReDoS 保护
- 新增 validateRegexSafety 检测危险正则模式
- 防止嵌套量词导致的灾难性回溯攻击
- 限制正则模式长度 (max 1000 chars)
- 补充 compression 和 accesslog 文档注释

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-03 16:57:40 +08:00

268 lines
5.9 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 提供 HTTP 响应压缩中间件,支持 gzip 和 brotli 算法。
//
// 该文件包含压缩相关的核心逻辑,包括:
// - gzip 压缩(兼容性好,所有浏览器支持)
// - brotli 压缩(压缩率更高,适合现代浏览器)
// - MIME 类型过滤
// - 最小压缩大小控制
//
// 主要用途:
//
// 用于压缩 HTTP 响应内容,减少传输数据量,提升页面加载速度。
//
// 注意事项:
// - 使用缓冲池复用压缩对象,减少内存分配
// - 小于 MinSize 的响应不压缩
//
// 作者xfy
package compression
import (
"bytes"
"strings"
"sync"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/gzip"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
// Algorithm 压缩算法类型。
type Algorithm int
const (
// AlgorithmGzip 使用 gzip 压缩。
AlgorithmGzip Algorithm = iota
// AlgorithmBrotli 使用 brotli 压缩。
AlgorithmBrotli
)
// CompressionMiddleware 响应压缩中间件。
type CompressionMiddleware struct {
// types 可压缩的 MIME 类型列表
types []string
// level 压缩级别1-9
level int
// minSize 最小压缩大小(字节)
minSize int
// algorithm 压缩算法
algorithm Algorithm
// gzipPool gzip.Writer 缓冲池
gzipPool sync.Pool
// brotliPool brotli.Writer 缓冲池
brotliPool sync.Pool
}
// New 创建压缩中间件。
func New(cfg *config.CompressionConfig) (*CompressionMiddleware, error) {
if cfg == nil {
cfg = &config.CompressionConfig{
Type: "gzip",
Level: 6,
MinSize: 1024,
Types: defaultCompressibleTypes(),
}
}
// 设置默认值
if cfg.Level == 0 {
cfg.Level = 6
}
if cfg.MinSize == 0 {
cfg.MinSize = 1024
}
if len(cfg.Types) == 0 {
cfg.Types = defaultCompressibleTypes()
}
// 解析算法类型
var algo Algorithm
switch strings.ToLower(cfg.Type) {
case "brotli":
algo = AlgorithmBrotli
case "gzip":
algo = AlgorithmGzip
case "both":
// both 模式优先使用 brotli如果客户端支持
algo = AlgorithmBrotli
default:
algo = AlgorithmGzip
}
m := &CompressionMiddleware{
types: cfg.Types,
level: cfg.Level,
minSize: cfg.MinSize,
algorithm: algo,
}
// 初始化缓冲池
m.gzipPool = sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(nil, cfg.Level)
return w
},
}
// 初始化 brotli 缓冲池
m.brotliPool = sync.Pool{
New: func() interface{} {
return brotli.NewWriterOptions(nil, brotli.WriterOptions{
Quality: cfg.Level,
})
},
}
return m, nil
}
// defaultCompressibleTypes 返回默认可压缩的 MIME 类型。
func defaultCompressibleTypes() []string {
return []string{
"text/html",
"text/css",
"text/javascript",
"text/plain",
"text/xml",
"application/json",
"application/javascript",
"application/xml",
"application/xhtml+xml",
}
}
// Name 返回中间件名称。
func (m *CompressionMiddleware) Name() string {
return "compression"
}
// Process 应用压缩中间件。
func (m *CompressionMiddleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
// 检查客户端是否支持压缩
acceptEncoding := string(ctx.Request.Header.Peek("Accept-Encoding"))
// 根据算法和客户端支持选择压缩方式
var useGzip, useBrotli bool
if m.algorithm == AlgorithmGzip {
useGzip = strings.Contains(acceptEncoding, "gzip")
} else if m.algorithm == AlgorithmBrotli {
// brotli 或 both 模式
if strings.Contains(acceptEncoding, "br") {
useBrotli = true
} else if strings.Contains(acceptEncoding, "gzip") {
useGzip = true
}
}
// 如果不需要压缩,直接执行
if !useGzip && !useBrotli {
next(ctx)
return
}
// 执行处理器
next(ctx)
// 获取响应体
body := ctx.Response.Body()
bodyLen := len(body)
// 检查是否满足压缩条件
if bodyLen < m.minSize {
return // 不压缩
}
// 检查 MIME 类型
contentType := string(ctx.Response.Header.ContentType())
if !m.isCompressible(contentType) {
return // 不压缩此类型
}
// 执行压缩
var compressed []byte
var encoding string
if useBrotli {
compressed = m.compressBrotli(body)
encoding = "br"
} else if useGzip {
compressed = m.compressGzip(body)
encoding = "gzip"
}
if len(compressed) > 0 && len(compressed) < bodyLen {
ctx.Response.SetBody(compressed)
ctx.Response.Header.Set("Content-Encoding", encoding)
ctx.Response.Header.Del("Content-Length") // 让 fasthttp 自动计算
}
}
}
// isCompressible 检查 MIME 类型是否可压缩。
func (m *CompressionMiddleware) isCompressible(contentType string) bool {
// 移除 charset 等参数
ct := contentType
if idx := strings.Index(ct, ";"); idx >= 0 {
ct = ct[:idx]
}
ct = strings.TrimSpace(strings.ToLower(ct))
for _, t := range m.types {
if strings.ToLower(t) == ct {
return true
}
// 支持通配符匹配
if strings.HasSuffix(t, "/*") {
base := strings.TrimSuffix(t, "/*")
if strings.HasPrefix(ct, base) {
return true
}
}
}
return false
}
// compressGzip 使用 gzip 压缩数据。
func (m *CompressionMiddleware) compressGzip(data []byte) []byte {
w := m.gzipPool.Get().(*gzip.Writer)
defer m.gzipPool.Put(w)
var buf bytes.Buffer
w.Reset(&buf)
w.Write(data)
w.Close()
return buf.Bytes()
}
// compressBrotli 使用 brotli 压缩数据。
func (m *CompressionMiddleware) compressBrotli(data []byte) []byte {
w := m.brotliPool.Get().(*brotli.Writer)
defer m.brotliPool.Put(w)
var buf bytes.Buffer
w.Reset(&buf)
w.Write(data)
w.Close()
return buf.Bytes()
}
// Types 返回可压缩的 MIME 类型列表。
func (m *CompressionMiddleware) Types() []string {
return m.types
}
// Level 返回压缩级别。
func (m *CompressionMiddleware) Level() int {
return m.level
}
// MinSize 返回最小压缩大小。
func (m *CompressionMiddleware) MinSize() int {
return m.minSize
}