Token bucket rate limiting
This commit is contained in:
parent
c65dedf9de
commit
77f6dfaada
10 changed files with 133 additions and 15 deletions
29
db/migrations/002-add-token-bucket.sql
Normal file
29
db/migrations/002-add-token-bucket.sql
Normal file
|
@ -0,0 +1,29 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
-- Add user-relative bucket
|
||||
ALTER TABLE "users" ADD COLUMN "bucket_tokens" INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE "users" ADD COLUMN "bucket_last_update" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Remove "DEFAULT 0" from user-relative bucket
|
||||
CREATE TABLE "users_temp" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"username" TEXT NOT NULL UNIQUE,
|
||||
"password" TEXT NOT NULL,
|
||||
"registration_date" TEXT NOT NULL,
|
||||
"bucket_tokens" INTEGER NOT NULL,
|
||||
"bucket_last_update" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
INSERT INTO "users_temp" SELECT "id","username","password","registration_date","bucket_tokens","bucket_last_update" FROM "users";
|
||||
DROP TABLE "users";
|
||||
ALTER TABLE "users_temp" RENAME TO "users";
|
||||
|
||||
-- Add instance-wide bucket
|
||||
CREATE TABLE IF NOT EXISTS "params" (
|
||||
"name" TEXT NOT NULL UNIQUE,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "params"("name", "value") VALUES("instance_bucket_tokens", "0");
|
||||
INSERT INTO "params"("name", "value") VALUES("instance_bucket_last_update", "0");
|
||||
|
||||
COMMIT;
|
|
@ -1,9 +1,13 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE IF NOT EXISTS "params" (
|
||||
"name" TEXT NOT NULL UNIQUE,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "registry" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"domain" TEXT NOT NULL UNIQUE,
|
||||
"username" TEXT NOT NULL,
|
||||
"last_renewal" INTEGER,
|
||||
"last_renewal" TEXT NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "zones" (
|
||||
|
@ -16,7 +20,9 @@ CREATE TABLE IF NOT EXISTS "users" (
|
|||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"username" TEXT NOT NULL UNIQUE,
|
||||
"password" TEXT NOT NULL,
|
||||
"registration_date" INTEGER NOT NULL,
|
||||
"registration_date" TEXT NOT NULL,
|
||||
"bucket_tokens" INTEGER NOT NULL,
|
||||
"bucket_last_update" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "sites" (
|
||||
|
@ -26,7 +32,9 @@ CREATE TABLE IF NOT EXISTS "sites" (
|
|||
"domain" TEXT NOT NULL UNIQUE,
|
||||
"domain_type" TEXT NOT NULL,
|
||||
"protocol" TEXT NOT NULL,
|
||||
"creation_date" INTEGER NOT NULL,
|
||||
"creation_date" TEXT NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
INSERT INTO "params"("name", "value") VALUES("instance_bucket_tokens", "0");
|
||||
INSERT INTO "params"("name", "value") VALUES("instance_bucket_last_update", "0");
|
||||
COMMIT;
|
||||
|
|
55
fn/auth.php
55
fn/auth.php
|
@ -50,3 +50,58 @@ function changePassword($username, $password) {
|
|||
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function rateLimit() {
|
||||
if (PAGE_METADATA['tokens_account_cost'] ?? 0 > 0)
|
||||
rateLimitAccount(PAGE_METADATA['tokens_account_cost']);
|
||||
|
||||
if (PAGE_METADATA['tokens_instance_cost'] ?? 0 > 0)
|
||||
rateLimitInstance(PAGE_METADATA['tokens_instance_cost']);
|
||||
}
|
||||
|
||||
function rateLimitAccount($requestedTokens) {
|
||||
// Get
|
||||
$userData = query('select', 'users', ['username' => $_SESSION['username']]);
|
||||
$tokens = $userData[0]['bucket_tokens'];
|
||||
$bucketLastUpdate = $userData[0]['bucket_last_update'];
|
||||
|
||||
// Compute
|
||||
$tokens = min(86400, $tokens + (time() - $bucketLastUpdate));
|
||||
|
||||
if ($requestedTokens > $tokens)
|
||||
output(453, 'Limite d\'actions par compte atteinte. Réessayez plus tard.');
|
||||
|
||||
$tokens -= $requestedTokens;
|
||||
|
||||
// Update
|
||||
$db = new PDO('sqlite:' . DB_PATH);
|
||||
$stmt = $db->prepare("UPDATE users SET bucket_tokens = :bucket_tokens, bucket_last_update = :bucket_last_update WHERE username = :username");
|
||||
$stmt->bindValue(':username', $_SESSION['username']);
|
||||
$stmt->bindValue(':bucket_tokens', $tokens);
|
||||
$stmt->bindValue(':bucket_last_update', time());
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function rateLimitInstance($requestedTokens) {
|
||||
// Get
|
||||
$tokens = query('select', 'params', ['name' => 'instance_bucket_tokens'], 'value')[0];
|
||||
$bucketLastUpdate = query('select', 'params', ['name' => 'instance_bucket_last_update'], 'value')[0];
|
||||
|
||||
// Compute
|
||||
$tokens = min(86400, $tokens + (time() - $bucketLastUpdate));
|
||||
|
||||
if ($requestedTokens > $tokens)
|
||||
output(453, 'Limite d\'actions globale atteinte. Réessayez plus tard.');
|
||||
|
||||
$tokens -= $requestedTokens;
|
||||
|
||||
// Update
|
||||
$db = new PDO('sqlite:' . DB_PATH);
|
||||
$stmt = $db->prepare("UPDATE params SET value = :bucket_tokens WHERE name = 'instance_bucket_tokens';");
|
||||
$stmt->bindValue(':bucket_tokens', $tokens);
|
||||
$stmt->execute();
|
||||
|
||||
$stmt = $db->prepare("UPDATE params SET value = :bucket_last_update WHERE name = 'instance_bucket_last_update';");
|
||||
$stmt->bindValue(':bucket_last_update', time());
|
||||
$stmt->execute();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ define('PAGES', [
|
|||
'register' => [
|
||||
'title' => 'Créer un compte',
|
||||
'description' => 'Créer un nouveau compte Niver',
|
||||
'tokens_instance_cost' => 7200,
|
||||
],
|
||||
'unregister' => [
|
||||
'title' => 'Supprimer son compte',
|
||||
|
@ -38,6 +39,7 @@ define('PAGES', [
|
|||
'register' => [
|
||||
'title' => 'Enregistrer un nouveau domaine',
|
||||
'description' => 'Prendre possession d\'un sous-domaine de <code>' . CONF['reg']['registry'] . '</code>',
|
||||
'tokens_account_cost' => 3600,
|
||||
],
|
||||
'unregister' => [
|
||||
'title' => 'Effacer un domaine',
|
||||
|
@ -68,6 +70,7 @@ define('PAGES', [
|
|||
'zone-add' => [
|
||||
'title' => 'Ajouter une zone',
|
||||
'description' => 'Pour qu\'elle soit gérée par le serveur de noms de Niver',
|
||||
'tokens_account_cost' => 1800,
|
||||
],
|
||||
'zone-del' => [
|
||||
'title' => 'Effacer une zone',
|
||||
|
@ -130,10 +133,12 @@ define('PAGES', [
|
|||
'add-http-onion' => [
|
||||
'title' => 'Ajouter un accès HTTP par Onion',
|
||||
'description' => 'Ajouter un accès HTTP par ' . linkToDocs('tor', 'service Onion') . ' sur un sous-dossier de l\'espace SFTP',
|
||||
'tokens_account_cost' => 1800,
|
||||
],
|
||||
'add-http-dns' => [
|
||||
'title' => 'Ajouter un accès HTTP par DNS+TLS',
|
||||
'description' => 'Ajouter un accès HTTP par ' . linkToDocs('dns', 'DNS') . ' et ' . linkToDocs('tls', 'TLS') . ' sur un sous-dossier de l\'espace SFTP',
|
||||
'tokens_account_cost' => 3600,
|
||||
],
|
||||
'del-http-onion' => [
|
||||
'title' => 'Retirer un accès HTTP par Onion',
|
||||
|
|
|
@ -8,6 +8,16 @@ if (processForm(false)) {
|
|||
if (userExist($_POST['username']) !== false)
|
||||
output(403, 'Ce nom de compte est déjà utilisé.');
|
||||
|
||||
rateLimit();
|
||||
|
||||
insert('users', [
|
||||
'username' => $_POST['username'],
|
||||
'password' => hashPassword($_POST['password']),
|
||||
'registration_date' => date("Y-m-d H:i:s"),
|
||||
'bucket_tokens' => 0,
|
||||
'bucket_last_update' => 0,
|
||||
]);
|
||||
|
||||
// Setup SFTP directory
|
||||
umask(0002);
|
||||
if (mkdir(CONF['ht']['ht_path'] . "/" . $_POST['username'], 0775) !== true)
|
||||
|
@ -25,12 +35,6 @@ if (processForm(false)) {
|
|||
if ($code !== 0)
|
||||
output(500, 'Can\'t create Tor keys directory.');
|
||||
|
||||
insert('users', [
|
||||
'username' => $_POST['username'],
|
||||
'password' => hashPassword($_POST['password']),
|
||||
'registration_date' => date("Y-m-d H:i:s"),
|
||||
]);
|
||||
|
||||
$_SESSION['username'] = $_POST['username'];
|
||||
|
||||
redir();
|
||||
|
|
|
@ -26,6 +26,8 @@ if (processForm()) {
|
|||
if (equalArrays([CONF['ht']['ipv4_address']], array_column($remoteARecords, 'ip')) !== true)
|
||||
output(403, 'Ce domaine doit avoir pour unique enregistrement A <code>' . CONF['ht']['ipv4_address'] . '</code>.');
|
||||
|
||||
rateLimit();
|
||||
|
||||
addSite($_SESSION['username'], $_POST['dir'], $_POST['domain'], "dns", "http");
|
||||
|
||||
exec(CONF['ht']['sudo_path'] . " " . CONF['ht']['certbot_path'] . " certonly --quiet" . (CONF['ht']['letsencrypt_use_production'] ? '' : ' --test-cert') . " --key-type rsa --rsa-key-size 3072 --webroot --webroot-path /srv/niver/acme --domain " . $_POST['domain'], $output, $returnCode);
|
||||
|
|
|
@ -9,6 +9,8 @@ if (processForm()) {
|
|||
if ($dirsStatuses[$_POST['dir']] !== false)
|
||||
output(403, 'Wrong value for <code>dir</code>.');
|
||||
|
||||
rateLimit();
|
||||
|
||||
// Add Tor config
|
||||
$torConf = "HiddenServiceDir " . CONF['ht']['tor_keys_path'] . "/" . $_SESSION['username'] . "/" . $_POST['dir'] . "/
|
||||
HiddenServicePort 80 [::1]:" . CONF['ht']['internal_onion_http_port'] . "
|
||||
|
|
|
@ -14,6 +14,8 @@ if (processForm()) {
|
|||
if (equalArrays(CONF['ns']['servers'], $matches[1]) !== true)
|
||||
output(403, 'Les serveurs ayant autorité dans cette zone indiqués par la zone parente ne sont pas ceux de Niver.');
|
||||
|
||||
rateLimit();
|
||||
|
||||
insert('zones', [
|
||||
'zone' => $_POST['domain'],
|
||||
'username' => $_SESSION['username'],
|
||||
|
|
|
@ -12,6 +12,8 @@ if (processForm()) {
|
|||
if (in_array($_POST['subdomain'], explode("\n", file_get_contents(CONF['common']['root_path'] . '/pages/reg/reserved.txt'))))
|
||||
output(403, 'Ce domaine n\'est pas disponible à l\'enregistrement. Il est réservé.');
|
||||
|
||||
rateLimit();
|
||||
|
||||
insert('registry', [
|
||||
'domain' => $domain,
|
||||
'username' => $_SESSION['username'],
|
||||
|
|
21
router.php
21
router.php
|
@ -26,19 +26,28 @@ define("PAGE_LINEAGE", explode('/', PAGE_ADDRESS));
|
|||
define("SERVICE", dirname(PAGE_ADDRESS));
|
||||
define("PAGE", basename(PAGE_ADDRESS, '.php'));
|
||||
|
||||
function getTitlesLineage($pages, $pageElements) {
|
||||
if (!isset($pages['index']) OR $pageElements[0] === 'index')
|
||||
return [$pages[$pageElements[0]]['title'] ?? false];
|
||||
$pageMetadata = [];
|
||||
function getPageInformations($pages, $pageElements) {
|
||||
global $pageMetadata;
|
||||
if (!isset($pages['index']) OR $pageElements[0] === 'index') {
|
||||
return [
|
||||
'titles_lineage' => [$pages[$pageElements[0]]['title'] ?? false],
|
||||
'page_metadata' => $pages[$pageElements[0]] ?? NULL
|
||||
];
|
||||
define("PAGE_METADATA", $pages[$pageElements[0]] ?? []);
|
||||
}
|
||||
$result = $pages['index']['title'];
|
||||
if (!isset($pageElements[1]))
|
||||
unset($pages['index']);
|
||||
else
|
||||
$pages = $pages[array_shift($pageElements)] ?? false;
|
||||
$results = getTitlesLineage($pages, $pageElements);
|
||||
$results[] = $result;
|
||||
$results = getPageInformations($pages, $pageElements);
|
||||
$results['titles_lineage'][] = $result;
|
||||
return $results;
|
||||
}
|
||||
define('TITLES_LINEAGE', array_reverse(getTitlesLineage(PAGES, PAGE_LINEAGE)));
|
||||
$pageInformations = getPageInformations(PAGES, PAGE_LINEAGE);
|
||||
define('TITLES_LINEAGE', array_reverse($pageInformations['titles_lineage']));
|
||||
define('PAGE_METADATA', $pageInformations['page_metadata']);
|
||||
|
||||
if (!TITLES_LINEAGE[array_key_last(TITLES_LINEAGE)]) {
|
||||
http_response_code(404);
|
||||
|
|
Loading…
Reference in a new issue