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 }