Redis + RateLimiting
This commit is contained in:
parent
c715dadb11
commit
6c39c292b0
@ -5,6 +5,7 @@ namespace Core\API;
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||
use Core\Objects\RateLimiting;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
|
||||
@ -23,6 +24,7 @@ abstract class Request {
|
||||
protected bool $isDisabled;
|
||||
protected bool $apiKeyAllowed;
|
||||
protected bool $csrfTokenRequired;
|
||||
protected ?RateLimiting $rateLimiting;
|
||||
|
||||
private array $defaultParams;
|
||||
private array $allowedMethods;
|
||||
@ -47,6 +49,7 @@ abstract class Request {
|
||||
$this->apiKeyAllowed = true;
|
||||
$this->allowedMethods = array("GET", "POST");
|
||||
$this->csrfTokenRequired = true;
|
||||
$this->rateLimiting = null;
|
||||
}
|
||||
|
||||
public function getAPIName(): string {
|
||||
@ -192,7 +195,26 @@ abstract class Request {
|
||||
$this->result['logoutIn'] = $session->getExpiresSeconds();
|
||||
}
|
||||
|
||||
if ($this->isDisabled) {
|
||||
$this->lastError = "This function is currently disabled.";
|
||||
http_response_code(503);
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
if ($sql === null || !$sql->isConnected()) {
|
||||
$this->lastError = $sql ? $sql->getLastError() : "Database not connected yet.";
|
||||
http_response_code(503);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall) {
|
||||
if (!$this->isPublic) {
|
||||
$this->lastError = 'This function is private.';
|
||||
http_response_code(403);
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = $_REQUEST;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"] ?? ""))) {
|
||||
$jsonData = json_decode(file_get_contents('php://input'), true);
|
||||
@ -204,21 +226,6 @@ abstract class Request {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isDisabled) {
|
||||
$this->lastError = "This function is currently disabled.";
|
||||
http_response_code(503);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall && !$this->isPublic) {
|
||||
$this->lastError = 'This function is private.';
|
||||
http_response_code(403);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall) {
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204); # No content
|
||||
@ -292,10 +299,15 @@ abstract class Request {
|
||||
$this->parseVariableParams($values);
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
if ($sql === null || !$sql->isConnected()) {
|
||||
$this->lastError = $sql ? $sql->getLastError() : "Database not connected yet.";
|
||||
return false;
|
||||
if ($this->externalCall && $this->rateLimiting) {
|
||||
$settings = $this->context->getSettings();
|
||||
if ($settings->isRateLimitingEnabled()) {
|
||||
if (!$this->rateLimiting->check($this->context, self::getEndpoint())) {
|
||||
http_response_code(429);
|
||||
$this->lastError = "Rate limit exceeded";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->success = true;
|
||||
|
@ -58,6 +58,8 @@ namespace Core\API\TFA {
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\TfaAPI;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\RateLimiting;
|
||||
use Core\Objects\RateLimitRule;
|
||||
use Core\Objects\TwoFactor\AttestationObject;
|
||||
use Core\Objects\TwoFactor\AuthenticationData;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
@ -214,6 +216,10 @@ namespace Core\API\TFA {
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
$this->rateLimiting = new RateLimiting(
|
||||
null,
|
||||
new RateLimitRule(5, 30, RateLimitRule::SECOND)
|
||||
);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
@ -347,6 +353,10 @@ namespace Core\API\TFA {
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
$this->rateLimiting = new RateLimiting(
|
||||
null,
|
||||
new RateLimitRule(20, 60, RateLimitRule::SECOND)
|
||||
);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
@ -29,6 +29,7 @@ trait Pagination {
|
||||
$this->paginationCondition = $condition;
|
||||
$this->entityCount = call_user_func("$this->paginationClass::count", $sql, $condition, $joins);
|
||||
$this->pageSize = $this->getParam("count");
|
||||
$this->page = $this->getParam("page");
|
||||
if ($this->entityCount === false) {
|
||||
return $this->createError("Error fetching $this->paginationClass::count: " . $sql->getLastError());
|
||||
}
|
||||
|
@ -137,6 +137,8 @@ namespace Core\API\User {
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Objects\RateLimiting;
|
||||
use Core\Objects\RateLimitRule;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use ImagickException;
|
||||
use Core\Objects\Context;
|
||||
@ -563,6 +565,9 @@ namespace Core\API\User {
|
||||
'token' => new StringType('token', 36)
|
||||
));
|
||||
$this->csrfTokenRequired = false;
|
||||
$this->rateLimiting = new RateLimiting(
|
||||
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
||||
);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
@ -601,8 +606,6 @@ namespace Core\API\User {
|
||||
|
||||
class Login extends UserAPI {
|
||||
|
||||
private int $startedAt;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'username' => new StringType('username'),
|
||||
@ -610,13 +613,9 @@ namespace Core\API\User {
|
||||
'stayLoggedIn' => new Parameter('stayLoggedIn', Parameter::TYPE_BOOLEAN, true, false)
|
||||
));
|
||||
$this->forbidMethod("GET");
|
||||
}
|
||||
|
||||
private function wrongCredentials(): bool {
|
||||
$runtime = microtime(true) - $this->startedAt;
|
||||
$sleepTime = round(3e6 - $runtime);
|
||||
if ($sleepTime > 0) usleep($sleepTime);
|
||||
return $this->createError(L('Wrong username or password'));
|
||||
$this->rateLimiting = new RateLimiting(
|
||||
new RateLimitRule(10, 30, RateLimitRule::SECOND)
|
||||
);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
@ -635,7 +634,6 @@ namespace Core\API\User {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->startedAt = microtime(true);
|
||||
$this->success = false;
|
||||
$username = $this->getParam('username');
|
||||
$password = $this->getParam('password');
|
||||
@ -648,7 +646,7 @@ namespace Core\API\User {
|
||||
|
||||
if ($user !== false) {
|
||||
if ($user === null) {
|
||||
return $this->wrongCredentials();
|
||||
return $this->createError(L('Wrong username or password'));
|
||||
} else if (!$user->isActive()) {
|
||||
return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake.");
|
||||
} else if (password_verify($password, $user->password)) {
|
||||
@ -668,7 +666,7 @@ namespace Core\API\User {
|
||||
$this->success = true;
|
||||
}
|
||||
} else {
|
||||
return $this->wrongCredentials();
|
||||
return $this->createError(L('Wrong username or password'));
|
||||
}
|
||||
} else {
|
||||
return $this->createError("Error fetching user details: " . $sql->getLastError());
|
||||
@ -1190,14 +1188,18 @@ namespace Core\API\User {
|
||||
class ResetPassword extends UserAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
parent::__construct($context, $externalCall, [
|
||||
'token' => new StringType('token', 36),
|
||||
'password' => new StringType('password'),
|
||||
'confirmPassword' => new StringType('confirmPassword'),
|
||||
));
|
||||
]);
|
||||
|
||||
$this->forbidMethod("GET");
|
||||
$this->csrfTokenRequired = false;
|
||||
$this->apiKeyAllowed = false;
|
||||
$this->rateLimiting = new RateLimiting(
|
||||
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
||||
);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
@ -15,6 +15,7 @@ use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\Captcha\CaptchaProvider;
|
||||
use Core\Objects\Captcha\GoogleRecaptchaProvider;
|
||||
use Core\Objects\Captcha\HCaptchaProvider;
|
||||
use Core\Objects\ConnectionData;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class Settings {
|
||||
@ -41,6 +42,12 @@ class Settings {
|
||||
private string $mailFooter;
|
||||
private bool $mailAsync;
|
||||
|
||||
// rate limiting
|
||||
private bool $rateLimitingEnabled;
|
||||
private string $redisHost;
|
||||
private int $redisPort;
|
||||
private string $redisPassword;
|
||||
|
||||
//
|
||||
private Logger $logger;
|
||||
|
||||
@ -112,6 +119,18 @@ class Settings {
|
||||
$settings->mailFooter = "";
|
||||
$settings->mailAsync = false;
|
||||
|
||||
// rate limiting
|
||||
$settings->redisPort = 6379;
|
||||
if (isDocker()) {
|
||||
$settings->rateLimitingEnabled = true;
|
||||
$settings->redisHost = "webbase-redis";
|
||||
$settings->redisPassword = "webbase-redis";
|
||||
} else {
|
||||
$settings->rateLimitingEnabled = false;
|
||||
$settings->redisHost = "";
|
||||
$settings->redisPassword = "";
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@ -136,6 +155,10 @@ class Settings {
|
||||
$this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
|
||||
$this->allowedExtensions = $result["allowed_extensions"] ?? $this->allowedExtensions;
|
||||
$this->trustedDomains = $result["trusted_domains"] ?? $this->trustedDomains;
|
||||
$this->rateLimitingEnabled = $result["rate_limiting_enabled"] ?? $this->rateLimitingEnabled;
|
||||
$this->redisHost = $result["redis_host"] ?? $this->redisHost;
|
||||
$this->redisPort = $result["redis_port"] ?? $this->redisPort;
|
||||
$this->redisPassword = $result["redis_password"] ?? $this->redisPassword;
|
||||
date_default_timezone_set($this->timeZone);
|
||||
}
|
||||
|
||||
@ -160,7 +183,12 @@ class Settings {
|
||||
->addRow("mail_from", '""', false, false)
|
||||
->addRow("mail_last_sync", '""', false, false)
|
||||
->addRow("mail_footer", '""', false, false)
|
||||
->addRow("mail_async", false, false, false);
|
||||
->addRow("mail_async", false, false, false)
|
||||
->addRow("rate_limiting_enabled", json_encode($this->allowedExtensions), false, false)
|
||||
->addRow("redis_host", json_encode($this->redisHost), false, false)
|
||||
->addRow("redis_port", json_encode($this->redisPort), false, false)
|
||||
->addRow("redis_password", json_encode($this->redisPassword), true, false)
|
||||
;
|
||||
}
|
||||
|
||||
public function getSiteName(): string {
|
||||
@ -240,4 +268,17 @@ class Settings {
|
||||
public function getTrustedDomains(): array {
|
||||
return $this->trustedDomains;
|
||||
}
|
||||
|
||||
public function isRateLimitingEnabled(): bool {
|
||||
return $this->rateLimitingEnabled;
|
||||
}
|
||||
|
||||
public function getRedisConfiguration(): ConnectionData {
|
||||
return new ConnectionData(
|
||||
$this->redisHost,
|
||||
$this->redisPort,
|
||||
"",
|
||||
$this->redisPassword
|
||||
);
|
||||
}
|
||||
}
|
@ -19,8 +19,11 @@ class InfoBody extends SimpleBody {
|
||||
protected function getContent(): string {
|
||||
$user = $this->getContext()->getUser();
|
||||
if ($user && $user->hasGroup(Group::ADMIN)) {
|
||||
ob_start();
|
||||
phpinfo();
|
||||
return "";
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $content;
|
||||
} else {
|
||||
$message = "You are not logged in or do not have the proper privileges to access this page.";
|
||||
return $this->getDocument()->getRouter()->returnStatusCode(403, [ "message" => $message] );
|
||||
|
@ -93,10 +93,6 @@ namespace Documents\Install {
|
||||
$this->steps = array();
|
||||
}
|
||||
|
||||
function isDocker(): bool {
|
||||
return file_exists("/.dockerenv");
|
||||
}
|
||||
|
||||
private function getParameter($name): ?string {
|
||||
if (isset($_REQUEST[$name]) && is_string($_REQUEST[$name])) {
|
||||
return trim($_REQUEST[$name]);
|
||||
@ -263,6 +259,11 @@ namespace Documents\Install {
|
||||
$success = false;
|
||||
}
|
||||
|
||||
if (!class_exists("Redis")) {
|
||||
$failedRequirements[] = "<b>redis</b> extension is not installed.";
|
||||
$success = false;
|
||||
}
|
||||
|
||||
if (!function_exists("yaml_emit")) {
|
||||
$failedRequirements[] = "<b>YAML</b> extension is not installed.";
|
||||
$success = false;
|
||||
@ -363,7 +364,7 @@ namespace Documents\Install {
|
||||
$connectionData->setProperty('database', $database);
|
||||
$connectionData->setProperty('encoding', $encoding);
|
||||
$connectionData->setProperty('type', $type);
|
||||
$connectionData->setProperty('isDocker', $this->isDocker());
|
||||
$connectionData->setProperty('isDocker', isDocker());
|
||||
$sql = SQL::createConnection($connectionData);
|
||||
$success = false;
|
||||
if (is_string($sql)) {
|
||||
@ -706,11 +707,17 @@ namespace Documents\Install {
|
||||
|
||||
private function createProgressMainview(): string {
|
||||
|
||||
$isDocker = $this->isDocker();
|
||||
$defaultHost = ($isDocker ? "db" : "localhost");
|
||||
$defaultUsername = ($isDocker ? "root" : "");
|
||||
$defaultPassword = ($isDocker ? "webbasedb" : "");
|
||||
$defaultDatabase = ($isDocker ? "webbase" : "");
|
||||
if (isDocker()) {
|
||||
$defaultHost = "db";
|
||||
$defaultUsername = "root";
|
||||
$defaultPassword = "webbasedb";
|
||||
$defaultDatabase = "webbase";
|
||||
} else {
|
||||
$defaultHost = "localhost";
|
||||
$defaultUsername = "";
|
||||
$defaultPassword = "";
|
||||
$defaultDatabase = "";
|
||||
}
|
||||
|
||||
$views = array(
|
||||
self::CHECKING_REQUIREMENTS => array(
|
||||
|
66
Core/Driver/Redis/RedisConnection.class.php
Normal file
66
Core/Driver/Redis/RedisConnection.class.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\Redis;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\ConnectionData;
|
||||
|
||||
class RedisConnection {
|
||||
|
||||
private \Redis $link;
|
||||
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct(?SQL $sql) {
|
||||
$this->logger = new Logger("Redis", $sql);
|
||||
$this->link = new \Redis();
|
||||
}
|
||||
|
||||
public function connect(ConnectionData $connectionData): bool {
|
||||
try {
|
||||
$this->link->connect($connectionData->getHost(), $connectionData->getPort());
|
||||
$this->link->auth($connectionData->getPassword());
|
||||
return true;
|
||||
} catch (\RedisException $e) {
|
||||
$this->logger->error("Error connecting to redis: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function isConnected(): bool {
|
||||
try {
|
||||
return $this->link->isConnected();
|
||||
} catch (\RedisException $e) {
|
||||
$this->logger->error("Error checking redis connection: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function hGet(string $hashKey, string $key): ?string {
|
||||
try {
|
||||
return $this->link->hGet($hashKey, $key);
|
||||
} catch (\RedisException $e) {
|
||||
$this->logger->error("Error fetching value from redis: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function hSet(string $hashKey, mixed $key, string $value): bool {
|
||||
try {
|
||||
return $this->link->hSet($hashKey, $key, $value);
|
||||
} catch (\RedisException $e) {
|
||||
$this->logger->error("Error setting value: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function close(): bool {
|
||||
try {
|
||||
return $this->link->close();
|
||||
} catch (\RedisException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -10,12 +10,12 @@ class ConnectionData {
|
||||
private string $password;
|
||||
private array $properties;
|
||||
|
||||
public function __construct($host, $port, $login, $password) {
|
||||
public function __construct(string $host, int $port, string $login, string $password) {
|
||||
$this->host = $host;
|
||||
$this->port = $port;
|
||||
$this->login = $login;
|
||||
$this->password = $password;
|
||||
$this->properties = array();
|
||||
$this->properties = [];
|
||||
}
|
||||
|
||||
public function getProperties(): array {
|
||||
|
@ -4,6 +4,7 @@ namespace Core\Objects;
|
||||
|
||||
use Core\Configuration\Configuration;
|
||||
use Core\Configuration\Settings;
|
||||
use Core\Driver\Redis\RedisConnection;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondLike;
|
||||
@ -25,6 +26,7 @@ class Context {
|
||||
private Configuration $configuration;
|
||||
private Language $language;
|
||||
public ?Router $router;
|
||||
private ?RedisConnection $redis;
|
||||
|
||||
private function __construct() {
|
||||
|
||||
@ -32,6 +34,7 @@ class Context {
|
||||
$this->session = null;
|
||||
$this->user = null;
|
||||
$this->router = null;
|
||||
$this->redis = null;
|
||||
$this->configuration = new Configuration();
|
||||
$this->setLanguage(Language::DEFAULT_LANGUAGE());
|
||||
|
||||
@ -52,6 +55,11 @@ class Context {
|
||||
$this->sql->close();
|
||||
$this->sql = null;
|
||||
}
|
||||
|
||||
if ($this->redis && $this->redis->isConnected()) {
|
||||
$this->redis->close();
|
||||
$this->redis = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLanguage(Language $language): void {
|
||||
@ -201,4 +209,18 @@ class Context {
|
||||
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
public function getRedis(): ?RedisConnection {
|
||||
|
||||
if ($this->redis === null) {
|
||||
$settings = $this->getSettings();
|
||||
$connectionData = $settings->getRedisConfiguration();
|
||||
$this->redis = new RedisConnection($this->sql);
|
||||
if (!$this->redis->connect($connectionData)) {
|
||||
$this->redis = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redis;
|
||||
}
|
||||
}
|
31
Core/Objects/RateLimitRule.class.php
Normal file
31
Core/Objects/RateLimitRule.class.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
class RateLimitRule {
|
||||
|
||||
const SECOND = 0;
|
||||
const MINUTE = 1;
|
||||
const HOUR = 2;
|
||||
|
||||
private int $count;
|
||||
private int $perSecond;
|
||||
|
||||
public function __construct(int $count, int $time, int $unit) {
|
||||
$this->count = $count;
|
||||
$this->perSecond = $time;
|
||||
if ($unit === self::HOUR) {
|
||||
$this->perSecond *= 60 * 60;
|
||||
} else if ($unit === self::MINUTE) {
|
||||
$this->perSecond *= 60;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCount(): int {
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
public function getWindow(): int {
|
||||
return $this->perSecond;
|
||||
}
|
||||
}
|
71
Core/Objects/RateLimiting.class.php
Normal file
71
Core/Objects/RateLimiting.class.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
|
||||
class RateLimiting {
|
||||
|
||||
private ?RateLimitRule $anonymousRule;
|
||||
private ?RateLimitRule $sessionRule;
|
||||
|
||||
public function __construct(?RateLimitRule $anonymousRule = null, ?RateLimitRule $sessionRule = null) {
|
||||
$this->anonymousRule = $anonymousRule;
|
||||
$this->sessionRule = $sessionRule;
|
||||
}
|
||||
|
||||
public function check(Context $context, string $method): bool {
|
||||
$session = $context->getSession();
|
||||
$logger = new Logger("RateLimiting", $context->getSQL());
|
||||
|
||||
if ($session !== null) {
|
||||
// session based rate limiting
|
||||
$key = $session->getUUID();
|
||||
$effectiveRule = $this->sessionRule;
|
||||
} else {
|
||||
// ip-based rate limiting
|
||||
$key = $_SERVER['REMOTE_ADDR'];
|
||||
$effectiveRule = $this->anonymousRule;
|
||||
}
|
||||
|
||||
if ($effectiveRule === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$redis = $context->getRedis();
|
||||
if (!$redis?->isConnected()) {
|
||||
$logger->error("Could not check rate limiting, redis is not connected.");
|
||||
return true;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$queue = json_decode($redis->hGet($method, $key)) ?? [];
|
||||
$pass = true;
|
||||
$queueSize = count($queue);
|
||||
if ($queueSize >= $effectiveRule->getCount()) {
|
||||
// check the last n entries, whether they fit in the current window
|
||||
$requestsInWindow = 0;
|
||||
foreach ($queue as $accessTime) {
|
||||
if ($accessTime >= $now - $effectiveRule->getWindow()) {
|
||||
$requestsInWindow++;
|
||||
if ($requestsInWindow >= $effectiveRule->getCount()) {
|
||||
$pass = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($pass) {
|
||||
array_unshift($queue, $now);
|
||||
if ($queueSize + 1 > $effectiveRule->getCount()) {
|
||||
$queue = array_slice($queue, 0, $effectiveRule->getCount());
|
||||
}
|
||||
$redis->hSet($method, $key, json_encode($queue));
|
||||
}
|
||||
|
||||
return $pass;
|
||||
}
|
||||
}
|
@ -259,6 +259,10 @@ function getClassName($class, bool $short = true): string {
|
||||
}
|
||||
}
|
||||
|
||||
function isDocker(): bool {
|
||||
return file_exists("/.dockerenv");
|
||||
}
|
||||
|
||||
function createError($msg): array {
|
||||
return ["success" => false, "msg" => $msg];
|
||||
}
|
||||
|
@ -29,3 +29,15 @@ services:
|
||||
context: './docker/php/'
|
||||
links:
|
||||
- db
|
||||
- cache
|
||||
cache:
|
||||
container_name: webbase-redis
|
||||
image: redis:latest
|
||||
ports:
|
||||
- '6379:6379'
|
||||
command: redis-server --save 20 1 --loglevel warning --requirepass webbase-redis
|
||||
volumes:
|
||||
- cache:/data
|
||||
volumes:
|
||||
cache:
|
||||
driver: local
|
@ -12,7 +12,7 @@ RUN mkdir -p /application/Core/Configuration /var/www/.gnupg && \
|
||||
# YAML + dev dependencies + additional packages
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y libyaml-dev libzip-dev libgmp-dev libpng-dev libmagickwand-dev gnupg2 git $ADDITIONAL_PACKAGES && \
|
||||
printf "\n" | pecl install yaml imagick && docker-php-ext-enable yaml imagick && \
|
||||
printf "\n" | pecl install yaml imagick redis && docker-php-ext-enable yaml imagick redis && \
|
||||
docker-php-ext-install gd
|
||||
|
||||
# Browscap
|
||||
|
Loading…
Reference in New Issue
Block a user