主要变更: - WebSocket 代理支持 (internal/proxy/websocket.go) - OCSP stapling 实现 (internal/ssl/ocsp.go) - 监控状态端点 (internal/server/status.go) - 新增 nginx 模块文档 (19-24) - UDP 代理超时配置支持 - 多模块代码注释完善和功能增强 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
5.3 KiB
Go
247 lines
5.3 KiB
Go
// Package compression 提供 HTTP 响应压缩中间件,支持 gzip 和 brotli 算法。
|
||
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 []string // 可压缩的 MIME 类型
|
||
level int // 压缩级别
|
||
minSize int // 最小压缩大小
|
||
algorithm Algorithm // 压缩算法
|
||
|
||
// 缓冲池
|
||
gzipPool sync.Pool
|
||
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
|
||
}
|