Conflict: sendfile.go (!linux build tag) was incorrectly modified to include linuxSendfile and getSocketFd functions which already exist in sendfile_linux.go. Resolution: Keep HEAD version (simple fallback returning ENOTSUP) as Linux implementation is properly separated in sendfile_linux.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
320 lines
7.7 KiB
Go
320 lines
7.7 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/netutil"
|
||
)
|
||
|
||
// 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"`
|
||
}
|
||
|
||
// 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") //nolint:errcheck
|
||
} else {
|
||
_, network, _ = net.ParseCIDR(cidr + "/128") //nolint:errcheck
|
||
}
|
||
}
|
||
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 {
|
||
return netutil.ExtractClientIPNet(ctx)
|
||
}
|
||
|
||
// 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 检查路径是否匹配通配符模式。
|
||
//
|
||
// 支持以下匹配模式:
|
||
// - "*":匹配所有路径
|
||
// - 以 "*" 结尾:前缀匹配(如 "/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)
|
||
}
|
||
|
||
// 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
|
||
}
|