$openness) if ($openness === 'all') define('SUFFIX', $suffix); if (!defined('SUFFIX')) exit('Unable to start tests: no suffix open to registration'); $test_start = 0; function startTest(string $test_name): void { global $test_start; $test_start = microtime(true); echo 'Testing ' . $test_name . '... '; } function stopTest(): void { global $test_start; echo 'OK (' . round(microtime(true) - $test_start, 2) . 's)' . LF; } startTest('DNSSEC resolution'); if (kdig(name: 'nlnet.nl', type: 'AAAA')['AD'] !== 1) exit('DNS queries don\'t seem to be DNSSEC-validated.' . LF); stopTest(); define('COOKIE_FILE', sys_get_temp_dir() . '/cookie-' . bin2hex(random_bytes(16)) . '.txt'); function curlTest(string $address, array $post = [], bool $tor = false): string { $req = curl_init(); curl_setopt($req, CURLOPT_RETURNTRANSFER, true); 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, CONF['check']['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($result); var_dump(curl_error($req)); throw new Exception($address . ' test failed with status code ' . $status_code); } return $result; } $username = 'check-' . bin2hex(random_bytes(16)); $password = bin2hex(random_bytes(16)); startTest('account registration'); curlTest('/auth/register', [ 'username' => $username, 'password' => $password, ]); stopTest(); startTest('account password change'); $new_password = bin2hex(random_bytes(16)); curlTest('/auth/password', [ 'current-password' => $password, 'new-password' => $new_password, ]); $password = $new_password; stopTest(); 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 . '3', ]); curlTest('/auth/username', [ 'current-password' => $password, 'new-username' => $username, ]); echo 'Created account with username "' . $username . '" and password "' . $password . '".' . LF; function testReg(): string { global $username, $password; $subdomain = bin2hex(random_bytes(16)); startTest('domain registration'); curlTest('/reg/register', [ 'subdomain' => $subdomain, 'suffix' => SUFFIX, 'action' => 'register', ]); stopTest(); $domain = $subdomain . '.' . SUFFIX; { startTest('NS record writing in registry'); curlTest('/reg/ns', [ 'action' => 'add', 'domain' => $domain, 'ns' => 'ns1.servnest.invalid.', ]); stopTest(); startTest('NS record reading in registry'); $results = kdig(name: $domain, type: 'NS', server: CONF['reg']['address']); if (($results['authorityRRs'][0]['rdataNS'] ?? NULL) !== 'ns1.servnest.invalid.') exit('Error: /reg/ns: NS record not set' . LF); stopTest(); } curlTest('/reg/ns', [ 'action' => 'delete', 'domain' => $domain, 'ns' => 'ns1.servnest.invalid.', ]); { // Domain transfer startTest('domain transfer procedure'); curlTest('/auth/logout'); curlTest('/auth/login', [ 'username' => $username . '2', 'password' => $password, ]); preg_match('#\(?[0-9a-z-]{16,128}\._transfer-verification\.' . preg_quote(CONF['common']['public_domains'][0], '#') . '\.)\#', 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'], ]); stopTest(); $username = $username . '2'; } return $domain; } function testNs(string $domain): void { foreach (CONF['ns']['servers'] as $ns) curlTest('/reg/ns', [ 'action' => 'add', 'domain' => $domain, 'ns' => $ns, ]); preg_match('#\(?[0-9a-z-]{16,128}\._domain-verification\.' . preg_quote(CONF['common']['public_domains'][0], '#') . '\.)\#', curlTest('/ns/zone-add'), $matches); curlTest('/reg/ns', [ 'action' => 'add', 'domain' => $domain, 'ns' => $matches['token'], ]); startTest('/ns/zone-add'); curlTest('/ns/zone-add', [ 'domain' => $domain, ]); stopTest(); curlTest('/reg/ns', [ 'action' => 'delete', 'domain' => $domain, 'ns' => $matches['token'], ]); { startTest('/ns/caa writing'); curlTest('/ns/caa', [ 'action' => 'add', 'subdomain' => '@', 'zone' => $domain, 'ttl-value' => '2', 'ttl-multiplier' => '3600', 'flag' => '0', 'tag' => 'issue', 'value' => 'letsencrypt.org', ]); stopTest(); startTest('/ns/caa reading'); $results = kdig(name: $domain, type: 'CAA', server: CONF['reg']['address']); if (($results['answerRRs'][0]['TTL'] ?? NULL) !== 7200) exit('Error: /ns/caa: wrong TTL' . LF); if (($results['answerRRs'][0]['rdataCAA'] ?? NULL) !== '0 issue "letsencrypt.org" ') exit('Error: /ns/caa: CAA record not set' . LF); stopTest(); } { startTest('/ns/edit writing'); curlTest('/ns/edit', [ 'domain' => $domain, 'records' => 'aaaa.' . $domain . ' 3600 AAAA ' . CONF['ht']['ipv6_address'] . "\r\n" . '@ 86400 NS ' . CONF['ns']['servers'][0] . "\r\n", ]); stopTest(); startTest('/ns/edit reading'); $results = kdig(name: 'aaaa.' . $domain, type: 'AAAA', server: CONF['reg']['address']); if (($results['answerRRs'][0]['rdataAAAA'] ?? NULL) !== CONF['ht']['ipv6_address']) exit('Error: /ns/edit: AAAA record not set' . LF); stopTest(); } } function testHt(string $username, string $password): void { startTest('SFTP file upload'); define('TEST_CONTENT', 'test-' . bin2hex(random_bytes(16))); 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(CONF['check']['sshpass_path'] . ' ' . CONF['check']['sftp_path'] . ' -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); stopTest(); { $ht_subpath = bin2hex(random_bytes(16)); startTest('subpath site creation'); curlTest('/ht/add-subpath', [ 'path' => $ht_subpath, 'dir' => '_site0-', ]); stopTest(); startTest('subpath site reachability'); if (curlTest('https://' . CONF['ht']['subpath_domain'] . ':' . CONF['check']['https_port'] . '/' . $ht_subpath . '/') !== TEST_CONTENT) exit('Unexpected subpath response' . LF); stopTest(); startTest('subpath site deletion'); curlTest('/ht/del', [ 'site' => 'subpath:' . $ht_subpath, ]); stopTest(); } { $ht_subdomain = bin2hex(random_bytes(16)); startTest('subdomain site creation'); curlTest('/ht/add-subdomain', [ 'subdomain' => $ht_subdomain, 'dir' => '_site0-', ]); stopTest(); startTest('subdomain site reachability'); if (curlTest('https://' . $ht_subdomain . '.' . CONF['ht']['subpath_domain'] . ':' . CONF['check']['https_port'] . '/') !== TEST_CONTENT) exit('Unexpected subpath response' . LF); stopTest(); startTest('subdomain site deletion'); curlTest('/ht/del', [ 'site' => 'subdomain:' . $ht_subdomain, ]); stopTest(); } { startTest('onion site creation'); $html = curlTest('/ht/add-onion', [ 'dir' => '_site0-', ]); stopTest(); if (preg_match('#\http\://(?[0-9a-z]{56})\.onion/\#D', $html, $matches) !== 1) exit('Can\'t find onion address.' . LF); sleep(5); // Onion services are not immediately reachable startTest('onion site reachability'); if (curlTest('http://' . $matches['onion'] . '.onion/', tor: true) !== TEST_CONTENT) exit('Unexpected onion service response (' . $matches['onion'] . '.onion)' . LF); stopTest(); startTest('onion site deletion'); curlTest('/ht/del', [ 'site' => 'onion:' . $matches['onion'] . '.onion', ]); stopTest(); } } $domain = testReg(); testNs($domain); testHt($username, $password); startTest('account deletion'); curlTest('/auth/unregister', [ 'current-password' => $password, 'delete' => 'on', ]); stopTest(); unlink(COOKIE_FILE); echo 'All tests succeeded! 🎉' . LF;