diff --git a/DOCS/architecture.md b/DOCS/architecture.md index d01b773..3914c85 100644 --- a/DOCS/architecture.md +++ b/DOCS/architecture.md @@ -2,6 +2,9 @@ ## Program flow +`init.php` +: Initializes common values + `router.php` : Receives every external HTTP request from the web server, executes actions required in any case, executes matching code in `pg-act` if appropriate, and calls `view.php` either way. @@ -21,12 +24,12 @@ The `output` function is used to return success or error messages and stop proce `fn/` : Functions, grouped by concerned service +`jobs/` +: CLI scripts + `sftpgo-auth.php` : When someone tries to log in over SFTP, SFTPGo sends username and password to this script, which queries the database and replies whether authentication succeeded or not. -`check.php` -: This file is not part of the normal program execution, it is meant to be run by developers to test that the current setup is working. - `DOCS/` : Documentation (some important or standard files may be directly in the root) diff --git a/fn/auth.php b/fn/auth.php index 7f6927f..14fc55f 100644 --- a/fn/auth.php +++ b/fn/auth.php @@ -87,6 +87,39 @@ function setupDisplayUsername($display_username) { $_SESSION['display-username-cyphertext'] = $cyphertext; } +function authDeleteUser($user_id) { + $user_services = explode(',', query('select', 'users', ['id' => $user_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.'), '' . PAGES[$service]['index']['title'] . '')); + + if (in_array('reg', $user_services, true)) + foreach (query('select', 'registry', ['username' => $user_id], 'domain') as $domain) + regDeleteDomain($domain, $user_id); + + if (in_array('ns', $user_services, true)) + foreach (query('select', 'zones', ['username' => $user_id], 'zone') as $zone) + nsDeleteZone($zone, $user_id); + + if (in_array('ht', $user_services, true)) { + foreach (query('select', 'sites', ['username' => $user_id]) as $site) + htDeleteSite($site['address'], $site['type'], $user_id); + + exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['tor_keys_path'] . '/' . $user_id, result_code: $code); + if ($code !== 0) + output(500, 'Can\'t remove Tor keys directory.'); + + removeDirectory(CONF['ht']['tor_config_path'] . '/' . $user_id); + + exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['ht_path'] . '/fs/' . $user_id, result_code: $code); + if ($code !== 0) + output(500, 'Can\'t remove user\'s directory.'); + } + + query('delete', 'users', ['id' => $user_id]); +} + function rateLimit() { if (PAGE_METADATA['tokens_account_cost'] ?? 0 > 0) rateLimitAccount(PAGE_METADATA['tokens_account_cost']); diff --git a/fn/common.php b/fn/common.php index fb2638b..3d255d9 100644 --- a/fn/common.php +++ b/fn/common.php @@ -10,7 +10,10 @@ function output($code, $msg = '', $logs = [''], $data = []) { 4 => '

' . _('User error: ') . '' . $msg . '

' . LF, 5 => '

' . _('Server error: ') . '' . $msg . '

