From e96d61703b5b2fc01da0a5e6fabb3081f33f9617 Mon Sep 17 00:00:00 2001 From: Miraty Date: Sun, 8 Oct 2023 00:50:48 +0200 Subject: [PATCH] ns/sync.php: instant sync when adding new sync --- fn/auth.php | 18 ++++++++-------- fn/common.php | 15 +++++++------- fn/ht.php | 4 ++-- fn/ns.php | 37 ++++++++++++++++++++++++++++++++- fn/reg.php | 4 ++-- init.php | 2 ++ jobs/ns-sync.php | 45 ++++++---------------------------------- pg-act/auth/approval.php | 2 +- pg-act/auth/login.php | 4 ++-- pg-act/ht/add-dns.php | 2 +- pg-act/ht/del.php | 2 +- pg-act/ns/sync.php | 40 ++++++++++++++++++++++++----------- pg-act/ns/zone-add.php | 2 +- pg-act/reg/transfer.php | 2 +- pg-view/ns/index.php | 2 +- pg-view/ns/sync.php | 2 +- pg-view/reg/index.php | 2 +- router.php | 4 ++-- 18 files changed, 105 insertions(+), 84 deletions(-) diff --git a/fn/auth.php b/fn/auth.php index d3e1b70..017f17b 100644 --- a/fn/auth.php +++ b/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.'), '' . PAGES[$service]['index']['title'] . '')); 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)); diff --git a/fn/common.php b/fn/common.php index 1491679..b687ff0 100644 --- a/fn/common.php +++ b/fn/common.php @@ -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); diff --git a/fn/ht.php b/fn/ht.php index fb4ef0b..020d058 100644 --- a/fn/ht.php +++ b/fn/ht.php @@ -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) diff --git a/fn/ns.php b/fn/ns.php index 62c99f8..97d04fc 100644 --- a/fn/ns.php +++ b/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'); + } +} diff --git a/fn/reg.php b/fn/reg.php index c5cf7fa..3e8a193 100644 --- a/fn/reg.php +++ b/fn/reg.php @@ -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'), ]); diff --git a/init.php b/init.php index cb1aaa0..d57eba6 100644 --- a/init.php +++ b/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); }); diff --git a/jobs/ns-sync.php b/jobs/ns-sync.php index fd74c42..6f27b4b 100644 --- a/jobs/ns-sync.php +++ b/jobs/ns-sync.php @@ -1,42 +1,9 @@ $source, 'destination' => $destination]) + try { + nsSync($source, $destination); + } catch (KdigException | NoDnssecException $e) { + fwrite(STDERR, $e->getMessage() . LF); + }; diff --git a/pg-act/auth/approval.php b/pg-act/auth/approval.php index b6f568e..1766373 100644 --- a/pg-act/auth/approval.php +++ b/pg-act/auth/approval.php @@ -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']]); diff --git a/pg-act/auth/login.php b/pg-act/auth/login.php index 77cbd39..0194ad3 100644 --- a/pg-act/auth/login.php +++ b/pg-act/auth/login.php @@ -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']); diff --git a/pg-act/ht/add-dns.php b/pg-act/ht/add-dns.php index 534e562..e235add 100644 --- a/pg-act/ht/add-dns.php +++ b/pg-act/ht/add-dns.php @@ -5,7 +5,7 @@ $_POST['domain'] = formatDomain($_POST['domain']); if (dirsStatuses('dns')[$_POST['dir']] !== false) output(403, 'Wrong value for dir.'); -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); diff --git a/pg-act/ht/del.php b/pg-act/ht/del.php index 8b25929..03e67a8 100644 --- a/pg-act/ht/del.php +++ b/pg-act/ht/del.php @@ -3,7 +3,7 @@ if (preg_match('/^(?subpath|subdomain|onion|dns):(?
[a-z0-9._-]{1,256})$/D', $_POST['site'], $site) !== 1) output(403, 'Malformed value for site.'); -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 site.'); htDeleteSite($site['address'], $site['type'], $_SESSION['id']); diff --git a/pg-act/ns/sync.php b/pg-act/ns/sync.php index 78ba6bd..251e15c 100644 --- a/pg-act/ns/sync.php +++ b/pg-act/ns/sync.php @@ -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,25 +12,41 @@ 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) - insert('ns-syncs', [ - 'username' => $_SESSION['id'], - 'source' => $sync['source'], - 'destination' => $sync['destination'], - ]); + 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' => $new_sync['source'], + 'destination' => $new_sync['destination'], + ]); DB->commit(); } catch (Exception $e) { diff --git a/pg-act/ns/zone-add.php b/pg-act/ns/zone-add.php index 420c200..b81b030 100644 --- a/pg-act/ns/zone-add.php +++ b/pg-act/ns/zone-add.php @@ -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, '.'), '.'); diff --git a/pg-act/reg/transfer.php b/pg-act/reg/transfer.php index 4d46619..f27ded1 100644 --- a/pg-act/reg/transfer.php +++ b/pg-act/reg/transfer.php @@ -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'); diff --git a/pg-view/ns/index.php b/pg-view/ns/index.php index 37f94b7..3c44f70 100644 --- a/pg-view/ns/index.php +++ b/pg-view/ns/index.php @@ -10,7 +10,7 @@ $_SESSION['id'] ?? ''], 'zone'); +$zones = query('select', 'zones', ['username' => $_SESSION['id'] ?? ''], ['zone']); if ($zones === []) echo '

' . LF; else { diff --git a/pg-view/ns/sync.php b/pg-view/ns/sync.php index 28555e2..0a914ad 100644 --- a/pg-view/ns/sync.php +++ b/pg-view/ns/sync.php @@ -25,7 +25,7 @@ foreach (array_slice(array_merge(query('select', 'ns-syncs', ['username' => $_SE