import type { IncomingMessage, ServerResponse } from "node:http"; import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH } from "../../canvas-host/a2ui.js"; import { safeEqualSecret } from "../../security/secret-equal.js"; import type { AuthRateLimiter } from "../auth-rate-limit.js"; import { authorizeHttpGatewayConnect, isLocalDirectRequest, type GatewayAuthResult, type ResolvedGatewayAuth, } from "../auth.js"; import { CANVAS_CAPABILITY_TTL_MS } from "../canvas-capability.js"; import { authorizeGatewayBearerRequestOrReply } from "../http-auth-helpers.js"; import { getBearerToken } from "../http-utils.js"; import { GATEWAY_CLIENT_MODES, normalizeGatewayClientMode } from "../protocol/client-info.js"; import type { GatewayWsClient } from "./ws-types.js"; export 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 ); } function isNodeWsClient(client: GatewayWsClient): boolean { if (client.connect.role === "node") { return true; } return normalizeGatewayClientMode(client.connect.client.mode) === GATEWAY_CLIENT_MODES.NODE; } function hasAuthorizedNodeWsClientForCanvasCapability( clients: Set, capability: string, ): boolean { const nowMs = Date.now(); for (const client of clients) { if (!isNodeWsClient(client)) { continue; } if (!client.canvasCapability || !client.canvasCapabilityExpiresAtMs) { continue; } if (client.canvasCapabilityExpiresAtMs <= nowMs) { continue; } if (safeEqualSecret(client.canvasCapability, capability)) { // Sliding expiration while the connected node keeps using canvas. client.canvasCapabilityExpiresAtMs = nowMs + CANVAS_CAPABILITY_TTL_MS; return true; } } return false; } export async function authorizeCanvasRequest(params: { req: IncomingMessage; auth: ResolvedGatewayAuth; trustedProxies: string[]; allowRealIpFallback: boolean; clients: Set; canvasCapability?: string; malformedScopedPath?: boolean; rateLimiter?: AuthRateLimiter; }): Promise { const { req, auth, trustedProxies, allowRealIpFallback, clients, canvasCapability, malformedScopedPath, rateLimiter, } = params; if (malformedScopedPath) { return { ok: false, reason: "unauthorized" }; } if (isLocalDirectRequest(req, trustedProxies, allowRealIpFallback)) { return { ok: true }; } let lastAuthFailure: GatewayAuthResult | null = null; const token = getBearerToken(req); if (token) { const authResult = await authorizeHttpGatewayConnect({ auth: { ...auth, allowTailscale: false }, connectAuth: { token, password: token }, req, trustedProxies, allowRealIpFallback, rateLimiter, }); if (authResult.ok) { return authResult; } lastAuthFailure = authResult; } if (canvasCapability && hasAuthorizedNodeWsClientForCanvasCapability(clients, canvasCapability)) { return { ok: true }; } return lastAuthFailure ?? { ok: false, reason: "unauthorized" }; } export async function enforcePluginRouteGatewayAuth(params: { req: IncomingMessage; res: ServerResponse; auth: ResolvedGatewayAuth; trustedProxies: string[]; allowRealIpFallback: boolean; rateLimiter?: AuthRateLimiter; }): Promise { return await authorizeGatewayBearerRequestOrReply(params); }