package httpapi import ( "net" "net/http" "strings" "sync" "time" ) type rateLimitEntry struct { windowStart time.Time count int } type rateLimiter struct { mu sync.Mutex limit int entries map[string]rateLimitEntry } func (s *Server) hostGuard(next http.Handler) http.Handler { if len(s.AllowedHosts) == 0 && !s.ForceHTTPS { return next } return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.URL.Path == "/healthz" && isLocalRequest(request) { next.ServeHTTP(writer, request) return } if len(s.AllowedHosts) > 0 && !hostAllowed(request.Host, s.AllowedHosts) { writeError(writer, http.StatusMisdirectedRequest, "请求域名不在允许列表中") return } if s.ForceHTTPS && requestURLScheme(request, s.TrustProxy) != "https" { writeError(writer, http.StatusBadRequest, "当前服务要求使用 HTTPS") return } next.ServeHTTP(writer, request) }) } func isLocalRequest(request *http.Request) bool { value := strings.TrimSpace(request.RemoteAddr) host, _, err := net.SplitHostPort(value) if err != nil { host = value } ip := net.ParseIP(strings.Trim(host, "[]")) return ip != nil && ip.IsLoopback() } func (s *Server) rateLimit(next http.Handler) http.Handler { if s.RateLimitPerMinute <= 0 { return next } limiter := &rateLimiter{limit: s.RateLimitPerMinute, entries: make(map[string]rateLimitEntry)} return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { key := clientAddress(request, s.TrustProxy) now := time.Now() if !limiter.allow(key, now) { writer.Header().Set("Retry-After", "60") writeError(writer, http.StatusTooManyRequests, "请求过于频繁,请稍后再试") return } next.ServeHTTP(writer, request) }) } func (limiter *rateLimiter) allow(key string, now time.Time) bool { limiter.mu.Lock() defer limiter.mu.Unlock() entry, ok := limiter.entries[key] if !ok || now.Sub(entry.windowStart) >= time.Minute { limiter.entries[key] = rateLimitEntry{windowStart: now, count: 1} if len(limiter.entries) > 10000 { for address, current := range limiter.entries { if now.Sub(current.windowStart) >= time.Minute { delete(limiter.entries, address) } } } return true } if entry.count >= limiter.limit { return false } entry.count++ limiter.entries[key] = entry return true } func (s *Server) cors(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { origin := strings.TrimSpace(request.Header.Get("Origin")) allowed := origin == "" if len(s.AllowedOrigins) == 0 { if origin != "" { writer.Header().Set("Access-Control-Allow-Origin", "*") } allowed = true } else { for _, configured := range s.AllowedOrigins { if origin == configured { writer.Header().Set("Access-Control-Allow-Origin", origin) writer.Header().Add("Vary", "Origin") allowed = true break } } } if origin != "" && !allowed { writeError(writer, http.StatusForbidden, "访问来源不在允许列表中") return } writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Developer-Token, X-Project-Token") writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") writer.Header().Set("Access-Control-Max-Age", "600") if request.Method == http.MethodOptions { writer.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(writer, request) }) } func securityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("X-Content-Type-Options", "nosniff") writer.Header().Set("X-Frame-Options", "DENY") writer.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") writer.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") writer.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") next.ServeHTTP(writer, request) }) } func hostAllowed(value string, allowed []string) bool { host := strings.ToLower(strings.TrimSpace(value)) if parsed, _, err := net.SplitHostPort(host); err == nil { host = parsed } host = strings.TrimSuffix(host, ".") for _, candidate := range allowed { candidate = strings.ToLower(strings.TrimSpace(candidate)) candidate = strings.TrimSuffix(candidate, ".") if candidate == host || candidate == "*" { return true } if strings.HasPrefix(candidate, "*.") && strings.HasSuffix(host, candidate[1:]) { return true } } return false } func requestURLScheme(request *http.Request, trustProxy bool) string { if trustProxy { if value := strings.ToLower(strings.TrimSpace(request.Header.Get("X-Forwarded-Proto"))); value != "" { return value } } if request.TLS != nil { return "https" } return "http" } func clientAddress(request *http.Request, trustProxy bool) string { if trustProxy { if forwarded := strings.TrimSpace(strings.Split(request.Header.Get("X-Forwarded-For"), ",")[0]); forwarded != "" { return forwarded } } value := strings.TrimSpace(request.RemoteAddr) if host, _, err := net.SplitHostPort(value); err == nil { return host } return value }