feat: Add ratelimit middleware

This commit is contained in:
overflowerror 2023-11-25 14:27:39 +01:00
parent 9d676afe5b
commit bb7304d793
7 changed files with 104 additions and 6 deletions

View file

@ -4,11 +4,12 @@ require_once(ROOT . "/router/Router.php");
require_once(ROOT . "/middleware/renderer.php"); require_once(ROOT . "/middleware/renderer.php");
require_once(ROOT . "/middleware/log.php"); require_once(ROOT . "/middleware/log.php");
require_once(ROOT . "/middleware/ratelimit.php");
function fromController(string $path, string $endpoint = null) { function fromController(string $path, string $endpoint = null) {
return function(array &$context) use ($path, $endpoint) { return function(array &$context) use ($path, $endpoint) {
if ($endpoint) if ($endpoint)
$context["endpoint"] = $endpoint; $context[ENDPOINT] = $endpoint;
return (require(ROOT . "/controllers/" . $path . ".php"))($context); return (require(ROOT . "/controllers/" . $path . ".php"))($context);
}; };
@ -27,7 +28,15 @@ return function(Router $router) {
fromController("/ipaddress/GET", "ipaddress") fromController("/ipaddress/GET", "ipaddress")
); );
$apiRouter->addRoute(GET, "/whois", $apiRouter->addRoute(GET, "/whois",
fromController("/whois/GET", "whois") useRateLimit(
fromController("/whois/GET", "whois"),
[
RATELIMIT_ENDPOINT => "whois",
RATELIMIT_AMOUNT_PER_IP => 1,
RATELIMIT_AMOUNT_PER_KEY => 10,
RATELIMIT_TIMEFRAME => 60,
]
)
); );
$apiRouter->addRoute(GET, "/punycode", $apiRouter->addRoute(GET, "/punycode",

View file

@ -95,7 +95,7 @@ function whoisDomain(string $domain) {
return function (array &$context) { return function (array &$context) {
if (key_exists("ip", $_GET)) { if (key_exists("ip", $_GET)) {
$context["endpoint"] = "whois/ip"; $context[ENDPOINT_DETAILS] = "whois/ip";
$ip = $_GET["ip"]; $ip = $_GET["ip"];
if (!filter_var($ip, FILTER_VALIDATE_IP)) { if (!filter_var($ip, FILTER_VALIDATE_IP)) {
@ -105,7 +105,7 @@ return function (array &$context) {
return whoisIp($ip); return whoisIp($ip);
} }
} elseif (key_exists("domain", $_GET)) { } elseif (key_exists("domain", $_GET)) {
$context["endpoint"] = "whois/domain"; $context[ENDPOINT_DETAILS] = "whois/domain";
$domain = $_GET["domain"]; $domain = $_GET["domain"];
return whoisDomain($domain); return whoisDomain($domain);

View file

@ -2,16 +2,21 @@
require_once(ROOT . "/persistence/models/LogEntry.php"); require_once(ROOT . "/persistence/models/LogEntry.php");
const ENDPOINT = "ENDPOINT";
const ENDPOINT_DETAILS = "ENDPOINT_DETAILS";
function useLog($handler, string $endpoint = "") { function useLog($handler, string $endpoint = "") {
return function (array &$context) use ($handler, $endpoint) { return function (array &$context) use ($handler, $endpoint) {
$context["endpoint"] = $endpoint; $context[ENDPOINT] = $endpoint;
$context[ENDPOINT_DETAILS] = "";
$result = $handler($context); $result = $handler($context);
$accessKey = $context["ACCESS_KEY"] ?? ""; $accessKey = $context["ACCESS_KEY"] ?? "";
$entry = new LogEntry( $entry = new LogEntry(
$context["endpoint"], $context[ENDPOINT],
$context[ENDPOINT_DETAILS],
$_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_ADDR'],
$accessKey, $accessKey,
); );

46
middleware/ratelimit.php Normal file
View file

@ -0,0 +1,46 @@
<?php
require_once(ROOT . "/utils/error.php");
const RATELIMIT_ENDPOINT = "RATELIMIT_ENDPOINT";
const RATELIMIT_TIMEFRAME = "RATELIMIT_TIMEFRAME";
const RATELIMIT_AMOUNT_PER_IP = "RATELIMIT_AMOUNT_PER_IP";
const RATELIMIT_AMOUNT_PER_KEY = "RATELIMIT_AMOUNT_PER_KEY";
function useRateLimit($handler, array $settings) {
return function (array &$context) use ($handler, $settings) {
$clientAddress = $_SERVER['REMOTE_ADDR'];
$accessKey = $context["ACCESS_KEY"] ?? "";
$endpoint = $settings[RATELIMIT_ENDPOINT];
$timeframe = $settings[RATELIMIT_TIMEFRAME];
$rateLimitReached = false;
if ($accessKey && key_exists(RATELIMIT_AMOUNT_PER_KEY, $settings)) {
$numberOfRequests = $context[REPOSITORIES]->logs()->countWithKeyInTimeframe(
$endpoint,
$accessKey,
$timeframe
);
$rateLimitReached = $numberOfRequests > $settings[RATELIMIT_AMOUNT_PER_KEY];
} else if(key_exists(RATELIMIT_AMOUNT_PER_IP, $settings)) {
$numberOfRequests = $context[REPOSITORIES]->logs()->countWithAddressInTimeframe(
$endpoint,
$clientAddress,
$timeframe
);
$rateLimitReached = $numberOfRequests > $settings[RATELIMIT_AMOUNT_PER_IP];
}
if ($rateLimitReached) {
setStatusCode(429);
return errorResponse("Rate limit reached", "Please wait before retrying.");
} else {
return $handler($context);
}
};
}

View file

@ -3,6 +3,7 @@ CREATE TABLE `ua_logs` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY, `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP, `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP,
`endpoint` VARCHAR(100), `endpoint` VARCHAR(100),
`endpoint_details` VARCHAR(100),
`client_address` VARCHAR(46), `client_address` VARCHAR(46),
`access_key` VARCHAR(255), `access_key` VARCHAR(255),
`clean` BOOLEAN DEFAULT 0, `clean` BOOLEAN DEFAULT 0,

View file

@ -4,16 +4,19 @@ class LogEntry {
public int $id; public int $id;
public DateTime $timestamp; public DateTime $timestamp;
public string $endpoint; public string $endpoint;
public string $endpointDetails;
public string $clientAddress; public string $clientAddress;
public string $accessKey; public string $accessKey;
public bool $isClean; public bool $isClean;
public function __construct( public function __construct(
string $endpoint, string $endpoint,
string $endpointDetails,
string $clientAddress, string $clientAddress,
string $accessKey, string $accessKey,
) { ) {
$this->endpoint = $endpoint; $this->endpoint = $endpoint;
$this->endpointDetails = $endpointDetails;
$this->clientAddress = $clientAddress; $this->clientAddress = $clientAddress;
$this->accessKey = $accessKey; $this->accessKey = $accessKey;
} }

View file

@ -23,4 +23,38 @@ class Logs {
$entry->accessKey, $entry->accessKey,
]); ]);
} }
public function countWithKeyInTimeframe(string $endpoint, string $key, string $timeframe) {
$statement = $this->connection->prepare(<<<EOF
SELECT COUNT(*)
FROM `$this->table`
WHERE
`endpoint` = ? AND
`key` = ? AND
`timestamp` > ?
EOF);
$statement->execute([
$endpoint,
$key,
(new DateTime())->sub(DateInterval::createFromDateString($timeframe))
]);
return $statement->fetchColumn()[0];
}
public function countWithAddressInTimeframe(string $endpoint, string $address, int $timeframe) {
$statement = $this->connection->prepare(<<<EOF
SELECT COUNT(*)
FROM `$this->table`
WHERE
`endpoint` = ? AND
`client_address` = ? AND
`timestamp` > CURRENT_TIMESTAMP - INTERVAL ? SECOND
EOF);
$statement->execute([
$endpoint,
$address,
$timeframe,
]);
return $statement->fetchColumn();
}
} }