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/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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
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,
|
`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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue