Add rate-limiting feature
- Set a global rate limit (in requests per second)
- Option: If an IP hits the rate limit a specified number of times (e.g. 10 in an hour), it's automatically blocked
- Set a global cooldown for those auto-added IPs (or UAs; see #59)
- Create an allow-list of IPs exempt from the rate limiter
Middleware MVP:
import logging
from django.conf import settings
from django.core.cache import cache
from django.http import HttpRequest, HttpResponse
from django_blocklist.utils import update_blocklist
logger = logging.getLogger(__name__)
BLOCKLIST_THRESHHOLD = settings.BLOCKLIST_CONFIG.get("ratelimit-auto-block", 100)
MAX_PER_SECOND = settings.BLOCKLIST_CONFIG.get("requests-per-second", 1)
CACHE_KEY_TEMPLATE = "django-blocklist-ratelimit-tracking-{}"
def user_ip_from_request(request: HttpRequest) -> str:
for key in ["HTTP_X_REAL_IP", "REMOTE_ADDR"]:
if ip := request.META.get(key):
return ip
# `HTTP_X_FORWARDED_FOR` is a comma-separated list with originating IP first
if forwarded := request.META.get("HTTP_X_FORWARDED_FOR"):
return forwarded.split(",")[0]
logger.warning("No requesting IP address found: {}".format(request.META))
return ""
def ip_and_count_and_key_for_requesting_ip(request):
ip = user_ip_from_request(request)
key = CACHE_KEY_TEMPLATE.format(ip)
count = cache.get(key, 0)
return ip, count, key
class RateLimitMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest):
ip, count, key = ip_and_count_and_key_for_requesting_ip(request)
count += 1
cache.set(key, count, timeout=1)
if count > MAX_PER_SECOND:
if count >= BLOCKLIST_THRESHHOLD:
update_blocklist({ip}, reason="429", cooldown=1)
logger.warning(
f"Added {ip} to blocklist after {count} rejections -- {request.method} {request.path}"
)
# retry_after should be an int >= 1
retry_after = max(1, int(1 / MAX_PER_SECOND))
return HttpResponse(
f"You have exceeded our rate limit of {MAX_PER_SECOND} request per second. Please pause at least {retry_after} seconds between requests.",
headers={"retry-after": retry_after},
status=429,
)
return self.get_response(request)