diff --git a/internal/cache/purge.go b/internal/cache/purge.go new file mode 100644 index 0000000..49abde9 --- /dev/null +++ b/internal/cache/purge.go @@ -0,0 +1,329 @@ +// 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" +) + +// PurgeAPI 缓存清理 API 处理器。 +// +// 提供 HTTP API 用于主动清理代理缓存,支持精确路径和通配符模式清理。 +// +// 注意事项: +// - 所有方法均为并发安全 +// - 支持 IP 白名单和 Token 认证 +// - 仅处理 POST 请求 +type PurgeAPI struct { + // cache 代理缓存实例 + cache *ProxyCache + + // allowed 允许访问的 IP 网络列表 + allowed []net.IPNet + + // auth 认证配置 + auth config.CacheAPIAuthConfig + + // path API 端点路径 + path string +} + +// PurgeRequest 清理请求结构。 +type PurgeRequest struct { + // Path 精确路径 + Path string `json:"path,omitempty"` + + // Pattern 通配符模式(支持 * 通配符) + Pattern string `json:"pattern,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" { + p.sendError(ctx, fasthttp.StatusMethodNotAllowed, "method not allowed") + return + } + + // 检查 IP 访问权限 + if !p.checkAccess(ctx) { + p.sendError(ctx, fasthttp.StatusForbidden, "forbidden") + return + } + + // 检查认证 + if !p.checkAuth(ctx) { + p.sendError(ctx, fasthttp.StatusUnauthorized, "unauthorized") + return + } + + // 解析请求体 + var req PurgeRequest + if err := json.Unmarshal(ctx.PostBody(), &req); err != nil { + p.sendError(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 { + p.sendError(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}) //nolint:errcheck +} + +// checkAccess 检查客户端 IP 是否在允许列表中。 +func (p *PurgeAPI) checkAccess(ctx *fasthttp.RequestCtx) bool { + // 如果没有配置允许列表,允许所有访问 + if len(p.allowed) == 0 { + return true + } + + clientIP := p.getClientIP(ctx) + if clientIP == nil { + return false + } + + // 检查是否在允许列表中 + for _, network := range p.allowed { + if network.Contains(clientIP) { + return true + } + } + + return false +} + +// checkAuth 检查认证。 +func (p *PurgeAPI) checkAuth(ctx *fasthttp.RequestCtx) bool { + // 无需认证 + if p.auth.Type == "" || p.auth.Type == "none" { + return true + } + + // Token 认证 + if p.auth.Type == "token" { + // 从 Authorization header 获取 token + authHeader := ctx.Request.Header.Peek("Authorization") + if len(authHeader) == 0 { + return false + } + + // 支持 Bearer token 格式 + authStr := string(authHeader) + if token, ok := strings.CutPrefix(authStr, "Bearer "); ok { + return token == p.auth.Token + } + + // 也支持直接传递 token + return authStr == p.auth.Token + } + + return false +} + +// getClientIP 从请求上下文提取客户端 IP。 +func (p *PurgeAPI) getClientIP(ctx *fasthttp.RequestCtx) net.IP { + // 检查 X-Forwarded-For 头部 + if xff := ctx.Request.Header.Peek("X-Forwarded-For"); len(xff) > 0 { + ips := strings.Split(string(xff), ",") + if len(ips) > 0 { + ipStr := strings.TrimSpace(ips[0]) + ip := net.ParseIP(ipStr) + if ip != nil { + return ip + } + } + } + + // 检查 X-Real-IP 头部 + if xri := ctx.Request.Header.Peek("X-Real-IP"); len(xri) > 0 { + ip := net.ParseIP(string(xri)) + if ip != nil { + return ip + } + } + + // 使用 RemoteAddr + if addr := ctx.RemoteAddr(); addr != nil { + if tcpAddr, ok := addr.(*net.TCPAddr); ok { + return tcpAddr.IP + } + } + + return nil +} + +// 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 +} + +// hashPath 使用 FNV-64a 计算路径的哈希值。 +// 与代理层 buildCacheKeyHash 使用相同的算法,确保一致性。 +// 注意:代理层的 key 格式为 "METHOD:URI",purge 时默认使用 GET 方法。 +func hashPath(path string) uint64 { + // 默认使用 GET 方法,与代理层 key 格式一致 + key := "GET:" + path + h := fnv.New64a() + h.Write([]byte(key)) + return h.Sum64() +} + +// matchPattern 检查路径是否匹配通配符模式。 +// 仅支持 * 通配符,匹配任意字符。 +func matchPattern(pattern, path string) bool { + // 特殊情况:* 匹配所有 + if pattern == "*" { + return true + } + + // 检查是否有通配符 + 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 +} + +// sendError 发送错误响应。 +func (p *PurgeAPI) sendError(ctx *fasthttp.RequestCtx, status int, errMsg string) { + ctx.SetContentType("application/json; charset=utf-8") + ctx.SetStatusCode(status) + json.NewEncoder(ctx).Encode(PurgeErrorResponse{Error: errMsg}) //nolint:errcheck +} diff --git a/internal/config/config.go b/internal/config/config.go index bfe8197..0036e0e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -271,6 +271,10 @@ type ServerConfig struct { // 支持单位:b, kb, mb, gb 或纯数字表示字节 // 默认值为 1MB ClientMaxBodySize string `yaml:"client_max_body_size"` + + // CacheAPI 缓存 API 配置 + // 用于主动清理代理缓存 + CacheAPI *CacheAPIConfig `yaml:"cache_api"` } // StaticConfig 静态文件服务配置。 @@ -1445,6 +1449,54 @@ type StatusConfig struct { Format string `yaml:"format"` } +// CacheAPIConfig 缓存 API 配置。 +// +// 配置缓存清理 API 端点,支持主动清理代理缓存。 +// +// 注意事项: +// - Enabled 默认为 false,需显式启用 +// - Allow 限制可访问的 IP 地址列表 +// - Auth 配置认证方式,推荐使用 token 认证 +// +// 使用示例: +// +// cache_api: +// enabled: true +// path: "/_cache/purge" +// allow: ["127.0.0.1", "10.0.0.0/8"] +// auth: +// type: "token" +// token: "${CACHE_API_TOKEN}" +type CacheAPIConfig struct { + // Enabled 是否启用缓存 API + // 默认为 false + Enabled bool `yaml:"enabled"` + + // Path API 端点路径 + // 默认为 "/_cache/purge" + Path string `yaml:"path"` + + // Allow 允许访问的 IP 列表 + // 可访问缓存 API 的 IP 地址或 CIDR + Allow []string `yaml:"allow"` + + // Auth 认证配置 + Auth CacheAPIAuthConfig `yaml:"auth"` +} + +// CacheAPIAuthConfig 缓存 API 认证配置。 +type CacheAPIAuthConfig struct { + // Type 认证类型 + // 支持 "none" 和 "token" 两种类型 + // 默认为 "none" + Type string `yaml:"type"` + + // Token 认证令牌 + // 当 Type 为 "token" 时使用 + // 支持环境变量替换,如 "${CACHE_API_TOKEN}" + Token string `yaml:"token"` +} + // StreamConfig TCP/UDP Stream 代理配置。 // // 用于四层网络代理,如数据库、Redis 等 TCP/UDP 服务。 diff --git a/internal/config/defaults.go b/internal/config/defaults.go index e6e03b1..408c6be 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -45,6 +45,15 @@ func DefaultConfig() *Config { IdleTimeout: 120 * time.Second, MaxConnsPerIP: 1000, MaxRequestsPerConn: 10000, + CacheAPI: &CacheAPIConfig{ + Enabled: false, + Path: "/_cache/purge", + Allow: []string{"127.0.0.1"}, + Auth: CacheAPIAuthConfig{ + Type: "none", + Token: "", + }, + }, Static: []StaticConfig{{ Path: "/", Root: "/var/www/html",