lolly/internal/server/purge.go
xfy bec8932561 feat(server): 添加缓存清理 API 支持
新增 PurgeHandler 处理器,支持:
- 按精确路径和通配符模式清理缓存
- HTTP 方法过滤(默认 GET)
- IP 白名单访问控制(CIDR/单 IP/localhost)
- Token 认证保护
- 三种启动模式路由注册

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 16:47:10 +08:00

230 lines
5.4 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 server 提供 HTTP 服务器核心功能。
//
// 该文件实现缓存清理 API 处理器,支持主动清理代理缓存。
package server
import (
"encoding/json"
"net"
"net/netip"
"strings"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/cache"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/netutil"
)
// PurgeHandler 缓存清理 API 处理器。
//
// 持有 Server 引用以访问所有代理实例的缓存。
// 支持 IP 白名单和 Token 认证保护。
//
// 注意事项:
// - 仅处理 POST 请求
// - 支持按路径和按模式两种清理方式
// - method 参数支持指定 HTTP 方法(默认 GET
type PurgeHandler struct {
server *Server
auth config.CacheAPIAuthConfig
path string
allowed []net.IPNet
}
// NewPurgeHandler 创建缓存清理 API 处理器。
//
// 解析 IP 白名单配置,支持 CIDR 格式和单个 IP。
// localhost 特殊处理为 127.0.0.1 和 ::1。
//
// 参数:
// - server: Server 实例,用于访问代理缓存
// - cfg: CacheAPI 配置
//
// 返回值:
// - *PurgeHandler: 配置好的处理器
// - error: IP 解析失败时返回非 nil 错误
func NewPurgeHandler(server *Server, cfg *config.CacheAPIConfig) (*PurgeHandler, error) {
h := &PurgeHandler{
server: server,
path: cfg.Path,
auth: cfg.Auth,
}
// 默认路径
if h.path == "" {
h.path = "/_cache/purge"
}
// 解析允许的 IP 列表
for _, cidr := range cfg.Allow {
// 处理 localhost 特殊情况
if cidr == "localhost" {
_, v4Network, _ := net.ParseCIDR("127.0.0.1/32")
_, v6Network, _ := net.ParseCIDR("::1/128")
if v4Network != nil {
h.allowed = append(h.allowed, *v4Network)
}
if v6Network != nil {
h.allowed = append(h.allowed, *v6Network)
}
continue
}
_, network, err := net.ParseCIDR(cidr)
if err != nil {
// 尝试作为单个 IP 解析
ip, err := netip.ParseAddr(cidr)
if err != nil {
return nil, err
}
// 转换为 CIDR 格式
if ip.Is4() {
_, network, _ = net.ParseCIDR(cidr + "/32")
} else {
_, network, _ = net.ParseCIDR(cidr + "/128")
}
}
if network != nil {
h.allowed = append(h.allowed, *network)
}
}
return h, nil
}
// Path 返回 API 端点路径。
func (h *PurgeHandler) Path() string {
return h.path
}
// ServeHTTP 处理缓存清理请求。
//
// 仅处理 POST 请求,支持精确路径和通配符模式清理。
// 返回 JSON 格式的响应。
func (h *PurgeHandler) ServeHTTP(ctx *fasthttp.RequestCtx) {
// 仅允许 POST 方法
if string(ctx.Method()) != "POST" {
h.sendError(ctx, fasthttp.StatusMethodNotAllowed, "method not allowed")
return
}
// 检查 IP 访问权限
if !h.checkAccess(ctx) {
h.sendError(ctx, fasthttp.StatusForbidden, "forbidden")
return
}
// 检查认证
if !h.checkAuth(ctx) {
h.sendError(ctx, fasthttp.StatusUnauthorized, "unauthorized")
return
}
// 解析请求体
var req cache.PurgeRequest
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
h.sendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
return
}
// 执行清理
deleted := 0
if req.Path != "" {
deleted = h.purgeByPath(req.Path, req.Method)
} else if req.Pattern != "" {
deleted = h.purgeByPattern(req.Pattern, req.Method)
} else {
h.sendError(ctx, fasthttp.StatusBadRequest, "missing path or pattern")
return
}
// 返回响应
ctx.SetContentType("application/json; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusOK)
_ = json.NewEncoder(ctx).Encode(cache.PurgeResponse{Deleted: deleted})
}
// checkAccess 检查客户端 IP 是否在允许列表中。
func (h *PurgeHandler) checkAccess(ctx *fasthttp.RequestCtx) bool {
// 如果没有配置允许列表,允许所有访问
if len(h.allowed) == 0 {
return true
}
clientIP := netutil.ExtractClientIPNet(ctx)
if clientIP == nil {
return false
}
// 检查是否在允许列表中
for _, network := range h.allowed {
if network.Contains(clientIP) {
return true
}
}
return false
}
// checkAuth 检查认证。
func (h *PurgeHandler) checkAuth(ctx *fasthttp.RequestCtx) bool {
// 无需认证
if h.auth.Type == "" || h.auth.Type == "none" {
return true
}
// Token 认证
if h.auth.Type == "token" {
authHeader := ctx.Request.Header.Peek("Authorization")
if len(authHeader) == 0 {
return false
}
authStr := string(authHeader)
// 支持 Bearer token 格式
if token, ok := strings.CutPrefix(authStr, "Bearer "); ok {
return token == h.auth.Token
}
// 也支持直接传递 token
return authStr == h.auth.Token
}
return false
}
// purgeByPath 按精确路径清理缓存。
func (h *PurgeHandler) purgeByPath(path string, method string) int {
hashKey := cache.HashPathWithMethod(path, method)
deleted := 0
for _, p := range h.server.proxies {
if pcache := p.GetCache(); pcache != nil {
pcache.Delete(hashKey)
deleted++
}
}
return deleted
}
// purgeByPattern 按通配符模式清理缓存。
func (h *PurgeHandler) purgeByPattern(pattern string, method string) int {
deleted := 0
for _, p := range h.server.proxies {
if pcache := p.GetCache(); pcache != nil {
deleted += pcache.DeleteByPatternWithMethod(pattern, method)
}
}
return deleted
}
// sendError 发送错误响应。
func (h *PurgeHandler) sendError(ctx *fasthttp.RequestCtx, status int, errMsg string) {
ctx.SetContentType("application/json; charset=utf-8")
ctx.SetStatusCode(status)
_ = json.NewEncoder(ctx).Encode(cache.PurgeErrorResponse{Error: errMsg})
}