Files
enkly-ai/pkg/authsupport/ratelimit.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

88 lines
2.4 KiB
Go

package authsupport
import (
"sync"
"time"
)
// Limiter slows repeated attempts against a single key (an email
// address or a client IP) instead of flatly blocking them: each
// consecutive failure within the window makes the *next* attempt's
// artificial delay longer, capped at maxDelay. That's enough to make
// password-guessing or verification-email-spam scripts painfully slow
// without needing a CAPTCHA or an IP ban list, and it degrades
// gracefully — a genuine user who mistypes their password twice waits a
// second, not minutes.
//
// State is in-memory and per-process, which is fine for enkly's
// single-instance deployment (see docker-compose.yml); it resets on
// restart and doesn't need a database round-trip on the hot path.
type Limiter struct {
mu sync.Mutex
attempts map[string]*attemptState
window time.Duration
baseWait time.Duration
maxWait time.Duration
}
type attemptState struct {
count int
lastSeen time.Time
}
// NewLimiter builds a Limiter. window is how long a run of failures is
// remembered before resetting; baseWait/maxWait bound the artificial
// delay (baseWait * 2^(failures-1), capped at maxWait).
func NewLimiter(window, baseWait, maxWait time.Duration) *Limiter {
return &Limiter{
attempts: make(map[string]*attemptState),
window: window,
baseWait: baseWait,
maxWait: maxWait,
}
}
// Delay returns how long the caller should sleep before processing the
// next attempt for key, based on how many recent failures it has seen.
// It does not itself sleep or record anything — call RecordFailure /
// RecordSuccess to update state.
func (l *Limiter) Delay(key string) time.Duration {
l.mu.Lock()
defer l.mu.Unlock()
st, ok := l.attempts[key]
if !ok || time.Since(st.lastSeen) > l.window {
return 0
}
if st.count <= 0 {
return 0
}
wait := l.baseWait << (st.count - 1) // baseWait * 2^(count-1)
if wait > l.maxWait || wait <= 0 { // <=0 guards against overflow
wait = l.maxWait
}
return wait
}
// RecordFailure increments key's failure streak.
func (l *Limiter) RecordFailure(key string) {
l.mu.Lock()
defer l.mu.Unlock()
st, ok := l.attempts[key]
if !ok || time.Since(st.lastSeen) > l.window {
st = &attemptState{}
l.attempts[key] = st
}
st.count++
st.lastSeen = time.Now()
}
// RecordSuccess clears key's failure streak.
func (l *Limiter) RecordSuccess(key string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.attempts, key)
}