diff --git a/CHANGELOG.md b/CHANGELOG.md index 34ae3b34527..3f6cec73315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai - Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis. - Control UI/Talk: stop and clear failed realtime Talk sessions when dismissing runtime error banners, so the next Talk click starts a fresh session instead of only stopping the stale one. Thanks @vincentkoc. - Control UI/Talk: retry from a failed realtime Talk session on the next Talk click instead of requiring a separate stale-session stop click first. Thanks @vincentkoc. +- Canvas host: preserve the Gateway TLS scheme in browser canvas host URLs and startup mount logs, so direct HTTPS gateways do not advertise insecure canvas links. Thanks @vincentkoc. - Google Chat: create an isolated Google auth transport per auth client, so google-auth-library interceptor mutations do not accumulate across webhook verification and access-token clients. Thanks @vincentkoc. - Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc. - Gateway/startup: log the canvas host mount only after the HTTP server has bound, so startup logs no longer report the canvas host as mounted before it can serve requests. diff --git a/src/gateway/server-ws-runtime.ts b/src/gateway/server-ws-runtime.ts index 340ccf3add0..e020d11491c 100644 --- a/src/gateway/server-ws-runtime.ts +++ b/src/gateway/server-ws-runtime.ts @@ -29,6 +29,7 @@ export function attachGatewayWsHandlers(params: GatewayWsRuntimeParams) { port: params.port, gatewayHost: params.gatewayHost, canvasHostEnabled: params.canvasHostEnabled, + canvasHostScheme: params.canvasHostScheme, canvasHostServerPort: params.canvasHostServerPort, resolvedAuth: params.resolvedAuth, getResolvedAuth: params.getResolvedAuth, diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 23972f23e18..8c92fa07008 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1329,6 +1329,7 @@ export async function startGatewayServer( } const { attachGatewayWsHandlers } = await import("./server-ws-runtime.js"); + const canvasHostScheme = gatewayTls.enabled ? "https" : "http"; attachGatewayWsHandlers({ wss, clients, @@ -1336,6 +1337,7 @@ export async function startGatewayServer( port, gatewayHost: bindHost ?? undefined, canvasHostEnabled: Boolean(canvasHost), + canvasHostScheme, canvasHostServerPort, resolvedAuth, getResolvedAuth, @@ -1357,7 +1359,7 @@ export async function startGatewayServer( await startListening(); if (canvasHost?.rootDir) { logCanvas.info( - `canvas host mounted at http://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${canvasHost.rootDir})`, + `canvas host mounted at ${canvasHostScheme}://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${canvasHost.rootDir})`, ); } startupTrace.mark("http.bound"); diff --git a/src/gateway/server/ws-connection.test.ts b/src/gateway/server/ws-connection.test.ts index 3e15cd44e3d..8e61172b8a6 100644 --- a/src/gateway/server/ws-connection.test.ts +++ b/src/gateway/server/ws-connection.test.ts @@ -111,6 +111,63 @@ describe("attachGatewayWsConnectionHandler", () => { ); }); + it("uses the gateway TLS scheme for canvas host URLs", async () => { + const listeners = new Map void>(); + const wss = { + on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { + listeners.set(event, handler); + }), + } as unknown as WebSocketServer; + const socket = Object.assign(new EventEmitter(), { + _socket: { + remoteAddress: "127.0.0.1", + remotePort: 1234, + localAddress: "127.0.0.1", + localPort: 5678, + }, + send: vi.fn(), + close: vi.fn(), + }); + const upgradeReq = { + headers: { host: "gateway.example.com" }, + socket: { localAddress: "127.0.0.1" }, + }; + + attachGatewayWsConnectionHandler({ + wss, + clients: new Set(), + preauthConnectionBudget: { release: vi.fn() } as never, + port: 18789, + canvasHostEnabled: true, + canvasHostScheme: "https", + resolvedAuth: createResolvedAuth("token"), + gatewayMethods: [], + events: [], + refreshHealthSnapshot: vi.fn(async () => ({}) as never), + logGateway: createLogger() as never, + logHealth: createLogger() as never, + logWsControl: createLogger() as never, + extraHandlers: {}, + broadcast: vi.fn(), + buildRequestContext: () => + ({ + unsubscribeAllSessionEvents: vi.fn(), + nodeRegistry: { unregister: vi.fn() }, + nodeUnsubscribeAll: vi.fn(), + }) as never, + }); + + const onConnection = listeners.get("connection"); + expect(onConnection).toBeTypeOf("function"); + onConnection?.(socket, upgradeReq); + await waitForLazyMessageHandler(); + + const passed = attachGatewayWsMessageHandlerMock.mock.calls[0]?.[0] as { + canvasHostUrl?: string; + }; + expect(passed.canvasHostUrl).toBe("https://gateway.example.com:443"); + }); + it("rejects late client registration after a pre-connect socket close", async () => { const listeners = new Map void>(); const wss = { diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index 22d9ebae3be..c36aa488906 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -124,6 +124,7 @@ export type GatewayWsSharedHandlerParams = { port: number; gatewayHost?: string; canvasHostEnabled: boolean; + canvasHostScheme?: "http" | "https"; canvasHostServerPort?: number; resolvedAuth: ResolvedGatewayAuth; getResolvedAuth?: () => ResolvedGatewayAuth; @@ -199,6 +200,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti port, gatewayHost, canvasHostEnabled, + canvasHostScheme, canvasHostServerPort, resolvedAuth, getResolvedAuth = () => resolvedAuth, @@ -253,6 +255,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti requestHost: upgradeReq.headers.host, forwardedProto: upgradeReq.headers["x-forwarded-proto"], localAddress: upgradeReq.socket?.localAddress, + scheme: canvasHostScheme, }); logWs("in", "open", { connId, remoteAddr, remotePort, localAddr, localPort, endpoint });