Ver Fonte

Split pages/ between pg-act/ and pg-view/

Miraty há 2 anos atrás
pai
commit
73c137aaba
95 ficheiros alterados com 1486 adições e 1365 exclusões
  1. 5 1
      fn/auth.php
  2. 4 25
      fn/common.php
  3. 14 0
      fn/dns.php
  4. 4 2
      fn/ns.php
  5. 5 3
      fn/reg.php
  6. 2 3
      form.ns.php
  7. 2 0
      pages.php
  8. 0 36
      pages/auth/approval.php
  9. 0 42
      pages/auth/login.php
  10. 0 69
      pages/auth/register.php
  11. 0 62
      pages/auth/unregister.php
  12. 0 30
      pages/auth/username.php
  13. 0 105
      pages/ht/add-http-dns.php
  14. 0 70
      pages/ht/add-http-onion.php
  15. 0 96
      pages/ns/print.php
  16. 0 78
      pages/ns/zone-add.php
  17. 0 25
      pages/ns/zone-del.php
  18. 0 49
      pages/reg/print.php
  19. 0 38
      pages/reg/register.php
  20. 0 44
      pages/reg/transfer.php
  21. 18 0
      pg-act/auth/approval.php
  22. 28 0
      pg-act/auth/login.php
  23. 0 0
      pg-act/auth/logout.php
  24. 10 0
      pg-act/auth/password.php
  25. 50 0
      pg-act/auth/register.php
  26. 40 0
      pg-act/auth/unregister.php
  27. 15 0
      pg-act/auth/username.php
  28. 59 0
      pg-act/ht/add-http-dns.php
  29. 47 0
      pg-act/ht/add-http-onion.php
  30. 8 0
      pg-act/ht/del-http-dns.php
  31. 8 0
      pg-act/ht/del-http-onion.php
  32. 23 0
      pg-act/ns/caa.php
  33. 14 0
      pg-act/ns/cname.php
  34. 14 0
      pg-act/ns/dname.php
  35. 15 77
      pg-act/ns/edit.php
  36. 14 0
      pg-act/ns/ip.php
  37. 70 0
      pg-act/ns/loc.php
  38. 18 0
      pg-act/ns/mx.php
  39. 14 0
      pg-act/ns/ns.php
  40. 25 0
      pg-act/ns/print.php
  41. 26 0
      pg-act/ns/srv.php
  42. 23 0
      pg-act/ns/sshfp.php
  43. 27 0
      pg-act/ns/tlsa.php
  44. 15 0
      pg-act/ns/txt.php
  45. 52 0
      pg-act/ns/zone-add.php
  46. 7 0
      pg-act/ns/zone-del.php
  47. 30 0
      pg-act/reg/ds.php
  48. 16 0
      pg-act/reg/glue.php
  49. 13 0
      pg-act/reg/ns.php
  50. 11 0
      pg-act/reg/print.php
  51. 22 0
      pg-act/reg/register.php
  52. 0 0
      pg-act/reg/reserved.txt
  53. 26 0
      pg-act/reg/transfer.php
  54. 7 0
      pg-act/reg/unregister.php
  55. 10 0
      pg-view/auth/approval.php
  56. 0 0
      pg-view/auth/index.php
  57. 13 0
      pg-view/auth/login.php
  58. 3 0
      pg-view/auth/logout.php
  59. 0 15
      pg-view/auth/password.php
  60. 17 0
      pg-view/auth/register.php
  61. 17 0
      pg-view/auth/unregister.php
  62. 10 0
      pg-view/auth/username.php
  63. 45 0
      pg-view/ht/add-http-dns.php
  64. 22 0
      pg-view/ht/add-http-onion.php
  65. 1 10
      pg-view/ht/del-http-dns.php
  66. 0 9
      pg-view/ht/del-http-onion.php
  67. 0 0
      pg-view/ht/index.php
  68. 0 0
      pg-view/index.php
  69. 0 28
      pg-view/ns/caa.php
  70. 0 19
      pg-view/ns/cname.php
  71. 0 19
      pg-view/ns/dname.php
  72. 58 0
      pg-view/ns/edit.php
  73. 0 0
      pg-view/ns/index.php
  74. 0 19
      pg-view/ns/ip.php
  75. 0 75
      pg-view/ns/loc.php
  76. 0 23
      pg-view/ns/mx.php
  77. 0 19
      pg-view/ns/ns.php
  78. 77 0
      pg-view/ns/print.php
  79. 0 31
      pg-view/ns/srv.php
  80. 0 28
      pg-view/ns/sshfp.php
  81. 0 32
      pg-view/ns/tlsa.php
  82. 0 20
      pg-view/ns/txt.php
  83. 19 0
      pg-view/ns/zone-add.php
  84. 12 0
      pg-view/ns/zone-del.php
  85. 1 43
      pg-view/reg/ds.php
  86. 2 24
      pg-view/reg/glue.php
  87. 0 0
      pg-view/reg/index.php
  88. 2 21
      pg-view/reg/ns.php
  89. 34 0
      pg-view/reg/print.php
  90. 11 0
      pg-view/reg/register.php
  91. 242 0
      pg-view/reg/reserved.txt
  92. 11 0
      pg-view/reg/transfer.php
  93. 2 15
      pg-view/reg/unregister.php
  94. 25 60
      router.php
  95. 51 0
      view.php

+ 5 - 1
fn/auth.php

@@ -49,9 +49,13 @@ function changePassword($id, $password) {
 		->execute([':password' => hashPassword($password), ':id' => $id]);
 }
 
