lolly/internal/cache/purge.go
xfy cf2fcca7e8 refactor: 提取公共逻辑、消除重复代码、加强错误处理
- 提取 App 公共逻辑到 app_common.go,消除 app.go/app_windows.go 重复定义
- 提取 Server 生命周期/中间件/路由逻辑到独立文件(lifecycle.go/middleware_builder.go/router.go)
- 提取 Proxy 缓存处理/头部修改/目标选择到独立模块
- 提取 CheckIPAccess/CheckTokenAuth 到 utils/httperror.go,消除 status/purge 重复实现
- 修复 stream 双向转发:任一方向完成立即关闭双端,避免连接泄漏
- 修复 SSL/TLS 中静默忽略错误的问题,添加日志记录
- 统一日志消息为英文

💘 Generated with Crush

Assisted-by: GLM 5.1 via Crush <crush@charm.land>
2026-04-28 18:00:48 +08:00

270 lines
6.5 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 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)
}