| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- package server
- import (
- "sync"
- "time"
- )
- // loginRateLimiter blunts online brute-force attacks by temporarily locking a
- // key (client IP + username) after too many consecutive failed logins.
- type loginRateLimiter struct {
- mu sync.Mutex
- attempts map[string]*loginAttempt
- maxFailures int
- window time.Duration
- lockout time.Duration
- now func() time.Time
- }
- type loginAttempt struct {
- failures int
- firstFail time.Time
- lockedUntil time.Time
- }
- func newLoginRateLimiter() *loginRateLimiter {
- return &loginRateLimiter{
- attempts: make(map[string]*loginAttempt),
- maxFailures: 5,
- window: 10 * time.Minute,
- lockout: 10 * time.Minute,
- now: time.Now,
- }
- }
- // checkLocked reports whether the key is currently locked and for how much longer.
- func (l *loginRateLimiter) checkLocked(key string) (time.Duration, bool) {
- l.mu.Lock()
- defer l.mu.Unlock()
- attempt, ok := l.attempts[key]
- if !ok {
- return 0, false
- }
- now := l.now()
- if now.Before(attempt.lockedUntil) {
- return attempt.lockedUntil.Sub(now), true
- }
- return 0, false
- }
- // recordFailure registers a failed attempt and locks the key once the failure
- // threshold is reached within the window. It returns the lockout duration when
- // a lock is newly applied.
- func (l *loginRateLimiter) recordFailure(key string) (time.Duration, bool) {
- l.mu.Lock()
- defer l.mu.Unlock()
- now := l.now()
- attempt, ok := l.attempts[key]
- if !ok || now.Sub(attempt.firstFail) > l.window {
- attempt = &loginAttempt{firstFail: now}
- l.attempts[key] = attempt
- }
- attempt.failures++
- l.pruneLocked(now)
- if attempt.failures >= l.maxFailures {
- attempt.lockedUntil = now.Add(l.lockout)
- attempt.failures = 0
- attempt.firstFail = now
- return l.lockout, true
- }
- return 0, false
- }
- func (l *loginRateLimiter) recordSuccess(key string) {
- l.mu.Lock()
- defer l.mu.Unlock()
- delete(l.attempts, key)
- }
- // pruneLocked drops entries that are neither locked nor accumulating, so the
- // map stays bounded. Callers must hold the lock.
- func (l *loginRateLimiter) pruneLocked(now time.Time) {
- if len(l.attempts) < 1024 {
- return
- }
- for key, attempt := range l.attempts {
- if now.After(attempt.lockedUntil) && now.Sub(attempt.firstFail) > l.window {
- delete(l.attempts, key)
- }
- }
- }
|