d30d1460bd
- Push heartbeat accepts Authorization: Bearer header (query string deprecated) - Gotify alerts use X-Gotify-Key header instead of token in URL - Per-IP rate limiting on all API endpoints (token-bucket) - /metrics gated behind cluster secret (UPTOP_METRICS_PUBLIC=true to opt out) - Config export redacts passwords/tokens by default (redact_secrets=false to override) - Fix rewritePlaceholders for 100+ SQL parameters - Fix AddSiteReturningID/AddAlertReturningID race with LastInsertId/RETURNING - HTTP server timeouts: read 30s, write 60s, idle 120s
92 lines
1.6 KiB
Go
92 lines
1.6 KiB
Go
package server
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type visitor struct {
|
|
tokens float64
|
|
lastSeen time.Time
|
|
}
|
|
|
|
type RateLimiter struct {
|
|
mu sync.Mutex
|
|
visitors map[string]*visitor
|
|
rate float64
|
|
burst float64
|
|
}
|
|
|
|
func NewRateLimiter(requestsPerMinute int) *RateLimiter {
|
|
rl := &RateLimiter{
|
|
visitors: make(map[string]*visitor),
|
|
rate: float64(requestsPerMinute) / 60.0,
|
|
burst: float64(requestsPerMinute),
|
|
}
|
|
go rl.cleanup()
|
|
return rl
|
|
}
|
|
|
|
func (rl *RateLimiter) Allow(ip string) bool {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
v, exists := rl.visitors[ip]
|
|
now := time.Now()
|
|
|
|
if !exists {
|
|
rl.visitors[ip] = &visitor{tokens: rl.burst - 1, lastSeen: now}
|
|
return true
|
|
}
|
|
|
|
elapsed := now.Sub(v.lastSeen).Seconds()
|
|
v.tokens += elapsed * rl.rate
|
|
if v.tokens > rl.burst {
|
|
v.tokens = rl.burst
|
|
}
|
|
v.lastSeen = now
|
|
|
|
if v.tokens < 1 {
|
|
return false
|
|
}
|
|
v.tokens--
|
|
return true
|
|
}
|
|
|
|
func (rl *RateLimiter) cleanup() {
|
|
for {
|
|
time.Sleep(5 * time.Minute)
|
|
rl.mu.Lock()
|
|
cutoff := time.Now().Add(-10 * time.Minute)
|
|
for ip, v := range rl.visitors {
|
|
if v.lastSeen.Before(cutoff) {
|
|
delete(rl.visitors, ip)
|
|
}
|
|
}
|
|
rl.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func clientIP(r *http.Request) string {
|
|
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
|
return fwd
|
|
}
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
func RateLimit(limiter *RateLimiter, next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !limiter.Allow(clientIP(r)) {
|
|
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
next(w, r)
|
|
}
|
|
}
|