diff --git a/db/migrations/002-add-token-bucket.sql b/db/migrations/002-add-token-bucket.sql
new file mode 100644
index 0000000..bceb12b
--- /dev/null
+++ b/db/migrations/002-add-token-bucket.sql
@@ -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;
diff --git a/db/schema.sql b/db/schema.sql
index 467c76e..9ac12e6 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -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;
diff --git a/fn/auth.php b/fn/auth.php
index 2856b6c..06e47dd 100644
--- a/fn/auth.php
+++ b/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();
+}
diff --git a/pages.php b/pages.php
index 5f7bb53..f00e0f1 100644
--- a/pages.php
+++ b/pages.php
@@ -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 ' . CONF['reg']['registry'] . '
',
+ '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',
diff --git a/pages/auth/register.php b/pages/auth/register.php
index 9ecce6f..a63d671 100644
--- a/pages/auth/register.php
+++ b/pages/auth/register.php
@@ -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();
diff --git a/pages/ht/add-http-dns.php b/pages/ht/add-http-dns.php
index 8833bf7..362b9bc 100644
--- a/pages/ht/add-http-dns.php
+++ b/pages/ht/add-http-dns.php
@@ -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 ' . CONF['ht']['ipv4_address'] . '
.');
+ 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);
diff --git a/pages/ht/add-http-onion.php b/pages/ht/add-http-onion.php
index be7ca24..242dac6 100644
--- a/pages/ht/add-http-onion.php
+++ b/pages/ht/add-http-onion.php
@@ -9,6 +9,8 @@ if (processForm()) {
if ($dirsStatuses[$_POST['dir']] !== false)
output(403, 'Wrong value for dir
.');
+ rateLimit();
+
// Add Tor config
$torConf = "HiddenServiceDir " . CONF['ht']['tor_keys_path'] . "/" . $_SESSION['username'] . "/" . $_POST['dir'] . "/
HiddenServicePort 80 [::1]:" . CONF['ht']['internal_onion_http_port'] . "
diff --git a/pages/ns/zone-add.php b/pages/ns/zone-add.php
index 503f355..0bf62ee 100644
--- a/pages/ns/zone-add.php
+++ b/pages/ns/zone-add.php
@@ -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'],
diff --git a/pages/reg/register.php b/pages/reg/register.php
index 7b19861..97cd77b 100644
--- a/pages/reg/register.php
+++ b/pages/reg/register.php
@@ -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'],
diff --git a/router.php b/router.php
index 0ef45f9..b5ad11f 100644
--- a/router.php
+++ b/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);