Merge pull request 'dev' (#8) from dev into main

Reviewed-on: https://code.antopie.org/servnest/servnest/pulls/8
This commit is contained in:
Miraty 2023-06-19 03:51:58 +02:00
commit f5aee06ff5
41 changed files with 645 additions and 241 deletions

View file

@ -2,6 +2,9 @@
## Program flow ## Program flow
`init.php`
: Initializes common values
`router.php` `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. : 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/` `fn/`
: Functions, grouped by concerned service : Functions, grouped by concerned service
`jobs/`
: CLI scripts
`sftpgo-auth.php` `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. : 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/` `DOCS/`
: Documentation (some important or standard files may be directly in the root) : Documentation (some important or standard files may be directly in the root)

View file

@ -24,7 +24,7 @@ I plan to create and maintain a public stable instance of ServNest, but I haven'
### Name server (`ns`) ### Name server (`ns`)
* Host a zone on the server * Host a zone on the server
* Zone file edition through `<textarea>` * Plain zone file edition
* Dedicated forms to set/unset `A`, `AAAA`, `NS`, `TXT`, `CAA`, `SRV`, `MX`, `SRV`, `SSHFP`, `TLSA`, `CNAME`, `DNAME` and `LOC` records * Dedicated forms to set/unset `A`, `AAAA`, `NS`, `TXT`, `CAA`, `SRV`, `MX`, `SRV`, `SSHFP`, `TLSA`, `CNAME`, `DNAME` and `LOC` records
* Display records or the full zone file * Display records or the full zone file

View file

@ -41,6 +41,14 @@ input[type=password] {
width: 7ch; width: 7ch;
} }
#public-key {
width: 70ch;
}
#key {
width: 65ch;
}
:disabled { :disabled {
cursor: not-allowed; cursor: not-allowed;
} }

View file

@ -36,7 +36,7 @@ h3 {
font-size: 1.1rem; font-size: 1.1rem;
} }
main > *:not(pre, form), form > *:not(textarea), footer { main > *:not(pre, form), footer {
max-width: 40rem; max-width: 40rem;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;

View file

@ -0,0 +1,11 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "ssh-keys" (
"key" TEXT NOT NULL,
"username" TEXT NOT NULL,
"directory" TEXT NOT NULL,
UNIQUE("key", "username", "directory"),
FOREIGN KEY("username") REFERENCES "users"("id")
);
COMMIT;

View file

@ -53,4 +53,11 @@ CREATE TABLE IF NOT EXISTS "sites" (
PRIMARY KEY("address", "type"), PRIMARY KEY("address", "type"),
FOREIGN KEY("username") REFERENCES "users"("id") FOREIGN KEY("username") REFERENCES "users"("id")
); );
CREATE TABLE IF NOT EXISTS "ssh-keys" (
"key" TEXT NOT NULL,
"username" TEXT NOT NULL,
"directory" TEXT NOT NULL,
UNIQUE("key", "username", "directory"),
FOREIGN KEY("username") REFERENCES "users"("id")
);
COMMIT; COMMIT;

View file

@ -87,6 +87,57 @@ function setupDisplayUsername($display_username) {
$_SESSION['display-username-cyphertext'] = $cyphertext; $_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.'), '<em>' . PAGES[$service]['index']['title'] . '</em>'));
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);
exescape([
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);
exescape([
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() { function rateLimit() {
if (PAGE_METADATA['tokens_account_cost'] ?? 0 > 0) if (PAGE_METADATA['tokens_account_cost'] ?? 0 > 0)
rateLimitAccount(PAGE_METADATA['tokens_account_cost']); rateLimitAccount(PAGE_METADATA['tokens_account_cost']);

View file

@ -10,7 +10,15 @@ function output($code, $msg = '', $logs = [''], $data = []) {
4 => '<p><output>' . _('<strong>User error</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF, 4 => '<p><output>' . _('<strong>User error</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF,
5 => '<p><output>' . _('<strong>Server error</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF, 5 => '<p><output>' . _('<strong>Server error</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF,
}; };
if (is_callable('displayPage'))
displayPage(array_merge(['final_message' => $final_message], $data)); displayPage(array_merge(['final_message' => $final_message], $data));
echo $final_message;
exit();
}
function exescape(array $args, array &$output = NULL, int &$result_code = NULL): int {
exec('2>&1 ' . implode(' ', array_map('escapeshellarg', $args)), $output, $result_code);
return $result_code;
} }
function insert($table, $values) { function insert($table, $values) {

View file

@ -15,42 +15,53 @@ function parseZoneFile($zone_content, $types, $filter_domain = false) {
return $parsed_zone_content; return $parsed_zone_content;
} }
function knotcConfExec($cmds) { function knotc(array $cmds, array &$output = NULL, int &$return_code = NULL): void {
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-begin', $output['begin'], $code['begin']); exescape([
CONF['dns']['knotc_path'],
'--blocking',
'--timeout',
'3',
'--',
...$cmds,
], $output, $return_code);
}
function knotcConfExec(array $cmds): void {
knotc(['conf-begin'], $output['begin'], $code['begin']);
if ($code['begin'] !== 0) if ($code['begin'] !== 0)
output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['begin'] . '</samp>: <samp>' . $output['begin'][0] . '</samp>.'); output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['begin'] . '</samp>: <samp>' . $output['begin'][0] . '</samp>.');
foreach ($cmds as $cmd) { foreach ($cmds as $cmd) {
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-' . $cmd, $output['op'], $code['op']); knotc($cmd, $output['op'], $code['op']);
if ($code['op'] !== 0) { if ($code['op'] !== 0) {
exec(CONF['dns']['knotc_path'] . ' --blocking conf-abort'); knotc(['conf-abort']);
output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['op'] . '</samp>: <samp>' . $output['op'][0] . '</samp>.'); output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['op'] . '</samp>: <samp>' . $output['op'][0] . '</samp>.');
} }
} }
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-commit', $output['commit'], $code['commit']); knotc(['conf-commit'], $output['commit'], $code['commit']);
if ($code['commit'] !== 0) { if ($code['commit'] !== 0) {
exec(CONF['dns']['knotc_path'] . ' --blocking conf-abort'); knotc(['conf-abort']);
output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['commit'] . '</samp>: <samp>' . $output['commit'][0] . '</samp>.'); output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['commit'] . '</samp>: <samp>' . $output['commit'][0] . '</samp>.');
} }
} }
function knotcZoneExec($zone, $cmd, $action = NULL) { function knotcZoneExec(string $zone, array $cmd, string $action = NULL) {
$action = checkAction($action ?? $_POST['action']); $action = checkAction($action ?? $_POST['action']);
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-begin ' . $zone, $output['begin'], $code['begin']); knotc(['zone-begin', $zone], $output['begin'], $code['begin']);
if ($code['begin'] !== 0) if ($code['begin'] !== 0)
output(500, 'knotcZoneExec: <code>knotc</code> failed with exit code <samp>' . $code['begin'] . '</samp>: <samp>' . $output['begin'][0] . '</samp>.'); output(500, 'knotcZoneExec: <code>knotc</code> failed with exit code <samp>' . $code['begin'] . '</samp>: <samp>' . $output['begin'][0] . '</samp>.');
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-' . $action . 'set ' . $zone . ' ' . implode(' ', $cmd), $output['op'], $code['op']); knotc(['zone-' . $action . 'set', $zone, ...$cmd], $output['op'], $code['op']);
if ($code['op'] !== 0) { if ($code['op'] !== 0) {
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-abort ' . $zone); knotc(['zone-abort', $zone]);
output(500, 'knotcZoneExec: <code>knotc</code> failed with exit code <samp>' . $code['op'] . '</samp>: <samp>' . $output['op'][0] . '</samp>.'); output(500, 'knotcZoneExec: <code>knotc</code> failed with exit code <samp>' . $code['op'] . '</samp>: <samp>' . $output['op'][0] . '</samp>.');
} }
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-commit ' . $zone, $output['commit'], $code['commit']); knotc(['zone-commit', $zone], $output['commit'], $code['commit']);
if ($code['commit'] !== 0) { if ($code['commit'] !== 0) {
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-abort ' . $zone); knotc(['zone-abort', $zone]);
output(500, 'knotcZoneExec: <code>knotc</code> failed with exit code <samp>' . $code['commit'] . '</samp>: <samp>' . $output['commit'][0] . '</samp>.'); output(500, 'knotcZoneExec: <code>knotc</code> failed with exit code <samp>' . $code['commit'] . '</samp>: <samp>' . $output['commit'][0] . '</samp>.');
} }
} }

