Add ns/sync and jobs/ns-syncs

This commit is contained in:
Miraty 2023-06-24 16:54:36 +02:00
parent ccd17b7ffa
commit 858d6e8d02
14 changed files with 178 additions and 11 deletions

View file

@ -0,0 +1,12 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "ns-syncs" (
"username" TEXT NOT NULL,
"source" TEXT NOT NULL,
"destination" TEXT NOT NULL UNIQUE,
UNIQUE("username", "source", "destination"),
FOREIGN KEY("username") REFERENCES "users"("id"),
FOREIGN KEY("destination") REFERENCES "zones"("zone")
);
COMMIT;

View file

@ -42,6 +42,14 @@ CREATE TABLE IF NOT EXISTS "zones" (
PRIMARY KEY("zone"), PRIMARY KEY("zone"),
FOREIGN KEY("username") REFERENCES "users"("id") FOREIGN KEY("username") REFERENCES "users"("id")
); );
CREATE TABLE IF NOT EXISTS "ns-syncs" (
"username" TEXT NOT NULL,
"source" TEXT NOT NULL,
"destination" TEXT NOT NULL UNIQUE,
UNIQUE("username", "source", "destination"),
FOREIGN KEY("username") REFERENCES "users"("id"),
FOREIGN KEY("destination") REFERENCES "zones"("zone")
);
CREATE TABLE IF NOT EXISTS "sites" ( CREATE TABLE IF NOT EXISTS "sites" (
"username" TEXT NOT NULL, "username" TEXT NOT NULL,
"site_dir" TEXT NOT NULL, "site_dir" TEXT NOT NULL,

View file

@ -98,9 +98,11 @@ function authDeleteUser(string $user_id): void {
foreach (query('select', 'registry', ['username' => $user_id], 'domain') as $domain) foreach (query('select', 'registry', ['username' => $user_id], 'domain') as $domain)
regDeleteDomain($domain, $user_id); regDeleteDomain($domain, $user_id);
if (in_array('ns', $user_services, true)) if (in_array('ns', $user_services, true)) {
query('delete', 'ns-syncs', ['username' => $user_id]);
foreach (query('select', 'zones', ['username' => $user_id], 'zone') as $zone) foreach (query('select', 'zones', ['username' => $user_id], 'zone') as $zone)
nsDeleteZone($zone, $user_id); nsDeleteZone($zone, $user_id);
}
if (in_array('ht', $user_services, true)) { if (in_array('ht', $user_services, true)) {
foreach (query('select', 'sites', ['username' => $user_id]) as $site) foreach (query('select', 'sites', ['username' => $user_id]) as $site)

View file

@ -21,6 +21,24 @@ function exescape(array $args, array &$output = NULL, int &$result_code = NULL):
return $result_code; return $result_code;
} }
class KdigException extends Exception {};
function kdig(string $name, string $type, string $server = NULL): array {
exescape([
CONF['dns']['kdig_path'],
'+json',
'+timeout=5',
'+retry=0',
'-q',
$name,
'-t',
$type,
...(isset($server) ? ['@' . $server] : []),
], $output, $code);
if ($code !== 0)
throw new KdigException();
return json_decode(implode(LF, $output), true, flags: JSON_THROW_ON_ERROR);
}
function insert(string $table, array $values): void { function insert(string $table, array $values): void {
$query = 'INSERT INTO "' . $table . '"('; $query = 'INSERT INTO "' . $table . '"(';

View file

@ -1,16 +1,19 @@
<?php <?php
function parseZoneFile(string $zone_content, array $types, bool|string $filter_domain = false): array { function parseZoneFile(string $zone_content, array $types, bool|string $filter_domain = false, bool $filter_include_subdomains = true): array {
$parsed_zone_content = []; $parsed_zone_content = [];
foreach (explode(LF, $zone_content) as $zone_line) { foreach (explode(LF, $zone_content) as $zone_line) {
if ($zone_line === '' OR str_starts_with($zone_line, ';')) if ($zone_line === '' OR str_starts_with($zone_line, ';'))
continue; // Ignore empty lines and comments continue; // Ignore empty lines and comments
$elements = preg_split('/[\t ]+/', $zone_line, 4); $elements = preg_split('/[\t ]+/', $zone_line, 4);
if ($filter_domain !== false AND !str_ends_with($elements[0], $filter_domain)) if ($filter_domain !== false AND match ($filter_include_subdomains) {
true => !str_ends_with($elements[0], $filter_domain),
false => $elements[0] !== $filter_domain,
})
continue; // Ignore records for other domains continue; // Ignore records for other domains
if (!in_array($elements[2], $types, true)) if (!in_array($elements[2], $types, true))
continue; // Ignore records generated by Knot continue; // Ignore records generated by Knot
array_push($parsed_zone_content, array_map('htmlspecialchars', $elements)); array_push($parsed_zone_content, $elements);
} }
return $parsed_zone_content; return $parsed_zone_content;
} }

View file

@ -17,6 +17,8 @@ const ALLOWED_TYPES = ['AAAA', 'A', 'TXT', 'SRV', 'MX', 'SVCB', 'HTTPS', 'NS', '
const ZONE_MAX_CHARACTERS = 10000; const ZONE_MAX_CHARACTERS = 10000;
const SYNC_TTL = 3600;
function nsParseCommonRequirements(): array { function nsParseCommonRequirements(): array {
nsCheckZonePossession($_POST['zone']); nsCheckZonePossession($_POST['zone']);

View file

@ -7,7 +7,7 @@ set_error_handler(function ($level, $message, $file = '', $line = 0) {
set_exception_handler(function ($e) { set_exception_handler(function ($e) {
error_log((string) $e); error_log((string) $e);
http_response_code(500); http_response_code(500);
echo '<h1>Error</h1>An error occured.'; echo '<h1>Error</h1><p>An error occured.<p>';
}); });
register_shutdown_function(function () { // Also catch fatal errors register_shutdown_function(function () { // Also catch fatal errors
if (($error = error_get_last()) !== NULL) if (($error = error_get_last()) !== NULL)

42
jobs/ns-sync.php Normal file
View file

@ -0,0 +1,42 @@
<?php
require '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', 'SRV', 'CAA'] as $type) {
// Get source/distant records
try {
$results = kdig(name: $sync['source'], type: $type);
} catch (KdigException) {
error_log($sync['source'] . ' resolution failed.' . LF);
break;
}
if ($results['AD'] !== 1) {
error_log($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'],
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');
}
}

View file

@ -163,6 +163,11 @@ define('PAGES', [
'description' => _('Store geographic coordinates'), 'description' => _('Store geographic coordinates'),
'tokens_account_cost' => 120, 'tokens_account_cost' => 120,
], ],
'sync' => [
'title' => sprintf(_('Synchronized records')),
'description' => _('Regularly fetch distant record value and update it to a local zone'),
'tokens_account_cost' => 900,
],
], ],
'ht' => [ 'ht' => [
'index' => [ 'index' => [

37
pg-act/ns/sync.php Normal file
View file

@ -0,0 +1,37 @@
<?php
$el_nb = count($_POST['syncs']);
if ($el_nb < 1 OR $el_nb > 8)
output(403, 'Wrong elements number.');
foreach ($_POST['syncs'] as $i => &$sync) {
if (($sync['source'] ?? '') === '') {
unset($_POST['syncs'][$i]);
continue;
}
$sync['source'] = formatAbsoluteDomain($sync['source']);
nsCheckZonePossession($sync['destination']);
}
$syncs = array_values($_POST['syncs']);
rateLimit();
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'],
]);
DB->commit();
} catch (Exception $e) {
DB->rollback();
output(500, 'Database error.', [$e->getMessage()]);
}
output(200, _('Synchronized records updated.'));

View file

@ -16,12 +16,12 @@ foreach (array_slice(array_merge(query('select', 'ssh-keys', ['username' => $_SE
<fieldset> <fieldset>
<legend><?= ($ssh_key['key'] === '') ? _('Add new SSH key access') : _('SSH key access') ?></legend> <legend><?= ($ssh_key['key'] === '') ? _('Add new SSH key access') : _('SSH key access') ?></legend>
<div> <div>
<label for="public-key"><?= _('Public key') ?></label><br> <label for="public-key<?= $i ?>"><?= _('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> <code>ssh-ed15519 <input pattern="<?= ED25519_PUBKEY_REGEX ?>" placeholder="AAAAC3NzaC1lZDI1NTE5AAAAI<?= substr(base64_encode(random_bytes(32)), 0, 43) ?>" id="public-key<?= $i ?>" name="keys[<?= $i ?>][public-key]" value="<?= $ssh_key['key'] ?>" type="text"></code>
</div> </div>
<div> <div>
<label for="dir"><?= _('Allowed directory') ?></label><br> <label for="dir<?= $i ?>"><?= _('Allowed directory') ?></label><br>
<input list="dirs" placeholder="/" value="<?= htmlspecialchars($ssh_key['directory']) ?>" id="dir" name="keys[<?= $i ?>][dir]" type="text"> <input list="dirs" placeholder="/" value="<?= htmlspecialchars($ssh_key['directory']) ?>" id="dir<?= $i ?>" name="keys[<?= $i ?>][dir]" type="text">
</div> </div>
</fieldset> </fieldset>
<?php <?php

View file

@ -15,7 +15,7 @@
<label for="zone"><?= _('Zone') ?></label> <label for="zone"><?= _('Zone') ?></label>
<br> <br>
<select required="" name="zone" id="zone"> <select required="" name="zone" id="zone">
<option value="" disabled="" selected="">-</option> <option value="" disabled="" selected=""></option>
<?php <?php
foreach (nsListUserZones() as $zone) foreach (nsListUserZones() as $zone)
echo "<option value='" . $zone . "'>" . $zone . "</option>"; echo "<option value='" . $zone . "'>" . $zone . "</option>";

View file

@ -38,7 +38,7 @@ if (isset($data['zone-table'])) { ?>
foreach ($data['zone-table'] as $zone_line) { foreach ($data['zone-table'] as $zone_line) {
echo ' <tr>' . LF; echo ' <tr>' . LF;
foreach ($zone_line as $element) foreach ($zone_line as $element)
echo ' <td><code>' . $element . '</code></td>' . LF; echo ' <td><code>' . htmlspecialchars($element) . '</code></td>' . LF;
echo ' </tr>' . LF; echo ' </tr>' . LF;
} }
} }

38
pg-view/ns/sync.php Normal file
View file

@ -0,0 +1,38 @@
<p>
<?= sprintf(_('AAAA, A and CAA records are regularly copied from the source domain to the target domain. Their TTLs are set to %s seconds.'), SYNC_TTL) ?>
</p>
<p>
<?= _('Source domains that are not signed with DNSSEC are not synchronized. Synchronizations that remain broken may be deleted.') ?>
</p>
<p>
<?= _('This is meant to be used for apex domains, where a CNAME record is not allowed. For non-apex domains, CNAME records should be used instead.') ?>
</p>
<form method="post">
<?php
foreach (array_slice(array_merge(query('select', 'ns-syncs', ['username' => $_SESSION['id'] ?? '']), [['source' => '', 'destination' => '—']]), 0, 8) as $i => $sync) {
?>
<fieldset>
<legend><?= ($sync['source'] === '') ? _('Add new domain records to be synchronized') : _('Synchronized record') ?></legend>
<div>
<label for="source<?= $i ?>"><?= _('Source domain') ?></label><br>
<input placeholder="provider.<?= PLACEHOLDER_DOMAIN ?>." id="source<?= $i ?>" name="syncs[<?= $i ?>][source]" value="<?= $sync['source'] ?>" type="text">
</div>
<div>
<label for="destination<?= $i ?>"><?= _('Target domain') ?></label>
<br>
<select required="" name="syncs[<?= $i ?>][destination]" id="destination<?= $i ?>">
<option <?= (($sync['destination'] === '') ? 'value="" disabled=""' : 'value="' . $sync['destination'] . '"') ?> selected=""><?= $sync['destination'] ?></option>
<?php
foreach (nsListUserZones() as $zone)
echo "<option value='" . $zone . "'>" . $zone . "</option>";
?>
</select>
</div>
</fieldset>
<?php
}
?>
<input type="submit" value="<?= _('Update') ?>">
</form>