-function logout() {
+function stopSession() {
 	if (session_status() === PHP_SESSION_ACTIVE)
 		session_destroy();
+}
+
+function logout() {
+	stopSession();
 
 	header('Clear-Site-Data: "*"');
 

+ 4 - 25
fn/common.php

@@ -1,37 +1,16 @@
 <?php
 
-$final_message = NULL;
-
 function output($code, $msg = '', $logs = ['']) {
-	global $final_message;
+	http_response_code($code);
 	$shortCode = $code / 100 % 10;
+	if ($shortCode === 5)
+		error_log('Niver internal error: ' . strip_tags($msg) . implode(LF, $logs));
 	$final_message = match ($shortCode) {
 		2 => ($msg === '') ? '' : '<p><output><strong>Succès</strong> : <em>' . $msg . '</em></output></p>' . LF,
 		4 => '<p><output><strong>Erreur utilisataire</strong> : <em>' . $msg . '</em></output></p>' . LF,
 		5 => '<p><output><strong>Server error</strong>: The server encountered an error: <em>' . $msg . '</em></output></p>' . LF,
 	};
-	http_response_code($code);
-	if ($shortCode === 5)
-		error_log('Niver internal error: ' . strip_tags($msg) . implode(LF, $logs));
-	if ($code !== 200)
-		executePage();
-}
-
-function processForm($requireLogin = true) {
-	if (http_response_code() !== 200)
-		return false;
-	if ($_POST === []) {
-		if ($requireLogin AND !isset($_SESSION['id']))
-			echo '<p>Ce formulaire ne sera pas accepté car il faut <a class="auth" href="' . redirUrl('auth/login') . '">se connecter</a> avant.</p>';
-		return false;
-	}
-	if ($requireLogin) {
-		if (isset($_SESSION['id']) !== true)
-			output(403, 'Vous devez être connecté·e à un compte pour effectuer cette action.');
-		if (isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0]) !== true)
-			output(403, 'Ce compte n\'existe plus. Déconnectez-vous pour terminer cette session fantôme.');
-	}
-	return true;
+	displayPage(['final_message' => $final_message]);
 }
 
 function insert($table, $values) {

+ 14 - 0
fn/dns.php

@@ -1,5 +1,19 @@
 <?php
 
+function parseZoneFile($zone_content, $types, $filter_domain = false) {
+	$parsed_zone_content = [];
+	foreach(explode(LF, $zone_content) as $zone_line) {
+		if ($zone_line === '' OR str_starts_with($zone_line, ';'))
+			continue; // Ignore empty lines and comments
+		$elements = preg_split('/[\t ]+/', $zone_line, 4);
+		if ($filter_domain !== false AND !str_ends_with($elements[0], $filter_domain))
+			continue; // Ignore records for other domains
+		if (!in_array($elements[2], $types, true)) continue; // Ignore records generated by Knot
+		array_push($parsed_zone_content, array_map('htmlspecialchars', $elements));
+	}
+	return $parsed_zone_content;
+}
+
 function knotcConfExec($cmds) {
 	exec(CONF['dns']['knotc_path'] . ' conf-begin', $output['begin'], $code['begin']);
 	if ($code['begin'] !== 0)

+ 4 - 2
fn/ns.php

@@ -44,8 +44,10 @@ function nsParseCommonRequirements() {
 	return $values;
 }
 
-function nsListUserZones($username) {
-	return query('select', 'zones', ['username' => $username], 'zone');
+function nsListUserZones() {
+	if (isset($_SESSION['id']))
+		return query('select', 'zones', ['username' => $_SESSION['id']], 'zone');
+	return [];
 }
 
 function nsCheckZonePossession($zone) {

+ 5 - 3
fn/reg.php

@@ -2,12 +2,14 @@
 
 const SUBDOMAIN_REGEX = '^[a-z0-9]{4,63}$';
 
-function regListUserDomains($username) {
-	return query('select', 'registry', ['username' => $username], 'domain');
+function regListUserDomains() {
+	if (isset($_SESSION['id']))
+		return query('select', 'registry', ['username' => $_SESSION['id']], 'domain');
+	return [];
 }
 
 function regCheckDomainPossession($domain) {
-	if (in_array($domain, regListUserDomains($_SESSION['id']), true) !== true)
+	if (in_array($domain, regListUserDomains(), true) !== true)
 		output(403, 'You don\'t own this domain.');
 }
 

+ 2 - 3
form.ns.php

@@ -18,9 +18,8 @@
 			<select required="" name="zone" id="zone">
 				<option value="" disabled="" selected="">-</option>
 <?php
-if (isset($_SESSION['id']))
-	foreach (nsListUserZones($_SESSION['id']) as $zone)
-		echo "<option value='" . $zone . "'>" . $zone . "</option>";
+foreach (nsListUserZones() as $zone)
+	echo "<option value='" . $zone . "'>" . $zone . "</option>";
 ?>
 
 			</select>

+ 2 - 0
pages.php

@@ -12,10 +12,12 @@ define('PAGES', [
 		'login' => [
 			'title' => 'Se connecter',
 			'description' => 'Démarrer une nouvelle session avec un compte existant',
+			'require-login' => false,
 		],
 		'register' => [
 			'title' => 'Créer un compte',
 			'description' => 'Créer un nouveau compte Niver',
+			'require-login' => false,
 			'tokens_instance_cost' => 7200,
 		],
 		'unregister' => [

+ 0 - 36
pages/auth/approval.php

@@ -1,36 +0,0 @@
-<?php
-
-insert('approval-keys', ['key' => bin2hex(random_bytes(16))]);
-
-if (processForm()) {
-
-	if ($_SESSION['type'] !== 'testing')
-		output(403, 'Approbation impossible : votre compte est déjà approuvé.');
-
-	if (isset(query('select', 'approval-keys', ['key' => $_POST['key']], 'key')[0]) !== true)
-		output(403, 'Approbation impossible : cette clé d\'approbation n\'est pas disponible. Elle a été mal saisie, a expiré ou a déjà été utilisée pour un autre compte.');
-
-	query('delete', 'approval-keys', ['key' => $_POST['key']]);
-
-	DB->prepare('UPDATE users SET type = "approved" WHERE id = :id')
-	->execute([':id' => $_SESSION['id']]);
-
-	$_SESSION['type'] = 'approved';
-
-	insert('approval-keys', ['key' => bin2hex(random_bytes(16))]);
-
-	output(200, 'Compte approuvé.');
-}
-
-?>
-
-<p>
-	Ce formulaire permet d'utiliser une clé d'approbation pour valider son compte. Une clé d'approbation est distribuée par l'administrataire sur demande.
-</p>
-
-<form method="post">
-	<label for="key">Clé d'approbation</label><br>
-	<input required="" id="key" size="33" name="key" type="text" placeholder="27b81fbd8277b11ed1cf03d476cec503">
-	<br>
-	<input type="submit" value="Utiliser">
-</form>

+ 0 - 42
pages/auth/login.php

@@ -1,42 +0,0 @@
-<?php
-
-if (processForm(false)) {
-	checkPasswordFormat($_POST['password']);
-
-	checkUsernameFormat($_POST['username']);
-
-	$username = hashUsername($_POST['username']);
-
-	if (usernameExists($username) !== true)
-		output(403, 'Connexion impossible : ce compte n\'existe pas.');
-
-	$id = query('select', 'users', ['username' => $username], 'id')[0];
-
-	if (checkPassword($id, $_POST['password']) !== true)
-		output(403, 'Connexion impossible : clé de passe invalide.');
-
-	$_SESSION['id'] = $id;
-	$_SESSION['display-username'] = htmlspecialchars($_POST['username']);
-	$_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
-
-	if (outdatedPasswordHash($id))
-		changePassword($id, $_POST['password']);
-
-	redir();
-}
-
-?>
-
-<p>Pas de compte ? <a class="auth" href="register">En créer un</a></p>
-
-<form method="post">
-	<label for="username">Identifiant</label><br>
-	<input required="" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" id="username" name="username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>">
-	<br>
-
-	<label for="password">Clé de passe</label><br>
-	<input required="" autocomplete="current-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="password" name="password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>">
-	<br>
-
-	<input type="submit">
-</form>

+ 0 - 69
pages/auth/register.php

@@ -1,69 +0,0 @@
-<?php
-
-if (processForm(false)) {
-	checkPasswordFormat($_POST['password']);
-
-	checkUsernameFormat($_POST['username']);
-
-	$username = hashUsername($_POST['username']);
-
-	if (usernameExists($username) !== false)
-		output(403, 'Ce nom de compte est déjà utilisé.');
-
-	rateLimit();
-
-	$id = hash('sha256', random_bytes(32));
-
-	insert('users', [
-		'id' => $id,
-		'username' => $username,
-		'password' => hashPassword($_POST['password']),
-		'registration_date' => date('Y-m-d H:i:s'),
-		'bucket_tokens' => 0,
-		'bucket_last_update' => 0,
-		'type' => 'testing',
-	]);
-
-	// Setup SFTP directory
-	umask(0002);
-	if (mkdir(CONF['ht']['ht_path'] . '/' . $id, 0775) !== true)
-		output(500, 'Can\'t create user directory.');
-	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/' . $id . ' --no-dereference', result_code: $code);
-	if ($code !== 0)
-		output(500, 'Can\'t change user directory group.');
-
-	// Setup Tor config directory
-	if (mkdir(CONF['ht']['tor_config_path'] . '/' . $id, 0755) !== true)
-		output(500, 'Can\'t create Tor config directory.');
-
-	// Setup Tor keys directory
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $id, result_code: $code);
-	if ($code !== 0)
-		output(500, 'Can\'t create Tor keys directory.');
-
-	$_SESSION['id'] = $id;
-	$_SESSION['display-username'] = htmlspecialchars($_POST['username']);
-	$_SESSION['type'] = 'testing';
-
-	redir();
-}
-
-?>
-
-<p>Déjà un compte ? <a class="auth" href="login">Se connecter</a></p>
-
-<form method="post">
-
-	<label for="username">Identifiant</label>
-	<br>
-	<input id="username" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" required="" name="username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>"><br>
-
-	<details>
-		<summary><label for="password">Clé de passe</label></summary>
-		<p>Une clé de passe sécurisée est trop compliquée à deviner pour une attaque qui testerait automatiquement plein de clés de passe tout en connaissant d'autres informations et secrets sur vous.</p>
-		<p>Minimum 8 caractères si elle contient minuscule, majuscule et chiffre, ou minimum 10 caractères sinon.</p>
-	</details>
-	<input autocomplete="new-password" id="password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" required="" name="password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>">
-	<br>
-	<input type="submit">
-</form>

+ 0 - 62
pages/auth/unregister.php

@@ -1,62 +0,0 @@
-<?php
-
-if (processForm()) {
-	if (!isset($_POST['delete']))
-		output(403, 'Il faut confirmer la suppression du compte');
-
-	foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain)
-		regDeleteDomain($domain);
-
-	foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone)
-		nsDeleteZone($zone);
-
-	foreach (query('select', 'sites', [
-		'username' => $_SESSION['id'],
-		'domain_type' => 'onion',
-		'protocol' => 'http',
-	], 'site_dir') as $dir)
-		htDeleteSite($dir, domainType: 'onion', protocol: 'http');
-
-	foreach (query('select', 'sites', [
-		'username' => $_SESSION['id'],
-		'domain_type' => 'dns',
-		'protocol' => 'http',
-	], 'site_dir') as $dir)
-		htDeleteSite($dir, domainType: 'dns', protocol: 'http');
-
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'], result_code: $code);
-	if ($code !== 0)
-		output(500, 'Can\'t remove Tor keys directory.');
-
-	removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id']);
-
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] .  ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'], result_code: $code);
-	if ($code !== 0)
-		output(500, 'Can\'t remove user\'s directory.');
-
-	query('delete', 'users', ['id' => $_SESSION['id']]);
-
-	logout();
-
-	output(200, 'Compte supprimé.');
-}
-
-?>
-
-<p>
-	Cette action supprimera toutes les données appartenant à ce compte, y compris :
-</p>
-
-<ul>
-	<li>la possession et la réservation des domaines dans le registre</li>
-	<li>les enregistrements DNS des zones hébergées sur le serveur de noms</li>
-	<li>le contenu des sites</li>
-	<li>les paires de clés des services Onion</li>
-</ul>
-
-<form method="post">
-	<input type="checkbox" name="delete" id="delete" required="">
-	<label for="delete">Supprimer mon compte et toutes ses données</label>
-	<br>
-	<input type="submit">
-</form>

+ 0 - 30
pages/auth/username.php

@@ -1,30 +0,0 @@
-<?php
-
-if (processForm()) {
-	checkUsernameFormat($_POST['new-username']);
-
-	$username = hashUsername($_POST['new-username']);
-
-	if (usernameExists($username) !== false)
-		output(403, 'Ce nom de compte est déjà utilisé.');
-
-	DB->prepare('UPDATE users SET username = :username WHERE id = :id')
-	->execute([':username' => $username, ':id' => $_SESSION['id']]);
-
-	$_SESSION['display-username'] = htmlspecialchars($_POST['new-username']);
-
-	output(200, 'Identifiant changé.');
-}
-
-?>
-
-<p>
-	Vous pouvez ici changer l'identifiant permettant d'accéder à votre compte Niver.
-</p>
-
-<form method="post">
-	<label for="new-username">Nouvel identifiant</label><br>
-	<input required="" autocomplete="new-username" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" id="new-username" name="new-username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>"><br>
-
-	<input type="submit">
-</form>

+ 0 - 105
pages/ht/add-http-dns.php

@@ -1,105 +0,0 @@
-<?php
-
-if (processForm()) {
-	$_POST['domain'] = formatDomain($_POST['domain']);
-
-	if (dirsStatuses('dns', 'http')[$_POST['dir']] !== false)
-		output(403, 'Wrong value for <code>dir</code>.');
-
-	if (query('select', 'sites', ['domain' => $_POST['domain']], 'domain') !== [])
-		output(403, 'Ce domaine existe déjà sur ce service.');
-
-	$remoteAaaaRecords = dns_get_record($_POST['domain'], DNS_AAAA);
-	if (is_array($remoteAaaaRecords) !== true)
-		output(500, 'Erreur lors de la récupération de l\'enregistrement AAAA.');
-	if (equalArrays([CONF['ht']['ipv6_address']], array_column($remoteAaaaRecords, 'ipv6')) !== true)
-		output(403, 'Ce domaine doit avoir pour unique enregistrement AAAA <code>' . CONF['ht']['ipv6_address'] . '</code>.');
-
-	$remoteARecords = dns_get_record($_POST['domain'], DNS_A);
-	if (is_array($remoteARecords) !== true)
-		output(500, 'Erreur lors de la récupération de l\'enregistrement A.');
-	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>.');
-
-	$remoteTXTRecords = dns_get_record($_POST['domain'], DNS_TXT);
-	if (is_array($remoteTXTRecords) !== true)
-		output(500, 'Erreur lors de la récupération de l\'enregistrement TXT.');
-	if (preg_match('/^' . preg_quote(SERVER_NAME, '/') . '_domain-verification=([0-9a-f]{8})-([0-9a-f]{32})$/Dm', implode(LF, array_column($remoteTXTRecords, 'txt')), $matches) !== 1)
-		output(403, 'Aucun enregistrement TXT au format correct trouvé.');
-
-	checkAuthToken($matches[1], $matches[2]);
-
-	rateLimit();
-
-	addSite($_SESSION['id'], $_POST['dir'], $_POST['domain'], 'dns', 'http');
-
-	exec('2>&1 ' . CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' certonly' . (($_SESSION['type'] === 'approved') ? '' : ' --test-cert') . ' --key-type rsa --rsa-key-size 3072 --webroot --webroot-path /srv/niver/acme --domain ' . $_POST['domain'], $output, $returnCode);
-	if ($returnCode !== 0)
-		output(500, 'Certbot failed to get a Let\'s Encrypt certificate.', $output);
-
-	$nginxConf = 'server {
-	listen [' . CONF['ht']['ipv6_listen_address'] . ']:' . CONF['ht']['https_port'] . ' ssl http2;
-	listen ' . CONF['ht']['ipv4_listen_address'] . ':' . CONF['ht']['https_port'] . ' ssl http2;
-	server_name ' . $_POST['domain'] . ';
-	root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . ';
-
-	ssl_certificate /etc/letsencrypt/live/' . $_POST['domain'] . '/fullchain.pem;
-	ssl_certificate_key /etc/letsencrypt/live/' . $_POST['domain'] . '/privkey.pem;
-
-	include inc/ht-tls.conf;
-}
-';
-	if (file_put_contents(CONF['ht']['nginx_config_path'] . '/' . $_POST['domain'] . '.conf', $nginxConf) === false)
-		output(500, 'Failed to write Nginx configuration.');
-
-	// Reload Nginx
-	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['nginx_reload_cmd'], result_code: $code);
-	if ($code !== 0)
-		output(500, 'Failed to reload Nginx.');
-
-	output(200, 'Accès HTTP par domaine ajouté sur ce dossier !');
-}
-
-$dirsStatuses = dirsStatuses('onion', 'http');
-
-$proof = getAuthToken();
-
-?>
-
-<p>
-	Ajouter sur un dossier de site un accès <?= linkToDocs('http', 'HTTP') ?> par <?= linkToDocs('dns', 'DNS') ?> et <?= linkToDocs('tls', 'TLS') ?> <?= linkToDocs('ca', 'authentifié par <em>Let\'s Encrypt</em>') ?>.
-</p>
-
-<p>
-	La présence des enregistrements ci-après sera vérifiée lors du traitement de ce formulaire.
-</p>
-
-<dl>
-	<dt><code>AAAA</code></dt>
-	<dd>
-		<code><?= CONF['ht']['ipv6_address'] ?></code>
-	</dd>
-	<dt><code>A</code></dt>
-	<dd>
-		<code><?= CONF['ht']['ipv4_address'] ?></code>
-	</dd>
-	<dt><code>TXT</code></dt>
-	<dd>
-		<code><?= SERVER_NAME ?>_domain-verification=<?= $proof ?></code>
-	</dd>
-</dl>
-
-<form method="post">
-	<label for="domain">Domaine sur lequel répondre</label><br>
-	<input required="" placeholder="site.<?= PLACEHOLDER_DOMAIN ?>" id="domain" name="domain" type="text"><br>
-	<label for="dir">Dossier ciblé</label><br>
-	<select required="" name="dir" id="dir">
-		<option value="" disabled="" selected="">---</option>
-<?php
-foreach ($dirsStatuses as $dir => $alreadyEnabled)
-	echo '		<option' . ($alreadyEnabled ? ' disabled=""' : '') . ' value="' . $dir . '">' . $dir . '</option>' . LF;
-?>
-	</select>
-	<br>
-	<input value="Valider" type="submit">
-</form>

+ 0 - 70
pages/ht/add-http-onion.php

@@ -1,70 +0,0 @@
-<?php
-
-if (processForm()) {
-	if (dirsStatuses('onion', 'http')[$_POST['dir']] !== false)
-		output(403, 'Wrong value for <code>dir</code>.');
-
-	rateLimit();
-
-	// Add Tor config
-	$torConf = 'HiddenServiceDir ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/
-	HiddenServicePort 80 [::1]:' . CONF['ht']['internal_onion_http_port'] . '
-	';
-	if (file_put_contents(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'], $torConf) === false)
-		output(500, 'Failed to write new Tor configuration.');
-
-	// Reload Tor
-	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['tor_reload_cmd'], $output, $code);
-	if ($code !== 0)
-		output(500, 'Failed to reload Tor.');
-
-	// Get the address generated by Tor
-	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['cat_path'] . ' ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname', $output);
-	$onion = $output[0];
-	if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1)
-		output(500, 'No onion address found.');
-
-	// Store it in the database
-	addSite($_SESSION['id'], $_POST['dir'], $onion, 'onion', 'http');
-
-	// Add Nginx config
-	$nginxConf = 'server {
-		listen [::1]:' . CONF['ht']['internal_onion_http_port'] . ';
-		server_name ' . $onion . ';
-		root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . ';
-
-		include inc/ht-onion.conf;
-	}
-	';
-	if (file_put_contents(CONF['ht']['nginx_config_path'] . '/' . $onion . '.conf', $nginxConf) === false)
-		output(500, 'Failed to write Nginx configuration.');
-
-	// Reload Nginx
-	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['nginx_reload_cmd'], result_code: $code);
-	if ($code !== 0)
-		output(500, 'Failed to reload Nginx.');
-
-	// Tell the user their site address
-	output(200, 'L\'adresse de votre service Onion HTTP est : <a href="http://' . $onion . '/"><code>http://' . $onion . '/</code></a>');
-}
-
-$dirsStatuses = dirsStatuses('onion', 'http');
-
-?>
-
-<p>
-	Ajouter un accès en .onion sur un dossier
-</p>
-
-<form method="post">
-	<label for="dir">Dossier ciblé</label><br>
-	<select required="" name="dir" id="dir">
-		<option value="" disabled="" selected="">---</option>
-<?php
-foreach ($dirsStatuses as $dir => $alreadyEnabled)
-	echo '		<option' . ($alreadyEnabled ? ' disabled=""' : '') . ' value="' . $dir . '">' . $dir . '</option>' . LF;
-?>
-	</select>
-	<br>
-	<input value="Valider" type="submit">
-</form>

+ 0 - 96
pages/ns/print.php

@@ -1,96 +0,0 @@
-
-<form method="post">
-	<input type="radio" name="print" id="table" value="table" checked="">
-	<label for="table">Tableau de mes enregistrements</label>
-	<br>
-	<input type="radio" name="print" id="ds" value="ds">
-	<label for="ds">Enregistrement DS</label>
-	<br>
-	<input type="radio" name="print" id="raw" value="raw">
-	<label for="raw">Fichier de zone brut</label>
-	<br>
-	<label for="zone">Zone</label>
-	<select required="" name="zone" id="zone">
-		<option value="" disabled="" selected="">-</option>
-<?php
-if (isset($_SESSION['id']))
-	foreach (nsListUserZones($_SESSION['id']) as $zone)
-		echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
-?>
-	</select>
-	<br>
-	<input value="Afficher" type="submit">
-</form>
-
-<?php
-
-if (processForm()) {
-	nsCheckZonePossession($_POST['zone']);
-
-	$zoneContent = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
-	if ($zoneContent === false)
-		output(500, 'Unable to read zone file.');
-
-	if ($_POST['print'] === 'raw') {
-		echo '<pre>' . htmlspecialchars($zoneContent) . '</pre>';
-		output(200);
-	}
-
-	if ($_POST['print'] === 'table') { ?>
-
-<table>
-	<tr>
-		<th>Domaine</th>
-		<th>TTL</th>
-		<th>Type</th>
-		<th>Contenu</th>
-	</tr>
-<?php
-		foreach(explode(LF, $zoneContent) as $zoneLine) {
-			if (str_starts_with($zoneLine, ';')) continue; // Ignore comments
-			if (empty($zoneLine)) continue;
-			$elements = preg_split('/[\t ]+/', $zoneLine, 4);
-			if (!in_array($elements[2], ALLOWED_TYPES, true)) continue; // Ignore records generated by Knot
-			echo '	<tr>';
-			foreach ($elements as $element)
-				echo '		<td><code>' . htmlspecialchars($element) . '</code></td>';
-			echo '	</tr>';
-		}
-		echo '</table>';
-	}
-
-	if ($_POST['print'] === 'ds') {
-
-		$found = preg_match('/^' . preg_quote($_POST['zone'], '/') . '[\t ]+0[\t ]+CDS[\t ]+(?<tag>[0-9]{1,5})[\t ]+(?<algo>[0-9]{1,2})[\t ]+(?<digest_type>[0-9])[\t ]+(?<digest>[0-9A-F]{64})$/Dm', $zoneContent, $matches);
-		if ($found !== 1)
-			output(500, 'Unable to get public key record from zone file.');
-
-?>
-
-<dl>
-	<dt>Zone</dt>
-	<dd>
-		<code><?= $_POST['zone'] ?></code>
-	</dd>
-	<dt>Tag</dt>
-	<dd>
-		<code><?= $matches['tag'] ?></code>
-	</dd>
-	<dt>Algorithme</dt>
-	<dd>
-		<code><?= $matches['algo'] ?></code><?php if ($matches['algo'] === '15') echo ' (Ed25519)'; ?>
-	</dd>
-	<dt>Type de condensat</dt>
-	<dd>
-		<code><?= $matches['digest_type'] ?></code><?php if ($matches['digest_type'] === '2') echo ' (SHA-256)'; ?>
-	</dd>
-	<dt>Condensat</dt>
-	<dd>
-		<code><?= $matches['digest'] ?></code>
-	</dd>
-</dl>
-
-<?php
-		output(200);
-	}
-}

