diff --git a/DOCS/configuration.md b/DOCS/configuration.md index 2409c71..dfafa3b 100644 --- a/DOCS/configuration.md +++ b/DOCS/configuration.md @@ -97,6 +97,12 @@ Filesystem path to the `kzonecheck` binary. Used to check sent plaintext zonefil Administrator email address published in every SOA record. Ends with a `.`, `@` is replaced by a `.`, an hypothetical `.` in the first part of the address is escaped using a `\` before, thus `contact.admin@servnest.example` becomes `contact\.admin.servnest.example.` +### `local_only_check` + +Check for records on the local registry name server when adding a zone. + +Development feature, should not be enabled for a public server. + ## `[ht]` ### `ht_path` diff --git a/check.php b/check.php index 22749e0..4e510b4 100644 --- a/check.php +++ b/check.php @@ -1,11 +1,14 @@ true, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => 0, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_HTTPHEADER => [ - 'Sec-Fetch-Site: none', - ], -]; +define('COOKIE_FILE', sys_get_temp_dir() . '/cookie-' . bin2hex(random_bytes(16)) . '.txt'); -define('COOKIE_FILE', sys_get_temp_dir() . '/cookie-' . random_bytes(16) . '.txt'); - -function curlTest($path, $args) { +function curlTest($address, $post = [], $tor = false) { $req = curl_init(); - curl_setopt_array($req, COMMON_OPT); - curl_setopt_array($req, [ - CURLOPT_URL => URL . $path, - CURLOPT_COOKIEFILE => COOKIE_FILE, - CURLOPT_COOKIEJAR => COOKIE_FILE, - CURLOPT_POSTFIELDS => $args, - ]); - $status_code = curl_getinfo($req, CURLINFO_RESPONSE_CODE); - if ($status_code >= 400) { - var_dump(curl_exec($req)); - exit($path . ' test failed with status code ' . $status_code . LF); - } - return curl_exec($req); -} + curl_setopt($req, CURLOPT_RETURNTRANSFER, true); -// AUTH TEST + if (str_starts_with($address, '/')) + curl_setopt_array($req, [ + CURLOPT_POST => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => [ + 'Sec-Fetch-Site: none', + ], + CURLOPT_URL => CORE_URL . $address, + CURLOPT_COOKIEFILE => COOKIE_FILE, + CURLOPT_COOKIEJAR => COOKIE_FILE, + CURLOPT_POSTFIELDS => $post, + ]); + else + curl_setopt($req, CURLOPT_URL, $address); + + if ($tor) + curl_setopt($req, CURLOPT_PROXY, TOR_PROXY); + else + curl_setopt($req, CURLOPT_SSL_VERIFYPEER, false); + + $result = curl_exec($req); + $status_code = curl_getinfo($req, CURLINFO_RESPONSE_CODE); + if ($status_code >= 400 OR $result === false) { + var_dump(curl_exec($req)); + var_dump(curl_error($req)); + exit($address . ' test failed with status code ' . $status_code . LF); + } + return $result; +} $username = 'check-' . bin2hex(random_bytes(16)); $password = bin2hex(random_bytes(16)); @@ -78,7 +85,6 @@ curlTest('/auth/username', [ 'current-password' => $password, 'new-username' => $username . '2', ]); - curlTest('/auth/username', [ 'current-password' => $password, 'new-username' => $username, @@ -86,68 +92,149 @@ curlTest('/auth/username', [ echo 'Created account with username "' . $username . '" and password "' . $password . '".' . LF; -// REG TEST +function testReg() { + $subdomain = bin2hex(random_bytes(16)); -$subdomain = bin2hex(random_bytes(16)); -var_dump($subdomain); -curlTest('/reg/register', [ - 'subdomain' => $subdomain, - 'suffix' => SUFFIX, - 'action' => 'register', -]); + curlTest('/reg/register', [ + 'subdomain' => $subdomain, + 'suffix' => SUFFIX, + 'action' => 'register', + ]); -curlTest('/reg/ns', [ - 'action' => 'add', - 'domain' => $subdomain . '.' . SUFFIX, - 'ns' => 'ns1.servnest.invalid.', -]); + $domain = $subdomain . '.' . SUFFIX; -exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $subdomain . '.' . SUFFIX . ' NS', $output2, $return_code); -if (preg_match('/[ \t]+ns1.servnest.invalid.$/Dm', implode(LF, $output2)) !== 1) - exit('Error: /reg/ns: NS record not set'); + curlTest('/reg/ns', [ + 'action' => 'add', + 'domain' => $domain, + 'ns' => 'ns1.servnest.invalid.', + ]); + exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $domain . ' NS', $output); + if (preg_match('/[ \t]+ns1\.servnest\.invalid\.$/Dm', implode(LF, $output)) !== 1) + exit('Error: /reg/ns: NS record not set' . LF); -// HT TEST + curlTest('/reg/ns', [ + 'action' => 'delete', + 'domain' => $domain, + 'ns' => 'ns1.servnest.invalid.', + ]); -curlTest('/ht/', []); + return $domain; +} -define('TEST_CONTENT', 'test-' . random_bytes(4)); +function testNs($domain) { + foreach (CONF['ns']['servers'] as $ns) + curlTest('/reg/ns', [ + 'action' => 'add', + 'domain' => $domain, + 'ns' => $ns, + ]); -file_put_contents(sys_get_temp_dir() . '/index.html', TEST_CONTENT); + preg_match('#\(?[0-9a-z-]{16,128}\._domain-verification\.' . preg_quote(CORE_DOMAIN) . '\.)\#', curlTest('/ns/zone-add', []), $matches); + curlTest('/reg/ns', [ + 'action' => 'add', + 'domain' => $domain, + 'ns' => $matches['token'], + ]); -file_put_contents(sys_get_temp_dir() . '/exec.txt', 'mkdir /_site0- + curlTest('/ns/zone-add', [ + 'domain' => $domain, + ]); + + curlTest('/reg/ns', [ + 'action' => 'delete', + 'domain' => $domain, + 'ns' => $matches['token'], + ]); + + curlTest('/ns/caa', [ + 'action' => 'add', + 'subdomain' => '@', + 'zone' => $domain, + 'ttl-value' => '2', + 'ttl-multiplier' => '3600', + 'flag' => '0', + 'tag' => 'issue', + 'value' => 'letsencrypt.org', + ]); + exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' ' . $domain . ' CAA', $output); + if (preg_match('/^' . preg_quote($domain) . '[ \t]+7200[ \t]+IN[ \t]+CAA[ \t]+0[ \t]+issue[ \t]+"letsencrypt\.org"$/Dm', implode(LF, $output)) !== 1) + 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" + . '@ 86400 NS ' . CONF['ns']['servers'][0] . "\r\n", + ]); + exec(CONF['dns']['kdig_path'] . ' @' . CONF['reg']['address'] . ' aaaa.' . $domain . ' AAAA', $output); + if (preg_match('/[ \t]+' . preg_quote(CONF['ht']['ipv6_address']) . '$/Dm', implode(LF, $output)) !== 1) + exit('Error: /ns/edit: AAAA record not set' . LF); +} + +function testHt($username, $password) { + curlTest('/ht/', []); + + define('TEST_CONTENT', 'test-' . random_bytes(4)); + + file_put_contents(sys_get_temp_dir() . '/index.html', TEST_CONTENT); + + file_put_contents(sys_get_temp_dir() . '/exec.txt', 'mkdir /_site0- put ' . sys_get_temp_dir() . '/index.html /_site0-/index.html exit '); -$process = proc_open(SSHPASS . ' ' . SFTP . ' -o BatchMode=no -b ' . sys_get_temp_dir() . '/exec.txt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P ' . CONF['ht']['public_sftp_port'] . ' ' . $username . '@' . CONF['ht']['sftp_domain'], [0 => ['pipe', 'r']], $pipes); -if (is_resource($process) !== true) - exit('Can\'t spawn sftp with sshpass.'); -fwrite($pipes[0], $password); -fclose($pipes[0]); -if (proc_close($process) !== 0) - exit('File not sent successfully.'); + $process = proc_open(SSHPASS . ' ' . SFTP . ' -o BatchMode=no -b ' . sys_get_temp_dir() . '/exec.txt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P ' . CONF['ht']['public_sftp_port'] . ' ' . $username . '@' . CONF['ht']['sftp_domain'], [0 => ['pipe', 'r']], $pipes); + if (is_resource($process) !== true) + exit('Can\'t spawn sftp with sshpass.' . LF); + fwrite($pipes[0], $password); + fclose($pipes[0]); + if (proc_close($process) !== 0) + exit('File not sent successfully.' . LF); -$html = curlTest('/ht/add-onion', [ - 'dir' => '_site0-' -]); -if (preg_match('#\http\://(?[0-9a-z]{56})\.onion/\#D', $html, $matches) !== 1) - exit('Can\'t find onion address.'); + { + $ht_subpath = bin2hex(random_bytes(16)); + curlTest('/ht/add-subpath', [ + 'path' => $ht_subpath, + 'dir' => '_site0-', + ]); + if (curlTest('https://' . CONF['ht']['subpath_domain'] . ':' . HTTPS_PORT . '/' . $ht_subpath . '/') !== TEST_CONTENT) + exit('Unexpected subpath response' . LF); + curlTest('/ht/del', [ + 'site' => 'subpath:' . $ht_subpath, + ]); + } -sleep(10); + { + $ht_subdomain = 'test3'; + curlTest('/ht/add-subdomain', [ + 'subdomain' => $ht_subdomain, + 'dir' => '_site0-', + ]); + if (curlTest('https://' . $ht_subdomain . '.' . CONF['ht']['subpath_domain'] . ':' . HTTPS_PORT . '/') !== TEST_CONTENT) + exit('Unexpected subpath response' . LF); + curlTest('/ht/del', [ + 'site' => 'subdomain:' . $ht_subdomain, + ]); + } + + { + $html = curlTest('/ht/add-onion', [ + 'dir' => '_site0-', + ]); + if (preg_match('#\http\://(?[0-9a-z]{56})\.onion/\#D', $html, $matches) !== 1) + exit('Can\'t find onion address.' . LF); + sleep(5); + if (curlTest('http://' . $matches['onion'] . '.onion/', tor: true) !== TEST_CONTENT) + exit('Unexpected onion service response (' . $matches['onion'] . '.onion)' . LF); + curlTest('/ht/del', [ + 'site' => 'onion:' . $matches['onion'] . '.onion', + ]); + } -$req = curl_init(); -curl_setopt_array($req, [ - CURLOPT_PROXY => TOR_PROXY, - CURLOPT_URL => 'http://' . $matches['onion'] . '.onion/', - CURLOPT_RETURNTRANSFER => true, -]); -var_dump(curl_error($req)); -if (curl_exec($req) !== TEST_CONTENT) { - var_dump(curl_exec($req)); - exit('Unexpected onion service response (status:' . $status_code . ') (' . $matches['onion'] . '.onion)' . LF); } -// STOP +$domain = testReg(); +testNs($domain); +testHt($username, $password); curlTest('/auth/unregister', [ 'current-password' => $password, diff --git a/config.ini b/config.ini index 5c82fd3..681acfc 100644 --- a/config.ini +++ b/config.ini @@ -27,6 +27,7 @@ servers[] = "ns1.servnest.test." servers[] = "ns2.servnest.test." kzonecheck_path = "/usr/bin/kzonecheck" public_soa_email = "hostmaster.invalid." +local_only_check = false [ht] ht_path = "/srv/servnest/ht" diff --git a/pg-act/ns/zone-add.php b/pg-act/ns/zone-add.php index 961cf30..3c0f503 100644 --- a/pg-act/ns/zone-add.php +++ b/pg-act/ns/zone-add.php @@ -5,13 +5,13 @@ $_POST['domain'] = formatAbsoluteDomain($_POST['domain']); if (query('select', 'zones', ['zone' => $_POST['domain']], 'zone') !== []) output(403, _('This zone already exists on the service.')); -exec(CONF['dns']['kdig_path'] . ' ' . ltrim(strstr($_POST['domain'], '.'), '.') . ' NS +short', $parentAuthoritatives); +exec(CONF['dns']['kdig_path'] . ' ' . ltrim(strstr($_POST['domain'], '.'), '.') . ' NS +short' . (CONF['ns']['local_only_check'] ? (' @' . CONF['reg']['address']) : ''), $parentAuthoritatives); if ($parentAuthoritatives === []) output(403, _('Parent zone\'s name servers not found.')); foreach ($parentAuthoritatives as $parentAuthoritative) checkAbsoluteDomainFormat($parentAuthoritative); -exec(CONF['dns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . $parentAuthoritatives[0] . ' +noidn', $results); +exec(CONF['dns']['kdig_path'] . ' ' . $_POST['domain'] . ' NS @' . (CONF['ns']['local_only_check'] ? CONF['reg']['address'] : $parentAuthoritatives[0]) . ' +noidn', $results); if (preg_match('/^' . preg_quote($_POST['domain'], '/') . '[\t ]+[0-9]{1,8}[\t ]+IN[\t ]+NS[\t ]+(?[0-9a-f]{8})-(?[0-9a-f]{32})\._domain-verification\.' . preg_quote(SERVER_NAME, '/') . '\.$/Dm', implode(LF, $results), $matches) !== 1) output(403, _('NS authentication record not found.'));