Add ns/sync and jobs/ns-syncs
This commit is contained in:
parent
ccd17b7ffa
commit
858d6e8d02
14 changed files with 178 additions and 11 deletions
12
db/migrations/006-create-ns-syncs-table.sql
Normal file
12
db/migrations/006-create-ns-syncs-table.sql
Normal 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;
|
|
@ -42,6 +42,14 @@ CREATE TABLE IF NOT EXISTS "zones" (
|
|||
PRIMARY KEY("zone"),
|
||||
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" (
|
||||
"username" TEXT NOT NULL,
|
||||
"site_dir" TEXT NOT NULL,
|
||||
|
|
|
@ -98,9 +98,11 @@ function authDeleteUser(string $user_id): void {
|
|||
foreach (query('select', 'registry', ['username' => $user_id], 'domain') as $domain)
|
||||
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)
|
||||
nsDeleteZone($zone, $user_id);
|
||||
}
|
||||
|
||||
if (in_array('ht', $user_services, true)) {
|
||||
foreach (query('select', 'sites', ['username' => $user_id]) as $site)
|
||||
|
|
|
@ -21,6 +21,24 @@ 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'],
|
||||
'+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 {
|
||||
$query = 'INSERT INTO "' . $table . '"(';
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
<?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 = [];
|
||||
foreach (explode(LF, $zone_content) as $zone_line) {
|
||||
if ($zone_line === '' OR str_starts_with($zone_line, ';'))
|
||||
continue; // Ignore empty lines and comments
|
||||
$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
|
||||
if (!in_array($elements[2], $types, true))
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ const ALLOWED_TYPES = ['AAAA', 'A', 'TXT', 'SRV', 'MX', 'SVCB', 'HTTPS', 'NS', '
|
|||
|
||||
const ZONE_MAX_CHARACTERS = 10000;
|
||||
|
||||
const SYNC_TTL = 3600;
|
||||
|
||||
function nsParseCommonRequirements(): array {
|
||||
nsCheckZonePossession($_POST['zone']);
|
||||
|
||||
|
|
2
init.php
2
init.php
|
@ -7,7 +7,7 @@ set_error_handler(function ($level, $message, $file = '', $line = 0) {
|
|||
set_exception_handler(function ($e) {
|
||||
error_log((string) $e);
|
||||
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
|
||||
if (($error = error_get_last()) !== NULL)
|
||||
|
|
42
jobs/ns-sync.php
Normal file
42
jobs/ns-sync.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -163,6 +163,11 @@ define('PAGES', [
|
|||
'description' => _('Store geographic coordinates'),
|
||||
'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' => [
|
||||
'index' => [
|
||||
|
|
37
pg-act/ns/sync.php
Normal file
37
pg-act/ns/sync.php
Normal 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.'));
|
|
@ -16,12 +16,12 @@ foreach (array_slice(array_merge(query('select', 'ssh-keys', ['username' => $_SE
|
|||
<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>
|
||||
<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<?= $i ?>" 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">
|
||||
<label for="dir<?= $i ?>"><?= _('Allowed directory') ?></label><br>
|
||||
<input list="dirs" placeholder="/" value="<?= htmlspecialchars($ssh_key['directory']) ?>" id="dir<?= $i ?>" name="keys[<?= $i ?>][dir]" type="text">
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<label for="zone"><?= _('Zone') ?></label>
|
||||
<br>
|
||||
<select required="" name="zone" id="zone">
|
||||
<option value="" disabled="" selected="">-</option>
|
||||
<option value="" disabled="" selected="">—</option>
|
||||
<?php
|
||||
foreach (nsListUserZones() as $zone)
|
||||
echo "<option value='" . $zone . "'>" . $zone . "</option>";
|
||||
|
|
|
@ -38,7 +38,7 @@ if (isset($data['zone-table'])) { ?>
|
|||
foreach ($data['zone-table'] as $zone_line) {
|
||||
echo ' <tr>' . LF;
|
||||
foreach ($zone_line as $element)
|
||||
echo ' <td><code>' . $element . '</code></td>' . LF;
|
||||
echo ' <td><code>' . htmlspecialchars($element) . '</code></td>' . LF;
|
||||
echo ' </tr>' . LF;
|
||||
}
|
||||
}
|
||||
|
|
38
pg-view/ns/sync.php
Normal file
38
pg-view/ns/sync.php
Normal 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>
|
Loading…
Reference in a new issue