comparison antispam/rate_limit.py @ 581:ee87ea74d46b

For Django 1.4, rearranged project structure for new manage.py.
author Brian Neal <bgneal@gmail.com>
date Sat, 05 May 2012 17:10:48 -0500
parents gpp/antispam/rate_limit.py@a18516692273
children
comparison
equal deleted inserted replaced
580:c525f3e0b5d0 581:ee87ea74d46b
1 """
2 This module contains the rate limiting functionality.
3
4 """
5 import datetime
6 import logging
7
8 import redis
9
10 from core.services import get_redis_connection
11
12
13 logger = logging.getLogger(__name__)
14
15
16 # This exception is thrown upon any Redis error. This insulates client code from
17 # knowing that we are using Redis and will allow us to use something else in the
18 # future.
19 class RateLimiterUnavailable(Exception):
20 pass
21
22
23 def _make_key(ip):
24 """
25 Creates and returns a key string from a given IP address.
26
27 """
28 return 'rate-limit-' + ip
29
30
31 def _get_connection():
32 """
33 Create and return a Redis connection. Returns None on failure.
34 """
35 try:
36 conn = get_redis_connection()
37 except redis.RedisError, e:
38 logger.error("rate limit: %s" % e)
39 raise RateLimiterUnavailable
40
41 return conn
42
43
44 def _to_seconds(interval):
45 """
46 Converts the timedelta interval object into a count of seconds.
47
48 """
49 return interval.days * 24 * 3600 + interval.seconds
50
51
52 def block_ip(ip, count=1000000, interval=datetime.timedelta(weeks=2)):
53 """
54 This function jams the rate limit record for the given IP so that the IP is
55 blocked for the given interval. If the record doesn't exist, it is created.
56 This is useful for manually blocking an IP after detecting suspicious
57 behavior.
58 This function may throw RateLimiterUnavailable.
59
60 """
61 key = _make_key(ip)
62 conn = _get_connection()
63
64 try:
65 conn.setex(key, time=_to_seconds(interval), value=count)
66 except redis.RedisError, e:
67 logger.error("rate limit (block_ip): %s" % e)
68 raise RateLimiterUnavailable
69
70 logger.info("Rate limiter blocked IP %s; %d / %s", ip, count, interval)
71
72
73 def unblock_ip(ip):
74 """
75 This function removes the block for the given IP address.
76
77 """
78 key = _make_key(ip)
79 conn = _get_connection()
80 try:
81 conn.delete(key)
82 except redis.RedisError, e:
83 logger.error("rate limit (unblock_ip): %s" % e)
84 raise RateLimiterUnavailable
85
86 logger.info("Rate limiter unblocked IP %s", ip)
87
88
89 class RateLimiter(object):
90 """
91 This class encapsulates the rate limiting logic for a given IP address.
92
93 """
94 def __init__(self, ip, set_point, interval, lockout):
95 self.ip = ip
96 self.set_point = set_point
97 self.interval = interval
98 self.lockout = lockout
99 self.key = _make_key(ip)
100 self.conn = _get_connection()
101
102 def is_blocked(self):
103 """
104 Return True if the IP is blocked, and false otherwise.
105
106 """
107 try:
108 val = self.conn.get(self.key)
109 except redis.RedisError, e:
110 logger.error("RateLimiter (is_blocked): %s" % e)
111 raise RateLimiterUnavailable
112
113 try:
114 val = int(val) if val else 0
115 except ValueError:
116 return False
117
118 blocked = val >= self.set_point
119 if blocked:
120 logger.info("Rate limiter blocking %s", self.ip)
121
122 return blocked
123
124 def incr(self):
125 """
126 One is added to a counter associated with the IP address. If the
127 counter exceeds set_point per interval, True is returned, and False
128 otherwise. If the set_point is exceeded for the first time, the counter
129 associated with the IP is set to expire according to the lockout
130 parameter.
131
132 """
133 try:
134 val = self.conn.incr(self.key)
135
136 # Set expire time, if necessary.
137 # If this is the first time, set it according to interval.
138 # If the set_point has just been exceeded, set it according to lockout.
139 if val == 1:
140 self.conn.expire(self.key, _to_seconds(self.interval))
141 elif val == self.set_point:
142 self.conn.expire(self.key, _to_seconds(self.lockout))
143
144 tripped = val >= self.set_point
145
146 if tripped:
147 logger.info("Rate limiter tripped for %s; counter = %d", self.ip, val)
148 return tripped
149
150 except redis.RedisError, e:
151 logger.error("RateLimiter (incr): %s" % e)
152 raise RateLimiterUnavailable