From 637dac1570233cb230217b515d5d45f32efcd060 Mon Sep 17 00:00:00 2001 From: flash Date: Sat, 5 Oct 2024 16:31:35 +0200 Subject: [PATCH] import --- config.php | 7 + index.php | 1674 ++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 323 ++++++++++ 3 files changed, 2004 insertions(+) create mode 100644 config.php create mode 100644 index.php create mode 100644 style.css diff --git a/config.php b/config.php new file mode 100644 index 0000000..57d26a0 --- /dev/null +++ b/config.php @@ -0,0 +1,7 @@ +' + . $title + . ''; + + foreach($headers as $header) { + $header = (object)$header; + $html .= '<' . $header->tag; + if(isset($header->props)) + $html .= rgDnsConstructProps($header->props); + $html .= empty($header->close) ? ' />' : '>tag . '>'; + } + + $html .= '

Railgun DNS

'; + if(rgDnsLoggedIn()) { + $html .= 'Main Menu | '; + $html .= 'Logged in as ' . $GLOBALS['userInfo']->user_name . ' [ '; + $html .= 'Log out ]'; + } else $html .= 'Log in'; + $html .= '
'; + + echo $html; +} +function rgDnsFoot(): void { + $html = '
'; + echo $html; +} +function rgDnsConstructProps(array $props): string { + $html = ''; + foreach($props as $name => $value) + $html .= ' ' . $name . (empty($value) ? '' : '="' . htmlspecialchars($value) . '"'); + return $html; +} +function rgDnsForm(string $action, array $fields, array $buttons = [], string $method = 'post', string $encType = 'application/x-www-form-urlencoded'): void { + $html = '
'; + + foreach($fields as $name => $info) + if(empty($info['hidden'])) + $html .= rgDnsFormField($name, $info); + + $html .= '
'; + + if(empty($buttons)) + $buttons = ['Submit']; + + foreach($buttons as $button) { + if(is_string($button)) + $button = ['text' => $button]; + $html .= ''; + } + + echo $html . '
'; +} +function rgDnsFormField(string $name, array $fi): string { + $fi = (object)$fi; // objects are less annoying to address + switch($fi->type = ($fi->type ?? 'text')) { + case 'hidden': + return ''; + case 'checkbox': + return ''; + case 'select': + $html = ''; + default: + return ''; + break; + } +} +function rgDnsHttpInfo(string $title, array $lines = [], array $headers = []): void { + rgDnsHead($title, $headers); + echo '

' . $title . '

'; + foreach($lines as $line) + echo '

' . $line . '

