Files
2026-10-25 20:24:50 -04:00

77 lines
1.6 KiB
Go
Executable File

package auth
import (
"sync"
"time"
)
const maxTrackedClients = 10000
type attemptWindow struct {
failures int
startedAt time.Time
}
type LoginLimiter struct {
mutex sync.Mutex
maxFailures int
window time.Duration
attempts map[string]*attemptWindow
}
func NewLoginLimiter(maxFailures int, window time.Duration) *LoginLimiter {
return &LoginLimiter{
maxFailures: maxFailures,
window: window,
attempts: make(map[string]*attemptWindow),
}
}
func (limiter *LoginLimiter) Allowed(client string) bool {
limiter.mutex.Lock()
defer limiter.mutex.Unlock()
attempt := limiter.current(client)
return attempt == nil || attempt.failures < limiter.maxFailures
}
func (limiter *LoginLimiter) RecordFailure(client string) {
limiter.mutex.Lock()
defer limiter.mutex.Unlock()
attempt := limiter.current(client)
if attempt == nil {
if len(limiter.attempts) >= maxTrackedClients {
limiter.removeExpired()
}
attempt = &attemptWindow{startedAt: time.Now()}
limiter.attempts[client] = attempt
}
attempt.failures++
}
func (limiter *LoginLimiter) Reset(client string) {
limiter.mutex.Lock()
defer limiter.mutex.Unlock()
delete(limiter.attempts, client)
}
func (limiter *LoginLimiter) current(client string) *attemptWindow {
attempt, found := limiter.attempts[client]
if !found {
return nil
}
if time.Since(attempt.startedAt) > limiter.window {
delete(limiter.attempts, client)
return nil
}
return attempt
}
func (limiter *LoginLimiter) removeExpired() {
for client, attempt := range limiter.attempts {
if time.Since(attempt.startedAt) > limiter.window {
delete(limiter.attempts, client)
}
}
}