60 lines
1.1 KiB
Go
60 lines
1.1 KiB
Go
|
|
package api
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
loginAttemptsPerIP = 10
|
||
|
|
loginAttemptWindow = time.Minute
|
||
|
|
logoutBodyMaxBytes = 4096
|
||
|
|
)
|
||
|
|
|
||
|
|
// loginRateLimiter limits POST /api/auth/login attempts per IP per minute.
|
||
|
|
type loginRateLimiter struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
now func() time.Time
|
||
|
|
byIP map[string][]time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
func newLoginRateLimiter() *loginRateLimiter {
|
||
|
|
return &loginRateLimiter{
|
||
|
|
now: time.Now,
|
||
|
|
byIP: make(map[string][]time.Time),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// allow reserves a slot for this request; false means rate limit exceeded.
|
||
|
|
func (l *loginRateLimiter) allow(ip string) bool {
|
||
|
|
l.mu.Lock()
|
||
|
|
defer l.mu.Unlock()
|
||
|
|
now := l.now()
|
||
|
|
cutoff := now.Add(-loginAttemptWindow)
|
||
|
|
times := l.byIP[ip]
|
||
|
|
var kept []time.Time
|
||
|
|
for _, ts := range times {
|
||
|
|
if ts.After(cutoff) {
|
||
|
|
kept = append(kept, ts)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(kept) >= loginAttemptsPerIP {
|
||
|
|
l.byIP[ip] = kept
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
kept = append(kept, now)
|
||
|
|
l.byIP[ip] = kept
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func clientIPForLimiter(r *http.Request) string {
|
||
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||
|
|
if err != nil {
|
||
|
|
return strings.TrimSpace(r.RemoteAddr)
|
||
|
|
}
|
||
|
|
return host
|
||
|
|
}
|