Mercurial > public > sg101
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/antispam/rate_limit.py Sat May 05 17:10:48 2012 -0500 @@ -0,0 +1,152 @@ +""" +This module contains the rate limiting functionality. + +""" +import datetime +import logging + +import redis + +from core.services import get_redis_connection + + +logger = logging.getLogger(__name__) + + +# This exception is thrown upon any Redis error. This insulates client code from +# knowing that we are using Redis and will allow us to use something else in the +# future. +class RateLimiterUnavailable(Exception): + pass + + +def _make_key(ip): + """ + Creates and returns a key string from a given IP address. + + """ + return 'rate-limit-' + ip + + +def _get_connection(): + """ + Create and return a Redis connection. Returns None on failure. + """ + try: + conn = get_redis_connection() + except redis.RedisError, e: + logger.error("rate limit: %s" % e) + raise RateLimiterUnavailable + + return conn + + +def _to_seconds(interval): + """ + Converts the timedelta interval object into a count of seconds. + + """ + return interval.days * 24 * 3600 + interval.seconds + + +def block_ip(ip, count=1000000, interval=datetime.timedelta(weeks=2)): + """ + This function jams the rate limit record for the given IP so that the IP is + blocked for the given interval. If the record doesn't exist, it is created. + This is useful for manually blocking an IP after detecting suspicious + behavior. + This function may throw RateLimiterUnavailable. + + """ + key = _make_key(ip) + conn = _get_connection() + + try: + conn.setex(key, time=_to_seconds(interval), value=count) + except redis.RedisError, e: + logger.error("rate limit (block_ip): %s" % e) + raise RateLimiterUnavailable + + logger.info("Rate limiter blocked IP %s; %d / %s", ip, count, interval) + + +def unblock_ip(ip): + """ + This function removes the block for the given IP address. + + """ + key = _make_key(ip) + conn = _get_connection() + try: + conn.delete(key) + except redis.RedisError, e: + logger.error("rate limit (unblock_ip): %s" % e) + raise RateLimiterUnavailable + + logger.info("Rate limiter unblocked IP %s", ip) + + +class RateLimiter(object): + """ + This class encapsulates the rate limiting logic for a given IP address. + + """ + def __init__(self, ip, set_point, interval, lockout): + self.ip = ip + self.set_point = set_point + self.interval = interval + self.lockout = lockout + self.key = _make_key(ip) + self.conn = _get_connection() + + def is_blocked(self): + """ + Return True if the IP is blocked, and false otherwise. + + """ + try: + val = self.conn.get(self.key) + except redis.RedisError, e: + logger.error("RateLimiter (is_blocked): %s" % e) + raise RateLimiterUnavailable + + try: + val = int(val) if val else 0 + except ValueError: + return False + + blocked = val >= self.set_point + if blocked: + logger.info("Rate limiter blocking %s", self.ip) + + return blocked + + def incr(self): + """ + One is added to a counter associated with the IP address. If the + counter exceeds set_point per interval, True is returned, and False + otherwise. If the set_point is exceeded for the first time, the counter + associated with the IP is set to expire according to the lockout + parameter. + + """ + try: + val = self.conn.incr(self.key) + + # Set expire time, if necessary. + # If this is the first time, set it according to interval. + # If the set_point has just been exceeded, set it according to lockout. + if val == 1: + self.conn.expire(self.key, _to_seconds(self.interval)) + elif val == self.set_point: + self.conn.expire(self.key, _to_seconds(self.lockout)) + + tripped = val >= self.set_point + + if tripped: + logger.info("Rate limiter tripped for %s; counter = %d", self.ip, val) + return tripped + + except redis.RedisError, e: + logger.error("RateLimiter (incr): %s" % e) + raise RateLimiterUnavailable