Last active
December 6, 2024 12:19
-
-
Save freekrai/cdcd6ebb29d84b9dc244282e64caf5fe to your computer and use it in GitHub Desktop.
PHP session-based rate limiter for APIs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
date_default_timezone_set('America/Los_Angeles'); | |
session_start(); | |
include("ratelimiter.php"); | |
// in this sample, we are using the originating IP, but you can modify to use API keys, or tokens or what-have-you. | |
$rateLimiter = new RateLimiter($_SERVER["REMOTE_ADDR"]); | |
$limit = 100; // number of connections to limit user to per $minutes | |
$minutes = 1; // number of $minutes to check for. | |
$seconds = floor($minutes * 60); // retry after $minutes in seconds. | |
try { | |
$rateLimiter->limitRequestsInMinutes($limit, $minutes); | |
} catch (RateExceededException $e) { | |
header("HTTP/1.1 429 Too Many Requests"); | |
header(sprintf("Retry-After: %d", $seconds)); | |
$data = 'Rate Limit Exceeded '; | |
die (json_encode($data)); | |
} | |
// ok, they were within their limit, so let's continue with our app.... | |
$data = "Data Returned from API "; | |
header('Content-Type: application/json'); | |
die(json_encode($data)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
This is a really simple plug and play rate limiter class meant to be used with APIs. | |
Using sessions means we can throw this into any PHP API quickly. | |
$token can be anything to uniquely identify a user, either an IP address, an API key, a JWT token, anything that is unique to a single user. | |
$prefix can be whatever you want, we default it to "rate" | |
On init, we create a md5 hash of our $prefix and our $token, this becomes the prefix throughout the class. | |
We then append a timestamp to this prefix | |
*/ | |
class RateExceededException extends Exception {} | |
class RateLimiter { | |
private $prefix; | |
public function __construct($token, $prefix = "rate") { | |
$this->prefix = md5($prefix . $token); | |
if( !isset($_SESSION["cache"]) ){ | |
$_SESSION["cache"] = array(); | |
} | |
if( !isset($_SESSION["expiries"]) ){ | |
$_SESSION["expiries"] = array(); | |
}else{ | |
$this->expireSessionKeys(); | |
} | |
} | |
public function limitRequestsInMinutes($allowedRequests, $minutes) { | |
$this->expireSessionKeys(); | |
$requests = 0; | |
foreach ($this->getKeys($minutes) as $key) { | |
$requestsInCurrentMinute = $this->getSessionKey($key); | |
if (false !== $requestsInCurrentMinute) $requests += $requestsInCurrentMinute; | |
} | |
if (false === $requestsInCurrentMinute) { | |
$this->setSessionKey( $key, 1, ($minutes * 60 + 1) ); | |
} else { | |
$this->increment($key, 1); | |
} | |
if ($requests > $allowedRequests) throw new RateExceededException; | |
} | |
private function getKeys($minutes) { | |
$keys = array(); | |
$now = time(); | |
for ($time = $now - $minutes * 60; $time <= $now; $time += 60) { | |
$keys[] = $this->prefix . date("dHi", $time); | |
} | |
return $keys; | |
} | |
private function increment( $key, $inc){ | |
$cnt = 0; | |
if( isset($_SESSION['cache'][$key]) ){ | |
$cnt = $_SESSION['cache'][$key]; | |
} | |
$_SESSION['cache'][$key] = $cnt + $inc; | |
} | |
private function setSessionKey( $key, $val, $expiry ){ | |
$_SESSION["expiries"][$key] = time() + $expiry; | |
$_SESSION['cache'][$key] = $val; | |
} | |
private function getSessionKey( $key ){ | |
return isset($_SESSION['cache'][$key]) ? $_SESSION['cache'][$key] : false; | |
} | |
private function expireSessionKeys() { | |
foreach ($_SESSION["expiries"] as $key => $value) { | |
if (time() > $value) { | |
unset($_SESSION['cache'][$key]); | |
unset($_SESSION["expiries"][$key]); | |
} | |
} | |
} | |
} |
Thanks, works great. However, there are two caveats I found:
$requests
are iterated from 0, so if I setallowedRequests
to 1, it actually allows 2 requests. It can be fixed by setting$requests
initial value to 1- Client can quite easily hack this rate limitter by simply deleting or changing session ID cookie. To make it safer, we can create session_id based on caller IP address + ideally salt. Example implementation below:
<?php
class IpBasedSessionStarter {
private string $salt;
public function __construct(string $salt) {
$this->salt = $salt;
}
public function start() {
$ip = $_SERVER['REMOTE_ADDR'];
session_id(md5($this->salt . $ip));
session_start();
}
}
Thanks, works great. However, there are two caveats I found:
$requests
are iterated from 0, so if I setallowedRequests
to 1, it actually allows 2 requests. It can be fixed by setting$requests
initial value to 1- Client can quite easily hack this rate limitter by simply deleting or changing session ID cookie. To make it safer, we can create session_id based on caller IP address + ideally salt. Example implementation below:
<?php class IpBasedSessionStarter { private string $salt; public function __construct(string $salt) { $this->salt = $salt; } public function start() { $ip = $_SERVER['REMOTE_ADDR']; session_id(md5($this->salt . $ip)); session_start(); } }
this is great! 👍
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thank you man...