Direct zone file edition through <textarea>
This commit is contained in:
parent
95095ee6ee
commit
e3f358a62c
12 changed files with 217 additions and 34 deletions
|
@ -23,6 +23,7 @@ Niver is a set of 3 network services:
|
|||
### Nameserver (`ns`)
|
||||
|
||||
* Host a zone on the server
|
||||
* Zone file edition through `<textarea>`
|
||||
* Dedicated forms to set/unset `A`, `AAAA`, `NS`, `TXT`, `CAA`, `SRV`, `MX`, `SRV`, `SSHFP`, `TLSA`, `CNAME`, `DNAME` and `LOC` records
|
||||
* Display your records or the full zone file
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ knot_zones_path = "/srv/niver/ns"
|
|||
servers[] = "ns1.niver.test."
|
||||
servers[] = "ns2.niver.test."
|
||||
kdig_path = "/usr/bin/kdig"
|
||||
kzonecheck_path = "/usr/bin/kzonecheck"
|
||||
; @ must be replaced by a dot
|
||||
public_soa_email = "hostmaster.niver.invalid."
|
||||
|
||||
[ht]
|
||||
enabled = true
|
||||
|
|
|
@ -2,7 +2,7 @@ form {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
input, select {
|
||||
input, select, textarea {
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
margin: 0.3rem;
|
||||
|
@ -52,3 +52,10 @@ fieldset {
|
|||
justify-content: center;
|
||||
border-color: var(--svc-color, --foreground-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
19
css/main.css
19
css/main.css
|
@ -33,20 +33,17 @@ h2 {
|
|||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
header, main > *:not(table, pre), footer {
|
||||
header, main > *:not(table, pre, form), form > *:not(textarea), footer {
|
||||
max-width: 40rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
main > nav {
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
header, main > *, footer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
header {
|
||||
header, footer {
|
||||
text-align: center;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
@ -77,18 +74,16 @@ code {
|
|||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline var(--svc-color) 0.2em;
|
||||
color: var(--foreground-color);
|
||||
text-decoration: underline var(--svc-color) 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline var(--svc-color) 0.25em;
|
||||
color: var(--foreground-color);
|
||||
text-decoration-thickness: 0.25em;
|
||||
}
|
||||
|
||||
a:active {
|
||||
text-decoration: underline var(--svc-color) 0.35em;
|
||||
color: var(--foreground-color);
|
||||
text-decoration-thickness: 0.35em;
|
||||
}
|
||||
|
||||
a[rel=help]:before {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
$final_message = null;
|
||||
$final_message = NULL;
|
||||
|
||||
function output($code, $msg = '', $logs = ['']) {
|
||||
global $final_message;
|
||||
|
|
25
fn/ns.php
25
fn/ns.php
|
@ -1,5 +1,22 @@
|
|||
<?php
|
||||
|
||||
define('SOA_VALUES', [
|
||||
'ttl' => 10800,
|
||||
'email' => CONF['ns']['public_soa_email'],
|
||||
'refresh' => 10800,
|
||||
'retry' => 3600,
|
||||
'expire' => 3628800,
|
||||
'negative' => 10800,
|
||||
]);
|
||||
|
||||
define('MIN_TTL', 300);
|
||||
define('DEFAULT_TTL', 10800);
|
||||
define('MAX_TTL', 1728000);
|
||||
|
||||
define('ALLOWED_TYPES', ['AAAA', 'A', 'TXT', 'SRV', 'MX', 'SVCB', 'HTTPS', 'NS', 'DS', 'CAA', 'CNAME', 'DNAME', 'LOC', 'SSHFP', 'TLSA']);
|
||||
|
||||
define('ZONE_MAX_CHARACTERS', 10000);
|
||||
|
||||
function nsCommonRequirements() {
|
||||
return (isset($_POST['action'])
|
||||
AND isset($_POST['zone'])
|
||||
|
@ -12,15 +29,17 @@ function nsCommonRequirements() {
|
|||
function nsParseCommonRequirements() {
|
||||
nsCheckZonePossession($_POST['zone']);
|
||||
|
||||
if (($_POST['subdomain'] === "") OR ($_POST['subdomain'] === "@"))
|
||||
if (($_POST['subdomain'] === '') OR ($_POST['subdomain'] === '@'))
|
||||
$values['domain'] = $_POST['zone'];
|
||||
else
|
||||
$values['domain'] = formatAbsoluteDomain(formatEndWithDot($_POST['subdomain']) . $_POST['zone']);
|
||||
|
||||
$values['ttl'] = $_POST['ttl-value'] * $_POST['ttl-multiplier'];
|
||||
|
||||
if (!($values['ttl'] >= 300 AND $values['ttl'] <= 432000))
|
||||
output(403, 'Le TTL doit être compris entre 5 minutes et 5 jours (entre 300 et 432000 secondes).');
|
||||
if ($values['ttl'] < MIN_TTL)
|
||||
output(403, 'Les TTLs inférieurs à ' . MIN_TTL . ' secondes ne sont pas autorisés.');
|
||||
if ($values['ttl'] > MAX_TTL)
|
||||
output(403, 'Les TTLs supérieurs à ' . MAX_TTL . ' secondes ne sont pas autorisés.');
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ if (isset($_SESSION['username']))
|
|||
<div>
|
||||
<label for="ttl-value">Valeur</label>
|
||||
<br>
|
||||
<input required="" id="ttl-value" list="ttls" name="ttl-value" size="6" type="number" min="1" max="432000" value="10800" placeholder="10800">
|
||||
<input required="" id="ttl-value" list="ttls" name="ttl-value" size="6" type="number" min="1" max="432000" value="<?= DEFAULT_TTL ?>" placeholder="<?= DEFAULT_TTL ?>">
|
||||
<datalist id="ttls">
|
||||
<option value="900">
|
||||
<option value="1800">
|
||||
|
|
|
@ -80,6 +80,11 @@ define('PAGES', [
|
|||
'title' => 'Afficher les données',
|
||||
'description' => 'Afficher le contenu de la zone',
|
||||
],
|
||||
'edit' => [
|
||||
'title' => 'Modifier une zone',
|
||||
'description' => 'Éditer un fichier de zone',
|
||||
'tokens_account_cost' => 300,
|
||||
],
|
||||
'ip' => [
|
||||
'title' => 'Enregistrements A et AAAA',
|
||||
'description' => 'Indiquer l\'adresse IP d\'un domaine',
|
||||
|
|
142
pages/ns/edit.php
Normal file
142
pages/ns/edit.php
Normal file
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
if (processForm() AND isset($_POST['zone-content'])) { // Update zone
|
||||
nsCheckZonePossession($_POST['zone']);
|
||||
|
||||
// Get current SOA record
|
||||
$current_zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
|
||||
if ($current_zone_content === false)
|
||||
output(500, 'Unable to read zone file.');
|
||||
if (preg_match('/^(?<soa>' . preg_quote($_POST['zone']) . '[\t ]+[0-9]{1,16}[\t ]+SOA[\t ]+.+)$/m', $current_zone_content, $matches) !== 1)
|
||||
output(500, 'Unable to get current serial from zone file.');
|
||||
|
||||
// Generate new zone content
|
||||
$new_zone_content = $matches['soa'] . "\n";
|
||||
if (strlen($_POST['zone-content']) > ZONE_MAX_CHARACTERS)
|
||||
output(403, 'La zone n\'est pas autorisée à dépasser ' . ZONE_MAX_CHARACTERS . ' caractères.');
|
||||
foreach (explode("\r\n", $_POST['zone-content']) as $line) {
|
||||
if ($line === '') continue;
|
||||
if (preg_match('/^(?<domain>[a-z0-9@._-]+)(?:[\t ]+(?<ttl>[0-9]{1,16}))?(?:[\t ]+IN)?[\t ]+(?<type>[A-Z]{1,16})[\t ]+(?<value>.+)$/', $line, $matches) !== 1)
|
||||
output(403, 'La zone est mal formatée (selon Niver).');
|
||||
if (in_array($matches['type'], ALLOWED_TYPES, true) !== true)
|
||||
output(403, 'Le type <code>' . $matches['type'] . '</code> n\'est pas autorisé.');
|
||||
if ($matches['ttl'] !== '' AND $matches['ttl'] < MIN_TTL)
|
||||
output(403, 'Les TTLs inférieurs à ' . MIN_TTL . ' secondes ne sont pas autorisés.');
|
||||
if ($matches['ttl'] !== '' AND $matches['ttl'] > MAX_TTL)
|
||||
output(403, 'Les TTLs supérieurs à ' . MAX_TTL . ' secondes ne sont pas autorisés.');
|
||||
$new_zone_content .= $matches['domain'] . ' ' . (($matches['ttl'] === '') ? DEFAULT_TTL : $matches['ttl']) . ' ' . $matches['type'] . ' ' . $matches['value'] . "\n";
|
||||
}
|
||||
|
||||
// Send the zone content to kzonecheck's stdin
|
||||
$process = proc_open(CONF['ns']['kzonecheck_path'] . ' --origin ' . $_POST['zone'] . ' --dnssec off -', [0 => ['pipe', 'r']], $pipes);
|
||||
if (is_resource($process) !== true)
|
||||
output(500, 'Can\'t spawn kzonecheck.');
|
||||
fwrite($pipes[0], $new_zone_content);
|
||||
fclose($pipes[0]);
|
||||
if (proc_close($process) !== 0)
|
||||
output(403, 'Le contenu de zone envoyé n\'est pas valide (selon <code>kzonecheck</code>).');
|
||||
|
||||
ratelimit();
|
||||
|
||||
exec(CONF['dns']['knotc_path'] . ' --blocking --timeout 10 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 10 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 10 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 10 zone-thaw ' . $_POST['zone'], $output, $return_code);
|
||||
if ($return_code !== 0)
|
||||
output(500, 'Failed to thaw zone file.', $output);
|
||||
|
||||
usleep(1000000);
|
||||
|
||||
output(200, 'La zone a été mise à jour.');
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<form method="post">
|
||||
<label for="zone">Zone à modifier</label>
|
||||
<br>
|
||||
<select required="" name="zone" id="zone">
|
||||
<option value="" disabled="" selected="">-</option>
|
||||
<?php
|
||||
if (isset($_SESSION['username']))
|
||||
foreach (nsListUserZones($_SESSION['username']) as $zone)
|
||||
echo ' <option value="' . $zone . '">' . $zone . '</option>' . "\n";
|
||||
?>
|
||||
</select>
|
||||
<br>
|
||||
<input type="submit" value="Afficher">
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
if (processForm()) { // Display zone
|
||||
nsCheckZonePossession($_POST['zone']);
|
||||
|
||||
$zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
|
||||
if ($zone_content === false)
|
||||
output(500, 'Unable to read zone file.');
|
||||
|
||||
$displayed_zone_content = '';
|
||||
foreach(explode("\n", $zone_content) as $zone_line) {
|
||||
if (empty($zone_line) OR str_starts_with($zone_line, ';'))
|
||||
continue;
|
||||
if (preg_match('/^(?:(?:[a-z0-9_-]{1,63}\.){1,127})?' . preg_quote($_POST['zone'], '/') . '[\t ]+[0-9]{1,8}[\t ]+(?<type>[A-Z]{1,16})[\t ]+.+$/', $zone_line, $matches)) {
|
||||
if (in_array($matches['type'], ALLOWED_TYPES, true) !== true)
|
||||
continue;
|
||||
$displayed_zone_content .= $zone_line . "\n";
|
||||
}
|
||||
}
|
||||
$displayed_zone_content .= "\n";
|
||||
|
||||
?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="zone" value="<?= $_POST['zone'] ?>">
|
||||
|
||||
<label for="zone-content">Nouveau contenu de la zone <code><strong><?= $_POST['zone'] ?></strong></code></label>
|
||||
<textarea id="zone-content" name="zone-content" wrap="off" rows="<?= substr_count($displayed_zone_content, "\n") + 1 ?>"><?= htmlspecialchars($displayed_zone_content) ?></textarea>
|
||||
<br>
|
||||
<input type="submit" value="Remplacer">
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
global $final_message;
|
||||
displayFinalMessage();
|
||||
|
||||
?>
|
||||
|
||||
<h2>Valeurs par défaut</h2>
|
||||
|
||||
<p>Si le TTL est omis, il sera définit à <code><?= DEFAULT_TTL ?></code> secondes.</p>
|
||||
|
||||
<p>La précision de la classe (<code>IN</code>) est facultative.</p>
|
||||
|
||||
<h2>Valeurs autorisées</h2>
|
||||
|
||||
<p>La zone n'est pas autorisée à dépasser <?= ZONE_MAX_CHARACTERS ?> caractères.</p>
|
||||
|
||||
<p>Les TTLs ne sont autorisés qu'entre <code><?= MIN_TTL ?></code> et <code><?= MAX_TTL ?></code> secondes.</p>
|
||||
|
||||
<p>Les seuls types dont l'édition est autorisée sont :</p>
|
||||
|
||||
<ul>
|
||||
<?php
|
||||
foreach (ALLOWED_TYPES as $allowed_type)
|
||||
echo ' <li><code>' . $allowed_type . '</code></li>';
|
||||
|
||||
?>
|
||||
</ul>
|
|
@ -50,7 +50,7 @@ if (processForm()) {
|
|||
if (str_starts_with($zoneLine, ';')) continue; // Ignore comments
|
||||
if (empty($zoneLine)) continue;
|
||||
$elements = preg_split("#[\t ]+#", $zoneLine, 4);
|
||||
if (!in_array($elements[2], ['CAA', 'A', 'AAAA', 'MX', 'NS', 'SRV', 'SSHFP', 'TLSA', 'TXT'], true)) continue; // Ignore records generated by Knot
|
||||
if (!in_array($elements[2], ALLOWED_TYPES, true)) continue; // Ignore records generated by Knot
|
||||
echo ' <tr>';
|
||||
foreach ($elements as $element)
|
||||
echo ' <td><code>' . htmlspecialchars($element) . '</code></td>';
|
||||
|
@ -61,15 +61,10 @@ if (processForm()) {
|
|||
|
||||
if ($_POST['print'] === 'ds') {
|
||||
|
||||
$found = preg_match("#\n" . preg_quote($_POST['zone']) . "\s+0\s+CDS\s+([0-9]{1,5})\s+([0-9]{1,2})\s+([0-9])\s+([0-9A-F]{64})\n#", $zoneContent, $matches);
|
||||
$found = preg_match('/^' . preg_quote($_POST['zone']) . '[\t ]+0[\t ]+CDS[\t ]+(?<tag>[0-9]{1,5})[\t ]+(?<algo>[0-9]{1,2})[\t ]+(?<digest_type>[0-9])[\t ]+(?<digest>[0-9A-F]{64})$/m', $zoneContent, $matches);
|
||||
if ($found !== 1)
|
||||
output(500, 'Unable to get public key record from zone file.');
|
||||
|
||||
$tag = $matches[1];
|
||||
$algo = $matches[2];
|
||||
$digestType = $matches[3];
|
||||
$digest = $matches[4];
|
||||
|
||||
?>
|
||||
|
||||
<dl>
|
||||
|
@ -79,19 +74,19 @@ if (processForm()) {
|
|||
</dd>
|
||||
<dt>Tag</dt>
|
||||
<dd>
|
||||
<code><?= $tag ?></code>
|
||||
<code><?= $matches['tag'] ?></code>
|
||||
</dd>
|
||||
<dt>Algorithme</dt>
|
||||
<dd>
|
||||
<code><?= $algo ?></code><?php if ($algo === "15") echo " (Ed25519)"; ?>
|
||||
<code><?= $matches['algo'] ?></code><?php if ($matches['algo'] === '15') echo ' (Ed25519)'; ?>
|
||||
</dd>
|
||||
<dt>Type de condensat</dt>
|
||||
<dd>
|
||||
<code><?= $digestType ?></code><?php if ($digestType === "2") echo " (SHA-256)"; ?>
|
||||
<code><?= $matches['digest_type'] ?></code><?php if ($matches['digest_type'] === '2') echo ' (SHA-256)'; ?>
|
||||
</dd>
|
||||
<dt>Condensat</dt>
|
||||
<dd>
|
||||
<code><?= $digest ?></code>
|
||||
<code><?= $matches['digest'] ?></code>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
|
|
@ -13,10 +13,10 @@ if (processForm()) {
|
|||
checkAbsoluteDomainFormat($parentAuthoritative);
|
||||
|
||||
exec(CONF['ns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . $parentAuthoritatives[0] . ' +noidn', $results);
|
||||
if (preg_match('/\n' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+([0-9a-f]{8})-([0-9a-f]{32})\.auth-owner.+\n/', implode("\n", $results), $matches) !== 1)
|
||||
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})\.auth-owner.+$/m', implode("\n", $results), $matches) !== 1)
|
||||
output(403, 'Enregistrement d\'authentification introuvable');
|
||||
|
||||
checkAuthToken($matches[1], $matches[2]);
|
||||
checkAuthToken($matches['salt'], $matches['hash']);
|
||||
|
||||
rateLimit();
|
||||
|
||||
|
@ -26,7 +26,18 @@ if (processForm()) {
|
|||
]);
|
||||
|
||||
$knotZonePath = CONF['ns']['knot_zones_path'] . "/" . $_POST['domain'] . "zone";
|
||||
$knotZone = $_POST['domain'] . ' 3600 SOA ' . CONF['ns']['servers'][0] . ' admin.niver.test. 1 21600 7200 3628800 3600' . "\n";
|
||||
$knotZone = implode(' ', [
|
||||
$_POST['domain'],
|
||||
SOA_VALUES['ttl'],
|
||||
'SOA',
|
||||
CONF['ns']['servers'][0],
|
||||
SOA_VALUES['email'],
|
||||
1,
|
||||
SOA_VALUES['refresh'],
|
||||
SOA_VALUES['retry'],
|
||||
SOA_VALUES['expire'],
|
||||
SOA_VALUES['negative'],
|
||||
]) . "\n";
|
||||
foreach (CONF['ns']['servers'] as $server)
|
||||
$knotZone .= $_POST['domain'] . ' 86400 NS ' . $server . "\n";
|
||||
if (is_int(file_put_contents($knotZonePath, $knotZone)) !== true)
|
||||
|
|
|
@ -117,11 +117,16 @@ if (in_array(SERVICE, ['reg', 'ns', 'ht']) AND CONF[SERVICE]['enabled'] !== true
|
|||
if (empty($_POST) === false AND (isset($_SERVER['HTTP_SEC_FETCH_SITE']) !== true OR $_SERVER['HTTP_SEC_FETCH_SITE'] !== "same-origin"))
|
||||
output(403, 'Anti-<abbr title="Cross-Site Request Forgery">CSRF</abbr> verification failed ! (Wrong or unset <code>Sec-Fetch-Site</code> HTTP header)');
|
||||
|
||||
function displayFinalMessage() {
|
||||
global $final_message;
|
||||
echo $final_message ?? '';
|
||||
$final_message = NULL;
|
||||
}
|
||||
|
||||
function executePage() {
|
||||
require "pages/" . PAGE_ADDRESS . ".php";
|
||||
|
||||
global $final_message;
|
||||
echo $final_message ?? '';
|
||||
displayFinalMessage();
|
||||
?>
|
||||
</main>
|
||||
<footer>
|
||||
|
|
Loading…
Reference in a new issue