| 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<GatewayWsClient>, |
| 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)) { |
| |
| 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<GatewayWsClient>; |
| canvasCapability?: string; |
| malformedScopedPath?: boolean; |
| rateLimiter?: AuthRateLimiter; |
| }): Promise<GatewayAuthResult> { |
| 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<boolean> { |
| return await authorizeGatewayBearerRequestOrReply(params); |
| } |
|
|