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__/__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";} + +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_destroy(); + } + 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=""> @@ -54,7 +72,6 @@ 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_destroy(); ++ } ++ 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 +620,11 @@ } function openNotifier() { @@ -24,7 +53,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php } function closeNotifier() { -@@ -749,12 +740,12 @@ +@@ -748,12 +757,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -38,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) -@@ -938,26 +929,18 @@ +@@ -937,26 +946,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -70,7 +99,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1334,7 +1317,8 @@ +@@ -1337,7 +1338,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 @@ +

+ +

+ + + + + - - 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",