Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,49 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;
}
$safePassword = escapeshellarg($password);

$output = array();
exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword 2>&1", $output, $code);
$payload = json_encode(["token" => $password]);
$response = false;
$code = 0;

if (function_exists("curl_init")) {
$ch = curl_init("http://127.0.0.1/auth/sso/validate");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
} else {
$context = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Content-Type: application/json
",
"content" => $payload,
"timeout" => 5,
],
]);
$response = @file_get_contents("http://127.0.0.1/auth/sso/validate", false, $context);
if (isset($http_response_header[0])) {
$code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', $http_response_header[0]);
}
Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Snapshot contains incorrect preg_replace replacement string.

Same issue as the patch file - uses '' instead of '$1'. Regenerate this snapshot after fixing the source.

🤖 Prompt for AI Agents
In
`@api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php`
around lines 45 - 47, The snapshot shows preg_replace using an empty replacement
which loses the captured status code; update the source where $code is set (the
preg_replace call that reads $http_response_header[0]) to use the captured group
('$1') as the replacement so the HTTP status code is extracted correctly, then
regenerate the .login.php.modified.snapshot.php snapshot to reflect the
corrected output.

}
my_logger("SSO Login Attempt Code: $code");
my_logger("SSO Login Attempt Response: " . print_r($output, true));
my_logger("SSO Login Attempt Response: " . print_r($response, true));

