;;;; ratelimit.lisp --- Thread-safe sliding-window rate limiter. ;;;; ;;;; SPDX-License-Identifier: MIT ;;;; ;;;; Used to bound the login endpoint: a per-IP attempt cap (checked BEFORE the ;;;; expensive Argon2i verify, so it also caps unauthenticated CPU/memory use) ;;;; and a per-account failure cap (lockout against password brute-force). (in-package #:usher) (defstruct limiter (lock (bordeaux-threads:make-lock "usher-limiter")) (table (make-hash-table :test 'equal))) ; key -> list of unix timestamps (defun %prune (events window now) (remove-if (lambda (ts) (< ts (- now window))) events)) (defun limiter-count (limiter key window &optional (now (unix-now))) "Number of events recorded for KEY within the last WINDOW seconds." (bordeaux-threads:with-lock-held ((limiter-lock limiter)) (length (%prune (gethash key (limiter-table limiter)) window now)))) (defun limiter-add (limiter key window &optional (now (unix-now))) "Record an event for KEY (pruning anything older than WINDOW). Returns the resulting count within the window." (bordeaux-threads:with-lock-held ((limiter-lock limiter)) (let ((events (cons now (%prune (gethash key (limiter-table limiter)) window now)))) (setf (gethash key (limiter-table limiter)) events) (length events)))) (defun limiter-clear (limiter key) "Forget all events for KEY (e.g. after a successful login)." (bordeaux-threads:with-lock-held ((limiter-lock limiter)) (remhash key (limiter-table limiter)))) (defun limiter-blocked-p (limiter key max window &optional (now (unix-now))) "True when KEY has reached MAX events within WINDOW seconds." (>= (limiter-count limiter key window now) max))