// 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) } } }