NewsBlur/utils/ratelimit.py

111 lines
3.6 KiB
Python
Raw Normal View History

2011-11-22 12:51:51 -05:00
import functools
import hashlib
2024-04-24 09:50:42 -04:00
from datetime import datetime, timedelta
from django.core.cache import cache
from django.http import HttpResponse
class ratelimit(object):
"Instances of this class can be used as decorators"
# This class is designed to be sub-classed
2024-04-24 09:43:56 -04:00
minutes = 1 # The time period
requests = 4 # Number of allowed requests in that time period
use_path = False # Whether to include the request path in the key
2024-04-24 09:43:56 -04:00
prefix = "rl-" # Prefix for memcache key
def __init__(self, **options):
for key, value in options.items():
setattr(self, key, value)
2024-04-24 09:43:56 -04:00
def __call__(self, fn):
def wrapper(request, *args, **kwargs):
return self.view_wrapper(request, fn, *args, **kwargs)
2024-04-24 09:43:56 -04:00
functools.update_wrapper(wrapper, fn)
return wrapper
2024-04-24 09:43:56 -04:00
def view_wrapper(self, request, fn, *args, **kwargs):
if not self.should_ratelimit(request):
return fn(request, *args, **kwargs)
2024-04-24 09:43:56 -04:00
2020-06-19 02:27:48 -04:00
counts = list(self.get_counters(request).values())
2024-04-24 09:43:56 -04:00
# Increment rate limiting counter
self.cache_incr(self.current_key(request))
2024-04-24 09:43:56 -04:00
# Have they failed?
if sum(counts) >= self.requests:
return self.disallowed(request)
2024-04-24 09:43:56 -04:00
return fn(request, *args, **kwargs)
2024-04-24 09:43:56 -04:00
def cache_get_many(self, keys):
return cache.get_many(keys)
2024-04-24 09:43:56 -04:00
def cache_incr(self, key):
# memcache is only backend that can increment atomically
try:
# add first, to ensure the key exists
2011-11-08 16:11:56 -08:00
cache.add(key, 0, self.expire_after())
cache.incr(key)
except (AttributeError, ValueError):
cache.set(key, cache.get(key, 0) + 1, self.expire_after())
2024-04-24 09:43:56 -04:00
def should_ratelimit(self, request):
return True
2024-04-24 09:43:56 -04:00
def get_counters(self, request):
return self.cache_get_many(self.keys_to_check(request))
2024-04-24 09:43:56 -04:00
def keys_to_check(self, request):
extra = self.key_extra(request)
now = datetime.now()
return [
2024-04-24 09:43:56 -04:00
"%s%s-%s" % (self.prefix, extra, (now - timedelta(minutes=minute)).strftime("%Y%m%d%H%M"))
for minute in range(self.minutes + 1)
]
2024-04-24 09:43:56 -04:00
def current_key(self, request):
2024-04-24 09:43:56 -04:00
return "%s%s-%s" % (self.prefix, self.key_extra(request), datetime.now().strftime("%Y%m%d%H%M"))
def key_extra(self, request):
2024-04-24 09:43:56 -04:00
key = getattr(request.session, "session_key", "")
if not key:
2024-04-24 09:43:56 -04:00
key = request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0]
if not key:
2024-04-24 09:43:56 -04:00
key = request.COOKIES.get("newsblur_sessionid", "")
if not key:
2024-04-24 09:43:56 -04:00
key = request.META.get("HTTP_USER_AGENT", "")
# Add request path to the key if use_path is enabled
if getattr(self, 'use_path', False):
path = request.path
key = f"{key}-{path}"
return key
2024-04-24 09:43:56 -04:00
def disallowed(self, request):
2024-04-24 09:43:56 -04:00
return HttpResponse("Rate limit exceeded", status=429)
def expire_after(self):
"Used for setting the memcached cache expiry"
return (self.minutes + 1) * 60
2024-04-24 09:43:56 -04:00
class ratelimit_post(ratelimit):
"Rate limit POSTs - can be used to protect a login form"
2024-04-24 09:43:56 -04:00
key_field = None # If provided, this POST var will affect the rate limit
def should_ratelimit(self, request):
2024-04-24 09:43:56 -04:00
return request.method == "POST"
def key_extra(self, request):
# IP address and key_field (if it is set)
extra = super(ratelimit_post, self).key_extra(request)
if self.key_field:
2024-04-24 09:43:56 -04:00
value = hashlib.sha1((request.POST.get(self.key_field, "")).encode("utf-8")).hexdigest()
extra += "-" + value
return extra