Traefik_Control/backend/internal/auth/ratelimit.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)