+ 0 - 78
pages/ns/zone-add.php

@@ -1,78 +0,0 @@
-<?php
-
-if (processForm()) {
-	$_POST['domain'] = formatAbsoluteDomain($_POST['domain']);
-
-	if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== [])
-		output(403, 'Cette zone existe déjà sur ce service.');
-
-	exec(CONF['dns']['kdig_path'] . ' ' . ltrim(strstr($_POST['domain'], '.'), '.') . ' NS +short', $parentAuthoritatives);
-	if ($parentAuthoritatives === [])
-		output(403, 'Serveurs de noms de la zone parente introuvables');
-	foreach ($parentAuthoritatives as $parentAuthoritative)
-		checkAbsoluteDomainFormat($parentAuthoritative);
-
-	exec(CONF['dns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . $parentAuthoritatives[0] . ' +noidn', $results);
-	if (preg_match('/^' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?<salt>[0-9a-f]{8})-(?<hash>[0-9a-f]{32})\._domain-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1)
-		output(403, 'Enregistrement d\'authentification introuvable');
-
-	checkAuthToken($matches['salt'], $matches['hash']);
-
-	rateLimit();
-
-	insert('zones', [
-		'zone' => $_POST['domain'],
-		'username' => $_SESSION['id'],
-	]);
-
-	$knotZonePath = CONF['ns']['knot_zones_path'] . '/' . $_POST['domain'] . 'zone';
-	$knotZone = implode(' ', [
-		$_POST['domain'],
-		SOA_VALUES['ttl'],
-		'SOA',
-		CONF['ns']['servers'][0],
-		SOA_VALUES['email'],
-		1,
-		SOA_VALUES['refresh'],
-		SOA_VALUES['retry'],
-		SOA_VALUES['expire'],
-		SOA_VALUES['negative'],
-	]) . LF;
-	foreach (CONF['ns']['servers'] as $server)
-		$knotZone .= $_POST['domain'] . ' 86400 NS ' . $server . LF;
-	if (is_int(file_put_contents($knotZonePath, $knotZone)) !== true)
-		output(500, 'Failed to write new zone file.');
-	if (chmod($knotZonePath, 0660) !== true)
-		output(500, 'Failed to chmod new zone file.');
-
-	knotcConfExec([
-		"set 'zone[" . $_POST['domain'] . "]'",
-		"set 'zone[" . $_POST['domain'] . "].template' 'niver'",
-	]);
-
-	output(200, 'La zone a été créée.');
-}
-
-$proof = getAuthToken();
-
-?>
-
-<p>
-	Pour prouver que vous possédez bien ce domaine, il doit posséder un <?= linkToDocs('ns-record', 'enregistrement NS') ?> égal à <code><?= $proof ?>._domain-verification.<?= SERVER_NAME ?>.</code> lors du traitement de ce formulaire.
-</p>
-
-<p>
-	La zone sera servie par ces serveurs de noms :
-	<ul>
-<?php
-foreach (CONF['ns']['servers'] as $server)
-	echo '	<li><code>' . $server . '</code></li>';
-?>
-	</ul>
-</p>
-
-<form method="post">
-	<label for="domain">Domaine</label><br>
-	<input required="" placeholder="domain.<?= PLACEHOLDER_DOMAIN ?>." id="domain" name="domain" type="text"><br>
-	<input value="Ajouter" type="submit">
-</form>

+ 0 - 25
pages/ns/zone-del.php

@@ -1,25 +0,0 @@
-<?php
-
-if (processForm()) {
-	nsCheckZonePossession($_POST['zone']);
-
-	nsDeleteZone($_POST['zone']);
-
-	output(200, 'La zone a été supprimée.');
-}
-
-?>
-
-<form method="post">
-	<label for="zone">Zone</label>
-	<select required="" name="zone" id="zone">
-		<option value="" disabled="" selected="">-</option>
-<?php
-if (isset($_SESSION['id']))
-	foreach (nsListUserZones($_SESSION['id']) as $zone)
-		echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
-?>
-	</select>
-	<br>
-	<input value="Supprimer toutes les données liées à cette zone" type="submit">
-</form>

+ 0 - 49
pages/reg/print.php

@@ -1,49 +0,0 @@
-<form method="post">
-	<label for="domain">Domaine</label>
-	<select required="" name="domain" id="domain">
-		<option value="" disabled="" selected="">-</option>
-<?php
-if (isset($_SESSION['id']))
-	foreach (regListUserDomains($_SESSION['id']) as $domain)
-		echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
-?>
-	</select>
-	<br>
-	<input value="Afficher" type="submit">
-</form>
-
-<?php
-
-if (processForm()) {
-	regCheckDomainPossession($_POST['domain']);
-
-	$zoneContent = file_get_contents(CONF['reg']['registry_file']);
-	if ($zoneContent === false)
-		output(500, 'Unable to read registry file.');
-
-?>
-<table>
-	<tr>
-		<th>Domaine</th>
-		<th>TTL</th>
-		<th>Type</th>
-		<th>Contenu</th>
-	</tr>
-<?php
-
-	foreach(explode(LF, $zoneContent) as $zoneLine) {
-		if (str_starts_with($zoneLine, ';')) continue; // Ignore comments
-		if (empty($zoneLine)) continue;
-		$elements = preg_split('/[\t ]+/', $zoneLine, 4);
-		if (!str_ends_with($elements[0], $_POST['domain'])) continue; // Ignore records for other domains
-		if (!in_array($elements[2], ['A', 'AAAA', 'NS', 'DS'], true)) continue; // Ignore records generated by Knot
-		echo '	<tr>' . LF;
-		foreach ($elements as $element)
-			echo '		<td><code>' . htmlspecialchars($element) . '</code></td>' . LF;
-		echo '	</tr>' . LF;
-	}
-
-	echo '</table>';
-
-	output(200);
-}

+ 0 - 38
pages/reg/register.php

@@ -1,38 +0,0 @@
-<?php
-
-if (processForm()) {
-	if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['subdomain']) !== 1)
-		output(403, 'Le nom de domaine doit être composé uniquement d\'entre 4 et 63 lettres minuscules ou chiffre (a-z et 0-9)');
-
-	$domain = formatAbsoluteDomain($_POST['subdomain'] . '.' . CONF['reg']['registry']);
-
-	if (query('select', 'registry', ['domain' => $domain], 'domain') !== [])
-		output(403, 'Ce domaine n\'est pas disponible à l\'enregistrement. Il est déjà enregistré.');
-
-	if (in_array($_POST['subdomain'], explode(LF, 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['id'],
-		'last_renewal' => date('Y-m-d H:i:s'),
-	]);
-
-	output(200, 'Domaine ajouté au registre.');
-}
-
-?>
-
-<p>
-	Enregistrer un nouveau domaine sur son compte. Ce domaine doit être composé uniquement d'au moins 4 lettres latines non accentuées (a-z).
-</p>
-
-<form method="post">
-	<label for="subdomain">Sous-domaine</label>
-	<br>
-	<code><input id="subdomain" pattern="<?= SUBDOMAIN_REGEX ?>" required="" placeholder="niver" name="subdomain" type="text">.<?= CONF['reg']['registry'] ?></code>
-	<br>
-	<input value="Enregistrer" type="submit">
-</form>

+ 0 - 44
pages/reg/transfer.php

@@ -1,44 +0,0 @@
-<?php
-
-if (processForm()) {
-	if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['domain']) !== 1)
-		output(403, 'Le nom de domaine semble incorrect');
-
-	$domain = $_POST['domain'] . '.' . CONF['reg']['registry'];
-
-	if (query('select', 'registry', ['username' => $_SESSION['id'], 'domain' => $domain], 'domain') !== [])
-		output(403, 'Le compte présent possède déjà ce domaine.');
-
-	exec(CONF['dns']['kdig_path'] . ' ' . $domain . ' NS @' . CONF['reg']['address'] . ' +noidn', $results);
-	if (preg_match('/^' . preg_quote($domain, '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?<salt>[0-9a-f]{8})-(?<hash>[0-9a-f]{32})\._transfer-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1)
-		output(403, 'Enregistrement d\'authentification introuvable');
-
-	checkAuthToken($matches['salt'], $matches['hash']);
-
-	DB->prepare('UPDATE registry SET username = :username WHERE domain = :domain')
-	->execute([':username' => $_SESSION['id'], ':domain' => $domain]);
-
-	knotcZoneExec(CONF['reg']['registry'], [
-		$domain,
-		'NS',
-		$matches['salt'] . '-' . $matches['hash'] . '._transfer-verification.' . SERVER_NAME . '.'
-	], 'delete');
-
-	output(200, 'Le domaine a été transféré vers le compte présent, l\'enregistrement d\'authentification a été automatiquement retiré.');
-}
-
-$proof = getAuthToken();
-
-?>
-
-<p>
-	Pour prouver que vous êtes autorisé à recevoir le domaine par san possessaire actuele, ledit domaine doit posséder un <?= linkToDocs('ns-record', 'enregistrement NS') ?> égal à <code><?= $proof ?>._transfer-verification.<?= SERVER_NAME ?>.</code> lors du traitement de ce formulaire. Cet enregistrement sera automatiquement retiré une fois validé.
-</p>
-
-<form method="post">
-	<label for="subdomain">Sous-domaine à recevoir</label>
-	<br>
-	<code><input required="" placeholder="subdomain" id="subdomain" name="subdomain" type="text">.<?= CONF['reg']['registry'] ?></code>
-	<br>
-	<input value="Recevoir ce domaine" type="submit">
-</form>

+ 18 - 0
pg-act/auth/approval.php

@@ -0,0 +1,18 @@
+<?php
+
+if ($_SESSION['type'] !== 'testing')
+	output(403, 'Approbation impossible : votre compte est déjà approuvé.');
+
+if (isset(query('select', 'approval-keys', ['key' => $_POST['key']], 'key')[0]) !== true)
+	output(403, 'Approbation impossible : cette clé d\'approbation n\'est pas disponible. Elle a été mal saisie, a expiré ou a déjà été utilisée pour un autre compte.');
+
+query('delete', 'approval-keys', ['key' => $_POST['key']]);
+
+DB->prepare('UPDATE users SET type = "approved" WHERE id = :id')
+->execute([':id' => $_SESSION['id']]);
+
+$_SESSION['type'] = 'approved';
+
+insert('approval-keys', ['key' => bin2hex(random_bytes(16))]);
+
+output(200, 'Compte approuvé.');

+ 28 - 0
pg-act/auth/login.php

@@ -0,0 +1,28 @@
+<?php
+
+checkPasswordFormat($_POST['password']);
+
+checkUsernameFormat($_POST['username']);
+
+$username = hashUsername($_POST['username']);
+
+if (usernameExists($username) !== true)
+	output(403, 'Connexion impossible : ce compte n\'existe pas.');
+
+$id = query('select', 'users', ['username' => $username], 'id')[0];
+
+if (checkPassword($id, $_POST['password']) !== true)
+	output(403, 'Connexion impossible : clé de passe invalide.');
+
+if (outdatedPasswordHash($id))
+	changePassword($id, $_POST['password']);
+
+stopSession();
+startSession();
+
+$_SESSION['id'] = $id;
+$_SESSION['display-username'] = htmlspecialchars($_POST['username']);
+$_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
+
+redir();
+

+ 0 - 0
pages/auth/logout.php → pg-act/auth/logout.php


+ 10 - 0
pg-act/auth/password.php

@@ -0,0 +1,10 @@
+<?php
+
+checkPasswordFormat($_POST['new-password']);
+
+if (checkPassword($_SESSION['id'], $_POST['current-password']) !== true)
+	output(403, 'Changement impossible : clé de passe invalide.');
+
+changePassword($_SESSION['id'], $_POST['new-password']);
+
+output(200, 'Clé de passe changée.');

+ 50 - 0
pg-act/auth/register.php

@@ -0,0 +1,50 @@
+<?php
+
+checkPasswordFormat($_POST['password']);
+
+checkUsernameFormat($_POST['username']);
+
+$username = hashUsername($_POST['username']);
+
+if (usernameExists($username) !== false)
+	output(403, 'Ce nom de compte est déjà utilisé.');
+
+rateLimit();
+
+$id = hash('sha256', random_bytes(32));
+
+insert('users', [
+	'id' => $id,
+	'username' => $username,
+	'password' => hashPassword($_POST['password']),
+	'registration_date' => date('Y-m-d H:i:s'),
+	'bucket_tokens' => 0,
+	'bucket_last_update' => 0,
+	'type' => 'testing',
+]);
+
+// Setup SFTP directory
+umask(0002);
+if (mkdir(CONF['ht']['ht_path'] . '/' . $id, 0775) !== true)
+	output(500, 'Can\'t create user directory.');
+exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/' . $id . ' --no-dereference', result_code: $code);
+if ($code !== 0)
+	output(500, 'Can\'t change user directory group.');
+
+// Setup Tor config directory
+if (mkdir(CONF['ht']['tor_config_path'] . '/' . $id, 0755) !== true)
+	output(500, 'Can\'t create Tor config directory.');
+
+// Setup Tor keys directory
+exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $id, result_code: $code);
+if ($code !== 0)
+	output(500, 'Can\'t create Tor keys directory.');
+
+stopSession();
+startSession();
+
+$_SESSION['id'] = $id;
+$_SESSION['display-username'] = htmlspecialchars($_POST['username']);
+$_SESSION['type'] = 'testing';
+
+redir();

+ 40 - 0
pg-act/auth/unregister.php

@@ -0,0 +1,40 @@
+<?php
+
+if (!isset($_POST['delete']))
+	output(403, 'Il faut confirmer la suppression du compte');
+
+foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain)
+	regDeleteDomain($domain);
+
+foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone)
+	nsDeleteZone($zone);
+
+foreach (query('select', 'sites', [
+	'username' => $_SESSION['id'],
+	'domain_type' => 'onion',
+	'protocol' => 'http',
+], 'site_dir') as $dir)
+	htDeleteSite($dir, domainType: 'onion', protocol: 'http');
+
+foreach (query('select', 'sites', [
+	'username' => $_SESSION['id'],
+	'domain_type' => 'dns',
+	'protocol' => 'http',
+], 'site_dir') as $dir)
+	htDeleteSite($dir, domainType: 'dns', protocol: 'http');
+
+exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'], result_code: $code);
+if ($code !== 0)
+	output(500, 'Can\'t remove Tor keys directory.');
+
+removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id']);
+
+exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] .  ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'], result_code: $code);
+if ($code !== 0)
+	output(500, 'Can\'t remove user\'s directory.');
+
+query('delete', 'users', ['id' => $_SESSION['id']]);
+
+logout();
+
+output(200, 'Compte supprimé.');

