129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// LoginRateLimiter tracks failed login attempts per key (IP or username) and enforces backoff.
|
|
type LoginRateLimiter struct {
|
|
mu sync.Mutex
|
|
attempts map[string][]time.Time
|
|
// config
|
|
maxAttempts int
|
|
window time.Duration
|
|
blockDuration time.Duration
|
|
}
|
|
|
|
func NewLoginRateLimiter(maxAttempts int, window time.Duration, blockDuration time.Duration) *LoginRateLimiter {
|
|
return &LoginRateLimiter{
|
|
attempts: make(map[string][]time.Time),
|
|
maxAttempts: maxAttempts,
|
|
window: window,
|
|
blockDuration: blockDuration,
|
|
}
|
|
}
|
|
|
|
// Allow returns true if the key is allowed to attempt login now.
|
|
// It also cleans up old entries.
|
|
func (r *LoginRateLimiter) Allow(key string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
now := time.Now()
|
|
times := r.attempts[key]
|
|
// filter within window
|
|
var filtered []time.Time
|
|
for _, t := range times {
|
|
if now.Sub(t) < r.window {
|
|
filtered = append(filtered, t)
|
|
}
|
|
}
|
|
r.attempts[key] = filtered
|
|
if len(filtered) >= r.maxAttempts {
|
|
// Check if still within block duration from last attempt
|
|
last := filtered[len(filtered)-1]
|
|
if now.Sub(last) < r.blockDuration {
|
|
return false
|
|
}
|
|
// block expired, allow and reset
|
|
r.attempts[key] = nil
|
|
return true
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RecordFailure records a failed attempt for key.
|
|
func (r *LoginRateLimiter) RecordFailure(key string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.attempts[key] = append(r.attempts[key], time.Now())
|
|
}
|
|
|
|
// RecordSuccess clears failures for key.
|
|
func (r *LoginRateLimiter) RecordSuccess(key string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.attempts, key)
|
|
}
|
|
|
|
// DefaultLoginLimiter is a global limiter: 5 failures per minute, block for 30s after
|
|
var DefaultLoginLimiter = NewLoginRateLimiter(5, time.Minute, 30*time.Second)
|
|
|
|
// ConfigOperationLimiter tracks config write operations per IP to prevent abuse.
|
|
type ConfigOperationLimiter struct {
|
|
mu sync.Mutex
|
|
attempts map[string][]time.Time
|
|
maxAttempts int
|
|
window time.Duration
|
|
blockDuration time.Duration
|
|
}
|
|
|
|
func NewConfigOperationLimiter(maxAttempts int, window time.Duration, blockDuration time.Duration) *ConfigOperationLimiter {
|
|
return &ConfigOperationLimiter{
|
|
attempts: make(map[string][]time.Time),
|
|
maxAttempts: maxAttempts,
|
|
window: window,
|
|
blockDuration: blockDuration,
|
|
}
|
|
}
|
|
|
|
// Allow returns true if the key is allowed to perform a config operation now.
|
|
func (r *ConfigOperationLimiter) Allow(key string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
now := time.Now()
|
|
times := r.attempts[key]
|
|
var filtered []time.Time
|
|
for _, t := range times {
|
|
if now.Sub(t) < r.window {
|
|
filtered = append(filtered, t)
|
|
}
|
|
}
|
|
r.attempts[key] = filtered
|
|
if len(filtered) >= r.maxAttempts {
|
|
last := filtered[len(filtered)-1]
|
|
if now.Sub(last) < r.blockDuration {
|
|
return false
|
|
}
|
|
r.attempts[key] = nil
|
|
return true
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RecordFailure records a failed config operation attempt.
|
|
func (r *ConfigOperationLimiter) RecordFailure(key string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.attempts[key] = append(r.attempts[key], time.Now())
|
|
}
|
|
|
|
// RecordSuccess clears failures for key.
|
|
func (r *ConfigOperationLimiter) RecordSuccess(key string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.attempts, key)
|
|
}
|
|
|
|
// DefaultConfigLimiter is a global limiter: 10 config operations per minute, block for 1 minute after
|
|
var DefaultConfigLimiter = NewConfigOperationLimiter(10, time.Minute, time.Minute)
|