diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index c0cd6081..17408f5f 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -100,6 +100,8 @@ http { 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 $proxy_x_forwarded_proto; + proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; } location ~ ^/version(/.*)?$ { diff --git a/docker/nginx.conf b/docker/nginx.conf index 2dddf395..de850e8b 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -89,6 +89,8 @@ http { 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 $proxy_x_forwarded_proto; + proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; } location ~ ^/version(/.*)?$ { diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 63b10212..d16fb942 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -43,6 +43,7 @@ import { generateDeviceFingerprint, } from "../../utils/user-agent-parser.js"; import { loginRateLimiter } from "../../utils/login-rate-limiter.js"; +import { getRequestOriginWithForceHTTPS } from "../../utils/request-origin.js"; const authManager = AuthManager.getInstance(); @@ -798,6 +799,9 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => { */ router.get("/oidc/authorize", async (req, res) => { try { + const origin = getRequestOriginWithForceHTTPS(req); + const backendCallbackUri = `${origin}/users/oidc/callback`; + authLogger.info("OIDC authorize request headers", { protocol: req.protocol, host: req.get("Host"), @@ -807,6 +811,8 @@ router.get("/oidc/authorize", async (req, res) => { "x-forwarded-host": req.get("X-Forwarded-Host"), "x-forwarded-port": req.get("X-Forwarded-Port"), secure: req.secure, + calculatedOrigin: origin, + backendCallbackUri: backendCallbackUri, }); const envConfig = getOIDCConfigFromEnv(); @@ -826,15 +832,6 @@ router.get("/oidc/authorize", async (req, res) => { const state = nanoid(); const nonce = nanoid(); - const protocol = - process.env.OIDC_FORCE_HTTPS === "true" - ? "https" - : req.get("X-Forwarded-Proto") || req.protocol; - - const host = req.get("Host"); - const origin = `${protocol}://${host}`; - const backendCallbackUri = `${origin}/users/oidc/callback`; - const referer = req.get("Referer"); let frontendOrigin; if (referer) { diff --git a/src/backend/ssh/opkssh-auth.ts b/src/backend/ssh/opkssh-auth.ts index cf3f08bc..ed906df9 100644 --- a/src/backend/ssh/opkssh-auth.ts +++ b/src/backend/ssh/opkssh-auth.ts @@ -12,6 +12,7 @@ import { FieldCrypto } from "../utils/field-crypto.js"; import { promises as fs } from "fs"; import path from "path"; import axios from "axios"; +import { getRequestOrigin } from "../utils/request-origin.js"; const AUTH_TIMEOUT = 60 * 1000; @@ -48,43 +49,6 @@ interface OPKSSHAuthSession { const activeAuthSessions = new Map(); const cleanupInProgress = new Set(); -export function getRequestOrigin(req: IncomingMessage): string { - const protoHeader = - req.headers["x-forwarded-proto"] || - ((req.socket as unknown as { encrypted?: boolean }).encrypted - ? "https" - : "http"); - const proto = - typeof protoHeader === "string" - ? protoHeader.split(",")[0].trim() - : String(protoHeader); - - const portHeader = req.headers["x-forwarded-port"]; - const port = - typeof portHeader === "string" - ? portHeader.split(",")[0].trim() - : undefined; - - const hostHeaderRaw = - req.headers["x-forwarded-host"] || req.headers.host || "localhost"; - const hostHeader = - typeof hostHeaderRaw === "string" - ? hostHeaderRaw.split(",")[0].trim() - : String(hostHeaderRaw); - - if (port) { - const hostWithoutPort = hostHeader.split(":")[0]; - const isDefaultPort = - (proto === "http" && port === "80") || - (proto === "https" && port === "443"); - return isDefaultPort - ? `${proto}://${hostWithoutPort}` - : `${proto}://${hostWithoutPort}:${port}`; - } - - return `${proto}://${hostHeader}`; -} - function getOPKConfigPath(): string { const dataDir = process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 6817cf63..c6664a7b 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -1001,6 +1001,28 @@ class PollingManager { } } + refreshAllPolling(): void { + const hostsToRefresh: Array<{ + host: SSHHostWithCredentials; + viewerUserId?: string; + }> = []; + + for (const [hostId, config] of this.pollingConfigs.entries()) { + hostsToRefresh.push({ + host: config.host, + viewerUserId: config.viewerUserId, + }); + } + + for (const hostId of this.pollingConfigs.keys()) { + this.stopPollingForHost(hostId, false); + } + + for (const { host, viewerUserId } of hostsToRefresh) { + this.startPollingForHost(host, { statusOnly: true, viewerUserId }); + } + } + registerViewer(hostId: number, sessionId: string, userId: string): void { if (!this.activeViewers.has(hostId)) { this.activeViewers.set(hostId, new Set()); @@ -1259,9 +1281,8 @@ async function resolveHostCredentials( const isSharedHost = userId !== ownerId; if (isSharedHost) { - const { SharedCredentialManager } = await import( - "../utils/shared-credential-manager.js" - ); + const { SharedCredentialManager } = + await import("../utils/shared-credential-manager.js"); const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCred = await sharedCredManager.getSharedCredentialForUser( host.id as number, @@ -3185,6 +3206,9 @@ app.post("/global-settings", requireAdmin, async (req, res) => { .run(String(metricsInterval)); } + // Refresh all active polling to apply new intervals immediately + pollingManager.refreshAllPolling(); + res.json({ success: true }); } catch (error) { statsLogger.error("Failed to save global settings", { diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 40efeda8..431a27a5 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -808,9 +808,9 @@ wss.on("connection", async (ws: WebSocket, req) => { case "opkssh_start_auth": { const opksshData = data as { hostId: number }; try { - const { startOPKSSHAuth, getRequestOrigin } = await import( - "./opkssh-auth.js" - ); + const { startOPKSSHAuth } = await import("./opkssh-auth.js"); + const { getRequestOrigin } = + await import("../utils/request-origin.js"); const db = getDb(); const hostRow = await db .select() diff --git a/src/backend/utils/request-origin.ts b/src/backend/utils/request-origin.ts new file mode 100644 index 00000000..2c4b7d78 --- /dev/null +++ b/src/backend/utils/request-origin.ts @@ -0,0 +1,56 @@ +import type { Request } from "express"; +import type { IncomingMessage } from "http"; + +export function getRequestOrigin(req: Request | IncomingMessage): string { + let protocol: string; + const protoHeader = req.headers["x-forwarded-proto"]; + + if (protoHeader) { + protocol = + typeof protoHeader === "string" + ? protoHeader.split(",")[0].trim() + : protoHeader[0]; + } else if ("protocol" in req && req.protocol) { + protocol = req.protocol; + } else { + protocol = (req.socket as unknown as { encrypted?: boolean }).encrypted + ? "https" + : "http"; + } + + const portHeader = req.headers["x-forwarded-port"]; + const port = + typeof portHeader === "string" + ? portHeader.split(",")[0].trim() + : undefined; + + const hostHeaderRaw = + req.headers["x-forwarded-host"] || req.headers.host || "localhost"; + const hostHeader = + typeof hostHeaderRaw === "string" + ? hostHeaderRaw.split(",")[0].trim() + : String(hostHeaderRaw); + + if (port) { + const hostWithoutPort = hostHeader.split(":")[0]; + const isDefaultPort = + (protocol === "http" && port === "80") || + (protocol === "https" && port === "443"); + + return isDefaultPort + ? `${protocol}://${hostWithoutPort}` + : `${protocol}://${hostWithoutPort}:${port}`; + } + + return `${protocol}://${hostHeader}`; +} + +export function getRequestOriginWithForceHTTPS( + req: Request | IncomingMessage, +): string { + if (process.env.OIDC_FORCE_HTTPS === "true") { + const origin = getRequestOrigin(req); + return origin.replace(/^http:/, "https:"); + } + return getRequestOrigin(req); +}