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)
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information