88 lines
2.4 KiB
Go
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)
|
|
}
|