167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
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 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 (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")
|
|
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, 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
|
|
}
|