package httpapi import ( "sync" "time" ) const loginFailureLimit = 10 type loginAttempt struct { failures int windowStart time.Time blockedTill time.Time } type loginLimiter struct { mu sync.Mutex attempts map[string]loginAttempt now func() time.Time } type queryLimitEntry struct { windowStart time.Time count int } // queryLimiter is a per-process fixed-window safety limit. Expired entries are // removed opportunistically so unique keys do not accumulate indefinitely. type queryLimiter struct { mu sync.Mutex entries map[string]queryLimitEntry now func() time.Time limit int window time.Duration nextCleanup time.Time } func newQueryLimiter(now func() time.Time, limit int, window time.Duration) *queryLimiter { if limit <= 0 || window <= 0 { return nil } return &queryLimiter{ entries: make(map[string]queryLimitEntry), now: now, limit: limit, window: window, } } func (l *queryLimiter) allow(key string) (bool, time.Duration) { if l == nil || l.limit <= 0 || l.window <= 0 { return true, 0 } l.mu.Lock() defer l.mu.Unlock() now := l.now() if l.nextCleanup.IsZero() || !now.Before(l.nextCleanup) { for entryKey, existing := range l.entries { if now.Sub(existing.windowStart) >= l.window { delete(l.entries, entryKey) } } l.nextCleanup = now.Add(l.window) } entry, ok := l.entries[key] if !ok || now.Sub(entry.windowStart) >= l.window { entry = queryLimitEntry{windowStart: now} } if entry.count >= l.limit { retry := l.window - now.Sub(entry.windowStart) if retry < 0 { retry = 0 } return false, retry } entry.count++ l.entries[key] = entry return true, 0 } func newLoginLimiter(now func() time.Time) *loginLimiter { return &loginLimiter{attempts: make(map[string]loginAttempt), now: now} } func (l *loginLimiter) allow(key string) (bool, time.Duration) { l.mu.Lock() defer l.mu.Unlock() now := l.now() attempt, ok := l.attempts[key] if !ok { return true, 0 } if now.Before(attempt.blockedTill) { return false, attempt.blockedTill.Sub(now) } if now.Sub(attempt.windowStart) > 10*time.Minute { delete(l.attempts, key) } return true, 0 } func (l *loginLimiter) failure(key string) { l.mu.Lock() defer l.mu.Unlock() now := l.now() attempt := l.attempts[key] if attempt.windowStart.IsZero() || now.Sub(attempt.windowStart) > 10*time.Minute { attempt = loginAttempt{windowStart: now} } attempt.failures++ if attempt.failures >= loginFailureLimit { attempt.blockedTill = now.Add(10 * time.Minute) } l.attempts[key] = attempt } func (l *loginLimiter) success(key string) { l.mu.Lock() delete(l.attempts, key) l.mu.Unlock() }