'; + echo '
'; + rgDnsFoot(); +} +function rgDnsHttpError(int $code): void { + http_response_code($code); + rgDnsHttpInfo('Error #' . $code, [ + 'Click here to return to where you came from.', + ]); + exit; +} + +function rgDnsGet(string $name): mixed { + return $_SESSION[RGDNS_PREFIX . $name] ?? null; +} +function rgDnsSet(string $name, $value): mixed { + return $_SESSION[RGDNS_PREFIX . $name] = $value; +} +function rgDnsIsset(string $name): bool { + return isset($_SESSION[RGDNS_PREFIX . $name]); +} + +function rgDnsLoggedIn(): bool { + return rgDnsIsset('uid'); +} + +function rgDnsConfig(string $name, $value = null) { + if($value === null) + return $GLOBALS[RGDNS_PREFIX . 'config'][$name] ?? null; + $GLOBALS[RGDNS_PREFIX . 'config'][$name] = $value; +} + +function rgDnsBase32Decode(string $str, string $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'): string { + $out = ''; + $length = strlen($str); + $char = $shift = 0; + + for($i = 0; $i < $length; $i++) { + $char <<= 5; + $char += stripos($chars, $str[$i]); + $shift = ($shift + 5) % 8; + $out .= $shift < 5 ? chr(($char & (0xFF << $shift)) >> $shift) : ''; + } + + return $out; +} +function rgDnsBase32Encode(string $data, string $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'): string { + $bin = ''; + $encoded = ''; + $length = strlen($data); + + for($i = 0; $i < $length; $i++) + $bin .= sprintf('%08b', ord($data[$i])); + + $bin = str_split($bin, 5); + $last = array_pop($bin); + $bin[] = str_pad($last, 5, '0', STR_PAD_RIGHT); + + foreach($bin as $part) + $encoded .= $chars[bindec($part)]; + + return $encoded; +} + +function rgDnsTOTPTime(?int $time = null, int $interval = 30): int { + $time ??= time(); + return ($time * 1000) / ($interval * 1000); +} +function rgDnsTOTPGenerateSecret(): string { + return rgDnsBase32Encode(random_bytes(16)); +} +function rgDnsTOTPGenerate(string $privateKey, ?int $time = null, int $digits = 6, int $interval = 30, string $algo = 'sha1'): string { + $hash = hash_hmac($algo, pack('J', rgDnsTOTPTime($time)), rgDnsBase32Decode($privateKey), true); + $offset = ord($hash[strlen($hash) - 1]) & 0x0F; + + $bin = 0; + $bin |= (ord($hash[$offset]) & 0x7F) << 24; + $bin |= (ord($hash[$offset + 1]) & 0xFF) << 16; + $bin |= (ord($hash[$offset + 2]) & 0xFF) << 8; + $bin |= (ord($hash[$offset + 3]) & 0xFF); + $otp = $bin % pow(10, $digits); + + return str_pad($otp, $digits, STR_PAD_LEFT); +} +function rgDnsTOTPValidTokens(string $privateKey): array { + return [ + rgDnsTOTPGenerate($privateKey, time()), + rgDnsTOTPGenerate($privateKey, time() - 30), + rgDnsTOTPGenerate($privateKey, time() + 30) + ]; +} + +function rgDnsCan(int $perm, ?stdClass $userInfo = null): bool { + $userInfo ??= $GLOBALS['userInfo']; + return $userInfo !== null && ($userInfo->user_perms & $perm) === $perm; +} + +function rgDnsDb(): PDO { + $name = RGDNS_PREFIX . 'pdo'; + if(empty($GLOBALS[$name])) + try { + $GLOBALS[$name] = new PDO( + rgDnsConfig('pdo_dsn'), + rgDnsConfig('pdo_user'), + rgDnsConfig('pdo_pass'), [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET SESSION time_zone = \'+00:00\'' + . ', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\'', + ]); + } catch(PDOException $ex) { + die($ex->getMessage()); + } + return $GLOBALS[$name]; +} + +function rgDnsCanLogin(?string $ipAddress = null): bool { + $ipAddress ??= $_SERVER['REMOTE_ADDR']; + $prep = rgDnsDb()->prepare('SELECT COUNT(*) < 5 FROM `' . RGDNS_PREFIX . 'attempts` WHERE `attempt_ip` = INET6_ATON(:ip) AND `attempt_success` = 0'); + $prep->bindValue('ip', $ipAddress); + return $prep->execute() && (bool)$prep->fetchColumn(); +} +function rgDnsRecordLogIn(bool $success, ?string $ipAddress = null): void { + $ipAddress ??= $_SERVER['REMOTE_ADDR']; + $prep = rgDnsDb()->prepare('INSERT INTO `' . RGDNS_PREFIX . 'attempts` (`attempt_ip`, `attempt_success`) VALUES (INET6_ATON(:ip), :success)'); + $prep->bindValue('ip', $ipAddress); + $prep->bindValue('success', $success ? 1 : 0); + $prep->execute(); +} +function rgDnsCleanLogInAttempts(): void { + rgDnsDb()->exec('DELETE FROM `' . RGDNS_PREFIX . 'attempts` WHERE `attempt_created` < NOW() - INTERVAL 1 HOUR'); +} + +function rgDnsUniqueChars(string $input, bool $multibyte = true): int { + $chars = []; + $strlen = $multibyte ? 'mb_strlen' : 'strlen'; + $substr = $multibyte ? 'mb_substr' : 'substr'; + $length = $strlen($input); + + for($i = 0; $i < $length; $i++) { + $current = $substr($input, $i, 1); + + if(!in_array($current, $chars, true)) { + $chars[] = $current; + } + } + + return count($chars); +} +function rgDnsCheckPasswordStrength(string $password): bool { + return rgDnsUniqueChars($password) >= 6; +} + +function rgDnsFetchUser(string $field, $match): stdClass|bool { + $prep = rgDnsDb()->prepare('SELECT *, UNIX_TIMESTAMP(`user_created`) AS `user_created`, UNIX_TIMESTAMP(`user_disabled`) AS `user_disabled` FROM `' . RGDNS_PREFIX . 'users` WHERE `user_' . $field . '` = :match'); + $prep->bindValue('match', $match); + $prep->execute(); + return $prep->fetchObject(); +} +function rgDnsFetchUsers(string $field, $after, int $limit = 10): array { + $prep = rgDnsDb()->prepare('SELECT *, UNIX_TIMESTAMP(`user_created`) AS `user_created`, UNIX_TIMESTAMP(`user_disabled`) AS `user_disabled` FROM `' . RGDNS_PREFIX . 'users` WHERE `user_' . $field . '` > :after LIMIT :limit'); + $prep->bindValue('after', $after); + $prep->bindValue('limit', $limit); + $prep->execute(); + $items = []; + while($item = $prep->fetchObject()) + $items[] = $item; + return $items; +} +function rgDnsSetUserPerms(int $userId, int $perms): void { + $prep = rgDnsDb()->prepare('UPDATE `' . RGDNS_PREFIX . 'users` SET `user_perms` = :perms WHERE `user_id` = :user'); + $prep->bindValue('user', $userId); + $prep->bindValue('perms', $perms); + $prep->execute(); +} +function rgDnsSetUserTOTPSecret(int $userId, ?string $privateKey): void { + $prep = rgDnsDb()->prepare('UPDATE `' . RGDNS_PREFIX . 'users` SET `user_totp_private` = :priv WHERE `user_id` = :user'); + $prep->bindValue('user', $userId); + $prep->bindValue('priv', $privateKey); + $prep->execute(); +} +function rgDnsSetUserPassword(int $userId, string $password): void { + $prep = rgDnsDb()->prepare('UPDATE `' . RGDNS_PREFIX . 'users` SET `user_password` = :pwd WHERE `user_id` = :user'); + $prep->bindValue('user', $userId); + $prep->bindValue('pwd', $password); + $prep->execute(); +} + +function rgDnsQueryLoginAttempts(int $start): array { + $prep = rgDnsDb()->prepare('SELECT *, INET6_NTOA(`attempt_ip`) AS `attempt_ip`, UNIX_TIMESTAMP(`attempt_created`) AS `attempt_created` FROM `' . RGDNS_PREFIX . 'attempts` WHERE `attempt_created` > FROM_UNIXTIME(:start) ORDER BY `attempt_created` ASC'); + $prep->bindValue('start', $start); + $prep->execute(); + $items = []; + while($item = $prep->fetchObject()) + $items[] = $item; + return $items; +} + +function rgDnsFetchNameserver(string $field, $match): stdClass|bool { + $prep = rgDnsDb()->prepare('SELECT *, UNIX_TIMESTAMP(`ns_created`) AS `user_created`, UNIX_TIMESTAMP(`ns_disabled`) AS `user_disabled`, u.`user_name` AS `ns_owner_name` FROM `' . RGDNS_PREFIX . 'nameservers` AS ns LEFT JOIN `' . RGDNS_PREFIX . 'users` AS u ON ns.`ns_owner` = u.`user_id` WHERE `ns_' . $field . '` = :match'); + $prep->bindValue('match', $match); + $prep->execute(); + return $prep->fetchObject(); +} +function rgDnsFetchNameservers(bool $withDisabled = true): array { + $prep = rgDnsDb()->prepare('SELECT ns.*, UNIX_TIMESTAMP(ns.`ns_created`) AS `ns_created`, UNIX_TIMESTAMP(ns.`ns_disabled`) AS `ns_disabled`, u.`user_name` AS `ns_owner_name` FROM `' . RGDNS_PREFIX . 'nameservers` AS ns LEFT JOIN `' . RGDNS_PREFIX . 'users` AS u ON ns.`ns_owner` = u.`user_id` ' . ($withDisabled ? '' : ' WHERE `ns_disabled` IS NULL')); + $prep->execute(); + $items = []; + while($item = $prep->fetchObject()) + $items[] = $item; + return $items; +} +function rgDnsDeleteNameserver(int $nsId): void { + $prep = rgDnsDb()->prepare('DELETE FROM `' . RGDNS_PREFIX . 'nameservers` WHERE `ns_id` = :id'); + $prep->bindValue('id', $nsId); + $prep->execute(); +} +function rgDnsEditNameserver(int $nsId, string $name, bool $master, bool $disable): bool { + $prep = rgDnsDb()->prepare('UPDATE `' . RGDNS_PREFIX . 'nameservers` SET `ns_name` = :name, `ns_type` = :type, `ns_disabled` = IF(:disable, COALESCE(`ns_disabled`, NOW()), NULL) WHERE `ns_id` = :id'); + $prep->bindValue('id', $nsId); + $prep->bindValue('name', $name); + $prep->bindValue('type', $master ? 'MASTER' : 'SLAVE'); + $prep->bindValue('disable', $disable ? 1 : 0); + try { + $prep->execute(); + } catch(PDOException $ex) { + if($ex->getCode() == '23000') + return false; + throw $ex; + } + return true; +} +function rgDnsAddNameserver(string $name, bool $master, bool $disable, int $owner): int { + $prep = rgDnsDb()->prepare('INSERT INTO `' . RGDNS_PREFIX . 'nameservers` (`ns_name`, `ns_owner`, `ns_type`, `ns_disabled`) VALUES (:name, :owner, :type, IF(:disable, NOW(), NULL))'); + $prep->bindValue('name', $name); + $prep->bindValue('owner', $owner); + $prep->bindValue('type', $master ? 'MASTER' : 'SLAVE'); + $prep->bindValue('disable', $disable ? 1 : 0); + try { + $prep->execute(); + } catch(PDOException $ex) { + if($ex->getCode() == '23000') + return -1; + throw $ex; + } + return (int)rgDnsDb()->lastInsertId(); +} +function rgDnsFetchNameserverAddresses(int $nsId): array { + $prep = rgDnsDb()->prepare('SELECT *, INET6_NTOA(`server_ip`) AS `server_ip` FROM `' . RGDNS_PREFIX . 'nameservers_addresses` WHERE `server_ns` = :ns'); + $prep->bindValue('ns', $nsId); + $prep->execute(); + $items = []; + while($item = $prep->fetchObject()) + $items[] = $item; + return $items; +} +function rgDnsAddNameserverAddress(int $nsId, string $address): bool { + $prep = rgDnsDb()->prepare('INSERT INTO `' . RGDNS_PREFIX . 'nameservers_addresses` (`server_ns`, `server_ip`) VALUES (:ns, INET6_ATON(:addr))'); + $prep->bindValue('ns', $nsId); + $prep->bindValue('addr', $address); + try { + $prep->execute(); + } catch(PDOException $ex) { + if($ex->getCode() == '23000') + return false; + throw $ex; + } + return true; +} +function rgDnsRemoveNameserverAddress(int $nsId, string $address): void { + $prep = rgDnsDb()->prepare('DELETE FROM `' . RGDNS_PREFIX . 'nameservers_addresses` WHERE `server_ns` = :ns AND `server_ip` = INET6_ATON(:addr)'); + $prep->bindValue('ns', $nsId); + $prep->bindValue('addr', $address); + $prep->execute(); +} + +function rgDnsFetchDomain(string $field, $match): stdClass|false { + $prep = rgDnsDb()->prepare('SELECT d.*, UNIX_TIMESTAMP(d.`domain_created`) AS `domain_created`, UNIX_TIMESTAMP(d.`domain_disabled`) AS `domain_disabled`, u.`user_name` AS `domain_owner_name` FROM `' . RGDNS_PREFIX . 'domains` AS d LEFT JOIN `' . RGDNS_PREFIX . 'users` AS u ON d.`domain_owner` = u.`user_id` WHERE d.`domain_' . $field . '` = :match'); + $prep->bindValue('match', $match); + $prep->execute(); + return $prep->fetchObject(); +} +function rgDnsFetchDomains(?int $owner = null, ?int $after = null, int $limit = 10): array { + $hasOwner = $owner !== null; + $hasAfter = $after !== null; + $query = 'SELECT d.*, UNIX_TIMESTAMP(d.`domain_created`) AS `domain_created`, UNIX_TIMESTAMP(d.`domain_disabled`) AS `domain_disabled`, u.`user_name` AS `domain_owner_name` FROM `' . RGDNS_PREFIX . 'domains` AS d LEFT JOIN `' . RGDNS_PREFIX . 'users` AS u ON d.`domain_owner` = u.`user_id`'; + if($hasOwner || $hasAfter) { + $query .= ' WHERE'; + if($hasOwner) { + $query .= ' d.`domain_owner` = :owner'; + if($hasAfter) + $query .= ' AND'; + } + if($hasAfter) + $query .= ' d.`domain_id` > :after LIMIT :limit'; + } + + $prep = rgDnsDb()->prepare($query); + if($hasOwner) + $prep->bindValue('owner', $owner); + if($hasAfter) { + $prep->bindValue('after', $after); + $prep->bindValue('limit', $limit); + } + $prep->execute(); + $items = []; + while($item = $prep->fetchObject()) + $items[] = $item; + return $items; +} +function rgDnsDeleteDomain(int $domainId): void { + $prep = rgDnsDb()->prepare('DELETE FROM `' . RGDNS_PREFIX . 'domains` WHERE `domain_id` = :domain'); + $prep->bindValue('domain', $domainId); + $prep->execute(); +} +function rgDnsAddDomain(int $userId, string $domainName, int $domainTTL, ?int $domainRefresh, ?int $domainRetry, ?int $domainExpire, ?int $domainCacheTTL, ?bool $domainDisable): int { + $prep = rgDnsDb()->prepare('INSERT INTO `' . RGDNS_PREFIX . 'domains` (`domain_name`, `domain_owner`, `domain_refresh`, `domain_retry`, `domain_expire`, `domain_ttl`, `domain_record_ttl`, `domain_disabled`) VALUES (:name, :owner, COALESCE(:refresh, DEFAULT(`domain_refresh`)), COALESCE(:retry, DEFAULT(`domain_retry`)), COALESCE(:expire, DEFAULT(`domain_expire`)), COALESCE(:cttl, DEFAULT(`domain_ttl`)), :rttl, IF(:disable, NOW(), NULL))'); + $prep->bindValue('name', $domainName); + $prep->bindValue('owner', $userId); + $prep->bindValue('refresh', $domainRefresh); + $prep->bindValue('retry', $domainRetry); + $prep->bindValue('expire', $domainExpire); + $prep->bindValue('cttl', $domainCacheTTL); + $prep->bindValue('rttl', $domainTTL); + $prep->bindValue('disable', $domainDisable ? 1 : 0); + try { + $prep->execute(); + } catch(PDOException $ex) { + var_dump($ex->getMessage()); + if($ex->getCode() == '23000') + return -1; + throw $ex; + } + return (int)rgDnsDb()->lastInsertId(); +} +function rgDnsEditDomain(int $domainId, int $domainTTL, ?int $domainRefresh, ?int $domainRetry, ?int $domainExpire, ?int $domainCacheTTL, ?bool $domainDisable): bool { + $prep = rgDnsDb()->prepare('UPDATE `' . RGDNS_PREFIX . 'domains` SET `domain_record_ttl` = :rttl, `domain_refresh` = COALESCE(:refresh, `domain_refresh`), `domain_retry` = COALESCE(:retry, `domain_retry`), `domain_expire` = COALESCE(:expire, `domain_expire`), `domain_ttl` = COALESCE(:cttl, `domain_ttl`), `domain_disabled` = IF(:hasDisable, IF(:disable, COALESCE(`domain_disabled`, NOW()), NULL), `domain_disabled`) WHERE `domain_id` = :domain'); + $prep->bindValue('domain', $domainId); + $prep->bindValue('refresh', $domainRefresh); + $prep->bindValue('retry', $domainRetry); + $prep->bindValue('expire', $domainExpire); + $prep->bindValue('cttl', $domainCacheTTL); + $prep->bindValue('rttl', $domainTTL); + $prep->bindValue('hasDisable', ($domainDisable === null) ? 0 : 1); + $prep->bindValue('disable', $domainDisable ? 1 : 0); + try { + $prep->execute(); + } catch(PDOException $ex) { + if($ex->getCode() == '23000') + return false; + throw $ex; + } + return true; +} + +function rgDnsFetchRecord(string $field, $match): stdClass|bool { + $prep = rgDnsDb()->prepare('SELECT *, UNIX_TIMESTAMP(`record_created`) AS `record_created`, UNIX_TIMESTAMP(`record_disabled`) AS `record_disabled` FROM `' . RGDNS_PREFIX . 'records` WHERE `record_' . $field . '` = :match'); + $prep->bindValue('match', $match); + $prep->execute(); + return $prep->fetchObject(); +} +function rgDnsFetchRecords(int $domainId): array { + $prep = rgDnsDb()->prepare('SELECT *, UNIX_TIMESTAMP(`record_created`) AS `record_created`, UNIX_TIMESTAMP(`record_disabled`) AS `record_disabled` FROM `' . RGDNS_PREFIX . 'records` WHERE `record_domain` = :domain ORDER BY `record_name`, `record_type`, `record_priority`'); + $prep->bindValue('domain', $domainId); + $prep->execute(); + $items = []; + while($item = $prep->fetchObject()) + $items[] = $item; + return $items; +} +function rgDnsDeleteRecord(int $recordId): void { + $prep = rgDnsDb()->prepare('DELETE FROM `' . RGDNS_PREFIX . 'records` WHERE `record_id` = :record'); + $prep->bindValue('record', $recordId); + $prep->execute(); +} + +function rgDnsFormatRecordValue(string $type, string $value): string { + switch($type) { + case 'TXT': + if(strlen($value) > 100) + $value = substr($value, 0, 100) . '...'; + $value = '"' . $value . '"'; + } + return htmlspecialchars($value); +} + +$GLOBALS[RGDNS_PREFIX . 'config'] = []; +require_once __DIR__ . '/config.php'; + +rgDnsCleanLogInAttempts(); + +session_name(RGDNS_PREFIX . 'session'); +ini_set('session.gc_maxlifetime', 86400); +session_set_cookie_params(86400); +session_start(); + +if(!rgDnsLoggedIn()) { + $canLogin = rgDnsCanLogin(); + if(!$canLogin) { + $error = 'You may no longer log in.'; + } elseif(!empty($_POST)) { + $sessionId = filter_input(INPUT_POST, 'sessid'); + if($sessionId !== session_id()) { + rgDnsRecordLogIn(false); + $error = 'Username, password or token was invalid.'; + } else { + $userName = filter_input(INPUT_POST, 'username'); + $password = filter_input(INPUT_POST, 'password'); + if(empty($userName) || empty($password)) { + rgDnsRecordLogIn(false); + $error = 'Username, password or token was invalid.'; + } else { + $userInfo = rgDnsFetchUser('name', $userName); + if(!$userInfo || !empty($userInfo->user_disabled) || !password_verify($password, $userInfo->user_password)) { + rgDnsRecordLogIn(false); + $error = 'Username, password or token was invalid.'; + } else { + $mfaDisarmed = empty($userInfo->user_totp_private); + + if(!$mfaDisarmed) { + $mfaToken = filter_input(INPUT_POST, 'mfa'); + $mfaValid = rgDnsTOTPValidTokens($userInfo->user_totp_private); + $mfaDisarmed = in_array($mfaToken, $mfaValid, true); + } + + if(!$mfaDisarmed) { + rgDnsRecordLogIn(false); + $error = 'Username, password or token was invalid.'; + } else { + rgDnsRecordLogIn(true); + rgDnsSet('uid', $userInfo->user_id); + rgDnsHead('Logged in!', [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=?']], + ]); + echo 'Logged in successfully! You will now be redirected to the main page.'; + rgDnsFoot(); + exit; + } + } + } + } + } + + rgDnsHead('Login'); + echo '

