bgneal@472
|
1 """
|
bgneal@472
|
2 This module contains the rate limiting functionality.
|
bgneal@472
|
3
|
bgneal@472
|
4 """
|
bgneal@472
|
5 import datetime
|
bgneal@472
|
6 import logging
|
bgneal@472
|
7
|
bgneal@472
|
8 import redis
|
bgneal@472
|
9 from django.conf import settings
|
bgneal@472
|
10
|
bgneal@472
|
11
|
bgneal@472
|
12 logger = logging.getLogger(__name__)
|
bgneal@472
|
13
|
bgneal@472
|
14 # Redis connection and database settings
|
bgneal@472
|
15 HOST = getattr(settings, 'RATE_LIMIT_REDIS_HOST', 'localhost')
|
bgneal@472
|
16 PORT = getattr(settings, 'RATE_LIMIT_REDIS_PORT', 6379)
|
bgneal@472
|
17 DB = getattr(settings, 'RATE_LIMIT_REDIS_DB', 0)
|
bgneal@472
|
18
|
bgneal@472
|
19
|
bgneal@472
|
20 def _make_key(ip):
|
bgneal@472
|
21 """
|
bgneal@472
|
22 Creates and returns a key string from a given IP address.
|
bgneal@472
|
23
|
bgneal@472
|
24 """
|
bgneal@472
|
25 return 'rate-limit-' + ip
|
bgneal@472
|
26
|
bgneal@472
|
27
|
bgneal@472
|
28 def _get_connection():
|
bgneal@472
|
29 """
|
bgneal@472
|
30 Create and return a Redis connection. Returns None on failure.
|
bgneal@472
|
31 """
|
bgneal@472
|
32 try:
|
bgneal@472
|
33 conn = redis.Redis(host=HOST, port=PORT, db=DB)
|
bgneal@472
|
34 return conn
|
bgneal@472
|
35 except redis.RedisError, e:
|
bgneal@472
|
36 logger.error("rate limit: %s" % e)
|
bgneal@472
|
37
|
bgneal@472
|
38 return None
|
bgneal@472
|
39
|
bgneal@472
|
40
|
bgneal@472
|
41 def _to_seconds(interval):
|
bgneal@472
|
42 """
|
bgneal@472
|
43 Converts the timedelta interval object into a count of seconds.
|
bgneal@472
|
44
|
bgneal@472
|
45 """
|
bgneal@472
|
46 return interval.days * 24 * 3600 + interval.seconds
|
bgneal@472
|
47
|
bgneal@472
|
48
|
bgneal@472
|
49 def rate_check(ip, set_point, interval, lockout):
|
bgneal@472
|
50 """
|
bgneal@472
|
51 This function performs a rate limit check.
|
bgneal@472
|
52 One is added to a counter associated with the IP address. If the
|
bgneal@472
|
53 counter exceeds set_point per interval, True is returned, and False
|
bgneal@472
|
54 otherwise. If the set_point is exceeded for the first time, the counter
|
bgneal@472
|
55 associated with the IP is set to expire according to the lockout parameter.
|
bgneal@472
|
56 This locks the IP address as this function will then return True for the
|
bgneal@472
|
57 period specified by lockout.
|
bgneal@472
|
58
|
bgneal@472
|
59 """
|
bgneal@472
|
60 if not ip:
|
bgneal@472
|
61 logger.error("rate_limit.rate_check could not get IP")
|
bgneal@472
|
62 return False
|
bgneal@472
|
63 key = _make_key(ip)
|
bgneal@472
|
64
|
bgneal@472
|
65 conn = _get_connection()
|
bgneal@472
|
66 if not conn:
|
bgneal@472
|
67 return False
|
bgneal@472
|
68
|
bgneal@472
|
69 val = conn.incr(key)
|
bgneal@472
|
70
|
bgneal@472
|
71 # Set expire time, if necessary.
|
bgneal@472
|
72 # If this is the first time, set it according to interval.
|
bgneal@472
|
73 # If the set_point has just been exceeded, set it according to lockout.
|
bgneal@472
|
74 if val == 1:
|
bgneal@472
|
75 conn.expire(key, _to_seconds(interval))
|
bgneal@472
|
76 elif val == set_point:
|
bgneal@472
|
77 conn.expire(key, _to_seconds(lockout))
|
bgneal@472
|
78
|
bgneal@472
|
79 tripped = val >= set_point
|
bgneal@472
|
80
|
bgneal@472
|
81 if tripped:
|
bgneal@472
|
82 logger.info("Rate limiter tripped for %s; counter = %d", ip, val)
|
bgneal@472
|
83
|
bgneal@472
|
84 return tripped
|
bgneal@472
|
85
|
bgneal@472
|
86
|
bgneal@472
|
87 def block_ip(ip, count=1000000, interval=datetime.timedelta(weeks=2)):
|
bgneal@472
|
88 """
|
bgneal@472
|
89 This function jams the rate limit record for the given IP so that the IP is
|
bgneal@472
|
90 blocked for the given interval. If the record doesn't exist, it is created.
|
bgneal@472
|
91 This is useful for manually blocking an IP after detecting suspicious
|
bgneal@472
|
92 behavior.
|
bgneal@472
|
93
|
bgneal@472
|
94 """
|
bgneal@472
|
95 if not ip:
|
bgneal@472
|
96 logger.error("rate_limit.block_ip could not get IP")
|
bgneal@472
|
97 return
|
bgneal@472
|
98
|
bgneal@472
|
99 key = _make_key(ip)
|
bgneal@472
|
100 conn = _get_connection()
|
bgneal@472
|
101 if not conn:
|
bgneal@472
|
102 return
|
bgneal@472
|
103
|
bgneal@472
|
104 conn.setex(key, count, _to_seconds(interval))
|
bgneal@472
|
105 logger.info("Rate limiter blocked IP %s; %d / %s", ip, count, interval)
|