From af50b914a4c2dba1ebf5ee5769d76787f89761fb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 13:30:11 +0100 Subject: [PATCH] refactor(browser): centralize http auth --- src/browser/bridge-server.ts | 64 +---------------------------------- src/browser/http-auth.ts | 63 ++++++++++++++++++++++++++++++++++ src/browser/server.ts | 65 ++---------------------------------- 3 files changed, 66 insertions(+), 126 deletions(-) create mode 100644 src/browser/http-auth.ts diff --git a/src/browser/bridge-server.ts b/src/browser/bridge-server.ts index 4ee73167aef..d513d2319f7 100644 --- a/src/browser/bridge-server.ts +++ b/src/browser/bridge-server.ts @@ -1,12 +1,11 @@ import type { Server } from "node:http"; -import type { IncomingMessage } from "node:http"; import type { AddressInfo } from "node:net"; import express from "express"; import type { ResolvedBrowserConfig } from "./config.js"; import type { BrowserRouteRegistrar } from "./routes/types.js"; import { isLoopbackHost } from "../gateway/net.js"; -import { safeEqualSecret } from "../security/secret-equal.js"; import { deleteBridgeAuthForPort, setBridgeAuthForPort } from "./bridge-auth-registry.js"; +import { isAuthorizedBrowserRequest } from "./http-auth.js"; import { registerBrowserRoutes } from "./routes/index.js"; import { type BrowserServerState, @@ -14,67 +13,6 @@ import { type ProfileContext, } from "./server-context.js"; -function firstHeaderValue(value: string | string[] | undefined): string { - return Array.isArray(value) ? (value[0] ?? "") : (value ?? ""); -} - -function parseBearerToken(authorization: string): string | undefined { - if (!authorization || !authorization.toLowerCase().startsWith("bearer ")) { - return undefined; - } - const token = authorization.slice(7).trim(); - return token || undefined; -} - -function parseBasicPassword(authorization: string): string | undefined { - if (!authorization || !authorization.toLowerCase().startsWith("basic ")) { - return undefined; - } - const encoded = authorization.slice(6).trim(); - if (!encoded) { - return undefined; - } - try { - const decoded = Buffer.from(encoded, "base64").toString("utf8"); - const sep = decoded.indexOf(":"); - if (sep < 0) { - return undefined; - } - const password = decoded.slice(sep + 1).trim(); - return password || undefined; - } catch { - return undefined; - } -} - -function isAuthorizedBrowserRequest( - req: IncomingMessage, - auth: { token?: string; password?: string }, -): boolean { - const authorization = firstHeaderValue(req.headers.authorization).trim(); - - if (auth.token) { - const bearer = parseBearerToken(authorization); - if (bearer && safeEqualSecret(bearer, auth.token)) { - return true; - } - } - - if (auth.password) { - const passwordHeader = firstHeaderValue(req.headers["x-openclaw-password"]).trim(); - if (passwordHeader && safeEqualSecret(passwordHeader, auth.password)) { - return true; - } - - const basicPassword = parseBasicPassword(authorization); - if (basicPassword && safeEqualSecret(basicPassword, auth.password)) { - return true; - } - } - - return false; -} - export type BrowserBridge = { server: Server; port: number; diff --git a/src/browser/http-auth.ts b/src/browser/http-auth.ts new file mode 100644 index 00000000000..df0ab440dea --- /dev/null +++ b/src/browser/http-auth.ts @@ -0,0 +1,63 @@ +import type { IncomingMessage } from "node:http"; +import { safeEqualSecret } from "../security/secret-equal.js"; + +function firstHeaderValue(value: string | string[] | undefined): string { + return Array.isArray(value) ? (value[0] ?? "") : (value ?? ""); +} + +function parseBearerToken(authorization: string): string | undefined { + if (!authorization || !authorization.toLowerCase().startsWith("bearer ")) { + return undefined; + } + const token = authorization.slice(7).trim(); + return token || undefined; +} + +function parseBasicPassword(authorization: string): string | undefined { + if (!authorization || !authorization.toLowerCase().startsWith("basic ")) { + return undefined; + } + const encoded = authorization.slice(6).trim(); + if (!encoded) { + return undefined; + } + try { + const decoded = Buffer.from(encoded, "base64").toString("utf8"); + const sep = decoded.indexOf(":"); + if (sep < 0) { + return undefined; + } + const password = decoded.slice(sep + 1).trim(); + return password || undefined; + } catch { + return undefined; + } +} + +export function isAuthorizedBrowserRequest( + req: IncomingMessage, + auth: { token?: string; password?: string }, +): boolean { + const authorization = firstHeaderValue(req.headers.authorization).trim(); + + if (auth.token) { + const bearer = parseBearerToken(authorization); + if (bearer && safeEqualSecret(bearer, auth.token)) { + return true; + } + } + + if (auth.password) { + const passwordHeader = firstHeaderValue(req.headers["x-openclaw-password"]).trim(); + if (passwordHeader && safeEqualSecret(passwordHeader, auth.password)) { + return true; + } + + const basicPassword = parseBasicPassword(authorization); + if (basicPassword && safeEqualSecret(basicPassword, auth.password)) { + return true; + } + } + + return false; +} diff --git a/src/browser/server.ts b/src/browser/server.ts index 03f084f168d..2392f9c48b8 100644 --- a/src/browser/server.ts +++ b/src/browser/server.ts @@ -1,12 +1,12 @@ -import type { IncomingMessage, Server } from "node:http"; +import type { Server } from "node:http"; import express from "express"; import type { BrowserRouteRegistrar } from "./routes/types.js"; import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { safeEqualSecret } from "../security/secret-equal.js"; import { resolveBrowserConfig, resolveProfile } from "./config.js"; import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./control-auth.js"; import { ensureChromeExtensionRelayServer } from "./extension-relay.js"; +import { isAuthorizedBrowserRequest } from "./http-auth.js"; import { isPwAiLoaded } from "./pw-ai-state.js"; import { registerBrowserRoutes } from "./routes/index.js"; import { @@ -19,67 +19,6 @@ let state: BrowserServerState | null = null; const log = createSubsystemLogger("browser"); const logServer = log.child("server"); -function firstHeaderValue(value: string | string[] | undefined): string { - return Array.isArray(value) ? (value[0] ?? "") : (value ?? ""); -} - -function parseBearerToken(authorization: string): string | undefined { - if (!authorization || !authorization.toLowerCase().startsWith("bearer ")) { - return undefined; - } - const token = authorization.slice(7).trim(); - return token || undefined; -} - -function parseBasicPassword(authorization: string): string | undefined { - if (!authorization || !authorization.toLowerCase().startsWith("basic ")) { - return undefined; - } - const encoded = authorization.slice(6).trim(); - if (!encoded) { - return undefined; - } - try { - const decoded = Buffer.from(encoded, "base64").toString("utf8"); - const sep = decoded.indexOf(":"); - if (sep < 0) { - return undefined; - } - const password = decoded.slice(sep + 1).trim(); - return password || undefined; - } catch { - return undefined; - } -} - -function isAuthorizedBrowserRequest( - req: IncomingMessage, - auth: { token?: string; password?: string }, -): boolean { - const authorization = firstHeaderValue(req.headers.authorization).trim(); - - if (auth.token) { - const bearer = parseBearerToken(authorization); - if (bearer && safeEqualSecret(bearer, auth.token)) { - return true; - } - } - - if (auth.password) { - const passwordHeader = firstHeaderValue(req.headers["x-openclaw-password"]).trim(); - if (passwordHeader && safeEqualSecret(passwordHeader, auth.password)) { - return true; - } - - const basicPassword = parseBasicPassword(authorization); - if (basicPassword && safeEqualSecret(basicPassword, auth.password)) { - return true; - } - } - - return false; -} - export async function startBrowserControlServerFromConfig(): Promise { if (state) { return state;