#5 - Rate Limiter Class
Data: 2018-04-21 12:00 - PHP
Allows to rate limit a request to an API or page, for example for login purposes. Makes use of the session save path to save the rate files, which might not be ideal.
<?php
class RateLimit {
private static $rateLimit;
private static function init($id, $rate) {
$rate_file = sha1($id);
$fp = @fopen(session_save_path() . '/rate_' . $rate_file, 'c+');
$count = 0;
$timeout = 5;
$got_lock = true;
while ($fp && !@flock($fp, LOCK_EX | LOCK_NB, $would_block)) {
if ($would_block && $count++ < $timeout) {
sleep(1);
} else {
$got_lock = false;
break;
}
}
if ($fp && $got_lock) {
$contents = stream_get_contents($fp);
if (!empty($contents))
self::$rateLimit = json_decode($contents, true);
else
self::$rateLimit = array('last' => microtime(true), 'allowance' => $rate);
self::$rateLimit['_fp'] = $fp;
return;
}
self::$rateLimit = array('last' => microtime(true), 'allowance' => $rate);
if ($fp !== FALSE)
fclose($fp);
}
private static function finish($save) {
if (!array_key_exists('_fp', self::$rateLimit))
return;
$fp = self::$rateLimit['_fp'];
unset(self::$rateLimit['_fp']);
if ($save) {
$str = json_encode(self::$rateLimit);
ftruncate($fp, 0);
fseek($fp, 0);
fputs($fp, $str);
fflush($fp);
}
self::$rateLimit = null;
flock($fp, LOCK_UN);
fclose($fp);
self::gc();
}
private static function gc() {
if (rand(0, 1) != 0)
return;
$max_lifetime = 3600 * 24 * 14;
foreach (glob(session_save_path() . '/rate_*') as $file) {
if (filemtime($file) + $max_lifetime < time() && file_exists($file)) {
unlink($file);
}
}
}
private static function consume($consume, $rate, $time, $limit_fn) {
$current = microtime('true');
$time_passed = $current - self::$rateLimit['last'];
self::$rateLimit['last'] = $current;
self::$rateLimit['allowance'] += $time_passed * ($rate / $time);
if (self::$rateLimit['allowance'] > $rate)
self::$rateLimit['allowance'] = $rate;
if (self::$rateLimit['allowance'] < $consume) {
self::finish(false);
if (is_callable($limit_fn))
$limit_fn();
return true;
}
self::$rateLimit['allowance'] -= $consume;
self::finish(true);
return false;
}
public static function limit($rate, $time, $id = '', $limit_fn = 'rate_limit_default_func') {
if (empty($id))
$id = rate_limit_default_id();
if (self::$rateLimit === null)
self::init($id, $rate);
return self::consume(1, $rate, $time, $limit_fn);
}
public static function delete($id) {
if (empty($id))
$id = rate_limit_default_id();
$file = session_save_path() . '/rate_' . sha1($id);
if (file_exists($file))
unlink($file);
}
}
function rate_limit_default_func() {
http_response_code(429);
echo 'rate_limited';
exit();
}
function rate_limit_default_id() {
return $_SERVER['SCRIPT_NAME'] . $_SERVER['REMOTE_ADDR'];
}
Utilização:
<?php
$accountName = 'myaccount';
$limited = RateLimit::limit(3, 60 * 30, 'login-' . strtolower($accountName), null);
if ($limited) {
echo 'You reached the limit of attempts of login. Try again in 30 minutes.';
}
// TODO: confirm login
RateLimit::delete('login-' . strtolower($accountName)); // removes the limit in case of successful login