feat: add per-IP rate limiting to login endpoint
All checks were successful
check / check (push) Successful in 59s

Add a token-bucket rate limiter (golang.org/x/time/rate) that limits
login attempts per client IP on POST /api/v1/login. Returns 429 Too
Many Requests with a Retry-After header when the limit is exceeded.

Configurable via LOGIN_RATE_LIMIT (requests/sec, default 1) and
LOGIN_RATE_BURST (burst size, default 5). Stale per-IP entries are
automatically cleaned up every 10 minutes.

Only the login endpoint is rate-limited per sneak's instruction —
session creation and registration use hashcash proof-of-work instead.
This commit is contained in:
user
2026-03-17 02:26:59 -07:00
committed by clawbot
parent db3d23c224
commit 0bcc711a92
9 changed files with 443 additions and 5 deletions

View File

@@ -168,6 +168,21 @@ func (hdlr *Handlers) handleLogin(
writer http.ResponseWriter,
request *http.Request,
) {
ip := clientIP(request)
if !hdlr.loginLimiter.Allow(ip) {
writer.Header().Set(
"Retry-After", "1",
)
hdlr.respondError(
writer, request,
"too many login attempts, try again later",
http.StatusTooManyRequests,
)
return
}
type loginRequest struct {
Nick string `json:"nick"`
Password string `json:"password"`
@@ -198,6 +213,16 @@ func (hdlr *Handlers) handleLogin(
return
}
hdlr.executeLogin(
writer, request, payload.Nick, payload.Password,
)
}
func (hdlr *Handlers) executeLogin(
writer http.ResponseWriter,
request *http.Request,
nick, password string,
) {
remoteIP := clientIP(request)
hostname := resolveHostname(
@@ -207,8 +232,7 @@ func (hdlr *Handlers) handleLogin(
sessionID, clientID, token, err :=
hdlr.params.Database.LoginUser(
request.Context(),
payload.Nick,
payload.Password,
nick, password,
remoteIP, hostname,
)
if err != nil {
@@ -224,18 +248,18 @@ func (hdlr *Handlers) handleLogin(
hdlr.stats.IncrConnections()
hdlr.deliverMOTD(
request, clientID, sessionID, payload.Nick,
request, clientID, sessionID, nick,
)
// Initialize channel state so the new client knows
// which channels the session already belongs to.
hdlr.initChannelState(
request, clientID, sessionID, payload.Nick,
request, clientID, sessionID, nick,
)
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
"nick": payload.Nick,
"nick": nick,
"token": token,
}, http.StatusOK)
}