View file

@ -1,12 +1,23 @@
<?php <?php
const SUBPATH_REGEX = '^[a-z0-9-]{4,63}$';
const ED25519_PUBKEY_REGEX = '^[a-zA-Z0-9/+]{68}$';
function htSetupUserFs($id) { function htSetupUserFs($id) {
// Setup SFTP directory // Setup SFTP directory
if (mkdir(CONF['ht']['ht_path'] . '/fs/' . $id, 0000) !== true) if (mkdir(CONF['ht']['ht_path'] . '/fs/' . $id, 0000) !== true)
output(500, 'Can\'t create user directory.'); output(500, 'Can\'t create user directory.');
if (chmod(CONF['ht']['ht_path'] . '/fs/' . $id, 0775) !== true) if (chmod(CONF['ht']['ht_path'] . '/fs/' . $id, 0775) !== true)
output(500, 'Can\'t chmod user directory.'); output(500, 'Can\'t chmod user directory.');
exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/fs/' . $id . ' --no-dereference', result_code: $code); exescape([
CONF['ht']['sudo_path'],
'--',
CONF['ht']['chgrp_path'],
'--no-dereference',
'--',
CONF['ht']['sftpgo_group'],
CONF['ht']['ht_path'] . '/fs/' . $id,
], result_code: $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Can\'t change user directory group.'); output(500, 'Can\'t change user directory group.');
@ -17,7 +28,16 @@ function htSetupUserFs($id) {
output(500, 'Can\'t chmod Tor config directory.'); output(500, 'Can\'t chmod Tor config directory.');
// Setup Tor keys 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); exescape([
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) if ($code !== 0)
output(500, 'Can\'t create Tor keys directory.'); output(500, 'Can\'t create Tor keys directory.');
} }
@ -35,6 +55,8 @@ function formatDomain($domain) {
} }
function listFsDirs($username) { function listFsDirs($username) {
if ($username === '')
return [];
$absoluteDirs = glob(CONF['ht']['ht_path'] . '/fs/' . $username . '/*/', GLOB_ONLYDIR); $absoluteDirs = glob(CONF['ht']['ht_path'] . '/fs/' . $username . '/*/', GLOB_ONLYDIR);
$dirs = []; $dirs = [];
foreach ($absoluteDirs as $absoluteDir) foreach ($absoluteDirs as $absoluteDir)
@ -74,33 +96,53 @@ function htRelativeSymlink($target, $name) {
output(500, 'Unable to create symlink.'); output(500, 'Unable to create symlink.');
} }
function htDeleteSite($address, $type) { function htDeleteSite($address, $type, $user_id) {
if ($type === 'onion') { if ($type === 'onion') {
$dir = query('select', 'sites', [ $dir = query('select', 'sites', [
'username' => $_SESSION['id'], 'username' => $user_id,
'address' => $address, 'address' => $address,
'type' => $type, 'type' => $type,
], 'site_dir')[0]; ], 'site_dir')[0];
// Delete Tor config // 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.'); output(500, 'Failed to delete Tor configuration.');
// Reload Tor // Reload Tor
exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['tor_reload_cmd'], result_code: $code); exescape([
CONF['ht']['sudo_path'],
'--',
...explode(' ', CONF['ht']['tor_reload_cmd']),
], result_code: $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Failed to reload Tor.'); output(500, 'Failed to reload Tor.');
// Delete Tor keys // 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); exescape([
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) if ($code !== 0)
output(500, 'Failed to delete Tor keys.'); output(500, 'Failed to delete Tor keys.');
} }
if ($type === 'dns') { if ($type === 'dns') {
// Delete Let's Encrypt certificate // Delete Let's Encrypt certificate
exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' delete --quiet --cert-name ' . $address, result_code: $code); exescape([
CONF['ht']['sudo_path'],
CONF['ht']['certbot_path'],
'delete',
'--quiet',
'--cert-name',
$address,
], result_code: $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Certbot failed to delete the Let\'s Encrypt certificate.'); output(500, 'Certbot failed to delete the Let\'s Encrypt certificate.');
} }
@ -115,7 +157,7 @@ function htDeleteSite($address, $type) {
output(500, 'Unable to delete symlink.'); output(500, 'Unable to delete symlink.');
query('delete', 'sites', [ query('delete', 'sites', [
'username' => $_SESSION['id'], 'username' => $user_id,
'type' => $type, 'type' => $type,
'address' => $address, 'address' => $address,
]); ]);

View file

@ -48,22 +48,32 @@ function nsCheckZonePossession($zone) {
output(403, 'You don\'t own this zone on the name server.'); output(403, 'You don\'t own this zone on the name server.');
} }
function nsDeleteZone($zone) { function nsDeleteZone($zone, $user_id) {
// Remove from Knot configuration // Remove from Knot configuration
knotcConfExec(["unset 'zone[$zone]'"]); knotcConfExec([['conf-unset', 'zone[' . $zone . ']']]);
// Remove Knot zone file // Remove Knot zone file
if (unlink(CONF['ns']['knot_zones_path'] . '/' . $zone . 'zone') !== true) if (unlink(CONF['ns']['knot_zones_path'] . '/' . $zone . 'zone') !== true)
output(500, 'Failed to remove Knot zone file.'); output(500, 'Failed to remove Knot zone file.');
// Remove Knot related data // Remove Knot related data
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 --force zone-purge ' . $zone . ' +orphan', result_code: $code); exescape([
CONF['dns']['knotc_path'],
'--blocking',
'--timeout',
'3',
'--force',
'--',
'zone-purge',
$zone,
'+orphan',
], result_code: $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Failed to purge zone data.'); output(500, 'Failed to purge zone data.');
// Remove from database // Remove from database
query('delete', 'zones', [ query('delete', 'zones', [
'zone' => $zone, 'zone' => $zone,
'username' => $_SESSION['id'], 'username' => $user_id,
]); ]);
} }

View file

