diff --git a/src/gateway/server/ws-connection/auth-messages.ts b/src/gateway/server/ws-connection/auth-messages.ts new file mode 100644 index 00000000000..12b167fb2c8 --- /dev/null +++ b/src/gateway/server/ws-connection/auth-messages.ts @@ -0,0 +1,79 @@ +import type { ResolvedGatewayAuth } from "../../auth.js"; +import { isGatewayCliClient, isWebchatClient } from "../../../utils/message-channel.js"; +import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js"; + +export type AuthProvidedKind = "token" | "password" | "none"; + +export function resolveHostName(hostHeader?: string): string { + const host = (hostHeader ?? "").trim().toLowerCase(); + if (!host) { + return ""; + } + if (host.startsWith("[")) { + const end = host.indexOf("]"); + if (end !== -1) { + return host.slice(1, end); + } + } + const [name] = host.split(":"); + return name ?? ""; +} + +export function formatGatewayAuthFailureMessage(params: { + authMode: ResolvedGatewayAuth["mode"]; + authProvided: AuthProvidedKind; + reason?: string; + client?: { id?: string | null; mode?: string | null }; +}): string { + const { authMode, authProvided, reason, client } = params; + const isCli = isGatewayCliClient(client); + const isControlUi = client?.id === GATEWAY_CLIENT_IDS.CONTROL_UI; + const isWebchat = isWebchatClient(client); + const uiHint = "open the dashboard URL and paste the token in Control UI settings"; + const tokenHint = isCli + ? "set gateway.remote.token to match gateway.auth.token" + : isControlUi || isWebchat + ? uiHint + : "provide gateway auth token"; + const passwordHint = isCli + ? "set gateway.remote.password to match gateway.auth.password" + : isControlUi || isWebchat + ? "enter the password in Control UI settings" + : "provide gateway auth password"; + switch (reason) { + case "token_missing": + return `unauthorized: gateway token missing (${tokenHint})`; + case "token_mismatch": + return `unauthorized: gateway token mismatch (${tokenHint})`; + case "token_missing_config": + return "unauthorized: gateway token not configured on gateway (set gateway.auth.token)"; + case "password_missing": + return `unauthorized: gateway password missing (${passwordHint})`; + case "password_mismatch": + return `unauthorized: gateway password mismatch (${passwordHint})`; + case "password_missing_config": + return "unauthorized: gateway password not configured on gateway (set gateway.auth.password)"; + case "tailscale_user_missing": + return "unauthorized: tailscale identity missing (use Tailscale Serve auth or gateway token/password)"; + case "tailscale_proxy_missing": + return "unauthorized: tailscale proxy headers missing (use Tailscale Serve or gateway token/password)"; + case "tailscale_whois_failed": + return "unauthorized: tailscale identity check failed (use Tailscale Serve auth or gateway token/password)"; + case "tailscale_user_mismatch": + return "unauthorized: tailscale identity mismatch (use Tailscale Serve auth or gateway token/password)"; + case "rate_limited": + return "unauthorized: too many failed authentication attempts (retry later)"; + case "device_token_mismatch": + return "unauthorized: device token mismatch (rotate/reissue device token)"; + default: + break; + } + + if (authMode === "token" && authProvided === "none") { + return `unauthorized: gateway token missing (${tokenHint})`; + } + if (authMode === "password" && authProvided === "none") { + return `unauthorized: gateway password missing (${passwordHint})`; + } + return "unauthorized"; +} diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index b17d71de5e3..ad67ad2acc6 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -58,87 +58,16 @@ import { incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "../health-state.js"; +import { + formatGatewayAuthFailureMessage, + resolveHostName, + type AuthProvidedKind, +} from "./auth-messages.js"; type SubsystemLogger = ReturnType; const DEVICE_SIGNATURE_SKEW_MS = 10 * 60 * 1000; -function resolveHostName(hostHeader?: string): string { - const host = (hostHeader ?? "").trim().toLowerCase(); - if (!host) { - return ""; - } - if (host.startsWith("[")) { - const end = host.indexOf("]"); - if (end !== -1) { - return host.slice(1, end); - } - } - const [name] = host.split(":"); - return name ?? ""; -} - -type AuthProvidedKind = "token" | "password" | "none"; - -function formatGatewayAuthFailureMessage(params: { - authMode: ResolvedGatewayAuth["mode"]; - authProvided: AuthProvidedKind; - reason?: string; - client?: { id?: string | null; mode?: string | null }; -}): string { - const { authMode, authProvided, reason, client } = params; - const isCli = isGatewayCliClient(client); - const isControlUi = client?.id === GATEWAY_CLIENT_IDS.CONTROL_UI; - const isWebchat = isWebchatClient(client); - const uiHint = "open the dashboard URL and paste the token in Control UI settings"; - const tokenHint = isCli - ? "set gateway.remote.token to match gateway.auth.token" - : isControlUi || isWebchat - ? uiHint - : "provide gateway auth token"; - const passwordHint = isCli - ? "set gateway.remote.password to match gateway.auth.password" - : isControlUi || isWebchat - ? "enter the password in Control UI settings" - : "provide gateway auth password"; - switch (reason) { - case "token_missing": - return `unauthorized: gateway token missing (${tokenHint})`; - case "token_mismatch": - return `unauthorized: gateway token mismatch (${tokenHint})`; - case "token_missing_config": - return "unauthorized: gateway token not configured on gateway (set gateway.auth.token)"; - case "password_missing": - return `unauthorized: gateway password missing (${passwordHint})`; - case "password_mismatch": - return `unauthorized: gateway password mismatch (${passwordHint})`; - case "password_missing_config": - return "unauthorized: gateway password not configured on gateway (set gateway.auth.password)"; - case "tailscale_user_missing": - return "unauthorized: tailscale identity missing (use Tailscale Serve auth or gateway token/password)"; - case "tailscale_proxy_missing": - return "unauthorized: tailscale proxy headers missing (use Tailscale Serve or gateway token/password)"; - case "tailscale_whois_failed": - return "unauthorized: tailscale identity check failed (use Tailscale Serve auth or gateway token/password)"; - case "tailscale_user_mismatch": - return "unauthorized: tailscale identity mismatch (use Tailscale Serve auth or gateway token/password)"; - case "rate_limited": - return "unauthorized: too many failed authentication attempts (retry later)"; - case "device_token_mismatch": - return "unauthorized: device token mismatch (rotate/reissue device token)"; - default: - break; - } - - if (authMode === "token" && authProvided === "none") { - return `unauthorized: gateway token missing (${tokenHint})`; - } - if (authMode === "password" && authProvided === "none") { - return `unauthorized: gateway password missing (${passwordHint})`; - } - return "unauthorized"; -} - export function attachGatewayWsMessageHandler(params: { socket: WebSocket; upgradeReq: IncomingMessage;