Merge pull request 'dev' (#8) from dev into main
Reviewed-on: https://code.antopie.org/servnest/servnest/pulls/8
This commit is contained in:
commit
f5aee06ff5
41 changed files with 645 additions and 241 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ I plan to create and maintain a public stable instance of ServNest, but I haven'
|
|||
### Name server (`ns`)
|
||||
|
||||
* 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
|
||||
* Display records or the full zone file
|
||||
|
||||
|
|
|
@ -41,6 +41,14 @@ input[type=password] {
|
|||
width: 7ch;
|
||||
}
|
||||
|
||||
#public-key {
|
||||
width: 70ch;
|
||||
}
|
||||
|
||||
#key {
|
||||
width: 65ch;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ h3 {
|
|||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
main > *:not(pre, form), form > *:not(textarea), footer {
|
||||
main > *:not(pre, form), footer {
|
||||
max-width: 40rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
|
11
db/migrations/005-create-ssh-keys-table.sql
Normal file
11
db/migrations/005-create-ssh-keys-table.sql
Normal 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;
|
|
@ -53,4 +53,11 @@ CREATE TABLE IF NOT EXISTS "sites" (
|
|||
PRIMARY KEY("address", "type"),
|
||||
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;
|
||||
|
|
51
fn/auth.php
51
fn/auth.php
|
@ -87,6 +87,57 @@ 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.'), '<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() {
|
||||
if (PAGE_METADATA['tokens_account_cost'] ?? 0 > 0)
|
||||
rateLimitAccount(PAGE_METADATA['tokens_account_cost']);
|
||||
|
|
|
@ -10,7 +10,15 @@ function output($code, $msg = '', $logs = [''], $data = []) {
|
|||
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,
|
||||
};
|
||||
if (is_callable('displayPage'))
|
||||
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) {
|
||||
|
|
35
fn/dns.php
35
fn/dns.php
|
@ -15,42 +15,53 @@ function parseZoneFile($zone_content, $types, $filter_domain = false) {
|
|||
return $parsed_zone_content;
|
||||
}
|
||||
|
||||
function knotcConfExec($cmds) {
|
||||
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 3 conf-begin', $output['begin'], $code['begin']);
|
||||
function knotc(array $cmds, array &$output = NULL, int &$return_code = NULL): void {
|
||||
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)
|
||||
output(500, 'knotcConfExec: <code>knotc</code> failed with exit code <samp>' . $code['begin'] . '</samp>: <samp>' . $output['begin'][0] . '</samp>.');
|
||||
|
||||
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) {
|
||||
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>.');
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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>.');
|
||||
}
|
||||
}
|
||||
|
||||
function knotcZoneExec($zone, $cmd, $action = NULL) {
|
||||
function knotcZoneExec(string $zone, array $cmd, string $action = NULL) {
|
||||
$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)
|
||||
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) {
|
||||
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>.');
|
||||
}
|
||||
|
||||
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) {
|
||||
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>.');
|
||||
}
|
||||
}
|
||||
|
|
60
fn/ht.php
60
fn/ht.php
|
@ -1,12 +1,23 @@
|
|||
<?php
|
||||
|
||||
const SUBPATH_REGEX = '^[a-z0-9-]{4,63}$';
|
||||
const ED25519_PUBKEY_REGEX = '^[a-zA-Z0-9/+]{68}$';
|
||||
|
||||
function htSetupUserFs($id) {
|
||||
// Setup SFTP directory
|
||||
if (mkdir(CONF['ht']['ht_path'] . '/fs/' . $id, 0000) !== true)
|
||||
output(500, 'Can\'t create user directory.');
|
||||
if (chmod(CONF['ht']['ht_path'] . '/fs/' . $id, 0775) !== true)
|
||||
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)
|
||||
output(500, 'Can\'t change user directory group.');
|
||||
|
||||
|
@ -17,7 +28,16 @@ function htSetupUserFs($id) {
|
|||
output(500, 'Can\'t chmod 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);
|
||||
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)
|
||||
output(500, 'Can\'t create Tor keys directory.');
|
||||
}
|
||||
|
@ -35,6 +55,8 @@ function formatDomain($domain) {
|
|||
}
|
||||
|
||||
function listFsDirs($username) {
|
||||
if ($username === '')
|
||||
return [];
|
||||
$absoluteDirs = glob(CONF['ht']['ht_path'] . '/fs/' . $username . '/*/', GLOB_ONLYDIR);
|
||||
$dirs = [];
|
||||
foreach ($absoluteDirs as $absoluteDir)
|
||||
|
@ -74,33 +96,53 @@ 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
|
||||
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)
|
||||
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);
|
||||
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)
|
||||
output(500, 'Failed to delete Tor keys.');
|
||||
}
|
||||
|
||||
if ($type === 'dns') {
|
||||
// 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)
|
||||
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.');
|
||||
|
||||
query('delete', 'sites', [
|
||||
'username' => $_SESSION['id'],
|
||||
'username' => $user_id,
|
||||
'type' => $type,
|
||||
'address' => $address,
|
||||
]);
|
||||
|
|
18
fn/ns.php
18
fn/ns.php
|
@ -48,22 +48,32 @@ 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]'"]);
|
||||
knotcConfExec([['conf-unset', 'zone[' . $zone . ']']]);
|
||||
|
||||
// Remove Knot zone file
|
||||
if (unlink(CONF['ns']['knot_zones_path'] . '/' . $zone . 'zone') !== true)
|
||||
output(500, 'Failed to remove Knot zone file.');
|
||||
|
||||
// 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)
|
||||
output(500, 'Failed to purge zone data.');
|
||||
|
||||
// Remove from database
|
||||
query('delete', 'zones', [
|
||||
'zone' => $zone,
|
||||
'username' => $_SESSION['id'],
|
||||
'username' => $user_id,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
const SUBDOMAIN_REGEX = '^[a-z0-9]{4,63}$';
|
||||
const SUBDOMAIN_REGEX = '^(?!\-)(?!..\-\-)[a-z0-9-]{4,63}(?<!\-)$';
|
||||
|
||||
function regListUserDomains() {
|
||||
if (isset($_SESSION['id']))
|
||||
|
@ -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', [
|
||||
|
|
48
init.php
Normal file
48
init.php
Normal 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';
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
umask(0077);
|
||||
const ROOT_PATH = __DIR__;
|
||||
define('CONF', parse_ini_file(ROOT_PATH . '/config.ini', true, INI_SCANNER_TYPED));
|
||||
<?php // Test that the current setup is working
|
||||
|
||||
require 'init.php';
|
||||
|
||||
const SFTP = '/usr/bin/sftp';
|
||||
const SSHPASS = '/usr/bin/sshpass';
|
||||
|
@ -13,16 +12,10 @@ const SUFFIX = 'test.servnest.test.';
|
|||
|
||||
const TOR_PROXY = 'socks5h://127.0.0.1:9050';
|
||||
|
||||
const LF = "\n";
|
||||
|
||||
exec(CONF['dns']['kdig_path'] . ' torproject.org AAAA', $output, $return_code);
|
||||
exescape([CONF['dns']['kdig_path'], 'torproject.org', 'AAAA'], $output, $return_code);
|
||||
if (preg_match('/^;; Flags: qr rd ra ad;/Dm', implode("\n", $output)) !== 1)
|
||||
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');
|
||||
|
||||
function curlTest($address, $post = [], $tor = false) {
|
||||
|
@ -108,7 +101,12 @@ function testReg() {
|
|||
'domain' => $domain,
|
||||
'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)
|
||||
exit('Error: /reg/ns: NS record not set' . LF);
|
||||
|
||||
|
@ -156,7 +154,12 @@ function testNs($domain) {
|
|||
'tag' => 'issue',
|
||||
'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)
|
||||
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"
|
||||
. '@ 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)
|
||||
exit('Error: /ns/edit: AAAA record not set' . LF);
|
||||
}
|
||||
|
||||
function testHt($username, $password) {
|
||||
curlTest('/ht/', []);
|
||||
|
||||
define('TEST_CONTENT', 'test-' . random_bytes(4));
|
||||
define('TEST_CONTENT', 'test-' . bin2hex(random_bytes(16)));
|
||||
|
||||
file_put_contents(sys_get_temp_dir() . '/index.html', TEST_CONTENT);
|
||||
|
10
jobs/delete-old-testing.php
Normal file
10
jobs/delete-old-testing.php
Normal 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']);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"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"
|
||||
"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"
|
||||
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."
|
||||
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."
|
||||
msgstr "Vous devez être connecté·e à un compte pour faire cela."
|
||||
|
||||
#: router.php:149
|
||||
msgid "This account doesn't exist anymore. Log out to end this ghost session."
|
||||
msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme."
|
||||
#: view.php:19
|
||||
msgid "You are using a testing account. It may be deleted anytime."
|
||||
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
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
msgstr "Limite de taux globale atteinte, réessayez plus tard."
|
||||
|
||||
|
@ -320,15 +341,15 @@ msgstr "<strong>Erreur de l'utilisataire</strong> : "
|
|||
msgid "<strong>Server error</strong>: "
|
||||
msgstr "<strong>Erreur du serveur</strong> : "
|
||||
|
||||
#: fn/common.php:129
|
||||
#: fn/common.php:132
|
||||
msgid "Wrong proof."
|
||||
msgstr "Preuve incorrecte."
|
||||
|
||||
#: fn/dns.php:63
|
||||
#: fn/dns.php:64
|
||||
msgid "IP address malformed."
|
||||
msgstr "Adresse IP malformée."
|
||||
|
||||
#: fn/dns.php:68 fn/ht.php:28
|
||||
#: fn/dns.php:69 fn/ht.php:31
|
||||
msgid "Domain malformed."
|
||||
msgstr "Domaine malformé."
|
||||
|
||||
|
@ -388,11 +409,6 @@ msgid "Account deletion must be confirmed."
|
|||
msgstr "La suppression du compte doit être confirmée."
|
||||
|
||||
#: 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."
|
||||
msgstr "Compte supprimé."
|
||||
|
||||
|
@ -445,6 +461,18 @@ msgstr "Ce chemin est déjà pris sur ce service. Utilisez-en un autre."
|
|||
msgid "Access removed."
|
||||
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/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
|
||||
|
@ -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/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/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
|
||||
msgid "Domain"
|
||||
msgstr "Domaine"
|
||||
|
||||
#: 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"
|
||||
msgstr "Dossier ciblé"
|
||||
|
||||
#: 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"
|
||||
msgstr "Créer l'accès"
|
||||
|
||||
#: pg-view/ht/add-subdomain.php:2 pg-view/ns/form.ns.php:10
|
||||
#: pg-view/reg/glue.php:10 pg-view/reg/register.php:9
|
||||
#: pg-view/ht/add-subdomain.php:2 pg-view/reg/register.php:6
|
||||
#, 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
|
||||
msgid "Subdomain"
|
||||
msgstr "Sous-domaine"
|
||||
|
||||
#: 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"
|
||||
msgstr "Chemin"
|
||||
|
||||
|
@ -795,6 +833,30 @@ msgstr "Approuvé"
|
|||
msgid "Stable Let's Encrypt certificates"
|
||||
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
|
||||
msgid "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 :"
|
||||
|
||||
#: 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."
|
||||
msgstr "Enregistrer un nouveau domaine sur son compte. Il doit être composé d'entre 4 et 63 lettres et chiffres."
|
||||
msgid "Register a new domain on your account."
|
||||
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"
|
||||
msgstr "Suffixe"
|
||||
|
||||
#: pg-view/reg/register.php:27
|
||||
#: pg-view/reg/register.php:31
|
||||
msgid "Check availability"
|
||||
msgstr "Vérifier sa disponibilité"
|
||||
|
||||
#: pg-view/reg/register.php:29
|
||||
#: pg-view/reg/register.php:33
|
||||
msgid "Register"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: router.php:147
|
||||
#: router.php:115
|
||||
msgid "You need to be logged in to do this."
|
||||
msgstr ""
|
||||
|
||||
#: router.php:149
|
||||
msgid "This account doesn't exist anymore. Log out to end this ghost session."
|
||||
#: view.php:19
|
||||
msgid "You are using a testing account. It may be deleted anytime."
|
||||
msgstr ""
|
||||
|
||||
#: view.php:19
|
||||
msgid "Read more"
|
||||
msgstr ""
|
||||
|
||||
#: view.php:21
|
||||
|
@ -312,11 +328,16 @@ msgstr ""
|
|||
msgid "%sSource code%s available under %s."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: fn/auth.php:135
|
||||
#: fn/auth.php:168
|
||||
msgid "Global rate limit reached, try again later."
|
||||
msgstr ""
|
||||
|
||||
|
@ -332,15 +353,15 @@ msgstr ""
|
|||
msgid "<strong>Server error</strong>: "
|
||||
msgstr ""
|
||||
|
||||
#: fn/common.php:129
|
||||
#: fn/common.php:132
|
||||
msgid "Wrong proof."
|
||||
msgstr ""
|
||||
|
||||
#: fn/dns.php:63
|
||||
#: fn/dns.php:64
|
||||
msgid "IP address malformed."
|
||||
msgstr ""
|
||||
|
||||
#: fn/dns.php:68 fn/ht.php:28
|
||||
#: fn/dns.php:69 fn/ht.php:31
|
||||
msgid "Domain malformed."
|
||||
msgstr ""
|
||||
|
||||
|
@ -400,11 +421,6 @@ msgid "Account deletion must be confirmed."
|
|||
msgstr ""
|
||||
|
||||
#: 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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -457,6 +473,18 @@ msgstr ""
|
|||
msgid "Access removed."
|
||||
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/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
|
||||
|
@ -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/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/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
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: pg-view/ht/add-subdomain.php:2 pg-view/ns/form.ns.php:10
|
||||
#: pg-view/reg/glue.php:10 pg-view/reg/register.php:9
|
||||
#: pg-view/ht/add-subdomain.php:2 pg-view/reg/register.php:6
|
||||
#, 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
|
||||
msgid "Subdomain"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -807,6 +845,30 @@ msgstr ""
|
|||
msgid "Stable Let's Encrypt certificates"
|
||||
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
|
||||
msgid "Flag"
|
||||
msgstr ""
|
||||
|
@ -1130,18 +1192,18 @@ msgid "Nobody can register a domain under these suffixes:"
|
|||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: pg-view/reg/register.php:27
|
||||
#: pg-view/reg/register.php:31
|
||||
msgid "Check availability"
|
||||
msgstr ""
|
||||
|
||||
#: pg-view/reg/register.php:29
|
||||
#: pg-view/reg/register.php:33
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -193,5 +193,10 @@ define('PAGES', [
|
|||
'title' => _('Delete access'),
|
||||
'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,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
|
|
@ -26,4 +26,3 @@ $_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
|
|||
setupDisplayUsername($_POST['username']);
|
||||
|
||||
redir();
|
||||
|
||||
|
|
|
@ -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.'), '<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']]);
|
||||
authDeleteUser($_SESSION['id']);
|
||||
|
||||
logout();
|
||||
|
||||
|
|
|
@ -30,7 +30,14 @@ checkAuthToken($matches[1], $matches[2]);
|
|||
|
||||
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)
|
||||
output(500, 'Certbot failed to get a Let\'s Encrypt certificate.', $output);
|
||||
|
||||
|
|
|
@ -15,16 +15,29 @@ if (chmod($torConfFile, 0644) !== true)
|
|||
output(500, 'Failed to give correct permissions to new Tor configuration file.');
|
||||
|
||||
// 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)
|
||||
output(500, 'Failed to reload Tor.');
|
||||
|
||||
usleep(10000);
|
||||
|
||||
// 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)
|
||||
output(500, 'Unable to read hostname file.');
|
||||
$onion = $output[0];
|
||||
if (preg_match('/^[0-9a-z]{56}\.onion$/D', $onion) !== 1)
|
||||
output(500, 'No onion address found.');
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
if (dirsStatuses('subdomain')[$_POST['dir']] !== false)
|
||||
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.'));
|
||||
|
||||
if (query('select', 'sites', ['address' => $_POST['subdomain'], 'type' => 'subdomain']) !== [])
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
if (dirsStatuses('subpath')[$_POST['dir']] !== false)
|
||||
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.'));
|
||||
|
||||
if (query('select', 'sites', ['address' => $_POST['path'], 'type' => 'subpath']) !== [])
|
||||
|
|
|
@ -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)
|
||||
output(403, 'Unavailable value for <code>site</code>.');
|
||||
|
||||
htDeleteSite($site['address'], $site['type']);
|
||||
htDeleteSite($site['address'], $site['type'], $_SESSION['id']);
|
||||
|
||||
output(200, _('Access removed.'));
|
||||
|
|
39
pg-act/ht/keys.php
Executable file
39
pg-act/ht/keys.php
Executable 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.'));
|
|
@ -39,22 +39,22 @@ if (isset($_POST['zone-content'])) { // Update zone
|
|||
|
||||
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)
|
||||
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)
|
||||
output(500, 'Failed to flush zone file.', $output);
|
||||
|
||||
if (file_put_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone', $new_zone_content) === false)
|
||||
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)
|
||||
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)
|
||||
output(500, 'Failed to thaw zone file.', $output);
|
||||
|
||||
|
|
|
@ -5,7 +5,13 @@ $_POST['domain'] = formatAbsoluteDomain($_POST['domain']);
|
|||
if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== [])
|
||||
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)
|
||||
output(500, 'Unable to query parent name servers.');
|
||||
if ($parentAuthoritatives === [])
|
||||
|
@ -13,7 +19,13 @@ if ($parentAuthoritatives === [])
|
|||
foreach ($parentAuthoritatives as $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)
|
||||
output(403, _('NS authentication record not found.'));
|
||||
|
||||
|
@ -47,8 +59,8 @@ if (chmod($knotZonePath, 0660) !== true)
|
|||
output(500, 'Failed to chmod new zone file.');
|
||||
|
||||
knotcConfExec([
|
||||
"set 'zone[" . $_POST['domain'] . "]'",
|
||||
"set 'zone[" . $_POST['domain'] . "].template' 'servnest'",
|
||||
['conf-set', 'zone[' . $_POST['domain'] . ']'],
|
||||
['conf-set', 'zone[' . $_POST['domain'] . '].template', 'servnest'],
|
||||
]);
|
||||
|
||||
output(200, _('Zone created.'));
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
nsCheckZonePossession($_POST['zone']);
|
||||
|
||||
nsDeleteZone($_POST['zone']);
|
||||
nsDeleteZone($_POST['zone'], $_SESSION['id']);
|
||||
|
||||
output(200, _('Zone deleted.'));
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
<?php
|
||||
|
||||
if (
|
||||
($_POST['algo'] !== '8')
|
||||
AND ($_POST['algo'] !== '13')
|
||||
AND ($_POST['algo'] !== '14')
|
||||
AND ($_POST['algo'] !== '15')
|
||||
AND ($_POST['algo'] !== '16')
|
||||
) output(403, 'Wrong value for <code>algo</code>.');
|
||||
if (!in_array($_POST['algo'], ['8', '13', '14', '15', '16'], true))
|
||||
output(403, 'Wrong value for <code>algo</code>.');
|
||||
|
||||
$_POST['keytag'] = intval($_POST['keytag']);
|
||||
if ((!preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535))
|
||||
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>.');
|
||||
|
||||
if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4')
|
||||
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']);
|
||||
|
||||
rateLimit();
|
||||
|
|
|
@ -11,7 +11,13 @@ $domain = formatAbsoluteDomain($_POST['subdomain'] . '.' . $_POST['suffix']);
|
|||
if (query('select', 'registry', ['username' => $_SESSION['id'], 'domain' => $domain], '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)
|
||||
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)
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
regCheckDomainPossession($_POST['domain']);
|
||||
|
||||
regDeleteDomain($_POST['domain']);
|
||||
regDeleteDomain($_POST['domain'], $_SESSION['id']);
|
||||
|
||||
output(200, _('Domain unregistered.'));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php displayIndex(); ?>
|
||||
|
||||
<h2><?= _('Account type') ?></h2>
|
||||
<h2 id="type"><?= _('Account type') ?></h2>
|
||||
|
||||
<p>
|
||||
<?php
|
||||
|
|
|
@ -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">
|
||||
<label for="subdomain"><?= _('Subdomain') ?></label><br>
|
||||
<input required="" placeholder="label" id="subdomain" name="subdomain" type="text"><code>.<?= CONF['ht']['subdomain_domain'] ?></code><br>
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
<select required="" name="dir" id="dir">
|
||||
<option value="" disabled="" selected="">—</option>
|
||||
|
|
31
pg-view/ht/keys.php
Executable file
31
pg-view/ht/keys.php
Executable 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>
|
|
@ -51,7 +51,7 @@ foreach (regListUserDomains() as $domain)
|
|||
<br>
|
||||
<label for="key"><?= _('Key') ?></label>
|
||||
<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>
|
||||
<input type="submit" value="<?= _('Apply') ?>">
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<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>
|
||||
|
||||
<form method="post">
|
||||
|
|
59
router.php
59
router.php
|
@ -1,40 +1,5 @@
|
|||
<?php
|
||||
umask(0077);
|
||||
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) {
|
||||
|
@ -99,28 +64,32 @@ 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.');
|
||||
$decryption_result = htmlspecialchars(sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
$decryption_result = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
$_SESSION['display-username-cyphertext'],
|
||||
'',
|
||||
$_SESSION['display-username-nonce'],
|
||||
base64_decode($_COOKIE['display-username-decryption-key'])
|
||||
));
|
||||
);
|
||||
if ($decryption_result === false)
|
||||
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
|
||||
$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;
|
||||
foreach (SERVICES_USER as $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')
|
||||
->execute([':services' => implode(',', $user_services), ':id' => $_SESSION['id']]);
|
||||
|
||||
if (SERVICE === 'ht')
|
||||
if ($service === 'ht')
|
||||
htSetupUserFs($_SESSION['id']);
|
||||
}
|
||||
}
|
||||
|
@ -142,12 +111,8 @@ if ($_POST !== []) {
|
|||
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>).');
|
||||
|
||||
if (PAGE_METADATA['require-login'] ?? true !== false) {
|
||||
if (isset($_SESSION['id']) !== true)
|
||||
if (PAGE_METADATA['require-login'] ?? true AND !isset($_SESSION['id']))
|
||||
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'))
|
||||
require ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php';
|
||||
|
|
|
@ -1,47 +1,54 @@
|
|||
<?php
|
||||
<?php // ServNest authenticator for SFTPGo https://github.com/drakkan/sftpgo/blob/main/docs/external-auth.md
|
||||
|
||||
const DEBUG = false;
|
||||
!DEBUG or ob_start();
|
||||
|
||||
require 'router.php';
|
||||
require 'init.php';
|
||||
|
||||
function deny() {
|
||||
!DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents());
|
||||
function deny($reason) {
|
||||
!DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents() . $reason . LF);
|
||||
http_response_code(403);
|
||||
exit();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$username = hashUsername($auth_data['username']);
|
||||
|
||||
if (usernameExists($username) !== true)
|
||||
deny();
|
||||
deny('This username doesn\'t exist.');
|
||||
|
||||
if (!in_array('ht', explode(',', query('select', 'users', ['username' => $username], 'services')[0]), true))
|
||||
deny();
|
||||
$account = query('select', 'users', ['username' => $username])[0];
|
||||
|
||||
$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)
|
||||
deny();
|
||||
const SFTPGO_DENY_PERMS = ['/' => ['list']];
|
||||
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 '
|
||||
{
|
||||
"status": 1,
|
||||
"username": ' . json_encode($auth_data['username']) . ',
|
||||
"home_dir": "' . CONF['ht']['ht_path'] . '/fs/' . $id . '",
|
||||
"quota_size": ' . ((query('select', 'users', ['id' => $id], 'type')[0] === 'approved') ? CONF['ht']['user_quota_approved'] : CONF['ht']['user_quota_testing']) . ',
|
||||
"permissions": {
|
||||
"/": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
';
|
||||
echo json_encode([
|
||||
'status' => 1,
|
||||
'username' => $auth_data['username'],
|
||||
'home_dir' => CONF['ht']['ht_path'] . '/fs/' . $account['id'],
|
||||
'quota_size' => ($account['type'] === 'approved') ? CONF['ht']['user_quota_approved'] : CONF['ht']['user_quota_testing'],
|
||||
'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);
|
||||
|
|
6
view.php
6
view.php
|
@ -14,11 +14,11 @@ foreach (glob('css/*.css') as $css_path)
|
|||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<p>
|
||||
<p class="auth">
|
||||
<?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 { ?>
|
||||
<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 } ?>
|
||||
</p>
|
||||
<nav>
|
||||
|
|
Loading…
Reference in a new issue