107 lines
2.3 KiB
Go
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()
|
|
}
|