pulse-lets-go/internal/api/ratelimit.go
Maksim Totmin cc9da3ad7d refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00

110 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"net"
"net/http"
"sync"
"time"
)
// rateLimiter — in-memory token bucket rate limiter.
type rateLimiter struct {
mu sync.Mutex
buckets map[string]*tokenBucket
rate float64 // токенов в секунду
burst int // максимальный размер бакета
cleanupInterval time.Duration
lastCleanup time.Time
}
type tokenBucket struct {
tokens float64
lastSeen time.Time
}
// newRateLimiter создаёт rate limiter с заданными параметрами.
func newRateLimiter(ratePerSec, burst int) *rateLimiter {
if burst <= 0 {
burst = ratePerSec
}
return &rateLimiter{
buckets: make(map[string]*tokenBucket),
rate: float64(ratePerSec),
burst: burst,
cleanupInterval: 60 * time.Second,
lastCleanup: time.Now(),
}
}
// allow проверяет, разрешён ли запрос для данного ключа.
// Возвращает true если разрешён, false если превышен лимит.
func (rl *rateLimiter) allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
// Периодическая очистка устаревших бакетов
if now.Sub(rl.lastCleanup) > rl.cleanupInterval {
for k, b := range rl.buckets {
if now.Sub(b.lastSeen) > rl.cleanupInterval {
delete(rl.buckets, k)
}
}
rl.lastCleanup = now
}
b, exists := rl.buckets[key]
if !exists {
// Новый бакет с полным запасом токенов
b = &tokenBucket{tokens: float64(rl.burst), lastSeen: now}
rl.buckets[key] = b
b.tokens-- // расходуем один токен
return true
}
// Пополняем токены
elapsed := now.Sub(b.lastSeen).Seconds()
b.tokens += elapsed * rl.rate
if b.tokens > float64(rl.burst) {
b.tokens = float64(rl.burst)
}
b.lastSeen = now
if b.tokens >= 1.0 {
b.tokens--
return true
}
return false
}
// rateLimitMiddleware создаёт middleware с rate limiting по IP.
// Ключ — RemoteAddr (или X-Forwarded-For, если за проксей).
func rateLimitMiddleware(limiter *rateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := stripPort(r.RemoteAddr)
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
key = fwd
}
if !limiter.allow(key) {
w.Header().Set("Retry-After", "1")
writeError(w, http.StatusTooManyRequests, "слишком много запросов")
return
}
next.ServeHTTP(w, r)
})
}
}
// stripPort убирает порт из адреса (напр. "[::1]:58544" → "::1", "127.0.0.1:1234" → "127.0.0.1").
func stripPort(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return addr // нет порта
}
return host
}