;;;; limiter.lisp - Rate limiter definitions for the application (in-package :asteroid) (defun render-rate-limit-error-page() (clip:process-to-string (load-template "error") :error-message "It seems that your acceleration has elevated your orbit out of your designated path." :error-action "Please wait a moment for it to stabilize and try your request again.")) (defun api-limit-error-output () (api-output `(("status" . "error") ("message" . "It seems that your acceleration has elevated your orbit out of your designated path.")) :message "It seems that your acceleration has elevated your orbit out of your designated path." :status 429)) (defun extract-limit-options (options) "Extracts the rate-limit options and forwards the reamaining radiance route options" (let ((limit (getf options :limit)) (timeout (getf options :timeout)) (group (getf options :limit-group)) (rest (loop for (k v) on options by #'cddr unless (member k '(:limit :timeout :limit-group)) append (list k v)))) (values limit timeout group rest))) (defmacro define-page-with-limit (name uri options &body body) "Rate limit for a page route. Defaults to 30 requests per minute." (multiple-value-bind (limit timeout group rest) (extract-limit-options options) (let* ((limit-name (string-upcase (format nil "~a-route-limit" (or group name)))) (limit-sym (intern limit-name)) (limit (or limit 30)) (timeout (or timeout 60))) `(eval-when (:compile-toplevel :load-toplevel :execute) (rate:define-limit ,limit-sym (time-left :limit ,limit :timeout ,timeout) ;; (format t "Route limit '~a' hit. Wait ~a seconds and retry.~%" ,(string name) time-left) (render-rate-limit-error-page)) (define-page ,name ,uri ,rest (rate:with-limitation (,limit-sym) ,@body)))))) (defmacro define-api-with-limit (name args options &body body) "Rate limit for api routes. Defaults to 60 requests per minute." (multiple-value-bind (limit timeout group rest) (extract-limit-options options) (let* ((limit-name (string-upcase (format nil "~a-api-limit" (or group name)))) (limit-sym (intern limit-name)) (limit (or limit 60)) (timeout (or timeout 60))) `(eval-when (:compile-toplevel :load-toplevel :execute) (rate:define-limit ,limit-sym (time-left :limit ,limit :timeout ,timeout) ;; (format t "API Rate limit '~a' hit. Wait ~a seconds and retry.~%" ,(string name) time-left) (api-limit-error-output)) (define-api ,name ,args ,rest (rate:with-limitation (,limit-sym) ,@body))))))