@ -1,6 +1,6 @@
<?php <?php
const SUBDOMAIN_REGEX = '^[a-z0-9]{4,63}$'; const SUBDOMAIN_REGEX = '^(?!\-)(?!..\-\-)[a-z0-9-]{4,63}(?<!\-)$';
function regListUserDomains() { function regListUserDomains() {
if (isset($_SESSION['id'])) if (isset($_SESSION['id']))
@ -13,7 +13,7 @@ function regCheckDomainPossession($domain) {
output(403, 'You don\'t own this domain on the registry.'); output(403, 'You don\'t own this domain on the registry.');
} }
function regDeleteDomain($domain) { function regDeleteDomain($domain, $user_id) {
// Delete domain from registry file // Delete domain from registry file
$path = CONF['reg']['suffixes_path'] . '/' . regParseDomain($domain)['suffix'] . 'zone'; $path = CONF['reg']['suffixes_path'] . '/' . regParseDomain($domain)['suffix'] . 'zone';
$content = file_get_contents($path); $content = file_get_contents($path);
@ -28,7 +28,7 @@ function regDeleteDomain($domain) {
$conditions = [ $conditions = [
'domain' => $domain, 'domain' => $domain,
'username' => $_SESSION['id'], 'username' => $user_id,
]; ];
insert('registry-history', [ insert('registry-history', [

48
init.php Normal file
View file

@ -0,0 +1,48 @@
<?php
umask(0077);
set_error_handler(function ($level, $message, $file = '', $line = 0) {
throw new ErrorException($message, 0, $level, $file, $line);
});
set_exception_handler(function ($e) {
error_log($e);
http_response_code(500);
echo '<h1>Error</h1>An 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';

View file

@ -1,7 +1,6 @@
<?php <?php // Test that the current setup is working
umask(0077);
const ROOT_PATH = __DIR__; require 'init.php';
define('CONF', parse_ini_file(ROOT_PATH . '/config.ini', true, INI_SCANNER_TYPED));
const SFTP = '/usr/bin/sftp'; const SFTP = '/usr/bin/sftp';
const SSHPASS = '/usr/bin/sshpass'; const SSHPASS = '/usr/bin/sshpass';
@ -13,16 +12,10 @@ const SUFFIX = 'test.servnest.test.';
const TOR_PROXY = 'socks5h://127.0.0.1:9050'; const TOR_PROXY = 'socks5h://127.0.0.1:9050';
const LF = "\n"; exescape([CONF['dns']['kdig_path'], 'torproject.org', 'AAAA'], $output, $return_code);
exec(CONF['dns']['kdig_path'] . ' torproject.org AAAA', $output, $return_code);
if (preg_match('/^;; Flags: qr rd ra ad;/Dm', implode("\n", $output)) !== 1) if (preg_match('/^;; Flags: qr rd ra ad;/Dm', implode("\n", $output)) !== 1)
exit('Unable to do a DNSSEC-validated DNS query.' . LF); exit('Unable to do a DNSSEC-validated DNS query.' . LF);
if (CONF['common']['services']['ns'] === 'rest') {
echo 'a';
}
define('COOKIE_FILE', sys_get_temp_dir() . '/cookie-' . bin2hex(random_bytes(16)) . '.txt'); define('COOKIE_FILE', sys_get_temp_dir() . '/cookie-' . bin2hex(random_bytes(16)) . '.txt');
function curlTest($address, $post = [], $tor = false) { function curlTest($address, $post = [], $tor = false) {
@ -108,7 +101,12 @@ function testReg() {
'domain' => $domain, 'domain' => $domain,
'ns' => 'ns1.servnest.invalid.', 'ns' => 'ns1.servnest.invalid.',
]); ]);
exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $domain . ' NS', $output); exescape([
CONF['dns']['kdig_path'],
'@' . CONF['reg']['address'],
$domain,
'NS',
], $output);
if (preg_match('/[ \t]+ns1\.servnest\.invalid\.$/Dm', implode(LF, $output)) !== 1) if (preg_match('/[ \t]+ns1\.servnest\.invalid\.$/Dm', implode(LF, $output)) !== 1)
exit('Error: /reg/ns: NS record not set' . LF); exit('Error: /reg/ns: NS record not set' . LF);
@ -156,7 +154,12 @@ function testNs($domain) {
'tag' => 'issue', 'tag' => 'issue',
'value' => 'letsencrypt.org', 'value' => 'letsencrypt.org',
]); ]);
exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $domain . ' CAA', $output); exescape([
CONF['dns']['kdig_path'],
'@' . CONF['reg']['address'],
$domain,
'CAA',
], $output);
if (preg_match('/^' . preg_quote($domain, '/') . '[ \t]+7200[ \t]+IN[ \t]+CAA[ \t]+0[ \t]+issue[ \t]+"letsencrypt\.org"$/Dm', implode(LF, $output)) !== 1) if (preg_match('/^' . preg_quote($domain, '/') . '[ \t]+7200[ \t]+IN[ \t]+CAA[ \t]+0[ \t]+issue[ \t]+"letsencrypt\.org"$/Dm', implode(LF, $output)) !== 1)
exit('Error: /ns/caa: CAA record not set' . LF); exit('Error: /ns/caa: CAA record not set' . LF);
@ -165,15 +168,18 @@ function testNs($domain) {
'zone-content' => 'aaaa.' . $domain . ' 3600 AAAA ' . CONF['ht']['ipv6_address'] . "\r\n" 'zone-content' => 'aaaa.' . $domain . ' 3600 AAAA ' . CONF['ht']['ipv6_address'] . "\r\n"
. '@ 86400 NS ' . CONF['ns']['servers'][0] . "\r\n", . '@ 86400 NS ' . CONF['ns']['servers'][0] . "\r\n",
]); ]);
exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' aaaa.' . $domain . ' AAAA', $output); exescape([
CONF['dns']['kdig_path'],
'@' . CONF['reg']['address'],
'aaaa.' . $domain,
'AAAA',
], $output);
if (preg_match('/[ \t]+' . preg_quote(CONF['ht']['ipv6_address'], '/') . '$/Dm', implode(LF, $output)) !== 1) if (preg_match('/[ \t]+' . preg_quote(CONF['ht']['ipv6_address'], '/') . '$/Dm', implode(LF, $output)) !== 1)
exit('Error: /ns/edit: AAAA record not set' . LF); exit('Error: /ns/edit: AAAA record not set' . LF);
} }
function testHt($username, $password) { function testHt($username, $password) {
curlTest('/ht/', []); define('TEST_CONTENT', 'test-' . bin2hex(random_bytes(16)));
define('TEST_CONTENT', 'test-' . random_bytes(4));
file_put_contents(sys_get_temp_dir() . '/index.html', TEST_CONTENT); file_put_contents(sys_get_temp_dir() . '/index.html', TEST_CONTENT);

View file

@ -0,0 +1,10 @@
<?php
require 'init.php';
const MAX_TESTING_ACCOUNT_AGE = 86400 * 10;
foreach (query('select', 'users', ['type' => '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']);
}

View file

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 23:15+0200\n" "POT-Creation-Date: 2023-06-15 01:33+0200\n"
"Language: fr\n" "Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -274,17 +274,33 @@ msgstr "Supprimer un accès"
msgid "Delete an existing HTTP access from a subdirectory of the SFTP space" 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" msgstr "Retirer un accès HTTP existant d'un sous-dossier de l'espace SFTP"
#: router.php:137 view.php:39 #: pages.php:197
msgid "Manage SSH keys"
msgstr "Gérer les clés SSH"
#: pages.php:198
msgid "Choose what SSH key can edit what directory"
msgstr "Choisir quelle clé SSH peut modifier quel dossier"
#: router.php:68
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."
#: router.php:106 view.php:39
msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it." 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." 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 #: router.php:115
msgid "You need to be logged in to do this." msgid "You need to be logged in to do this."
msgstr "Vous devez être connecté·e à un compte pour faire cela." msgstr "Vous devez être connecté·e à un compte pour faire cela."
#: router.php:149 #: view.php:19
msgid "This account doesn't exist anymore. Log out to end this ghost session." msgid "You are using a testing account. It may be deleted anytime."
msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme." msgstr "Vous utilisez un compte de test. Il risque d'être supprimé n'importe quand."
#: view.php:19
msgid "Read more"
msgstr "En savoir plus"
#: view.php:21 #: view.php:21
msgid "Anonymous" msgid "Anonymous"
@ -300,11 +316,16 @@ msgstr "Ce formulaire ne sera pas accepté car il faut %sse connecter%s d'abord.
msgid "%sSource code%s available under %s." msgid "%sSource code%s available under %s."
msgstr "%sCode source%s disponible sous %s." msgstr "%sCode source%s disponible sous %s."
#: fn/auth.php:110 #: fn/auth.php:95
#, 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."
#: fn/auth.php:143
msgid "Account rate limit reached, try again later." msgid "Account rate limit reached, try again later."
msgstr "Limite de taux pour ce compte atteinte, réessayez plus tard." msgstr "Limite de taux pour ce compte atteinte, réessayez plus tard."
#: fn/auth.php:135 #: fn/auth.php:168
msgid "Global rate limit reached, try again later." msgid "Global rate limit reached, try again later."
msgstr "Limite de taux globale atteinte, réessayez plus tard." msgstr "Limite de taux globale atteinte, réessayez plus tard."
@ -320,15 +341,15 @@ msgstr "<strong>Erreur de l'utilisataire</strong>&nbsp;: "
msgid "<strong>Server error</strong>: " msgid "<strong>Server error</strong>: "
msgstr "<strong>Erreur du serveur</strong>&nbsp;: " msgstr "<strong>Erreur du serveur</strong>&nbsp;: "
#: fn/common.php:129 #: fn/common.php:132
msgid "Wrong proof." msgid "Wrong proof."
msgstr "Preuve incorrecte." msgstr "Preuve incorrecte."
#: fn/dns.php:63 #: fn/dns.php:64
msgid "IP address malformed." msgid "IP address malformed."
msgstr "Adresse IP malformée." msgstr "Adresse IP malformée."
#: fn/dns.php:68 fn/ht.php:28 #: fn/dns.php:69 fn/ht.php:31
msgid "Domain malformed." msgid "Domain malformed."
msgstr "Domaine malformé." msgstr "Domaine malformé."
@ -388,11 +409,6 @@ msgid "Account deletion must be confirmed."
msgstr "La suppression du compte doit être confirmée." msgstr "La suppression du compte doit être confirmée."
#: pg-act/auth/unregister.php:13 #: pg-act/auth/unregister.php:13
#, 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:42
msgid "Account deleted." msgid "Account deleted."
msgstr "Compte supprimé." msgstr "Compte supprimé."
@ -445,6 +461,18 @@ msgstr "Ce chemin est déjà pris sur ce service. Utilisez-en un autre."
msgid "Access removed." msgid "Access removed."
msgstr "Accès retiré." msgstr "Accès retiré."
#: pg-act/ht/keys.php:13
msgid "Path is not valid."
msgstr "Le chemin n'est pas valide."
#: pg-act/ht/keys.php:15
msgid "Ed25519 public key seems wrongly formatted."
msgstr "La clé public Ed25519 semble mal formattée."
#: pg-act/ht/keys.php:39
msgid "SSH keys updated."
msgstr "Clés SSH mises à jour."
#: pg-act/ns/caa.php:25 pg-act/ns/cname.php:16 pg-act/ns/dname.php:16 #: pg-act/ns/caa.php:25 pg-act/ns/cname.php:16 pg-act/ns/dname.php:16
#: pg-act/ns/ip.php:16 pg-act/ns/loc.php:72 pg-act/ns/mx.php:20 #: pg-act/ns/ip.php:16 pg-act/ns/loc.php:72 pg-act/ns/mx.php:20
#: pg-act/ns/ns.php:16 pg-act/ns/srv.php:28 pg-act/ns/sshfp.php:25 #: pg-act/ns/ns.php:16 pg-act/ns/srv.php:28 pg-act/ns/sshfp.php:25
@ -664,28 +692,38 @@ msgstr "Le domaine doit avoir les enregistrements suivants pendant le traitement
#: pg-view/ht/add-dns.php:29 pg-view/ns/form.ns.php:8 pg-view/ns/print.php:32 #: pg-view/ht/add-dns.php:29 pg-view/ns/form.ns.php:8 pg-view/ns/print.php:32
#: pg-view/ns/zone-add.php:6 pg-view/reg/ds.php:8 pg-view/reg/glue.php:8 #: pg-view/ns/zone-add.php:6 pg-view/reg/ds.php:8 pg-view/reg/glue.php:8
#: pg-view/reg/glue.php:15 pg-view/reg/ns.php:8 pg-view/reg/print.php:2 #: pg-view/reg/glue.php:15 pg-view/reg/ns.php:8 pg-view/reg/print.php:2
#: pg-view/reg/print.php:16 pg-view/reg/register.php:7 #: pg-view/reg/print.php:16 pg-view/reg/register.php:11
#: pg-view/reg/unregister.php:6 #: pg-view/reg/unregister.php:6
msgid "Domain" msgid "Domain"
msgstr "Domaine" msgstr "Domaine"
#: pg-view/ht/add-dns.php:31 pg-view/ht/add-onion.php:2 #: pg-view/ht/add-dns.php:31 pg-view/ht/add-onion.php:2
#: pg-view/ht/add-subdomain.php:4 pg-view/ht/add-subpath.php:4 #: pg-view/ht/add-subdomain.php:8 pg-view/ht/add-subpath.php:8
msgid "Target directory" msgid "Target directory"
msgstr "Dossier ciblé" msgstr "Dossier ciblé"
#: pg-view/ht/add-dns.php:40 pg-view/ht/add-onion.php:11 #: pg-view/ht/add-dns.php:40 pg-view/ht/add-onion.php:11
#: pg-view/ht/add-subdomain.php:13 pg-view/ht/add-subpath.php:13 #: pg-view/ht/add-subdomain.php:17 pg-view/ht/add-subpath.php:17
msgid "Setup access" msgid "Setup access"
msgstr "Créer l'accès" msgstr "Créer l'accès"
#: pg-view/ht/add-subdomain.php:2 pg-view/ns/form.ns.php:10 #: pg-view/ht/add-subdomain.php:2 pg-view/reg/register.php:6
#: pg-view/reg/glue.php:10 pg-view/reg/register.php:9 #, php-format
msgid "The subdomain can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters. It can't have an hyphen (%3$s) in first, last or both third and fourth position."
msgstr "Le sous-domain peut uniquement contenir %1$s, %2$s et %3$s, et doit être entre 4 et 63 caractères. Il ne peut pas avoir un tiret (%3$s) en première, dernière ou à la fois troisième et quatrième position."
#: pg-view/ht/add-subdomain.php:6 pg-view/ns/form.ns.php:10
#: pg-view/reg/glue.php:10 pg-view/reg/register.php:13
#: pg-view/reg/transfer.php:9 #: pg-view/reg/transfer.php:9
msgid "Subdomain" msgid "Subdomain"
msgstr "Sous-domaine" msgstr "Sous-domaine"
#: pg-view/ht/add-subpath.php:2 #: pg-view/ht/add-subpath.php:2
#, php-format
msgid "The path can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters."
msgstr "Le chemin peut uniquement contenir %1$s, %2$s et %3$s, et doit être entre 4 et 63 caractères."
#: pg-view/ht/add-subpath.php:6
msgid "Path" msgid "Path"
msgstr "Chemin" msgstr "Chemin"
@ -795,6 +833,30 @@ msgstr "Approuvé"
msgid "Stable Let's Encrypt certificates" msgid "Stable Let's Encrypt certificates"
msgstr "Vrai certificat Let's Encrypt" msgstr "Vrai certificat Let's Encrypt"
#: pg-view/ht/keys.php:2
msgid "In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content."
msgstr "En plus de la clé de passe, c'est également possible d'accéder à l'espace SFTP en utilisant des clés SSH Ed25519. Une clé peut être autorisée à modifier dans tout l'espace (<code>/</code>) ou dans un quelconque sous-dossier spécifique. Une clé est toujours autorisée à lister le contenu de n'importe quel dossier."
#: pg-view/ht/keys.php:17
msgid "Add new SSH key access"
msgstr "Ajouter un nouvel accès par clé SSH"
#: pg-view/ht/keys.php:17
msgid "SSH key access"
msgstr "Accès par clé SSH"
#: pg-view/ht/keys.php:19
msgid "Public key"
msgstr "Clé publique"
#: pg-view/ht/keys.php:23
msgid "Allowed directory"
msgstr "Dossier autorisé"
#: pg-view/ht/keys.php:30
msgid "Update"
msgstr "Mettre à jour"
#: pg-view/ns/caa.php:3 #: pg-view/ns/caa.php:3
msgid "Flag" msgid "Flag"
msgstr "Flag" msgstr "Flag"
@ -1118,18 +1180,18 @@ msgid "Nobody can register a domain under these suffixes:"
msgstr "Personne ne peut enregistrer un domain sous ces suffixes&nbsp;:" msgstr "Personne ne peut enregistrer un domain sous ces suffixes&nbsp;:"
#: pg-view/reg/register.php:2 #: pg-view/reg/register.php:2
msgid "Register a new domain on your account. It must consist of between 4 and 63 letters and digits." msgid "Register a new domain on your account."
msgstr "Enregistrer un nouveau domaine sur son compte. Il doit être composé d'entre 4 et 63 lettres et chiffres." msgstr "Enregistrer un nouveau domaine sur son compte."
#: pg-view/reg/register.php:14 pg-view/reg/transfer.php:14 #: pg-view/reg/register.php:18 pg-view/reg/transfer.php:14
msgid "Suffix" msgid "Suffix"
msgstr "Suffixe" msgstr "Suffixe"
#: pg-view/reg/register.php:27 #: pg-view/reg/register.php:31
msgid "Check availability" msgid "Check availability"
msgstr "Vérifier sa disponibilité" msgstr "Vérifier sa disponibilité"
#: pg-view/reg/register.php:29 #: pg-view/reg/register.php:33
msgid "Register" msgid "Register"
msgstr "Enregistrer" msgstr "Enregistrer"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 23:15+0200\n" "POT-Creation-Date: 2023-06-15 01:33+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -286,16 +286,32 @@ msgstr ""
msgid "Delete an existing HTTP access from a subdirectory of the SFTP space" msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
msgstr "" msgstr ""
#: router.php:137 view.php:39 #: pages.php:197
msgid "Manage SSH keys"
msgstr ""
#: pages.php:198
msgid "Choose what SSH key can edit what directory"
msgstr ""
#: router.php:68
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr ""
#: router.php:106 view.php:39
msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it." msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
msgstr "" msgstr ""
#: router.php:147 #: router.php:115
msgid "You need to be logged in to do this." msgid "You need to be logged in to do this."
msgstr "" msgstr ""
#: router.php:149 #: view.php:19
msgid "This account doesn't exist anymore. Log out to end this ghost session." msgid "You are using a testing account. It may be deleted anytime."
msgstr ""
#: view.php:19
msgid "Read more"
msgstr "" msgstr ""
#: view.php:21 #: view.php:21
@ -312,11 +328,16 @@ msgstr ""
msgid "%sSource code%s available under %s." msgid "%sSource code%s available under %s."
msgstr "" msgstr ""
#: fn/auth.php:110 #: fn/auth.php:95
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr ""
#: fn/auth.php:143
msgid "Account rate limit reached, try again later." msgid "Account rate limit reached, try again later."
msgstr "" msgstr ""
#: fn/auth.php:135 #: fn/auth.php:168
msgid "Global rate limit reached, try again later." msgid "Global rate limit reached, try again later."
msgstr "" msgstr ""
@ -332,15 +353,15 @@ msgstr ""
msgid "<strong>Server error</strong>: " msgid "<strong>Server error</strong>: "
msgstr "" msgstr ""
#: fn/common.php:129 #: fn/common.php:132
msgid "Wrong proof." msgid "Wrong proof."
msgstr "" msgstr ""
#: fn/dns.php:63 #: fn/dns.php:64
msgid "IP address malformed." msgid "IP address malformed."
msgstr "" msgstr ""
#: fn/dns.php:68 fn/ht.php:28 #: fn/dns.php:69 fn/ht.php:31
msgid "Domain malformed." msgid "Domain malformed."
msgstr "" msgstr ""
@ -400,11 +421,6 @@ msgid "Account deletion must be confirmed."
msgstr "" msgstr ""
#: pg-act/auth/unregister.php:13 #: pg-act/auth/unregister.php:13
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr ""
#: pg-act/auth/unregister.php:42
msgid "Account deleted." msgid "Account deleted."
msgstr "" msgstr ""
@ -457,6 +473,18 @@ msgstr ""
msgid "Access removed." msgid "Access removed."
msgstr "" msgstr ""
#: pg-act/ht/keys.php:13
msgid "Path is not valid."
msgstr ""
#: pg-act/ht/keys.php:15
msgid "Ed25519 public key seems wrongly formatted."
msgstr ""
#: pg-act/ht/keys.php:39
msgid "SSH keys updated."
msgstr ""
#: pg-act/ns/caa.php:25 pg-act/ns/cname.php:16 pg-act/ns/dname.php:16 #: pg-act/ns/caa.php:25 pg-act/ns/cname.php:16 pg-act/ns/dname.php:16
#: pg-act/ns/ip.php:16 pg-act/ns/loc.php:72 pg-act/ns/mx.php:20 #: pg-act/ns/ip.php:16 pg-act/ns/loc.php:72 pg-act/ns/mx.php:20
#: pg-act/ns/ns.php:16 pg-act/ns/srv.php:28 pg-act/ns/sshfp.php:25 #: pg-act/ns/ns.php:16 pg-act/ns/srv.php:28 pg-act/ns/sshfp.php:25
@ -676,28 +704,38 @@ msgstr ""
#: pg-view/ht/add-dns.php:29 pg-view/ns/form.ns.php:8 pg-view/ns/print.php:32 #: pg-view/ht/add-dns.php:29 pg-view/ns/form.ns.php:8 pg-view/ns/print.php:32
#: pg-view/ns/zone-add.php:6 pg-view/reg/ds.php:8 pg-view/reg/glue.php:8 #: pg-view/ns/zone-add.php:6 pg-view/reg/ds.php:8 pg-view/reg/glue.php:8
#: pg-view/reg/glue.php:15 pg-view/reg/ns.php:8 pg-view/reg/print.php:2 #: pg-view/reg/glue.php:15 pg-view/reg/ns.php:8 pg-view/reg/print.php:2
#: pg-view/reg/print.php:16 pg-view/reg/register.php:7 #: pg-view/reg/print.php:16 pg-view/reg/register.php:11
#: pg-view/reg/unregister.php:6 #: pg-view/reg/unregister.php:6
msgid "Domain" msgid "Domain"
msgstr "" msgstr ""
#: pg-view/ht/add-dns.php:31 pg-view/ht/add-onion.php:2 #: pg-view/ht/add-dns.php:31 pg-view/ht/add-onion.php:2
#: pg-view/ht/add-subdomain.php:4 pg-view/ht/add-subpath.php:4 #: pg-view/ht/add-subdomain.php:8 pg-view/ht/add-subpath.php:8
msgid "Target directory" msgid "Target directory"
msgstr "" msgstr ""
#: pg-view/ht/add-dns.php:40 pg-view/ht/add-onion.php:11 #: pg-view/ht/add-dns.php:40 pg-view/ht/add-onion.php:11
#: pg-view/ht/add-subdomain.php:13 pg-view/ht/add-subpath.php:13 #: pg-view/ht/add-subdomain.php:17 pg-view/ht/add-subpath.php:17
msgid "Setup access" msgid "Setup access"
msgstr "" msgstr ""
#: pg-view/ht/add-subdomain.php:2 pg-view/ns/form.ns.php:10 #: pg-view/ht/add-subdomain.php:2 pg-view/reg/register.php:6
#: pg-view/reg/glue.php:10 pg-view/reg/register.php:9 #, php-format
msgid "The subdomain can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters. It can't have an hyphen (%3$s) in first, last or both third and fourth position."
msgstr ""
#: pg-view/ht/add-subdomain.php:6 pg-view/ns/form.ns.php:10
#: pg-view/reg/glue.php:10 pg-view/reg/register.php:13
#: pg-view/reg/transfer.php:9 #: pg-view/reg/transfer.php:9
msgid "Subdomain" msgid "Subdomain"
msgstr "" msgstr ""
#: pg-view/ht/add-subpath.php:2 #: pg-view/ht/add-subpath.php:2
#, php-format
msgid "The path can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters."
msgstr ""
#: pg-view/ht/add-subpath.php:6
msgid "Path" msgid "Path"
msgstr "" msgstr ""
@ -807,6 +845,30 @@ msgstr ""
msgid "Stable Let's Encrypt certificates" msgid "Stable Let's Encrypt certificates"
msgstr "" msgstr ""
#: pg-view/ht/keys.php:2
msgid "In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content."
msgstr ""
#: pg-view/ht/keys.php:17
msgid "Add new SSH key access"
msgstr ""
#: pg-view/ht/keys.php:17
msgid "SSH key access"
msgstr ""
#: pg-view/ht/keys.php:19
msgid "Public key"
msgstr ""
#: pg-view/ht/keys.php:23
msgid "Allowed directory"
msgstr ""
#: pg-view/ht/keys.php:30
msgid "Update"
msgstr ""
#: pg-view/ns/caa.php:3 #: pg-view/ns/caa.php:3
msgid "Flag" msgid "Flag"
msgstr "" msgstr ""
@ -1130,18 +1192,18 @@ msgid "Nobody can register a domain under these suffixes:"
msgstr "" msgstr ""
#: pg-view/reg/register.php:2 #: pg-view/reg/register.php:2
msgid "Register a new domain on your account. It must consist of between 4 and 63 letters and digits." msgid "Register a new domain on your account."
msgstr "" msgstr ""
#: pg-view/reg/register.php:14 pg-view/reg/transfer.php:14 #: pg-view/reg/register.php:18 pg-view/reg/transfer.php:14
msgid "Suffix" msgid "Suffix"
msgstr "" msgstr ""
#: pg-view/reg/register.php:27 #: pg-view/reg/register.php:31
msgid "Check availability" msgid "Check availability"
msgstr "" msgstr ""
#: pg-view/reg/register.php:29 #: pg-view/reg/register.php:33
msgid "Register" msgid "Register"
msgstr "" msgstr ""

View file

@ -193,5 +193,10 @@ define('PAGES', [
'title' => _('Delete access'), 'title' => _('Delete access'),
'description' => _('Delete an existing HTTP access from a subdirectory of the SFTP space'), 'description' => _('Delete an existing HTTP access from a subdirectory of the SFTP space'),
], ],
'keys' => [
'title' => _('Manage SSH keys'),
'description' => _('Choose what SSH key can edit what directory'),
'tokens_account_cost' => 300,
],
], ],
]); ]);