+ 15 - 0
pg-act/auth/username.php

@@ -0,0 +1,15 @@
+<?php
+
+checkUsernameFormat($_POST['new-username']);
+
+$username = hashUsername($_POST['new-username']);
+
+if (usernameExists($username) !== false)
+	output(403, 'Ce nom de compte est déjà utilisé.');
+
+DB->prepare('UPDATE users SET username = :username WHERE id = :id')
+->execute([':username' => $username, ':id' => $_SESSION['id']]);
+
+$_SESSION['display-username'] = htmlspecialchars($_POST['new-username']);
+
+output(200, 'Identifiant changé.');

+ 59 - 0
pg-act/ht/add-http-dns.php

@@ -0,0 +1,59 @@
+<?php
+
+$_POST['domain'] = formatDomain($_POST['domain']);
+
+if (dirsStatuses('dns', 'http')[$_POST['dir']] !== false)
+	output(403, 'Wrong value for <code>dir</code>.');
+
+if (query('select', 'sites', ['domain' => $_POST['domain']], 'domain') !== [])
+	output(403, 'Ce domaine existe déjà sur ce service.');
+
+$remoteAaaaRecords = dns_get_record($_POST['domain'], DNS_AAAA);
+if (is_array($remoteAaaaRecords) !== true)
+	output(500, 'Erreur lors de la récupération de l\'enregistrement AAAA.');
+if (equalArrays([CONF['ht']['ipv6_address']], array_column($remoteAaaaRecords, 'ipv6')) !== true)
+	output(403, 'Ce domaine doit avoir pour unique enregistrement AAAA <code>' . CONF['ht']['ipv6_address'] . '</code>.');
+
+$remoteARecords = dns_get_record($_POST['domain'], DNS_A);
+if (is_array($remoteARecords) !== true)
+	output(500, 'Erreur lors de la récupération de l\'enregistrement A.');
+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>.');
+
+$remoteTXTRecords = dns_get_record($_POST['domain'], DNS_TXT);
+if (is_array($remoteTXTRecords) !== true)
+	output(500, 'Erreur lors de la récupération de l\'enregistrement TXT.');
+if (preg_match('/^' . preg_quote(SERVER_NAME, '/') . '_domain-verification=([0-9a-f]{8})-([0-9a-f]{32})$/Dm', implode(LF, array_column($remoteTXTRecords, 'txt')), $matches) !== 1)
+	output(403, 'Aucun enregistrement TXT au format correct trouvé.');
+
+checkAuthToken($matches[1], $matches[2]);
+
+rateLimit();
+
+addSite($_SESSION['id'], $_POST['dir'], $_POST['domain'], 'dns', 'http');
+
+exec('2>&1 ' . CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' certonly' . (($_SESSION['type'] === 'approved') ? '' : ' --test-cert') . ' --key-type rsa --rsa-key-size 3072 --webroot --webroot-path /srv/niver/acme --domain ' . $_POST['domain'], $output, $returnCode);
+if ($returnCode !== 0)
+	output(500, 'Certbot failed to get a Let\'s Encrypt certificate.', $output);
+
+$nginxConf = 'server {
+	listen [' . CONF['ht']['ipv6_listen_address'] . ']:' . CONF['ht']['https_port'] . ' ssl http2;
+	listen ' . CONF['ht']['ipv4_listen_address'] . ':' . CONF['ht']['https_port'] . ' ssl http2;
+	server_name ' . $_POST['domain'] . ';
+	root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . ';
+
+	ssl_certificate /etc/letsencrypt/live/' . $_POST['domain'] . '/fullchain.pem;
+	ssl_certificate_key /etc/letsencrypt/live/' . $_POST['domain'] . '/privkey.pem;
+
+	include inc/ht-tls.conf;
+}
+';
+if (file_put_contents(CONF['ht']['nginx_config_path'] . '/' . $_POST['domain'] . '.conf', $nginxConf) === false)
+	output(500, 'Failed to write Nginx configuration.');
+
+// Reload Nginx
+exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['nginx_reload_cmd'], result_code: $code);
+if ($code !== 0)
+	output(500, 'Failed to reload Nginx.');
+
+output(200, 'Accès HTTP par domaine ajouté sur ce dossier !');

+ 47 - 0
pg-act/ht/add-http-onion.php

@@ -0,0 +1,47 @@
+<?php
+
+if (dirsStatuses('onion', 'http')[$_POST['dir']] !== false)
+	output(403, 'Wrong value for <code>dir</code>.');
+
+rateLimit();
+
+// Add Tor config
+$torConf = 'HiddenServiceDir ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/
+HiddenServicePort 80 [::1]:' . CONF['ht']['internal_onion_http_port'] . '
+';
+if (file_put_contents(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'], $torConf) === false)
+	output(500, 'Failed to write new Tor configuration.');
+
+// Reload Tor
+exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['tor_reload_cmd'], $output, $code);
+if ($code !== 0)
+	output(500, 'Failed to reload Tor.');
+
+// Get the address generated by Tor
+exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['cat_path'] . ' ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname', $output);
+$onion = $output[0];
+if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1)
+	output(500, 'No onion address found.');
+
+// Store it in the database
+addSite($_SESSION['id'], $_POST['dir'], $onion, 'onion', 'http');
+
+// Add Nginx config
+$nginxConf = 'server {
+	listen [::1]:' . CONF['ht']['internal_onion_http_port'] . ';
+	server_name ' . $onion . ';
+	root ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . ';
+
+	include inc/ht-onion.conf;
+}
+';
+if (file_put_contents(CONF['ht']['nginx_config_path'] . '/' . $onion . '.conf', $nginxConf) === false)
+	output(500, 'Failed to write Nginx configuration.');
+
+// Reload Nginx
+exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['nginx_reload_cmd'], result_code: $code);
+if ($code !== 0)
+	output(500, 'Failed to reload Nginx.');
+
+// Tell the user their site address
+output(200, 'L\'adresse de votre service Onion HTTP est : <a href="http://' . $onion . '/"><code>http://' . $onion . '/</code></a>');

+ 8 - 0
pg-act/ht/del-http-dns.php

@@ -0,0 +1,8 @@
+<?php
+
+if (dirsStatuses('dns', 'http')[$_POST['dir']] !== true)
+	output(403, 'Wrong value for <code>dir</code>.');
+
+htDeleteSite($_POST['dir'], domainType: 'dns', protocol: 'http');
+
+output(200, 'Accès retiré.');

+ 8 - 0
pg-act/ht/del-http-onion.php

@@ -0,0 +1,8 @@
+<?php
+
+if (dirsStatuses('onion', 'http')[$_POST['dir']] !== true)
+	output(403, 'Wrong value for <code>dir</code>.');
+
+htDeleteSite($_POST['dir'], domainType: 'onion', protocol: 'http');
+
+output(200, 'Accès retiré.');

+ 23 - 0
pg-act/ns/caa.php

@@ -0,0 +1,23 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (!($_POST['flag'] >= 0 AND $_POST['flag'] <= 255))
+	output(403, 'Wrong value for <code>flag</code>.');
+
+if (!(preg_match('/^[a-z]{1,127}$/D', $_POST['tag'])))
+	output(403, 'Wrong value for <code>tag</code>.');
+
+if (!(preg_match('/^[a-z0-9.-]{1,255}$/D', $_POST['value'])))
+	output(403, 'Wrong value for <code>value</code>.');
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'CAA',
+	$_POST['flag'],
+	$_POST['tag'],
+	$_POST['value']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 14 - 0
pg-act/ns/cname.php

@@ -0,0 +1,14 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+$_POST['cname'] = formatAbsoluteDomain($_POST['cname']);
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'CNAME',
+	$_POST['cname']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 14 - 0
pg-act/ns/dname.php

@@ -0,0 +1,14 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+$_POST['dname'] = formatAbsoluteDomain($_POST['dname']);
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'DNAME',
+	$_POST['dname']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 15 - 77
pages/ns/edit.php → pg-act/ns/edit.php

@@ -1,7 +1,8 @@
 <?php
 
