- 提取 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>
270 lines
6.5 KiB
Go
270 lines
6.5 KiB
Go
// 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)
|
||
}
|
||
|