Log in

'; + if(isset($error)) + echo ''; + rgDnsForm('?', [ + 'sessid' => [ + 'type' => 'hidden', + 'value' => $canLogin ? session_id() : '', + ], + 'username' => [ + 'title' => 'Username', + 'disable' => !$canLogin, + ], + 'password' => [ + 'title' => 'Password', + 'type' => 'password', + 'disable' => !$canLogin, + ], + 'mfa' => [ + 'title' => 'Authenticator Token', + 'type' => 'text', + 'disable' => !$canLogin, + 'monospace' => true, + 'props' => [ + 'maxlength' => 6, + 'placeholder' => '------', + 'style' => 'text-align:center', + 'autocomplete' => 'off', + ], + ], + ], [[ + 'text' => 'Log in', + 'disable' => !$canLogin, + ]]); + echo '
'; + rgDnsFoot(); + exit; +} + +$userInfo = rgDnsFetchUser('id', rgDnsGet('uid')); +if(empty($userInfo) || !empty($userInfo->user_disabled)) { + session_destroy(); + rgDnsHttpInfo('Account Disabled', [ + 'Your account has been disabled. You will now be redirected to the login page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '5; url=?']], + ]); + exit; +} + +switch(filter_input(INPUT_GET, 'mode')) { + case '': + rgDnsHead('Main Menu'); + $showLine = false; + echo '

Main Menu

'; + rgDnsFoot(); + break; + + case 'records': + if(!rgDnsCan(RGDNSP_MANAGE_RECORDS)) + rgDnsHttpError(403); + + $returnUrl = (string)filter_input(INPUT_GET, 'ret'); + if(substr($returnUrl, 0, 6) !== '?mode=') + $returnUrl = '?mode=domains'; + $returnUrl = htmlspecialchars($returnUrl); + + $manageAllDomains = rgDnsCan(RGDNSP_MANAGE_ALL_DOMAINS); // read-only + $manageAllRecords = rgDnsCan(RGDNSP_MANAGE_ALL_RECORDS); // read-write + $manageAddRecords = rgDnsCan(RGDNSP_ADD_RECORDS); // can write at all + + $domainId = (int)filter_input(INPUT_GET, 'di'); + $domainInfo = rgDnsFetchDomain('id', $domainId); + if(empty($domainInfo)) { + rgDnsHttpError(404); + break; + } + if(!$manageAllDomains && $domainInfo->domain_owner !== $userInfo->user_id) { + rgDnsHttpError(403); + break; + } + + if(isset($_GET['smode'])) { + $recordId = (int)filter_input(INPUT_GET, 'ri', FILTER_SANITIZE_NUMBER_INT); + $hasRecord = $recordId > 0; + if($hasRecord) { + $recordInfo = rgDnsFetchRecord('id', $recordId); + if($recordInfo->record_domain !== $domainInfo->domain_id) { + rgDnsHttpError(403); + break; + } + } else $recordInfo = null; + + $returnNoAmp = '?mode=records&di=' . $domainInfo->domain_id . '&ret=' . rawurlencode($returnUrl); + $return = htmlspecialchars($returnNoAmp); + + switch(filter_input(INPUT_GET, 'smode')) { + case 'add': + case 'edit': + if(!$hasRecord && !$manageAddRecords) { + rgDnsHttpError(403); + break; + } + + if(!empty($_POST)) { + if(filter_input(INPUT_POST, 'sessid') !== session_id()) { + $error = 'Invalid request.'; + } else { + $recordTTL = ($recordTTL = (int)(filter_input(INPUT_POST, 'rttl'))) < 1 ? null : $recordTTL; + $recordPrio = ($recordPrio = (int)(filter_input(INPUT_POST, 'rprio'))) < 1 ? 10 : $recordPrio; + $recordValue = (string)filter_input(INPUT_POST, 'rvalue'); + $recordDisable = !empty($_POST['rdisable']); + if($domainIsset) { + if($recordInfo->record_type !== 'MX') + $recordPrio = null; + if(rgDnsEditDomain($recordInfo->record_id, $recordTTL, $recordPrio, $recordValue, $recordDisable)) { + $recordInfo->record_ttl = $recordTTL; + $recordInfo->record_priority = $recordPrio; + $recordInfo->record_value = $recordValue; + $recordInfo->record_disabled = $recordDisable ? ($recordInfo->record_disabled ?? time()) : null; + } else + $error = 'This domain has already been added to the system.'; + } else { + $recordName = trim((string)filter_input(INPUT_POST, 'rname'), " \n\r\t\v\0."); + $recordType = (string)filter_input(INPUT_POST, 'rtype'); + $recordId = rgDnsAddRecord($domainInfo->domain_id, $recordName, $recordType, $recordTTL, $recordPrio, $recordValue, $recordDisable); + if($recordId < 1) + $error = 'Something went wrong while adding that record.'; + else { + $recordInfo = rgDnsFetchRecord('id', $recordId); + $hasRecord = true; + } + } + } + } + + $title = $hasRecord ? ('Editing ' . $recordInfo->record_type . ' record ' . $recordInfo->record_name . ' for ' . $domainInfo->domain_name) : ('Adding a new DNS record for ' . $domainInfo->domain_name); + $formTarget = $return . '&smode=' . ($hasRecord ? ('edit&ri=' . $recordInfo->record_id) : 'add'); + + rgDnsHead($title); + echo '

' . $title . '

'; + if(isset($error)) + echo '

Error: ' . $error . '

'; + echo ''; + + echo '
'; + $recordFields = [ + 'sessid' => ['type' => 'hidden', 'value' => session_id()], + 'rname' => [ + 'type' => 'text', + 'title' => 'Name', + 'value' => ($recordInfo?->record_name ?? ''), + 'disable' => $hasRecord, + ], + 'rtype' => [ + 'type' => 'select', + 'title' => 'Type', + 'value' => ($recordInfo?->record_type ?? ''), + 'options' => RGDNS_RECORD_TYPES, + 'disable' => $hasRecord, + ], + 'rttl' => [ + 'type' => 'number', + 'title' => 'TTL (in Seconds)', + 'value' => ($recordInfo?->record_ttl ?? ''), + 'props' => ['placeholder' => $domainInfo->domain_record_ttl], + ], + 'rprio' => [ + 'type' => 'number', + 'title' => 'Priority (for MX)', + 'value' => ($recordInfo?->record_priority ?? ''), + 'hidden' => $hasRecord && $recordInfo->record_type !== 'MX', + 'props' => ['placeholder' => '10'], + ], + 'rvalue' => [ + 'type' => 'text', + 'title' => 'Value', + 'value' => ($recordInfo?->record_value ?? ''), + ], + 'rdisable' => [ + 'type' => 'checkbox', + 'title' => 'Disabled', + 'value' => !empty($recordInfo->record_disabled), + ], + ]; + + rgDnsForm($formTarget, $recordFields); + echo '
'; + + echo '
'; + rgDnsFoot(); + break; + + case 'delete': + if(filter_input(INPUT_GET, 'confirm') === session_id()) { + rgDnsDeleteRecord($recordInfo->record_id); + rgDnsHttpInfo('Successfully deleted ' . $recordInfo->record_type . ' record ' . $recordInfo->record_name . ' from ' . $domainInfo->domain_name . '!', [ + 'The record has been deleted successfully. You will now be redirected to the records page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=' . $returnNoAmp]], + ]); + break; + } + + $title = 'Deleting ' . $recordInfo->record_type . ' record ' . $recordInfo->record_name . ' from ' . $domainInfo->domain_name; + rgDnsHead($title); + echo '

' . $title . '

' + . '

Are you sure you want to irreversably delete this record?

' + . '
'; + rgDnsFoot(); + break; + + default: + rgDnsHttpError(404); + break; + } + } else { + $records = rgDnsFetchRecords($domainInfo->domain_id); + $title = 'DNS Records for ' . $domainInfo->domain_name; + + rgDnsHead($title); + echo '

' . $title . '

'; + if(!empty($returnUrl)) + echo ''; + + if($manageAddRecords) + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach($records as $record) { + echo ''; + printf( + '', + strtolower($record->record_type), + $record->record_type, + $record->record_name, + empty($record->record_ttl) ? 'rgdns-records-default' : '', + $record->record_ttl ?? $domainInfo->domain_record_ttl, + $record->record_priority ?? '', + rgDnsFormatRecordValue($record->record_type, $record->record_value), + ); + echo ''; + } + + echo ''; + echo '
TypeNameTTL (Seconds)PriorityValue
%s%s%d%s%s'; + echo 'Edit'; + echo ' Delete'; + echo '
'; + + echo '
'; + rgDnsFoot(); + } + break; + + case 'domains': + if(!rgDnsCan(RGDNSP_MANAGE_DOMAINS)) + rgDnsHttpError(403); + + $manageAll = rgDnsCan(RGDNSP_MANAGE_ALL_DOMAINS); + $domainsUserId = $manageAll + ? filter_input(INPUT_GET, 'uid', FILTER_SANITIZE_NUMBER_INT) + : $userInfo->user_id; + + if(isset($_GET['smode'])) { + $domainsUserId ??= $userInfo->user_id; + $domainIsset = isset($_GET['di']); + $domain = $domainIsset + ? rgDnsFetchDomain('id', (int)filter_input(INPUT_GET, 'di', FILTER_SANITIZE_NUMBER_INT)) + : null; + $returnPrefix = '?mode=domains'; + $return = (string)filter_input(INPUT_GET, 'ret'); + if(substr($return, 0, strlen($returnPrefix)) !== $returnPrefix) + $return = $returnPrefix; + $returnNoAmp = rawurlencode($return); + $return = htmlspecialchars($return); + + if($domainIsset) { + if(empty($domain)) { + rgDnsHttpError(404); + break; + } + if(!$manageAll && ($domain->domain_owner !== $userInfo->user_id || !empty($domain->domain_disabled))) { + rgDnsHttpError(403); + break; + } + } + + $domainsUserInfo = $domainsUserId === $userInfo->user_id ? $userInfo : rgDnsFetchUser('id', $domainsUserId); + if(empty($domainsUserInfo)) { + rgDnsHttpError(400); + break; + } + + switch(filter_input(INPUT_GET, 'smode')) { + case 'add': + case 'edit': + if(!rgDnsCan(RGDNSP_ADD_DOMAINS)) { + rgDnsHttpError(403); + break; + } + + if(!empty($_POST)) { + if(filter_input(INPUT_POST, 'sessid') !== session_id()) { + $error = 'Invalid request.'; + } else { + $domainTTL = ($domainTTL = (int)(filter_input(INPUT_POST, 'drttl'))) < 1 ? 3600 : $domainTTL; + $domainRefresh = $manageAll ? (int)(filter_input(INPUT_POST, 'drefresh') ?? 3600) : null; + $domainRetry = $manageAll ? (int)(filter_input(INPUT_POST, 'dretry') ?? 1800) : null; + $domainExpire = $manageAll ? (int)(filter_input(INPUT_POST, 'dexpire') ?? 1209600) : null; + $domainCacheTTL = $manageAll ? (int)(filter_input(INPUT_POST, 'dcttl')) : null; + $domainDisable = $manageAll ? !empty($_POST['ddisable']) : null; + if($domainIsset) { + if(rgDnsEditDomain($domain->domain_id, $domainTTL, $domainRefresh, $domainRetry, $domainExpire, $domainCacheTTL, $domainDisable)) { + $domain->domain_record_ttl = $domainTTL; + $domain->domain_refresh = $domainRefresh ?? $domain->domain_refresh; + $domain->domain_retry = $domainRetry ?? $domain->domain_retry; + $domain->domain_expire = $domainExpire ?? $domain->domain_expire; + $domain->domain_ttl = $domainCacheTTL ?? $domain->domain_ttl; + $domain->domain_disabled = $domainDisable ? ($domain->domain_disabled ?? time()) : null; + } else + $error = 'This domain has already been added to the system.'; + } else { + $domainName = trim((string)filter_input(INPUT_POST, 'dname'), " \n\r\t\v\0."); + $domainId = rgDnsAddDomain($domainsUserInfo->user_id, $domainName, $domainTTL, $domainRefresh, $domainRetry, $domainExpire, $domainCacheTTL, $domainDisable); + if($domainId < 1) + $error = 'This domain has already been added to the system.'; + else { + $domain = rgDnsFetchDomain('id', $domainId); + $domainIsset = true; + } + } + } + } + + $title = $domainIsset ? ('Editing domain #' . $domain->domain_id . ' ' . $domain->domain_name) : 'Adding a new Domain'; + $formTarget = $return . '&smode=' . ($domainIsset ? ('edit&di=' . $domain->domain_id) : 'add') . '&ret=' . rawurlencode($return); + + rgDnsHead($title); + echo '

' . $title . '

'; + if(isset($error)) + echo '

Error: ' . $error . '

'; + echo ''; + + echo '
'; + $domainFields = [ + 'sessid' => ['type' => 'hidden', 'value' => session_id()], + 'dname' => [ + 'type' => 'text', + 'title' => 'Name', + 'value' => ($domain?->domain_name ?? ''), + 'disable' => $domainIsset, + ], + 'drttl' => [ + 'type' => 'number', + 'title' => 'Default TTL (Seconds)', + 'value' => ($domain?->domain_record_ttl ?? ''), + 'props' => ['placeholder' => '3600'], + ], + ]; + + if($manageAll) { + $domainFields['drefresh'] = [ + 'type' => 'number', + 'title' => 'Refresh (Seconds)', + 'value' => ($domain?->domain_refresh ?? ''), + 'props' => ['placeholder' => '3600'], + ]; + $domainFields['dretry'] = [ + 'type' => 'number', + 'title' => 'Retry (Seconds)', + 'value' => ($domain?->domain_retry ?? ''), + 'props' => ['placeholder' => '1800'], + ]; + $domainFields['dexpire'] = [ + 'type' => 'number', + 'title' => 'Expires (Seconds)', + 'value' => ($domain?->domain_expire ?? ''), + 'props' => ['placeholder' => '1209600'], + ]; + $domainFields['dcttl'] = [ + 'type' => 'number', + 'title' => 'Cache TTL (Seconds)', + 'value' => ($domain?->domain_ttl ?? ''), + 'props' => ['placeholder' => '86400'], + ]; + $domainFields['ddisable'] = [ + 'type' => 'checkbox', + 'title' => 'Disabled', + 'value' => !empty($domain->domain_disabled), + ]; + } + + rgDnsForm($formTarget, $domainFields); + echo '
'; + + echo '
'; + rgDnsFoot(); + break; + + case 'delete': + if(empty($domain)) { + rgDnsHttpError(404); + break; + } + + if(isset($_GET['confirm']) && $_GET['confirm'] === session_id()) { + rgDnsDeleteDomain($domain->domain_id); + rgDnsHttpInfo('Successfully deleted ' . $domain->domain_name . '!', [ + 'Domain ' . $domain->domain_name . ' with ID ' . $domain->domain_id . ' has been deleted successfully. You will now be redirected to the domains page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=' . $returnNoAmp]], + ]); + break; + } + + $title = 'Deleting domain #' . $domain->domain_id . ' ' . $domain->domain_name; + + rgDnsHead($title); + echo '

' . $title . '

' + . '

Are you sure you want to irreversably delete ' . $domain->domain_name . '?

' + . '
'; + rgDnsFoot(); + break; + + default: + rgDnsHttpError(404); + break; + } + } else { + $domainsShowAll = $domainsUserId === null; + $domainsAfter = $domainsShowAll ? (int)filter_input(INPUT_GET, 'after', FILTER_SANITIZE_NUMBER_INT) : null; + + $domains = rgDnsFetchDomains($domainsUserId, $domainsAfter); + $lastId = 0; + + $baseUrl = '?mode=domains'; + if(!$domainsShowAll && $manageAll) + $baseUrl .= '&uid=' . $domainsUserId; + $returnUrl = rawurlencode($baseUrl . ($domainsAfter > 0 ? ('&after=' . $domainsAfter) : '')); + $baseUrl = htmlspecialchars($baseUrl); + + rgDnsHead('Domains'); + echo '

Domains

'; + + if(rgDnsCan(RGDNSP_ADD_DOMAINS)) + echo '
'; + + if(empty($domains)) { + if($domainsAfter > 0) { + echo '

You\'ve reached the end of the list!

' + . ''; + } else { + echo '

There are no domains.

'; + } + } else { + echo ''; + if($domainsShowAll) echo ''; + echo ''; + echo ''; + if($domainsShowAll) echo ''; + if($manageAll) { + echo '' + . '' + . '' + . ''; + } + echo ''; + echo ''; + if($manageAll) echo ''; + echo ''; + foreach($domains as $domain) { + $lastId = $domain->domain_id; + echo ''; + if($domainsShowAll) printf('', $domain->domain_id); + printf( + '', + $domain->domain_name, + empty($domain->domain_disabled) ? 'active' : 'inactive', + empty($domain->domain_disabled) ? 'ACTIVE' : 'INACTIVE', + ); + if($domainsShowAll) printf('', $domain->domain_owner, $domain->domain_owner_name); + if($manageAll) + printf( + '', + $domain->domain_refresh, $domain->domain_retry, + $domain->domain_expire, $domain->domain_ttl, + ); + printf( + '', + $domain->domain_record_ttl, + date('c', $domain->domain_created) + ); + if($manageAll) { + echo ''; + } + echo ''; + } + echo '
NameStatusOwnerRefreshRetryExpireCache TTLDefault TTLCreatedDisabled
%d%s%s№%d %s%d%d%d%d%d Seconds%s'; + if(empty($domain->domain_disabled)) + echo 'No'; + else { + printf('Yes (on %s)', date('c', $domain->domain_disabled)); + } + echo ''; + echo 'Records'; + if(empty($domain->domain_disabled) || $manageAll) { + echo ' Edit'; + echo ' Delete'; + } + echo '
'; + + if($domainsAfter !== null && count($domains) >= 10) + echo ''; + + echo '
'; + rgDnsForm($formTarget, [ + 'sessid' => ['type' => 'hidden', 'value' => session_id()], + 'nsname' => [ + 'type' => 'text', + 'title' => 'Name', + 'value' => ($ns?->ns_name ?? ''), + ], + 'nsmaster' => [ + 'type' => 'checkbox', + 'title' => 'Master', + 'value' => ($ns?->ns_type ?? '') === 'MASTER', + ], + 'nsdisable' => [ + 'type' => 'checkbox', + 'title' => 'Disabled', + 'value' => !empty($ns->ns_disabled), + ], + ]); + echo '
'; + + echo ''; + rgDnsFoot(); + break; + + case 'delete': + if(!$nsIsset || empty($ns)) + rgDnsHttpError(404); + + if(isset($_GET['confirm']) && $_GET['confirm'] === session_id()) { + rgDnsDeleteNameserver($ns->ns_id); + rgDnsHttpInfo('Successfully deleted ' . $ns->ns_name . '!', [ + 'Nameserver ' . $ns->ns_name . ' with ID ' . $ns->ns_id . ' has been deleted successfully. You will now be redirected to the nameservers page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=?mode=nameservers']], + ]); + break; + } + + $title = 'Deleting nameserver #' . $ns->ns_id . ' ' . $ns->ns_name; + + rgDnsHead($title); + echo '

' . $title . '

' + . '

Are you sure you want to irreversably delete ' . $ns->ns_name . '?

' + . '
'; + rgDnsFoot(); + break; + + case 'addresses': + if(!$nsIsset || empty($ns)) + rgDnsHttpError(404); + + $addrs = array_column(rgDnsFetchNameserverAddresses($ns->ns_id), 'server_ip'); + + if(isset($_GET['amode'])) { + switch(filter_input(INPUT_GET, 'amode')) { + case 'add': + if(empty($_POST['sessid']) || $_POST['sessid'] !== session_id()) { + rgDnsHttpError(403); + break; + } + + $addr = (string)filter_input(INPUT_POST, 'addr'); + if(filter_var($addr, FILTER_VALIDATE_IP) !== false && rgDnsAddNameserverAddress($ns->ns_id, $addr)) { + $title = 'Successfully added ' . $addr . '!'; + $message = 'Address ' . htmlspecialchars($addr) . ' has been successfully added to nameserver ' . $ns->ns_name . ' with ID ' . $ns->ns_id . '.'; + } else { + $title = 'Address could not be added.'; + $message = 'Address ' . htmlspecialchars($addr) . ' could not be added to nameserver ' . $ns->ns_name . ' with ID ' . $ns->ns_id . '.'; + } + + rgDnsHttpInfo($title, [ + $message . ' You will now be redirected to the nameserver\'s addresses page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=?mode=nameservers&smode=addresses&nsi=' . $ns->ns_id]], + ]); + break; + + case 'remove': + if(empty($_GET['sessid']) || $_GET['sessid'] !== session_id()) { + rgDnsHttpError(403); + break; + } + + $addr = (string)filter_input(INPUT_GET, 'addr'); + if(!in_array($addr, $addrs)) { + rgDnsHttpError(404); + break; + } + + rgDnsRemoveNameserverAddress($ns->ns_id, $addr); + rgDnsHttpInfo('Successfully removed ' . htmlspecialchars($addr) . '!', [ + 'Address ' . htmlspecialchars($addr) . ' has been successfully removed from nameserver ' . $ns->ns_name . ' with ID ' . $ns->ns_id . '. You will now be redirected to the nameserver\'s addresses page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=?mode=nameservers&smode=addresses&nsi=' . $ns->ns_id]], + ]); + break; + + default: + rgDnsHttpError(404); + break; + } + } else { + $title = 'Addresses for ' . $ns->ns_name; + $addrs = array_column(rgDnsFetchNameserverAddresses($ns->ns_id), 'server_ip'); + + rgDnsHead($title); + echo '

' . $title . '

' + . ''; + + echo ''; + if(empty($addrs)) { + echo ''; + } else { + foreach($addrs as $addr) { + printf( + '', + $addr, $ns->ns_id, rawurlencode($addr), session_id() + ); + } + } + echo '
Address
No addresses.
%sRemove
'; + rgDnsFoot(); + } + break; + + default: + rgDnsHttpError(404); + break; + } + } else { + $nameservers = rgDnsFetchNameservers(); + + rgDnsHead('Nameservers'); + echo '

Nameservers

' + . ''; + + if(empty($nameservers)) + echo '

No nameservers have been registered yet.

'; + else { + echo ''; + foreach($nameservers as $ns) { + printf( + '', + $ns->ns_id, + $ns->ns_name, + $ns->ns_owner ?? 0, + $ns->ns_owner_name ?? 'None', + $ns->ns_type, + date('c', $ns->ns_created), + empty($ns->ns_disabled) ? 'No' : 'Yes (on ' . date('c', $ns->ns_created) . ')', + ); + } + echo '
NameOwnerTypeCreatedDisabled
%d%s№%d %s%s%s%sEdit Delete Addresses
'; + } + + echo '
'; + rgDnsFoot(); + } + break; + + case 'users': + if(!rgDnsCan(RGDNSP_MANAGE_USERS)) + rgDnsHttpError(403); + + $after = (int)filter_input(INPUT_GET, 'after', FILTER_SANITIZE_NUMBER_INT); + $users = rgDnsFetchUsers('id', $after); + $lastId = 0; + + rgDnsHead('User Listing'); + echo '

User Listing

'; + + if(empty($users)) { + echo '

You have reached the end of the list!

' + . ''; + } else { + echo '' + . ''; + + foreach($users as $user) { + $lastId = $user->user_id; + printf( + '', + $user->user_id, + $user->user_name, + empty($user->user_totp_private) ? 'no' : 'yes', + empty($user->user_totp_private) ? 'No' : 'Yes', + date('c', $user->user_created), + empty($user->user_disabled) ? 'No' : 'Yes (on ' . date('c', $user->user_created) . ')', + $user->user_id, + ); + } + + echo '
Name2FACreatedDisabled
%d%s%s%s%sEdit Settings
'; + + if(count($users) >= 10) + echo ''; + } + + echo '
'; + rgDnsFoot(); + break; + + case 'attempts': + if(!rgDnsCan(RGDNSP_VIEW_LOGIN_ATTEMPTS)) + rgDnsHttpError(403); + + if(isset($_GET['query'])) { + header('Content-Type: application/json; charset=utf-8'); + $after = (int)filter_input(INPUT_GET, 'query', FILTER_SANITIZE_NUMBER_INT); + echo json_encode(rgDnsQueryLoginAttempts($after)); + break; + } + + rgDnsHead('Login Attempts'); + echo '

Log in Attempts

Log in attempt history, 5 failed attempts without an hour will prevent further log in attempts. This table automatically updates every 5 seconds.

Date/TimeSuccessIP Address
'; + echo << +SCRIPT; + rgDnsFoot(); + break; + + case 'settings': + if(!rgDnsCan(RGDNSP_EDIT_SETTINGS)) + rgDnsHttpError(403); + + $permsList = !rgDnsCan(RGDNSP_EDIT_PERMISSIONS) ? [] : [ + 'dns-records' => [ + 'name' => 'Manage Own DNS Records', + 'perm' => RGDNSP_MANAGE_RECORDS, + ], + 'dns-records-add' => [ + 'name' => 'Create DNS Records', + 'perm' => RGDNSP_ADD_RECORDS, + ], + 'dns-records-all' => [ + 'name' => 'Manage All DNS Records', + 'perm' => RGDNSP_MANAGE_ALL_RECORDS, + ], + 'dns-domains' => [ + 'name' => 'Manage Own Domains', + 'perm' => RGDNSP_MANAGE_DOMAINS, + ], + 'dns-domains-add' => [ + 'name' => 'Add New Domains', + 'perm' => RGDNSP_ADD_DOMAINS, + ], + 'dns-domains-all' => [ + 'name' => 'Manage All Domains', + 'perm' => RGDNSP_MANAGE_ALL_DOMAINS, + ], + 'dns-namservers' => [ + 'name' => 'Manage Nameservers', + 'perm' => RGDNSP_MANAGE_NAMESERVERS, + ], + 'user-settings' => [ + 'name' => 'Edit Own Settings', + 'perm' => RGDNSP_EDIT_SETTINGS, + ], + 'user-settings-all' => [ + 'name' => 'Manage Other Users', + 'perm' => RGDNSP_MANAGE_USERS, + ], + 'user-permissions' => [ + 'name' => 'Manage Permissions', + 'perm' => RGDNSP_EDIT_PERMISSIONS, + ], + 'user-strip-mfa' => [ + 'name' => 'Allow Authenticator Removal', + 'perm' => RGDNSP_REMOVE_AUTHENTICATOR, + ], + 'login-attempts-view' => [ + 'name' => 'View Log In Attempts', + 'perm' => RGDNSP_VIEW_LOGIN_ATTEMPTS, + ], + ]; + + $settingsUserId = $userInfo->user_id; + $settingsUserInfo = $userInfo; + if(isset($_GET['uid']) && rgDnsCan(RGDNSP_MANAGE_USERS)) { + $settingsUserId = (int)filter_input(INPUT_GET, 'uid', FILTER_SANITIZE_NUMBER_INT); + if($settingsUserId !== $userInfo->user_id) { + $settingsUserInfo = rgDnsFetchUser('id', $settingsUserId); + if(empty($settingsUserInfo)) + rgDnsHttpError(404); + } + } + $settingsIsSelf = $userInfo->user_id === $settingsUserId; + $settingsManage = !empty($_GET['manage']) && rgDnsCan(RGDNSP_MANAGE_USERS); + $formTarget = '?mode=settings'; + if(!$settingsIsSelf) + $formTarget .= '&uid=' . $settingsUserId; + if($settingsManage) + $formTarget .= '&manage=1'; + + if(!empty($_POST)) { + if(filter_input(INPUT_POST, 'sessid') !== session_id()) { + $error = 'Invalid request.'; + } else { + switch(filter_input(INPUT_POST, 'smode')) { + case 'perms': + if(!rgDnsCan(RGDNSP_EDIT_PERMISSIONS)) + rgDnsHttpError(403); + $newPerms = 0; + foreach($permsList as $permName => $permInfo) + if(!empty($_POST['perm_' . $permName])) + $newPerms |= $permInfo['perm']; + rgDnsSetUserPerms($settingsUserId, $settingsUserInfo->user_perms = $newPerms); + break; + + case 'pwd': + $oldPassword = filter_input(INPUT_POST, 'oldpwd'); + if(!password_verify($oldPassword, $userInfo->user_password)) { // intentionally refers to userInfo rather than settingsUserInfo, + $error = 'Your password was invalid.'; // an admin in management mode isn't gonna know the user's password. + } else { + $newPassword = filter_input(INPUT_POST, 'newpwd'); + if(!rgDnsCheckPasswordStrength($newPassword)) { + $error = 'Your password is too weak, it must contain at least 6 unique characters.'; + } else { + $agnPassword = filter_input(INPUT_POST, 'agnpwd'); + if($newPassword !== $agnPassword) { + $error = 'Your new passwords did not match.'; + } else { + $passwordWasUpdated = true; + rgDnsSetUserPassword($settingsUserId, $settingsUserInfo->user_password = password_hash($newPassword, PASSWORD_ARGON2ID)); + } + } + } + $oldPassword = $newPassword = $agnPassword = null; + break; + + case 'mfa': + switch(filter_input(INPUT_POST, 'mfa_mode')) { + case 'enable': + if(!empty($settingsUserInfo->user_totp_private)) { + $error = 'Authenticator is already enabled for your account.'; + } else { + rgDnsSetUserTOTPSecret($settingsUserId, $settingsUserInfo->user_totp_private = rgDnsTOTPGenerateSecret()); + $mfaWasEnabled = true; + } + break; + case 'disable': + if(empty($settingsUserInfo->user_totp_private)) { + $error = 'Authenticator is already disabled for your account.'; + } else { + $mfaValid = rgDnsTOTPValidTokens($settingsUserInfo->user_totp_private); + $mfaValid[] = $settingsUserInfo->user_totp_private; + if(rgDnsCan(RGDNSP_REMOVE_AUTHENTICATOR)) + $mfaValid[] = 'MM3510'; + if(!in_array(filter_input(INPUT_POST, 'mfa_token'), $mfaValid, true)) { + $error = 'Invalid Authenticator token.'; + } else { + rgDnsSetUserTOTPSecret($settingsUserId, $settingsUserInfo->user_totp_private = null); + } + } + break; + default: + rgDnsHttpError(400); + break; + } + break; + } + } + } + + $title = $settingsIsSelf ? 'Settings' : 'Settings for ' . $settingsUserInfo->user_name; + + rgDnsHead($title); + echo '

' . htmlspecialchars($title) . '

'; + if($settingsManage) + echo ''; + + if(isset($error)) + echo '

Error: ' . $error . '

'; + + echo '

Password

'; + if(!empty($passwordWasUpdated)) + echo '

Your password has been changed!

'; + rgDnsForm($formTarget, [ + 'sessid' => ['type' => 'hidden', 'value' => session_id()], + 'smode' => ['type' => 'hidden', 'value' => 'pwd'], + 'oldpwd' => [ + 'title' => 'Current Password', + 'type' => 'password', + ], + 'newpwd' => [ + 'title' => 'New Password', + 'type' => 'password', + ], + 'agnpwd' => [ + 'title' => 'New Password Again', + 'type' => 'password', + ], + ], ['Save Password']); + echo '
'; + + echo '

Authenticator

'; + $mfaForm = [ + 'sessid' => ['type' => 'hidden', 'value' => session_id()], + 'smode' => ['type' => 'hidden', 'value' => 'mfa'], + ]; + $mfaButtons = []; + + if(empty($settingsUserInfo->user_totp_private)) { + $mfaForm['mfa_mode'] = ['type' => 'hidden', 'value' => 'enable']; + $mfaButtons[] = 'Enable Authenticator'; + echo '

You do not have an Authenticator app enabled.

'; + } else { + $mfaForm['mfa_mode'] = ['type' => 'hidden', 'value' => 'disable']; + $mfaButtons[] = 'Disable Authenticator'; + if(empty($mfaWasEnabled)) { + echo '

Your account is secured with an Authenticator app!

'; + $mfaForm['mfa_token'] = [ + 'title' => 'Confirm Authenticator Token', + 'type' => 'text', + 'monospace' => true, + 'props' => [ + 'maxlength' => 6, + 'placeholder' => '------', + 'style' => 'text-align:center', + 'autocomplete' => 'off', + ], + ]; + } else { + $mfaForm['mfa_token'] = ['type' => 'hidden', 'value' => $settingsUserInfo->user_totp_private]; + echo '

You\'ve just enabled an Authenticator app.

' + . '

You secret key is ' . $settingsUserInfo->user_totp_private . '

' + . '

DO NOT REFRESH THE PAGE UNTIL YOU\'VE FINISHED ENTERING IT.

' + . '

Alternatively you can click on Disable Authenticator to disable it without needing a code for now. This option will disappear on refresh.

'; + } + } + + rgDnsForm($formTarget, $mfaForm, $mfaButtons); + echo '
'; + + if(rgDnsCan(RGDNSP_EDIT_PERMISSIONS)) { + $permsForm = [ + 'sessid' => ['type' => 'hidden', 'value' => session_id()], + 'smode' => ['type' => 'hidden', 'value' => 'perms'], + ]; + foreach($permsList as $permName => $permInfo) { + $permsForm['perm_' . $permName] = [ + 'title' => $permInfo['name'], + 'type' => 'checkbox', + 'value' => rgDnsCan($permInfo['perm'], $settingsUserInfo), + ]; + } + + echo '

Permissions

Some permissions will trigger others, you\'ll find out when you submit.

'; + rgDnsForm($formTarget, $permsForm, ['Save Permissions']); + echo '
'; + } + + echo '
'; + rgDnsFoot(); + break; + + case 'logout': + $sessionId = (string)filter_input(INPUT_GET, 'sess'); + $time = (int)filter_input(INPUT_GET, 'time', FILTER_SANITIZE_NUMBER_INT); + if($time < strtotime('-5 minutes') || $sessionId !== session_id()) { + rgDnsHttpInfo('Are you sure?', [ + 'Are you sure you want to log out?', + ' ', + ]); + break; + } + + session_destroy(); + rgDnsHttpInfo('Good bye!', [ + 'You have been logged out. You will now be redirected to the login page.', + ], [ + ['tag' => 'meta', 'props' => ['http-equiv' => 'refresh', 'content' => '2; url=?']], + ]); + break; + + default: + rgDnsHttpError(404); + break; +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..69f9354 --- /dev/null +++ b/style.css @@ -0,0 +1,323 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + position: relative; + outline-style: none; +} + +html, +body { + width: 100%; + height: 100%; +} + +.monospace { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; +} + +.rgdns { + font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif; + padding-top: 1px; + background: #fff; + color: #000; +} + +.rgdns a { + color: #000; + text-decoration: none; +} +.rgdns a:hover { + text-decoration: underline; +} +.rgdns a:focus { + margin: -1px; + border: 1px dotted #000; +} + +.rgdns-header { + background-color: #ccf; + margin: 4px 5px 5px 5px; +} +.rgdns-logo { + padding: 10px; + padding-bottom: 5px; +} +.rgdns-navigation { + padding: 3px 5px 5px 10px; +} + +.rgdns-footer { + padding: 5px; + text-align: center; + font-size: .9em; +} + +.rgdns-input { + background-color: #fff; + color: #000; + border: 1px solid #7f9db9; + padding: 0 2px; + min-height: 22px; + font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif; +} +.rgdns-input::selection { + background-color: #b2b4bf; +} +.rgdns-input[disabled] { + border-color: #bbb; + background-color: #eee; +} +select.rgdns-input { + padding: 2px 0; +} + +.rgdns-button { + border: 1px solid #003c74; + border-radius: 3px; + background: linear-gradient(0deg, #c6c5d7 0%, #dedfec 45%, #f4f5fd 55%, #ffffff 100%); + min-width: 75px; + min-height: 23px; + color: #000; + box-shadow: 0 1px 0 1px #ffffff inset, 1px 1px 0 0 #f1f1ed, -1px -1px 0 0 #dfdbd7; + margin: 3px; + padding: 0 6px; +} +.rgdns-button:focus { + box-shadow: 0 0 2px 1px #6982ee inset, 0 1px 0 2px #ffffff inset, 1px 1px 0 0 #f1f1ed, -1px -1px 0 0 #dfdbd7; +} +.rgdns-button:hover { + box-shadow: 0 0 2px 1px #e59700 inset, 0 1px 0 2px #ffffff inset, 1px 1px 0 0 #f1f1ed, -1px -1px 0 0 #dfdbd7; +} +.rgdns-button:focus:before { + content: '-'; + font-size: 0; + border: 1px dotted #000; + border-radius: 2px; + position: absolute; + top: 1px; + left: 1px; + bottom: 1px; + right: 1px; +} +.rgdns-button:active { + background: linear-gradient(180deg, #c6c5d7 0%, #dedfec 45%, #f4f5fd 55%, #ffffff 100%); + box-shadow: 0 0 0 1px #ffffff inset, 1px 1px 0 0 #f1f1ed, -1px -1px 0 0 #dfdbd7; +} +.rgdns-button[disabled] { + background: #f1f1ed !important; + box-shadow: initial !important; + color: #a1a192 !important; + border-color: #c4c3bf !important; +} + +.rgdns-form-field { + display: block; + margin: 7px 2px; +} +.rgdns-form-field-title { + padding: 0 3px; +} +.rgdns-form-field-value .rgdns-input { + width: 100%; +} +.rgdns-form-field-checkbox .rgdns-form-field-title, +.rgdns-form-field-checkbox .rgdns-form-field-value { + display: inline-block; +} +.rgdns-login-error { + color: #c22; + font-size: 1.1em; + padding: 4px; + padding-bottom: 0; + margin-bottom: -4px; +} +.rgdns-form-buttons { + text-align: right; +} + +.rgdns-login { + max-width: 300px; + margin: 10px auto; + border: 1px solid #ccf; + background-color: #eef; +} +.rgdns-login h2 { + padding: 2px 4px 5px; + background-color: #ccf; +} + +.rgdns-container { + margin: 5px 15px; +} + +.rgdns-ul { + margin-left: 1.5em; + list-style: square; +} +.rgdns-li-hr { + list-style: none; + border-top: 1px solid #000; + margin: 2px 0; + margin-left: -1.5em; +} + +.rgdns-home { + max-width: 200px; +} + +.rgdns-setting { + max-width: 400px; + margin: 10px 0; +} + +.rgdns-error-paragraph { + color: #c22; +} + +.rgdns-table { + border: 1px solid #ccf; + border-spacing: 0; + margin: 10px; +} +.rgdns-table a { + margin: 1px; +} +.rgdns-table a:focus { + margin: 0; +} +.rgdns-table th { + background-color: #ccf; +} +.rgdns-table th, +.rgdns-table td { + padding: 4px 6px; +} +.rgdns-table tr:not(:last-child) td { + border-bottom: 1px solid #ccf; +} +.rgdns-table-important { + background-color: #ccf; + font-weight: bold; +} +.rgdns-table-centre { + text-align: center; +} +.rgdns-table-right { + text-align: right; +} + +.rgdns-attempts-table th:nth-child(1) { + min-width: 140px; +} +.rgdns-attempts-table th:nth-child(2) { + min-width: 60px; +} +.rgdns-attempts-table th:nth-child(3) { + min-width: 250px; +} + +.rgdns-attempts-table td { + text-align: center; +} +.rgdns-attempts-table td:nth-child(2) { + font-weight: bold; +} +.rgdns-attempts-table-success { + background-color: #cfc; +} +.rgdns-attempts-table-success td:nth-child(2) { + color: #0c0; +} +.rgdns-attempts-table-fail { + background-color: #fcc; +} +.rgdns-attempts-table-fail td:nth-child(2) { + color: #c00; +} + +.rgdns-users-table td:nth-child(1), +.rgdns-nameservers-table td:nth-child(1) { + text-align: right; + background-color: #ccf; +} +.rgdns-users-table td:nth-child(2), +.rgdns-nameservers-table td:nth-child(2), +.rgdns-nameservers-table td:nth-child(3), +.rgdns-domains-table-name, +.rgdns-domains-table-owner { + min-width: 150px; +} +.rgdns-users-table td:nth-child(3) { + min-width: 40px; +} +.rgdns-users-table td:nth-child(3), +.rgdns-users-table td:nth-child(4), +.rgdns-users-table td:nth-child(5), +.rgdns-users-table td:nth-child(6), +.rgdns-nameservers-table td:nth-child(4), +.rgdns-nameservers-table td:nth-child(5), +.rgdns-nameservers-table td:nth-child(6) { + text-align: center; +} +.rgdns-users-table td:nth-child(1), +.rgdns-users-table td:nth-child(2), +.rgdns-users-table td:nth-child(3) +.rgdns-nameservers-table td:nth-child(1), +.rgdns-nameservers-table td:nth-child(2) { + font-weight: bold; +} +.rgdns-records-name, +.rgdns-users-table td:nth-child(4), +.rgdns-nameservers-table td:nth-child(5), +.rgdns-domains-table-created { + min-width: 200px; +} +.rgdns-users-table td:nth-child(5), +.rgdns-nameservers-table td:nth-child(6), +.rgdns-domains-table-disabled { + min-width: 240px; +} +.rgdns-domains-table-created, +.rgdns-domains-table-disabled { + text-align: center; +} +.rgdns-users-table-yes, +.rgdns-domains-table-active { + color: #0c0; +} +.rgdns-users-table-no, +.rgdns-domains-table-inactive { + color: #c00; +} + +.rgdns-domains-form, +.rgdns-nameserver-form, +.rgdns-records-form { + max-width: 400px; +} + +.rgdns-domains-table-status { + text-align: center; + font-weight: bold; + min-width: 100px; +} + +.rgdns-records-type { + text-align: center; + min-width: 60px; +} +.rgdns-records-type { + font-weight: bold; +} + +.rgdns-records-name { + text-align: right; +} +.rgdns-records-ttl, +.rgdns-records-priority { + text-align: center; +} +.rgdns-records-default { + color: #aaa; +}