/** * A token bucket filter. allowed() will return true when hit() is called no more than $max_hits_per_period per $period__in_minutes minutes. * * Example usage: * // Allow each IP to make 15 requests each two minutes. * $rl = new RateLimiter($REQUEST["REMOTE_ADDR"], 15, 2)); * $rl->hit(); * if (!$rl->allowed()) { * $rl->blockRequest(); // will terminate request * } * * Alternate: * // Allow each IP to make 15 requests each two minutes, block request if exceeds. * (new RateLimiter($REQUEST["REMOTE_ADDR"], 15, 2)))->rateLimit(); */ class RateLimiter { private $bucket_name; private $period_in_minutes; // rename this private $max_per_minute; /** * Allows $max_per_period calls to hit() in $period_duration minutes. * * $bucket_name: unique per thing to be rate limited. */ public function __construct($bucket_name, $period_in_minutes, $max_per_period) { $this->bucket_name = $bucket_name; $this->period_in_minutes = $period_in_minutes; $this->max_per_period = $max_per_period; $this->key_prefix = sprintf("RL-%s-", md5($bucket_name)); $this->now_timestamp = time(); $this->key_now = buildKey($now_timestamp); } /** * Constructs a memcache-safe key string for a specific timestamp. */ private function buildKey($when) { return $this->key_prefix . strftime("%Y%m%d-%H%M", $when); } /** * Returns true if the rate limit is hit or exceeded; false otherwise. */ public function allowed() { // Generate span of hashes (many clouds) we want from the DB. $counters = array($key_now); for ($i = 1; $i <= $this->period_in_minutes; $i++) { $counters[] = buildKey($this->now_timestamp - 60 * $i); } $counts = array_sum(memcache->get_multi($counters)); return $counts <= $this->max_per_minute; } /** * Records a hit. */ public function hit() { memcache->add($this->key_now, 0); memcache->incr($this->key_now, 1); } /** * Sets the HTTP 503 Service Unavailable header, and increments the RL-blocked stats counter. */ public function blockRequest() { header("X-RateLimited: true", true, 503); memcache->add("RL-blocked", 0); memcache->incr("RL-blocked", 1); exit; } /** * Records a hit and aborts the request with a 503 if the rate limit is reached. */ public function rateLimit() { $this->hit(); if (!$this->allowed()) { $this->blockRequest(); } } }