diff --git a/Dockerfile b/Dockerfile index 4efb3ba..21af13c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine AS builder +FROM golang:1.23-alpine AS builder WORKDIR /app @@ -9,15 +9,20 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o tts-api . -FROM alpine:3.18 +FROM alpine:3.21 -RUN apk --no-cache add ca-certificates tzdata +RUN apk --no-cache add ca-certificates tzdata \ + && addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app COPY --from=builder /app/tts-api . COPY --from=builder /app/health.html . +RUN chown -R appuser:appgroup /app + +USER appuser + EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ diff --git a/common/constants.go b/common/constants.go index fbdb6e2..ea7a6d9 100644 --- a/common/constants.go +++ b/common/constants.go @@ -1,4 +1,4 @@ -package common +package common import "time" @@ -17,4 +17,5 @@ const ( MaxConcurrentRequests = 10 CleanupInterval = time.Hour MaxModelNameLength = 64 + MaxRateLimiterEntries = 100000 ) diff --git a/middleware/auth.go b/middleware/auth.go index a18f9a2..422b0c9 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "crypto/subtle" "encoding/json" "log" "net/http" @@ -39,7 +40,7 @@ func ValidateAPIKey(r *http.Request) bool { token := strings.TrimPrefix(authHeader, "Bearer ") for _, validKey := range validAPIKeys { - if token == validKey { + if subtle.ConstantTimeCompare([]byte(token), []byte(validKey)) == 1 { return true } } diff --git a/middleware/cors.go b/middleware/cors.go index 22a0775..8b04ff0 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -96,16 +96,13 @@ func CORS(next http.Handler) http.Handler { } else if !strings.Contains(vary, "Origin") { w.Header().Set("Vary", vary+", Origin") } + } else { + log.Printf("CORS拦截: 来源=%q 路径=%s 方法=%s 客户端=%s", + origin, r.URL.Path, r.Method, GetClientIP(r)) } } if r.Method == http.MethodOptions { - if origin != "" { - if _, matched := matchOrigin(origin); !matched { - w.WriteHeader(http.StatusNoContent) - return - } - } w.WriteHeader(http.StatusNoContent) return } diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go index 6f22cf3..8d06022 100644 --- a/middleware/ratelimit.go +++ b/middleware/ratelimit.go @@ -1,6 +1,7 @@ package middleware import ( + "log" "net" "net/http" "strings" @@ -77,6 +78,16 @@ func (rl *RateLimiter) cleanup() { rl.requests[k] = valid } } + + if len(rl.requests) > common.MaxRateLimiterEntries { + log.Printf("警告: 限流器条目数 %d 超过上限 %d,触发强制清理", len(rl.requests), common.MaxRateLimiterEntries) + for k := range rl.requests { + if len(rl.requests) <= common.MaxRateLimiterEntries/2 { + break + } + delete(rl.requests, k) + } + } } func GetClientIP(r *http.Request) string { @@ -84,11 +95,14 @@ func GetClientIP(r *http.Request) string { if xForwardedFor != "" { ips := strings.Split(xForwardedFor, ",") if len(ips) > 0 { - return strings.TrimSpace(ips[0]) + ip := strings.TrimSpace(ips[0]) + if ip != "" { + return ip + } } } - xRealIP := r.Header.Get("X-Real-IP") + xRealIP := strings.TrimSpace(r.Header.Get("X-Real-IP")) if xRealIP != "" { return xRealIP } diff --git a/middleware/security.go b/middleware/security.go new file mode 100644 index 0000000..a525481 --- /dev/null +++ b/middleware/security.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" + "strings" +) + +func SecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + + if strings.HasPrefix(r.URL.Path, "/v1/") || r.URL.Path == "/health" { + w.Header().Set("Cache-Control", "no-store") + } + + next.ServeHTTP(w, r) + }) +} diff --git a/router/router.go b/router/router.go index 29b3731..d1c2010 100644 --- a/router/router.go +++ b/router/router.go @@ -12,6 +12,7 @@ func Setup() *mux.Router { r := mux.NewRouter() r.Use(middleware.CORS) + r.Use(middleware.SecurityHeaders) r.Use(middleware.Logger) r.HandleFunc("/v1/audio/speech", controller.OpenaiTTSHandler).Methods("POST", "OPTIONS") diff --git a/tts-api.exe b/tts-api.exe new file mode 100644 index 0000000..af8fb85 Binary files /dev/null and b/tts-api.exe differ