From 2df1290681565ae748c1a439c2330cfdd7235409 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Thu, 24 Apr 2025 11:18:19 -0400 Subject: [PATCH 1/8] chore: mv unraid-ui preparation into npm lifecycle --- justfile | 1 - unraid-ui/package.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index 34302797c3..39e6947657 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,6 @@ setup: @just ignore api/dev/states/myservers.cfg api/dev/Unraid.net/myservers.cfg pnpm install - just unraid-ui/setup # restore notification files under api/dev restore-notifications: diff --git a/unraid-ui/package.json b/unraid-ui/package.json index 63b200cf05..e0bfdec1ae 100644 --- a/unraid-ui/package.json +++ b/unraid-ui/package.json @@ -13,6 +13,7 @@ "tailwind.config.ts" ], "scripts": { + "prepare": "pnpm build", "// Development": "", "dev": "vite", "preview": "vite preview", From dcd2c9a08e5b698d931d23d3ad0237403cb2f2fb Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Thu, 24 Apr 2025 11:51:58 -0400 Subject: [PATCH 2/8] add session patch to DefaultPageLayout --- .../__fixtures__/downloaded/.login.php | 255 +++++------------- .../downloaded/.login.php.last-download-time | 2 +- .../downloaded/DefaultPageLayout.php | 7 +- .../DefaultPageLayout.php.last-download-time | 2 +- .../Notifications.page.last-download-time | 2 +- .../auth-request.php.last-download-time | 2 +- .../.login.php.modified.snapshot.php | 255 +++++------------- ...efaultPageLayout.php.modified.snapshot.php | 17 +- .../default-page-layout.modification.ts | 26 ++ .../patches/default-page-layout.patch | 29 +- .../modifications/patches/sso.patch | 34 +-- 11 files changed, 230 insertions(+), 401 deletions(-) diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php index 73b6b554ba..e6367dcbd4 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php @@ -2,10 +2,12 @@ // Included in login.php // Only start a session to check if they have a cookie that looks like our session -$server_name = strtok($_SERVER['HTTP_HOST'],":"); +$server_name = strtok($_SERVER['HTTP_HOST'], ":"); if (!empty($_COOKIE['unraid_'.md5($server_name)])) { // Start the session so we can check if $_SESSION has data - if (session_status()==PHP_SESSION_NONE) session_start(); + if (session_status() == PHP_SESSION_NONE) { + session_start(); + } // Check if the user is already logged in if ($_SESSION && !empty($_SESSION['unraid_user'])) { @@ -15,10 +17,11 @@ } } -function readFromFile($file): string { +function readFromFile($file): string +{ $text = ""; if (file_exists($file) && filesize($file) > 0) { - $fp = fopen($file,"r"); + $fp = fopen($file, "r"); if (flock($fp, LOCK_EX)) { $text = fread($fp, filesize($file)); flock($fp, LOCK_UN); @@ -28,8 +31,9 @@ function readFromFile($file): string { return $text; } -function appendToFile($file, $text): void { - $fp = fopen($file,"a"); +function appendToFile($file, $text): void +{ + $fp = fopen($file, "a"); if (flock($fp, LOCK_EX)) { fwrite($fp, $text); fflush($fp); @@ -38,8 +42,9 @@ function appendToFile($file, $text): void { } } -function writeToFile($file, $text): void { - $fp = fopen($file,"w"); +function writeToFile($file, $text): void +{ + $fp = fopen($file, "w"); if (flock($fp, LOCK_EX)) { fwrite($fp, $text); fflush($fp); @@ -56,7 +61,8 @@ function isValidTimeStamp($timestamp) && ($timestamp >= ~PHP_INT_MAX); } -function cleanupFails(string $failFile, int $time): int { +function cleanupFails(string $failFile, int $time): int +{ global $cooldown; // Read existing fails @@ -67,8 +73,8 @@ function cleanupFails(string $failFile, int $time): int { // Remove entries older than $cooldown minutes, and entries that are not timestamps $updateFails = false; foreach ((array) $fails as $key => $value) { - if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) { - unset ($fails[$key]); + if (!isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time)) { + unset($fails[$key]); $updateFails = true; } } @@ -81,94 +87,19 @@ function cleanupFails(string $failFile, int $time): int { return count($fails); } -function verifyUsernamePassword(string $username, string $password): bool { - if ($username != "root") return false; +function verifyUsernamePassword(string $username, string $password): bool +{ + if ($username != "root") { + return false; + } $output = exec("/usr/bin/getent shadow $username"); - if ($output === false) return false; - $credentials = explode(":", $output); - return password_verify($password, $credentials[1]); -} - -function verifyTwoFactorToken(string $username, string $token): bool { - try { - // Create curl client - $curlClient = curl_init(); - curl_setopt($curlClient, CURLOPT_HEADER, true); - curl_setopt($curlClient, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curlClient, CURLOPT_UNIX_SOCKET_PATH, '/var/run/unraid-api.sock'); - curl_setopt($curlClient, CURLOPT_URL, 'http://unixsocket/verify'); - curl_setopt($curlClient, CURLOPT_BUFFERSIZE, 256); - curl_setopt($curlClient, CURLOPT_TIMEOUT, 5); - curl_setopt($curlClient, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Origin: /var/run/unraid-notifications.sock')); - curl_setopt($curlClient, CURLOPT_POSTFIELDS, json_encode([ - 'username' => $username, - 'token' => $token - ])); - - // Send the request - curl_exec($curlClient); - - // Get the http status code - $httpCode = curl_getinfo($curlClient, CURLINFO_HTTP_CODE); - - // Close the connection - curl_close($curlClient); - - // Error - // This should accept 200 or 204 status codes - if ($httpCode !== 200 && $httpCode !== 204) { - // Log error to syslog - my_logger("2FA code for {$username} is invalid, blocking access!"); - return false; - } - - // Log success to syslog - my_logger("2FA code for {$username} is valid, allowing login!"); - - // Success - return true; - } catch (Exception $exception) { - // Error + if ($output === false) { return false; } + $credentials = explode(":", $output); + return password_verify($password, $credentials[1]); } - -// Check if a haystack ends in a needle -function endsWith($haystack, $needle): bool { - return substr_compare($haystack, $needle, -strlen($needle)) === 0; -} - -// Check if we're accessing this via a wildcard cert -function isWildcardCert(): bool { - global $server_name; - return endsWith($server_name, '.myunraid.net'); -} - -// Check if we're accessing this locally via the expected myunraid.net url -function isLocalAccess(): bool { - global $nginx, $server_name; - return isWildcardCert() && $nginx['NGINX_LANFQDN'] === $server_name; -} - -// Check if we're accessing this remotely via the expected myunraid.net url -function isRemoteAccess(): bool { - global $nginx, $server_name; - return isWildcardCert() && $nginx['NGINX_WANFQDN'] === $server_name; -} - -// Check if 2fa is enabled for local (requires USE_SSL to be "auto" so no alternate urls can access the server) -function isLocalTwoFactorEnabled(): bool { - global $nginx, $my_servers; - return $nginx['NGINX_USESSL'] === "auto" && ($my_servers['local']['2Fa']??'') === 'yes'; -} - -// Check if 2fa is enabled for remote -function isRemoteTwoFactorEnabled(): bool { - global $my_servers; - return ($my_servers['remote']['2Fa']??'') === 'yes'; -} - // Load configs into memory $my_servers = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true); $nginx = @parse_ini_file('/var/local/emhttp/nginx.ini'); @@ -180,38 +111,34 @@ function isRemoteTwoFactorEnabled(): bool { $failFile = "/var/log/pwfail/{$remote_addr}"; // Get the credentials -$username = $_POST['username']??''; -$password = $_POST['password']??''; -$token = $_REQUEST['token']??''; - -// Check if we need 2fa -$twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled()); +$username = $_POST['username'] ?? ''; +$password = $_POST['password'] ?? ''; // If we have a username + password combo attempt to login if (!empty($username) && !empty($password)) { try { - // Bail if we're missing the 2FA token and we expect one - if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected')); - // Read existing fails, cleanup expired ones $time = time(); $failCount = cleanupFails($failFile, $time); // Check if we're limited if ($failCount >= $maxFails) { - if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}"); + if ($failCount == $maxFails) { + my_logger("Ignoring login attempts for {$username} from {$remote_addr}"); + } throw new Exception(_('Too many invalid login attempts')); } // Bail if username + password combo doesn't work - if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password')); - - // Bail if we need a token but it's invalid - if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token')); + if (!verifyUsernamePassword($username, $password)) { + throw new Exception(_('Invalid username or password')); + } // Successful login, start session @unlink($failFile); - if (session_status()==PHP_SESSION_NONE) session_start(); + if (session_status() == PHP_SESSION_NONE) { + session_start(); + } $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = $username; session_regenerate_id(true); @@ -274,8 +201,8 @@ function isRemoteTwoFactorEnabled(): bool { / /************************/ body { - background: ; - color: ; + background: ; + color: ; font-family: clear-sans, sans-serif; font-size: .875rem; padding: 0; @@ -359,7 +286,7 @@ function isRemoteTwoFactorEnabled(): bool { width: 500px; margin: 6rem auto; border-radius: 10px; - background: ; + background: ; } #login::after { content: ""; @@ -392,7 +319,7 @@ function isRemoteTwoFactorEnabled(): bool { } #login .error { color: red; - margin-top: -20px; + margin-top: 1rem; } #login .content { padding: 2rem; @@ -451,7 +378,7 @@ function isRemoteTwoFactorEnabled(): bool { /************************/ @media (max-width: 500px) { body { - background: ; + background: ; } [type=email], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], textarea { font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */ @@ -483,7 +410,7 @@ function isRemoteTwoFactorEnabled(): bool {