-if (processForm() AND isset($_POST['zone-content'])) { // Update zone
-	nsCheckZonePossession($_POST['zone']);
+nsCheckZonePossession($_POST['zone']);
+
+if (isset($_POST['zone-content'])) { // Update zone
 
 	// Get current SOA record
 	$current_zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
@@ -58,85 +59,22 @@ if (processForm() AND isset($_POST['zone-content'])) { // Update zone
 		output(500, 'Failed to thaw zone file.', $output);
 
 	usleep(1000000);
-
-	output(200, 'La zone a été mise à jour.');
 }
 
-?>
+// Display zone
 
-<form method="post">
-	<label for="zone">Zone à modifier</label>
-	<br>
-	<select required="" name="zone" id="zone">
-		<option value="" disabled="" selected="">-</option>
-<?php
-if (isset($_SESSION['id']))
-	foreach (nsListUserZones($_SESSION['id']) as $zone)
-		echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
-?>
-	</select>
-	<br>
-	<input type="submit" value="Afficher">
-</form>
+$zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
+if ($zone_content === false)
+	output(500, 'Unable to read zone file.');
 
-<?php
-
-if (processForm()) { // Display zone
-	nsCheckZonePossession($_POST['zone']);
-
-	$zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
-	if ($zone_content === false)
-		output(500, 'Unable to read zone file.');
-
-	$displayed_zone_content = '';
-	foreach(explode(LF, $zone_content) as $zone_line) {
-		if (empty($zone_line) OR str_starts_with($zone_line, ';'))
+$data['zone_content'] = '';
+foreach(explode(LF, $zone_content) as $zone_line) {
+	if (empty($zone_line) OR str_starts_with($zone_line, ';'))
+		continue;
+	if (preg_match('/^(?:(?:[a-z0-9_-]{1,63}\.){1,127})?' . preg_quote($_POST['zone'], '/') . '[\t ]+[0-9]{1,8}[\t ]+(?<type>[A-Z]{1,16})[\t ]+.+$/D', $zone_line, $matches)) {
+		if (in_array($matches['type'], ALLOWED_TYPES, true) !== true)
 			continue;
-		if (preg_match('/^(?:(?:[a-z0-9_-]{1,63}\.){1,127})?' . preg_quote($_POST['zone'], '/') . '[\t ]+[0-9]{1,8}[\t ]+(?<type>[A-Z]{1,16})[\t ]+.+$/D', $zone_line, $matches)) {
-			if (in_array($matches['type'], ALLOWED_TYPES, true) !== true)
-				continue;
-			$displayed_zone_content .= $zone_line . LF;
-		}
+		$data['zone_content'] .= $zone_line . LF;
 	}
-	$displayed_zone_content .= LF;
-
-?>
-<form method="post">
-	<input type="hidden" name="zone" value="<?= $_POST['zone'] ?>">
-
-	<label for="zone-content">Nouveau contenu de la zone <code><strong><?= $_POST['zone'] ?></strong></code></label>
-	<textarea id="zone-content" name="zone-content" wrap="off" rows="<?= substr_count($displayed_zone_content, LF) + 1 ?>"><?= htmlspecialchars($displayed_zone_content) ?></textarea>
-	<br>
-	<input type="submit" value="Remplacer">
-</form>
-
-<?php
-
 }
-
-global $final_message;
-displayFinalMessage();
-
-?>
-
-<h2>Valeurs par défaut</h2>
-
-<p>Si le TTL est omis, il sera définit à <code><time datetime="PT<?= DEFAULT_TTL ?>S"><?= DEFAULT_TTL ?></time></code> secondes.</p>
-
-<p>La précision de la classe (<code>IN</code>) est facultative.</p>
-
-<h2>Valeurs autorisées</h2>
-
-<p>La zone n'est pas autorisée à dépasser <?= ZONE_MAX_CHARACTERS ?> caractères.</p>
-
-<p>Les TTLs ne sont autorisés qu'entre <code><time datetime="PT<?= MIN_TTL ?>S"><?= MIN_TTL ?></time></code> et <code><time datetime="PT<?= MAX_TTL ?>S"><?= MAX_TTL ?></time></code> secondes.</p>
-
-<p>Les seuls types dont l'édition est autorisée sont :</p>
-
-<ul>
-<?php
-	foreach (ALLOWED_TYPES as $allowed_type)
-		echo '	<li><code>' . $allowed_type . '</code></li>';
-
-?>
-</ul>
+$data['zone_content'] .= LF;

+ 14 - 0
pg-act/ns/ip.php

@@ -0,0 +1,14 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+$record = checkIpFormat($_POST['ip']);
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	$record,
+	$_POST['ip']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 70 - 0
pg-act/ns/loc.php

@@ -0,0 +1,70 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (empty($_POST['lat-min']))
+	$_POST['lat-min'] = 0;
+if (empty($_POST['lat-sec']))
+	$_POST['lat-sec'] = 0;
+if (empty($_POST['lon-min']))
+	$_POST['lon-min'] = 0;
+if (empty($_POST['lon-sec']))
+	$_POST['lon-sec'] = 0;
+if (empty($_POST['size']))
+	$_POST['size'] = 1;
+if (empty($_POST['hp']))
+	$_POST['hp'] = 10000;
+if (empty($_POST['vp']))
+	$_POST['vp'] = 10;
+
+if (!($_POST['lat-deg'] >= 0 AND $_POST['lat-deg'] <= 90))
+	output(403, 'Wrong value for <code>lat-deg</code>.');
+if (!($_POST['lat-min'] >= 0 AND $_POST['lat-min'] <= 59))
+	output(403, 'Wrong value for <code>lat-min</code>.');
+if (!($_POST['lat-sec'] >= 0 AND $_POST['lat-sec'] <= 59.999))
+	output(403, 'Wrong value for <code>lat-sec</code>.');
+
+if ($_POST['lat-dir'] !== 'N' AND $_POST['lat-dir'] !== 'S')
+	output(403, 'Wrong value for <code>lat-dir</code>.');
+
+if (!($_POST['lon-deg'] >= 0 AND $_POST['lon-deg'] <= 180))
+	output(403, 'Wrong value for <code>lon-deg</code>.');
+if (!($_POST['lon-min'] >= 0 AND $_POST['lon-min'] <= 59))
+	output(403, 'Wrong value for <code>lon-min</code>.');
+if (!($_POST['lon-sec'] >= 0 AND $_POST['lon-sec'] <= 59.999))
+	output(403, 'Wrong value for <code>lon-sec</code>.');
+
+if ($_POST['lon-dir'] !== 'E' AND $_POST['lon-dir'] !== 'W')
+	output(403, 'Wrong value for <code>lon-dir</code>.');
+
+if (!($_POST['alt'] >= -100000 AND $_POST['alt'] <= 42849672.95))
+	output(403, 'Wrong value for <code>alt</code>.');
+
+if (!($_POST['size'] >= 0 AND $_POST['size'] <= 90000000))
+	output(403, 'Wrong value for <code>size</code>.');
+
+if (!($_POST['hp'] >= 0 AND $_POST['hp'] <= 90000000))
+	output(403, 'Wrong value for <code>hp</code>.');
+
+if (!($_POST['vp'] >= 0 AND $_POST['vp'] <= 90000000))
+	output(403, 'Wrong value for <code>vp</code>.');
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'LOC',
+	$_POST['lat-deg'],
+	$_POST['lat-min'],
+	$_POST['lat-sec'],
+	$_POST['lat-dir'],
+	$_POST['lon-deg'],
+	$_POST['lon-min'],
+	$_POST['lon-sec'],
+	$_POST['lon-dir'],
+	$_POST['alt'] . 'm',
+	$_POST['size'] . 'm',
+	$_POST['hp'] . 'm',
+	$_POST['vp'] . 'm',
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 18 - 0
pg-act/ns/mx.php

@@ -0,0 +1,18 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (!($_POST['priority'] >= 0 AND $_POST['priority'] <= 255))
+	output(403, 'Wrong value for <code>priority</code>.');
+
+$_POST['host'] = formatAbsoluteDomain($_POST['host']);
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'MX',
+	$_POST['priority'],
+	$_POST['host']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 14 - 0
pg-act/ns/ns.php

@@ -0,0 +1,14 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+$_POST['ns'] = formatAbsoluteDomain($_POST['ns']);
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'NS',
+	$_POST['ns']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 25 - 0
pg-act/ns/print.php

@@ -0,0 +1,25 @@
+<?php
+
+nsCheckZonePossession($_POST['zone']);
+
+$data['zone_name'] = $_POST['zone'];
+
+$zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $data['zone_name'] . 'zone');
+if ($zone_content === false)
+	output(500, 'Unable to read zone file.');
+
+if ($_POST['print'] === 'raw')
+	$data['zone-raw'] = $zone_content;
+
+elseif ($_POST['print'] === 'table')
+	$data['zone-table'] = parseZoneFile($zone_content, ALLOWED_TYPES);
+
+elseif ($_POST['print'] === 'ds') {
+	$found = preg_match('/^' . preg_quote($data['zone_name'], '/') . '[\t ]+0[\t ]+CDS[\t ]+(?<tag>[0-9]{1,5})[\t ]+(?<algo>[0-9]{1,2})[\t ]+(?<digest_type>[0-9])[\t ]+(?<digest>[0-9A-F]{64})$/Dm', $zone_content, $data['zone-ds']);
+	if ($found !== 1)
+		output(500, 'Unable to get public key record from zone file.');
+}
+
+else
+	output(403, 'Wrong <code>print</code> method.');
+

+ 26 - 0
pg-act/ns/srv.php

@@ -0,0 +1,26 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (!($_POST['priority'] >= 0 AND $_POST['priority'] <= 65535))
+	output(403, 'Wrong value for <code>priority</code>.');
+
+if (!($_POST['weight'] >= 0 AND $_POST['weight'] <= 65535))
+	output(403, 'Wrong value for <code>weight</code>.');
+
+if (!($_POST['port'] >= 0 AND $_POST['port'] <= 65535))
+	output(403, 'Wrong value for <code>port</code>.');
+
+$_POST['target'] = formatAbsoluteDomain($_POST['target']);
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'SRV',
+	$_POST['priority'],
+	$_POST['weight'],
+	$_POST['port'],
+	$_POST['target']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 23 - 0
pg-act/ns/sshfp.php

@@ -0,0 +1,23 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (!($_POST['algo'] === '1' OR $_POST['algo'] === '3' OR $_POST['algo'] === '4'))
+	output(403, 'Wrong value for <code>algo</code>.');
+
+if (!($_POST['type'] === '2'))
+	output(403, 'Wrong value for <code>type</code>.');
+
+if (!(preg_match('/^[a-z0-9]{64}$/D', $_POST['fp'])))
+	output(403, 'Wrong value for <code>fp</code>.');
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'SSHFP',
+	$_POST['algo'],
+	$_POST['type'],
+	$_POST['fp']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 27 - 0
pg-act/ns/tlsa.php

@@ -0,0 +1,27 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (!($_POST['use'] >= 0 AND $_POST['use'] <= 3))
+	output(403, 'Wrong value for <code>use</code>.');
+
+if (!($_POST['selector'] === '0' OR $_POST['selector'] === '1'))
+	output(403, 'Wrong value for <code>selector</code>.');
+
+if (!($_POST['type'] >= 0 AND $_POST['type'] <= 2))
+	output(403, 'Wrong value for <code>type</code>.');
+
+if (!(preg_match('/^[a-zA-Z0-9.-]{1,1024}$/D', $_POST['content'])))
+	output(403, 'Wrong value for <code>content</code>.');
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'TLSA',
+	$_POST['use'],
+	$_POST['selector'],
+	$_POST['type'],
+	$_POST['content']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 15 - 0
pg-act/ns/txt.php

@@ -0,0 +1,15 @@
+<?php
+
+$values = nsParseCommonRequirements();
+
+if (!(preg_match('/^[a-zA-Z0-9 .@=:!%$+\/\()[\]_-]{5,8192}$/D', $_POST['txt'])))
+	output(403, 'Wrong value for <code>txt</code>.');
+
+knotcZoneExec($_POST['zone'], [
+	$values['domain'],
+	$values['ttl'],
+	'TXT',
+	'"' . $_POST['txt'] . '"'
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 52 - 0
pg-act/ns/zone-add.php

@@ -0,0 +1,52 @@
+<?php
+
+$_POST['domain'] = formatAbsoluteDomain($_POST['domain']);
+
+if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== [])
+	output(403, 'Cette zone existe déjà sur ce service.');
+
+exec(CONF['dns']['kdig_path'] . ' ' . ltrim(strstr($_POST['domain'], '.'), '.') . ' NS +short', $parentAuthoritatives);
+if ($parentAuthoritatives === [])
+	output(403, 'Serveurs de noms de la zone parente introuvables');
+foreach ($parentAuthoritatives as $parentAuthoritative)
+	checkAbsoluteDomainFormat($parentAuthoritative);
+
+exec(CONF['dns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . $parentAuthoritatives[0] . ' +noidn', $results);
+if (preg_match('/^' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?<salt>[0-9a-f]{8})-(?<hash>[0-9a-f]{32})\._domain-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1)
+	output(403, 'Enregistrement d\'authentification introuvable');
+
+checkAuthToken($matches['salt'], $matches['hash']);
+
+rateLimit();
+
+insert('zones', [
+	'zone' => $_POST['domain'],
+	'username' => $_SESSION['id'],
+]);
+
+$knotZonePath = CONF['ns']['knot_zones_path'] . '/' . $_POST['domain'] . 'zone';
+$knotZone = implode(' ', [
+	$_POST['domain'],
+	SOA_VALUES['ttl'],
+	'SOA',
+	CONF['ns']['servers'][0],
+	SOA_VALUES['email'],
+	1,
+	SOA_VALUES['refresh'],
+	SOA_VALUES['retry'],
+	SOA_VALUES['expire'],
+	SOA_VALUES['negative'],
+]) . LF;
+foreach (CONF['ns']['servers'] as $server)
+	$knotZone .= $_POST['domain'] . ' 86400 NS ' . $server . LF;
+if (is_int(file_put_contents($knotZonePath, $knotZone)) !== true)
+	output(500, 'Failed to write new zone file.');
+if (chmod($knotZonePath, 0660) !== true)
+	output(500, 'Failed to chmod new zone file.');
+
+knotcConfExec([
+	"set 'zone[" . $_POST['domain'] . "]'",
+	"set 'zone[" . $_POST['domain'] . "].template' 'niver'",
+]);
+
+output(200, 'La zone a été créée.');

+ 7 - 0
pg-act/ns/zone-del.php

@@ -0,0 +1,7 @@
+<?php
+
+nsCheckZonePossession($_POST['zone']);
+
+nsDeleteZone($_POST['zone']);
+
+output(200, 'La zone a été supprimée.');

+ 30 - 0
pg-act/reg/ds.php

@@ -0,0 +1,30 @@
+<?php
+
+if (
+	($_POST['algo'] !== '8')
+	AND ($_POST['algo'] !== '13')
+	AND ($_POST['algo'] !== '14')
+	AND ($_POST['algo'] !== '15')
+	AND ($_POST['algo'] !== '16')
+) output(403, 'Wrong value for <code>algo</code>.');
+
+$_POST['keytag'] = intval($_POST['keytag']);
+if ((!preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535))
+	output(403, 'Wrong value for <code>keytag</code>.');
+
+if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4')
+	output(403, 'Wrong value for <code>dt</code>.');
+
+regCheckDomainPossession($_POST['zone']);
+
+knotcZoneExec(CONF['reg']['registry'], [
+	$_POST['zone'],
+	CONF['reg']['ttl'],
+	'DS',
+	$_POST['keytag'],
+	$_POST['algo'],
+	$_POST['dt'],
+	$_POST['key']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 16 - 0
pg-act/reg/glue.php

@@ -0,0 +1,16 @@
+<?php
+
+regCheckDomainPossession($_POST['suffix']);
+
+$domain = formatAbsoluteDomain(formatEndWithDot($_POST['subdomain']) . $_POST['suffix']);
+
+$record = checkIpFormat($_POST['ip']);
+
+knotcZoneExec(CONF['reg']['registry'], [
+	$domain,
+	CONF['reg']['ttl'],
+	$record,
+	$_POST['ip']
+]);
+
+output(200, 'Glue ajouté/retiré.');

+ 13 - 0
pg-act/reg/ns.php

@@ -0,0 +1,13 @@
+<?php
+
+regCheckDomainPossession($_POST['domain']);
+$_POST['ns'] = formatAbsoluteDomain($_POST['ns']);
+
+knotcZoneExec(CONF['reg']['registry'], [
+	$_POST['domain'],
+	CONF['reg']['ttl'],
+	'NS',
+	$_POST['ns']
+]);
+
+output(200, 'Enregistrement ajouté/retiré.');

+ 11 - 0
pg-act/reg/print.php

@@ -0,0 +1,11 @@
+<?php
+
+regCheckDomainPossession($_POST['domain']);
+
+$zone_content = file_get_contents(CONF['reg']['registry_file']);
+if ($zone_content === false)
+	output(500, 'Unable to read registry file.');
+
+$data['zone-content'] = parseZoneFile($zone_content, ['A', 'AAAA', 'NS', 'DS'], $_POST['domain']);
+
+output(200);

+ 22 - 0
pg-act/reg/register.php

@@ -0,0 +1,22 @@
+<?php
+
+if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['subdomain']) !== 1)
+	output(403, 'Le nom de domaine doit être composé uniquement d\'entre 4 et 63 lettres minuscules ou chiffre (a-z et 0-9)');
+
+$domain = formatAbsoluteDomain($_POST['subdomain'] . '.' . CONF['reg']['registry']);
+
+if (query('select', 'registry', ['domain' => $domain], 'domain') !== [])
+	output(403, 'Ce domaine n\'est pas disponible à l\'enregistrement. Il est déjà enregistré.');
+
+if (in_array($_POST['subdomain'], explode(LF, file_get_contents(CONF['common']['root_path'] . '/pg-act/reg/reserved.txt'))))
+	output(403, 'Ce domaine n\'est pas disponible à l\'enregistrement. Il est réservé.');
+
+rateLimit();
+
+insert('registry', [
+	'domain' => $domain,
+	'username' => $_SESSION['id'],
+	'last_renewal' => date('Y-m-d H:i:s'),
+]);
+
+output(200, 'Domaine ajouté au registre.');

+ 0 - 0
pages/reg/reserved.txt → pg-act/reg/reserved.txt


+ 26 - 0
pg-act/reg/transfer.php

@@ -0,0 +1,26 @@
+<?php
+
+if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['domain']) !== 1)
+	output(403, 'Le nom de domaine semble incorrect');
+
+$domain = $_POST['domain'] . '.' . CONF['reg']['registry'];
+
+if (query('select', 'registry', ['username' => $_SESSION['id'], 'domain' => $domain], 'domain') !== [])
+	output(403, 'Le compte présent possède déjà ce domaine.');
+
+exec(CONF['dns']['kdig_path'] . ' ' . $domain . ' NS @' . CONF['reg']['address'] . ' +noidn', $results);
+if (preg_match('/^' . preg_quote($domain, '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?<salt>[0-9a-f]{8})-(?<hash>[0-9a-f]{32})\._transfer-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1)
+	output(403, 'Enregistrement d\'authentification introuvable');
+
+checkAuthToken($matches['salt'], $matches['hash']);
+
+DB->prepare('UPDATE registry SET username = :username WHERE domain = :domain')
+->execute([':username' => $_SESSION['id'], ':domain' => $domain]);
+
+knotcZoneExec(CONF['reg']['registry'], [
+	$domain,
+	'NS',
+	$matches['salt'] . '-' . $matches['hash'] . '._transfer-verification.' . SERVER_NAME . '.'
+], 'delete');
+
+output(200, 'Le domaine a été transféré vers le compte présent, l\'enregistrement d\'authentification a été automatiquement retiré.');

+ 7 - 0
pg-act/reg/unregister.php

@@ -0,0 +1,7 @@
+<?php
+
+regCheckDomainPossession($_POST['domain']);
+
+regDeleteDomain($_POST['domain']);
+
+output(200, 'Domaine effacé du registre.');

+ 10 - 0
pg-view/auth/approval.php

@@ -0,0 +1,10 @@
+<p>
+	Ce formulaire permet d'utiliser une clé d'approbation pour valider son compte. Une clé d'approbation est distribuée par l'administrataire sur demande.
+</p>
+
+<form method="post">
+	<label for="key">Clé d'approbation</label><br>
+	<input required="" id="key" size="33" name="key" type="text" placeholder="27b81fbd8277b11ed1cf03d476cec503">
+	<br>
+	<input type="submit" value="Utiliser">
+</form>

+ 0 - 0
pages/auth/index.php → pg-view/auth/index.php


+ 13 - 0
pg-view/auth/login.php

@@ -0,0 +1,13 @@
+<p>Pas de compte ? <a href="register">En créer un</a></p>
+
+<form method="post">
+	<label for="username">Identifiant</label><br>
+	<input required="" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" id="username" name="username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>">
+	<br>
+
+	<label for="password">Clé de passe</label><br>
+	<input required="" autocomplete="current-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="password" name="password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>">
+	<br>
+
+	<input type="submit">
+</form>

+ 3 - 0
pg-view/auth/logout.php

@@ -0,0 +1,3 @@
+<?php
+
+logout();

+ 0 - 15
pages/auth/password.php → pg-view/auth/password.php

@@ -1,18 +1,3 @@
-<?php
-
-if (processForm()) {
-	checkPasswordFormat($_POST['new-password']);
-
-	if (checkPassword($_SESSION['id'], $_POST['current-password']) !== true)
-		output(403, 'Changement impossible : clé de passe invalide.');
-
-	changePassword($_SESSION['id'], $_POST['new-password']);
-
-	output(200, 'Clé de passe changée.');
-}
-
-?>
-
 <p>
 	Vous pouvez ici changer la clé de passe permettant d'accéder à votre compte Niver.
 </p>

+ 17 - 0
pg-view/auth/register.php

@@ -0,0 +1,17 @@
+<p>Déjà un compte ? <a href="login">Se connecter</a></p>
+
+<form method="post">
+
+	<label for="username">Identifiant</label>
+	<br>
+	<input id="username" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" required="" name="username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>"><br>
+
+	<details>
+		<summary><label for="password">Clé de passe</label></summary>
+		<p>Une clé de passe sécurisée est trop compliquée à deviner pour une attaque qui testerait automatiquement plein de clés de passe tout en connaissant d'autres informations et secrets sur vous.</p>
+		<p>Minimum 8 caractères si elle contient minuscule, majuscule et chiffre, ou minimum 10 caractères sinon.</p>
+	</details>
+	<input autocomplete="new-password" id="password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" required="" name="password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>">
+	<br>
+	<input type="submit">
+</form>

+ 17 - 0
pg-view/auth/unregister.php

@@ -0,0 +1,17 @@
+<p>
+	Cette action supprimera toutes les données appartenant à ce compte, y compris :
+</p>
+
+<ul>
+	<li>la possession et la réservation des domaines dans le registre</li>
+	<li>les enregistrements DNS des zones hébergées sur le serveur de noms</li>
+	<li>le contenu des sites</li>
+	<li>les paires de clés des services Onion</li>
+</ul>
+
+<form method="post">
+	<input type="checkbox" name="delete" id="delete" required="">
+	<label for="delete">Supprimer mon compte et toutes ses données</label>
+	<br>
+	<input type="submit">
+</form>

+ 10 - 0
pg-view/auth/username.php

@@ -0,0 +1,10 @@
+<p>
+	Vous pouvez ici changer l'identifiant permettant d'accéder à votre compte Niver.
+</p>
+
+<form method="post">
+	<label for="new-username">Nouvel identifiant</label><br>
+	<input required="" autocomplete="new-username" minlength="1" maxlength="1024" pattern="<?= USERNAME_REGEX ?>" id="new-username" name="new-username" type="text" placeholder="<?= PLACEHOLDER_USERNAME ?>"><br>
+
+	<input type="submit">
+</form>

+ 45 - 0
pg-view/ht/add-http-dns.php

@@ -0,0 +1,45 @@
+<?php
+
+$dirsStatuses = dirsStatuses('dns', 'http');
+
+$proof = getAuthToken();
+
+?>
+
+<p>
+	Ajouter sur un dossier de site un accès <?= linkToDocs('http', 'HTTP') ?> par <?= linkToDocs('dns', 'DNS') ?> et <?= linkToDocs('tls', 'TLS') ?> <?= linkToDocs('ca', 'authentifié par <em>Let\'s Encrypt</em>') ?>.
+</p>
+
+<p>
+	La présence des enregistrements ci-après sera vérifiée lors du traitement de ce formulaire.
+</p>
+
+<dl>
+	<dt><code>AAAA</code></dt>
+	<dd>
+		<code><?= CONF['ht']['ipv6_address'] ?></code>
+	</dd>
+	<dt><code>A</code></dt>
+	<dd>
+		<code><?= CONF['ht']['ipv4_address'] ?></code>
+	</dd>
+	<dt><code>TXT</code></dt>
+	<dd>
+		<code><?= SERVER_NAME ?>_domain-verification=<?= $proof ?></code>
+	</dd>
+</dl>
+
+<form method="post">
+	<label for="domain">Domaine sur lequel répondre</label><br>
+	<input required="" placeholder="site.<?= PLACEHOLDER_DOMAIN ?>" id="domain" name="domain" type="text"><br>
+	<label for="dir">Dossier ciblé</label><br>
+	<select required="" name="dir" id="dir">
+		<option value="" disabled="" selected="">---</option>
+<?php
+foreach ($dirsStatuses as $dir => $alreadyEnabled)
+	echo '		<option' . ($alreadyEnabled ? ' disabled=""' : '') . ' value="' . $dir . '">' . $dir . '</option>' . LF;
+?>
+	</select>
+	<br>
+	<input value="Valider" type="submit">
+</form>

+ 22 - 0
pg-view/ht/add-http-onion.php

@@ -0,0 +1,22 @@
+<?php
+
+$dirsStatuses = dirsStatuses('onion', 'http');
+
+?>
+
+<p>
+	Ajouter un accès en .onion sur un dossier
+</p>
+
+<form method="post">
+	<label for="dir">Dossier ciblé</label><br>
+	<select required="" name="dir" id="dir">
+		<option value="" disabled="" selected="">---</option>
+<?php
+foreach ($dirsStatuses as $dir => $alreadyEnabled)
+	echo '		<option' . ($alreadyEnabled ? ' disabled=""' : '') . ' value="' . $dir . '">' . $dir . '</option>' . LF;
+?>
+	</select>
+	<br>
+	<input value="Valider" type="submit">
+</form>

+ 1 - 10
pages/ht/del-http-dns.php → pg-view/ht/del-http-dns.php

@@ -1,15 +1,6 @@
 <?php
 
-if (processForm()) {
-	if (dirsStatuses('dns', 'http')[$_POST['dir']] !== true)
-		output(403, 'Wrong value for <code>dir</code>.');
-
-	htDeleteSite($_POST['dir'], domainType: 'dns', protocol: 'http');
-
-	output(200, 'Accès retiré.');
-}
-
-$dirsStatuses = dirsStatuses('onion', 'http');
+$dirsStatuses = dirsStatuses('dns', 'http');
 
 ?>
 

+ 0 - 9
pages/ht/del-http-onion.php → pg-view/ht/del-http-onion.php

@@ -1,14 +1,5 @@
 <?php
 
-if (processForm()) {
-	if (dirsStatuses('onion', 'http')[$_POST['dir']] !== true)
-		output(403, 'Wrong value for <code>dir</code>.');
-
-	htDeleteSite($_POST['dir'], domainType: 'onion', protocol: 'http');
-
-	output(200, 'Accès retiré.');
-}
-
 $dirsStatuses = dirsStatuses('onion', 'http');
 
 ?>

+ 0 - 0
pages/ht/index.php → pg-view/ht/index.php


+ 0 - 0
pages/index.php → pg-view/index.php


+ 0 - 28
pages/ns/caa.php → pg-view/ns/caa.php

@@ -1,31 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (!($_POST['flag'] >= 0 AND $_POST['flag'] <= 255))
-		output(403, 'Wrong value for <code>flag</code>.');
-
-	if (!(preg_match('/^[a-z]{1,127}$/D', $_POST['tag'])))
-		output(403, 'Wrong value for <code>tag</code>.');
-
-	if (!(preg_match('/^[a-z0-9.-]{1,255}$/D', $_POST['value'])))
-		output(403, 'Wrong value for <code>value</code>.');
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'CAA',
-		$_POST['flag'],
-		$_POST['tag'],
-		$_POST['value']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-caa', 'Documentation du type d\'enregistrement CAA') ?>
 </p>

+ 0 - 19
pages/ns/cname.php → pg-view/ns/cname.php

@@ -1,22 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	$_POST['cname'] = formatAbsoluteDomain($_POST['cname']);
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'CNAME',
-		$_POST['cname']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-cname', 'Documentation du type d\'enregistrement CNAME') ?>
 </p>

+ 0 - 19
pages/ns/dname.php → pg-view/ns/dname.php

@@ -1,22 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	$_POST['dname'] = formatAbsoluteDomain($_POST['dname']);
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'DNAME',
-		$_POST['dname']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-dname', 'Documentation du type d\'enregistrement DNAME') ?>
 </p>

+ 58 - 0
pg-view/ns/edit.php

@@ -0,0 +1,58 @@
+<form method="post">
+	<label for="zone">Zone à modifier</label>
+	<br>
+	<select required="" name="zone" id="zone">
+		<option value="" disabled="" selected="">-</option>
+<?php
+foreach (nsListUserZones() as $zone)
+	echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
+?>
+	</select>
+	<br>
+	<input type="submit" value="Afficher">
+</form>
+
+<?php
+
+if (isset($data['zone_content'])) { // Display zone
+
+?>
+<form method="post">
+	<input type="hidden" name="zone" value="<?= $_POST['zone'] ?>">
+
+	<label for="zone-content">Nouveau contenu de la zone <code><strong><?= $_POST['zone'] ?></strong></code></label>
+	<br>
+	<textarea id="zone-content" name="zone-content" wrap="off" rows="<?= substr_count($data['zone_content'], LF) + 1 ?>"><?= htmlspecialchars($data['zone_content']) ?></textarea>
+	<br>
+	<input type="submit" value="Remplacer">
+</form>
+
+<?php
+
+}
+
+displayFinalMessage($data);
+
+?>
+
+<h2>Valeurs par défaut</h2>
+
+<p>Si le TTL est omis, il sera définit à <code><time datetime="PT<?= DEFAULT_TTL ?>S"><?= DEFAULT_TTL ?></time></code> secondes.</p>
+
+<p>La précision de la classe (<code>IN</code>) est facultative.</p>
+
+<h2>Valeurs autorisées</h2>
+
+<p>La zone n'est pas autorisée à dépasser <?= ZONE_MAX_CHARACTERS ?> caractères.</p>
+
+<p>Les TTLs ne sont autorisés qu'entre <code><time datetime="PT<?= MIN_TTL ?>S"><?= MIN_TTL ?></time></code> et <code><time datetime="PT<?= MAX_TTL ?>S"><?= MAX_TTL ?></time></code> secondes.</p>
+
+<p>Les seuls types dont l'édition est autorisée sont :</p>
+
+<ul>
+<?php
+	foreach (ALLOWED_TYPES as $allowed_type)
+		echo '	<li><code>' . $allowed_type . '</code></li>';
+
+?>
+</ul>

+ 0 - 0
pages/ns/index.php → pg-view/ns/index.php


+ 0 - 19
pages/ns/ip.php → pg-view/ns/ip.php

@@ -1,22 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	$record = checkIpFormat($_POST['ip']);
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		$record,
-		$_POST['ip']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-ip', 'Documentation des types d\'enregistrements A et AAAA') ?>
 </p>

+ 0 - 75
pages/ns/loc.php → pg-view/ns/loc.php

@@ -1,78 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (empty($_POST['lat-min']))
-		$_POST['lat-min'] = 0;
-	if (empty($_POST['lat-sec']))
-		$_POST['lat-sec'] = 0;
-	if (empty($_POST['lon-min']))
-		$_POST['lon-min'] = 0;
-	if (empty($_POST['lon-sec']))
-		$_POST['lon-sec'] = 0;
-	if (empty($_POST['size']))
-		$_POST['size'] = 1;
-	if (empty($_POST['hp']))
-		$_POST['hp'] = 10000;
-	if (empty($_POST['vp']))
-		$_POST['vp'] = 10;
-
-	if (!($_POST['lat-deg'] >= 0 AND $_POST['lat-deg'] <= 90))
-		output(403, 'Wrong value for <code>lat-deg</code>.');
-	if (!($_POST['lat-min'] >= 0 AND $_POST['lat-min'] <= 59))
-		output(403, 'Wrong value for <code>lat-min</code>.');
-	if (!($_POST['lat-sec'] >= 0 AND $_POST['lat-sec'] <= 59.999))
-		output(403, 'Wrong value for <code>lat-sec</code>.');
-
-	if ($_POST['lat-dir'] !== 'N' AND $_POST['lat-dir'] !== 'S')
-		output(403, 'Wrong value for <code>lat-dir</code>.');
-
-	if (!($_POST['lon-deg'] >= 0 AND $_POST['lon-deg'] <= 180))
-		output(403, 'Wrong value for <code>lon-deg</code>.');
-	if (!($_POST['lon-min'] >= 0 AND $_POST['lon-min'] <= 59))
-		output(403, 'Wrong value for <code>lon-min</code>.');
-	if (!($_POST['lon-sec'] >= 0 AND $_POST['lon-sec'] <= 59.999))
-		output(403, 'Wrong value for <code>lon-sec</code>.');
-
-	if ($_POST['lon-dir'] !== 'E' AND $_POST['lon-dir'] !== 'W')
-		output(403, 'Wrong value for <code>lon-dir</code>.');
-
-	if (!($_POST['alt'] >= -100000 AND $_POST['alt'] <= 42849672.95))
-		output(403, 'Wrong value for <code>alt</code>.');
-
-	if (!($_POST['size'] >= 0 AND $_POST['size'] <= 90000000))
-		output(403, 'Wrong value for <code>size</code>.');
-
-	if (!($_POST['hp'] >= 0 AND $_POST['hp'] <= 90000000))
-		output(403, 'Wrong value for <code>hp</code>.');
-
-	if (!($_POST['vp'] >= 0 AND $_POST['vp'] <= 90000000))
-		output(403, 'Wrong value for <code>vp</code>.');
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'LOC',
-		$_POST['lat-deg'],
-		$_POST['lat-min'],
-		$_POST['lat-sec'],
-		$_POST['lat-dir'],
-		$_POST['lon-deg'],
-		$_POST['lon-min'],
-		$_POST['lon-sec'],
-		$_POST['lon-dir'],
-		$_POST['alt'] . 'm',
-		$_POST['size'] . 'm',
-		$_POST['hp'] . 'm',
-		$_POST['vp'] . 'm',
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-loc', 'Documentation du type d\'enregistrement LOC') ?>
 </p>

+ 0 - 23
pages/ns/mx.php → pg-view/ns/mx.php

@@ -1,26 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (!($_POST['priority'] >= 0 AND $_POST['priority'] <= 255))
-		output(403, 'Wrong value for <code>priority</code>.');
-
-	$_POST['host'] = formatAbsoluteDomain($_POST['host']);
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'MX',
-		$_POST['priority'],
-		$_POST['host']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-mx', 'Documentation du type d\'enregistrement MX') ?>
 </p>

+ 0 - 19
pages/ns/ns.php → pg-view/ns/ns.php

@@ -1,22 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	$_POST['ns'] = formatAbsoluteDomain($_POST['ns']);
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'NS',
-		$_POST['ns']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-ns', 'Documentation du type d\'enregistrement NS') ?>
 </p>

