Merge pull request 'dev' (#9) from dev into main

Reviewed-on: #9
This commit is contained in:
Miraty 2023-08-02 00:52:33 +02:00
commit 95f376ab00
102 changed files with 1586 additions and 814 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,13 @@ 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,
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,

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
const USERNAME_REGEX = '^.{1,1024}$';
const PASSWORD_REGEX = '^(?=.*[\p{Ll}])(?=.*[\p{Lu}])(?=.*[\p{N}]).{8,1024}|.{10,1024}$';
@ -14,47 +14,47 @@ const OPTIONS_PASSWORD = [
'threads' => 64,
];
function checkUsernameFormat($username) {
function checkUsernameFormat(string $username): void {
if (preg_match('/' . USERNAME_REGEX . '/Du', $username) !== 1)
output(403, 'Username malformed.');
}
function checkPasswordFormat($password) {
function checkPasswordFormat(string $password): void {
if (preg_match('/' . PASSWORD_REGEX . '/Du', $password) !== 1)
output(403, 'Password malformed.');
}
function hashUsername($username) {
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));
}
function hashPassword($password) {
function hashPassword(string $password): string {
return password_hash($password, ALGO_PASSWORD, OPTIONS_PASSWORD);
}
function usernameExists($username) {
function usernameExists(string $username): bool {
return isset(query('select', 'users', ['username' => $username], 'id')[0]);
}
function checkPassword($id, $password) {
function checkPassword(string $id, string $password): bool {
return password_verify($password, query('select', 'users', ['id' => $id], 'password')[0]);
}
function outdatedPasswordHash($id) {
function outdatedPasswordHash(string $id): bool {
return password_needs_rehash(query('select', 'users', ['id' => $id], 'password')[0], ALGO_PASSWORD, OPTIONS_PASSWORD);
}
function changePassword($id, $password) {
function changePassword(string $id, string $password): void {
DB->prepare('UPDATE users SET password = :password WHERE id = :id')
->execute([':password' => hashPassword($password), ':id' => $id]);
}
function stopSession() {
function stopSession(): void {
if (session_status() === PHP_SESSION_ACTIVE)
session_destroy();
}
function logout() {
function logout(): never {
stopSession();
header('Clear-Site-Data: "*"');
@ -62,7 +62,7 @@ function logout() {
redir();
}
function setupDisplayUsername($display_username) {
function setupDisplayUsername(string $display_username): void {
$nonce = random_bytes(24);
$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
$cyphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
@ -87,7 +87,7 @@ function setupDisplayUsername($display_username) {
$_SESSION['display-username-cyphertext'] = $cyphertext;
}
function authDeleteUser($user_id) {
function authDeleteUser(string $user_id): void {
$user_services = explode(',', query('select', 'users', ['id' => $user_id], 'services')[0]);
foreach (SERVICES_USER as $service)
@ -133,12 +133,14 @@ function authDeleteUser($user_id) {
], result_code: $code);
if ($code !== 0)
output(500, 'Can\'t remove user\'s directory.');
query('delete', 'ssh-keys', ['username' => $user_id]);
}
query('delete', 'users', ['id' => $user_id]);
}
function rateLimit() {
function rateLimit(): void {
if (PAGE_METADATA['tokens_account_cost'] ?? 0 > 0)
rateLimitAccount(PAGE_METADATA['tokens_account_cost']);
@ -147,7 +149,7 @@ function rateLimit() {
}
const MAX_ACCOUNT_TOKENS = 86400;
function rateLimitAccount($requestedTokens) {
function rateLimitAccount(int $requestedTokens): int {
// Get
$userData = query('select', 'users', ['id' => $_SESSION['id']]);
$tokens = $userData[0]['bucket_tokens'];
@ -174,7 +176,7 @@ function rateLimitAccount($requestedTokens) {
return $tokens;
}
function rateLimitInstance($requestedTokens) {
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];

View File

@ -1,6 +1,6 @@
<?php
<?php declare(strict_types=1);
function output($code, $msg = '', $logs = [''], $data = []) {
function output(int $code, string $msg = '', array $logs = [''], array $data = []): never {
http_response_code($code);
$shortCode = intval($code / 100);
if ($shortCode === 5)
@ -21,7 +21,25 @@ function exescape(array $args, array &$output = NULL, int &$result_code = NULL):
return $result_code;
}
function insert($table, $values) {
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 . '"(';
foreach ($values as $key => $val) {
@ -44,7 +62,7 @@ function insert($table, $values) {
->execute($values);
}
function query($action, $table, $conditions = [], $column = NULL) {
function query(string $action, string $table, array $conditions = [], string $column = NULL): array {
$query = match ($action) {
'select' => 'SELECT *',
@ -66,7 +84,7 @@ function query($action, $table, $conditions = [], $column = NULL) {
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), $column);
}
function displayIndex() { ?>
function displayIndex(): void { ?>
<nav>
<dl>
<?php foreach (PAGES[SERVICE] as $pageId => $page) {
@ -82,11 +100,11 @@ function displayIndex() { ?>
<?php
}
function redirUrl($pageId) {
function redirUrl(string $pageId): string {
return CONF['common']['prefix'] . '/' . $pageId . '?redir=' . PAGE_URL;
}
function redir($redir_to = NULL) {
function redir(string $redir_to = NULL): never {
$redir_to ??= $_GET['redir'] ?? NULL;
if ($redir_to === NULL) {
@ -100,7 +118,7 @@ function redir($redir_to = NULL) {
}
// PHP rmdir() only works on empty directories
function removeDirectory($dir) {
function removeDirectory(string $dir): void {
$dirObj = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($dirObj, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file)
@ -109,7 +127,7 @@ function removeDirectory($dir) {
output(500, 'Unable to remove directory.');
}
function equalArrays($a, $b) {
function equalArrays(array $a, array $b): bool {
return array_diff($a, $b) === [] AND array_diff($b, $a) === [];
}
@ -126,12 +144,12 @@ if (time() - query('select', 'params', ['name' => 'secret_key_last_change'], 'va
->execute([':last_change' => time()]);
}
define('SECRET_KEY', hex2bin(query('select', 'params', ['name' => 'secret_key'], 'value')[0]));
function getAuthToken() {
function getAuthToken(): string {
$salt = bin2hex(random_bytes(4));
$hash = hash_hmac('sha256', $salt . ($_SESSION['id'] ?? ''), SECRET_KEY);
return $salt . '-' . substr($hash, 0, 32);
}
function checkAuthToken($salt, $hash) {
function checkAuthToken(string $salt, string $hash): void {
$correctProof = substr(hash_hmac('sha256', $salt . $_SESSION['id'], SECRET_KEY), 0, 32);
if (hash_equals($correctProof, $hash) !== true)
output(403, _('Wrong proof.'));

View File

@ -1,16 +1,19 @@
<?php
<?php declare(strict_types=1);
function parseZoneFile($zone_content, $types, $filter_domain = false) {
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;
}
@ -46,7 +49,7 @@ function knotcConfExec(array $cmds): void {
}
}
function knotcZoneExec(string $zone, array $cmd, string $action = NULL) {
function knotcZoneExec(string $zone, array $cmd, string $action = NULL): void {
$action = checkAction($action ?? $_POST['action']);
knotc(['zone-begin', $zone], $output['begin'], $code['begin']);
@ -66,32 +69,32 @@ function knotcZoneExec(string $zone, array $cmd, string $action = NULL) {
}
}
function checkIpFormat($ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))
return 'A';
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))
return 'AAAA';
output(403, _('IP address malformed.'));
function checkIpFormat(string $ip): string {
return match ($ip) {
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) => 'AAAA',
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) => 'A',
default => output(403, _('IP address malformed.')),
};
}
function checkAbsoluteDomainFormat($domain) { // If the domain must end with a dot
function checkAbsoluteDomainFormat(string $domain): void { // If the domain must end with a dot
if (!filter_var($domain, FILTER_VALIDATE_DOMAIN) OR preg_match('/^(?=^.{1,254}$)([a-z0-9_-]{1,63}\.){2,127}$/D', $domain) !== 1)
output(403, _('Domain malformed.'));
}
function formatEndWithDot($str) {
function formatEndWithDot(string $str): string {
if (!str_ends_with($str, '.'))
$str .= '.';
return $str;
}
function formatAbsoluteDomain($domain) {
function formatAbsoluteDomain(string $domain): string {
$domain = formatEndWithDot(strtolower($domain));
checkAbsoluteDomainFormat($domain);
return $domain;
}
function checkAction($action) {
function checkAction(string $action): string {
return match ($action) {
'add' => '',
'delete' => 'un',

View File

@ -1,9 +1,9 @@
<?php
<?php declare(strict_types=1);
const SUBPATH_REGEX = '^[a-z0-9-]{4,63}$';
const ED25519_PUBKEY_REGEX = '^[a-zA-Z0-9/+]{68}$';
function htSetupUserFs($id) {
function htSetupUserFs(string $id): void {
// Setup SFTP directory
if (mkdir(CONF['ht']['ht_path'] . '/fs/' . $id, 0000) !== true)
output(500, 'Can\'t create user directory.');
@ -42,19 +42,19 @@ function htSetupUserFs($id) {
output(500, 'Can\'t create Tor keys directory.');
}
function checkDomainFormat($domain) {
function checkDomainFormat(string $domain): void {
// If the domain must end without a dot
if (!filter_var($domain, FILTER_VALIDATE_DOMAIN) OR !preg_match('/^(?=^.{1,254}$)([a-z0-9_-]{1,63}\.){1,126}[a-z0-9]{1,63}$/D', $domain))
output(403, _('Domain malformed.'));
}
function formatDomain($domain) {
function formatDomain(string $domain): string {
$domain = rtrim(strtolower($domain), '.');
checkDomainFormat($domain);
return $domain;
}
function listFsDirs($username) {
function listFsDirs(string $username): array {
if ($username === '')
return [];
$absoluteDirs = glob(CONF['ht']['ht_path'] . '/fs/' . $username . '/*/', GLOB_ONLYDIR);
@ -65,7 +65,7 @@ function listFsDirs($username) {
return $dirs;
}
function addSite($username, $siteDir, $address, $type) {
function addSite(string $username, string $siteDir, string $address, string $type): void {
insert('sites', [
'username' => $username,
'site_dir' => $siteDir,
@ -75,7 +75,7 @@ function addSite($username, $siteDir, $address, $type) {
]);
}
function dirsStatuses($type) {
function dirsStatuses(string $type): array {
if (isset($_SESSION['id']) !== true)
return [];
$dbDirs = query('select', 'sites', [
@ -88,7 +88,7 @@ function dirsStatuses($type) {
return $dirs;
}
function htRelativeSymlink($target, $name) {
function htRelativeSymlink(string $target, string $name): void {
chdir(pathinfo($name)['dirname']);
$symlink = symlink($target, pathinfo($name)['basename']);
chdir(ROOT_PATH);
@ -96,7 +96,7 @@ function htRelativeSymlink($target, $name) {
output(500, 'Unable to create symlink.');
}
function htDeleteSite($address, $type, $user_id) {
function htDeleteSite(string $address, string $type, string $user_id): void {
if ($type === 'onion') {
$dir = query('select', 'sites', [

View File

@ -1,6 +1,6 @@
<?php
<?php declare(strict_types=1);
const SOA_VALUES = [
const NS_SOA_VALUES = [
'ttl' => 10800,
'email' => CONF['ns']['public_soa_email'],
'refresh' => 10800,
@ -9,15 +9,17 @@ const SOA_VALUES = [
'negative' => 10800,
];
const MIN_TTL = 300;
const DEFAULT_TTL = 10800;
const MAX_TTL = 1728000;
const NS_MIN_TTL = 300;
const NS_DEFAULT_TTL = 10800;
const NS_MAX_TTL = 1728000;
const ALLOWED_TYPES = ['AAAA', 'A', 'TXT', 'SRV', 'MX', 'SVCB', 'HTTPS', 'NS', 'DS', 'CAA', 'CNAME', 'DNAME', 'LOC', 'SSHFP', 'TLSA'];
const NS_ALLOWED_TYPES = ['AAAA', 'A', 'TXT', 'SRV', 'MX', 'SSHFP', 'TLSA', 'NS', 'DS', 'CSYNC', 'CAA', 'CNAME', 'DNAME', 'SVCB', 'HTTPS', 'LOC'];
const ZONE_MAX_CHARACTERS = 10000;
const NS_TEXTAREA_MAX_CHARACTERS = 10000;
function nsParseCommonRequirements() {
const NS_SYNC_TTL = 10800;
function nsParseCommonRequirements(): array {
nsCheckZonePossession($_POST['zone']);
if (($_POST['subdomain'] === '') OR ($_POST['subdomain'] === '@'))
@ -27,28 +29,28 @@ function nsParseCommonRequirements() {
$values['ttl'] = intval($_POST['ttl-value'] * $_POST['ttl-multiplier']);
if ($values['ttl'] < MIN_TTL)
output(403, sprintf(_('TTLs shorter than %s seconds are forbidden.'), MIN_TTL));
if ($values['ttl'] > MAX_TTL)
output(403, sprintf(_('TTLs longer than %s seconds are forbidden.'), MAX_TTL));
if ($values['ttl'] < NS_MIN_TTL)
output(403, sprintf(_('TTLs shorter than %s seconds are forbidden.'), NS_MIN_TTL));
if ($values['ttl'] > NS_MAX_TTL)
output(403, sprintf(_('TTLs longer than %s seconds are forbidden.'), NS_MAX_TTL));
return $values;
}
function nsListUserZones() {
function nsListUserZones(): array {
if (isset($_SESSION['id']))
return query('select', 'zones', ['username' => $_SESSION['id']], 'zone');
return [];
}
function nsCheckZonePossession($zone) {
function nsCheckZonePossession(string $zone): void {
checkAbsoluteDomainFormat($zone);
if (!in_array($zone, nsListUserZones(), true))
output(403, 'You don\'t own this zone on the name server.');
}
function nsDeleteZone($zone, $user_id) {
function nsDeleteZone(string $zone, string $user_id): void {
// Remove from Knot configuration
knotcConfExec([['conf-unset', 'zone[' . $zone . ']']]);
@ -71,6 +73,8 @@ function nsDeleteZone($zone, $user_id) {
if ($code !== 0)
output(500, 'Failed to purge zone data.');
query('delete', 'ns-syncs', ['destination' => $zone]);
// Remove from database
query('delete', 'zones', [
'zone' => $zone,

View File

@ -1,26 +1,31 @@
<?php
<?php declare(strict_types=1);
const SUBDOMAIN_REGEX = '^(?!\-)(?!..\-\-)[a-z0-9-]{4,63}(?<!\-)$';
function regListUserDomains() {
const REG_TEXTAREA_MAX_CHARACTERS = 5000;
const REG_ALLOWED_TYPES = ['NS', 'DS', 'AAAA', 'A'];
function regListUserDomains(): array {
if (isset($_SESSION['id']))
return query('select', 'registry', ['username' => $_SESSION['id']], 'domain');
return [];
}
function regCheckDomainPossession($domain) {
function regCheckDomainPossession(string $domain): void {
if (in_array($domain, regListUserDomains(), true) !== true)
output(403, 'You don\'t own this domain on the registry.');
}
function regDeleteDomain($domain, $user_id) {
function regStripDomain(string $domain, string $content): string {
return preg_replace('/^(?:[a-z0-9._-]+\.)?' . preg_quote($domain, '/') . '[\t ]+.+$/Dm', '', $content);
}
function regDeleteDomain(string $domain, string $user_id): void {
// Delete domain from registry file
$path = CONF['reg']['suffixes_path'] . '/' . regParseDomain($domain)['suffix'] . 'zone';
$content = file_get_contents($path);
if ($content === false)
if (($content = file_get_contents($path)) === false)
output(500, 'Failed to read current registry file.');
$content = preg_replace('/^(?:[a-z0-9._-]+\.)?' . preg_quote($domain, '/') . '[\t ]+.+$/Dm', '', $content);
if (file_put_contents($path, $content) === false)
if (file_put_contents($path, regStripDomain($domain, $content)) === false)
output(500, 'Failed to write new registry file.');
try {
@ -46,7 +51,7 @@ function regDeleteDomain($domain, $user_id) {
}
}
function regParseDomain($domain) {
function regParseDomain(string $domain): array {
$parts = explode('.', $domain, 2);
$subdomain = $parts[0];
$suffix = $parts[1];
@ -59,3 +64,53 @@ function regParseDomain($domain) {
'suffix' => $suffix,
];
}
function regParseRecord(string $domain, array $record): array {
checkAbsoluteDomainFormat($record['domain']);
if ($record['domain'] !== $_POST['domain']) {
if ($record['type'] !== 'ip')
output(403, _('You can only set a NS/DS record for an apex domain.'));
else if (!str_ends_with($record['domain'], '.' . $domain))
output(403, _('You can\'t set a record for another domain.'));
}
$new_rec = [
$record['domain'],
CONF['reg']['ttl'],
$record['type'],
];
if ($record['type'] === 'DS') {
if (!in_array($record['algo'], ['8', '13', '14', '15', '16'], true))
output(403, 'Wrong value for <code>algo</code>.');
if ((preg_match('/^[0-9]{1,6}$/D', $record['keytag'])) !== 1 OR !($record['keytag'] >= 1) OR !($record['keytag'] <= 65535))
output(403, 'Wrong value for <code>keytag</code>.');
if ($record['dt'] !== '2' AND $record['dt'] !== '4')
output(403, 'Wrong value for <code>dt</code>.');
if (preg_match('/^(?:[0-9a-fA-F]{64}|[0-9a-fA-F]{96})$/D', $record['key']) !== 1)
output(403, 'Wrong value for <code>key</code>.');
return [
...$new_rec,
$record['keytag'],
$record['algo'],
$record['dt'],
$record['key'],
];
}
if ($record['type'] === 'NS')
return [
...$new_rec,
formatAbsoluteDomain($record['ns']),
];
if ($record['type'] === 'ip')
return [
$record['domain'],
CONF['reg']['ttl'],
checkIpFormat($record['ip']),
$record['ip'],
];
}

View File

@ -1,13 +1,13 @@
<?php
<?php declare(strict_types=1);
umask(0077);
set_error_handler(function ($level, $message, $file = '', $line = 0) {
throw new ErrorException($message, 0, $level, $file, $line);
});
set_exception_handler(function ($e) {
error_log($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)

View File

@ -1,6 +1,7 @@
<?php // Test that the current setup is working
<?php declare(strict_types=1);
// Test that the current setup is working
require 'init.php';
require __DIR__ . '/../init.php';
const SFTP = '/usr/bin/sftp';
const SSHPASS = '/usr/bin/sshpass';
@ -18,7 +19,7 @@ if (preg_match('/^;; Flags: qr rd ra ad;/Dm', implode("\n", $output)) !== 1)
define('COOKIE_FILE', sys_get_temp_dir() . '/cookie-' . bin2hex(random_bytes(16)) . '.txt');
function curlTest($address, $post = [], $tor = false) {
function curlTest(string $address, array $post = [], bool $tor = false): string {
$req = curl_init();
curl_setopt($req, CURLOPT_RETURNTRANSFER, true);
@ -47,7 +48,7 @@ function curlTest($address, $post = [], $tor = false) {
if ($status_code >= 400 OR $result === false) {
var_dump($result);
var_dump(curl_error($req));
exit($address . ' test failed with status code ' . $status_code . LF);
throw new Exception($address . ' test failed with status code ' . $status_code);
}
return $result;
}
@ -60,13 +61,6 @@ curlTest('/auth/register', [
'password' => $password,
]);
curlTest('/auth/logout', []);
curlTest('/auth/login', [
'username' => $username,
'password' => $password,
]);
$new_password = bin2hex(random_bytes(16));
curlTest('/auth/password', [
'current-password' => $password,
@ -74,9 +68,20 @@ curlTest('/auth/password', [
]);
$password = $new_password;
curlTest('/auth/register', [
'username' => $username . '2',
'password' => $password,
]);
curlTest('/auth/logout', []);
curlTest('/auth/login', [
'username' => $username,
'password' => $password,
]);
curlTest('/auth/username', [
'current-password' => $password,
'new-username' => $username . '2',
'new-username' => $username . '3',
]);
curlTest('/auth/username', [
'current-password' => $password,
@ -85,7 +90,9 @@ curlTest('/auth/username', [
echo 'Created account with username "' . $username . '" and password "' . $password . '".' . LF;
function testReg() {
function testReg(): string {
global $username, $password;
$subdomain = bin2hex(random_bytes(16));
curlTest('/reg/register', [
@ -116,10 +123,43 @@ function testReg() {
'ns' => 'ns1.servnest.invalid.',
]);
{ // Domain transfer
curlTest('/auth/logout', []);
curlTest('/auth/login', [
'username' => $username . '2',
'password' => $password,
]);
preg_match('#\<code\>(?<token>[0-9a-z-]{16,128}\._transfer-verification\.' . preg_quote(CORE_DOMAIN, '#') . '\.)\</code\>#', curlTest('/reg/transfer', []), $matches);
curlTest('/auth/logout', []);
curlTest('/auth/login', [
'username' => $username,
'password' => $password,
]);
curlTest('/reg/ns', [
'action' => 'add',
'domain' => $domain,
'ns' => $matches['token'],
]);
curlTest('/auth/logout', []);
curlTest('/auth/login', [
'username' => $username . '2',
'password' => $password,
]);
curlTest('/reg/transfer', [
'subdomain' => $subdomain,
'suffix' => SUFFIX,
'ns' => $matches['token'],
]);
$username = $username . '2';
}
return $domain;
}
function testNs($domain) {
function testNs(string $domain): void {
foreach (CONF['ns']['servers'] as $ns)
curlTest('/reg/ns', [
'action' => 'add',
@ -164,8 +204,8 @@ function testNs($domain) {
exit('Error: /ns/caa: CAA record not set' . LF);
curlTest('/ns/edit', [
'zone' => $domain,
'zone-content' => 'aaaa.' . $domain . ' 3600 AAAA ' . CONF['ht']['ipv6_address'] . "\r\n"
'domain' => $domain,
'records' => 'aaaa.' . $domain . ' 3600 AAAA ' . CONF['ht']['ipv6_address'] . "\r\n"
. '@ 86400 NS ' . CONF['ns']['servers'][0] . "\r\n",
]);
exescape([
@ -178,7 +218,7 @@ function testNs($domain) {
exit('Error: /ns/edit: AAAA record not set' . LF);
}
function testHt($username, $password) {
function testHt(string $username, string $password): void {
define('TEST_CONTENT', 'test-' . bin2hex(random_bytes(16)));
file_put_contents(sys_get_temp_dir() . '/index.html', TEST_CONTENT);

View File

@ -1,5 +1,5 @@
<?php
require 'init.php';
<?php declare(strict_types=1);
require __DIR__ . '/../init.php';
const MAX_TESTING_ACCOUNT_AGE = 86400 * 10;

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

@ -0,0 +1,42 @@
<?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
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'],
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');
}
}

57
jobs/reg-cds.php Normal file
View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
/*
RFC 7344: Automating DNSSEC Delegation Trust Maintenance
RFC 8078: Managing DS Records from the Parent via CDS/CDNSKEY
*/
require __DIR__ . '/../init.php';
foreach (query('select', 'registry') as $entry) {
$suffix = regParseDomain($entry['domain'])['suffix'];
// Get child/distant records
try {
$results = kdig(name: $entry['domain'], type: 'CDS');
} catch (KdigException) {
fwrite(STDERR, $entry['domain'] . ' resolution failed.' . LF);
continue;
}
if ($results['AD'] !== 1) // No DNSSEC
continue;
$cds_records = array_column($results['answerRRs'] ?? [], 'rdataCDS');
// Skip if no updates
if ($cds_records === [])
continue;
// Get parent/local CDS records
$zone_raw = file_get_contents(CONF['reg']['suffixes_path'] . '/' . $suffix . 'zone');
if ($zone_raw === false)
output(403, 'Unable to read zone file.');
$pds_records = array_column(parseZoneFile($zone_raw, ['DS'], $entry['domain'], false), 3);
if ($cds_records === ['0 0 0 0'])
// Delete every parent DS records
foreach ($pds_records as $value_to_delete)
knotcZoneExec($suffix, [
$entry['domain'],
'DS',
$value_to_delete,
], 'delete');
else {
// Add child DS records that are not yet in parent
foreach (array_diff($cds_records, $pds_records) as $value_to_add)
knotcZoneExec($suffix, [
$entry['domain'],
CONF['reg']['ttl'],
'DS',
$value_to_add,
], 'add');
// Delete parent DS records that are not part of child anymore
foreach (array_diff($pds_records, $cds_records) as $value_to_delete)
knotcZoneExec($suffix, [
$entry['domain'],
'DS',
$value_to_delete,
], 'delete');
}
}

61
jobs/reg-csync.php Normal file
View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
/*
RFC 7477: Child-to-Parent Synchronization in DNS
*/
require __DIR__ . '/../init.php';
foreach (query('select', 'registry') as $entry) {
$suffix = regParseDomain($entry['domain'])['suffix'];
// Get child/distant CSYNC records
try {
$results = kdig(name: $entry['domain'], type: 'CSYNC');
} catch (KdigException) {
fwrite(STDERR, $entry['domain'] . ' CSYNC resolution failed.' . LF);
continue;
}
if ($results['AD'] !== 1)
continue;
if (count($results['answerRRs'] ?? []) !== 1) // Parental agents MUST ignore a child's CSYNC RDATA set if multiple CSYNC resource records are found; only a single CSYNC record should ever be present.
continue;
list($serial, $flags, $types) = explode(' ', $results['answerRRs'][0]['rdataCSYNC'], 3);
if ($flags !== '1')
continue; // Skip unsupported flags
if ($types !== 'NS')
continue; // Skip unsupported types
// Get child/distant NS records
try {
$results = kdig(name: $entry['domain'], type: 'NS');
} catch (KdigException) {
fwrite(STDERR, $entry['domain'] . ' NS resolution failed.' . LF);
continue;
}
if ($results['AD'] !== 1)
continue;
$child_ns_records = array_column($results['answerRRs'] ?? [], 'rdataNS');
if (count($child_ns_records) === []) // Parental agents MUST NOT perform NS updates if there are no NS records returned in a query, as verified by DNSSEC denial-of-existence protection.
continue;
// Get parent/local NS records
$zone_raw = file_get_contents(CONF['reg']['suffixes_path'] . '/' . $suffix . 'zone');
if ($zone_raw === false)
output(403, 'Unable to read zone file.');
$parent_ns_records = array_column(parseZoneFile($zone_raw, ['NS'], $entry['domain'], false), 3);
// Add child NS records that are not yet in parent
foreach (array_diff($child_ns_records, $parent_ns_records) as $value_to_add)
knotcZoneExec($suffix, [
$entry['domain'],
CONF['reg']['ttl'],
'NS',
$value_to_add,
], 'add');
// Delete parent NS records that are not part of child anymore
foreach (array_diff($parent_ns_records, $child_ns_records) as $value_to_delete)
knotcZoneExec($suffix, [
$entry['domain'],
'NS',
$value_to_delete,
], 'delete');
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
define('PAGES', [
'index' => [
@ -63,6 +63,11 @@ define('PAGES', [
'description' => _('Print every record related to a domain and served by the registry'),
'tokens_account_cost' => 60,
],
'edit' => [
'title' => _('Edit records'),
'description' => _('Set registry records to delegate a domain to chosen name servers'),
'tokens_account_cost' => 900,
],
'ns' => [
'title' => sprintf(_('%s records'), '<abbr title="Name Server">NS</abbr>'),
'description' => sprintf(_('Indicate the name servers of a %s subdomain'), '<code>' . key(CONF['reg']['suffixes']) . '</code>'),
@ -163,6 +168,11 @@ define('PAGES', [
'description' => _('Store geographic coordinates'),
'tokens_account_cost' => 120,
],
'sync' => [
'title' => sprintf(_('Synchronized records')),
'description' => _('Regularly fetch distant records and update them to a local zone'),
'tokens_account_cost' => 900,
],
],
'ht' => [
'index' => [

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if ($_SESSION['type'] !== 'testing')
output(403, _('This account is already approved.'));

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
checkPasswordFormat($_POST['password']);

View File

@ -1,3 +0,0 @@
<?php
logout();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
checkPasswordFormat($_POST['new-password']);

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (CONF['common']['services']['auth'] !== 'enabled')
output(403, _('Registrations are currently closed on this installation.'));

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (checkPassword($_SESSION['id'], $_POST['current-password']) !== true)
output(403, _('Wrong current password.'));

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
checkUsernameFormat($_POST['new-username']);

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$_POST['domain'] = formatDomain($_POST['domain']);

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (dirsStatuses('onion')[$_POST['dir']] !== false)
output(403, 'Wrong value for <code>dir</code>.');

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (dirsStatuses('subdomain')[$_POST['dir']] !== false)
output(403, 'Wrong value for <code>dir</code>.');

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (dirsStatuses('subpath')[$_POST['dir']] !== false)
output(403, 'Wrong value for <code>dir</code>.');

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
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>.');

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$el_nb = count($_POST['keys']);
if ($el_nb < 1 OR $el_nb > 8)

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,60 +1,62 @@
<?php
<?php declare(strict_types=1);
nsCheckZonePossession($_POST['zone']);
nsCheckZonePossession($_POST['domain']);
if (isset($_POST['zone-content'])) { // Update zone
$path = CONF['ns']['knot_zones_path'] . '/' . $_POST['domain'] . 'zone';
if (isset($_POST['records'])) {
// Get current SOA record
$current_zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
$current_zone_content = file_get_contents($path);
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 ]+.+)$/Dm', $current_zone_content, $matches) !== 1)
if (preg_match('/^(?<soa>' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,16}[\t ]+SOA[\t ]+.+)$/Dm', $current_zone_content, $matches) !== 1)
output(500, 'Unable to get current SOA record from zone file.');
// Generate new zone content
// Generate new content
$new_zone_content = $matches['soa'] . LF;
if (strlen($_POST['zone-content']) > ZONE_MAX_CHARACTERS)
output(403, sprintf(_('The zone is limited to %s characters.'), ZONE_MAX_CHARACTERS));
foreach (explode("\r\n", $_POST['zone-content']) as $line) {
if ($line === '') continue;
if (preg_match('/^(?<domain>[a-z0-9@._-]{1,256})(?:[\t ]+(?<ttl>[0-9]{1,16}))?(?:[\t ]+IN)?[\t ]+(?<type>[A-Z]{1,16})[\t ]+(?<value>.+)$/D', $line, $matches) !== 1)
output(403, _('The following line does not match the expected format: ') . '<code>' . htmlspecialchars($line) . '</code>');
if (in_array($matches['type'], ALLOWED_TYPES, true) !== true)
if (strlen($_POST['records']) > NS_TEXTAREA_MAX_CHARACTERS)
output(403, sprintf(_('The zone is limited to %s characters.'), NS_TEXTAREA_MAX_CHARACTERS));
foreach (explode("\r\n", $_POST['records']) as $record) {
if ($record === '') continue;
if (preg_match('/^(?<domain>[a-z0-9@._-]{1,256})(?:[\t ]+(?<ttl>[0-9]{1,16}))?(?:[\t ]+IN)?[\t ]+(?<type>[A-Z]{1,16})[\t ]+(?<value>.+)$/D', $record, $matches) !== 1)
output(403, _('The following line does not match the expected format: ') . '<code>' . htmlspecialchars($record) . '</code>');
if (in_array($matches['type'], NS_ALLOWED_TYPES, true) !== true)
output(403, sprintf(_('The %s type is not allowed.'), '<code>' . $matches['type'] . '</code>'));
if ($matches['ttl'] !== '' AND $matches['ttl'] < MIN_TTL)
output(403, sprintf(_('TTLs shorter than %s seconds are forbidden.'), MIN_TTL));
if ($matches['ttl'] !== '' AND $matches['ttl'] > MAX_TTL)
output(403, sprintf(_('TTLs longer than %s seconds are forbidden.'), MAX_TTL));
$new_zone_content .= $matches['domain'] . ' ' . (($matches['ttl'] === '') ? DEFAULT_TTL : $matches['ttl']) . ' ' . $matches['type'] . ' ' . $matches['value'] . LF;
if ($matches['ttl'] !== '' AND $matches['ttl'] < NS_MIN_TTL)
output(403, sprintf(_('TTLs shorter than %s seconds are forbidden.'), NS_MIN_TTL));
if ($matches['ttl'] !== '' AND $matches['ttl'] > NS_MAX_TTL)
output(403, sprintf(_('TTLs longer than %s seconds are forbidden.'), NS_MAX_TTL));
$new_zone_content .= $matches['domain'] . ' ' . (($matches['ttl'] === '') ? NS_DEFAULT_TTL : $matches['ttl']) . ' ' . $matches['type'] . ' ' . $matches['value'] . LF;
}
// Send the zone content to kzonecheck's stdin
$process = proc_open(CONF['ns']['kzonecheck_path'] . ' --origin ' . $_POST['zone'] . ' --dnssec off -', [0 => ['pipe', 'r']], $pipes);
$process = proc_open(CONF['ns']['kzonecheck_path'] . ' --origin ' . escapeshellarg($_POST['domain']) . ' --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, _('Sent zone content is not correct (according to <code>kzonecheck</code>).'));
output(403, _('Sent content is not correct (according to <code>kzonecheck</code>).'));
ratelimit();
knotc(['zone-freeze', $_POST['zone']], $output, $return_code);
knotc(['zone-freeze', $_POST['domain']], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to freeze zone file.', $output);
knotc(['zone-flush', $_POST['zone']], $output, $return_code);
knotc(['zone-flush', $_POST['domain']], $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)
if (file_put_contents($path, $new_zone_content) === false)
output(500, 'Failed to write zone file.');
knotc(['zone-reload', $_POST['zone']], $output, $return_code);
knotc(['zone-reload', $_POST['domain']], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to reload zone file.', $output);
knotc(['zone-thaw', $_POST['zone']], $output, $return_code);
knotc(['zone-thaw', $_POST['domain']], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to thaw zone file.', $output);
@ -63,18 +65,17 @@ if (isset($_POST['zone-content'])) { // Update zone
// Display zone
$zone_content = file_get_contents(CONF['ns']['knot_zones_path'] . '/' . $_POST['zone'] . 'zone');
if ($zone_content === false)
if (($records = file_get_contents($path)) === false)
output(500, 'Unable to read zone file.');
$data['zone_content'] = '';
foreach (explode(LF, $zone_content) as $zone_line) {
$data['records'] = '';
foreach (explode(LF, $records) 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 ]+.+$/D', $zone_line, $matches)) {
if (in_array($matches['type'], ALLOWED_TYPES, true) !== true)
if (preg_match('/^(?:(?:[a-z0-9_-]{1,63}\.){1,127})?' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+(?<type>[A-Z]{1,16})[\t ]+.+$/D', $zone_line, $matches)) {
if (in_array($matches['type'], NS_ALLOWED_TYPES, true) !== true)
continue;
$data['zone_content'] .= $zone_line . LF;
$data['records'] .= $zone_line . LF;
}
}
$data['zone_content'] .= LF;
$data['records'] .= LF;

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
nsCheckZonePossession($_POST['zone']);

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

41
pg-act/ns/sync.php Normal file
View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
$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']);
$destinations = array_column($syncs, 'destination');
if (count($destinations) !== count(array_unique($destinations)))
output(403, _('Multiple source domains can\'t be applied to the same target domain.'));
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

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$values = nsParseCommonRequirements();

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
$_POST['domain'] = formatAbsoluteDomain($_POST['domain']);
@ -41,18 +41,19 @@ insert('zones', [
$knotZonePath = CONF['ns']['knot_zones_path'] . '/' . $_POST['domain'] . 'zone';
$knotZone = implode(' ', [
$_POST['domain'],
SOA_VALUES['ttl'],
NS_SOA_VALUES['ttl'],
'SOA',
CONF['ns']['servers'][0],
SOA_VALUES['email'],
NS_SOA_VALUES['email'],
1,
SOA_VALUES['refresh'],
SOA_VALUES['retry'],
SOA_VALUES['expire'],
SOA_VALUES['negative'],
NS_SOA_VALUES['refresh'],
NS_SOA_VALUES['retry'],
NS_SOA_VALUES['expire'],
NS_SOA_VALUES['negative'],
]) . LF;
foreach (CONF['ns']['servers'] as $server)
$knotZone .= $_POST['domain'] . ' 86400 NS ' . $server . LF;
$knotZone .= $_POST['domain'] . ' 86400 CSYNC 0 1 NS' . LF;
if (is_int(file_put_contents($knotZonePath, $knotZone)) !== true)
output(500, 'Failed to write new zone file.');
if (chmod($knotZonePath, 0660) !== true)

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
nsCheckZonePossession($_POST['zone']);

View File

@ -1,30 +1,12 @@
<?php
<?php declare(strict_types=1);
if (!in_array($_POST['algo'], ['8', '13', '14', '15', '16'], true))
output(403, 'Wrong value for <code>algo</code>.');
$_POST['keytag'] = intval($_POST['keytag']);
if ((preg_match('/^[0-9]{1,6}$/D', $_POST['keytag'])) !== 1 OR !($_POST['keytag'] >= 1) OR !($_POST['keytag'] <= 65535))
output(403, 'Wrong value for <code>keytag</code>.');
if ($_POST['dt'] !== '2' AND $_POST['dt'] !== '4')
output(403, 'Wrong value for <code>dt</code>.');
if (preg_match('/^(?:[0-9a-fA-F]{64}|[0-9a-fA-F]{96})$/D', $_POST['key']) !== 1)
output(403, 'Wrong value for <code>key</code>.');
regCheckDomainPossession($_POST['zone']);
regCheckDomainPossession($_POST['domain']);
rateLimit();
knotcZoneExec(regParseDomain($_POST['zone'])['suffix'], [
$_POST['zone'],
CONF['reg']['ttl'],
'DS',
$_POST['keytag'],
$_POST['algo'],
$_POST['dt'],
$_POST['key']
]);
knotcZoneExec(regParseDomain($_POST['domain'])['suffix'], regParseRecord($_POST['domain'], [
'type' => 'DS',
...$_POST,
]));
output(200, _('Modification done.'));

97
pg-act/reg/edit.php Normal file
View File

@ -0,0 +1,97 @@
<?php declare(strict_types=1);
regCheckDomainPossession($_POST['domain']);
$suffix = regParseDomain($_POST['domain'])['suffix'];
$path = CONF['reg']['suffixes_path'] . '/' . $suffix . 'zone';
if (isset($_POST['records'])) {
// Generate new content
$new_records = '';
if (strlen($_POST['records']) > REG_TEXTAREA_MAX_CHARACTERS)
output(403, sprintf(_('The zone is limited to %s characters.'), REG_TEXTAREA_MAX_CHARACTERS));
foreach (explode("\r\n", $_POST['records']) as $record) {
if ($record === '') continue;
if (preg_match('/^(?<domain>[a-z0-9@._-]{1,256})(?:[\t ]+(?<ttl>[0-9]{1,16}))?(?:[\t ]+IN)?[\t ]+(?<type>[A-Z]{1,16})[\t ]+(?<value>.+)$/D', $record, $matches) !== 1)
output(403, _('The following line does not match the expected format: ') . '<code>' . htmlspecialchars($record) . '</code>');
if (in_array($matches['type'], REG_ALLOWED_TYPES, true) !== true)
output(403, sprintf(_('The %s type is not allowed.'), '<code>' . $matches['type'] . '</code>'));
if ($matches['type'] === 'DS' AND count($record_values = explode(' ', $matches['value'])) !== 4)
output(403, _('A DS record expects 4 arguments.'));
$new_records .= implode(' ', regParseRecord($_POST['domain'], [
'domain' => $matches['domain'],
'type' => match ($matches['type']) {
'NS', 'DS' => $matches['type'],
'AAAA', 'A' => 'ip',
default => output(403, sprintf(_('The %s type is not allowed.'), '<code>' . $matches['type'] . '</code>')),
},
...match ($matches['type']) {
'NS' => ['ns' => $matches['value']],
'AAAA', 'A' => ['ip' => $matches['value']],
'DS' => array_combine([
'keytag',
'algo',
'dt',
'key',
], $record_values),
},
])) . LF;
}
// Send the zone content to kzonecheck's stdin
$process = proc_open(CONF['ns']['kzonecheck_path'] . ' --origin ' . escapeshellarg($suffix) . ' --dnssec off -', [0 => ['pipe', 'r']], $pipes);
if (is_resource($process) !== true)
output(500, 'Can\'t spawn kzonecheck.');
$new = $suffix . ' 10800 SOA invalid. invalid. 0 21600 7200 3628800 3600' . LF . $suffix . ' 10800 NS invalid.' . LF . $new_records;
fwrite($pipes[0], $new);
fclose($pipes[0]);
if (proc_close($process) !== 0)
output(403, _('Sent content is not correct (according to <code>kzonecheck</code>).'));
ratelimit();
knotc(['zone-freeze', $suffix], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to freeze zone file.', $output);
knotc(['zone-flush', $suffix], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to flush zone file.', $output);
if (($zone_content = file_get_contents($path)) === false)
output(500, 'Unable to read zone file.');
$zone_content = regStripDomain($_POST['domain'], $zone_content);
if (file_put_contents($path, $zone_content . LF . $new_records) === false)
output(500, 'Failed to write zone file.');
knotc(['zone-reload', $suffix], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to reload zone file.', $output);
knotc(['zone-thaw', $suffix], $output, $return_code);
if ($return_code !== 0)
output(500, 'Failed to thaw zone file.', $output);
usleep(1000000);
}
// Display zone
if (($records = file_get_contents($path)) === false)
output(500, 'Unable to read zone file.');
$data['records'] = '';
foreach (explode(LF, $records) 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['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+(?<type>[A-Z]{1,16})[\t ]+.+$/D', $zone_line, $matches)) {
if (in_array($matches['type'], REG_ALLOWED_TYPES, true) !== true)
continue;
$data['records'] .= $zone_line . LF;
}
}
$data['records'] .= LF;

View File

@ -1,14 +1,13 @@
<?php
<?php declare(strict_types=1);
regCheckDomainPossession($_POST['suffix']);
regCheckDomainPossession($_POST['domain']);
rateLimit();
knotcZoneExec(regParseDomain($_POST['suffix'])['suffix'], [
formatAbsoluteDomain(formatEndWithDot($_POST['subdomain']) . $_POST['suffix']),
CONF['reg']['ttl'],
checkIpFormat($_POST['ip']),
$_POST['ip']
]);
knotcZoneExec(regParseDomain($_POST['domain'])['suffix'], regParseRecord($_POST['domain'], [
'type' => 'ip',
'domain' => formatAbsoluteDomain(formatEndWithDot($_POST['subdomain']) . $_POST['domain']),
...$_POST,
]));
output(200, _('Modification done.'));

View File

@ -1,14 +1,12 @@
<?php
<?php declare(strict_types=1);
regCheckDomainPossession($_POST['domain']);
rateLimit();
knotcZoneExec(regParseDomain($_POST['domain'])['suffix'], [
$_POST['domain'],
CONF['reg']['ttl'],
'NS',
formatAbsoluteDomain($_POST['ns'])
]);
knotcZoneExec(regParseDomain($_POST['domain'])['suffix'], regParseRecord($_POST['domain'], [
'type' => 'NS',
...$_POST,
]));
output(200, _('Modification done.'));

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
regCheckDomainPossession($_POST['domain']);

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['subdomain']) !== 1)
output(403, _('This format of subdomain is not allowed.'));
@ -53,7 +53,7 @@ if ($blocked OR $registration_data !== [])
if ($_POST['action'] !== 'register')
message($message . ' ✔️ ' . _('This domain is open to registration!'));
function message($message) {
function message(string $message): never {
output(200, data: [
'message' => '<p>' . $message . '</p>',
'domain' => htmlspecialchars($_POST['subdomain']),

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
if (preg_match('/' . SUBDOMAIN_REGEX . '/D', $_POST['subdomain']) !== 1)
output(403, _('This format of subdomain is not allowed.'));

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
regCheckDomainPossession($_POST['domain']);

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('This form allows to use an approval key to validate your account. Approval keys are distributed by an administrator upon request.') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<?php displayIndex(); ?>
<h2 id="type"><?= _('Account type') ?></h2>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p><?= _('New?') ?> <a href="register"><?= _('Create an account') ?></a></p>
<form method="post">

View File

@ -1,3 +1,3 @@
<?php
<?php declare(strict_types=1);
logout();

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="current-password"><?= _('Current password') ?></label><br>
<input required="" autocomplete="current-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="current-password" name="current-password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>"><br>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p><?= _('Already have an account?') ?> <a href="login"><?= _('Log in') ?></a></p>
<?= (CONF['common']['services']['auth'] !== 'enabled') ? '<p><strong>' . _('Registrations are currently closed on this installation.') . '</strong></p>' : '' ?>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('This will delete every resource managed by the current account, including registered domains, hosted DNS records, websites files and cryptographic keys for Onion services and DNSSEC.') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="current-password"><?= _('Current password') ?></label><br>
<input required="" autocomplete="current-password" minlength="8" maxlength="1024" pattern="<?= PASSWORD_REGEX ?>" id="current-password" name="current-password" type="password" placeholder="<?= PLACEHOLDER_PASSWORD ?>"><br>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('A Let\'s Encrypt certificate will be obtained.') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="dir"><?= _('Target directory') ?></label><br>
<select required="" name="dir" id="dir">

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= sprintf(_('The subdomain can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters. It can\'t have an hyphen (%3$s) in first, last or both third and fourth position.'), '<abbr title="abcdefghijklmnopqrstuvwxyz"><code>a</code>-<code>z</code></abbr>', '<abbr title="0123456789"><code>0</code>-<code>9</code></abbr>', '<code>-</code>') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= sprintf(_('The path can only contain %1$s, %2$s and %3$s, and must be between 4 and 63 characters.'), '<abbr title="abcdefghijklmnopqrstuvwxyz"><code>a</code>-<code>z</code></abbr>', '<abbr title="0123456789"><code>0</code>-<code>9</code></abbr>', '<code>-</code>') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="site"><?= _('Access to delete') ?></label><br>
<select required="" name="site" id="site">
@ -10,7 +11,7 @@ foreach (query('select', 'sites', ['username' => $_SESSION['id'] ?? '']) as $sit
'onion' => 'http://' . $site['address'] . '/',
'dns' => 'https://' . $site['address'] . '/',
};
echo ' <option value="' . $site['type'] . ':' . $site['address'] . '">' . $url . ' vers /' . $site['site_dir'] . '</option>' . LF;
echo ' <option value="' . $site['type'] . ':' . $site['address'] . '">' . ' ' . sprintf(_('%1$s to %2$s'), $url, '/' . $site['site_dir']) . '</option>' . LF;
}
?>
</select>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('This service allows you to send files on the server using SFTP, and to make them publicly available with HTTP.') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content.') ?>
</p>
@ -16,12 +17,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

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<nav>
<p>
<span aria-hidden="true">➡️ </span><em><a href="<?= CONF['common']['about_url'] ?>"><?= _('About this installation') ?></a></em>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="flag"><?= _('Flag') ?></label>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="cname"><?= _('Canonical name') ?></label>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="dname"><?= _('Delegation name') ?></label>

View File

@ -1,11 +1,12 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="zone"><?= _('Zone to be changed') ?></label>
<label for="domain"><?= _('Zone to be changed') ?></label>
<br>
<select required="" name="zone" id="zone">
<select required="" name="domain" id="domain">
<option value="" disabled="" selected="">-</option>
<?php
foreach (nsListUserZones() as $zone)
echo ' <option value="' . $zone . '">' . $zone . '</option>' . LF;
foreach (nsListUserZones() as $domain)
echo ' <option value="' . $domain . '">' . $domain . '</option>' . LF;
?>
</select>
<br>
@ -14,15 +15,15 @@ foreach (nsListUserZones() as $zone)
<?php
if (isset($data['zone_content'])) { // Display zone
if (isset($data['records'])) { // Display zone
?>
<form method="post">
<input type="hidden" name="zone" value="<?= $_POST['zone'] ?>">
<input type="hidden" name="domain" value="<?= $_POST['domain'] ?>">
<label for="zone-content"><?= sprintf(_('New content of the %s zone'), '<code><strong>' . $_POST['zone'] . '</strong></code>') ?></label>
<label for="zone-content"><?= sprintf(_('Authoritative records for %s'), '<code><strong>' . $_POST['domain'] . '</strong></code>') ?></label>
<br>
<textarea id="zone-content" name="zone-content" wrap="off" rows="<?= substr_count($data['zone_content'], LF) + 1 ?>"><?= htmlspecialchars($data['zone_content']) ?></textarea>
<textarea id="records" name="records" wrap="off" rows="<?= substr_count($data['records'], LF) + 1 ?>"><?= htmlspecialchars($data['records']) ?></textarea>
<br>
<input type="submit" value="<?= _('Replace') ?>">
</form>
@ -37,21 +38,21 @@ displayFinalMessage($data);
<h2><?= _('Default values') ?></h2>
<p><?= sprintf(_('If the TTL is omitted, it will default to %s seconds.'), '<code><time datetime="PT' . DEFAULT_TTL . 'S">' . DEFAULT_TTL . '</time></code>') ?></p>
<p><?= sprintf(_('If the TTL is omitted, it will default to %s seconds.'), '<code><time datetime="PT' . NS_DEFAULT_TTL . 'S">' . NS_DEFAULT_TTL . '</time></code>') ?></p>
<p><?= _('Precising the class (<code>IN</code>) is optional.') ?></p>
<h2><?= _('Allowed values') ?></h2>
<p><?= sprintf(_('Submitted zone content is limited to %s characters.'), ZONE_MAX_CHARACTERS) ?></p>
<p><?= sprintf(_('Submitted field content is limited to %s characters.'), NS_TEXTAREA_MAX_CHARACTERS) ?></p>
<p><?= sprintf(_('TTLs must last between %1$s and %2$s seconds.'), '<code><time datetime="PT' . MIN_TTL . 'S">' . MIN_TTL . '</time></code>', '<code><time datetime="PT' . MAX_TTL . 'S">' . MAX_TTL . '</time></code>') ?></p>
<p><?= sprintf(_('TTLs must last between %1$s and %2$s seconds.'), '<code><time datetime="PT' . NS_MIN_TTL . 'S">' . NS_MIN_TTL . '</time></code>', '<code><time datetime="PT' . NS_MAX_TTL . 'S">' . NS_MAX_TTL . '</time></code>') ?></p>
<p><?= _('The only types that can be defined are:') ?></p>
<p><?= _('The only types that can be defined here are:') ?></p>
<ul>
<?php
foreach (ALLOWED_TYPES as $allowed_type)
foreach (NS_ALLOWED_TYPES as $allowed_type)
echo ' <li><code>' . $allowed_type . '</code></li>';
?>
</ul>

View File

@ -1,7 +1,8 @@
<?php declare(strict_types=1); ?>
<label for="action"><?= _('Action') ?></label>
<select name="action" id="action">
<option value="add"><?= _('Add') ?></option>
<option value="delete"><?= _('Delete') ?></option>
<option value="add"<?= ($_POST['action'] ?? NULL) === 'add' ? ' selected=""' : '' ?>><?= _('Add') ?></option>
<option value="delete"<?= ($_POST['action'] ?? NULL) === 'delete' ? ' selected=""' : '' ?>><?= _('Delete') ?></option>
</select>
<fieldset>
@ -9,18 +10,19 @@
<div>
<label for="subdomain"><?= _('Subdomain') ?></label>
<br>
<input id="subdomain" size="16" placeholder="www" pattern="^(([a-z0-9_-]{1,63}\.?){1,127})|(@){1}$" name="subdomain" type="text">
<input id="subdomain" size="16" placeholder="www" pattern="^(([a-z0-9_-]{1,63}\.?){1,127})|(@){1}$" name="subdomain" type="text" value="<?= htmlspecialchars($_POST['subdomain'] ?? '') ?>">
</div>
<div>
<label for="zone"><?= _('Zone') ?></label>
<br>
<select required="" name="zone" id="zone">
<option value="" disabled="" selected="">-</option>
<?php
foreach (nsListUserZones() as $zone)
echo "<option value='" . $zone . "'>" . $zone . "</option>";
$user_zones = nsListUserZones();
if (!in_array($_POST['zone'] ?? NULL, $user_zones, true))
echo ' <option value="" disabled="" selected="">—</option>' . LF;
foreach ($user_zones as $zone)
echo ' <option value="' . $zone . '"' . (($_POST['zone'] ?? NULL) === $zone ? ' selected=""' : '') . '>.' . $zone . '</option>' . LF;
?>
</select>
</div>
</fieldset>
@ -30,7 +32,7 @@ foreach (nsListUserZones() as $zone)
<div>
<label for="ttl-value"><?= _('Value') ?></label>
<br>
<input required="" id="ttl-value" list="ttls" name="ttl-value" size="6" type="number" min="1" max="432000" value="<?= DEFAULT_TTL ?>" placeholder="<?= DEFAULT_TTL ?>">
<input required="" id="ttl-value" list="ttls" name="ttl-value" size="6" type="number" min="1" max="432000" value="<?= $_POST['ttl-value'] ?? NS_DEFAULT_TTL ?>" placeholder="<?= NS_DEFAULT_TTL ?>">
<datalist id="ttls">
<option value="900">
<option value="1800">
@ -45,10 +47,10 @@ foreach (nsListUserZones() as $zone)
<label for="ttl-multiplier"><?= _('Unit') ?></label>
<br>
<select required="" name="ttl-multiplier" id="ttl-multiplier">
<option value="1"><?= _('second') ?></option>
<option value="60"><?= _('minute') ?></option>
<option value="3600"><?= _('hour') ?></option>
<option value="86400"><?= _('day') ?></option>
<option value="1"<?= ($_POST['ttl-multiplier'] ?? NULL) === '1' ? ' selected=""' : '' ?>><?= _('second') ?></option>
<option value="60"<?= ($_POST['ttl-multiplier'] ?? NULL) === '60' ? ' selected=""' : '' ?>><?= _('minute') ?></option>
<option value="3600"<?= ($_POST['ttl-multiplier'] ?? NULL) === '3600' ? ' selected=""' : '' ?>><?= _('hour') ?></option>
<option value="86400"<?= ($_POST['ttl-multiplier'] ?? NULL) === '86400' ? ' selected=""' : '' ?>><?= _('day') ?></option>
</select>
</div>
</fieldset>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('This service allows to host and manage DNS records inside a DNS zone.') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="ip"><?= _('IP address') ?></label><br>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<fieldset>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="priority"><?= _('Priority') ?></label>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="ns"><?= _('Name server') ?></label>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<input type="radio" name="print" id="table" value="table" checked="">
<label for="table"><?= _('Records table') ?></label>
@ -38,7 +39,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;
}
}

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>

39
pg-view/ns/sync.php Normal file
View File

@ -0,0 +1,39 @@
<?php declare(strict_types=1); ?>
<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 CNAME records are 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 domain') ?></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 (array_diff(nsListUserZones(), query('select', 'ns-syncs', ['username' => $_SESSION['id'] ?? ''], 'destination')) as $zone)
echo "<option value='" . $zone . "'>" . $zone . "</option>";
?>
</select>
</div>
</fieldset>
<?php
}
?>
<input type="submit" value="<?= _('Update') ?>">
</form>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<?php require ROOT_PATH . '/pg-view/ns/form.ns.php'; ?>
<label for="txt"><?= _('Text') ?></label>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= sprintf(_('To prove that you own this domain, it must have a NS record equal to %s when the form is being processed.'), '<code>' . getAuthToken() . '._domain-verification.' . SERVER_NAME . '.</code>') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="zone"><?= _('Zone') ?></label>
<select required="" name="zone" id="zone">

View File

@ -1,19 +1,8 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="action"><?= _('Action') ?></label>
<select name="action" id="action">
<option value="add"><?= _('Add') ?></option>
<option value="delete"><?= _('Delete') ?></option>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-action.inc.php'; ?>
<br>
<label for="zone"><?= _('Domain') ?></label>
<br>
<select required="" name="zone" id="zone">
<option value="" disabled="" selected=""></option>
<?php
foreach (regListUserDomains() as $domain)
echo ' <option value="' . $domain . '">' . $domain . '</option>' . LF;
?>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-domain.inc.php'; ?>
<br>
<label for="keytag"><?= _('Key tag') ?></label>
<br>

54
pg-view/reg/edit.php Normal file
View File

@ -0,0 +1,54 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="domain"><?= _('Domain to be changed') ?></label>
<br>
<select required="" name="domain" id="domain">
<option value="" disabled="" selected="">-</option>
<?php
foreach (regListUserDomains() as $domain)
echo ' <option value="' . $domain . '">' . $domain . '</option>' . LF;
?>
</select>
<br>
<input type="submit" value="<?= _('Display') ?>">
</form>
<?php
if (isset($data['records'])) { // Display zone
?>
<form method="post">
<input type="hidden" name="domain" value="<?= $_POST['domain'] ?>">
<label for="records"><?= sprintf(_('Delegation records for %s'), '<code><strong>' . $_POST['domain'] . '</strong></code>') ?></label>
<br>
<textarea id="records" name="records" wrap="off" rows="<?= substr_count($data['records'], LF) + 1 ?>"><?= htmlspecialchars($data['records']) ?></textarea>
<br>
<input type="submit" value="<?= _('Replace') ?>">
</form>
<?php
}
displayFinalMessage($data);
?>
<h2><?= _('Input values') ?></h2>
<p><?= _('Precising the class (<code>IN</code>) is optional.') ?></p>
<p><?= sprintf(_('Submitted field content is limited to %s characters.'), REG_TEXTAREA_MAX_CHARACTERS) ?></p>
<p><?= sprintf(_('TTL values are ignored and always set to %s seconds.'), '<code><time datetime="PT' . CONF['reg']['ttl'] . 'S">' . CONF['reg']['ttl'] . '</time></code>') ?></p>
<p><?= _('The only types that can be defined here are:') ?></p>
<ul>
<?php
foreach (REG_ALLOWED_TYPES as $allowed_type)
echo ' <li><code>' . $allowed_type . '</code></li>';
?>
</ul>

View File

@ -1,9 +1,6 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="action"><?= _('Action') ?></label>
<select name="action" id="action">
<option value="add"><?= _('Add') ?></option>
<option value="delete"><?= _('Delete') ?></option>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-action.inc.php'; ?>
<fieldset>
<legend><?= _('Domain') ?></legend>
<div>
@ -12,15 +9,7 @@
<input required="" id="subdomain" placeholder="ns1" name="subdomain" type="text">
</div>
<div>
<label for="suffix"><?= _('Domain') ?></label>
<br>
<select required="" name="suffix" id="suffix">
<option value="" disabled="" selected=""></option>
<?php
foreach (regListUserDomains() as $suffix)
echo ' <option value="' . $suffix . '">' . $suffix . '</option>' . LF;
?>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-domain.inc.php'; ?>
</div>
</fieldset>
<label for="ip"><?= _('IP address') ?></label><br>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= sprintf(_('This domain name registry allows to register domains ending with <code>%1$s</code>, for instance <code><em>domain</em>%1$s</code>.'), '.' . key(CONF['reg']['suffixes'])) ?>
</p>
@ -30,7 +31,9 @@ else {
<?php
foreach (CONF['reg']['suffixes'] as $suffix => $condition)
if ($condition === 'all')
echo '<li><code>' . $suffix . ' </code></li>';
echo '<li><code>' . $suffix . ' </code></li>' . LF;
if (!in_array('all', CONF['reg']['suffixes'], true))
echo '<li>∅</li>' . LF;
?>
</ul>
</dd>
@ -41,7 +44,9 @@ foreach (CONF['reg']['suffixes'] as $suffix => $condition)
<?php
foreach (CONF['reg']['suffixes'] as $suffix => $condition)
if ($condition === 'approved')
echo '<li><code>' . $suffix . ' </code></li>';
echo '<li><code>' . $suffix . ' </code></li>' . LF;
if (!in_array('approved', CONF['reg']['suffixes'], true))
echo '<li>∅</li>' . LF;
?>
</ul>
</dd>
@ -52,9 +57,23 @@ foreach (CONF['reg']['suffixes'] as $suffix => $condition)
<?php
foreach (CONF['reg']['suffixes'] as $suffix => $condition)
if ($condition === 'none')
echo '<li><code>' . $suffix . ' </code></li>';
echo '<li><code>' . $suffix . ' </code></li>' . LF;
if (!in_array('none', CONF['reg']['suffixes'], true))
echo '<li>∅</li>' . LF;
?>
</ul>
</dd>
</dl>
</section>
<section>
<h2><?= _('Automatic updates from child zone') ?></h2>
<h3><?= _('CSYNC records') ?></h3>
<p>
<?= _('The registry can synchronize NS records from the child zone if a CSYNC record is present at the apex of the child zone, has flags <code>1</code> and type bit map <code>NS</code>, and can be DNSSEC-validated. Others values are not supported.') ?>
</p>
<h3><?= _('DNSSEC and DS records') ?></h3>
<p>
<?= _('Once DNSSEC has been manually enabled through the current interface, the delegated zone can publish a CDS record in order to update the DS record in the registry. A single CDS record with value <code>0 0 0 0</code> tells the registry to disable DNSSEC. Using a CDS record to enable DNSSEC is not supported.') ?>
</p>
</section>

View File

@ -1,19 +1,8 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="action"><?= _('Action') ?></label>
<select name="action" id="action">
<option value="add"><?= _('Add') ?></option>
<option value="delete"><?= _('Delete') ?></option>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-action.inc.php'; ?>
<br>
<label for="domain"><?= _('Domain') ?></label>
<br>
<select required="" name="domain" id="domain">
<option value="" disabled="" selected=""></option>
<?php
foreach (regListUserDomains() as $domain)
echo ' <option value="' . $domain . '">' . $domain . '</option>' . LF;
?>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-domain.inc.php'; ?>
<br>
<label for="ns"><?= _('Name server') ?></label>
<br>

View File

@ -1,12 +1,6 @@
<?php declare(strict_types=1); ?>
<form method="post">
<label for="domain"><?= _('Domain') ?></label>
<select required="" name="domain" id="domain">
<option value="" disabled="" selected="">-</option>
<?php
foreach (regListUserDomains() as $domain)
echo ' <option value="' . $domain . '">' . $domain . '</option>' . LF;
?>
</select>
<?php require ROOT_PATH . '/pg-view/reg/select-domain.inc.php'; ?>
<br>
<input type="submit" value="<?= _('Display') ?>">
</form>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('Register a new domain on your account.') ?>
</p>

View File

@ -0,0 +1,6 @@
<?php declare(strict_types=1); ?>
<label for="action"><?= _('Action') ?></label>
<select name="action" id="action">
<option value="add"<?= ($_POST['action'] ?? NULL) === 'add' ? ' selected=""' : '' ?>><?= _('Add') ?></option>
<option value="delete"<?= ($_POST['action'] ?? NULL) === 'delete' ? ' selected=""' : '' ?>><?= _('Delete') ?></option>
</select>

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1); ?>
<label for="domain"><?= _('Domain') ?></label>
<br>
<select required="" name="domain" id="domain">
<?php
$user_domains = regListUserDomains();
if (!in_array($_POST['domain'] ?? NULL, $user_domains, true))
echo ' <option value="" disabled="" selected="">—</option>' . LF;
foreach ($user_domains as $domain)
echo ' <option value="' . $domain . '"' . (($_POST['domain'] ?? NULL === $domain) ? ' selected=""' : '') . '>' . $domain . '</option>' . LF;
?>
</select>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= sprintf(_('To prove that you are allowed to receive the domain by its current owner, the domain must have an NS record equal to %s when the form is being processed. The NS record will be automatically deleted once validated.'), '<code>' . getAuthToken() . '._transfer-verification.' . SERVER_NAME . '.</code>') ?>
</p>

View File

@ -1,3 +1,4 @@
<?php declare(strict_types=1); ?>
<p>
<?= _('This will unregister the domain, making it registerable by anyone again (after a delay of 1 year plus half the registration period, with a maximum of 8 years).') ?>
</p>

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
require 'init.php';
$pageAddress = substr($_SERVER['REQUEST_URI'], strlen(CONF['common']['prefix']) + 1);
@ -11,7 +11,7 @@ define('PAGE_ADDRESS', $pageAddress . ((substr($pageAddress, -1) === '/' OR $pag
define('PAGE_LINEAGE', explode('/', PAGE_ADDRESS));
define('SERVICE', dirname(PAGE_ADDRESS));
function getPageInformations($pages, $pageElements) {
function getPageInformations(array $pages, array $pageElements): array {
if (!isset($pages['index']) OR $pageElements[0] === 'index')
return [
'titles_lineage' => [$pages[$pageElements[0]]['title'] ?? false],
@ -44,7 +44,7 @@ if (in_array($_SERVER['SERVER_NAME'], CONF['common']['public_domains'], true) !=
define('SERVER_NAME', $_SERVER['SERVER_NAME']);
const SESSION_COOKIE_NAME = 'servnest-session-key';
function startSession() {
function startSession(): void {
session_start([
'name' => SESSION_COOKIE_NAME,
'sid_length' => 64,
@ -94,7 +94,7 @@ if (isset($_SESSION['id'])) {
}
}
function displayFinalMessage($data) {
function displayFinalMessage(?array $data): void {
if (isset($data['final_message'])) {
echo $data['final_message'];
unset($data['final_message']);
@ -118,7 +118,7 @@ if ($_POST !== []) {
require ROOT_PATH . '/pg-act/' . PAGE_ADDRESS . '.php';
}
function displayPage($data) {
function displayPage(?array $data): never {
require ROOT_PATH . '/view.php';
exit();
}

Some files were not shown because too many files have changed in this diff Show More