Security: - Enforce client_max_body_size in HTTP/2 and HTTP/3 adapters before buffering - Use trusted-proxy-aware client IP extraction for access control and rate limiting - Include Host and Vary headers in proxy cache key to prevent cache poisoning - Harden static file path traversal check with filepath.Clean + prefix validation - Honor proxy_ssl config for WebSocket upstream TLS Proxy/handler bugs: - Decrement WebSocket connection count immediately on return (not deferred in retry loop) - Remove ineffective headersPool - Coalesce concurrent background cache refreshes with singleflight Dev/build: - Fix Makefile run and test-config targets - Remove broken wget healthcheck from docker-compose - Add missing integration build tags - Fix go vet warnings in tests CI: - Run golangci-lint and integration tests in Gitea Actions Refs: docs/superpowers/plans/2026-06-17-fix-review-issues.md
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
// Package utils provides utility functions for HTTP error handling.
|
|
//
|
|
// This file implements a unified HTTP error response helper to reduce
|
|
// the scattered pattern of ctx.Error throughout the codebase.
|
|
package utils
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/valyala/fasthttp"
|
|
"rua.plus/lolly/internal/config"
|
|
"rua.plus/lolly/internal/netutil"
|
|
)
|
|
|
|
// HTTPError represents an HTTP error with a message and status code.
|
|
type HTTPError struct {
|
|
Message string
|
|
StatusCode int
|
|
}
|
|
|
|
// Predefined common HTTP errors.
|
|
var (
|
|
// ErrNotFound 资源未找到错误。
|
|
ErrNotFound = HTTPError{Message: "Not Found", StatusCode: fasthttp.StatusNotFound}
|
|
// ErrForbidden 禁止访问错误。
|
|
ErrForbidden = HTTPError{Message: "Forbidden", StatusCode: fasthttp.StatusForbidden}
|
|
// ErrUnauthorized 未授权错误。
|
|
ErrUnauthorized = HTTPError{Message: "Unauthorized", StatusCode: fasthttp.StatusUnauthorized}
|
|
// ErrBadGateway 错误网关错误。
|
|
ErrBadGateway = HTTPError{Message: "Bad Gateway", StatusCode: fasthttp.StatusBadGateway}
|
|
// ErrGatewayTimeout 网关超时错误。
|
|
ErrGatewayTimeout = HTTPError{Message: "Gateway Timeout", StatusCode: fasthttp.StatusGatewayTimeout}
|
|
// ErrInternalError 内部服务器错误。
|
|
ErrInternalError = HTTPError{Message: "Internal Server Error", StatusCode: fasthttp.StatusInternalServerError}
|
|
// ErrTooManyRequests 请求过多错误。
|
|
ErrTooManyRequests = HTTPError{Message: "Too Many Requests", StatusCode: fasthttp.StatusTooManyRequests}
|
|
// ErrServiceUnavailable 服务不可用错误。
|
|
ErrServiceUnavailable = HTTPError{Message: "Service Unavailable", StatusCode: fasthttp.StatusServiceUnavailable}
|
|
)
|
|
|
|
// SendError sends an HTTP error response to the client.
|
|
func SendError(ctx *fasthttp.RequestCtx, err HTTPError) {
|
|
ctx.Error(err.Message, err.StatusCode)
|
|
}
|
|
|
|
// SendErrorWithDetail sends an HTTP error response with additional detail.
|
|
func SendErrorWithDetail(ctx *fasthttp.RequestCtx, err HTTPError, detail string) {
|
|
if detail != "" {
|
|
ctx.Error(err.Message+": "+detail, err.StatusCode)
|
|
} else {
|
|
SendError(ctx, err)
|
|
}
|
|
}
|
|
|
|
// SendJSONError sends a JSON error response.
|
|
func SendJSONError(ctx *fasthttp.RequestCtx, status int, errMsg string) {
|
|
ctx.SetContentType("application/json; charset=utf-8")
|
|
ctx.SetStatusCode(status)
|
|
_ = json.NewEncoder(ctx).Encode(struct {
|
|
Error string `json:"error"`
|
|
}{Error: errMsg})
|
|
}
|
|
|
|
// CheckIPAccess checks whether the client IP is in the allowed list.
|
|
// If allowed is empty, all access is permitted.
|
|
// trustedProxies is used to safely extract the client IP from X-Forwarded-For.
|
|
func CheckIPAccess(ctx *fasthttp.RequestCtx, allowed []net.IPNet, trustedProxies []net.IPNet) bool {
|
|
if len(allowed) == 0 {
|
|
return true
|
|
}
|
|
|
|
clientIP := netutil.ExtractClientIPWithTrustedProxies(ctx, trustedProxies)
|
|
if clientIP == nil {
|
|
return false
|
|
}
|
|
|
|
return IPInAllowList(clientIP, allowed)
|
|
}
|
|
|
|
// CheckTokenAuth checks token-based authentication.
|
|
// Returns true if auth is disabled or the token matches.
|
|
func CheckTokenAuth(ctx *fasthttp.RequestCtx, auth config.CacheAPIAuthConfig) bool {
|
|
if auth.Type == "" || auth.Type == "none" {
|
|
return true
|
|
}
|
|
|
|
if auth.Type == "token" {
|
|
authHeader := ctx.Request.Header.Peek("Authorization")
|
|
if len(authHeader) == 0 {
|
|
return false
|
|
}
|
|
|
|
authStr := string(authHeader)
|
|
if token, ok := strings.CutPrefix(authStr, "Bearer "); ok {
|
|
return subtle.ConstantTimeCompare([]byte(token), []byte(auth.Token)) == 1
|
|
}
|
|
|
|
return subtle.ConstantTimeCompare([]byte(authStr), []byte(auth.Token)) == 1
|
|
}
|
|
|
|
return false
|
|
}
|