if ($code !== 0) {
if ($code !== 200) {
return false;
}

if (empty($output)) {
if (empty($response)) {
return false;
}

try {
// Split on first { and take everything after it
$jsonParts = explode('{', $output[0], 2);
if (count($jsonParts) < 2) {
my_logger("SSO Login Attempt Failed: No JSON found in response");
return false;
}
$response = json_decode('{' . $jsonParts[1], true);
if (isset($response['valid']) && $response['valid'] === true) {
$decoded = json_decode($response, true);
if (isset($decoded['valid']) && $decoded['valid'] === true) {
return true;
}
} catch (Exception $e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,17 @@ build_locations(){
include fastcgi_params;
}
#
# SSO endpoints (public)
location /auth/sso {
allow all;
proxy_pass http://unix:/var/run/unraid-core.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
#
# Redirect to login page on failed authentication (401)
#
error_page 401 @401;
Expand Down Expand Up @@ -417,10 +428,31 @@ build_locations(){
#
# my servers proxy
#
location /graphql/api/auth/oidc {
allow all;
proxy_pass http://unix:/var/run/unraid-core.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /graphql/api {
allow all;
proxy_pass http://unix:/var/run/unraid-api.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /graphql {
allow all;
error_log /dev/null crit;
proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
if ($http_upgrade = "websocket") {
rewrite ^/graphql$ /graphql/socket break;
}
proxy_pass http://unix:/var/run/unraid-core.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,66 @@ Index: /etc/rc.d/rc.nginx
T=' '
if check && [[ $1 == lo ]]; then
if [[ $IPV4 == yes ]]; then
@@ -566,11 +584,11 @@
@@ -363,10 +381,21 @@
allow all;
try_files /login.php =404;
include fastcgi_params;
}
#
+ # SSO endpoints (public)
+ location /auth/sso {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+ #
# Redirect to login page on failed authentication (401)
#
error_page 401 @401;
location @401 {
return 302 $scheme://$http_host/login;
@@ -397,14 +426,35 @@
nchan_stub_status;
}
#
# my servers proxy
#
+ location /graphql/api/auth/oidc {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+ location /graphql/api {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-api.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
location /graphql {
allow all;
error_log /dev/null crit;
- proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
+ if ($http_upgrade = "websocket") {
+ rewrite ^/graphql$ /graphql/socket break;
+ }
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
@@ -566,11 +616,11 @@
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
Expand All @@ -57,7 +116,7 @@ Index: /etc/rc.d/rc.nginx
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
@@ -660,14 +678,14 @@
@@ -660,14 +710,14 @@
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
===================================================================
--- /usr/local/emhttp/plugins/dynamix/include/.login.php original
+++ /usr/local/emhttp/plugins/dynamix/include/.login.php modified
@@ -1,6 +1,57 @@
@@ -1,6 +1,76 @@
<?php
+
+
Expand All @@ -22,30 +22,49 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
+ my_logger("SSO Login Attempt Failed: Invalid token format");
+ return false;
+ }
+ $safePassword = escapeshellarg($password);
+ $payload = json_encode(["token" => $password]);
+ $response = false;
+ $code = 0;
+
+ $output = array();
+ exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword 2>&1", $output, $code);
+ if (function_exists("curl_init")) {
+ $ch = curl_init("http://127.0.0.1/auth/sso/validate");
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+ $response = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+ curl_close($ch);
+ } else {
+ $context = stream_context_create([
+ "http" => [
+ "method" => "POST",
+ "header" => "Content-Type: application/json
+",
+ "content" => $payload,
+ "timeout" => 5,
+ ],
+ ]);
+ $response = @file_get_contents("http://127.0.0.1/auth/sso/validate", false, $context);
+ if (isset($http_response_header[0])) {
+ $code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', $http_response_header[0]);
+ }
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

HTTP status code extraction returns empty string instead of the status code.

The preg_replace uses '' as the replacement, which removes the entire match instead of extracting the captured group. This will always set $code to 0.

🐛 Proposed fix
             if (isset($http_response_header[0])) {
-                $code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', $http_response_header[0]);
+                $code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '$1', $http_response_header[0]);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ if (isset($http_response_header[0])) {
+ $code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', $http_response_header[0]);
+ }
if (isset($http_response_header[0])) {
$code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '$1', $http_response_header[0]);
}
🤖 Prompt for AI Agents
In `@api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch`
around lines 50 - 52, The HTTP status extraction is wrong because preg_replace
with an empty replacement removes the match and yields an empty string; update
the logic that sets $code (which currently references $http_response_header[0]
and uses preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', ...)) to instead capture
the group — either use preg_replace with '$1' as the replacement or use
preg_match to extract the first capture and cast that to int — so $code becomes
the numeric HTTP status code.

+ }
+ my_logger("SSO Login Attempt Code: $code");
+ my_logger("SSO Login Attempt Response: " . print_r($output, true));
+ my_logger("SSO Login Attempt Response: " . print_r($response, true));
+
+ if ($code !== 0) {
+ if ($code !== 200) {
+ return false;
+ }
+
+ if (empty($output)) {
+ if (empty($response)) {
+ return false;
+ }
+
+ try {
+ // Split on first { and take everything after it
+ $jsonParts = explode('{', $output[0], 2);
+ if (count($jsonParts) < 2) {
+ my_logger("SSO Login Attempt Failed: No JSON found in response");
+ return false;
+ }
+ $response = json_decode('{' . $jsonParts[1], true);
+ if (isset($response['valid']) && $response['valid'] === true) {
+ $decoded = json_decode($response, true);
+ if (isset($decoded['valid']) && $decoded['valid'] === true) {
+ return true;
+ }
+ } catch (Exception $e) {
Expand All @@ -60,7 +79,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
// Only start a session to check if they have a cookie that looks like our session
$server_name = strtok($_SERVER['HTTP_HOST'], ":");
if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
@@ -128,11 +179,11 @@
@@ -128,11 +198,11 @@
}
throw new Exception(_('Too many invalid login attempts'));
}
Expand All @@ -73,7 +92,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php

// Successful login, start session
@unlink($failFile);
@@ -434,10 +485,11 @@
@@ -434,10 +504,11 @@
</p>
<?php if ($error) { ?>
<p class="error"><?= $error ?></p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';

/**
* Patch rc.nginx on < Unraid 7.2.0 to read the updated connect & api config files
* Patch rc.nginx to read the updated connect & api config files.
*
* Backport of https://github.com/unraid/webgui/pull/2269
* Backport of https://github.com/unraid/webgui/pull/2269. This modification
* runs on all versions but uses idempotent guards to avoid double-injection
* when the base OS already includes the changes.
*/
export default class RcNginxModification extends FileModification {
public filePath: string = '/etc/rc.d/rc.nginx' as const;
Expand All @@ -29,9 +31,9 @@ export default class RcNginxModification extends FileModification {
throw new Error(`File ${this.filePath} not found.`);
}
const fileContent = await readFile(this.filePath, 'utf8');
if (!fileContent.includes('MYSERVERS=')) {
throw new Error(`MYSERVERS not found in the file; incorrect target?`);
}
// if (!fileContent.includes('MYSERVERS=')) {
// throw new Error(`MYSERVERS not found in the file; incorrect target?`);
// }

let newContent = fileContent.replace(
'MYSERVERS="/boot/config/plugins/dynamix.my.servers/myservers.cfg"',
Expand Down Expand Up @@ -68,6 +70,27 @@ check_remote_access(){
`if [[ -L /usr/local/sbin/unraid-api ]] && check_remote_access; then`
);

newContent = newContent.replace(
'proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;',
'if ($http_upgrade = "websocket") {\n\t rewrite ^/graphql$ /graphql/socket break;\n\t }\n\t proxy_pass http://unix:/var/run/unraid-core.sock:;'
);

if (!newContent.includes('location /auth/sso')) {
newContent = newContent.replace(
'\t# Redirect to login page on failed authentication (401)\n',
// prettier-ignore
`\t# SSO endpoints (public)\n\tlocation /auth/sso {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-core.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\t#\n\t# Redirect to login page on failed authentication (401)\n`
);
}

if (!newContent.includes('location /graphql/api/auth/oidc')) {
newContent = newContent.replace(
'\t# my servers proxy\n\t#\n\tlocation /graphql {',
// prettier-ignore
`\t# my servers proxy\n\t#\n\tlocation /graphql/api/auth/oidc {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-core.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\tlocation /graphql/api {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-api.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\tlocation /graphql {`
);
}

newContent = newContent.replace(
'for NET in ${!NET_FQDN6[@]}; do',
'for NET in "${!NET_FQDN6[@]}"; do'
Expand All @@ -91,7 +114,7 @@ check_remote_access(){
}

async shouldApply(): Promise<ShouldApplyWithReason> {
const { shouldApply, reason } = await super.shouldApply();
const { shouldApply, reason } = await super.shouldApply({ checkOsVersion: false });
return {
Comment on lines 116 to 118

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip rc.nginx patching when no diff exists

By forcing shouldApply to ignore the OS version, this modification now runs on systems where rc.nginx already contains all of these changes (e.g., Unraid 7.2+). In that case every replace in generatePatch is a no-op and createPatchWithDiff returns a patch with zero hunks, which FileModification.apply() treats as invalid and throws. The result is a hard failure during install/upgrade on up‑to‑date systems. Consider keeping the version gate or explicitly short‑circuiting when newContent === fileContent before returning a patch.

Useful? React with 👍 / 👎.

shouldApply,
reason,
Expand Down
Loading
Loading