View file

@ -26,4 +26,3 @@ $_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
setupDisplayUsername($_POST['username']); setupDisplayUsername($_POST['username']);
redir(); redir();

View file

@ -6,36 +6,7 @@ if (checkPassword($_SESSION['id'], $_POST['current-password']) !== true)
if (!isset($_POST['delete'])) if (!isset($_POST['delete']))
output(403, _('Account deletion must be confirmed.')); output(403, _('Account deletion must be confirmed.'));
$user_services = explode(',', query('select', 'users', ['id' => $_SESSION['id']], 'services')[0]); authDeleteUser($_SESSION['id']);
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']);
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']]);
logout(); logout();

View file

@ -30,7 +30,14 @@ checkAuthToken($matches[1], $matches[2]);
rateLimit(); rateLimit();
exec('2>&1 ' . CONF['ht']['sudo_path'] . ' ' . CONF['ht']['certbot_path'] . ' certonly' . (($_SESSION['type'] === 'approved') ? '' : ' --test-cert') . ' --domain ' . $_POST['domain'], $output, $returnCode); exescape([
CONF['ht']['sudo_path'],
CONF['ht']['certbot_path'],
'certonly',
'--domain',
$_POST['domain'],
...(($_SESSION['type'] === 'approved') ? [] : ['--test-cert']),
], $output, $returnCode);
if ($returnCode !== 0) if ($returnCode !== 0)
output(500, 'Certbot failed to get a Let\'s Encrypt certificate.', $output); output(500, 'Certbot failed to get a Let\'s Encrypt certificate.', $output);

