1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
from django.template import RequestContext
5
from django.http import HttpResponseRedirect, HttpResponseForbidden
6
from django.core.cache import cache
7
from django.conf import settings
8
from django.template.loader import render_to_string
9
from datetime import datetime, timedelta
11
from hashlib import sha1
14
def guest_required(func):
15
def _guest_required_decorator(request, *args, **kwargs):
16
if request.user.is_authenticated():
17
return HttpResponseRedirect('/')
19
return func(request, *args, **kwargs)
20
return _guest_required_decorator
24
def _dont_cache_decorator(request, *args, **kwargs):
25
response = func(request, *args, **kwargs)
26
response["Expires"] = "Tue, 03 Jul 2001 06:00:00 GMT"
27
response["Cache-Control"] = ("no-store, no-cache, "
28
"must-revalidate, max-age=0")
29
response['Pragma'] = 'no-cache'
31
return _dont_cache_decorator
34
def check_readonly(func):
35
""" A readonly aware decorator.
37
The decorated view will not be accessible at all during readonly mode.
38
Instead, a static warning page will be displayed.
40
def wrapper(request, *args, **kwargs):
41
if getattr(settings, 'READ_ONLY_MODE', False):
42
html = render_to_string('readonly.html',
44
context_instance=RequestContext(request))
45
return HttpResponseForbidden(html)
46
return func(request, *args, **kwargs)
47
functools.update_wrapper(wrapper, func)
51
class ratelimit(object):
52
""" A rate-limiting decorator.
53
Strongly based on Simon Willison's code,
54
http://github.com/simonw/ratelimitcache/blob/master/ratelimitcache.py
56
# This class is designed to be sub-classed
57
minutes = 2 # The time period
58
requests = 20 # Number of allowed requests in that time period
60
prefix = 'rl-' # Prefix for memcache key
62
def __init__(self, **options):
63
for key, value in options.items():
64
setattr(self, key, value)
66
def __call__(self, fn):
67
def wrapper(request, *args, **kwargs):
68
return self.view_wrapper(request, fn, *args, **kwargs)
69
functools.update_wrapper(wrapper, fn)
72
def view_wrapper(self, request, fn, *args, **kwargs):
73
if not self.should_ratelimit(request):
74
return fn(request, *args, **kwargs)
76
counts = self.get_counters(request).values()
77
# Increment rate limiting counter
78
self.cache_incr(self.current_key(request))
81
if sum(int(x) for x in counts) >= self.requests:
82
return self.disallowed(request)
84
return fn(request, *args, **kwargs)
86
def cache_get_many(self, keys):
87
return cache.get_many(keys)
89
def cache_incr(self, key):
90
# memcache is only backend that can increment atomically
92
# add first, to ensure the key exists
93
cache._cache.add(key, '0', time=self.expire_after())
94
cache._cache.incr(key)
95
except AttributeError:
96
cache.set(key, cache.get(key, 0) + 1, self.expire_after())
98
def should_ratelimit(self, request):
101
def get_counters(self, request):
102
return self.cache_get_many(self.keys_to_check(request))
104
def keys_to_check(self, request):
105
extra = self.key_extra(request)
111
(now - timedelta(minutes=minute)).strftime('%Y%m%d%H%M')
112
) for minute in range(self.minutes + 1)
115
def current_key(self, request):
118
self.key_extra(request),
119
datetime.now().strftime('%Y%m%d%H%M')
122
def key_extra(self, request):
123
# By default, their IP address is used
124
return request.META.get('REMOTE_ADDR', '')
126
def disallowed(self, request):
127
"Over-ride this method if you want to log incidents"
128
return HttpResponseForbidden(render_to_string('limitexceeded.html'))
130
def expire_after(self):
131
"Used for setting the memcached cache expiry"
132
return (self.minutes + 1) * 60
134
def reset_count(self, request):
135
"Reset the rate limiting limit count for a request"
136
for key in self.keys_to_check(request):
140
class ratelimit_post(ratelimit):
141
"Rate limit POSTs - can be used to protect a login form"
142
key_field = None # If provided, this POST var will affect the rate limit
144
def should_ratelimit(self, request):
145
return request.method == 'POST'
147
def key_extra(self, request):
148
# IP address and key_field (if it is set)
149
extra = super(ratelimit_post, self).key_extra(request)
151
value = sha1(request.POST.get(self.key_field, '')).hexdigest()
156
class limitlogin(ratelimit_post):
157
""" Limit login POSTs, per username.
158
Also, take default values from settings.
162
def __init__(self, **options):
164
'minutes': getattr(settings, 'LOGIN_LIMIT_MINUTES', 2),
165
'requests': getattr(settings, 'LOGIN_LIMIT_REQUESTS', 20),
168
super(limitlogin, self).__init__(**args)