Advanced services status management

This commit is contained in:
Miraty 2023-03-09 01:35:30 +01:00
parent a95f6203eb
commit abb9aabf5b
13 changed files with 143 additions and 109 deletions

View file

@ -32,6 +32,15 @@ String defining the displayed identity of the service.
Pretty string sometimes prefixed to the service name. Can be empty.
### `services[]`
Keys `reg`, `ns` and `ht` are required.
Values can be:
* `enabled`: the service is provided as usual
* `error`: the service is temporarily unavailable for maintenance/debugging
* `disabled`: the service is ignored everywhere ; this installation never provides it
## `[dns]`
This configuration section is used by both the registry (`reg`) and the public name server (`ns`).
@ -46,10 +55,6 @@ Filesystem path to the `kdig` binary. Used to authenticate resources possession
## `[reg]`
### `enabled`
Defines whether the web interface for this service is accessible.
### `suffixes[]`
Lists the suffixes that the registry manages.
@ -78,10 +83,6 @@ Host where the Knot DNS server answers the registry values. Should be a secure (
## `[ns]`
### `enabled`
Defines whether the web interface for this service is accessible.
### `knot_zones_path`
Filesystem path to the zones directory. The full path to created zonefiles will be `knot_zones_path/<zone-apex-domain>.zone`.
@ -102,10 +103,6 @@ Administrator email address published in every SOA record. Ends with a `.`, `@`
## `[ht]`
### `enabled`
Defines whether the web interface for this service is accessible. (Users can still use SFTP.)
### `ht_path`
Filesystem path to the users files base directory. Files of a user are located inside `ht_path/<their-internal-user-id>/`

View file

@ -3,13 +3,13 @@
## As a developer
Extract messages to be translated from the source files and into a Portable Object Template file:
```
xgettext --from-code=UTF-8 --no-wrap -d messages -p locales/ --from-code=UTF-8 *.php */*.php */*/*.php
```shell
xgettext --from-code=UTF-8 --no-wrap -d messages -p locales/ *.php */*.php */*/*.php
mv locales/messages.po locales/messages.pot
```
Merge messages into existing Portable Objects:
```
```shell
msgmerge --no-wrap locales/fr/C/LC_MESSAGES/messages.po locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
```
@ -17,7 +17,7 @@ msgmerge --no-wrap locales/fr/C/LC_MESSAGES/messages.po locales/messages.pot -o
### To start a new translation
```
```shell
mkdir -p locales/fr/C/LC_MESSAGES/
msginit -i locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
```
@ -26,12 +26,12 @@ msginit -i locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
Edit `locales/fr/C/LC_MESSAGES/messages.po` using either
* any text editor
* a dedicated translation software like [Poedit](https://poedit.net/), [KDE's Lokalize](https://apps.kde.org/lokalize/) or [GNOME Translation Editor](https://wiki.gnome.org/Apps/Gtranslator).
* dedicated translation software like [Poedit](https://poedit.net/), [KDE's Lokalize](https://apps.kde.org/lokalize/) or [GNOME Translation Editor](https://wiki.gnome.org/Apps/Gtranslator).
## As an administrator
To compile Portable Objects into Machine Objects:
```
```shell
msgfmt locales/fr/C/LC_MESSAGES/messages.po -o locales/fr/C/LC_MESSAGES/messages.mo
```

View file

@ -6,13 +6,15 @@ public_domains[] = "servnest.test"
prefix = ""
service_name = "ServNest"
service_emoji = "🪺"
services[reg] = "enabled"
services[ns] = "enabled"
services[ht] = "enabled"
[dns]
knotc_path = "/usr/sbin/knotc"
kdig_path = "/usr/bin/kdig"
[reg]
enabled = true
suffixes[servnest.test.] = "approved"
suffixes[test.servnest.test.] = "all"
suffixes[old.sernnest.test.] = "none"
@ -21,7 +23,6 @@ ttl = 86400
address = "[::1]:42053"
[ns]
enabled = true
knot_zones_path = "/srv/servnest/ns"
servers[] = "ns1.servnest.test."
servers[] = "ns2.servnest.test."
@ -29,8 +30,6 @@ kzonecheck_path = "/usr/bin/kzonecheck"
public_soa_email = "hostmaster.invalid."
[ht]
enabled = true
ht_path = "/srv/servnest/ht"
subpath_domain = "ht.servnest.test"

View file

@ -4,11 +4,11 @@ CREATE TABLE IF NOT EXISTS "params" (
"value" TEXT NOT NULL,
PRIMARY KEY("name")
);
INSERT INTO "params"("name", "value") VALUES("instance_bucket_tokens", "0");
INSERT INTO "params"("name", "value") VALUES("instance_bucket_last_update", "0");
INSERT INTO "params"("name", "value") VALUES("secret_key", "0");
INSERT INTO "params"("name", "value") VALUES("secret_key_last_change", "0");
INSERT INTO "params"("name", "value") VALUES("username_salt", "00000000000000000000000000000000"); -- Should be unique and secret ; generate one using `openssl rand -hex 16` ; can't be changed without breaking current accounts login
INSERT INTO "params"("name", "value") VALUES('instance_bucket_tokens', '0');
INSERT INTO "params"("name", "value") VALUES('instance_bucket_last_update', '0');
INSERT INTO "params"("name", "value") VALUES('secret_key', '0');
INSERT INTO "params"("name", "value") VALUES('secret_key_last_change', '0');
INSERT INTO "params"("name", "value") VALUES('username_salt', '00000000000000000000000000000000'); -- Should be unique and secret ; generate one using `openssl rand -hex 16` ; can't be changed without breaking current accounts login
CREATE TABLE IF NOT EXISTS "users" (
"id" TEXT NOT NULL UNIQUE,
"username" TEXT NOT NULL UNIQUE,
@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS "users" (
"bucket_tokens" INTEGER NOT NULL,
"bucket_last_update" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"services" TEXT NOT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS "approval-keys" (

View file

@ -1,5 +1,24 @@
<?php
function htSetupUserFs($id) {
// 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.');
}
function checkDomainFormat($domain) {
// If the domain must end without a dot
if (!filter_var($domain, FILTER_VALIDATE_DOMAIN) OR !preg_match('/^([a-z0-9_-]{1,63}\.){1,126}[a-z0-9]{1,63}$/D', $domain))

View file

@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
#: pages.php:9
msgid "Authentication"
@ -272,11 +272,15 @@ msgstr "Supprimer un accès"
msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
msgstr "Retirer un accès HTTP existant d'un sous-dossier de l'espace SFTP"
#: router.php:134
#: router.php:137 view.php:39
msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
msgstr "Ce service est en cours de maintenance. Aucune action ne peut être effectuée avant qu'ane administrataire termine de le réparer."
#: router.php:147
msgid "You need to be logged in to do this."
msgstr "Vous devez être connecté·e à un compte pour faire cela."
#: router.php:136
#: router.php:149
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme."
@ -284,12 +288,12 @@ msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session f
msgid "Anonymous"
msgstr "Anonyme"
#: view.php:41
#: view.php:44
#, php-format
msgid "This form won't be accepted because you need to %slog in%s first."
msgstr "Ce forumulaire ne sera pas accepté car il faut %sse connecter%s d'abord."
#: view.php:48
#: view.php:51
#, php-format
msgid "%sSource code%s available under %s."
msgstr "%sCode source%s disponible sous %s."
@ -314,7 +318,7 @@ msgstr "<strong>Erreur de l'utilisataire</strong>&nbsp;: "
msgid "<strong>Server error</strong>: "
msgstr "<strong>Erreur du serveur</strong>&nbsp;: "
#: fn/common.php:133
#: fn/common.php:129
msgid "Wrong proof."
msgstr "Preuve incorrecte."
@ -322,7 +326,7 @@ msgstr "Preuve incorrecte."
msgid "IP address malformed."
msgstr "Adresse IP malformée."
#: fn/dns.php:67 fn/ht.php:6
#: fn/dns.php:67 fn/ht.php:25
msgid "Domain malformed."
msgstr "Domaine malformé."
@ -372,7 +376,12 @@ msgstr "Cet identifiant est déjà pris."
msgid "Account deletion must be confirmed."
msgstr "La suppression du compte doit être confirmée."
#: pg-act/auth/unregister.php:29
#: pg-act/auth/unregister.php:10
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr "Votre compte ne peut pas être supprimé car le service %s est actuellement indisponible."
#: pg-act/auth/unregister.php:39
msgid "Account deleted."
msgstr "Compte supprimé."
@ -566,7 +575,7 @@ msgid "Approved"
msgstr "Approuvé"
#: pg-view/auth/index.php:27
msgid "It was originally a testing account, but has been approved by an administrator, and is suited for stable usecases:"
msgid "It was originally a testing account, but has been approved by an administrator, and is suitable for stable use cases:"
msgstr "C'est originellement un compte de test, mais qui a été approuvé par ane administrataire, et qui permet une utilisation stable&nbsp;:"
#: pg-view/auth/index.php:30

View file

@ -271,11 +271,15 @@ msgstr ""
msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
msgstr ""
#: router.php:134
#: router.php:137 view.php:39
msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
msgstr ""
#: router.php:147
msgid "You need to be logged in to do this."
msgstr ""
#: router.php:136
#: router.php:149
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr ""
@ -283,12 +287,12 @@ msgstr ""
msgid "Anonymous"
msgstr ""
#: view.php:41
#: view.php:44
#, php-format
msgid "This form won't be accepted because you need to %slog in%s first."
msgstr ""
#: view.php:48
#: view.php:51
#, php-format
msgid "%sSource code%s available under %s."
msgstr ""
@ -313,7 +317,7 @@ msgstr ""
msgid "<strong>Server error</strong>: "
msgstr ""
#: fn/common.php:133
#: fn/common.php:129
msgid "Wrong proof."
msgstr ""
@ -321,7 +325,7 @@ msgstr ""
msgid "IP address malformed."
msgstr ""
#: fn/dns.php:67 fn/ht.php:6
#: fn/dns.php:67 fn/ht.php:25
msgid "Domain malformed."
msgstr ""
@ -371,7 +375,12 @@ msgstr ""
msgid "Account deletion must be confirmed."
msgstr ""
#: pg-act/auth/unregister.php:29
#: pg-act/auth/unregister.php:10
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr ""
#: pg-act/auth/unregister.php:39
msgid "Account deleted."
msgstr ""
@ -565,7 +574,7 @@ msgid "Approved"
msgstr ""
#: pg-view/auth/index.php:27
msgid "It was originally a testing account, but has been approved by an administrator, and is suited for stable usecases:"
msgid "It was originally a testing account, but has been approved by an administrator, and is suitable for stable use cases:"
msgstr ""
#: pg-view/auth/index.php:30

View file

@ -21,25 +21,9 @@ insert('users', [
'bucket_tokens' => 0,
'bucket_last_update' => 0,
'type' => 'testing',
'services' => '',
]);
// 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();

View file

@ -3,12 +3,21 @@
if (!isset($_POST['delete']))
output(403, _('Account deletion must be confirmed.'));
$user_services = explode(',', query('select', 'users', ['username' => $_SESSION['id']], 'services')[0]);
foreach (SERVICES_USER as $service)
if (in_array($service, $user_services, true) AND CONF['common']['services'][$service] !== 'enabled')
output(503, sprintf(_('Your account can\'t be deleted because the %s service is currently unavailable.'), '<em>' . PAGES[$service]['index']['title'] . '</em>'));
if (in_array('reg', $user_services, true))
foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain)
regDeleteDomain($domain);
if (in_array('ns', $user_services, true))
foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone)
nsDeleteZone($zone);
if (in_array('ht', $user_services, true)) {
foreach (query('select', 'sites', ['username' => $_SESSION['id']]) as $site)
htDeleteSite($site['address'], $site['type']);
@ -21,6 +30,7 @@ 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']]);

View file

@ -24,7 +24,7 @@
</dd>
<dt><span aria-hidden="true">👤 </span><em><?= _('Approved') ?></em></dt>
<dd>
<?= _('It was originally a testing account, but has been approved by an administrator, and is suited for stable usecases:') ?>
<?= _('It was originally a testing account, but has been approved by an administrator, and is suitable for stable use cases:') ?>
<ul>
<li><?= sprintf(_('%s of SFTP quota'), ((CONF['ht']['user_quota_approved'] >> 30) >= 1) ? CONF['ht']['user_quota_approved'] >> 30 . ' ' . _('<abbr title="gibibyte">GiB</abbr>') : CONF['ht']['user_quota_approved'] >> 20 . ' ' . _('<abbr title="mebibyte">MiB</abbr>')) ?></li>
<li><?= _('Stable Let\'s Encrypt certificates') ?></li>

View file

@ -1,26 +1,17 @@
<nav>
<dl>
<dt><a class="auth" href="auth/"><?= PAGES['auth']['index']['title'] ?></a></dt>
<?php
foreach (array_merge(['auth' => 'enabled'], CONF['common']['services']) as $service => $status) {
if ($status !== 'enabled' AND $status !== 'error')
continue;
?>
<?= ($status === 'error') ? '<s>' : '' ?>
<dt><a class="<?= $service ?>" href="<?= $service ?>/"><?= PAGES[$service]['index']['title'] ?></a></dt>
<dd>
<?= PAGES['auth']['index']['description'] ?>
</dd>
<?php if (CONF['reg']['enabled'] === true) { ?>
<dt><a class="reg" href="reg/"><?= PAGES['reg']['index']['title'] ?></code></a></dt>
<dd>
<?= PAGES['reg']['index']['description'] ?>
</dd>
<?php } ?>
<?php if (CONF['ns']['enabled'] === true) { ?>
<dt><a class="ns" href="ns/"><?= PAGES['ns']['index']['title'] ?></a></dt>
<dd>
<?= PAGES['ns']['index']['description'] ?>
</dd>
<?php } ?>
<?php if (CONF['ht']['enabled'] === true) { ?>
<dt><a class="ht" href="ht/"><?= PAGES['ht']['index']['title'] ?></a></dt>
<dd>
<?= PAGES['ht']['index']['description'] ?>
<?= PAGES[$service]['index']['description'] ?>
</dd>
<?= ($status === 'error') ? '</s>' : '' ?>
<?php } ?>
</dl>
</nav>

View file

@ -15,13 +15,15 @@ setlocale(LC_MESSAGES, 'C.UTF-8');
bindtextdomain('messages', 'locales/' . LOCALE);
header('Content-Language: ' . LOCALE);
const SERVICES_USER = ['reg', 'ns', 'ht'];
const LF = "\n";
const PLACEHOLDER_DOMAIN = 'example'; // From RFC2606: Reserved Top Level DNS Names > 2. TLDs for Testing, & Documentation Examples
const PLACEHOLDER_IPV6 = '2001:db8::3'; // From RFC3849: IPv6 Address Prefix Reserved for Documentation
const PLACEHOLDER_IPV4 = '203.0.113.42'; // From RFC5737: IPv4 Address Blocks Reserved for Documentation
foreach (array_diff(scandir(CONF['common']['root_path'] . '/fn'), array('..', '.')) as $file)
foreach (array_diff(scandir(CONF['common']['root_path'] . '/fn'), ['..', '.']) as $file)
require CONF['common']['root_path'] . '/fn/' . $file;
require 'pages.php';
@ -38,7 +40,6 @@ define('PAGE_URL', $pageAddress);
define('PAGE_ADDRESS', $pageAddress . ((substr($pageAddress, -1) === '/' OR $pageAddress === '') ? 'index' : ''));
define('PAGE_LINEAGE', explode('/', PAGE_ADDRESS));
define('SERVICE', dirname(PAGE_ADDRESS));
define('PAGE', basename(PAGE_ADDRESS, '.php'));
function getPageInformations($pages, $pageElements) {
if (!isset($pages['index']) OR $pageElements[0] === 'index')
@ -87,6 +88,7 @@ if (isset($_COOKIE[SESSION_COOKIE_NAME]))
startSession(); // Resume session
if (isset($_SESSION['id'])) {
// Decrypt display username
if (!isset($_COOKIE['display-username-decryption-key']))
output(403, 'The display username decryption key has not been sent.');
$decryption_result = htmlspecialchars(sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
@ -98,17 +100,18 @@ if (isset($_SESSION['id'])) {
if ($decryption_result === false)
output(403, 'Unable to decrypt display username.');
define('DISPLAY_USERNAME', $decryption_result);
// Enable not already enabled services for this user
$user_services = array_filter(explode(',', query('select', 'users', ['id' => $_SESSION['id']], 'services')[0]));
if (in_array(SERVICE, SERVICES_USER, true) AND !in_array(SERVICE, $user_services, true) AND CONF['common']['services'][SERVICE] === 'enabled') {
$user_services[] = SERVICE;
DB->prepare('UPDATE users SET services = :services WHERE id = :id')
->execute([':services' => implode(',', $user_services), ':id' => $_SESSION['id']]);
if (SERVICE === 'ht')
htSetupUserFs($_SESSION['id']);
}
if (in_array(SERVICE, ['reg', 'ns', 'ht']) AND CONF[SERVICE]['enabled'] !== true)
output(403, 'Ce service est désactivé.');
// Protect against cross-site request forgery if a POST request is received
if ($_POST !== []) {
if (isset($_SERVER['HTTP_SEC_FETCH_SITE']) !== true)
output(403, 'The <code>Sec-Fetch-Site</code> HTTP header is required when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin')
output(403, 'The <code>Sec-Fetch-Site</code> HTTP header must be <code>same-origin</code> when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
}
if (isset($_SERVER['SERVER_NAME']) !== true)
@ -125,18 +128,27 @@ function displayFinalMessage($data) {
}
if ($_POST !== []) {
if (in_array(SERVICE, SERVICES_USER, true) AND CONF['common']['services'][SERVICE] !== 'enabled')
output(503, _('This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it.'));
// Protect against cross-site request forgery if a POST request is received
if (isset($_SERVER['HTTP_SEC_FETCH_SITE']) !== true)
output(403, 'The <code>Sec-Fetch-Site</code> HTTP header is required when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin')
output(403, 'The <code>Sec-Fetch-Site</code> HTTP header must be <code>same-origin</code> when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
if (PAGE_METADATA['require-login'] ?? true !== false) {
if (isset($_SESSION['id']) !== true)
output(403, _('You need to be logged in to do this.'));
if (isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0]) !== true)
output(403, _('This account doesn\'t exist anymore. Log out to end this ghost session.'));
}
if (file_exists('pg-act/' . PAGE_ADDRESS . '.php'))
require 'pg-act/' . PAGE_ADDRESS . '.php';
}
function displayPage($data) {
require 'view.php';
exit();
}

View file

@ -35,6 +35,9 @@
<main>
<?php
if (in_array(SERVICE, SERVICES_USER, true) AND CONF['common']['services'][SERVICE] === 'error')
echo '<p><strong>' . _('This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it.') . '</strong></p>';
require 'pg-view/' . PAGE_ADDRESS . '.php';
if ($_POST === [] AND PAGE_METADATA['require-login'] ?? true !== false AND !isset($_SESSION['id']) AND PAGE_TERMINAL)