From c4c51e83c2522f31d308664c8846a328c1fe107d Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Tue, 7 Nov 2023 16:36:40 -0800 Subject: [PATCH] refactor: php $docroot null coalescing assignment --- .../dynamix.my.servers/data/server-state.php | 2 +- .../include/UpdateFlashBackup.php | 662 ++++++++++++++++++ .../dynamix.my.servers/include/myservers2.php | 2 +- .../include/reboot-details.php | 2 +- .../dynamix.my.servers/include/state.php | 2 +- .../dynamix.my.servers/include/unraid-api.php | 2 +- .../dynamix.my.servers/scripts/gitflash_log | 32 + .../plugins/dynamix/include/UpdateDNS.php | 439 ++++++++++++ 8 files changed, 1138 insertions(+), 5 deletions(-) create mode 100644 plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php create mode 100644 plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/gitflash_log create mode 100644 plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php index 6ed4af1f9..7b424583f 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php @@ -1,5 +1,5 @@ + $value) { + if ($value === false || $value === 'false') $value = 'no'; + if ($value === true || $value === 'true') $value = 'yes'; + $text .= "$key=" . $value . "\n"; + } + $flashbackup_tmp = '/var/local/emhttp/flashbackup.new'; + file_put_contents($flashbackup_tmp, $text); + rename($flashbackup_tmp, $flashbackup_ini); +} + +function load_flash_backup_state() { + global $arrState,$flashbackup_ini,$isRegistered; + + $arrState = [ + 'activated' => 'no', + 'uptodate' => 'no', + 'loading' => '', + 'error' => '', + 'remoteerror' => '' + ]; + + $arrNewState = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : []; + if ($arrNewState) { + $arrState = array_merge($arrState, $arrNewState); + $arrState['activated'] = ($arrState['activated'] === true || $arrState['activated'] === 'true') ? 'yes' : 'no'; + $arrState['uptodate'] = ($arrState['uptodate'] === true || $arrState['uptodate'] === 'true') ? 'yes' : 'no'; + } + + $arrState['registered'] = ($isRegistered) ? 'yes' : 'no'; +} + +function write_log($msg) { + global $gitflash, $command; + error_log('['.date("Y/m/d H:i:s e").'] '.$command.' '.$msg."\n\n", 3, $gitflash); +} + +function exec_log($cmd, &$output = [], &$retval = 0) { + try { + exec($cmd.' 2>&1', $output, $retval); + + if ($retval === 0) { + write_log(' Command \''.$cmd.'\' exited with code '.$retval); + } else { + write_log(' Command \''.$cmd.'\' exited with code '.$retval.', response was:'."\n".implode("\n", $output)); + } + } catch (Exception $e) { + write_log(' Command \''.$cmd.'\' exited with code '.$retval.' with exception:'."\n".$e->getMessage()); + } +} + +function set_git_config($name, $value) { + $config_output = $return_var = null; + exec('git -C /boot config --get '.escapeshellarg($name).' 2>&1', $config_output, $return_var); + if (empty($config_output) || strcmp($config_output[0], $value) !== 0) { + exec_log('git -C /boot config '.escapeshellarg($name).' '.escapeshellarg($value)); + } +} + +function readFromFile($file): string { + $text = ""; + if (file_exists($file)) { + $fp = fopen($file,"r"); + if (flock($fp, LOCK_EX)) { + $text = fread($fp, filesize($file)); + flock($fp, LOCK_UN); + fclose($fp); + } + } + return $text; +} + +function appendToFile($file, $text): void { + $fp = fopen($file,"a"); + if (flock($fp, LOCK_EX)) { + fwrite($fp, $text); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + } +} + +function writeToFile($file, $text): void { + $fp = fopen($file,"w"); + if (flock($fp, LOCK_EX)) { + fwrite($fp, $text); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + } +} + +// Source: https://stackoverflow.com/a/2524761 +function isValidTimeStamp($timestamp) +{ + return ((string) (int) $timestamp === $timestamp) + && ($timestamp <= PHP_INT_MAX) + && ($timestamp >= ~PHP_INT_MAX); +} + +function cleanupCounter(string $dataFile, int $time): int { + global $cooldown; + + // Read existing dataFile + @mkdir(dirname($dataFile), 0755); + $dataText = readFromFile($dataFile); + $data = explode("\n", trim($dataText)); + + // Remove entries older than $cooldown minutes, and entries that are not timestamps + $updateDataFile = false; + foreach ((array) $data as $key => $value) { + if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) { + unset ($data[$key]); + $updateDataFile = true; + } + } + + // Save data to disk + if ($updateDataFile) { + $dataText = implode("\n", $data)."\n"; + writeToFile($dataFile, $dataText); + } + return count($data); +} + +// rename /boot/.git to /boot/.git{random}, then start process to delete in background +function deleteLocalRepo() { + global $arrState; + + $mainGitDir = '/boot/.git'; + $tmpGitDir = '/boot/.git'.rand(); + if (is_dir($mainGitDir)) { + rename($mainGitDir, $tmpGitDir); + exec('echo "rm -rf '.$tmpGitDir.' &>/dev/null" | at -q f -M now &>/dev/null'); + } + + // reset state + $arrState['activated'] = 'no'; + $arrState['uptodate'] = 'no'; + $arrState['loading'] = ''; + $arrState['error'] = ''; + $arrState['remoteerror'] = ''; +} + +$validCommands = [ + 'init', //default + 'activate', + 'status', + 'update', + 'update_nolimit', + 'flush', + 'deactivate' +]; + +$command = 'init'; +if ($cli) { + if ($argc > 1) $command = $argv[1]; + if ($argc > 2) $commitmsg = $argv[2]; +} else { + $command = $_POST['command']??''; + $commitmsg = $_POST['commitmsg']??''; +} +if (!in_array($command, $validCommands)) $command = 'init'; +if (empty($commitmsg)) { + $commitmsg = 'Config change'; +} +$ignoreRateLimit = false; +if ($command == 'update_nolimit') { + $ignoreRateLimit = true; + $command = 'update'; +} + +$loadingMessage = ''; + +switch ($command) { + case 'activate': + $loadingMessage = 'Activating'; + break; + case 'deactivate': + $loadingMessage = 'Deactivating'; + break; + case 'update': + case 'update_nolimit': + case 'flush': + $loadingMessage = 'Processing'; + break; + case 'status': + $loadingMessage = 'Loading'; + break; +} + +// rotate gitflash log file so it doesn't get too large +$gitflash = '/var/log/gitflash'; +if (@filesize($gitflash) > 100000) { // 100kb + if (file_exists($gitflash."1")) unlink($gitflash."1"); + rename($gitflash, $gitflash."1"); +} + +load_flash_backup_state(); + +// don't interrupt activate command +if ($command != 'activate' && $loadingMessage == 'Activating') { + exit('{}'); +} + +// if already processing, bail +if ($arrState['loading'] == 'Processing' && $loadingMessage == 'Processing') { + exit('{}'); +} + +// if git is still running, bail +exec("pgrep -f '^git -C /boot' -c 2>&1", $pgrep_output, $retval); +if ($pgrep_output[0] != "0") { + exit('{}'); +} + +// check if signed-in +if (!$isRegistered) { + response_complete(406, array('error' => 'Must be signed in to My Servers to use Flash Backup')); +} + +// keyfile +if (!file_exists('/var/local/emhttp/var.ini')) { + response_complete(406, array('error' => 'Machine still booting')); +} +$var = parse_ini_file("/var/local/emhttp/var.ini"); +$keyfile = empty($var['regFILE']) ? false : @file_get_contents($var['regFILE']); +if ($keyfile === false) { + response_complete(406, array('error' => 'Registration key required')); +} +$keyfile = @base64_encode($keyfile); + +// check if activated +if ($command != 'activate') { + $config_output = $return_var = null; + exec('git -C /boot config --get remote.origin.url 2>&1', $config_output, $return_var); + if (($return_var != 0) || (strpos($config_output[0],'backup.unraid.net') === false)) { + $arrState['activated'] = 'no'; + response_complete(406, array('error' => 'Not activated')); + } else { + $arrState['activated'] = 'yes'; + } +} + +// if flush command, invoke our background rc.flash_backup to flush +if ($command == 'flush') { + exec('/etc/rc.d/rc.flash_backup flush &>/dev/null'); + response_complete(200, '{}'); +} + +if (!empty($loadingMessage)) { + save_flash_backup_state($loadingMessage); +} + +if ($command == 'deactivate') { + exec_log('git -C /boot remote remove origin'); + exec('/etc/rc.d/rc.flash_backup stop &>/dev/null'); + deleteLocalRepo(); + response_complete(200, '{}'); +} + +// build a list of sha256 hashes of the bzfiles +$bzfilehashes = []; +$allbzfiles = ['bzimage','bzfirmware','bzmodules','bzroot','bzroot-gui']; +foreach ($allbzfiles as $bzfile) { + if (!file_exists("/boot/$bzfile")) { + response_complete(406, array('error' => 'missing /boot/'.$bzfile)); + } + $sha256 = trim(@file_get_contents("/boot/$bzfile.sha256")); + if (strlen($sha256) != 64) { + $sha256 = hash_file('sha256', "/boot/$bzfile"); + file_put_contents("/boot/$bzfile.sha256", $sha256."\n"); + } + $bzfilehashes[] = $sha256; +} + +$ch = curl_init('https://keys.lime-technology.com/backup/flash/activate'); +curl_setopt($ch, CURLOPT_POST, 1); +curl_setopt($ch, CURLOPT_POSTFIELDS, [ + 'keyfile' => $keyfile, + 'version' => $var['version'], + 'bzfiles' => implode(',', $bzfilehashes) +]); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$result = curl_exec($ch); +$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); + +if ($result === false) { + response_complete(500, array('error' => $error)); +} + +$json = json_decode($result, true); +if (empty($json) || empty($json['ssh_privkey']) || empty($json['ssh_pubkey'])) { + response_complete(406, $result); +} + +// Show any warnings from the key-server +if (!empty($json['warn'])) { + $arrState['remoteerror'] = $json['warn']; +} + +// save the public and private keys +if (!file_exists('/root/.ssh')) { + mkdir('/root/.ssh', 0700); +} + +$privkey_file='/root/.ssh/unraidbackup_id_ed25519'; +$pubkey_file='/root/.ssh/unraidbackup_id_ed25519.pub'; +if (!file_exists($privkey_file) || ($json['ssh_privkey'] != file_get_contents($privkey_file))) { + file_put_contents($privkey_file, $json['ssh_privkey']); + chmod($privkey_file, 0600); +} +if (!file_exists($pubkey_file) || ($json['ssh_pubkey'] != file_get_contents($pubkey_file))) { + file_put_contents($pubkey_file, $json['ssh_pubkey']); + chmod($pubkey_file, 0644); +} + +// add configuration to use our keys +$sshconfig_file='/root/.ssh/config'; +$sshconfig_fix=false; +if (!file_exists($sshconfig_file)) { + $sshconfig_fix=true; +} else { + // detect uncommented 'Host backup.unraid.net' + preg_match_all('/^\s*[^#]?\s*Host backup.unraid.net/m', file_get_contents($sshconfig_file), $matches, PREG_SET_ORDER, 0); + if (empty($matches)) { + $sshconfig_fix=true; + } +} +if ($sshconfig_fix) { + file_put_contents($sshconfig_file, 'Host backup.unraid.net +IdentityFile ~/.ssh/unraidbackup_id_ed25519 +IdentitiesOnly yes +', FILE_APPEND); + chmod($sshconfig_file, 0644); +} + +// add all of our server keys as known hosts +$arrKnownHosts = [ + 'backup.unraid.net ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCg2CMfRk0Vmkmec04TlgHyZB4F/u+EyfL1BtrWQzu8p2DzRKZww0JXTxHfNc06kQ/EvRW6lkJUQX2eug7UgnRImenxusgMAYnxBCdj+txnzHQ6/JPpXtde54H8tpC8c6xV5BP8UVQ/whBskGIMeM5HTcvSd5cZa1+KaFanygQ20kM6YbZMP9M+UYG59USJs2XD9HP9Pcb4W18y1lMCU2PPrhxCK4dtZxe/903ir6jt3VXES1EV5q6uLAyPtEhB5sybr5a/P9dy41q0v/GxK12VNDJxywHx1muYuSilOXz5lB6KSc1lLKAtitgC5Q5K/A1akgdXY7MDPwnF/rji3jgF', + 'backup.unraid.net ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKrKXKQwPZTY25MoveIw7fZ3IoZvvffnItrx6q7nkNriDMr2WAsoxu0DrU2QrSLH5zFF1ibv4tChS1hOpiYObiI=', + 'backup.unraid.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINw447tJ+nQ/dGz05Gn9VtzGZdXI7o+srED3Gi9kImY5' +]; +$knownhosts_file='/root/.ssh/known_hosts'; +foreach ($arrKnownHosts as $strKnownHost) { + if (!file_exists($knownhosts_file) || strpos(file_get_contents($knownhosts_file),$strKnownHost) === false) { + file_put_contents($knownhosts_file, $strKnownHost."\n", FILE_APPEND); + } +} + +// blow away existing repo if activate command +if ($command == 'activate' && file_exists('/boot/.git')) { + deleteLocalRepo(); +} + +// ensure git repo is setup on the flash drive +if (!file_exists('/boot/.git/info/exclude')) { + exec_log('git init /boot'); +} + +// setup a nice git description +$gitdesc_file='/boot/.git/description'; +if (!file_exists($gitdesc_file) || strpos(file_get_contents($gitdesc_file),$var['NAME']) === false) { + file_put_contents($gitdesc_file, 'Unraid flash drive for '.$var['NAME']."\n"); +} + +// configure git to use the noprivatekeys filter +set_git_config('filter.noprivatekeys.clean', '/usr/local/emhttp/plugins/dynamix.my.servers/scripts/git-noprivatekeys-clean'); + +// configure git to apply the noprivatekeys filter to wireguard config files +$gitattributes_file='/boot/.gitattributes'; +if (!file_exists($gitattributes_file) || strpos(file_get_contents($gitattributes_file),'noprivatekeys') === false) { + file_put_contents($gitattributes_file, '# file managed by Unraid, do not modify +config/wireguard/*.cfg filter=noprivatekeys +config/wireguard/*.conf filter=noprivatekeys +config/wireguard/peers/*.conf filter=noprivatekeys +'); +} + +// setup git ignore for files we dont need in the flash backup +$gitexclude_file='/boot/.git/info/exclude'; +if (!file_exists($gitexclude_file) || strpos(file_get_contents($gitexclude_file),'# version 1.0') === false) { + file_put_contents($gitexclude_file, '# file managed by Unraid, do not modify +# version 1.0 + +# Blacklist everything +/* + +# Whitelist selected root files +!*.sha256 +!changes.txt +!license.txt +!startup.nsh + +!EFI*/ +EFI*/boot/* +!EFI*/boot/syslinux.cfg + +!syslinux/ +syslinux/* +!syslinux/syslinux.cfg +!syslinux/syslinux.cfg- + +# Whitelist entire config directory +!config/ +# except for selected files +config/drift +config/forcesync +config/plugins/unRAIDServer.plg +config/random-seed +config/shadow +config/smbpasswd +config/plugins/**/*.tgz +config/plugins/**/*.txz +config/plugins/**/*.tar.bz2 +config/plugins-error +config/plugins-old-versions +config/plugins/dockerMan/images +config/wireguard/peers/*.png +'); +} + +// ensure git user is configured +set_git_config('user.email', 'gitbot@unraid.net'); +set_git_config('user.name', 'gitbot'); + +// ensure dns can resolve backup.unraid.net +if (! checkdnsrr("backup.unraid.net","A") ) { + $arrState['loading'] = ''; + $arrState['error'] = 'DNS is unable to resolve backup.unraid.net'; + response_complete(406, array('error' => $arrState['error'])); +} + +// bail if too many recent git updates. +$cooldown = 3 * 60 * 60; // 180 mins / 3 hours +$maxCommitCount = 20; // maxCommitCount per cooldown minutes +$commitCountFile = "/var/log/gitcount"; +$time = time(); +$commitCount = cleanupCounter($commitCountFile, $time); +if (!$ignoreRateLimit && $commitCount >= $maxCommitCount) { + $arrState['remoteerror'] = 'Rate limited, will try again later'; + // log once every 10 minutes + if (date("i") % 10 === 0) write_log($arrState['error'].'; '.$arrState['remoteerror']); + response_complete(406, array('error' => $arrState['remoteerror'])); +} elseif ($arrState['remoteerror']??'' == 'Rate limited, will try again later') { + // no longer rate limited, clear the 'remoteerror' + $arrState['remoteerror'] = ''; +} + +// test which ssh port allows a connection (standard ssh port 22 or alternative port 443) +$SSH_PORT = ''; +exec('timeout 17 ssh -o ConnectTimeout=15 -T git@backup.unraid.net 2>&1', $ssh_output, $return_var); +if ($return_var == 128) { + $SSH_PORT = '22'; +} else { + exec('timeout 17 ssh -o ConnectTimeout=15 -p 443 -T git@backup.unraid.net 2>&1', $ssh_output, $return_var); + if ($return_var == 128) { + $SSH_PORT = '443'; + } +} +write_log('ssh_output '.implode($ssh_output)); +if (empty($SSH_PORT)) { + if ($loadingMessage == 'Activating') { + // still syncing auth_keys on the serverside, ignore for activation + $SSH_PORT = '22'; + } else { + $arrState['loading'] = ''; + if (stripos(implode($ssh_output),'permission denied') !== false) { + $arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud'; + } else { + $arrState['error'] = 'Unable to connect to backup.unraid.net:22'; + } + response_complete(406, array('error' => $arrState['error'])); + } +} else if ($arrState['error'] == 'Unable to connect to backup.unraid.net:22') { + // can now connect, clear previous error + $arrState['error'] = ''; +} + +// ensure upstream git server is configured and in-sync +if (strpos(file_get_contents('/boot/.git/config'),'[remote "origin"]') === false) { + exec('git -C /boot remote add -f -t master -m master origin ssh://git@backup.unraid.net:'.$SSH_PORT.'/~/flash.git &>/dev/null'); +} else if (strpos(file_get_contents('/boot/.git/config'),'ssh://git@backup.unraid.net:22/~/flash.git') === false) { + exec('git -C /boot remote set-url origin ssh://git@backup.unraid.net:'.$SSH_PORT.'/~/flash.git &>/dev/null'); +} + +if ($command == 'activate') { + $arrState['uptodate'] == 'no'; + +} else { + // determine current status of local repo + + exec_log('git -C /boot reset origin/master'); + exec_log('git -C /boot checkout -B master origin/master'); + + // establish status + exec_log('git -C /boot status --porcelain 2>&1', $status_output, $return_var); + + if ($return_var != 0) { + // detect git submodule + if (stripos(implode($status_output),'failed in submodule') !== false) { + $arrState['loading'] = ''; + $arrState['error'] = 'git submodules are incompatible with our flash backup solution'; + response_complete(406, array('error' => $arrState['error'])); + } + + if (stripos(implode($status_output),'index file smaller than expected') !== false) { + // repair git index + exec_log('rm -f /boot/.git/index'); + exec_log('git -C /boot reset HEAD .'); + exec('git -C /boot status --porcelain 2>&1', $status_output, $return_var); + } + } + + if ($return_var != 0) { + write_log('bailing - status return_var is '.$return_var); + $arrState['loading'] = ''; + $arrState['error'] = $status_output[0]; + response_complete(406, array('error' => $arrState['error'])); + } + + $arrState['uptodate'] = empty($status_output) ? 'yes' : 'no'; + + // check for any pending commits + if ($arrState['uptodate'] == 'yes') { + // no untracked files; check if there are pending commits + exec('git -C /boot rev-list origin/master..master --count 2>&1', $revlist_output); + if (trim($revlist_output[0]) != '0') { + $arrState['uptodate'] = 'no'; + } else { + $arrState['error'] = ''; + } + } + + if ($arrState['error'] != 'Failed to sync flash backup') { + $arrState['error'] = ''; + } + + // detect corruption #1 + exec_log('git -C /boot show --summary 2>&1', $show_output, $return_var); + if ($return_var != 0) { + if (stripos(implode($show_output),'fatal: your current branch appears to be broken') !== false) { + $arrState['error'] = 'Error: Backup corrupted'; + exec('/etc/rc.d/rc.flash_backup stop &>/dev/null'); + } + } + + if ($command == 'status') { + $data = implode("\n", $status_output); + response_complete($httpcode, array('data' => $data), $data); + } + +} // end check for ($command == 'activate') + +if ($command == 'update' || $command == 'activate') { + + if ($arrState['uptodate'] == 'no') { + // increment git commit counter + appendToFile($commitCountFile, $time."\n"); + + // add and commit all file changes + exec_log('git -C /boot add -A'); + exec_log('git -C /boot commit -m ' . escapeshellarg($commitmsg)); + + // push changes upstream + exec_log('git -C /boot push --set-upstream origin master', $push_output, $return_var); + if ($return_var != 0) { + exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var); + } + if ($return_var != 0) { + // check for permission denied + if (stripos(implode($push_output),'permission denied') !== false) { + $arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud'; + } elseif (stripos(implode($push_output),'fatal: loose object') !== false && stripos(implode($push_output),'is corrupt') !== false) { + // detect corruption #2 + $arrState['error'] = 'Error: Backup corrupted'; + exec('/etc/rc.d/rc.flash_backup stop &>/dev/null'); + } else { + $arrState['error'] = 'Failed to sync flash backup'; + } + response_complete($httpcode, '{}'); + } + $arrState['uptodate'] = 'yes'; + $arrState['error'] = ''; + } + + if ($arrState['error'] == 'Failed to sync flash backup') { + // only clear the error state if it failed to connect before + $arrState['error'] = ''; + } +} + +if ($command == 'activate') { + exec('/etc/rc.d/rc.flash_backup start &>/dev/null'); +} + +response_complete($httpcode, '{}'); +?> diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php index 0ef9dae2a..ed8c5f839 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php @@ -1,5 +1,5 @@ diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php index 29c91e48f..db6be53fd 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php @@ -30,7 +30,7 @@ class RebootDetails */ private function detectRebootType() { - $docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'; + $docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'); $rebootReadme = @file_get_contents("$docroot/plugins/unRAIDServer/README.md", false, null, 0, 20) ?: ''; $rebootDetected = preg_match("/^\*\*(REBOOT REQUIRED|DOWNGRADE)/", $rebootReadme); diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php index c83821088..77e92bab4 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php @@ -1,5 +1,5 @@ +ErrorWarningSystem

