refactor: php $docroot null coalescing assignment

This commit is contained in:
Zack Spear
2023-11-07 16:36:40 -08:00
parent c91fef9c5f
commit c4c51e83c2
8 changed files with 1138 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
<?php
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$var = (array)parse_ini_file('state/var.ini');
require_once "$docroot/webGui/include/Wrappers.php";

View File

@@ -0,0 +1,662 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
// set GIT_OPTIONAL_LOCKS=0 globally to reduce/eliminate writes to /boot
putenv('GIT_OPTIONAL_LOCKS=0');
$cli = php_sapi_name()=='cli';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$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) : [];
$isRegistered = !empty($myservers['remote']['username']);
$myservers_memory_cfg_path ='/var/local/emhttp/myservers.cfg';
$mystatus = (file_exists($myservers_memory_cfg_path)) ? @parse_ini_file($myservers_memory_cfg_path) : [];
$isConnected = (($mystatus['minigraph']??'')==='CONNECTED') ? true : false;
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
/**
* @name response_complete
* @param {HTTP Response Status Code} $httpcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {String|Array} $result - strings are assumed to be encoded JSON. Arrays will be encoded to JSON.
* @param {String} $cli_success_msg
*/
function response_complete($httpcode, $result, $cli_success_msg='') {
global $cli;
save_flash_backup_state();
$mutatedResult = is_array($result) ? json_encode($result) : $result;
if ($cli) {
$json = @json_decode($mutatedResult,true);
if (!empty($json['error'])) {
echo 'Error: '.$json['error'].PHP_EOL;
exit(1);
}
exit($cli_success_msg.PHP_EOL);
}
header('Content-Type: application/json');
http_response_code($httpcode);
exit((string)$mutatedResult);
}
function save_flash_backup_state($loading='') {
global $arrState,$flashbackup_ini;
$arrState['loading'] = $loading;
$text = "[flashbackup]\n";
foreach ($arrState as $key => $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, '{}');
?>

View File

@@ -1,5 +1,5 @@
<?php
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once("$docroot/plugins/dynamix.my.servers/include/state.php");
require_once("$docroot/plugins/dynamix.my.servers/include/translations.php");
?>

View File

@@ -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);

View File

@@ -1,5 +1,5 @@
<?php
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
// read flashbackup ini file

View File