View file

@ -15,16 +15,29 @@ if (chmod($torConfFile, 0644) !== true)
output(500, 'Failed to give correct permissions to new Tor configuration file.'); output(500, 'Failed to give correct permissions to new Tor configuration file.');
// Reload Tor // Reload Tor
exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['tor_reload_cmd'], result_code: $code); exescape([
CONF['ht']['sudo_path'],
'--',
...explode(' ', CONF['ht']['tor_reload_cmd']),
], result_code: $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Failed to reload Tor.'); output(500, 'Failed to reload Tor.');
usleep(10000); usleep(10000);
// Get the hostname generated by Tor // Get the hostname generated by Tor
$onion = exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['cat_path'] . ' ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname', result_code: $code); exescape([
CONF['ht']['sudo_path'],
'-u',
CONF['ht']['tor_user'],
'--',
CONF['ht']['cat_path'],
'--',
CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'] . '/' . $_POST['dir'] . '/hostname',
], $output, $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Unable to read hostname file.'); output(500, 'Unable to read hostname file.');
$onion = $output[0];
if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1) if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1)
output(500, 'No onion address found.'); output(500, 'No onion address found.');

View file

@ -3,7 +3,7 @@
if (dirsStatuses('subdomain')[$_POST['dir']] !== false) if (dirsStatuses('subdomain')[$_POST['dir']] !== false)
output(403, 'Wrong value for <code>dir</code>.'); output(403, 'Wrong value for <code>dir</code>.');
if (preg_match('/^[a-z0-9]{1,32}$/D', $_POST['subdomain']) !== 1) if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['subdomain']) !== 1)
output(403, _('Invalid domain label.')); output(403, _('Invalid domain label.'));
if (query('select', 'sites', ['address' => $_POST['subdomain'], 'type' => 'subdomain']) !== []) if (query('select', 'sites', ['address' => $_POST['subdomain'], 'type' => 'subdomain']) !== [])