+ 77 - 0
pg-view/ns/print.php

@@ -0,0 +1,77 @@
+
+<form method="post">
+	<input type="radio" name="print" id="table" value="table" checked="">
+	<label for="table">Tableau de mes enregistrements</label>
+	<br>
+	<input type="radio" name="print" id="ds" value="ds">
+	<label for="ds">Enregistrement DS</label>
+	<br>
+	<input type="radio" name="print" id="raw" value="raw">
+	<label for="raw">Fichier de zone brut</label>
+	<br>
+	<label for="zone">Zone</label>
+	<select required="" name="zone" id="zone">
+		<option value="" disabled="" selected="">-</option>
+<?php
+foreach (nsListUserZones() as $zone)
+	echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
+?>
+	</select>
+	<br>
+	<input value="Afficher" type="submit">
+</form>
+
+<?php
+
+if (isset($data['zone-raw']))
+	echo '<pre>' . htmlspecialchars($data['zone-raw']) . '</pre>';
+
+if (isset($data['zone-table'])) { ?>
+
+<table>
+	<tr>
+		<th>Domaine</th>
+		<th>TTL</th>
+		<th>Type</th>
+		<th>Contenu</th>
+	</tr>
+<?php
+	foreach ($data['zone-table'] as $zone_line) {
+		echo '	<tr>' . LF;
+		foreach ($zone_line as $element)
+			echo '		<td><code>' . htmlspecialchars($element) . '</code></td>' . LF;
+		echo '	</tr>' . LF;
+	}
+}
+?>
+</table>
+<?php
+
+if (isset($data['zone-ds'])) { ?>
+
+<dl>
+	<dt>Zone</dt>
+	<dd>
+		<code><?= $_POST['zone'] ?></code>
+	</dd>
+	<dt>Tag</dt>
+	<dd>
+		<code><?= $data['zone-ds']['tag'] ?></code>
+	</dd>
+	<dt>Algorithme</dt>
+	<dd>
+		<code><?= $data['zone-ds']['algo'] ?></code><?= ($data['zone-ds']['algo'] === '15') ? ' (Ed25519)' : '' ?>
+	</dd>
+	<dt>Type de condensat</dt>
+	<dd>
+		<code><?= $data['zone-ds']['digest_type'] ?></code><?= ($data['zone-ds']['digest_type'] === '2') ? ' (SHA-256)' : '' ?>
+	</dd>
+	<dt>Condensat</dt>
+	<dd>
+		<code><?= $data['zone-ds']['digest'] ?></code>
+	</dd>
+</dl>
+
+<?php
+}
+

