98 lines
2.6 KiB
Go
98 lines
2.6 KiB
Go
// Dieses File implementiert ein einfaches In-Memory-Rate-Limiting für den Login —
|
|
// ohne das würde ein Angreifer beliebig oft Zugriffskeys durchprobieren können.
|
|
//
|
|
// Fixed-Window-Zähler pro Client-IP: nach loginRateLimitMax fehlgeschlagenen
|
|
// Versuchen innerhalb von loginRateLimitWindow muss die IP bis zum Fensterende warten.
|
|
// Ein erfolgreicher Login setzt den Zähler der IP sofort zurück.
|
|
package handler
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
loginRateLimitMax = 5 // erlaubte Fehlversuche pro Fenster
|
|
loginRateLimitWindow = 1 * time.Minute // Fenstergröße
|
|
)
|
|
|
|
// attemptRecord zählt Fehlversuche einer IP innerhalb eines Zeitfensters.
|
|
type attemptRecord struct {
|
|
count int
|
|
windowEnds time.Time
|
|
}
|
|
|
|
// LoginRateLimiter begrenzt Login-Versuche pro Client-IP.
|
|
type LoginRateLimiter struct {
|
|
mu sync.Mutex
|
|
attempts map[string]*attemptRecord
|
|
}
|
|
|
|
// NewLoginRateLimiter erstellt einen Rate-Limiter und startet einen Hintergrund-Job
|
|
// der abgelaufene Einträge regelmäßig entfernt (verhindert unbegrenztes Wachstum
|
|
// der Map bei vielen verschiedenen IPs, z.B. durch Scanner/Bots).
|
|
func NewLoginRateLimiter() *LoginRateLimiter {
|
|
l := &LoginRateLimiter{attempts: make(map[string]*attemptRecord)}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(10 * time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
l.cleanup()
|
|
}
|
|
}()
|
|
|
|
return l
|
|
}
|
|
|
|
// Allow prüft ob die IP noch einen Versuch frei hat, ohne ihn zu verbrauchen.
|
|
// retryAfter gibt an wie lange bei einer Sperre noch gewartet werden muss.
|
|
func (l *LoginRateLimiter) Allow(ip string) (ok bool, retryAfter time.Duration) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
rec, exists := l.attempts[ip]
|
|
now := time.Now()
|
|
|
|
if !exists || now.After(rec.windowEnds) {
|
|
return true, 0
|
|
}
|
|
if rec.count >= loginRateLimitMax {
|
|
return false, rec.windowEnds.Sub(now)
|
|
}
|
|
return true, 0
|
|
}
|
|
|
|
// RecordFailure zählt einen fehlgeschlagenen Login-Versuch für die IP.
|
|
func (l *LoginRateLimiter) RecordFailure(ip string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
rec, exists := l.attempts[ip]
|
|
if !exists || now.After(rec.windowEnds) {
|
|
rec = &attemptRecord{windowEnds: now.Add(loginRateLimitWindow)}
|
|
l.attempts[ip] = rec
|
|
}
|
|
rec.count++
|
|
}
|
|
|
|
// Reset löscht den Zähler einer IP nach einem erfolgreichen Login.
|
|
func (l *LoginRateLimiter) Reset(ip string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
delete(l.attempts, ip)
|
|
}
|
|
|
|
// cleanup entfernt abgelaufene Einträge aus der Map.
|
|
func (l *LoginRateLimiter) cleanup() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
for ip, rec := range l.attempts {
|
|
if now.After(rec.windowEnds) {
|
|
delete(l.attempts, ip)
|
|
}
|
|
}
|
|
}
|