View file

@ -3,7 +3,7 @@
if (dirsStatuses('subpath')[$_POST['dir']] !== false) if (dirsStatuses('subpath')[$_POST['dir']] !== false)
output(403, 'Wrong value for <code>dir</code>.'); output(403, 'Wrong value for <code>dir</code>.');
if (preg_match('/^[a-z0-9]{1,32}$/D', $_POST['path']) !== 1) if (preg_match('/' . SUBPATH_REGEX . '/D', $_POST['path']) !== 1)
output(403, _('Invalid path.')); output(403, _('Invalid path.'));
if (query('select', 'sites', ['address' => $_POST['path'], 'type' => 'subpath']) !== []) if (query('select', 'sites', ['address' => $_POST['path'], 'type' => 'subpath']) !== [])

View file

@ -6,6 +6,6 @@ if (preg_match('/^(?<type>subpath|subdomain|onion|dns):(?<address>[a-z0-9._-]{1,
if (isset(query('select', 'sites', ['username' => $_SESSION['id'], 'address' => $site['address'], 'type' => $site['type']], 'address')[0]) !== true) if (isset(query('select', 'sites', ['username' => $_SESSION['id'], 'address' => $site['address'], 'type' => $site['type']], 'address')[0]) !== true)
output(403, 'Unavailable value for <code>site</code>.'); output(403, 'Unavailable value for <code>site</code>.');
htDeleteSite($site['address'], $site['type']); htDeleteSite($site['address'], $site['type'], $_SESSION['id']);
output(200, _('Access removed.')); output(200, _('Access removed.'));

39
pg-act/ht/keys.php Executable file
View file

@ -0,0 +1,39 @@
<?php
$el_nb = count($_POST['keys']);
if ($el_nb < 1 OR $el_nb > 8)
output(403, 'Wrong elements number.');
foreach ($_POST['keys'] as $i => $key) {
if (($key['public-key'] ?? '') === '') {
unset($_POST['keys'][$i]);
continue;
}
if (preg_match('#^/[/\p{L}\{M}\p{N}\p{P}\p{S}\p{Zs}]{1,254}$#Du', $key['dir'] ?? '') !== 1)
output(403, _('Path is not valid.'));
if (preg_match('#' . ED25519_PUBKEY_REGEX . '#D', $key['public-key']) !== 1)
output(403, _('Ed25519 public key seems wrongly formatted.'));
}
$keys = array_values($_POST['keys']);
rateLimit();
try {
DB->beginTransaction();
query('delete', 'ssh-keys', ['username' => $_SESSION['id']]);
foreach ($keys as $key)
insert('ssh-keys', [
'key' => $key['public-key'],
'username' => $_SESSION['id'],
'directory' => $key['dir'],
]);
DB->commit();
} catch (Exception $e) {
DB->rollback();
output(500, 'Database error.', [$e->getMessage()]);
}
output(200, _('SSH keys updated.'));

View file

@ -39,22 +39,22 @@ if (isset($_POST['zone-content'])) { // Update zone
ratelimit(); ratelimit();
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-freeze ' . $_POST['zone'], $output, $return_code); knotc(['zone-freeze', $_POST['zone']], $output, $return_code);
if ($return_code !== 0) if ($return_code !== 0)
output(500, 'Failed to freeze zone file.', $output); output(500, 'Failed to freeze zone file.', $output);
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-flush ' . $_POST['zone'], $output, $return_code); knotc(['zone-flush', $_POST['zone']], $output, $return_code);
if ($return_code !== 0) if ($return_code !== 0)
output(500, 'Failed to flush zone file.', $output); output(500, 'Failed to flush zone file.', $output);
if (file_put_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone', $new_zone_content) === false) if (file_put_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone', $new_zone_content) === false)
output(500, 'Failed to write zone file.'); output(500, 'Failed to write zone file.');
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-reload ' . $_POST['zone'], $output, $return_code); knotc(['zone-reload', $_POST['zone']], $output, $return_code);
if ($return_code !== 0) if ($return_code !== 0)
output(500, 'Failed to reload zone file.', $output); output(500, 'Failed to reload zone file.', $output);
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 zone-thaw ' . $_POST['zone'], $output, $return_code); knotc(['zone-thaw', $_POST['zone']], $output, $return_code);
if ($return_code !== 0) if ($return_code !== 0)
output(500, 'Failed to thaw zone file.', $output); output(500, 'Failed to thaw zone file.', $output);

View file

@ -5,7 +5,13 @@ $_POST['domain'] = formatAbsoluteDomain($_POST['domain']);
if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== []) if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== [])
output(403, _('This zone already exists on the service.')); output(403, _('This zone already exists on the service.'));
exec(CONF['dns']['kdig_path'] . ' ' . ltrim(strstr($_POST['domain'], '.'), '.') . ' NS +short' . (CONF['ns']['local_only_check'] ? (' @' . CONF['reg']['address']) : ''), $parentAuthoritatives, $code); exescape([
CONF['dns']['kdig_path'],
ltrim(strstr($_POST['domain'], '.'), '.'),
'NS',
'+short',
...(CONF['ns']['local_only_check'] ? ['@' . CONF['reg']['address']] : []),
], $parentAuthoritatives, $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Unable to query parent name servers.'); output(500, 'Unable to query parent name servers.');
if ($parentAuthoritatives === []) if ($parentAuthoritatives === [])
@ -13,7 +19,13 @@ if ($parentAuthoritatives === [])
foreach ($parentAuthoritatives as $parentAuthoritative) foreach ($parentAuthoritatives as $parentAuthoritative)
checkAbsoluteDomainFormat($parentAuthoritative); checkAbsoluteDomainFormat($parentAuthoritative);
exec(CONF['dns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . (CONF['ns']['local_only_check'] ? CONF['reg']['address'] : $parentAuthoritatives[0]) . ' +noidn', $results); exescape([
CONF['dns']['kdig_path'],
$_POST['domain'],
'NS',
'@' . (CONF['ns']['local_only_check'] ? CONF['reg']['address'] : $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) 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, _('NS authentication record not found.')); output(403, _('NS authentication record not found.'));
@ -47,8 +59,8 @@ if (chmod($knotZonePath, 0660) !== true)
output(500, 'Failed to chmod new zone file.'); output(500, 'Failed to chmod new zone file.');
knotcConfExec([ knotcConfExec([
"set 'zone[" . $_POST['domain'] . "]'", ['conf-set', 'zone[' . $_POST['domain'] . ']'],
"set 'zone[" . $_POST['domain'] . "].template' 'servnest'", ['conf-set', 'zone[' . $_POST['domain'] . '].template', 'servnest'],
]); ]);
output(200, _('Zone created.')); output(200, _('Zone created.'));

View file

@ -2,6 +2,6 @@
nsCheckZonePossession($_POST['zone']); nsCheckZonePossession($_POST['zone']);
nsDeleteZone($_POST['zone']); nsDeleteZone($_POST['zone'], $_SESSION['id']);
output(200, _('Zone deleted.')); output(200, _('Zone deleted.'));

View file

@ -1,20 +1,18 @@
<?php <?php
if ( if (!in_array($_POST['algo'], ['8', '13', '14', '15', '16'], true))
($_POST['algo'] !== '8') output(403, 'Wrong value for <code>algo</code>.');
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']); $_POST['keytag'] = intval($_POST['keytag']);
if ((!preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535)) if ((preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) !== 1 OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535))
output(403, 'Wrong value for <code>keytag</code>.'); output(403, 'Wrong value for <code>keytag</code>.');
if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4') if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4')
output(403, 'Wrong value for <code>dt</code>.'); output(403, 'Wrong value for <code>dt</code>.');
if (preg_match('/^(?:[0-9a-fA-F]{64}|[0-9a-fA-F]{96})$/D', $_POST['key']) !== 1)
output(403, 'Wrong value for <code>key</code>.');
regCheckDomainPossession($_POST['zone']); regCheckDomainPossession($_POST['zone']);
rateLimit(); rateLimit();

View file

@ -11,7 +11,13 @@ $domain = formatAbsoluteDomain($_POST['subdomain'] . '.' . $_POST['suffix']);
if (query('select', 'registry', ['username' => $_SESSION['id'], 'domain' => $domain], 'domain') !== []) if (query('select', 'registry', ['username' => $_SESSION['id'], 'domain' => $domain], 'domain') !== [])
output(403, _('The current account already owns this domain.')); output(403, _('The current account already owns this domain.'));
exec(CONF['dns']['kdig_path'] . ' ' . $domain . ' NS @' . CONF['reg']['address'] . ' +noidn', $results, $code); exescape([
CONF['dns']['kdig_path'],
$domain,
'NS',
'@' . CONF['reg']['address'],
'+noidn',
], $results, $code);
if ($code !== 0) if ($code !== 0)
output(500, 'Unable to query registry\'s name servers.'); output(500, 'Unable to query registry\'s name servers.');
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) 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)