@@ -13,7 +13,7 @@
<?
$cli = php_sapi_name() == 'cli';
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
/**
* @name response_complete

View File

@@ -0,0 +1,32 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
echo "<p style='text-align:center'><span class='error label'>Error</span><span class='warn label'>Warning</span><span class='system label'>System</span></p>";
echo "<p style='text-align:center'><em>Transient errors in this log can be ignored unless you are having issues</em></p>\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</span>";
}
}
?>

View File

@@ -0,0 +1,439 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
// add translations
$_SERVER['REQUEST_URI'] = 'settings';
require_once "$docroot/webGui/include/Translations.php";
require_once "$docroot/webGui/include/Helpers.php";
function host_lookup_ip($host) {
$result = @dns_get_record($host, DNS_A);
$ip = ($result) ? $result[0]['ip']??'' : '';
return($ip);
}
function rebindDisabled() {
global $isLegacyCert;
$rebindtesturl = $isLegacyCert ? "rebindtest.unraid.net" : "rebindtest.myunraid.net";
// DNS Rebind Protection - this checks the server but clients could still have issues
$validResponse = array("192.168.42.42", "fd42");
$response = host_lookup_ip($rebindtesturl);
return in_array(explode('::',$response)[0], $validResponse);
}
function format_port($port) {
return ($port != 80 && $port != 443) ? ':'.$port : '';
}
function anonymize_host($host) {
global $anon;
if ($anon) {
$host = preg_replace('/.*\.myunraid\.net/', '*.hash.myunraid.net', $host);
$host = preg_replace('/.*\.unraid\.net/', 'hash.unraid.net', $host);
}
return $host;
}
function anonymize_ip($ip) {
global $anon;
if ($anon && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
$ip = "[redacted]";
}
return $ip;
}
function generate_internal_host($host, $ip) {
if (strpos($host,'.myunraid.net') !== false) {
$host = str_replace('*', str_replace('.', '-', $ip), $host);
}
return $host;
}
function generate_external_host($host, $ip) {
if (strpos($host,'.myunraid.net') !== false) {
$host = str_replace('*', str_replace('.', '-', $ip), $host);
} elseif (strpos($host,'.unraid.net') !== false) {
$host = "www.".$host;
}
return $host;
}
function verbose_output($httpcode, $result) {
global $cli, $verbose, $anon, $plgversion, $post, $var, $isRegistered, $myservers, $reloadNginx, $nginx, $isLegacyCert;
global $remoteaccess;
global $icon_warn, $icon_ok;
if (!$cli || !$verbose) return;
if ($anon) echo "(Output is anonymized, use '-vv' to see full details)".PHP_EOL;
echo "Unraid OS {$var['version']}".((strpos($plgversion, "base-") === false) ? " with My Servers plugin version {$plgversion}" : '').PHP_EOL;
echo ($isRegistered) ? "{$icon_ok}Signed in to Unraid.net as {$myservers['remote']['username']}".PHP_EOL : "{$icon_warn}Not signed in to Unraid.net".PHP_EOL ;
echo "Use SSL is {$nginx['NGINX_USESSL']}".PHP_EOL;
echo (rebindDisabled()) ? "{$icon_ok}Rebind protection is disabled" : "{$icon_warn}Rebind protection is enabled";
echo " for ".($isLegacyCert ? "unraid.net" : "myunraid.net").PHP_EOL;
if ($post) {
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
// check the data
$certhostname = $nginx['NGINX_CERTNAME'];
if ($certhostname) {
// $certhostname is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$certhostip = host_lookup_ip(generate_internal_host($certhostname, $post['internalip']));
$certhosterr = ($certhostip != $post['internalip']);
}
if ($post['internalhostname'] != $certhostname) {
// $post['internalhostname'] is $nginx['NGINX_LANMDNS'] (no cert, or Server_unraid_bundle.pem) || $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$internalhostip = host_lookup_ip(generate_internal_host($post['internalhostname'], $post['internalip']));
$internalhosterr = ($internalhostip != $post['internalip']);
}
if (!empty($post['externalhostname'])) {
// $post['externalhostname'] is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$externalhostip = host_lookup_ip(generate_external_host($post['externalhostname'], $wanip));
$externalhosterr = ($externalhostip != $wanip);
}
// anonymize data. no caclulations can be done with this data beyond this point.
if ($anon) {
if (!empty($certhostip)) $certhostip = anonymize_ip($certhostip);
if (!empty($certhostname)) $certhostname = anonymize_host($certhostname);
if (!empty($internalhostip)) $internalhostip = anonymize_ip($internalhostip);
if (!empty($externalhostip)) $externalhostip = anonymize_ip($externalhostip);
if (!empty($wanip)) $wanip = anonymize_ip($wanip);
if (!empty($post['internalip'])) $post['internalip'] = anonymize_ip($post['internalip']);
if (!empty($post['internalhostname'])) $post['internalhostname'] = anonymize_host($post['internalhostname']);
if (!empty($post['externalhostname'])) $post['externalhostname'] = anonymize_host($post['externalhostname']);
if (!empty($post['externalport'])) $post['externalport'] = "[redacted]";
}
// always anonymize the keyfile
if (!empty($post['keyfile'])) $post['keyfile'] = "[redacted]";
// output notes
if (!empty($post['internalprotocol']) && !empty($post['internalhostname']) && !empty($post['internalport'])) {
$localurl = $post['internalprotocol']."://".generate_internal_host($post['internalhostname'], $post['internalip']).format_port($post['internalport']);
echo 'Local Access url: '.$localurl.PHP_EOL;
if ($internalhostip) {
// $internalhostip will not be defined for .local domains, ok to skip
echo ($internalhosterr) ? $icon_warn : $icon_ok;
echo generate_internal_host($post['internalhostname'], $post['internalip'])." resolves to {$internalhostip}";
echo ($internalhosterr) ? ", it should resolve to {$post['internalip']}" : "";
echo PHP_EOL;
}
if ($certhostname) {
echo ($certhosterr) ? $icon_warn : $icon_ok;
echo generate_internal_host($certhostname, $post['internalip']).' ';
echo ($certhostip) ? "resolves to {$certhostip}" : "does not resolve to an IP address";
echo ($certhosterr) ? ", it should resolve to {$post['internalip']}" : "";
echo PHP_EOL;
}
if ($remoteaccess == 'yes' && !empty($post['externalprotocol']) && !empty($post['externalhostname']) && !empty($post['externalport'])) {
$remoteurl = $post['externalprotocol']."://".generate_external_host($post['externalhostname'], $wanip).format_port($post['externalport']);
echo 'Remote Access url: '.$remoteurl.PHP_EOL;
echo ($externalhosterr) ? $icon_warn : $icon_ok;
echo generate_external_host($post['externalhostname'], $wanip).' ';
echo ($externalhosterr) ? "does not resolve to an IP address" : "resolves to {$externalhostip}";
echo PHP_EOL;
}
if ($reloadNginx) {
echo "IP address changes were detected, nginx was reloaded".PHP_EOL;
}
}
// output post data
echo PHP_EOL.'Request:'.PHP_EOL;
echo @json_encode($post, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
}
if ($result) {
echo "Response (HTTP $httpcode):".PHP_EOL;
$mutatedResult = is_array($result) ? json_encode($result) : $result;
echo @json_encode(@json_decode($mutatedResult, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
}
}
/**
* @name response_complete
* @param {HTTP Response Status Code} $httpcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {String|Array} $result - strings are assumed to be encoded JSON. Arrays will be encoded to JSON.
* @param {String} $cli_success_msg
*/
function response_complete($httpcode, $result, $cli_success_msg='') {
global $cli, $verbose;
$mutatedResult = is_array($result) ? json_encode($result) : $result;
if ($cli) {
if ($verbose) verbose_output($httpcode, $result);
$json = @json_decode($mutatedResult,true);
if (!empty($json['error'])) {
echo 'Error: '.$json['error'].PHP_EOL;
exit(1);
}
exit($cli_success_msg.PHP_EOL);
}
header('Content-Type: application/json');
http_response_code($httpcode);
exit((string)$mutatedResult);
}
$cli = php_sapi_name()=='cli';
$verbose = $anon = false;
if ($cli && ($argc > 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'));
?>