// 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}) }