ns/sync.php: instant sync when adding new sync
This commit is contained in:
parent
8624262482
commit
e96d61703b
18 changed files with 105 additions and 84 deletions
18
fn/auth.php
18
fn/auth.php
|
@ -25,7 +25,7 @@ function checkPasswordFormat(string $password): void {
|
|||
}
|
||||
|
||||
function hashUsername(string $username): string {
|
||||
return base64_encode(sodium_crypto_pwhash(32, $username, hex2bin(query('select', 'params', ['name' => 'username_salt'], 'value')[0]), 2**10, 2**14, SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13));
|
||||
return base64_encode(sodium_crypto_pwhash(32, $username, hex2bin(query('select', 'params', ['name' => 'username_salt'], ['value'])[0]), 2**10, 2**14, SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13));
|
||||
}
|
||||
|
||||
function hashPassword(string $password): string {
|
||||
|
@ -33,15 +33,15 @@ function hashPassword(string $password): string {
|
|||
}
|
||||
|
||||
function usernameExists(string $username): bool {
|
||||
return isset(query('select', 'users', ['username' => $username], 'id')[0]);
|
||||
return isset(query('select', 'users', ['username' => $username], ['id'])[0]);
|
||||
}
|
||||
|
||||
function checkPassword(string $id, string $password): bool {
|
||||
return password_verify($password, query('select', 'users', ['id' => $id], 'password')[0]);
|
||||
return password_verify($password, query('select', 'users', ['id' => $id], ['password'])[0]);
|
||||
}
|
||||
|
||||
function outdatedPasswordHash(string $id): bool {
|
||||
return password_needs_rehash(query('select', 'users', ['id' => $id], 'password')[0], ALGO_PASSWORD, OPTIONS_PASSWORD);
|
||||
return password_needs_rehash(query('select', 'users', ['id' => $id], ['password'])[0], ALGO_PASSWORD, OPTIONS_PASSWORD);
|
||||
}
|
||||
|
||||
function changePassword(string $id, string $password): void {
|
||||
|
@ -88,18 +88,18 @@ function setupDisplayUsername(string $display_username): void {
|
|||
}
|
||||
|
||||
function authDeleteUser(string $user_id): void {
|
||||
$user_services = explode(',', query('select', 'users', ['id' => $user_id], 'services')[0]);
|
||||
$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)
|
||||
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)
|
||||
foreach (query('select', 'zones', ['username' => $user_id], ['zone']) as $zone)
|
||||
nsDeleteZone($zone, $user_id);
|
||||
|
||||
if (in_array('ht', $user_services, true)) {
|
||||
|
@ -178,8 +178,8 @@ function rateLimitAccount(int $requestedTokens): int {
|
|||
|
||||
function rateLimitInstance(int $requestedTokens): void {
|
||||
// Get
|
||||
$tokens = query('select', 'params', ['name' => 'instance_bucket_tokens'], 'value')[0];
|
||||
$bucketLastUpdate = query('select', 'params', ['name' => 'instance_bucket_last_update'], 'value')[0];
|
||||
$tokens = query('select', 'params', ['name' => 'instance_bucket_tokens'], ['value'])[0];
|
||||
$bucketLastUpdate = query('select', 'params', ['name' => 'instance_bucket_last_update'], ['value'])[0];
|
||||
|
||||
// Compute
|
||||
$tokens = min(86400, $tokens + (time() - $bucketLastUpdate));
|
||||
|
|
|
@ -21,7 +21,6 @@ function exescape(array $args, array &$output = NULL, int &$result_code = NULL):
|
|||
return $result_code;
|
||||
}
|
||||
|
||||
class KdigException extends Exception {};
|
||||
function kdig(string $name, string $type, string $server = NULL): array {
|
||||
exescape([
|
||||
CONF['dns']['kdig_path'],
|
||||
|
@ -36,7 +35,7 @@ function kdig(string $name, string $type, string $server = NULL): array {
|
|||
...(isset($server) ? ['@' . $server] : []),
|
||||
], $output, $code);
|
||||
if ($code !== 0)
|
||||
throw new KdigException();
|
||||
throw new KdigException($name . ' ' . $type . ' resolution failed.');
|
||||
return json_decode(implode(LF, $output), true, flags: JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
|
@ -63,10 +62,10 @@ function insert(string $table, array $values): void {
|
|||
->execute($values);
|
||||
}
|
||||
|
||||
function query(string $action, string $table, array $conditions = [], string $column = NULL): array {
|
||||
function query(string $action, string $table, array $conditions = [], array $columns = NULL): array {
|
||||
|
||||
$query = match ($action) {
|
||||
'select' => 'SELECT *',
|
||||
'select' => 'SELECT ' . implode(',', $columns ?? ['*']),
|
||||
'delete' => 'DELETE',
|
||||
};
|
||||
|
||||
|
@ -82,7 +81,9 @@ function query(string $action, string $table, array $conditions = [], string $co
|
|||
$stmt = DB->prepare($query);
|
||||
$stmt->execute($conditions);
|
||||
|
||||
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), $column);
|
||||
if (count($columns ?? []) === 1)
|
||||
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), $columns[0]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function displayIndex(): void { ?>
|
||||
|
@ -138,13 +139,13 @@ function equalArrays(array $a, array $b): bool {
|
|||
- the user's id
|
||||
- that a same user used a token multiple times (by using a unique salt for each token)
|
||||
*/
|
||||
if (time() - query('select', 'params', ['name' => 'secret_key_last_change'], 'value')[0] >= 86400 * 20) {
|
||||
if (time() - query('select', 'params', ['name' => 'secret_key_last_change'], ['value'])[0] >= 86400 * 20) {
|
||||
DB->prepare("UPDATE params SET value = :secret_key WHERE name = 'secret_key';")
|
||||
->execute([':secret_key' => bin2hex(random_bytes(32))]);
|
||||
DB->prepare("UPDATE params SET value = :last_change WHERE name = 'secret_key_last_change';")
|
||||
->execute([':last_change' => time()]);
|
||||
}
|
||||
define('SECRET_KEY', hex2bin(query('select', 'params', ['name' => 'secret_key'], 'value')[0]));
|
||||
define('SECRET_KEY', hex2bin(query('select', 'params', ['name' => 'secret_key'], ['value'])[0]));
|
||||
function getAuthToken(): string {
|
||||
$salt = bin2hex(random_bytes(4));
|
||||
$hash = hash_hmac('sha256', $salt . ($_SESSION['id'] ?? ''), SECRET_KEY);
|
||||
|
|
|
@ -81,7 +81,7 @@ function dirsStatuses(string $type): array {
|
|||
$dbDirs = query('select', 'sites', [
|
||||
'username' => $_SESSION['id'],
|
||||
'type' => $type,
|
||||
], 'site_dir');
|
||||
], ['site_dir']);
|
||||
$dirs = [];
|
||||
foreach (listFsDirs($_SESSION['id']) as $fsDir)
|
||||
$dirs[$fsDir] = in_array($fsDir, $dbDirs);
|
||||
|
@ -103,7 +103,7 @@ function htDeleteSite(string $address, string $type, string $user_id): void {
|
|||
'username' => $user_id,
|
||||
'address' => $address,
|
||||
'type' => $type,
|
||||
], 'site_dir')[0];
|
||||
], ['site_dir'])[0];
|
||||
|
||||
// Delete Tor config
|
||||
if (unlink(CONF['ht']['tor_config_path'] . '/' . $user_id . '/' . $dir) !== true)
|
||||
|
|
37
fn/ns.php
37
fn/ns.php
|
@ -39,7 +39,7 @@ function nsParseCommonRequirements(): array {
|
|||
|
||||
function nsListUserZones(): array {
|
||||
if (isset($_SESSION['id']))
|
||||
return query('select', 'zones', ['username' => $_SESSION['id']], 'zone');
|
||||
return query('select', 'zones', ['username' => $_SESSION['id']], ['zone']);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -81,3 +81,38 @@ function nsDeleteZone(string $zone, string $user_id): void {
|
|||
'username' => $user_id,
|
||||
]);
|
||||
}
|
||||
|
||||
function nsSync(string $source, string $destination): void {
|
||||
$zone_raw = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $destination . 'zone');
|
||||
if ($zone_raw === false)
|
||||
output(403, 'Unable to read zone file.');
|
||||
|
||||
foreach (['AAAA', 'A', 'CAA'] as $type) {
|
||||
// Get source/distant records
|
||||
$results = kdig(name: $source, type: $type);
|
||||
|
||||
if ($results['AD'] !== 1)
|
||||
throw new NoDnssecException($source . ' not DNSSEC-signed.');
|
||||
|
||||
$source_records = array_column($results['answerRRs'] ?? [], 'rdata' . $type);
|
||||
|
||||
// Get destination/local records
|
||||
$dest_records = array_column(parseZoneFile($zone_raw, [$type], $destination, false), 3);
|
||||
|
||||
// Add source records that are not yet in destination
|
||||
foreach (array_diff($source_records, $dest_records) as $value_to_add)
|
||||
knotcZoneExec($destination, [
|
||||
$destination,
|
||||
NS_SYNC_TTL,
|
||||
$type,
|
||||
$value_to_add,
|
||||
], 'add');
|
||||
// Delete destination records that are not part of source anymore
|
||||
foreach (array_diff($dest_records, $source_records) as $value_to_delete)
|
||||
knotcZoneExec($destination, [
|
||||
$destination,
|
||||
$type,
|
||||
$value_to_delete,
|
||||
], 'delete');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ const REG_ALLOWED_TYPES = ['NS', 'DS', 'AAAA', 'A'];
|
|||
|
||||
function regListUserDomains(): array {
|
||||
if (isset($_SESSION['id']))
|
||||
return query('select', 'registry', ['username' => $_SESSION['id']], 'domain');
|
||||
return query('select', 'registry', ['username' => $_SESSION['id']], ['domain']);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ function regDeleteDomain(string $domain, string $user_id): void {
|
|||
|
||||
insert('registry-history', [
|
||||
'domain' => $domain,
|
||||
'creation' => query('select', 'registry', $conditions, 'creation')[0],
|
||||
'creation' => query('select', 'registry', $conditions, ['creation'])[0],
|
||||
'expiration' => date('Y-m'),
|
||||
]);
|
||||
|
||||
|
|
2
init.php
2
init.php
|
@ -2,6 +2,8 @@
|
|||
umask(0077);
|
||||
const LF = "\n";
|
||||
|
||||
class KdigException extends Exception {};
|
||||
class NoDnssecException extends Exception {};
|
||||
set_error_handler(function ($level, $message, $file = '', $line = 0) {
|
||||
throw new ErrorException($message, 0, $level, $file, $line);
|
||||
});
|
||||
|
|
|
@ -1,42 +1,9 @@
|
|||
<?php declare(strict_types=1);
|
||||
require __DIR__ . '/../init.php';
|
||||
|
||||
foreach (query('select', 'ns-syncs') as $sync) {
|
||||
$zone_raw = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $sync['destination'] . 'zone');
|
||||
if ($zone_raw === false)
|
||||
output(403, 'Unable to read zone file.');
|
||||
|
||||
foreach (['AAAA', 'A', 'CAA'] as $type) {
|
||||
// Get source/distant records
|
||||
foreach (query('select', 'ns-syncs', columns: ['source', 'destination']) as ['source' => $source, 'destination' => $destination])
|
||||
try {
|
||||
$results = kdig(name: $sync['source'], type: $type);
|
||||
} catch (KdigException) {
|
||||
fwrite(STDERR, $sync['source'] . ' resolution failed.' . LF);
|
||||
break;
|
||||
}
|
||||
if ($results['AD'] !== 1) {
|
||||
fwrite(STDERR, $sync['source'] . ' skipped: not signed using DNSSEC.' . LF);
|
||||
continue;
|
||||
}
|
||||
$source_records = array_column($results['answerRRs'] ?? [], 'rdata' . $type);
|
||||
|
||||
// Get destination/local records
|
||||
$dest_records = array_column(parseZoneFile($zone_raw, [$type], $sync['destination'], false), 3);
|
||||
|
||||
// Add source records that are not yet in destination
|
||||
foreach (array_diff($source_records, $dest_records) as $value_to_add)
|
||||
knotcZoneExec($sync['destination'], [
|
||||
$sync['destination'],
|
||||
NS_SYNC_TTL,
|
||||
$type,
|
||||
$value_to_add,
|
||||
], 'add');
|
||||
// Delete destination records that are not part of source anymore
|
||||
foreach (array_diff($dest_records, $source_records) as $value_to_delete)
|
||||
knotcZoneExec($sync['destination'], [
|
||||
$sync['destination'],
|
||||
$type,
|
||||
$value_to_delete,
|
||||
], 'delete');
|
||||
}
|
||||
}
|
||||
nsSync($source, $destination);
|
||||
} catch (KdigException | NoDnssecException $e) {
|
||||
fwrite(STDERR, $e->getMessage() . LF);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ if ($_SESSION['type'] !== 'testing')
|
|||
|
||||
rateLimit();
|
||||
|
||||
if (isset(query('select', 'approval-keys', ['key' => $_POST['key']], 'key')[0]) !== true)
|
||||
if (isset(query('select', 'approval-keys', ['key' => $_POST['key']], ['key'])[0]) !== true)
|
||||
output(403, _('This approval key is not available. It has been mistyped, used for another account, or has expired.'));
|
||||
|
||||
query('delete', 'approval-keys', ['key' => $_POST['key']]);
|
||||
|
|
|
@ -9,7 +9,7 @@ $username = hashUsername($_POST['username']);
|
|||
if (usernameExists($username) !== true)
|
||||
output(403, _('This account does not exist.'));
|
||||
|
||||
$id = query('select', 'users', ['username' => $username], 'id')[0];
|
||||
$id = query('select', 'users', ['username' => $username], ['id'])[0];
|
||||
|
||||
if (checkPassword($id, $_POST['password']) !== true)
|
||||
output(403, _('Wrong password.'));
|
||||
|
@ -21,7 +21,7 @@ stopSession();
|
|||
startSession();
|
||||
|
||||
$_SESSION['id'] = $id;
|
||||
$_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
|
||||
$_SESSION['type'] = query('select', 'users', ['id' => $id], ['type'])[0];
|
||||
|
||||
setupDisplayUsername($_POST['username']);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ $_POST['domain'] = formatDomain($_POST['domain']);
|
|||
if (dirsStatuses('dns')[$_POST['dir']] !== false)
|
||||
output(403, 'Wrong value for <code>dir</code>.');
|
||||
|
||||
if (query('select', 'sites', ['address' => $_POST['domain']], 'address') !== [])
|
||||
if (query('select', 'sites', ['address' => $_POST['domain']], ['address']) !== [])
|
||||
output(403, _('This domain already exists on this service. Use another one.'));
|
||||
|
||||
$remoteAaaaRecords = dns_get_record($_POST['domain'], DNS_AAAA);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
if (preg_match('/^(?<type>subpath|subdomain|onion|dns):(?<address>[a-z0-9._-]{1,256})$/D', $_POST['site'], $site) !== 1)
|
||||
output(403, 'Malformed value for <code>site</code>.');
|
||||
|
||||
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>.');
|
||||
|
||||
htDeleteSite($site['address'], $site['type'], $_SESSION['id']);
|
||||
|
|
|
@ -4,7 +4,7 @@ $el_nb = count($_POST['syncs']);
|
|||
if ($el_nb < 1 OR $el_nb > 8)
|
||||
output(403, 'Wrong elements number.');
|
||||
|
||||
foreach ($_POST['syncs'] as $i => $sync) {
|
||||
foreach ($_POST['syncs'] as $i => &$sync) {
|
||||
if (($sync['source'] ?? '') === '') {
|
||||
unset($_POST['syncs'][$i]);
|
||||
continue;
|
||||
|
@ -12,24 +12,40 @@ foreach ($_POST['syncs'] as $i => $sync) {
|
|||
$sync['source'] = formatAbsoluteDomain($sync['source']);
|
||||
nsCheckZonePossession($sync['destination']);
|
||||
}
|
||||
$syncs = array_values($_POST['syncs']);
|
||||
$new_syncs = array_values($_POST['syncs']);
|
||||
|
||||
$destinations = array_column($syncs, 'destination');
|
||||
if (count($destinations) !== count(array_unique($destinations)))
|
||||
$new_destinations = array_column($new_syncs, 'destination');
|
||||
if (count($new_destinations) !== count(array_unique($new_destinations)))
|
||||
output(403, _('Multiple source domains can\'t be applied to the same target domain.'));
|
||||
|
||||
rateLimit();
|
||||
|
||||
$current_syncs = query('select', 'ns-syncs', ['username' => $_SESSION['id']], ['source', 'destination']);
|
||||
|
||||
try {
|
||||
foreach ($new_syncs as $new_sync)
|
||||
if (!in_array($new_sync, $current_syncs))
|
||||
nsSync($new_sync['source'], $new_sync['destination']);
|
||||
} catch (KdigException | NoDnssecException $e) {
|
||||
output(403, $e->getMessage() . LF);
|
||||
}
|
||||
|
||||
try {
|
||||
DB->beginTransaction();
|
||||
|
||||
query('delete', 'ns-syncs', ['username' => $_SESSION['id']]);
|
||||
|
||||
foreach ($syncs as $sync)
|
||||
foreach ($current_syncs as $current_sync) // Deletions
|
||||
if (!in_array($current_sync, $new_syncs))
|
||||
query('delete', 'ns-syncs', [
|
||||
'username' => $_SESSION['id'],
|
||||
'source' => $current_sync['source'],
|
||||
'destination' => $current_sync['destination'],
|
||||
]);
|
||||
foreach ($new_syncs as $new_sync) // Adds
|
||||
if (!in_array($new_sync, $current_syncs))
|
||||
insert('ns-syncs', [
|
||||
'username' => $_SESSION['id'],
|
||||
'source' => $sync['source'],
|
||||
'destination' => $sync['destination'],
|
||||
'source' => $new_sync['source'],
|
||||
'destination' => $new_sync['destination'],
|
||||
]);
|
||||
|
||||
DB->commit();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
$domain = formatAbsoluteDomain($_POST['domain']);
|
||||
|
||||
if (query('select', 'zones', ['zone' => $domain], 'zone') !== [])
|
||||
if (query('select', 'zones', ['zone' => $domain], ['zone']) !== [])
|
||||
output(403, _('This zone already exists on the service.'));
|
||||
|
||||
$parent_domain = ltrim(strstr($domain, '.'), '.');
|
||||
|
|
|
@ -8,7 +8,7 @@ if (array_key_exists($_POST['suffix'], CONF['reg']['suffixes']) !== true)
|
|||
|
||||
$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.'));
|
||||
|
||||
$ns_records = array_column(kdig(name: $domain, type: 'NS', server: CONF['reg']['address'])['authorityRRs'], 'rdataNS');
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<?php
|
||||
|
||||
$zones = query('select', 'zones', ['username' => $_SESSION['id'] ?? ''], 'zone');
|
||||
$zones = query('select', 'zones', ['username' => $_SESSION['id'] ?? ''], ['zone']);
|
||||
if ($zones === [])
|
||||
echo '<p>∅<p>' . LF;
|
||||
else {
|
||||
|
|
|
@ -25,7 +25,7 @@ foreach (array_slice(array_merge(query('select', 'ns-syncs', ['username' => $_SE
|
|||
<select required="" name="syncs[<?= $i ?>][destination]" id="destination<?= $i ?>">
|
||||
<option <?= (($sync['destination'] === '') ? 'value="" disabled=""' : 'value="' . $sync['destination'] . '"') ?> selected=""><?= $sync['destination'] ?></option>
|
||||
<?php
|
||||
foreach (array_diff(nsListUserZones(), query('select', 'ns-syncs', ['username' => $_SESSION['id'] ?? ''], 'destination')) as $zone)
|
||||
foreach (array_diff(nsListUserZones(), query('select', 'ns-syncs', ['username' => $_SESSION['id'] ?? ''], ['destination'])) as $zone)
|
||||
echo "<option value='" . $zone . "'>" . $zone . "</option>";
|
||||
?>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<?php
|
||||
|
||||
$domains = query('select', 'registry', ['username' => $_SESSION['id'] ?? ''], 'domain');
|
||||
$domains = query('select', 'registry', ['username' => $_SESSION['id'] ?? ''], ['domain']);
|
||||
if ($domains === [])
|
||||
echo '<p>∅</p>' . LF;
|
||||
else {
|
||||
|
|
|
@ -64,7 +64,7 @@ if (isset($_COOKIE[SESSION_COOKIE_NAME]))
|
|||
startSession(); // Resume session
|
||||
|
||||
if (isset($_SESSION['id'])) {
|
||||
if (!isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0]))
|
||||
if (!isset(query('select', 'users', ['id' => $_SESSION['id']], ['id'])[0]))
|
||||
logout();
|
||||
|
||||
// Decrypt display username
|
||||
|
@ -81,7 +81,7 @@ if (isset($_SESSION['id'])) {
|
|||
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]));
|
||||
$user_services = array_filter(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') {
|
||||
$user_services[] = $service;
|
||||
|
|
Loading…
Reference in a new issue