Skip to content

Commit

Permalink
add ratelimit middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
sgmdlt committed Dec 27, 2024
1 parent e86de2e commit 6cf680b
Show file tree
Hide file tree
Showing 5 changed files with 604 additions and 0 deletions.
51 changes: 51 additions & 0 deletions config/middlewares.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import re
import socket
from typing import List, Pattern

from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse, HttpResponseForbidden


class GlobalHostRateLimitMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.requests_limit = getattr(settings, "RATELIMIT_REQUESTS", 1000)
self.timeframe = getattr(settings, "RATELIMIT_TIMEFRAME", 3600)
# Load blacklist patterns from settings
self.blacklist_patterns: List[Pattern] = []
blacklist = getattr(settings, "HOSTNAME_BLACKLIST", [])
for pattern in blacklist:
try:
self.blacklist_patterns.append(re.compile(pattern, re.IGNORECASE))
except re.error:
continue

def is_blacklisted(self, hostname: str) -> bool:
return any(pattern.search(hostname) for pattern in self.blacklist_patterns)

def __call__(self, request):
try:
ip = request.META.get("REMOTE_ADDR")
hostname = socket.gethostbyaddr(ip)[0]
except (socket.herror, socket.gaierror):
hostname = ip

if self.is_blacklisted(hostname):
return HttpResponseForbidden(
"Access denied due to hostname restrictions", content_type="text/plain"
)

cache_key = f"rl:global:{hostname}"
request_count = cache.get(cache_key, 0)

if request_count >= self.requests_limit:
return HttpResponse(
"Global rate limit exceeded", content_type="text/plain", status=429
)

# Use atomic increment
if not cache.add(cache_key, 1, timeout=self.timeframe):
cache.incr(cache_key)

return self.get_response(request)
6 changes: 6 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'config.middlewares.GlobalHostRateLimitMiddleware'
]


Expand Down Expand Up @@ -311,3 +312,8 @@
}
},
}

HOSTNAME_BLACKLIST = []

RATELIMIT_REQUESTS = int(os.getenv('RATELIMIT_REQUESTS'))
RATELIMIT_TIMEFRAME = int(os.getenv('RATELIMIT_TIMEFRAME'))
9 changes: 9 additions & 0 deletions contributors/tests/locustfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from locust import HttpUser, between, task


class WebsiteUser(HttpUser):
wait_time = between(1, 2)

@task
def load_test(self):
self.client.get("/")
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ dev-dependencies = [
"pre-commit>=4.0.1",
"pydot>=3.0.2",
"pyparsing>=3.2.0",
"locust>=2.32.5",
]
Loading

0 comments on commit 6cf680b

Please sign in to comment.