' . LF, }; - displayPage(array_merge(['final_message' => $final_message], $data)); + if (is_callable('displayPage')) + displayPage(array_merge(['final_message' => $final_message], $data)); + echo $final_message; + exit(); } function insert($table, $values) { diff --git a/fn/ht.php b/fn/ht.php index bc2d388..76c4527 100644 --- a/fn/ht.php +++ b/fn/ht.php @@ -76,17 +76,17 @@ function htRelativeSymlink($target, $name) { output(500, 'Unable to create symlink.'); } -function htDeleteSite($address, $type) { +function htDeleteSite($address, $type, $user_id) { if ($type === 'onion') { $dir = query('select', 'sites', [ - 'username' => $_SESSION['id'], + 'username' => $user_id, 'address' => $address, 'type' => $type, ], 'site_dir')[0]; // Delete Tor config - if (unlink(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id'] . '/' . $dir) !== true) + if (unlink(CONF['ht']['tor_config_path'] . '/' . $user_id . '/' . $dir) !== true) output(500, 'Failed to delete Tor configuration.'); // Reload Tor @@ -95,7 +95,7 @@ function htDeleteSite($address, $type) { output(500, 'Failed to reload Tor.'); // Delete Tor keys - exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $dir, result_code: $code); + exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . CONF['ht']['tor_keys_path'] . '/' . $user_id . '/' . $dir, result_code: $code); if ($code !== 0) output(500, 'Failed to delete Tor keys.'); } @@ -117,7 +117,7 @@ function htDeleteSite($address, $type) { output(500, 'Unable to delete symlink.'); query('delete', 'sites', [ - 'username' => $_SESSION['id'], + 'username' => $user_id, 'type' => $type, 'address' => $address, ]); diff --git a/fn/ns.php b/fn/ns.php index 30fc277..e6c70c4 100644 --- a/fn/ns.php +++ b/fn/ns.php @@ -48,7 +48,7 @@ function nsCheckZonePossession($zone) { output(403, 'You don\'t own this zone on the name server.'); } -function nsDeleteZone($zone) { +function nsDeleteZone($zone, $user_id) { // Remove from Knot configuration knotcConfExec(["unset 'zone[$zone]'"]); @@ -64,6 +64,6 @@ function nsDeleteZone($zone) { // Remove from database query('delete', 'zones', [ 'zone' => $zone, - 'username' => $_SESSION['id'], + 'username' => $user_id, ]); } diff --git a/fn/reg.php b/fn/reg.php index fa1c4aa..0a5f981 100644 --- a/fn/reg.php +++ b/fn/reg.php @@ -13,7 +13,7 @@ function regCheckDomainPossession($domain) { output(403, 'You don\'t own this domain on the registry.'); } -function regDeleteDomain($domain) { +function regDeleteDomain($domain, $user_id) { // Delete domain from registry file $path = CONF['reg']['suffixes_path'] . '/' . regParseDomain($domain)['suffix'] . 'zone'; $content = file_get_contents($path); @@ -28,7 +28,7 @@ function regDeleteDomain($domain) { $conditions = [ 'domain' => $domain, - 'username' => $_SESSION['id'], + 'username' => $user_id, ]; insert('registry-history', [ diff --git a/init.php b/init.php new file mode 100644 index 0000000..703cd93 --- /dev/null +++ b/init.php @@ -0,0 +1,48 @@ +ErrorAn error occured.'; +}); +register_shutdown_function(function () { // Also catch fatal errors + if (($error = error_get_last()) !== NULL) + throw new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']); +}); + +const ROOT_PATH = __DIR__; +define('CONF', parse_ini_file(ROOT_PATH . '/config.ini', true, INI_SCANNER_TYPED)); + +define('DB', new PDO('sqlite:' . ROOT_PATH . '/db/servnest.db')); +DB->exec('PRAGMA foreign_keys = ON;'); +DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +date_default_timezone_set('UTC'); + +foreach (explode(',', preg_replace('/[A-Z0-9]|q=|;|-|\./', '', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')) as $client_locale) + if (in_array($client_locale, array_diff(scandir(ROOT_PATH . '/locales'), ['..', '.']), true)) { + $locale = $client_locale; + break; + } +define('LOCALE', $locale ?? 'en'); +putenv('LANG=C.UTF-8'); +setlocale(LC_MESSAGES, 'C.UTF-8'); +bindtextdomain('messages', ROOT_PATH . '/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(ROOT_PATH . '/fn'), ['..', '.']) as $file) + require ROOT_PATH . '/fn/' . $file; + +require ROOT_PATH . '/pages.php'; diff --git a/check.php b/jobs/check.php similarity index 96% rename from check.php rename to jobs/check.php index 79e2982..fa09715 100644 --- a/check.php +++ b/jobs/check.php @@ -1,7 +1,6 @@ - 'testing']) as $account) { + $account_age = time() - date_create_immutable_from_format('Y-m-d H:i:s', $account['registration_date'])->getTimestamp(); + if ($account_age > MAX_TESTING_ACCOUNT_AGE) + authDeleteUser($account['id']); +} diff --git a/pg-act/auth/unregister.php b/pg-act/auth/unregister.php index 91792a1..13b4357 100644 --- a/pg-act/auth/unregister.php +++ b/pg-act/auth/unregister.php @@ -6,36 +6,7 @@ if (checkPassword($_SESSION['id'], $_POST['current-password']) !== true) if (!isset($_POST['delete'])) output(403, _('Account deletion must be confirmed.')); -$user_services = explode(',', query('select', 'users', ['id' => $_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.'), '' . PAGES[$service]['index']['title'] . '')); - -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']); - - exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' -r ' . 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'] . ' -r ' . CONF['ht']['ht_path'] . '/fs/' . $_SESSION['id'], result_code: $code); - if ($code !== 0) - output(500, 'Can\'t remove user\'s directory.'); -} - -query('delete', 'users', ['id' => $_SESSION['id']]); +authDeleteUser($_SESSION['id']); logout(); diff --git a/pg-act/ht/del.php b/pg-act/ht/del.php index 611bbab..476cfcc 100644 --- a/pg-act/ht/del.php +++ b/pg-act/ht/del.php @@ -6,6 +6,6 @@ if (preg_match('/^(?subpath|subdomain|onion|dns):(?
[a-z0-9._-]{1, if (isset(query('select', 'sites', ['username' => $_SESSION['id'], 'address' => $site['address'], 'type' => $site['type']], 'address')[0]) !== true) output(403, 'Unavailable value for site.'); -htDeleteSite($site['address'], $site['type']); +htDeleteSite($site['address'], $site['type'], $_SESSION['id']); output(200, _('Access removed.')); diff --git a/pg-act/ns/zone-del.php b/pg-act/ns/zone-del.php index 71b39fc..1a53f91 100644 --- a/pg-act/ns/zone-del.php +++ b/pg-act/ns/zone-del.php @@ -2,6 +2,6 @@ nsCheckZonePossession($_POST['zone']); -nsDeleteZone($_POST['zone']); +nsDeleteZone($_POST['zone'], $_SESSION['id']); output(200, _('Zone deleted.')); diff --git a/pg-act/reg/unregister.php b/pg-act/reg/unregister.php index 702541d..e926a92 100644 --- a/pg-act/reg/unregister.php +++ b/pg-act/reg/unregister.php @@ -2,6 +2,6 @@ regCheckDomainPossession($_POST['domain']); -regDeleteDomain($_POST['domain']); +regDeleteDomain($_POST['domain'], $_SESSION['id']); output(200, _('Domain unregistered.')); diff --git a/router.php b/router.php index 14dd515..9109a4d 100644 --- a/router.php +++ b/router.php @@ -1,54 +1,5 @@ ErrorAn error occured.'; -}); -register_shutdown_function(function () { // Also catch fatal errors - if (($error = error_get_last()) !== NULL) - throw new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']); -}); - -const ROOT_PATH = __DIR__; -define('CONF', parse_ini_file(ROOT_PATH . '/config.ini', true, INI_SCANNER_TYPED)); - -define('DB', new PDO('sqlite:' . ROOT_PATH . '/db/servnest.db')); -DB->exec('PRAGMA foreign_keys = ON;'); -DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - -date_default_timezone_set('UTC'); - -foreach (explode(',', preg_replace('/[A-Z0-9]|q=|;|-|\./', '', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')) as $client_locale) - if (in_array($client_locale, array_diff(scandir(ROOT_PATH . '/locales'), ['..', '.']), true)) { - $locale = $client_locale; - break; - } -define('LOCALE', $locale ?? 'en'); -putenv('LANG=C.UTF-8'); -setlocale(LC_MESSAGES, 'C.UTF-8'); -bindtextdomain('messages', ROOT_PATH . '/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(ROOT_PATH . '/fn'), ['..', '.']) as $file) - require ROOT_PATH . '/fn/' . $file; - -require ROOT_PATH . '/pages.php'; - -if ($_SERVER['REQUEST_URI'] === '/sftpgo-auth.php') - return; +require 'init.php'; $pageAddress = substr($_SERVER['REQUEST_URI'], strlen(CONF['common']['prefix']) + 1); if (strpos($pageAddress, '?') !== false) { @@ -113,6 +64,9 @@ if (isset($_COOKIE[SESSION_COOKIE_NAME])) startSession(); // Resume session if (isset($_SESSION['id'])) { + if (!isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0])) + output(403, _('This account doesn\'t exist anymore. Log out to end this ghost session.')); + // Decrypt display username if (!isset($_COOKIE['display-username-decryption-key'])) output(403, 'The display username decryption key has not been sent.'); @@ -157,12 +111,8 @@ if ($_POST !== []) { if (!in_array($_SERVER['HTTP_SEC_FETCH_SITE'], ['none', 'same-origin'], true)) output(403, 'The Sec-Fetch-Site HTTP header must be same-origin or none 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 (PAGE_METADATA['require-login'] ?? true AND !isset($_SESSION['id'])) + output(403, _('You need to be logged in to do this.')); if (file_exists(ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php')) require ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php'; diff --git a/sftpgo-auth.php b/sftpgo-auth.php index b52e7bf..af63da3 100644 --- a/sftpgo-auth.php +++ b/sftpgo-auth.php @@ -3,7 +3,7 @@ const DEBUG = false; !DEBUG or ob_start(); -require 'router.php'; +require 'init.php'; function deny($reason) { !DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents() . $reason . LF);