76ba500417
CI / docker (push) Successful in 2m4s
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
1.9 KiB
Go
94 lines
1.9 KiB
Go
package loginlimit
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Limiter tracks failed admin login attempts per client IP.
|
|
type Limiter struct {
|
|
mu sync.Mutex
|
|
attempts map[string]*record
|
|
maxAttempts int
|
|
lockout time.Duration
|
|
}
|
|
|
|
type record struct {
|
|
failures int
|
|
lockedUntil time.Time
|
|
}
|
|
|
|
func New(maxAttempts int, lockout time.Duration) *Limiter {
|
|
if maxAttempts <= 0 {
|
|
maxAttempts = 5
|
|
}
|
|
if lockout <= 0 {
|
|
lockout = 15 * time.Minute
|
|
}
|
|
return &Limiter{
|
|
attempts: make(map[string]*record),
|
|
maxAttempts: maxAttempts,
|
|
lockout: lockout,
|
|
}
|
|
}
|
|
|
|
// Check reports whether the IP is locked and how long to wait.
|
|
func (l *Limiter) Check(ip string) (locked bool, retryAfter time.Duration) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
r := l.attempts[ip]
|
|
if r == nil {
|
|
return false, 0
|
|
}
|
|
now := time.Now()
|
|
if now.Before(r.lockedUntil) {
|
|
return true, r.lockedUntil.Sub(now)
|
|
}
|
|
if r.failures >= l.maxAttempts {
|
|
delete(l.attempts, ip)
|
|
}
|
|
return false, 0
|
|
}
|
|
|
|
// RecordFailure increments the failure count and locks the IP when the limit is reached.
|
|
func (l *Limiter) RecordFailure(ip string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
now := time.Now()
|
|
r := l.attempts[ip]
|
|
if r == nil {
|
|
r = &record{}
|
|
l.attempts[ip] = r
|
|
}
|
|
if now.Before(r.lockedUntil) {
|
|
return
|
|
}
|
|
if r.failures >= l.maxAttempts && !r.lockedUntil.IsZero() && now.After(r.lockedUntil) {
|
|
r.failures = 0
|
|
r.lockedUntil = time.Time{}
|
|
}
|
|
r.failures++
|
|
if r.failures >= l.maxAttempts {
|
|
r.lockedUntil = now.Add(l.lockout)
|
|
}
|
|
}
|
|
|
|
// Configure updates rate limit parameters at runtime.
|
|
func (l *Limiter) Configure(maxAttempts int, lockout time.Duration) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if maxAttempts > 0 {
|
|
l.maxAttempts = maxAttempts
|
|
}
|
|
if lockout > 0 {
|
|
l.lockout = lockout
|
|
}
|
|
}
|
|
|
|
// Reset clears failure state after a successful login.
|
|
func (l *Limiter) Reset(ip string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
delete(l.attempts, ip)
|
|
}
|