View file

@ -2,6 +2,6 @@
regCheckDomainPossession($_POST['domain']); regCheckDomainPossession($_POST['domain']);
regDeleteDomain($_POST['domain']); regDeleteDomain($_POST['domain'], $_SESSION['id']);
output(200, _('Domain unregistered.')); output(200, _('Domain unregistered.'));

View file

@ -1,6 +1,6 @@
<?php displayIndex(); ?> <?php displayIndex(); ?>
<h2><?= _('Account type') ?></h2> <h2 id="type"><?= _('Account type') ?></h2>
<p> <p>
<?php <?php

View file

@ -1,3 +1,7 @@
<p>
<?= sprintf(_('The subdomain can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters. It can\'t have an hyphen (%3$s) in first, last or both third and fourth position.'), '<abbr title="abcdefghijklmnopqrstuvwxyz"><code>a</code>-<code>z</code></abbr>', '<abbr title="0123456789"><code>0</code>-<code>9</code></abbr>', '<code>-</code>') ?>
</p>
<form method="post"> <form method="post">
<label for="subdomain"><?= _('Subdomain') ?></label><br> <label for="subdomain"><?= _('Subdomain') ?></label><br>
<input required="" placeholder="label" id="subdomain" name="subdomain" type="text"><code>.<?= CONF['ht']['subdomain_domain'] ?></code><br> <input required="" placeholder="label" id="subdomain" name="subdomain" type="text"><code>.<?= CONF['ht']['subdomain_domain'] ?></code><br>

View file

@ -1,6 +1,10 @@
<p>
<?= sprintf(_('The path can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters.'), '<abbr title="abcdefghijklmnopqrstuvwxyz"><code>a</code>-<code>z</code></abbr>', '<abbr title="0123456789"><code>0</code>-<code>9</code></abbr>', '<code>-</code>') ?>
</p>
<form method="post"> <form method="post">
<label for="path"><?= _('Path') ?></label><br> <label for="path"><?= _('Path') ?></label><br>
<code>https://<?= CONF['ht']['subpath_domain'] ?>/</code><input required="" placeholder="path" id="path" name="path" type="text"><br> <code>https://<?= CONF['ht']['subpath_domain'] ?>/</code><input required="" pattern="<?= SUBPATH_REGEX ?>" placeholder="path" id="path" name="path" type="text"><br>
<label for="dir"><?= _('Target directory') ?></label><br> <label for="dir"><?= _('Target directory') ?></label><br>
<select required="" name="dir" id="dir"> <select required="" name="dir" id="dir">
<option value="" disabled="" selected=""></option> <option value="" disabled="" selected=""></option>

31
pg-view/ht/keys.php Executable file
View file

@ -0,0 +1,31 @@
<p>
<?= _('In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content.') ?>
</p>
<form method="post">
<datalist id="dirs">
<option value="/"></option>
<?php
foreach (listFsDirs($_SESSION['id'] ?? '') as $dir)
echo ' <option value="/' . $dir . '"></option>' . LF;
?>
</datalist>
<?php
foreach (array_slice(array_merge(query('select', 'ssh-keys', ['username' => $_SESSION['id'] ?? '']), [['key' => '', 'username' => '', 'directory' => '/']]), 0, 8) as $i => $ssh_key) {
?>
<fieldset>
<legend><?= ($ssh_key['key'] === '') ? _('Add new SSH key access') : _('SSH key access') ?></legend>
<div>
<label for="public-key"><?= _('Public key') ?></label><br>
<code>ssh-ed15519 <input pattern="<?= ED25519_PUBKEY_REGEX ?>" placeholder="AAAAC3NzaC1lZDI1NTE5AAAAI<?= substr(base64_encode(random_bytes(32)), 0, 43) ?>" id="public-key" name="keys[<?= $i ?>][public-key]" value="<?= $ssh_key['key'] ?>" type="text"></code>
</div>
<div>
<label for="dir"><?= _('Allowed directory') ?></label><br>
<input list="dirs" placeholder="/" value="<?= htmlspecialchars($ssh_key['directory']) ?>" id="dir" name="keys[<?= $i ?>][dir]" type="text">
</div>
</fieldset>
<?php
}
?>
<input type="submit" value="<?= _('Update') ?>">
</form>

View file

@ -51,7 +51,7 @@ foreach (regListUserDomains() as $domain)
<br> <br>
<label for="key"><?= _('Key') ?></label> <label for="key"><?= _('Key') ?></label>
<br> <br>
<input id="key" required="" name="key" type="text" placeholder="018F25E4A022463478C9E30136EC53771A1704A0F0B3CE5B883AC9C8A6A55D16B638B4DE70662ACA5295D3669E7CADD9"> <input id="key" required="" name="key" type="text" pattern="^([0-9a-fA-F]{64}|[0-9a-fA-F]{96})$" placeholder="<?= strtoupper(bin2hex(random_bytes(32))) ?>">
<br> <br>
<input type="submit" value="<?= _('Apply') ?>"> <input type="submit" value="<?= _('Apply') ?>">
</form> </form>

View file

@ -1,5 +1,9 @@
<p> <p>
<?= _('Register a new domain on your account. It must consist of between 4 and 63 letters and digits.') ?> <?= _('Register a new domain on your account.') ?>
</p>
<p>
<?= sprintf(_('The subdomain can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters. It can\'t have an hyphen (%3$s) in first, last or both third and fourth position.'), '<abbr title="abcdefghijklmnopqrstuvwxyz"><code>a</code>-<code>z</code></abbr>', '<abbr title="0123456789"><code>0</code>-<code>9</code></abbr>', '<code>-</code>') ?>
</p> </p>
<form method="post"> <form method="post">

