Cache-based rate-limiting for Django.
Project description
Django Brake provides a decorator to rate-limit views. Limiting can be based on IP address or a field in the request–either a GET or POST variable.
If the rate limit is exceded, either a 403 Forbidden can be sent, or the request can be annotated with a limited attribute, allowing you to take another action like adding a captcha to a form.
This is a fork of Django Ratelimit, to support:
: Django 1.3 and above : : Multiple buckets (e.g. separate endpoints) : : Allow for multiple time thresholds (periods) per bucket :
The intention is to remain API compliant with Django Ratelimit.
Using Django Brake
from brake.decorators import ratelimit is the biggest thing you need to do. The @ratelimit decorator provides several optional arguments with sensible defaults (in italics).
- ip:
Whether to rate-limit based on the IP. True
- block:
Whether to block the request instead of annotating. False
- method:
Which HTTP method(s) to rate-limit. May be a string or a list. all
- field:
Which HTTP field(s) to use to rate-limit. May be a string or a list. none
- rate:
The number of requests per unit time allowed. 5/m
Examples
@ratelimit() def myview(request): # Will be true if the same IP makes more than 5 requests/minute. was_limited = getattr(request, 'limited', False) return HttpResponse() @ratelimit(block=True) def myview(request): # If the same IP makes >5 reqs/min, will return HttpResponseForbidden return HttpResponse() @ratelimit(field='username') def login(request): # If the same username OR IP is used >5 times/min, this will be True. # The `username` value will come from GET or POST, determined by the # request method. was_limited = getattr(request, 'limited', False) return HttpResponse() @ratelimit(method='POST') def login(request): # Only apply rate-limiting to POSTs. return HttpResponseRedirect() @ratelimit(field=['username', 'other_field']) def login(request): # Use multiple field values. return HttpResponse() @ratelimit(rate='1/m') @ratelimit(rate='10/h') @ratelimit(rate='100/d') def slow(request): # Allow 1 reqs/min, 10 per hour, and 100 per day. return HttpResponse() # ## Example Login Code to *only* block login failures ## @ratelimit(field='username', method='POST', rate='5/m') @ratelimit(field='username', method='POST', rate='10/h') @ratelimit(field='username', method='POST', rate='20/d') def ratelimit_login(request): """Increment cache counters, 403 if over limit.""" was_limited = getattr(request, 'limited', False) if was_limited: request.flash['error'] = 'You have been ratelimited' # Log the error, etc. return http.HttpResponseRedirect(urlresolvers.reverse( 'auth_login' )) def login(request): """Just a regular django login flow.""" form = forms.AuthenticationForm() if form.method == 'POST': form = forms.AuthenticationForm(data=request.POST): if form.is_valid() # Log in user and redirect to next page. # Login information was not correct. ratelimit_login(request)
Implementation Details:
Some Required Customization
By default we only track the IP that we get form request.META[‘HOST_ADDR’]. Unless your webservers are sitting directly on routable IPs and have no loadbalancers or upstream proxies, this is probably not what you want!
Since this is a deployment detail, we leave this up to those who choose to implement Django Brake. You do so with a simple bit of Inheritence and override.
from brake.backends import cachebe MyBrake(cachebe.CacheBackend): def get_ip(request): return request.META.get('HTTP_TRUE_CLIENT_IP', 'HOST_ADDR')
Internals
These are variables which you do not need to modify directly, but are essential to the functioning of Brake
- function_name:
This is the name of the function decorated with Brake; this allows us to separate into different “buckets” for each view. This is automatically added and doesn’t need to be specified.
- period:
This is derrived from the rate information passed in as a string. It’s the number of seconds for which the increment on a bucket + period will be valid. It sets the TTL in memcache.
The cache key structure from one bad login attempt from our example above would look something like this:
# The form value derived counters: rl:func:<function_name>:period:<60>:field:<username>:<sha1 of username> rl:func:<function_name>:period:<3600>:field:<username>:<sha1 of username> rl:func:<function_name>:period:<86400>:field:<username>:<sha1 of username> # The IP derived counters: rl:func:<function_name>:period:<60>:ip:<ip_address> rl:func:<function_name>:period:<3600>:ip:<ip_address> rl:func:<function_name>:period:<86500>:ip:<ip_address>
All period numbers are equivilent to the TTL for that key.
If any of these thresholds are passed, then the view will 403. This is a huge improvement in terms of usablity and security of many existing ratelimiting applications.
Acknowledgements
Thanks to James Socol (jsocol) on Github. A vast majority of the work on this project is his (django-ratelimit).
Also thanks to Simon Willison’s ratelimitcache, on which Jsocol’s version of this library is largly based.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.