From 1679675aa1c29d24344dd2e091ff252accb111d6 Mon Sep 17 00:00:00 2001 From: Michael Kaufmann Date: Tue, 2 May 2023 10:19:53 +0200 Subject: [PATCH] introduce http-request rate-limit; smaller fixes Signed-off-by: Michael Kaufmann --- actions/admin/settings/110.accounts.php | 20 ++++++++++ install/froxlor.sql.php | 4 +- install/install.php | 2 + install/updates/froxlor/update_2.x.inc.php | 9 +++++ lib/Froxlor/Api/Api.php | 3 ++ lib/Froxlor/Cli/CliCommand.php | 2 +- lib/Froxlor/Froxlor.php | 2 +- lib/Froxlor/Http/RateLimiter.php | 46 ++++++++++++++++++++++ lib/Froxlor/PhpHelper.php | 7 +++- lib/init.php | 2 + lng/de.lng.php | 8 ++++ lng/en.lng.php | 8 ++++ 12 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 lib/Froxlor/Http/RateLimiter.php diff --git a/actions/admin/settings/110.accounts.php b/actions/admin/settings/110.accounts.php index c3ebcca27d..7a0b92abaa 100644 --- a/actions/admin/settings/110.accounts.php +++ b/actions/admin/settings/110.accounts.php @@ -138,6 +138,26 @@ 'save_method' => 'storeSettingField', 'advanced_mode' => true ], + 'system_req_limit_per_interval' => [ + 'label' => lng('serversettings.req_limit_per_interval'), + 'settinggroup' => 'system', + 'varname' => 'req_limit_per_interval', + 'type' => 'number', + 'min' => 30, + 'default' => 60, + 'save_method' => 'storeSettingField', + 'advanced_mode' => true + ], + 'system_req_limit_interval' => [ + 'label' => lng('serversettings.req_limit_interval'), + 'settinggroup' => 'system', + 'varname' => 'req_limit_interval', + 'type' => 'number', + 'min' => 5, + 'default' => 60, + 'save_method' => 'storeSettingField', + 'advanced_mode' => true + ], 'customer_accountprefix' => [ 'label' => lng('serversettings.accountprefix'), 'settinggroup' => 'customer', diff --git a/install/froxlor.sql.php b/install/froxlor.sql.php index d37da714d8..95e840aa64 100644 --- a/install/froxlor.sql.php +++ b/install/froxlor.sql.php @@ -699,6 +699,8 @@ ('system', 'updatecheck_data', ''), ('system', 'update_notify_last', '2.0.15'), ('system', 'traffictool', 'goaccess'), + ('system', 'req_limit_per_interval', 60), + ('system', 'req_limit_interval', 60), ('api', 'enabled', '0'), ('api', 'customer_default', '1'), ('2fa', 'enabled', '1'), @@ -743,7 +745,7 @@ ('panel', 'logo_overridecustom', '0'), ('panel', 'settings_mode', '0'), ('panel', 'version', '2.0.15'), - ('panel', 'db_version', '202303150'); + ('panel', 'db_version', '202304260'); DROP TABLE IF EXISTS `panel_tasks`; diff --git a/install/install.php b/install/install.php index b585c1b6f6..365908afc7 100644 --- a/install/install.php +++ b/install/install.php @@ -23,6 +23,7 @@ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ +use Froxlor\Http\RateLimiter; use Froxlor\UI\Panel\UI; use Froxlor\Install\Install; @@ -62,6 +63,7 @@ // init twig UI::initTwig(true); UI::sendHeaders(); +RateLimiter::run(true); $installer = new Install(); $installer->handle(); diff --git a/install/updates/froxlor/update_2.x.inc.php b/install/updates/froxlor/update_2.x.inc.php index 20daf7b746..30237d7e23 100644 --- a/install/updates/froxlor/update_2.x.inc.php +++ b/install/updates/froxlor/update_2.x.inc.php @@ -463,3 +463,12 @@ Update::showUpdateStep("Updating from 2.0.14 to 2.0.15", false); Froxlor::updateToVersion('2.0.15'); } + +if (Froxlor::isDatabaseVersion('202303150')) { + Update::showUpdateStep("Adding new request rate limit settings"); + Settings::AddNew("system.req_limit_per_interval", "60"); + Settings::AddNew("system.req_limit_interval", "60"); + Update::lastStepStatus(0); + + Froxlor::updateToDbVersion('202304260'); +} diff --git a/lib/Froxlor/Api/Api.php b/lib/Froxlor/Api/Api.php index f32d73a62f..ca6f2ba851 100644 --- a/lib/Froxlor/Api/Api.php +++ b/lib/Froxlor/Api/Api.php @@ -26,6 +26,7 @@ namespace Froxlor\Api; use Exception; +use Froxlor\Http\RateLimiter; use Froxlor\Settings; use voku\helper\AntiXSS; @@ -52,6 +53,8 @@ public function __construct() if (Settings::Get('api.enabled') != 1) { throw new Exception('API is not enabled. Please contact the administrator if you think this is wrong.', 400); } + + RateLimiter::run(); } /** diff --git a/lib/Froxlor/Cli/CliCommand.php b/lib/Froxlor/Cli/CliCommand.php index e3dbe7f6f0..0db8c5a284 100644 --- a/lib/Froxlor/Cli/CliCommand.php +++ b/lib/Froxlor/Cli/CliCommand.php @@ -122,7 +122,7 @@ private function runUpdate(OutputInterface $output): int include_once Froxlor::getInstallDir() . '/lib/tables.inc.php'; define('_CRON_UPDATE', 1); ob_start([ - 'this', + $this, 'cleanUpdateOutput' ]); include_once Froxlor::getInstallDir() . '/install/updatesql.php'; diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php index befe575c6c..5a9295eaeb 100644 --- a/lib/Froxlor/Froxlor.php +++ b/lib/Froxlor/Froxlor.php @@ -34,7 +34,7 @@ final class Froxlor const VERSION = '2.0.15'; // Database version (YYYYMMDDC where C is a daily counter) - const DBVERSION = '202303150'; + const DBVERSION = '202304260'; // Distribution branding-tag (used for Debian etc.) const BRANDING = ''; diff --git a/lib/Froxlor/Http/RateLimiter.php b/lib/Froxlor/Http/RateLimiter.php new file mode 100644 index 0000000000..a5934c5dc6 --- /dev/null +++ b/lib/Froxlor/Http/RateLimiter.php @@ -0,0 +1,46 @@ + $reset) { + $remaining = self::$limit_per_interval; + $reset = self::$reset_time; + } + + // If we've hit the limit, return an error + if ($remaining <= 0) { + header('HTTP/1.1 429 Too Many Requests'); + header("Retry-After: $reset"); + exit(); + } + + // Decrement the remaining requests and update the headers + $remaining--; + $_SESSION['HTTP_X_RATELIMIT_REMAINING'] = $remaining; + $_SESSION['HTTP_X_RATELIMIT_RESET'] = $reset; + + header("X-RateLimit-Limit: " . self::$limit_per_interval); + header("X-RateLimit-Remaining: " . $remaining); + header("X-RateLimit-Reset: " . $reset); + } +} diff --git a/lib/Froxlor/PhpHelper.php b/lib/Froxlor/PhpHelper.php index b4af5b62df..f5ee808938 100644 --- a/lib/Froxlor/PhpHelper.php +++ b/lib/Froxlor/PhpHelper.php @@ -519,7 +519,12 @@ public static function parseArrayToString(array $array, string $key = null, int } elseif (is_int($value)) { $str .= self::tabPrefix($depth, "'{$key}' => $value,\n"); } else { - $str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n"); + if ($key == 'password') { + // special case for passwords (nowdoc) + $str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n"); + } else { + $str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n"); + } } } else { $str .= self::parseArrayToString($value, $key, ($depth + 1)); diff --git a/lib/init.php b/lib/init.php index 78e67f6c83..2b5909bc74 100644 --- a/lib/init.php +++ b/lib/init.php @@ -52,6 +52,7 @@ use Froxlor\CurrentUser; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; +use Froxlor\Http\RateLimiter; use Froxlor\Idna\IdnaWrapper; use Froxlor\Language; use Froxlor\PhpHelper; @@ -121,6 +122,7 @@ // send ssl-related headers (later than the others because we need a working database-connection and installation) UI::sendSslHeaders(); +RateLimiter::run(); // create a new idna converter $idna_convert = new IdnaWrapper(); diff --git a/lng/de.lng.php b/lng/de.lng.php index 8af35cebb8..aeb904292a 100644 --- a/lng/de.lng.php +++ b/lng/de.lng.php @@ -2072,6 +2072,14 @@ 'toolselect' => 'Traffic Analyzer', ], 'requires_reconfiguration' => 'Änderungen an dieser Einstellungen benötigen unter Umständen eine erneute Konfiguration der folgenden Dienste:
%s', + 'req_limit_per_interval' => [ + 'title' => 'Anzahl der HTTP-Anfragen pro Intervall', + 'description' => 'Erlaubte Anzahl von HTTP-Anfragen pro Intervall (siehe unten) auf froxlor, Standard ist "60"', + ], + 'req_limit_interval' => [ + 'title' => 'Rate-Limit-Intervall', + 'description' => 'Specify the time in seconds for the number of HTTP requests, default is "60"', + ], ], 'spf' => [ 'use_spf' => 'Aktiviere SPF für Domains?', diff --git a/lng/en.lng.php b/lng/en.lng.php index 47e7f6b42c..9e83d43ca6 100644 --- a/lng/en.lng.php +++ b/lng/en.lng.php @@ -2196,6 +2196,14 @@ 'goaccess' => 'goacccess' ], 'requires_reconfiguration' => 'Changing this settings might require a reconfiguration of the following services:
%s', + 'req_limit_per_interval' => [ + 'title' => 'Number of HTTP requests per interval', + 'description' => 'Limit the number of HTTP requests per interval (see below) to froxlor, default is "60"', + ], + 'req_limit_interval' => [ + 'title' => 'Rate-limit interval', + 'description' => 'Zeit in Sekunden für die maximale Anzahl von HTTP-Anfragen, Standard ist "60".', + ], ], 'spf' => [ 'use_spf' => 'Activate SPF for domains?',