72 lines
2.0 KiB
Go
72 lines
2.0 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// rateLimiter — простой лимитер в памяти по ключу (обычно IP): не больше max
|
|
// событий за окно window. Хватает для одного экземпляра сервиса. Старые записи
|
|
// подчищаются фоном, чтобы карта не росла.
|
|
type rateLimiter struct {
|
|
mu sync.Mutex
|
|
hits map[string][]int64 // ключ -> времена событий (unix-наносекунды)
|
|
}
|
|
|
|
func newRateLimiter() *rateLimiter {
|
|
rl := &rateLimiter{hits: make(map[string][]int64)}
|
|
go rl.cleanup()
|
|
return rl
|
|
}
|
|
|
|
// allow регистрирует событие и возвращает false, если за окно их уже было max.
|
|
func (rl *rateLimiter) allow(key string, max int, window time.Duration) bool {
|
|
now := time.Now().UnixNano()
|
|
cutoff := now - int64(window)
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
kept := rl.hits[key][:0]
|
|
for _, t := range rl.hits[key] {
|
|
if t > cutoff {
|
|
kept = append(kept, t)
|
|
}
|
|
}
|
|
if len(kept) >= max {
|
|
rl.hits[key] = kept
|
|
return false
|
|
}
|
|
rl.hits[key] = append(kept, now)
|
|
return true
|
|
}
|
|
|
|
func (rl *rateLimiter) cleanup() {
|
|
for range time.Tick(10 * time.Minute) {
|
|
now := time.Now().UnixNano()
|
|
rl.mu.Lock()
|
|
for k, ts := range rl.hits {
|
|
alive := ts[:0]
|
|
for _, t := range ts {
|
|
if t > now-int64(time.Hour) {
|
|
alive = append(alive, t)
|
|
}
|
|
}
|
|
if len(alive) == 0 {
|
|
delete(rl.hits, k)
|
|
} else {
|
|
rl.hits[k] = alive
|
|
}
|
|
}
|
|
rl.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// tooMany отвечает 429, если ключ превысил лимит. Возвращает true, если запрос надо прервать.
|
|
func (a *API) tooMany(w http.ResponseWriter, key string, max int, window time.Duration) bool {
|
|
if a.limiter == nil || a.limiter.allow(key, max, window) {
|
|
return false
|
|
}
|
|
fail(w, http.StatusTooManyRequests, "rate_limited", "Слишком много запросов. Повторите позже.")
|
|
return true
|
|
}
|