mirror of
https://github.com/sigmasternchen/useful-api.org
synced 2025-03-15 07:58:55 +00:00
feat: Add ratelimit middleware
This commit is contained in:
parent
9d676afe5b
commit
bb7304d793
7 changed files with 104 additions and 6 deletions
|
@ -4,11 +4,12 @@ require_once(ROOT . "/router/Router.php");
|
|||
|
||||
require_once(ROOT . "/middleware/renderer.php");
|
||||
require_once(ROOT . "/middleware/log.php");
|
||||
require_once(ROOT . "/middleware/ratelimit.php");
|
||||
|
||||
function fromController(string $path, string $endpoint = null) {
|
||||
return function(array &$context) use ($path, $endpoint) {
|
||||
if ($endpoint)
|
||||
$context["endpoint"] = $endpoint;
|
||||
$context[ENDPOINT] = $endpoint;
|
||||
|
||||
return (require(ROOT . "/controllers/" . $path . ".php"))($context);
|
||||
};
|
||||
|
@ -27,7 +28,15 @@ return function(Router $router) {
|
|||
fromController("/ipaddress/GET", "ipaddress")
|
||||
);
|
||||
$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",
|
||||
|
|
|
@ -95,7 +95,7 @@ function whoisDomain(string $domain) {
|
|||
|
||||
return function (array &$context) {
|
||||
if (key_exists("ip", $_GET)) {
|
||||
$context["endpoint"] = "whois/ip";
|
||||
$context[ENDPOINT_DETAILS] = "whois/ip";
|
||||
$ip = $_GET["ip"];
|
||||
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
|
@ -105,7 +105,7 @@ return function (array &$context) {
|
|||
return whoisIp($ip);
|
||||
}
|
||||
} elseif (key_exists("domain", $_GET)) {
|
||||
$context["endpoint"] = "whois/domain";
|
||||
$context[ENDPOINT_DETAILS] = "whois/domain";
|
||||
$domain = $_GET["domain"];
|
||||
|
||||
return whoisDomain($domain);
|
||||
|
|
|
@ -2,16 +2,21 @@
|
|||
|
||||
require_once(ROOT . "/persistence/models/LogEntry.php");
|
||||
|
||||
const ENDPOINT = "ENDPOINT";
|
||||
const ENDPOINT_DETAILS = "ENDPOINT_DETAILS";
|
||||
|
||||
function useLog($handler, string $endpoint = "") {
|
||||
return function (array &$context) use ($handler, $endpoint) {
|
||||
$context["endpoint"] = $endpoint;
|
||||
$context[ENDPOINT] = $endpoint;
|
||||
$context[ENDPOINT_DETAILS] = "";
|
||||
|
||||
$result = $handler($context);
|
||||
|
||||
$accessKey = $context["ACCESS_KEY"] ?? "";
|
||||
|
||||
$entry = new LogEntry(
|
||||
$context["endpoint"],
|
||||
$context[ENDPOINT],
|
||||
$context[ENDPOINT_DETAILS],
|
||||
$_SERVER['REMOTE_ADDR'],
|
||||
$accessKey,
|
||||
);
|
||||
|
|
46
middleware/ratelimit.php
Normal file
46
middleware/ratelimit.php
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -3,6 +3,7 @@ CREATE TABLE `ua_logs` (
|
|||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
`timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`endpoint` VARCHAR(100),
|
||||
`endpoint_details` VARCHAR(100),
|
||||
`client_address` VARCHAR(46),
|
||||
`access_key` VARCHAR(255),
|
||||
`clean` BOOLEAN DEFAULT 0,
|
||||
|
|
|
@ -4,16 +4,19 @@ class LogEntry {
|
|||
public int $id;
|
||||
public DateTime $timestamp;
|
||||
public string $endpoint;
|
||||
public string $endpointDetails;
|
||||
public string $clientAddress;
|
||||
public string $accessKey;
|
||||
public bool $isClean;
|
||||
|
||||
public function __construct(
|
||||
string $endpoint,
|
||||
string $endpointDetails,
|
||||
string $clientAddress,
|
||||
string $accessKey,
|
||||
) {
|
||||
$this->endpoint = $endpoint;
|
||||
$this->endpointDetails = $endpointDetails;
|
||||
$this->clientAddress = $clientAddress;
|
||||
$this->accessKey = $accessKey;
|
||||
}
|
||||
|
|
|
@ -23,4 +23,38 @@ class Logs {
|
|||
$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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue