diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index 3b7734dbfe..39548108df 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -10,9 +10,15 @@ import { createServer as createHttpsServer } from "node:https"; import type { CanvasHostHandler } from "../canvas-host/server.js"; import type { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveAgentAvatar } from "../agents/identity-avatar.js"; -import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js"; +import { + A2UI_PATH, + CANVAS_HOST_PATH, + CANVAS_WS_PATH, + handleA2uiHttpRequest, +} from "../canvas-host/a2ui.js"; import { loadConfig } from "../config/config.js"; import { handleSlackHttpRequest } from "../slack/http/index.js"; +import { authorizeGatewayConnect } from "./auth.js"; import { handleControlUiAvatarRequest, handleControlUiHttpRequest, @@ -31,6 +37,8 @@ import { resolveHookChannel, resolveHookDeliver, } from "./hooks.js"; +import { sendUnauthorized } from "./http-common.js"; +import { getBearerToken } from "./http-utils.js"; import { handleOpenAiHttpRequest } from "./openai-http.js"; import { handleOpenResponsesHttpRequest } from "./openresponses-http.js"; import { handleToolsInvokeHttpRequest } from "./tools-invoke-http.js"; @@ -60,6 +68,16 @@ function sendJson(res: ServerResponse, status: number, body: unknown) { res.end(JSON.stringify(body)); } +function isCanvasPath(pathname: string): boolean { + return ( + pathname === A2UI_PATH || + pathname.startsWith(`${A2UI_PATH}/`) || + pathname === CANVAS_HOST_PATH || + pathname.startsWith(`${CANVAS_HOST_PATH}/`) || + pathname === CANVAS_WS_PATH + ); +} + export type HooksRequestHandler = (req: IncomingMessage, res: ServerResponse) => Promise; export function createHooksRequestHandler( @@ -287,6 +305,20 @@ export function createGatewayHttpServer(opts: { } } if (canvasHost) { + const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); + if (isCanvasPath(url.pathname)) { + const token = getBearerToken(req); + const authResult = await authorizeGatewayConnect({ + auth: resolvedAuth, + connectAuth: token ? { token, password: token } : null, + req, + trustedProxies, + }); + if (!authResult.ok) { + sendUnauthorized(res); + return; + } + } if (await handleA2uiHttpRequest(req, res)) { return; } @@ -331,14 +363,41 @@ export function attachGatewayUpgradeHandler(opts: { httpServer: HttpServer; wss: WebSocketServer; canvasHost: CanvasHostHandler | null; + resolvedAuth: import("./auth.js").ResolvedGatewayAuth; }) { - const { httpServer, wss, canvasHost } = opts; + const { httpServer, wss, canvasHost, resolvedAuth } = opts; httpServer.on("upgrade", (req, socket, head) => { - if (canvasHost?.handleUpgrade(req, socket, head)) { - return; - } - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); + void (async () => { + if (canvasHost) { + const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); + if (url.pathname === CANVAS_WS_PATH) { + const configSnapshot = loadConfig(); + const token = getBearerToken(req); + const authResult = await authorizeGatewayConnect({ + auth: resolvedAuth, + connectAuth: token ? { token, password: token } : null, + req, + trustedProxies: configSnapshot.gateway?.trustedProxies ?? [], + }); + if (!authResult.ok) { + socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n"); + socket.destroy(); + return; + } + } + } + if (canvasHost?.handleUpgrade(req, socket, head)) { + return; + } + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + })().catch(() => { + try { + socket.destroy(); + } catch { + // ignore + } }); }); } diff --git a/src/gateway/server-runtime-state.ts b/src/gateway/server-runtime-state.ts index dc8a2e6bfc..f0282af505 100644 --- a/src/gateway/server-runtime-state.ts +++ b/src/gateway/server-runtime-state.ts @@ -164,7 +164,12 @@ export async function createGatewayRuntimeState(params: { maxPayload: MAX_PAYLOAD_BYTES, }); for (const server of httpServers) { - attachGatewayUpgradeHandler({ httpServer: server, wss, canvasHost }); + attachGatewayUpgradeHandler({ + httpServer: server, + wss, + canvasHost, + resolvedAuth: params.resolvedAuth, + }); } const clients = new Set();