package api import ( "net" "net/http" "sync" "time" ) // loginLimiter limita tentativas de login por IP em janela fixa, em memória. // Escopo deliberado: instância única (sem cluster). Atrás de proxy reverso o // RemoteAddr é o do proxy — o limite vira global, o que só torna o limite mais // rígido; NÃO confiamos em X-Forwarded-For (spoofável). type loginLimiter struct { mu sync.Mutex fails map[string]*loginBucket max int window time.Duration } type loginBucket struct { count int resetAt time.Time } func newLoginLimiter(max int, window time.Duration) *loginLimiter { return &loginLimiter{fails: map[string]*loginBucket{}, max: max, window: window} } // remoteIP extrai o host de RemoteAddr sem depender de headers do cliente. func remoteIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host } // allow diz se o IP ainda pode tentar; não consome tentativa. func (l *loginLimiter) allow(ip string) bool { l.mu.Lock() defer l.mu.Unlock() l.sweep() b, ok := l.fails[ip] if !ok { return true } if time.Now().After(b.resetAt) { delete(l.fails, ip) return true } return b.count < l.max } // fail registra uma tentativa errada. func (l *loginLimiter) fail(ip string) { l.mu.Lock() defer l.mu.Unlock() l.sweep() b, ok := l.fails[ip] if !ok || time.Now().After(b.resetAt) { l.fails[ip] = &loginBucket{count: 1, resetAt: time.Now().Add(l.window)} return } b.count++ } // success limpa o histórico do IP após login correto. func (l *loginLimiter) success(ip string) { l.mu.Lock() delete(l.fails, ip) l.mu.Unlock() } // sweep evita crescimento ilimitado do mapa com IPs variados. func (l *loginLimiter) sweep() { if len(l.fails) < 8192 { return } now := time.Now() for ip, b := range l.fails { if now.After(b.resetAt) { delete(l.fails, ip) } } }