+ 0 - 31
pages/ns/srv.php → pg-view/ns/srv.php

@@ -1,34 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (!($_POST['priority'] >= 0 AND $_POST['priority'] <= 65535))
-		output(403, 'Wrong value for <code>priority</code>.');
-
-	if (!($_POST['weight'] >= 0 AND $_POST['weight'] <= 65535))
-		output(403, 'Wrong value for <code>weight</code>.');
-
-	if (!($_POST['port'] >= 0 AND $_POST['port'] <= 65535))
-		output(403, 'Wrong value for <code>port</code>.');
-
-	$_POST['target'] = formatAbsoluteDomain($_POST['target']);
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'SRV',
-		$_POST['priority'],
-		$_POST['weight'],
-		$_POST['port'],
-		$_POST['target']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-srv', 'Documentation du type d\'enregistrement SRV') ?>
 </p>

+ 0 - 28
pages/ns/sshfp.php → pg-view/ns/sshfp.php

@@ -1,31 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (!($_POST['algo'] === '1' OR $_POST['algo'] === '3' OR $_POST['algo'] === '4'))
-		output(403, 'Wrong value for <code>algo</code>.');
-
-	if (!($_POST['type'] === '2'))
-		output(403, 'Wrong value for <code>type</code>.');
-
-	if (!(preg_match('/^[a-z0-9]{64}$/D', $_POST['fp'])))
-		output(403, 'Wrong value for <code>fp</code>.');
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'SSHFP',
-		$_POST['algo'],
-		$_POST['type'],
-		$_POST['fp']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-sshfp', 'Documentation du type d\'enregistrement SSHFP') ?>
 </p>

+ 0 - 32
pages/ns/tlsa.php → pg-view/ns/tlsa.php

@@ -1,35 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (!($_POST['use'] >= 0 AND $_POST['use'] <= 3))
-		output(403, 'Wrong value for <code>use</code>.');
-
-	if (!($_POST['selector'] === '0' OR $_POST['selector'] === '1'))
-		output(403, 'Wrong value for <code>selector</code>.');
-
-	if (!($_POST['type'] >= 0 AND $_POST['type'] <= 2))
-		output(403, 'Wrong value for <code>type</code>.');
-
-	if (!(preg_match('/^[a-zA-Z0-9.-]{1,1024}$/D', $_POST['content'])))
-		output(403, 'Wrong value for <code>content</code>.');
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'TLSA',
-		$_POST['use'],
-		$_POST['selector'],
-		$_POST['type'],
-		$_POST['content']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-tlsa', 'Documentation du type d\'enregistrement TLSA') ?>
 </p>

+ 0 - 20
pages/ns/txt.php → pg-view/ns/txt.php

