Files
XQKqueue/server/internal/httpapi/limiter.go
2026-07-15 11:26:37 +08:00

107 lines
2.3 KiB
Go

package httpapi
import (
"sync"
"time"
)
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 deliberately small and in-memory for the temporary public
// phone lookup. The production replacement will be an OTP or external
// identity provider, so this limiter is only a safety net for the test flow.
type queryLimiter struct {
mu sync.Mutex
entries map[string]queryLimitEntry
now func() time.Time
limit int
window time.Duration
}
func newQueryLimiter(now func() time.Time, limit int, window time.Duration) *queryLimiter {
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()
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 >= 5 {
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()
}