View file

@ -1,40 +1,5 @@
<?php <?php
umask(0077); require 'init.php';
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;
$pageAddress = substr($_SERVER['REQUEST_URI'], strlen(CONF['common']['prefix']) + 1); $pageAddress = substr($_SERVER['REQUEST_URI'], strlen(CONF['common']['prefix']) + 1);
if (strpos($pageAddress, '?') !== false) { if (strpos($pageAddress, '?') !== false) {
@ -99,28 +64,32 @@ if (isset($_COOKIE[SESSION_COOKIE_NAME]))
startSession(); // Resume session startSession(); // Resume session
if (isset($_SESSION['id'])) { 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 // Decrypt display username
if (!isset($_COOKIE['display-username-decryption-key'])) if (!isset($_COOKIE['display-username-decryption-key']))
output(403, 'The display username decryption key has not been sent.'); output(403, 'The display username decryption key has not been sent.');
$decryption_result = htmlspecialchars(sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $decryption_result = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$_SESSION['display-username-cyphertext'], $_SESSION['display-username-cyphertext'],
'', '',
$_SESSION['display-username-nonce'], $_SESSION['display-username-nonce'],
base64_decode($_COOKIE['display-username-decryption-key']) base64_decode($_COOKIE['display-username-decryption-key'])
)); );
if ($decryption_result === false) if ($decryption_result === false)
output(403, 'Unable to decrypt display username.'); output(403, 'Unable to decrypt display username.');
define('DISPLAY_USERNAME', $decryption_result); define('DISPLAY_USERNAME', htmlspecialchars($decryption_result));
// Enable not already enabled services for this user // Enable not already enabled services for this user
$user_services = array_filter(explode(',', query('select', 'users', ['id' => $_SESSION['id']], 'services')[0])); $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') { foreach (SERVICES_USER as $service)
$user_services[] = SERVICE; if (!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') DB->prepare('UPDATE users SET services = :services WHERE id = :id')
->execute([':services' => implode(',', $user_services), ':id' => $_SESSION['id']]); ->execute([':services' => implode(',', $user_services), ':id' => $_SESSION['id']]);
if (SERVICE === 'ht') if ($service === 'ht')
htSetupUserFs($_SESSION['id']); htSetupUserFs($_SESSION['id']);
} }
} }
@ -142,12 +111,8 @@ if ($_POST !== []) {
if (!in_array($_SERVER['HTTP_SEC_FETCH_SITE'], ['none', 'same-origin'], true)) if (!in_array($_SERVER['HTTP_SEC_FETCH_SITE'], ['none', 'same-origin'], true))
output(403, 'The <code>Sec-Fetch-Site</code> HTTP header must be <code>same-origin</code> or <code>none</code> when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).'); output(403, 'The <code>Sec-Fetch-Site</code> HTTP header must be <code>same-origin</code> or <code>none</code> when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
if (PAGE_METADATA['require-login'] ?? true !== false) { if (PAGE_METADATA['require-login'] ?? true AND !isset($_SESSION['id']))
if (isset($_SESSION['id']) !== true)
output(403, _('You need to be logged in to do this.')); 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(ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php')) if (file_exists(ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php'))
require ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php'; require ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php';

View file

@ -1,47 +1,54 @@
<?php <?php // ServNest authenticator for SFTPGo https://github.com/drakkan/sftpgo/blob/main/docs/external-auth.md
const DEBUG = false; const DEBUG = false;
!DEBUG or ob_start(); !DEBUG or ob_start();
require 'router.php'; require 'init.php';
function deny() { function deny($reason) {
!DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents()); !DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents() . $reason . LF);
http_response_code(403); http_response_code(403);
exit(); exit();
} }
if (CONF['common']['services']['ht'] !== 'enabled') if (CONF['common']['services']['ht'] !== 'enabled')
deny(); deny('Service not enabled.');
$auth_data = json_decode(file_get_contents('php://input'), true, flags: JSON_THROW_ON_ERROR); $auth_data = json_decode(file_get_contents('php://input'), true, flags: JSON_THROW_ON_ERROR);
$username = hashUsername($auth_data['username']); $username = hashUsername($auth_data['username']);
if (usernameExists($username) !== true) if (usernameExists($username) !== true)
deny(); deny('This username doesn\'t exist.');
if (!in_array('ht', explode(',', query('select', 'users', ['username' => $username], 'services')[0]), true)) $account = query('select', 'users', ['username' => $username])[0];
deny();
$id = query('select', 'users', ['username' => $username], 'id')[0]; if (!in_array('ht', explode(',', $account['services']), true))
deny('Service not enabled for this user.');
if (checkPassword($id, $auth_data['password']) !== true) const SFTPGO_DENY_PERMS = ['/' => ['list']];
deny(); const SFTPGO_ALLOW_PERMS = ['list', 'download', 'upload', 'overwrite', 'delete_files', 'delete_dirs', 'rename_files', 'rename_dirs', 'create_dirs', 'chtimes'];
if ($auth_data['password'] !== '') {
if (checkPassword($account['id'], $auth_data['password']) !== true)
deny('Wrong password.');
$permissions['/'] = SFTPGO_ALLOW_PERMS;
} else if ($auth_data['public_key'] !== '') {
$permissions = SFTPGO_DENY_PERMS;
foreach (query('select', 'ssh-keys', ['username' => $account['id']]) as $key)
if (hash_equals('ssh-ed25519 ' . $key['key'] . LF, $auth_data['public_key']))
$permissions[$key['directory']] = SFTPGO_ALLOW_PERMS;
if ($permissions === SFTPGO_DENY_PERMS)
deny('No matching SSH key allowed.');
} else
deny('Unknown authentication method.');
echo ' echo json_encode([
{ 'status' => 1,
"status": 1, 'username' => $auth_data['username'],
"username": ' . json_encode($auth_data['username']) . ', 'home_dir' => CONF['ht']['ht_path'] . '/fs/' . $account['id'],
"home_dir": "' . CONF['ht']['ht_path'] . '/fs/' . $id . '", 'quota_size' => ($account['type'] === 'approved') ? CONF['ht']['user_quota_approved'] : CONF['ht']['user_quota_testing'],
"quota_size": ' . ((query('select', 'users', ['id' => $id], 'type')[0] === 'approved') ? CONF['ht']['user_quota_approved'] : CONF['ht']['user_quota_testing']) . ', 'permissions' => $permissions,
"permissions": { ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
"/": [
"*"
]
}
}
';
!DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents()); !DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents() . 'accepted');
http_response_code(200); http_response_code(200);

View file

@ -14,11 +14,11 @@ foreach (glob('css/*.css') as $css_path)
</head> </head>
<body> <body>
<header> <header>
<p> <p class="auth">
<?php if (isset($_SESSION['id'])) { ?> <?php if (isset($_SESSION['id'])) { ?>
<span aria-hidden="true"><?= ($_SESSION['type'] === 'approved') ? '👤' : '⏳' ?> </span><strong><?= (defined('DISPLAY_USERNAME') ? DISPLAY_USERNAME : '<em>?</em>') ?></strong> <a class="auth" href="<?= CONF['common']['prefix'] ?>/auth/logout"><?= _('Log out') ?></a> <span aria-hidden="true"><?= ($_SESSION['type'] === 'approved') ? '👤' : '⏳' ?> </span><strong><?= (defined('DISPLAY_USERNAME') ? DISPLAY_USERNAME : '<em>?</em>') ?></strong> <a href="<?= CONF['common']['prefix'] ?>/auth/logout"><?= _('Log out') ?></a><?= ($_SESSION['type'] === 'testing') ? '<br>' . _('You are using a testing account. It may be deleted anytime.') . ' <a href="' . CONF['common']['prefix'] . '/auth/#type">' . _('Read more') . '</a>' : '' ?>
<?php } else { ?> <?php } else { ?>
<span aria-hidden="true">👻 </span><em><?= _('Anonymous') ?></em> <a class="auth" href="<?= redirUrl('auth/login') ?>"><?= _('Log in') ?></a> <span aria-hidden="true">👻 </span><em><?= _('Anonymous') ?></em> <a href="<?= redirUrl('auth/login') ?>"><?= _('Log in') ?></a>
<?php } ?> <?php } ?>
</p> </p>
<nav> <nav>