@@ -1,23 +1,3 @@
-<?php
-
-if (processForm()) {
-	$values = nsParseCommonRequirements();
-
-	if (!(preg_match('/^[a-zA-Z0-9 .@=:!%$+\/\()[\]_-]{5,8192}$/D', $_POST['txt'])))
-		output(403, 'Wrong value for <code>txt</code>.');
-
-	knotcZoneExec($_POST['zone'], array(
-		$values['domain'],
-		$values['ttl'],
-		'TXT',
-		'"' . $_POST['txt'] . '"'
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-txt', 'Documentation du type d\'enregistrement TXT') ?>
 </p>

+ 19 - 0
pg-view/ns/zone-add.php

@@ -0,0 +1,19 @@
+<p>
+	Pour prouver que vous possédez bien ce domaine, il doit posséder un <?= linkToDocs('ns-record', 'enregistrement NS') ?> égal à <code><?= $proof ?>._domain-verification.<?= SERVER_NAME ?>.</code> lors du traitement de ce formulaire.
+</p>
+
+<p>
+	La zone sera servie par ces serveurs de noms :
+	<ul>
+<?php
+foreach (CONF['ns']['servers'] as $server)
+	echo '	<li><code>' . $server . '</code></li>';
+?>
+	</ul>
+</p>
+
+<form method="post">
+	<label for="domain">Domaine</label><br>
+	<input required="" placeholder="domain.<?= PLACEHOLDER_DOMAIN ?>." id="domain" name="domain" type="text"><br>
+	<input value="Ajouter" type="submit">
+</form>

+ 12 - 0
pg-view/ns/zone-del.php

@@ -0,0 +1,12 @@
+<form method="post">
+	<label for="zone">Zone</label>
+	<select required="" name="zone" id="zone">
+		<option value="" disabled="" selected="">-</option>
+<?php
+foreach (nsListUserZones() as $zone)
+	echo '		<option value="' . $zone . '">' . $zone . '</option>' . LF;
+?>
+	</select>
+	<br>
+	<input value="Supprimer toutes les données liées à cette zone" type="submit">
+</form>

+ 1 - 43
pages/reg/ds.php → pg-view/reg/ds.php

@@ -1,45 +1,3 @@
-<?php
-
-if (isset($_SESSION['id']))
-	$domains = regListUserDomains($_SESSION['id']);
-else
-	$domains = [];
-
-if (processForm()) {
-	if (
-		($_POST['algo'] !== '8')
-		AND ($_POST['algo'] !== '13')
-		AND ($_POST['algo'] !== '14')
-		AND ($_POST['algo'] !== '15')
-		AND ($_POST['algo'] !== '16')
-	) output(403, 'Wrong value for <code>algo</code>.');
-
-	$_POST['keytag'] = intval($_POST['keytag']);
-	if ((!preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535))
-		output(403, 'Wrong value for <code>keytag</code>.');
-
-	if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4')
-		output(403, 'Wrong value for <code>dt</code>.');
-
-	regCheckDomainPossession($_POST['zone']);
-
-	$action = checkAction($_POST['action']);
-
-	knotcZoneExec(CONF['reg']['registry'], array(
-		$_POST['zone'],
-		CONF['reg']['ttl'],
-		'DS',
-		$_POST['keytag'],
-		$_POST['algo'],
-		$_POST['dt'],
-		$_POST['key']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	Ici vous pouvez indiquer au registre l'enregistrement DS d'une zone afin de permettre de déléguer la confiance <?= linkToDocs('dnssec', 'DNSSEC') ?>.
 </p>
@@ -56,7 +14,7 @@ if (processForm()) {
 	<select required="" name="zone" id="zone">
 		<option value="" disabled="" selected="">---</option>
 <?php
-foreach ($domains as $domain)
+foreach (regListUserDomains() as $domain)
 	echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
 ?>
 	</select>

+ 2 - 24
pages/reg/glue.php → pg-view/reg/glue.php

@@ -1,24 +1,3 @@
-<?php
-
-if (processForm()) {
-	regCheckDomainPossession($_POST['suffix']);
-
-	$domain = formatAbsoluteDomain(formatEndWithDot($_POST['subdomain']) . $_POST['suffix']);
-
-	$record = checkIpFormat($_POST['ip']);
-
-	knotcZoneExec(CONF['reg']['registry'], array(
-		$domain,
-		CONF['reg']['ttl'],
-		$record,
-		$_POST['ip']
-	));
-
-	output(200, 'Glue ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('glue-record', 'Documentation sur le glue record'); ?>
 </p>
@@ -42,9 +21,8 @@ if (processForm()) {
 			<select required="" name="suffix" id="suffix">
 				<option value="" disabled="" selected="">---</option>
 <?php
-if (isset($_SESSION['id']))
-	foreach(regListUserDomains($_SESSION['id']) as $suffix)
-		echo '		<option value="' . $suffix . '">' . $suffix . '</option>' . LF;
+foreach(regListUserDomains() as $suffix)
+	echo '		<option value="' . $suffix . '">' . $suffix . '</option>' . LF;
 ?>
 			</select>
 		</div>

+ 0 - 0
pages/reg/index.php → pg-view/reg/index.php


+ 2 - 21
pages/reg/ns.php → pg-view/reg/ns.php

@@ -1,21 +1,3 @@
-<?php
-
-if (processForm()) {
-	regCheckDomainPossession($_POST['domain']);
-	$_POST['ns'] = formatAbsoluteDomain($_POST['ns']);
-
-	knotcZoneExec(CONF['reg']['registry'], array(
-		$_POST['domain'],
-		CONF['reg']['ttl'],
-		'NS',
-		$_POST['ns']
-	));
-
-	output(200, 'Enregistrement ajouté/retiré.');
-}
-
-?>
-
 <p>
 	<?= linkToDocs('record-ns', 'Documentation du type d\'enregistrement NS') ?>
 </p>
@@ -32,9 +14,8 @@ if (processForm()) {
 	<select required="" name="domain" id="domain">
 		<option value="" disabled="" selected="">---</option>
 <?php
-if (isset($_SESSION['id']))
-	foreach (regListUserDomains($_SESSION['id']) as $domain)
-		echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
+foreach (regListUserDomains() as $domain)
+	echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
 ?>
 	</select>
 	<br>

+ 34 - 0
pg-view/reg/print.php

@@ -0,0 +1,34 @@
+<form method="post">
+	<label for="domain">Domaine</label>
+	<select required="" name="domain" id="domain">
+		<option value="" disabled="" selected="">-</option>
+<?php
+foreach (regListUserDomains() as $domain)
+	echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
+?>
+	</select>
+	<br>
+	<input value="Afficher" type="submit">
+</form>
+
+<table>
+	<tr>
+		<th>Domaine</th>
+		<th>TTL</th>
+		<th>Type</th>
+		<th>Contenu</th>
+	</tr>
+
+<?php
+
+if (isset($data['zone-content'])) {
+	foreach ($data['zone-content'] as $zone_line) {
+		echo '	<tr>' . LF;
+		foreach ($zone_line as $element)
+			echo '		<td><code>' . htmlspecialchars($element) . '</code></td>' . LF;
+		echo '	</tr>' . LF;
+	}
+}
+
+?>
+</table>

+ 11 - 0
pg-view/reg/register.php

@@ -0,0 +1,11 @@
+<p>
+	Enregistrer un nouveau domaine sur son compte. Ce domaine doit être composé uniquement d'au moins 4 lettres latines non accentuées (a-z).
+</p>
+
+<form method="post">
+	<label for="subdomain">Sous-domaine</label>
+	<br>
+	<code><input id="subdomain" pattern="<?= SUBDOMAIN_REGEX ?>" required="" placeholder="niver" name="subdomain" type="text">.<?= CONF['reg']['registry'] ?></code>
+	<br>
+	<input value="Enregistrer" type="submit">
+</form>

+ 242 - 0
pg-view/reg/reserved.txt

@@ -0,0 +1,242 @@
+# List of subdomains not available to register
+#
+# They may be forbidden because:
+# - they may be privileged for impersonating Niver, spamming or fishing
+# - they are reserved for a project asking for it and deserving such a well-known name
+
+niver
+
+# Registry-related
+nic
+domain
+domains
+reg
+registry
+
+# Special subdomains
+autoconfig
+autodiscover
+
+# Special TLDs
+example
+invalid
+test
+local
+localhost
+onion
+
+# Standard-related
+ns0
+ns1
+ns2
+ns3
+ns4
+ns5
+ns6
+ns7
+ns8
+ns9
+dns
+dns0
+dns1
+dns2
+dns3
+dns4
+dns5
+dns6
+dns7
+dns8
+dns9
+www
+wwww
+www0
+www1
+www2
+www3
+www4
+www5
+www6
+www7
+www8
+www9
+srv
+srv0
+srv1
+srv2
+srv3
+srv4
+srv5
+srv6
+srv7
+srv8
+srv9
+ssh
+sftp
+http
+https
+ssl
+tls
+mtx
+matrix
+gmi
+gemini
+ftp
+ftps
+mx
+imap
+imaps
+smtp
+smtps
+pop
+xmpp
+fedi
+html
+rss
+ipv4
+ipv6
+
+# Prevent account fishing
+account
+accounts
+register
+profile
+signup
+login
+auth
+authenticate
+connect
+
+# Commercial
+com
+free
+trial
+ads
+bank
+banks
+business
+customer
+customers
+store
+stores
+shop
+shops
+job
+jobs
+marketing
+sales
+
+# Miscellaneous
+org
+net
+com
+gov
+gouv
+edu
+api
+cdn
+support
+admin
+web
+dev
+host
+portal
+beta
+alpha
+demo
+vpn
+temp
+root
+data
+stats
+chat
+about
+remote
+portal
+boost
+core
+learn
+community
+meta
+news
+public
+online
+join
+mobile
+tech
+space
+zone
+name
+access
+search
+static
+secure
+security
+bbs
+help
+info
+code
+doc
+docs
+server
+servers
+client
+clients
+mail
+mails
+email
+emails
+webmail
+site
+sites
+website
+websites
+blog
+blogs
+gemlog
+gemlogs
+capsule
+capsules
+source
+sources
+update
+updates
+forum
+forums
+service
+services
+ressource
+ressources
+image
+images
+video
+videos
+radio
+radios
+music
+map
+maps
+app
+apps
+dev
+devs
+developer
+developers
+social
+cloud
+clouds
+network
+networks
+survey
+surveys
+build
+builds
+upload
+uploads
+download
+downloads
+content
+contents
+drive
+drives
+home
+homes

+ 11 - 0
pg-view/reg/transfer.php

@@ -0,0 +1,11 @@
+<p>
+	Pour prouver que vous êtes autorisé à recevoir le domaine par san possessaire actuele, ledit domaine doit posséder un <?= linkToDocs('ns-record', 'enregistrement NS') ?> égal à <code><?= getAuthToken() ?>._transfer-verification.<?= SERVER_NAME ?>.</code> lors du traitement de ce formulaire. Cet enregistrement sera automatiquement retiré une fois validé.
+</p>
+
+<form method="post">
+	<label for="subdomain">Sous-domaine à recevoir</label>
+	<br>
+	<code><input required="" placeholder="subdomain" id="subdomain" name="subdomain" type="text">.<?= CONF['reg']['registry'] ?></code>
+	<br>
+	<input value="Recevoir ce domaine" type="submit">
+</form>

+ 2 - 15
pages/reg/unregister.php → pg-view/reg/unregister.php

@@ -1,15 +1,3 @@
-<?php
-
-if (processForm()) {
-	regCheckDomainPossession($_POST['domain']);
-
-	regDeleteDomain($_POST['domain']);
-
-	output(200, 'Domaine effacé du registre.');
-}
-
-?>
-
 <p>
 	Ceci désenregistrera le domaine, et le rendra ainsi à nouveau disponible à l'enregistrement par n'importe qui.
 </p>
@@ -20,9 +8,8 @@ if (processForm()) {
 	<select required="" name="domain" id="domain">
 		<option value="" disabled="" selected="">---</option>
 <?php
-if (isset($_SESSION['id']))
-	foreach(regListUserDomains($_SESSION['id']) as $domain)
-		echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
+foreach(regListUserDomains() as $domain)
+	echo '		<option value="' . $domain . '">' . $domain . '</option>' . LF;
 ?>
 	</select>
 	<br>

+ 25 - 60
router.php

@@ -1,4 +1,5 @@
 <?php
+define('TIME', hrtime(true));
 define('CONF', parse_ini_file(__DIR__ . '/config.ini', true, INI_SCANNER_TYPED));
 
 foreach (array_diff(scandir(CONF['common']['root_path'] . '/fn'), array('..', '.')) as $file)
@@ -31,7 +32,8 @@ function getPageInformations($pages, $pageElements) {
 	if (!isset($pages['index']) OR $pageElements[0] === 'index')
 		return [
 			'titles_lineage' => [$pages[$pageElements[0]]['title'] ?? false],
-			'page_metadata' => $pages[$pageElements[0]] ?? NULL
+			'page_metadata' => $pages[$pageElements[0]] ?? NULL,
+			'terminal' => $pageElements[0] !== 'index'
 		];
 	$result = $pages['index']['title'];
 	if (!isset($pageElements[1]))
@@ -45,6 +47,7 @@ function getPageInformations($pages, $pageElements) {
 $pageInformations = getPageInformations(PAGES, PAGE_LINEAGE);
 define('TITLES_LINEAGE', array_reverse($pageInformations['titles_lineage']));
 define('PAGE_METADATA', $pageInformations['page_metadata']);
+define('PAGE_TERMINAL', $pageInformations['terminal']);
 
 if (!TITLES_LINEAGE[array_key_last(TITLES_LINEAGE)]) {
 	http_response_code(404);
@@ -52,11 +55,7 @@ if (!TITLES_LINEAGE[array_key_last(TITLES_LINEAGE)]) {
 }
 
 const SESSION_COOKIE_NAME = 'niver-session-key';
-if (
-		isset($_COOKIE[SESSION_COOKIE_NAME]) // Resume session
-	OR
-		(isset($_POST['username']) AND in_array(PAGE_URL, ['auth/login', 'auth/register'])) // Start new session
-	) {
+function startSession() {
 	session_start([
 		'name' => SESSION_COOKIE_NAME,
 		'sid_length' => 64,
@@ -72,44 +71,8 @@ if (
 		'use_only_cookies' => true,
 	]);
 }
-
-?>
-<!DOCTYPE html>
-<html lang="fr"<?php if (!empty(SERVICE)) echo ' class="' . SERVICE . '"'; ?>>
-	<head>
-		<meta charset="utf-8">
-		<title><?php
-foreach(array_reverse(TITLES_LINEAGE) as $id => $title)
-	echo strip_tags($title) . (array_key_last(TITLES_LINEAGE) === $id ? '' : ' < ');
-?></title>
-<?php
-foreach (glob('css/*.css') as $cssPath)
-	echo '		<link type="text/css" rel="stylesheet" media="screen" href="' . CONF['common']['prefix'] . '/' .  $cssPath . '">' . LF;
-?>
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-	</head>
-	<body>
-		<header>
-			<p>
-<?php if (isset($_SESSION['id'])) { ?>
-				<?= ($_SESSION['type'] === 'approved') ? '<span title="Compte approuvé">👤 </span>' : '<span title="Compte de test">⏳ </span>' ?><strong><?= $_SESSION['display-username'] ?></strong> <a class="auth" href="<?= CONF['common']['prefix'] ?>/auth/logout">Se déconnecter</a>
-<?php } else { ?>
-				<span aria-hidden="true">👻 </span><em>Anonyme</em> <a class="auth" href="<?= redirUrl('auth/login') ?>">Se connecter</a>
-<?php } ?>
-			</p>
-			<nav>
-<?php
-foreach (TITLES_LINEAGE as $id => $title) {
-	$lastTitle = (TITLES_LINEAGE[array_key_last(TITLES_LINEAGE)] === $title);
-	echo '<ul><li>' . ($lastTitle ? '<h1>' : '') . '<a' . (($id === 0) ? ' class="niver"' : '') . ' href="' . CONF['common']['prefix'] . ($lastTitle ? '/' . PAGE_URL : '/' . implode('/', array_slice(PAGE_LINEAGE, 0, $id)) . (($lastTitle OR $id === 0) ? '' : '/')) . '">' . $title . '</a>' . ($lastTitle ? '</h1>' : '') . LF;
-}
-echo str_repeat('</li></ul>', count(TITLES_LINEAGE));
-?>
-
-			</nav>
-		</header>
-		<main>
-<?php
+if (isset($_COOKIE[SESSION_COOKIE_NAME]))
+	startSession(); // Resume session
 
 if (in_array(SERVICE, ['reg', 'ns', 'ht']) AND CONF[SERVICE]['enabled'] !== true)
 	output(403, 'Ce service est désactivé.');
@@ -128,24 +91,26 @@ if (in_array($_SERVER['SERVER_NAME'], CONF['common']['public_domains'], true) !=
 	output(500, 'The current server name is not allowed in configuration.');
 define('SERVER_NAME', $_SERVER['SERVER_NAME']);
 
-function displayFinalMessage() {
-	global $final_message;
-	echo $final_message ?? '';
-	$final_message = NULL;
+function displayFinalMessage($data) {
+	if (isset($data['final_message'])) {
+		echo $data['final_message'];
+		unset($data['final_message']);
+	}
 }
 
-function executePage() {
-	require 'pages/' . PAGE_ADDRESS . '.php';
+if ($_POST !== []) {
+	if (PAGE_METADATA['require-login'] ?? true !== false) {
+		if (isset($_SESSION['id']) !== true)
+			output(403, 'Vous devez être connecté·e à un compte pour effectuer cette action.');
+		if (isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0]) !== true)
+			output(403, 'Ce compte n\'existe plus. Déconnectez-vous pour terminer cette session fantôme.');
+	}
+	if (file_exists('pg-act/' . PAGE_ADDRESS . '.php'))
+		require 'pg-act/' . PAGE_ADDRESS . '.php';
+}
 
-	displayFinalMessage();
-?>
-		</main>
-		<footer>
-			<small><a rel="external" href="https://code.antopie.org/niver/niver" class="niver">Code source</a> sous <abbr title="Cooperative Nonviolent Public License No Attribution version 7 ou plus">CNPL-NAv7+</abbr>.</small>
-		</footer>
-	</body>
-</html>
-<?php
+function displayPage($data) {
+	require 'view.php';
 	exit();
 }
-executePage();
+displayPage($data ??= NULL);

+ 51 - 0
view.php

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="fr"<?php if (!empty(SERVICE)) echo ' class="' . SERVICE . '"'; ?>>
+	<head>
+		<meta charset="utf-8">
+		<title><?php
+	foreach(array_reverse(TITLES_LINEAGE) as $id => $title)
+		echo strip_tags($title) . (array_key_last(TITLES_LINEAGE) === $id ? '' : ' < ');
+?></title>
+<?php
+	foreach (glob('css/*.css') as $css_path)
+		echo '		<link type="text/css" rel="stylesheet" media="screen" href="' . CONF['common']['prefix'] . '/' .  $css_path . '">' . LF;
+?>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+	</head>
+	<body>
+		<header>
+			<p>
+<?php if (isset($_SESSION['id'])) { ?>
+				<?= ($_SESSION['type'] === 'approved') ? '<span title="Compte approuvé">👤 </span>' : '<span title="Compte de test">⏳ </span>' ?><strong><?= $_SESSION['display-username'] ?></strong> <a class="auth" href="<?= CONF['common']['prefix'] ?>/auth/logout">Se déconnecter</a>
+<?php } else { ?>
+				<span aria-hidden="true">👻 </span><em>Anonyme</em> <a class="auth" href="<?= redirUrl('auth/login') ?>">Se connecter</a>
+<?php } ?>
+			</p>
+			<nav>
+<?php
+	foreach (TITLES_LINEAGE as $id => $title) {
+		$lastTitle = (TITLES_LINEAGE[array_key_last(TITLES_LINEAGE)] === $title);
+		echo '<ul><li>' . ($lastTitle ? '<h1>' : '') . '<a' . (($id === 0) ? ' class="niver"' : '') . ' href="' . CONF['common']['prefix'] . ($lastTitle ? '/' . PAGE_URL : '/' . implode('/', array_slice(PAGE_LINEAGE, 0, $id)) . (($lastTitle OR $id === 0) ? '' : '/')) . '">' . $title . '</a>' . ($lastTitle ? '</h1>' : '') . LF;
+	}
+	echo str_repeat('</li></ul>', count(TITLES_LINEAGE));
+?>
+
+			</nav>
+		</header>
+		<main>
+<?php
+
+	require 'pg-view/' . PAGE_ADDRESS . '.php';
+
+	if ($_POST === [] AND PAGE_METADATA['require-login'] ?? true !== false AND !isset($_SESSION['id']) AND PAGE_TERMINAL)
+		echo '<p>Ce formulaire ne sera pas accepté car il faut <a class="auth" href="' . redirUrl('auth/login') . '">se connecter</a> avant.</p>';
+
+	displayFinalMessage($data);
+
+?>
+		</main>
+		<footer>
+			<small><a rel="external" href="https://code.antopie.org/niver/niver" class="niver">Code source</a> sous <abbr title="Cooperative Nonviolent Public License No Attribution version 7 ou plus">CNPL-NAv7+</abbr>.</small>
+		</footer>
+	</body>
+</html>