"; +echo "

Transient errors in this log can be ignored unless you are having issues

\n"; + +$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'); +require_once "$docroot/webGui/include/ColorCoding.php"; + +$log = "/var/log/gitflash"; +if (file_exists($log) && filesize($log)) { + $lines = file($log); + + foreach ($lines as $line) { + $span = "span"; + foreach ($match as $type) foreach ($type['text'] as $text) if (preg_match("/$text/i",$line)) {$span = "span class='{$type['class']}'"; break 2;} + echo "<$span>".htmlspecialchars(trim($line))."\n"; + } +} + +?> \ No newline at end of file diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php new file mode 100644 index 000000000..59512d69e --- /dev/null +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php @@ -0,0 +1,439 @@ + + 1) && $argv[1] == "-v") { + $verbose = true; + $anon = true; +} +if ($cli && ($argc > 1) && $argv[1] == "-vv") { + $verbose = true; +} +$var = parse_ini_file('/var/local/emhttp/var.ini'); +$nginx = parse_ini_file('/var/local/emhttp/nginx.ini'); +$is69 = version_compare($var['version'],"6.9.9","<"); +$reloadNginx = false; +$dnserr = false; +$icon_warn = "⚠️ "; +$icon_ok = "✅ "; + +$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg'; +$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : []; +// ensure some vars are defined here so we don't have to test them later +if (empty($myservers['remote']['apikey'])) { + $myservers['remote']['apikey'] = ""; +} +if (empty($myservers['remote']['wanaccess'])) { + $myservers['remote']['wanaccess'] = "no"; +} +if (empty($myservers['remote']['wanport'])) { + $myservers['remote']['wanport'] = 443; +} +// remoteaccess, externalport +if ($cli) { + $remoteaccess = (empty($nginx['NGINX_WANFQDN'])) ? 'no' : 'yes'; + $externalport = $myservers['remote']['wanport']; +} else { + $remoteaccess = $_POST['remoteaccess']??'no'; + $externalport = intval($_POST['externalport']??443); + + if ($remoteaccess != 'yes') { + $remoteaccess = 'no'; + } + + if ($externalport < 1 || $externalport > 65535) { + $externalport = 443; + } + + if ($myservers['remote']['wanaccess'] != $remoteaccess) { + // update the wanaccess ini value + $orig = file_exists($myservers_flash_cfg_path) ? parse_ini_file($myservers_flash_cfg_path,true) : []; + if (!$orig) { + $orig = ['remote' => $myservers['remote']]; + } + $orig['remote']['wanaccess'] = $remoteaccess; + $text = ''; + foreach ($orig as $section => $block) { + $pairs = ""; + foreach ($block as $key => $value) if (strlen($value)) $pairs .= "$key=\"$value\"\n"; + if ($pairs) $text .= "[$section]\n".$pairs; + } + if ($text) file_put_contents($myservers_flash_cfg_path, $text); + // need nginx reload + $reloadNginx = true; + } +} +$isRegistered = !empty($myservers['remote']['username']); + +// protocols, hostnames, ports +$internalprotocol = 'http'; +$internalport = $nginx['NGINX_PORT']; +$internalhostname = $nginx['NGINX_LANMDNS']; +$externalprotocol = 'https'; +// keyserver will expand *.hash.myunraid.net or add www to hash.unraid.net as needed +$externalhostname = $nginx['NGINX_CERTNAME']; +$isLegacyCert = preg_match('/.*\.unraid\.net$/', $nginx['NGINX_CERTNAME']); +$isWildcardCert = preg_match('/.*\.myunraid\.net$/', $nginx['NGINX_CERTNAME']); +$internalip = $nginx['NGINX_LANIP']; + +if ($nginx['NGINX_USESSL']=='yes') { + // When NGINX_USESSL is 'yes' in 6.9, it could be using either Server_unraid_bundle.pem or certificate_bundle.pem + // When NGINX_USESSL is 'yes' in 6.10, it is is using Server_unraid_bundle.pem + $internalprotocol = 'https'; + $internalport = $nginx['NGINX_PORTSSL']; + if ($is69 && $nginx['NGINX_CERTNAME']) { + // this is from certificate_bundle.pem + $internalhostname = $nginx['NGINX_CERTNAME']; + } +} +if ($nginx['NGINX_USESSL']=='auto') { + // NGINX_USESSL cannot be 'auto' in 6.9, it is either 'yes' or 'no' + // When NGINX_USESSL is 'auto' in 6.10, it is using certificate_bundle.pem + $internalprotocol = 'https'; + $internalport = $nginx['NGINX_PORTSSL']; + // keyserver will expand *.hash.myunraid.net as needed + $internalhostname = $nginx['NGINX_CERTNAME']; +} + +// My Servers version +$plgversion = file_exists("/var/log/plugins/dynamix.unraid.net.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null')) + : ( file_exists("/var/log/plugins/dynamix.unraid.net.staging.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null')) + : 'base-'.$var['version'] ); + +// only proceed when when signed in or when legacy unraid.net SSL certificate exists +if (!$isRegistered && !$isLegacyCert) { + response_complete(406, array('error' => _('Nothing to do'))); +} + +// keyfile +$keyfile = empty($var['regFILE']) ? false : @file_get_contents($var['regFILE']); +if ($keyfile === false) { + response_complete(406, array('error' => _('Registration key required'))); +} +$keyfile = @base64_encode($keyfile); + +// build post array +$post = [ + 'keyfile' => $keyfile, + 'plgversion' => $plgversion +]; +if ($isLegacyCert) { + // sign in not required to maintain local ddns for unraid.net cert + // enable local ddns regardless of use_ssl value + $post['internalip'] = $internalip; + // if host.unraid.net does not resolve to the internalip and DNS Rebind Protection is disabled, disable caching + if (host_lookup_ip(generate_internal_host($nginx['NGINX_CERTNAME'], $post['internalip'])) != $post['internalip'] && rebindDisabled()) $dnserr = true; +} +if ($isRegistered) { + // if signed in, send data needed to maintain My Servers Dashboard + $post['internalhostname'] = $internalhostname; + $post['internalport'] = $internalport; + $post['internalprotocol'] = $internalprotocol; + $post['remoteaccess'] = $remoteaccess; + $post['servercomment'] = $var['COMMENT']; + $post['servername'] = $var['NAME']; + if ($isWildcardCert) { + // keyserver needs the internalip to generate the local access url + $post['internalip'] = $internalip; + } + if ($remoteaccess == 'yes') { + // include wanip in the cache file so we can track if it changes + $post['_wanip'] = trim(@file_get_contents("https://wanip4.unraid.net/")); + $post['externalhostname'] = $externalhostname; + $post['externalport'] = $externalport; + $post['externalprotocol'] = $externalprotocol; + // if wanip.hash.myunraid.net or www.hash.unraid.net does not resolve to the wanip, disable caching + if (host_lookup_ip(generate_external_host($post['externalhostname'], $post['_wanip'])) != $post['_wanip']) $dnserr = true; + } +} + +// Include unraid-api report +$unraidreport = []; +if (file_exists('/usr/local/sbin/unraid-api')) { + $jsonString = trim(@exec("/usr/local/sbin/unraid-api report --json 2>/dev/null")); + $unraidreport = @json_decode($jsonString, true); + if ($unraidreport === false) { + $post['unraidreport'] = $jsonString; + } else { + // remove fields we don't need to submit + unset($unraidreport['servers']); + } +} elseif (strpos($plgversion, "base-") === false) { + // The plugin is installed but the api doesn't exist. This is a failed install. Generate basic troubleshooting data. + if (file_exists('/boot/config/plugins/dynamix.my.servers/env')) { + @extract(parse_ini_file('/boot/config/plugins/dynamix.my.servers/env',true)); + } + if (empty($env)) { + $env = "production"; + } + $unraidreport['os']['version'] = $var['version']; + $unraidreport['api']['version'] = "failed install"; + $unraidreport['api']['status'] = "missing"; + $unraidreport['api']['environment'] = $env; + $unraidreport['relay']['status'] = "disconnected"; + $unraidreport['minigraph']['status'] = "disconnected"; + if ($isRegistered) { + $unraidreport['myServers']['status'] = "authenticated"; + $unraidreport['myServers']['myServersUsername'] = $myservers['remote']['username']; + } else { + $unraidreport['myServers']['status'] = "signed out"; + } + $unraidreport['apiKey'] = (empty($myservers['remote']['apikey'])) ? "invalid" : "exists"; +} + +if (!empty($unraidreport)) { + // include unraid-api crash logs + $crashLog = '/var/log/unraid-api/crash.json'; + $crashAge = 0; + if (file_exists($crashLog)) { + $crashTime = filemtime($crashLog); + $crashAge = time() - $crashTime; // age of crashLog in seconds + $crashDetails = @json_decode(@file_get_contents($crashLog), true); + if (empty($crashDetails['apiVersion']) && $crashAge < 30*60) { + // found a recent crash log without an apiVersion, assume was created by current version of api + $crashDetails['apiVersion'] = $unraidreport['api']['version']; + // overwrite the crash log so it will always have the apiVersion + file_put_contents($crashLog, json_encode($crashDetails, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + // reset to original timestamp so crashAge remains accurate + touch($crashLog, $crashTime); + } + $unraidreport['crashAge'] = $crashAge; + $unraidreport['crashLogs'] = $crashDetails; + } + + // add flash backup status + $flashbackup_ini = '/var/local/emhttp/flashbackup.ini'; + $flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : []; + if (empty($flashbackup_status['activated'])) { + $flashbackup_status['activated'] = ""; + } + if (empty($flashbackup_status['error'])) { + $flashbackup_status['error'] = ""; + } + $unraidreport['flashbackup']['activated'] = ($flashbackup_status['activated']) ? "yes" : "no"; + $unraidreport['flashbackup']['error'] = ($flashbackup_status['error']) ? $flashbackup_status['error'] : "no"; + + // add unraidreport to payload + $post['unraidreport'] = json_encode($unraidreport); + + // if the api is stopped and there are no crashLogs, or any crashLogs are more than maxCrashAge, start the api + $maxCrashAge = 1*60*60; // 1 hour + if ($unraidreport['api']['status'] == 'stopped' && (empty($unraidreport['crashLogs']) || $crashAge > $maxCrashAge)) { + exec("echo \"/usr/local/sbin/unraid-api start\" | at -M now >/dev/null 2>&1"); + } +} + +// if remoteaccess is enabled in 6.10.0-rc3+ and WANIP has changed since nginx started, reload nginx +if (isset($post['_wanip']) && ($post['_wanip'] != $nginx['NGINX_WANIP']) && version_compare($var['version'],"6.10.0-rc2",">")) $reloadNginx = true; +// if remoteaccess is currently disabled (perhaps because a wanip was not available when nginx was started) +// BUT the system is configured to have it enabled AND a wanip is now available +// then reload nginx +if ($remoteaccess == 'no' && $nginx['NGINX_WANACCESS'] == 'yes' && !empty(trim(@file_get_contents("https://wanip4.unraid.net/")))) $reloadNginx = true; +if ($reloadNginx) { + exec("/etc/rc.d/rc.nginx reload &>/dev/null"); +} + +// maxage is 36 hours +$maxage = 36*60*60; +if ($dnserr || $verbose) $maxage = 0; +$datafile = "/tmp/UpdateDNS.txt"; +$datafiletmp = "/tmp/UpdateDNS.txt.new"; +$dataprev = @file_get_contents($datafile) ?: ''; +$datanew = implode("\n",$post)."\n"; +if ($datanew == $dataprev && (time()-filemtime($datafile) < $maxage)) { + response_complete(204, null, _('No change to report')); +} +file_put_contents($datafiletmp,$datanew); +rename($datafiletmp, $datafile); + +// do not submit the wanip, it will be captured from the submission if needed for remote access +unset($post['_wanip']); + +// report necessary server details to limetech for DNS updates +$ch = curl_init('https://keys.lime-technology.com/account/server/register'); +curl_setopt($ch, CURLOPT_POST, 1); +curl_setopt($ch, CURLOPT_POSTFIELDS, $post); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$result = curl_exec($ch); +$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); + +if ( ($result === false) || ($httpcode != "200") ) { + // delete cache file to retry submission on next run + @unlink($datafile); + response_complete($httpcode ?? "500", array('error' => $error)); +} + +response_complete($httpcode, $result, _('success')); +?>