b93e14ae88f05c6e01d25ab41d2ecb7456d769b3 / internal/web/ratelimit.go · 1631 bytes · raw
package web
import (
"sync"
"time"
)
// ipLimiter is a small in-memory sliding-window rate limiter keyed by client
// IP. It guards unauthenticated guest writes; a periodic sweep inside allow
// drops keys whose hits have all aged out.
type ipLimiter struct {
mu sync.Mutex
hits map[string][]time.Time
limit int
window time.Duration
calls int
}
func newIPLimiter(limit int, window time.Duration) *ipLimiter {
return &ipLimiter{hits: make(map[string][]time.Time), limit: limit, window: window}
}
// allow records a hit for key and reports whether it is within the limit.
func (l *ipLimiter) allow(key string) bool {
now := time.Now()
cut := now.Add(-l.window)
l.mu.Lock()
defer l.mu.Unlock()
l.calls++
if l.calls%512 == 0 {
l.sweep(cut)
}
recent := l.hits[key][:0]
for _, t := range l.hits[key] {
if t.After(cut) {
recent = append(recent, t)
}
}
if len(recent) >= l.limit {
l.hits[key] = recent
return false
}
l.hits[key] = append(recent, now)
return true
}
// sweep drops keys whose recorded hits have all left the window. Called
// with the mutex held.
func (l *ipLimiter) sweep(cut time.Time) {
for key, times := range l.hits {
recent := times[:0]
for _, t := range times {
if t.After(cut) {
recent = append(recent, t)
}
}
if len(recent) == 0 {
delete(l.hits, key)
continue
}
l.hits[key] = recent
}
}
// reset forgets key's recorded hits, so a limiter keyed on credentials
// rewards a success (e.g. a signed-in user clears the failed-login window).
func (l *ipLimiter) reset(key string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.hits, key)
}