- +

@@ -497,82 +424,38 @@ function isRemoteTwoFactorEnabled(): bool {
-
- -

- - - - - -

- $error

"; ?> -

- -

- - -
-

-
- -
-

-
- -
- -
- - + +

+ + +

+

+ +

+ +

+
- - - -
- -

- + - - - + + diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time index 3f9a43ff4f..41ec194694 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time @@ -1 +1 @@ -1744137872623 +1745509523866 \ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php index e71f307c16..65eb45cfd9 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php @@ -54,7 +54,6 @@ function annotate($text) {echo "\n\n";} + +if (session_status() == PHP_SESSION_NONE) { + session_start(); + $_SESSION['unraid_login'] = time(); + $_SESSION['unraid_user'] = 'root'; + session_regenerate_id(true); + session_write_close(); + # This situation should only be possible when booting into GUI mode + my_logger("Page accessed without session; created session for root user."); +} ?> lang="" class=""> @@ -54,7 +64,6 @@ function annotate($text) {echo "\n\n";} ++ ++if (session_status() == PHP_SESSION_NONE) { ++ session_start(); ++ $_SESSION['unraid_login'] = time(); ++ $_SESSION['unraid_user'] = 'root'; ++ session_regenerate_id(true); ++ session_write_close(); ++ # This situation should only be possible when booting into GUI mode ++ my_logger("Page accessed without session; created session for root user."); ++} + ?> + + lang="" class=""> + + <?=_var($var,'NAME')?>/<?=_var($myPage,'name')?> +@@ -602,20 +612,11 @@ } function openNotifier() { @@ -24,7 +45,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php } function closeNotifier() { -@@ -749,12 +740,12 @@ +@@ -748,12 +749,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -38,7 +59,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php foreach ($buttons as $button) { annotate($button['file']); // include page specific stylesheets (if existing) -@@ -938,26 +929,18 @@ +@@ -937,26 +938,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -70,7 +91,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1334,7 +1317,8 @@ +@@ -1337,7 +1330,8 @@ } } } diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch index 841c19ffa4..31a743dfcf 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch @@ -58,30 +58,30 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php // Included in login.php // Only start a session to check if they have a cookie that looks like our session - $server_name = strtok($_SERVER['HTTP_HOST'],":"); + $server_name = strtok($_SERVER['HTTP_HOST'], ":"); if (!empty($_COOKIE['unraid_'.md5($server_name)])) { -@@ -202,11 +253,11 @@ - if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}"); +@@ -128,11 +179,11 @@ + } throw new Exception(_('Too many invalid login attempts')); } // Bail if username + password combo doesn't work -- if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password')); -+ if (!verifyUsernamePasswordAndSSO($username, $password)) throw new Exception(_('Invalid username or password')); - - // Bail if we need a token but it's invalid - if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token')); +- if (!verifyUsernamePassword($username, $password)) { ++ if (!verifyUsernamePasswordAndSSO($username, $password)) { + throw new Exception(_('Invalid username or password')); + } // Successful login, start session -@@ -536,10 +587,11 @@ - document.body.textContent = ''; - document.body.appendChild(errorElement); - } - + @unlink($failFile); +@@ -434,10 +485,11 @@ +

+ +

+ + + + + - - From db3dbe103a907822e57db3046e48e30cca0fea03 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Thu, 24 Apr 2025 16:42:13 -0400 Subject: [PATCH 3/8] fix: brute force valid session in DefaultPageLayout --- ...efaultPageLayout.php.modified.snapshot.php | 10 ++++++++-- .../default-page-layout.modification.ts | 12 ++++++++--- .../patches/default-page-layout.patch | 20 ++++++++++++------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php index 3fa99f777c..4511314d0e 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php @@ -32,11 +32,17 @@ function annotate($text) {echo "\n\n";} -if (session_status() == PHP_SESSION_NONE) { +function is_localhost() { + $server_name = strtok($_SERVER['HTTP_HOST'], ":"); + return $server_name == 'localhost' || $server_name == '127.0.0.1'; +} +function is_good_session() { + return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); +} +if (is_localhost() && !is_good_session()) { session_start(); $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = 'root'; - session_regenerate_id(true); session_write_close(); # This situation should only be possible when booting into GUI mode my_logger("Page accessed without session; created session for root user."); diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts index 57f68f67fb..48a24b9dbc 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts @@ -34,7 +34,7 @@ export default class DefaultPageLayoutModification extends FileModification { private prependDoctypeWithPhp(source: string, phpToAdd: string): string { // The regex to find the target string `?>\s*` at the beginning of a line - const targetRegex = /^\?>\s*/m; + const targetRegex = /^\s*\?>\s*/m; // Prepend the phpToAdd before the matched string return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`); @@ -44,11 +44,17 @@ export default class DefaultPageLayoutModification extends FileModification { // prettier-ignore const newPhpCode = ` -if (session_status() == PHP_SESSION_NONE) { +function is_localhost() { + $server_name = strtok($_SERVER['HTTP_HOST'], ":"); + return $server_name == 'localhost' || $server_name == '127.0.0.1'; +} +function is_good_session() { + return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); +} +if (is_localhost() && !is_good_session()) { session_start(); $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = 'root'; - session_regenerate_id(true); session_write_close(); # This situation should only be possible when booting into GUI mode my_logger("Page accessed without session; created session for root user."); diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch index 87dc5812d9..557fef0034 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch @@ -2,18 +2,24 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php =================================================================== --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php original +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php modified -@@ -29,10 +29,20 @@ +@@ -29,10 +29,26 @@ // adjust the text color in docker log window $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2'; exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &"); function annotate($text) {echo "\n\n";} + -+if (session_status() == PHP_SESSION_NONE) { ++function is_localhost() { ++ $server_name = strtok($_SERVER['HTTP_HOST'], ":"); ++ return $server_name == 'localhost' || $server_name == '127.0.0.1'; ++} ++function is_good_session() { ++ return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); ++} ++if (is_localhost() && !is_good_session()) { + session_start(); + $_SESSION['unraid_login'] = time(); + $_SESSION['unraid_user'] = 'root'; -+ session_regenerate_id(true); + session_write_close(); + # This situation should only be possible when booting into GUI mode + my_logger("Page accessed without session; created session for root user."); @@ -23,7 +29,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php lang="" class=""> <?=_var($var,'NAME')?>/<?=_var($myPage,'name')?> -@@ -602,20 +612,11 @@ +@@ -602,20 +618,11 @@ } function openNotifier() { @@ -45,7 +51,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php } function closeNotifier() { -@@ -748,12 +749,12 @@ +@@ -748,12 +755,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -59,7 +65,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php foreach ($buttons as $button) { annotate($button['file']); // include page specific stylesheets (if existing) -@@ -937,26 +938,18 @@ +@@ -937,26 +944,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -91,7 +97,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1337,7 +1330,8 @@ +@@ -1337,7 +1336,8 @@ } } } From a9f0ff6b9fba47634c2a700cbefb03648f9a3ff6 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Fri, 25 Apr 2025 11:51:19 -0400 Subject: [PATCH 4/8] don't add session patch if it already exists in DefaultPageLayout --- .../modifications/default-page-layout.modification.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts index 48a24b9dbc..f4aa640568 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts @@ -41,6 +41,9 @@ export default class DefaultPageLayoutModification extends FileModification { } private patchGuiBootAuth(source: string): string { + if (source.includes('if (is_localhost() && !is_good_session())')) { + return source; + } // prettier-ignore const newPhpCode = ` From a65e0f5836e608d7024400fc25d2f3af76981140 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 30 Apr 2025 21:59:10 -0400 Subject: [PATCH 5/8] chore: load GUI mode based on slim --- api/dev/Unraid.net/myservers.cfg | 2 +- api/dev/states/myservers.cfg | 2 +- api/src/core/utils/validation/is-gui-mode.ts | 21 +++++++++++ ...efaultPageLayout.php.modified.snapshot.php | 16 --------- .../default-page-layout.modification.ts | 28 ++++++++++++--- .../patches/default-page-layout.patch | 35 +++---------------- 6 files changed, 51 insertions(+), 53 deletions(-) create mode 100644 api/src/core/utils/validation/is-gui-mode.ts diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index 42c67a36b0..1c664dad22 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="4.6.6" +version="4.4.1" extraOrigins="https://google.com,https://test.com" [local] sandbox="yes" diff --git a/api/dev/states/myservers.cfg b/api/dev/states/myservers.cfg index 42ccb6be12..103160cf35 100644 --- a/api/dev/states/myservers.cfg +++ b/api/dev/states/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="4.6.6" +version="4.7.0" extraOrigins="https://google.com,https://test.com" [local] sandbox="yes" diff --git a/api/src/core/utils/validation/is-gui-mode.ts b/api/src/core/utils/validation/is-gui-mode.ts new file mode 100644 index 0000000000..e7165ef6cd --- /dev/null +++ b/api/src/core/utils/validation/is-gui-mode.ts @@ -0,0 +1,21 @@ +import { execa } from 'execa'; + +import { internalLogger } from '@app/core/log.js'; + +/** + * Check if Unraid is in GUI mode by looking for the slim process. + * @returns true if Unraid is in GUI mode, false otherwise. + */ +const isGuiMode = async (): Promise => { + try { + // Use pgrep to check if slim process is running + const { exitCode } = await execa('pgrep', ['slim'], { reject: false }); + // exitCode 0 means process was found, 1 means not found + return exitCode === 0; + } catch (error) { + internalLogger.error('Error checking GUI mode: %s', error); + return false; + } +}; + +export default isGuiMode; diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php index 4511314d0e..50bf7fdc0b 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php @@ -31,22 +31,6 @@ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &"); function annotate($text) {echo "\n\n";} - -function is_localhost() { - $server_name = strtok($_SERVER['HTTP_HOST'], ":"); - return $server_name == 'localhost' || $server_name == '127.0.0.1'; -} -function is_good_session() { - return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); -} -if (is_localhost() && !is_good_session()) { - session_start(); - $_SESSION['unraid_login'] = time(); - $_SESSION['unraid_user'] = 'root'; - session_write_close(); - # This situation should only be possible when booting into GUI mode - my_logger("Page accessed without session; created session for root user."); -} ?> lang="" class=""> diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts index f4aa640568..0294ede920 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'; import { createPatch } from 'diff'; +import isGuiMode from '@app/core/utils/validation/is-gui-mode.js'; import { FileModification, ShouldApplyWithReason, @@ -40,7 +41,12 @@ export default class DefaultPageLayoutModification extends FileModification { return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`); } - private patchGuiBootAuth(source: string): string { + private async patchGuiBootAuth(source: string): Promise { + const isUnraidGuiMode = await isGuiMode(); + if (!isUnraidGuiMode) { + return source; + } + if (source.includes('if (is_localhost() && !is_good_session())')) { return source; } @@ -66,20 +72,34 @@ if (is_localhost() && !is_good_session()) { return this.prependDoctypeWithPhp(source, newPhpCode); } - private applyToSource(fileContent: string): string { + private async applyToSource(fileContent: string): Promise { const transformers = [ this.removeNotificationBell.bind(this), this.replaceToasts.bind(this), this.addToaster.bind(this), this.patchGuiBootAuth.bind(this), ]; - return transformers.reduce((content, fn) => fn(content), fileContent); + + return transformers.reduce(async (contentPromise, transformer) => { + const content = await contentPromise; + return transformer(content); + }, Promise.resolve(fileContent)); + } + + protected async getPregeneratedPatch(): Promise { + const isUnraidGuiMode = await isGuiMode(); + if (!isUnraidGuiMode) { + // If we're not in GUI mode, we can use the pregenerated patch + return super.getPregeneratedPatch(); + } + + return null; } protected async generatePatch(overridePath?: string): Promise { const fileContent = await readFile(this.filePath, 'utf-8'); - const newContent = this.applyToSource(fileContent); + const newContent = await this.applyToSource(fileContent); return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); } diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch index 557fef0034..e5330ec0e2 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch @@ -2,34 +2,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php =================================================================== --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php original +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php modified -@@ -29,10 +29,26 @@ - // adjust the text color in docker log window - $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2'; - exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &"); - - function annotate($text) {echo "\n\n";} -+ -+function is_localhost() { -+ $server_name = strtok($_SERVER['HTTP_HOST'], ":"); -+ return $server_name == 'localhost' || $server_name == '127.0.0.1'; -+} -+function is_good_session() { -+ return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); -+} -+if (is_localhost() && !is_good_session()) { -+ session_start(); -+ $_SESSION['unraid_login'] = time(); -+ $_SESSION['unraid_user'] = 'root'; -+ session_write_close(); -+ # This situation should only be possible when booting into GUI mode -+ my_logger("Page accessed without session; created session for root user."); -+} - ?> - - lang="" class=""> - - <?=_var($var,'NAME')?>/<?=_var($myPage,'name')?> -@@ -602,20 +618,11 @@ +@@ -602,20 +602,11 @@ } function openNotifier() { @@ -51,7 +24,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php } function closeNotifier() { -@@ -748,12 +755,12 @@ +@@ -748,12 +739,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -65,7 +38,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php foreach ($buttons as $button) { annotate($button['file']); // include page specific stylesheets (if existing) -@@ -937,26 +944,18 @@ +@@ -937,26 +928,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -97,7 +70,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1337,7 +1336,8 @@ +@@ -1337,7 +1320,8 @@ } } } From 2f9b577af7ca89120210e1cb60961b42b33a632e Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 30 Apr 2025 22:15:52 -0400 Subject: [PATCH 6/8] chore: cleanup GUI gui mode checks (unneeded) --- .../default-page-layout.modification.ts | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts index 0294ede920..c551a825f4 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts @@ -42,11 +42,6 @@ export default class DefaultPageLayoutModification extends FileModification { } private async patchGuiBootAuth(source: string): Promise { - const isUnraidGuiMode = await isGuiMode(); - if (!isUnraidGuiMode) { - return source; - } - if (source.includes('if (is_localhost() && !is_good_session())')) { return source; } @@ -54,19 +49,20 @@ export default class DefaultPageLayoutModification extends FileModification { const newPhpCode = ` function is_localhost() { - $server_name = strtok($_SERVER['HTTP_HOST'], ":"); - return $server_name == 'localhost' || $server_name == '127.0.0.1'; + // Use the peer IP, not the Host header which can be spoofed + return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1'; } function is_good_session() { return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); } if (is_localhost() && !is_good_session()) { - session_start(); + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = 'root'; session_write_close(); - # This situation should only be possible when booting into GUI mode - my_logger("Page accessed without session; created session for root user."); + my_logger("Unraid GUI-boot: created root session for localhost request."); }`; // Add the PHP code before the DOCTYPE declaration return this.prependDoctypeWithPhp(source, newPhpCode); @@ -86,16 +82,6 @@ if (is_localhost() && !is_good_session()) { }, Promise.resolve(fileContent)); } - protected async getPregeneratedPatch(): Promise { - const isUnraidGuiMode = await isGuiMode(); - if (!isUnraidGuiMode) { - // If we're not in GUI mode, we can use the pregenerated patch - return super.getPregeneratedPatch(); - } - - return null; - } - protected async generatePatch(overridePath?: string): Promise { const fileContent = await readFile(this.filePath, 'utf-8'); From 753c6ed645dd530e8035a553fddbf27468bba962 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 30 Apr 2025 22:24:11 -0400 Subject: [PATCH 7/8] fix: unit tests --- ...efaultPageLayout.php.modified.snapshot.php | 17 +++++++++ .../patches/default-page-layout.patch | 36 ++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php index 50bf7fdc0b..c32e4dad55 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php @@ -31,6 +31,23 @@ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &"); function annotate($text) {echo "\n\n";} + +function is_localhost() { + // Use the peer IP, not the Host header which can be spoofed + return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1'; +} +function is_good_session() { + return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); +} +if (is_localhost() && !is_good_session()) { + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + $_SESSION['unraid_login'] = time(); + $_SESSION['unraid_user'] = 'root'; + session_write_close(); + my_logger("Unraid GUI-boot: created root session for localhost request."); +} ?> lang="" class=""> diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch index e5330ec0e2..7c998c27b3 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch @@ -2,7 +2,35 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php =================================================================== --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php original +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php modified -@@ -602,20 +602,11 @@ +@@ -29,10 +29,27 @@ + // adjust the text color in docker log window + $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2'; + exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &"); + + function annotate($text) {echo "\n\n";} ++ ++function is_localhost() { ++ // Use the peer IP, not the Host header which can be spoofed ++ return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1'; ++} ++function is_good_session() { ++ return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); ++} ++if (is_localhost() && !is_good_session()) { ++ if (session_status() !== PHP_SESSION_ACTIVE) { ++ session_start(); ++ } ++ $_SESSION['unraid_login'] = time(); ++ $_SESSION['unraid_user'] = 'root'; ++ session_write_close(); ++ my_logger("Unraid GUI-boot: created root session for localhost request."); ++} + ?> + + lang="" class=""> + + <?=_var($var,'NAME')?>/<?=_var($myPage,'name')?> +@@ -602,20 +619,11 @@ } function openNotifier() { @@ -24,7 +52,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php } function closeNotifier() { -@@ -748,12 +739,12 @@ +@@ -748,12 +756,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -38,7 +66,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php foreach ($buttons as $button) { annotate($button['file']); // include page specific stylesheets (if existing) -@@ -937,26 +928,18 @@ +@@ -937,26 +945,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -70,7 +98,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1337,7 +1320,8 @@ +@@ -1337,7 +1337,8 @@ } } } From 173c4e29d8d4d1cc4c59cddd7aacda85de62571e Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 30 Apr 2025 23:23:34 -0400 Subject: [PATCH 8/8] chore: always restart the session --- .../DefaultPageLayout.php.modified.snapshot.php | 5 +++-- .../default-page-layout.modification.ts | 5 +++-- .../patches/default-page-layout.patch | 15 ++++++++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php index c32e4dad55..5c6b58b32d 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php @@ -40,9 +40,10 @@ function is_good_session() { return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); } if (is_localhost() && !is_good_session()) { - if (session_status() !== PHP_SESSION_ACTIVE) { - session_start(); + if (session_status() === PHP_SESSION_ACTIVE) { + session_destroy(); } + session_start(); $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = 'root'; session_write_close(); diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts index c551a825f4..a4193c8d91 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts @@ -56,9 +56,10 @@ function is_good_session() { return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); } if (is_localhost() && !is_good_session()) { - if (session_status() !== PHP_SESSION_ACTIVE) { - session_start(); + if (session_status() === PHP_SESSION_ACTIVE) { + session_destroy(); } + session_start(); $_SESSION['unraid_login'] = time(); $_SESSION['unraid_user'] = 'root'; session_write_close(); diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch index 7c998c27b3..1953c61c09 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch @@ -2,7 +2,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php =================================================================== --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php original +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php modified -@@ -29,10 +29,27 @@ +@@ -29,10 +29,28 @@ // adjust the text color in docker log window $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2'; exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &"); @@ -17,9 +17,10 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php + return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']); +} +if (is_localhost() && !is_good_session()) { -+ if (session_status() !== PHP_SESSION_ACTIVE) { -+ session_start(); ++ if (session_status() === PHP_SESSION_ACTIVE) { ++ session_destroy(); + } ++ session_start(); + $_SESSION['unraid_login'] = time(); + $_SESSION['unraid_user'] = 'root'; + session_write_close(); @@ -30,7 +31,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php lang="" class=""> <?=_var($var,'NAME')?>/<?=_var($myPage,'name')?> -@@ -602,20 +619,11 @@ +@@ -602,20 +620,11 @@ } function openNotifier() { @@ -52,7 +53,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php } function closeNotifier() { -@@ -748,12 +756,12 @@ +@@ -748,12 +757,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -66,7 +67,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php foreach ($buttons as $button) { annotate($button['file']); // include page specific stylesheets (if existing) -@@ -937,26 +945,18 @@ +@@ -937,26 +946,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -98,7 +99,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1337,7 +1337,8 @@ +@@ -1337,7 +1338,8 @@ } } }