diff --git a/DOCS/configuration.md b/DOCS/configuration.md index 718c218..5ead30a 100644 --- a/DOCS/configuration.md +++ b/DOCS/configuration.md @@ -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`. @@ -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//` diff --git a/DOCS/translation.md b/DOCS/translation.md index f792f7a..05420e2 100644 --- a/DOCS/translation.md +++ b/DOCS/translation.md @@ -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 ``` diff --git a/config.ini b/config.ini index 7d499c8..4d95fb3 100644 --- a/config.ini +++ b/config.ini @@ -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" diff --git a/db/schema.sql b/db/schema.sql index d99c4b8..75c511f 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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" ( diff --git a/fn/ht.php b/fn/ht.php index b0f4d0a..956a6c4 100644 --- a/fn/ht.php +++ b/fn/ht.php @@ -1,5 +1,24 @@ Erreur de l'utilisataire : " msgid "Server error: " msgstr "Erreur du serveur : " -#: 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 :" #: pg-view/auth/index.php:30 diff --git a/locales/messages.pot b/locales/messages.pot index bff598d..04f3a94 100644 --- a/locales/messages.pot +++ b/locales/messages.pot @@ -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 "Server error: " 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 diff --git a/pg-act/auth/register.php b/pg-act/auth/register.php index 1c13ac5..aa7c936 100644 --- a/pg-act/auth/register.php +++ b/pg-act/auth/register.php @@ -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(); diff --git a/pg-act/auth/unregister.php b/pg-act/auth/unregister.php index 1f58e20..c25dc8b 100644 --- a/pg-act/auth/unregister.php +++ b/pg-act/auth/unregister.php @@ -3,24 +3,34 @@ if (!isset($_POST['delete'])) output(403, _('Account deletion must be confirmed.')); -foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain) - regDeleteDomain($domain); +$user_services = explode(',', query('select', 'users', ['username' => $_SESSION['id']], 'services')[0]); -foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone) - nsDeleteZone($zone); +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.'), '' . PAGES[$service]['index']['title'] . '')); -foreach (query('select', 'sites', ['username' => $_SESSION['id']]) as $site) - htDeleteSite($site['address'], $site['type']); +if (in_array('reg', $user_services, true)) + foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain) + regDeleteDomain($domain); -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.'); +if (in_array('ns', $user_services, true)) + foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone) + nsDeleteZone($zone); -removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id']); +if (in_array('ht', $user_services, true)) { + foreach (query('select', 'sites', ['username' => $_SESSION['id']]) as $site) + htDeleteSite($site['address'], $site['type']); -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.'); + 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']]); diff --git a/pg-view/auth/index.php b/pg-view/auth/index.php index 7d0f8a9..ec2010f 100644 --- a/pg-view/auth/index.php +++ b/pg-view/auth/index.php @@ -24,7 +24,7 @@
- +
  • > 30) >= 1) ? CONF['ht']['user_quota_approved'] >> 30 . ' ' . _('GiB') : CONF['ht']['user_quota_approved'] >> 20 . ' ' . _('MiB')) ?>
  • diff --git a/pg-view/index.php b/pg-view/index.php index 29bb9b9..2e5fe75 100644 --- a/pg-view/index.php +++ b/pg-view/index.php @@ -1,26 +1,17 @@ diff --git a/router.php b/router.php index a3c0ad6..c26026b 100644 --- a/router.php +++ b/router.php @@ -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); -} -if (in_array(SERVICE, ['reg', 'ns', 'ht']) AND CONF[SERVICE]['enabled'] !== true) - output(403, 'Ce service est dĂ©sactivĂ©.'); + // 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; -// 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 Sec-Fetch-Site HTTP header is required when submitting a POST request to prevent Cross-Site Request Forgery (CSRF).'); - if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin') - output(403, 'The Sec-Fetch-Site HTTP header must be same-origin when submitting a POST request to prevent Cross-Site Request Forgery (CSRF).'); + 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 (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 Sec-Fetch-Site HTTP header is required when submitting a POST request to prevent Cross-Site Request Forgery (CSRF).'); + if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin') + output(403, 'The Sec-Fetch-Site HTTP header must be same-origin when submitting a POST request to prevent Cross-Site Request Forgery (CSRF).'); + 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(); } diff --git a/view.php b/view.php index d9afb77..7f98c73 100644 --- a/view.php +++ b/view.php @@ -35,6 +35,9 @@
    ' . _('This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it.') . '

    '; + require 'pg-view/' . PAGE_ADDRESS . '.php'; if ($_POST === [] AND PAGE_METADATA['require-login'] ?? true !== false AND !isset($_SESSION['id']) AND PAGE_TERMINAL)