- 新增 validateRegexSafety 检测危险正则模式 - 防止嵌套量词导致的灾难性回溯攻击 - 限制正则模式长度 (max 1000 chars) - 补充 compression 和 accesslog 文档注释 Co-Authored-By: Claude <noreply@anthropic.com>
268 lines
5.9 KiB
Go
268 lines
5.9 KiB
Go
// 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
|
||
}
|