Download src/gateway/server/plugins-http.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/gateway/server/plugins-http.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/gateway/server/plugins-http.ts
-
curl -L -o plugins-http.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/gateway/server/plugins-http.ts
14.4 kB
| // Plugin HTTP routing dispatches registered plugin routes, upgrades, auth policy, and runtime request scope. | |
| import type { IncomingMessage, ServerResponse } from "node:http"; | |
| import type { Duplex } from "node:stream"; | |
| import { | |
| GATEWAY_CLIENT_IDS, | |
| GATEWAY_CLIENT_MODES, | |
| } from "../../../packages/gateway-protocol/src/client-info.js"; | |
| import { PROTOCOL_VERSION } from "../../../packages/gateway-protocol/src/index.js"; | |
| import type { createSubsystemLogger } from "../../logging/subsystem.js"; | |
| import { runPluginHttpRoute } from "../../plugins/http-route-owner.js"; | |
| import type { PluginHttpRouteRegistration, PluginRegistry } from "../../plugins/registry.js"; | |
| import { withPluginRuntimeGatewayRequestScope } from "../../plugins/runtime/gateway-request-scope.js"; | |
| import { rejectWebSocketUpgrade } from "../../shared/websocket-upgrade-reject.js"; | |
| import { respondControlUiPluginAuthCookieProbe } from "../control-ui-plugin-auth-cookie.js"; | |
| import { finishFailedGatewayHttpResponse } from "../http-common.js"; | |
| import type { AuthorizedGatewayHttpRequest } from "../http-utils.js"; | |
| import type { GatewayRequestContext, GatewayRequestOptions } from "../server-methods/types.js"; | |
| import { | |
| runWithGatewayHttpWorkAdmission, | |
| runWithGatewayUpgradeWorkAdmission, | |
| } from "./http-work-admission.js"; | |
| import { resolvePluginRouteRuntimeOperatorScopes } from "./plugin-route-runtime-scopes.js"; | |
| import { | |
| resolvePluginRoutePathContext, | |
| type PluginRoutePathContext, | |
| } from "./plugins-http/path-context.js"; | |
| import { matchedPluginRoutesRequireGatewayAuth } from "./plugins-http/route-auth.js"; | |
| import { findMatchingPluginHttpRoutes } from "./plugins-http/route-match.js"; | |
| export { | |
| isProtectedPluginRoutePathFromContext, | |
| resolvePluginRoutePathContext, | |
| type PluginRoutePathContext, | |
| } from "./plugins-http/path-context.js"; | |
| export { | |
| findRegisteredPluginHttpRoute, | |
| isRegisteredPluginHttpRoutePath, | |
| } from "./plugins-http/route-match.js"; | |
| export { | |
| isPluginAuthenticatedRoutePath, | |
| shouldEnforceGatewayAuthForPluginPath, | |
| } from "./plugins-http/route-auth.js"; | |
| type SubsystemLogger = ReturnType<typeof createSubsystemLogger>; | |
| type PluginRouteRuntimeScope = Parameters<typeof withPluginRuntimeGatewayRequestScope>[0]; | |
| function resolvePluginRoutePathContextForRequest( | |
| req: IncomingMessage, | |
| providedPathContext: PluginRoutePathContext | undefined, | |
| ): PluginRoutePathContext { | |
| if (providedPathContext) { | |
| return providedPathContext; | |
| } | |
| const url = new URL(req.url ?? "/", "http://localhost"); | |
| return resolvePluginRoutePathContext(url.pathname); | |
| } | |
| function createPluginRouteRuntimeClient( | |
| scopes: readonly string[], | |
| clientIp: string | undefined, | |
| requestAuth?: AuthorizedGatewayHttpRequest, | |
| ): GatewayRequestOptions["client"] { | |
| const authenticatedUserProfile = requestAuth?.authenticatedUserProfile; | |
| const operatorRoleActor = requestAuth?.operatorRoleActor; | |
| return { | |
| connId: `plugin-http:${clientIp ?? "unknown"}`, | |
| ...(clientIp ? { clientIp } : {}), | |
| ...(authenticatedUserProfile ? { authenticatedUserProfile } : {}), | |
| ...(operatorRoleActor ? { internal: { operatorRoleActor } } : {}), | |
| connect: { | |
| minProtocol: PROTOCOL_VERSION, | |
| maxProtocol: PROTOCOL_VERSION, | |
| client: { | |
| id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, | |
| version: "internal", | |
| platform: "node", | |
| mode: GATEWAY_CLIENT_MODES.BACKEND, | |
| }, | |
| role: "operator", | |
| scopes: [...scopes], | |
| }, | |
| }; | |
| } | |
| type PluginRouteRuntimeDispatchContext = { | |
| gatewayRequestAuth?: AuthorizedGatewayHttpRequest; | |
| gatewayRequestOperatorScopes?: readonly string[]; | |
| gatewayRequestClientIp?: string; | |
| }; | |
| function getMissingPluginRouteRuntimeContext( | |
| route: PluginHttpRouteRegistration, | |
| context: PluginRouteRuntimeDispatchContext, | |
| ): "caller auth context" | "caller scope context" | undefined { | |
| if (route.auth !== "gateway") { | |
| return undefined; | |
| } | |
| if (route.gatewayRuntimeScopeSurface === "trusted-operator") { | |
| return context.gatewayRequestAuth ? undefined : "caller auth context"; | |
| } | |
| return context.gatewayRequestOperatorScopes === undefined ? "caller scope context" : undefined; | |
| } | |
| function canRunPluginHttpRouteWithoutAdmission(route: PluginHttpRouteRegistration): boolean { | |
| // The manifest entitlement is plugin-wide; require the route-specific trusted operator | |
| // surface so an ordinary sibling cannot start work after suspension reports ready. | |
| return ( | |
| route.auth === "gateway" && | |
| route.gatewayRuntimeScopeSurface === "trusted-operator" && | |
| route.gatewayMethodDispatchAllowed === true | |
| ); | |
| } | |
| function createPluginRouteRuntimeScope(params: { | |
| registry: PluginRegistry; | |
| route: PluginHttpRouteRegistration; | |
| req: IncomingMessage; | |
| gatewayRequestContext?: GatewayRequestContext; | |
| gatewayRequestAuth?: AuthorizedGatewayHttpRequest; | |
| gatewayRequestOperatorScopes?: readonly string[]; | |
| gatewayRequestClientIp?: string; | |
| }): PluginRouteRuntimeScope { | |
| const runtimeScopes = | |
| params.route.auth !== "gateway" | |
| ? [] | |
| : params.gatewayRequestAuth?.controlUiPluginGrant | |
| ? params.gatewayRequestOperatorScopes! | |
| : params.route.gatewayRuntimeScopeSurface === "trusted-operator" | |
| ? resolvePluginRouteRuntimeOperatorScopes( | |
| params.req, | |
| params.gatewayRequestAuth!, | |
| "trusted-operator", | |
| ) | |
| : params.gatewayRequestOperatorScopes!; | |
| const runtimeClient = createPluginRouteRuntimeClient( | |
| runtimeScopes, | |
| params.gatewayRequestClientIp, | |
| params.route.auth === "gateway" ? params.gatewayRequestAuth : undefined, | |
| ); | |
| return { | |
| pluginRegistry: params.registry, | |
| ...(params.route.auth === "gateway" && params.gatewayRequestAuth?.revalidate | |
| ? { revalidate: params.gatewayRequestAuth.revalidate } | |
| : {}), | |
| ...(params.gatewayRequestContext ? { context: params.gatewayRequestContext } : {}), | |
| client: runtimeClient, | |
| isWebchatConnect: () => false, | |
| ...(params.route.pluginId ? { pluginId: params.route.pluginId } : {}), | |
| ...(params.route.source ? { pluginSource: params.route.source } : {}), | |
| ...(params.route.gatewayMethodDispatchAllowed === true | |
| ? { gatewayMethodDispatchAllowed: true } | |
| : {}), | |
| }; | |
| } | |
| export type PluginRouteDispatchContext = { | |
| gatewayAuthSatisfied?: boolean; | |
| gatewayRequestAuth?: AuthorizedGatewayHttpRequest; | |
| gatewayRequestOperatorScopes?: readonly string[]; | |
| gatewayRequestClientIp?: string; | |
| }; | |
| export type PluginHttpRequestHandler = ( | |
| req: IncomingMessage, | |
| res: ServerResponse, | |
| pathContext?: PluginRoutePathContext, | |
| dispatchContext?: PluginRouteDispatchContext, | |
| ) => Promise<boolean>; | |
| export type PluginHttpUpgradeHandler = ( | |
| req: IncomingMessage, | |
| socket: Duplex, | |
| head: Buffer, | |
| pathContext?: PluginRoutePathContext, | |
| dispatchContext?: PluginRouteDispatchContext, | |
| ) => Promise<boolean>; | |
| export function createGatewayPluginRequestHandler(params: { | |
| registry: PluginRegistry; | |
| getRouteRegistry?: () => PluginRegistry; | |
| log: SubsystemLogger; | |
| getGatewayRequestContext?: () => GatewayRequestContext | undefined; | |
| }): PluginHttpRequestHandler { | |
| const { log } = params; | |
| return async (req, res, providedPathContext, dispatchContext) => { | |
| const registry = params.getRouteRegistry?.() ?? params.registry; | |
| const gatewayRequestContext = params.getGatewayRequestContext?.(); | |
| const routes = registry.httpRoutes ?? []; | |
| if (routes.length === 0) { | |
| return false; | |
| } | |
| const pathContext = resolvePluginRoutePathContextForRequest(req, providedPathContext); | |
| const matchedRoutes = findMatchingPluginHttpRoutes(registry, pathContext); | |
| if (matchedRoutes.length === 0) { | |
| return false; | |
| } | |
| const requiresGatewayAuth = matchedPluginRoutesRequireGatewayAuth(matchedRoutes); | |
| if (requiresGatewayAuth && dispatchContext?.gatewayAuthSatisfied !== true) { | |
| log.warn(`plugin http route blocked without gateway auth (${pathContext.canonicalPath})`); | |
| return false; | |
| } | |
| const firstGatewayRoute = matchedRoutes.find((route) => route.auth === "gateway"); | |
| const presentedGatewayRequestAuth = dispatchContext?.gatewayRequestAuth; | |
| const presentedControlUiPluginGrants = presentedGatewayRequestAuth?.controlUiPluginGrants; | |
| const controlUiPluginGrant = presentedControlUiPluginGrants?.find( | |
| (grant) => grant.pluginId === firstGatewayRoute?.pluginId, | |
| ); | |
| if (presentedControlUiPluginGrants && (!firstGatewayRoute || !controlUiPluginGrant)) { | |
| log.warn( | |
| `plugin http route blocked for mismatched control ui grant (${pathContext.canonicalPath})`, | |
| ); | |
| res.statusCode = 401; | |
| res.setHeader("Content-Type", "text/plain; charset=utf-8"); | |
| res.end("Unauthorized"); | |
| return true; | |
| } | |
| const gatewayRequestAuth = controlUiPluginGrant | |
| ? { | |
| ...presentedGatewayRequestAuth!, | |
| controlUiPluginGrant, | |
| } | |
| : presentedGatewayRequestAuth; | |
| const gatewayRequestOperatorScopes = controlUiPluginGrant | |
| ? controlUiPluginGrant.scopes | |
| : dispatchContext?.gatewayRequestOperatorScopes; | |
| // Fail closed before invoking any handlers when matched gateway routes are | |
| // missing the runtime auth/scope context they require. | |
| for (const route of matchedRoutes) { | |
| if ( | |
| controlUiPluginGrant && | |
| route.auth === "gateway" && | |
| route.pluginId !== controlUiPluginGrant.pluginId | |
| ) { | |
| continue; | |
| } | |
| const missingRuntimeContext = getMissingPluginRouteRuntimeContext(route, { | |
| gatewayRequestAuth, | |
| gatewayRequestOperatorScopes, | |
| }); | |
| if (missingRuntimeContext) { | |
| log.warn( | |
| `plugin http route blocked without ${missingRuntimeContext} (${pathContext.canonicalPath})`, | |
| ); | |
| return false; | |
| } | |
| } | |
| // The probe is intercepted only after route ownership and cookie auth are | |
| // established. Plugin code never sees the reserved capability request. | |
| if (controlUiPluginGrant && respondControlUiPluginAuthCookieProbe(req, res)) { | |
| return true; | |
| } | |
| for (const route of matchedRoutes) { | |
| if ( | |
| controlUiPluginGrant && | |
| route.auth === "gateway" && | |
| route.pluginId !== controlUiPluginGrant.pluginId | |
| ) { | |
| continue; | |
| } | |
| try { | |
| const runRoute = async () => | |
| (await withPluginRuntimeGatewayRequestScope( | |
| createPluginRouteRuntimeScope({ | |
| registry, | |
| route, | |
| req, | |
| gatewayRequestContext, | |
| gatewayRequestAuth, | |
| gatewayRequestOperatorScopes, | |
| gatewayRequestClientIp: dispatchContext?.gatewayRequestClientIp, | |
| }), | |
| async () => | |
| runPluginHttpRoute(registry, route, route.handler, () => route.handler(req, res)), | |
| )) !== false; | |
| // Entitled trusted-operator routes delegate substantive work through Gateway dispatch. | |
| // An outer root would make gateway.suspend.prepare nested and permanently unreachable. | |
| const handled = canRunPluginHttpRouteWithoutAdmission(route) | |
| ? await runRoute() | |
| : await runWithGatewayHttpWorkAdmission(res, runRoute); | |
| if (handled) { | |
| return true; | |
| } | |
| } catch (err) { | |
| log.warn(`plugin http route failed (${route.pluginId ?? "unknown"}): ${String(err)}`); | |
| finishFailedGatewayHttpResponse(res); | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| } | |
| export function createGatewayPluginUpgradeHandler(params: { | |
| registry: PluginRegistry; | |
| getRouteRegistry?: () => PluginRegistry; | |
| log: SubsystemLogger; | |
| getGatewayRequestContext?: () => GatewayRequestContext | undefined; | |
| }): PluginHttpUpgradeHandler { | |
| const { log } = params; | |
| return async (req, socket, head, providedPathContext, dispatchContext) => { | |
| const registry = params.getRouteRegistry?.() ?? params.registry; | |
| const gatewayRequestContext = params.getGatewayRequestContext?.(); | |
| const routes = registry.httpRoutes ?? []; | |
| if (routes.length === 0) { | |
| return false; | |
| } | |
| const pathContext = resolvePluginRoutePathContextForRequest(req, providedPathContext); | |
| const matchedRoutes = findMatchingPluginHttpRoutes(registry, pathContext).filter( | |
| (route) => typeof route.handleUpgrade === "function", | |
| ); | |
| if (matchedRoutes.length === 0) { | |
| return false; | |
| } | |
| const requiresGatewayAuth = matchedPluginRoutesRequireGatewayAuth(matchedRoutes); | |
| if (requiresGatewayAuth && dispatchContext?.gatewayAuthSatisfied !== true) { | |
| log.warn(`plugin http upgrade blocked without gateway auth (${pathContext.canonicalPath})`); | |
| rejectWebSocketUpgrade(socket, { status: 401 }); | |
| return true; | |
| } | |
| const gatewayRequestAuth = dispatchContext?.gatewayRequestAuth; | |
| const gatewayRequestOperatorScopes = dispatchContext?.gatewayRequestOperatorScopes; | |
| for (const route of matchedRoutes) { | |
| const missingRuntimeContext = getMissingPluginRouteRuntimeContext(route, { | |
| gatewayRequestAuth, | |
| gatewayRequestOperatorScopes, | |
| }); | |
| if (missingRuntimeContext) { | |
| log.warn( | |
| `plugin http upgrade blocked without ${missingRuntimeContext} (${pathContext.canonicalPath})`, | |
| ); | |
| rejectWebSocketUpgrade(socket, { status: 401 }); | |
| return true; | |
| } | |
| } | |
| for (const route of matchedRoutes) { | |
| try { | |
| const handled = await runWithGatewayUpgradeWorkAdmission( | |
| socket, | |
| async () => | |
| (await withPluginRuntimeGatewayRequestScope( | |
| createPluginRouteRuntimeScope({ | |
| registry, | |
| route, | |
| req, | |
| gatewayRequestContext, | |
| gatewayRequestAuth, | |
| gatewayRequestOperatorScopes, | |
| gatewayRequestClientIp: dispatchContext?.gatewayRequestClientIp, | |
| }), | |
| async () => { | |
| const handleUpgrade = route.handleUpgrade!; | |
| return runPluginHttpRoute(registry, route, handleUpgrade, () => | |
| handleUpgrade(req, socket, head), | |
| ); | |
| }, | |
| )) !== false, | |
| ); | |
| if (handled) { | |
| return true; | |
| } | |
| } catch (err) { | |
| log.warn(`plugin http upgrade failed (${route.pluginId ?? "unknown"}): ${String(err)}`); | |
| socket.destroy(); | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| } | |