package httpapi // ratelimit.go — Einfaches In-Memory-Rate-Limiting für POST /login (N1). // // Implementierung: Sliding-Window-Counter pro IP-Adresse. // Erlaubt maximal loginMaxAttempts Versuche pro loginWindow. // Ältere Einträge werden periodisch aus der Map bereinigt. import ( "net" "net/http" "sync" "time" ) const ( loginMaxAttempts = 5 loginWindow = 1 * time.Minute cleanupInterval = 5 * time.Minute ) type loginAttempt struct { count int windowEnd time.Time } type loginRateLimiter struct { mu sync.Mutex entries map[string]*loginAttempt } func newLoginRateLimiter() *loginRateLimiter { rl := &loginRateLimiter{ entries: make(map[string]*loginAttempt), } go rl.cleanup() return rl } // Allow returns true if the IP is within the rate limit, false if it should be blocked. func (rl *loginRateLimiter) Allow(ip string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() e, ok := rl.entries[ip] if !ok || now.After(e.windowEnd) { // Neues Fenster. rl.entries[ip] = &loginAttempt{count: 1, windowEnd: now.Add(loginWindow)} return true } e.count++ return e.count <= loginMaxAttempts } // cleanup bereinigt abgelaufene Einträge periodisch. func (rl *loginRateLimiter) cleanup() { ticker := time.NewTicker(cleanupInterval) defer ticker.Stop() for range ticker.C { rl.mu.Lock() now := time.Now() for ip, e := range rl.entries { if now.After(e.windowEnd) { delete(rl.entries, ip) } } rl.mu.Unlock() } } // LoginRateLimit ist eine globale Instanz des Rate-Limiters (package-level Singleton). var LoginRateLimit = newLoginRateLimiter() // RateLimitLogin ist Middleware, die Brute-Force-Angriffe auf den Login-Endpoint verhindert. // Bei Überschreitung wird 429 Too Many Requests zurückgegeben. func RateLimitLogin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // IP-Adresse extrahieren (berücksichtigt X-Forwarded-For nicht, um Spoofing zu vermeiden). ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { ip = r.RemoteAddr } if !LoginRateLimit.Allow(ip) { http.Error(w, "Zu viele Anmeldeversuche. Bitte warte eine Minute.", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) }