lolly/internal/cache/purge.go
xfy 11e22c80b8 perf: 零分配优化与 Dial timeout 支持
- 添加 b2s/s2b 零分配字节-字符串转换工具函数
- WebSocket 数据转发使用 sync.Pool 复用 32KB buffer
- 条件化 Debug 日志避免非 Debug 级别的字符串分配
- 缓存键哈希计算直接写入 []byte 避免 string 转换
- 使用 bytes.EqualFold 替代 strings.ToLower 进行大小写不敏感比较
- generateETag 使用 strconv.AppendInt 避免 fmt.Sprintf
- 支持 Dial timeout 配置,区分 TCP 连接建立和总连接超时
- MaxConnsPerHost 默认值改为 512(fasthttp 推荐)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:11:20 +08:00

269 lines
6.5 KiB
Go
Raw Permalink 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 cache 提供文件缓存和代理缓存功能,支持 LRU 淘汰和缓存锁防击穿。
//
// 该文件实现了缓存清理 API用于主动清理代理缓存。
//
// 主要功能:
// - 精确路径清理:删除指定路径的缓存条目
// - 通配符模式清理:按模式批量删除缓存条目
// - IP 白名单访问控制
// - Token 认证支持
//
// 作者xfy
package cache
import (
"encoding/json"
"hash/fnv"
"net"
"strings"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/utils"
)
// PurgeAPI 缓存清理 API 处理器。
//
// 提供 HTTP API 用于主动清理代理缓存,支持精确路径和通配符模式清理。
//
// 注意事项:
// - 所有方法均为并发安全
// - 支持 IP 白名单和 Token 认证
// - 仅处理 POST 请求
type PurgeAPI struct {
cache *ProxyCache
auth config.CacheAPIAuthConfig
path string
allowed []net.IPNet
}
// PurgeRequest 清理请求结构。
type PurgeRequest struct {
// Path 精确路径
Path string `json:"path,omitempty"`
// Pattern 通配符模式(支持 * 通配符)
Pattern string `json:"pattern,omitempty"`
// Method HTTP 方法,默认 "GET"
Method string `json:"method,omitempty"`
}
// PurgeResponse 清理响应结构。
type PurgeResponse struct {
// Deleted 被删除的缓存条目数
Deleted int `json:"deleted"`
}
// PurgeErrorResponse 错误响应结构。
type PurgeErrorResponse struct {
// Error 错误信息
Error string `json:"error"`
}
// NewPurgeAPI 创建缓存清理 API 处理器。
//
// 参数:
// - cache: 代理缓存实例
// - cfg: 缓存 API 配置
//
// 返回值:
// - *PurgeAPI: 配置好的处理器
// - error: IP 解析失败时返回非 nil 错误
func NewPurgeAPI(cache *ProxyCache, cfg *config.CacheAPIConfig) (*PurgeAPI, error) {
p := &PurgeAPI{
cache: cache,
auth: cfg.Auth,
path: cfg.Path,
}
// 解析允许的 IP 列表
for _, cidr := range cfg.Allow {
_, network, err := net.ParseCIDR(cidr)
if err != nil {
// 尝试作为单个 IP 解析
ip := net.ParseIP(cidr)
if ip == nil {
return nil, err
}
// 转换为 CIDR 格式
if ip.To4() != nil {
_, network, _ = net.ParseCIDR(cidr + "/32")
} else {
_, network, _ = net.ParseCIDR(cidr + "/128")
}
}
if network != nil {
p.allowed = append(p.allowed, *network)
}
}
return p, nil
}
// Path 返回 API 端点路径。
func (p *PurgeAPI) Path() string {
if p.path == "" {
return "/_cache/purge"
}
return p.path
}
// ServeHTTP 处理缓存清理请求。
//
// 仅处理 POST 请求,支持精确路径和通配符模式清理。
// 返回 JSON 格式的响应。
func (p *PurgeAPI) ServeHTTP(ctx *fasthttp.RequestCtx) {
// 仅允许 POST 方法
if string(ctx.Method()) != "POST" {
utils.SendJSONError(ctx, fasthttp.StatusMethodNotAllowed, "method not allowed")
return
}
// 检查 IP 访问权限
if !utils.CheckIPAccess(ctx, p.allowed) {
utils.SendJSONError(ctx, fasthttp.StatusForbidden, "forbidden")
return
}
// 检查认证
if !utils.CheckTokenAuth(ctx, p.auth) {
utils.SendJSONError(ctx, fasthttp.StatusUnauthorized, "unauthorized")
return
}
// 解析请求体
var req PurgeRequest
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
utils.SendJSONError(ctx, fasthttp.StatusBadRequest, "invalid request body")
return
}
// 执行清理
deleted := 0
if req.Path != "" {
deleted = p.purgeByPath(req.Path)
} else if req.Pattern != "" {
deleted = p.purgeByPattern(req.Pattern)
} else {
utils.SendJSONError(ctx, fasthttp.StatusBadRequest, "missing path or pattern")
return
}
// 返回响应
ctx.SetContentType("application/json; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusOK)
_ = json.NewEncoder(ctx).Encode(PurgeResponse{Deleted: deleted})
}
// purgeByPath 按精确路径清理缓存。
func (p *PurgeAPI) purgeByPath(path string) int {
if p.cache == nil {
return 0
}
// 计算缓存键的哈希值
hashKey := hashPath(path)
// 尝试删除
p.cache.mu.Lock()
defer p.cache.mu.Unlock()
if _, ok := p.cache.entries[hashKey]; ok {
delete(p.cache.entries, hashKey)
return 1
}
return 0
}
// purgeByPattern 按通配符模式清理缓存。
func (p *PurgeAPI) purgeByPattern(pattern string) int {
if p.cache == nil {
return 0
}
p.cache.mu.Lock()
defer p.cache.mu.Unlock()
deleted := 0
for hashKey, entry := range p.cache.entries {
if matchPattern(pattern, entry.OrigKey) {
delete(p.cache.entries, hashKey)
deleted++
}
}
return deleted
}
// HashPathWithMethod 使用 FNV-64a 计算缓存键的哈希值。
// method 为空时默认使用 "GET"。
func HashPathWithMethod(path string, method string) uint64 {
if method == "" {
method = "GET"
}
key := method + ":" + path
h := fnv.New64a()
h.Write([]byte(key))
return h.Sum64()
}
// hashPath 使用 FNV-64a 计算路径的哈希值。
// 与代理层 buildCacheKeyHash 使用相同的算法,确保一致性。
// 注意:代理层的 key 格式为 "METHOD:URI"purge 时默认使用 GET 方法。
func hashPath(path string) uint64 {
return HashPathWithMethod(path, "GET")
}
// MatchPattern 检查路径是否匹配通配符模式。
//
// 支持以下匹配模式:
// - "*":匹配所有路径
// - 以 "*" 结尾:前缀匹配(如 "/api/*" 匹配 "/api/xxx"
// - 以 "/" 结尾:目录前缀匹配
// - 中间通配符:"/api/*/users" 匹配 "/api/v1/users"
// - 其他:精确匹配
//
// 参数:
// - pattern: 匹配模式,支持通配符
// - path: 待检查的路径
//
// 返回值:
// - bool: true 表示匹配false 表示不匹配
func MatchPattern(pattern, path string) bool {
// 特殊情况:* 匹配所有
if pattern == "*" {
return true
}
// 目录前缀匹配pattern 以 / 结尾)
if strings.HasSuffix(pattern, "/") {
return strings.HasPrefix(path, pattern)
}
// 检查是否有通配符
if !strings.Contains(pattern, "*") {
return path == pattern
}
// 简单的前缀匹配:/api/users/* 匹配 /api/users/123
if prefix, ok := strings.CutSuffix(pattern, "*"); ok {
return strings.HasPrefix(path, prefix)
}
// 中间通配符:/api/*/users 匹配 /api/v1/users
parts := strings.Split(pattern, "*")
if len(parts) == 2 {
return strings.HasPrefix(path, parts[0]) && strings.HasSuffix(path, parts[1])
}
// 复杂模式不支持,返回 false
return false
}
// matchPattern 是 MatchPattern 的内部别名,保持向后兼容。
func matchPattern(pattern, path string) bool {
return MatchPattern(pattern, path)
}