UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

737 lines (736 loc) • 29 kB
import http from "node:http"; import https from "node:https"; import { AsyncLocalStorage } from "node:async_hooks"; import { X509Certificate } from "node:crypto"; import net from "node:net"; import tls from "node:tls"; // The explicit peer entry avoids Bun's builtin while remaining statically bundleable. import { Agent as UndiciAgent, Dispatcher, FormData as UndiciFormData, Headers as UndiciHeaders, Pool as UndiciPool, Request as UndiciRequest, Response as UndiciResponse, errors as undiciErrors, fetch as undiciFetch, getGlobalDispatcher, ProxyAgent as UndiciProxyAgent, setGlobalDispatcher, } from "undici/index.js"; import { createAmbientProxyResolver, EMPTY_PROXY_ENV, resolveAmbientProxyForUrl, readProxyEnv, } from "./env.js"; import { bindNodeHttpMethod, createDirectNodeAgent, createNodeProxyAgent, } from "./node-http.js"; import { formatUrl, ProxylineError, redactProxyUrl, resolveProxyTlsCa, } from "./shared.js"; import { PROXYLINE_DISPATCHER_BRAND } from "./dispatcher-brand.js"; let activeRuntime; let activeHandle; // Node's global fetch types come from bundled undici-types, while the runtime // implementation intentionally delegates to this package's undici dependency. const proxylineHeaders = UndiciHeaders; const proxylineRequest = UndiciRequest; const proxylineResponse = UndiciResponse; const proxylineFormData = UndiciFormData; function getRequestDispatcher(request) { for (const symbol of Object.getOwnPropertySymbols(request)) { if (symbol.description !== "dispatcher") { continue; } return Reflect.get(request, symbol); } return undefined; } function isFetchRequestLike(value) { if (typeof value !== "object" || value === null) { return false; } const record = value; return (typeof record.url === "string" && typeof record.method === "string" && typeof record.arrayBuffer === "function" && record.headers !== undefined); } async function createProxylineRequestFromRequestLike(request, options) { const init = { headers: request.headers, method: request.method, }; if (request.cache !== undefined) { init.cache = request.cache; } if (request.credentials !== undefined) { init.credentials = request.credentials; } if (request.integrity !== undefined) { init.integrity = request.integrity; } if (request.keepalive !== undefined) { init.keepalive = request.keepalive; } if (request.mode !== undefined) { init.mode = request.mode; } if (request.redirect !== undefined) { init.redirect = request.redirect; } if (request.referrer !== undefined) { init.referrer = request.referrer; } if (request.referrerPolicy !== undefined) { init.referrerPolicy = request.referrerPolicy; } if (options.preserveDispatcher) { const dispatcher = getRequestDispatcher(request); if (dispatcher !== undefined) { Reflect.set(init, "dispatcher", dispatcher); } } if (request.signal !== undefined) { init.signal = request.signal; } if (options.includeBody && request.body !== null && request.method !== "GET" && request.method !== "HEAD") { init.body = request.body; init.duplex = "half"; } const requestUnknown = Reflect.construct(proxylineRequest, [request.url, init]); if (!(requestUnknown instanceof proxylineRequest)) { throw new TypeError("Proxyline failed to normalize a fetch Request."); } return requestUnknown; } function requestInitOverridesBody(init) { if (typeof init !== "object" || init === null) { return false; } return "body" in init; } async function normalizeFetchInput(input, init, options) { if ((input instanceof proxylineRequest && options.preserveDispatcher) || !isFetchRequestLike(input)) { return input; } return await createProxylineRequestFromRequestLike(input, { includeBody: !requestInitOverridesBody(init), preserveDispatcher: options.preserveDispatcher, }); } function withManagedFetchDispatcher(init, dispatcher) { if (init !== undefined && init !== null && typeof init !== "object" && typeof init !== "function") { throw new TypeError(`Request constructor: Expected ${String(init)} to be one of: Null, Undefined, Object.`); } const sanitized = init === undefined || init === null ? {} : Object.create(init); Reflect.defineProperty(sanitized, "dispatcher", { configurable: true, enumerable: true, value: dispatcher, writable: true, }); return sanitized; } const proxylineFetch = async (input, init) => { const managedDispatcher = activeRuntime?.mode === "managed" ? activeRuntime.installedDispatcher : undefined; const normalizedInput = await normalizeFetchInput(input, init, { preserveDispatcher: managedDispatcher === undefined, }); const normalizedInit = managedDispatcher === undefined ? init : withManagedFetchDispatcher(init, managedDispatcher); const response = await Reflect.apply(undiciFetch, undefined, normalizedInit === undefined ? [normalizedInput] : [normalizedInput, normalizedInit]); if (!(response instanceof proxylineResponse)) { throw new TypeError("Proxyline fetch returned a non-Response value."); } return response; }; function normalizeProxyUrl(value) { if (value === undefined) { return undefined; } const url = value instanceof URL ? new URL(value.href) : new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") { throw new ProxylineError("UNSUPPORTED_PROXY_PROTOCOL", `Proxyline only supports http:// and https:// proxy endpoints in this slice: ${url.protocol}`); } return url; } function emit(onEvent, event) { try { onEvent?.(event); } catch { // Observer failures must not fail install or leave patched globals unstoppable. } } function isProxyableUrlProtocol(protocol) { return protocol === "http:" || protocol === "https:" || protocol === "ws:" || protocol === "wss:"; } function shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface) { if (bypasses.has(url, surface)) { return true; } if (bypassPolicy === undefined) { return false; } return bypassPolicy({ surface, url: formatUrl(url) }); } function bypassKey(url, surface) { return `${surface ?? "*"}\n${formatUrl(url)}`; } function createDynamicBypassRegistry() { const counts = new Map(); const scopedBypasses = new AsyncLocalStorage(); const hasScopedBypass = (url, surface) => { const scoped = scopedBypasses.getStore(); return scoped !== undefined && (scoped.has(bypassKey(url, surface)) || scoped.has(bypassKey(url, undefined))); }; return { add: (registration) => { const key = bypassKey(registration.url, registration.surface); counts.set(key, (counts.get(key) ?? 0) + 1); let stopped = false; return () => { if (stopped) { return; } stopped = true; const next = (counts.get(key) ?? 1) - 1; if (next <= 0) { counts.delete(key); } else { counts.set(key, next); } }; }, has: (url, surface) => hasScopedBypass(url, surface) || (counts.get(bypassKey(url, surface)) ?? 0) > 0 || (counts.get(bypassKey(url, undefined)) ?? 0) > 0, runScoped: (registration, run) => { const inherited = scopedBypasses.getStore(); const scoped = new Set(inherited); scoped.add(bypassKey(registration.url, registration.surface)); return scopedBypasses.run(scoped, run); }, }; } function proxyEnvSnapshotKey(env) { return JSON.stringify(env ?? EMPTY_PROXY_ENV); } function createManagedProxyResolver(proxyUrl, bypassPolicy, bypasses) { const redactedProxyUrl = redactProxyUrl(proxyUrl); return { active: true, describeProxy: () => redactedProxyUrl, explain: (url, surface) => { const formattedUrl = formatUrl(url); if (!isProxyableUrlProtocol(new URL(url).protocol)) { return { kind: "direct", reason: "managed-proxy-unsupported-url-scheme", surface, url: formattedUrl, }; } if (shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface)) { return { kind: "direct", reason: "managed-proxy-bypass-policy", surface, url: formattedUrl, }; } return { kind: "proxied", reason: "managed-proxy-active", surface, url: formattedUrl, proxyUrl: redactedProxyUrl, }; }, getProxyForUrl: (url, surface = "unknown") => { const protocol = new URL(url).protocol; return isProxyableUrlProtocol(protocol) && !shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface) ? proxyUrl.href : ""; }, }; } function finiteNonNegativeInteger(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined; } function finitePositiveInteger(value) { return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined; } function resolveUndiciBaseOptions(options) { const bodyTimeout = finiteNonNegativeInteger(options?.bodyTimeout); const headersTimeout = finiteNonNegativeInteger(options?.headersTimeout); return { ...(options?.allowH2 !== undefined ? { allowH2: options.allowH2 } : {}), ...(bodyTimeout !== undefined ? { bodyTimeout } : {}), ...(headersTimeout !== undefined ? { headersTimeout } : {}), ...(options?.connect !== undefined ? { connect: { ...(options.connect.autoSelectFamily !== undefined ? { autoSelectFamily: options.connect.autoSelectFamily } : {}), ...(finitePositiveInteger(options.connect.autoSelectFamilyAttemptTimeout) !== undefined ? { autoSelectFamilyAttemptTimeout: finitePositiveInteger(options.connect.autoSelectFamilyAttemptTimeout), } : {}), }, } : {}), }; } function createUndiciAgent(options) { return new UndiciAgent(resolveUndiciBaseOptions(options)); } function isObjectRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function stripIpServernameFromConnectOptions(options) { if (!isObjectRecord(options) || typeof options.servername !== "string") { return options; } const servername = options.servername.replace(/^\[|\]$/g, ""); if (net.isIP(servername) === 0) { return options; } const next = { ...options }; delete next.servername; return next; } function stripIpServernameFromConnect(connect) { if (typeof connect !== "function") { return connect; } return (options, callback) => connect(stripIpServernameFromConnectOptions(options), callback); } const checkProxyServerIdentity = (hostname, certificate) => { const normalizedHostname = hostname.replace(/^\[|\]$/g, ""); if (net.isIP(normalizedHostname) === 6) { try { // Compare binary addresses so compressed and expanded IPv6 SAN forms remain equivalent. if (new X509Certificate(certificate.raw).checkIP(normalizedHostname) !== undefined) { return undefined; } } catch { // Let Node return its canonical certificate validation error below. } } return tls.checkServerIdentity(normalizedHostname, certificate); }; function createProxyClientFactory() { return (origin, options) => { const clientOptions = isObjectRecord(options) ? { ...options, connect: stripIpServernameFromConnect(options.connect) } : options; return new UndiciPool(origin, clientOptions); }; } function createUndiciProxyAgent(proxyUrl, options) { const proxyHostname = new URL(proxyUrl).hostname.replace(/^\[|\]$/g, ""); const proxyTls = options.proxyCa !== undefined || net.isIP(proxyHostname) === 6 ? { ...(options.proxyCa !== undefined ? { ca: options.proxyCa } : {}), ...(net.isIP(proxyHostname) === 6 ? { checkServerIdentity: checkProxyServerIdentity } : {}), } : undefined; const dispatcherFactory = createProxyClientFactory(); return new UndiciProxyAgent({ ...resolveUndiciBaseOptions(options.undici), uri: proxyUrl, clientFactory: dispatcherFactory, factory: dispatcherFactory, ...(proxyTls !== undefined ? { proxyTls } : {}), }); } function normalizeUndiciDispatchOptions(options) { if (options.origin === undefined || !/^https?:\/\//i.test(options.path)) { return options; } const pathUrl = new URL(options.path); return { ...options, path: `${pathUrl.pathname}${pathUrl.search}`, }; } function createUndiciProxyDispatcher(options, dispatcherOptions) { if (options.mode === "ambient") { if (!options.active) { return createUndiciAgent(dispatcherOptions.undici); } return new AmbientUndiciDispatcher(options.env, dispatcherOptions); } return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions); } function reportClosedDispatchError(handler, error) { const compatibleHandler = handler; if (compatibleHandler.onResponseError !== undefined) { compatibleHandler.onResponseError(null, error); return false; } if (compatibleHandler.onError !== undefined) { compatibleHandler.onError(error); return false; } throw error; } class ManagedUndiciDispatcher extends Dispatcher { [PROXYLINE_DISPATCHER_BRAND] = true; #directDispatcher; #dispatcherOptions; #proxyDispatchers = new Map(); #resolver; #closedError; constructor(resolver, dispatcherOptions) { super(); this.#resolver = resolver; this.#dispatcherOptions = dispatcherOptions; this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici); } dispatch(options, handler) { if (this.#closedError !== undefined) { return reportClosedDispatchError(handler, this.#closedError); } const normalizedOptions = normalizeUndiciDispatchOptions(options); const url = resolveUndiciDispatchUrl(normalizedOptions); const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici"); const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl); return dispatcher.dispatch(normalizedOptions, handler); } close(callback) { const closing = this.#closeAll(); if (callback === undefined) { return closing; } closing.then(callback, callback); } destroy(errorOrCallback, callback) { const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null; const destroyCallback = typeof errorOrCallback === "function" ? errorOrCallback : callback; const destroying = this.#destroyAll(error); if (destroyCallback === undefined) { return destroying; } destroying.then(destroyCallback, destroyCallback); } #proxyDispatcher(proxyUrl) { const existing = this.#proxyDispatchers.get(proxyUrl); if (existing !== undefined) { return existing; } const dispatcher = createUndiciProxyAgent(proxyUrl, this.#dispatcherOptions); this.#proxyDispatchers.set(proxyUrl, dispatcher); return dispatcher; } async #closeAll() { this.#closedError ??= new undiciErrors.ClientClosedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.close(), ...proxyDispatchers.map((dispatcher) => dispatcher.close()), ]); } async #destroyAll(error) { this.#closedError ??= error ?? new undiciErrors.ClientDestroyedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.destroy(error), ...proxyDispatchers.map((dispatcher) => dispatcher.destroy(error)), ]); } } class AmbientUndiciDispatcher extends Dispatcher { [PROXYLINE_DISPATCHER_BRAND] = true; #directDispatcher; #dispatcherOptions; #env; #proxyDispatchers = new Map(); #closedError; constructor(env, dispatcherOptions) { super(); this.#env = env; this.#dispatcherOptions = dispatcherOptions; this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici); } dispatch(options, handler) { if (this.#closedError !== undefined) { return reportClosedDispatchError(handler, this.#closedError); } const normalizedOptions = normalizeUndiciDispatchOptions(options); const url = resolveUndiciDispatchUrl(normalizedOptions); const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env); const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl); return dispatcher.dispatch(normalizedOptions, handler); } close(callback) { const closing = this.#closeAll(); if (callback === undefined) { return closing; } closing.then(callback, callback); } destroy(errorOrCallback, callback) { const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null; const destroyCallback = typeof errorOrCallback === "function" ? errorOrCallback : callback; const destroying = this.#destroyAll(error); if (destroyCallback === undefined) { return destroying; } destroying.then(destroyCallback, destroyCallback); } #proxyDispatcher(proxyUrl) { const existing = this.#proxyDispatchers.get(proxyUrl); if (existing !== undefined) { return existing; } const dispatcher = createUndiciProxyAgent(proxyUrl, this.#dispatcherOptions); this.#proxyDispatchers.set(proxyUrl, dispatcher); return dispatcher; } async #closeAll() { this.#closedError ??= new undiciErrors.ClientClosedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.close(), ...proxyDispatchers.map((dispatcher) => dispatcher.close()), ]); } async #destroyAll(error) { this.#closedError ??= error ?? new undiciErrors.ClientDestroyedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.destroy(error), ...proxyDispatchers.map((dispatcher) => dispatcher.destroy(error)), ]); } } function resolveUndiciDispatchUrl(options) { if (options.origin !== undefined) { const origin = options.origin.toString().replace(/\/$/, ""); const path = options.path.startsWith("/") ? options.path : `/${options.path}`; return new URL(`${origin}${path}`).href; } try { return new URL(options.path).href; } catch { return undefined; } } function restoreNodeHttpSnapshot(snapshot) { http.request = snapshot.httpRequest; http.get = snapshot.httpGet; http.globalAgent = snapshot.httpGlobalAgent; https.request = snapshot.httpsRequest; https.get = snapshot.httpsGet; https.globalAgent = snapshot.httpsGlobalAgent; } function installRuntime(resolver, dispatcherOptions, proxyCa, options) { if (activeRuntime !== undefined) { throw new ProxylineError("RUNTIME_ALREADY_ACTIVE", "Proxyline already has an active runtime."); } const snapshot = { httpRequest: http.request, httpGet: http.get, httpGlobalAgent: http.globalAgent, httpsRequest: https.request, httpsGet: https.get, httpsGlobalAgent: https.globalAgent, }; const nodeHttpAgent = createNodeProxyAgent(resolver, proxyCa, "http"); const nodeHttpsAgent = createNodeProxyAgent(resolver, proxyCa, "https"); const originalDispatcher = getGlobalDispatcher(); const originalFetch = globalThis.fetch; const originalFormData = globalThis.FormData; const originalHeaders = globalThis.Headers; const originalRequest = globalThis.Request; const originalResponse = globalThis.Response; const installedDispatcher = createUndiciProxyDispatcher(dispatcherOptions, { proxyCa, undici: options.undici, }); const runtime = { ambientEnv: options.ambientEnv, bypassPolicy: options.bypassPolicy, installedDispatcher, mode: dispatcherOptions.mode, nodeHttpAgent, nodeHttpsAgent, originalDispatcher, originalFetch, originalFormData, originalHeaders, originalRequest, originalResponse, proxyCa, proxyUrl: options.proxyUrl?.href, snapshot, undiciOptions: options.undici, }; activeRuntime = runtime; try { http.globalAgent = nodeHttpAgent; https.globalAgent = nodeHttpsAgent; http.request = bindNodeHttpMethod(snapshot.httpRequest, () => createNodeProxyAgent(resolver, proxyCa, "http")); http.get = bindNodeHttpMethod(snapshot.httpGet, () => createNodeProxyAgent(resolver, proxyCa, "http")); https.request = bindNodeHttpMethod(snapshot.httpsRequest, () => createNodeProxyAgent(resolver, proxyCa, "https")); https.get = bindNodeHttpMethod(snapshot.httpsGet, () => createNodeProxyAgent(resolver, proxyCa, "https")); setGlobalDispatcher(installedDispatcher); globalThis.fetch = proxylineFetch; globalThis.FormData = proxylineFormData; globalThis.Headers = proxylineHeaders; globalThis.Request = proxylineRequest; globalThis.Response = proxylineResponse; } catch (error) { restoreNodeHttpSnapshot(snapshot); setGlobalDispatcher(originalDispatcher); globalThis.fetch = originalFetch; globalThis.FormData = originalFormData; globalThis.Headers = originalHeaders; globalThis.Request = originalRequest; globalThis.Response = originalResponse; activeRuntime = undefined; void installedDispatcher.destroy(); nodeHttpAgent.destroy(); nodeHttpsAgent.destroy(); throw error; } return runtime; } function stopRuntime(runtime) { if (activeRuntime !== runtime) { return; } restoreNodeHttpSnapshot(runtime.snapshot); setGlobalDispatcher(runtime.originalDispatcher); globalThis.fetch = runtime.originalFetch; globalThis.FormData = runtime.originalFormData; globalThis.Headers = runtime.originalHeaders; globalThis.Request = runtime.originalRequest; globalThis.Response = runtime.originalResponse; void runtime.installedDispatcher.destroy(); runtime.nodeHttpAgent.destroy(); runtime.nodeHttpsAgent.destroy(); activeRuntime = undefined; activeHandle = undefined; } export function installProxyline(options) { const proxyUrl = options.mode === "managed" ? normalizeProxyUrl(options.proxyUrl) : undefined; const ambientEnv = proxyUrl === undefined ? readProxyEnv() : undefined; if (options.mode === "managed" && proxyUrl === undefined) { throw new ProxylineError("MANAGED_PROXY_URL_REQUIRED", "Proxyline managed mode requires an explicit proxyUrl."); } const activePolicy = options.ifActive ?? "error"; if (activeRuntime !== undefined) { if (activePolicy === "replace") { activeHandle?.stop(); } else if (activePolicy === "reuse-compatible" && activeHandle !== undefined && activeRuntime.mode === options.mode && activeRuntime.proxyUrl === proxyUrl?.href && proxyEnvSnapshotKey(activeRuntime.ambientEnv) === proxyEnvSnapshotKey(ambientEnv) && activeRuntime.proxyCa === resolveProxyTlsCa(options.proxyTls) && activeRuntime.bypassPolicy === options.bypassPolicy && JSON.stringify(activeRuntime.undiciOptions ?? {}) === JSON.stringify(options.undici ?? {})) { return activeHandle; } else { throw new ProxylineError("RUNTIME_ALREADY_ACTIVE", "Proxyline already has an active runtime."); } } let stopped = false; const proxyCa = resolveProxyTlsCa(options.proxyTls); const dynamicBypasses = createDynamicBypassRegistry(); const resolver = proxyUrl !== undefined ? createManagedProxyResolver(proxyUrl, options.bypassPolicy, dynamicBypasses) : createAmbientProxyResolver(ambientEnv ?? EMPTY_PROXY_ENV); const redactedProxyUrl = resolver.describeProxy(); const hasActiveProxy = resolver.active; const runtime = hasActiveProxy ? installRuntime(resolver, proxyUrl !== undefined ? { mode: "managed", resolver } : { mode: "ambient", env: ambientEnv ?? EMPTY_PROXY_ENV, active: hasActiveProxy }, proxyCa, { ambientEnv, bypassPolicy: options.bypassPolicy, proxyUrl, undici: options.undici, }) : undefined; const handle = { mode: options.mode, active: hasActiveProxy, ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}), createNodeAgent: () => { if (!hasActiveProxy || stopped) { return createDirectNodeAgent(); } return createNodeProxyAgent(resolver, proxyCa); }, createUndiciDispatcher: () => stopped ? createUndiciAgent(options.undici) : createUndiciProxyDispatcher(proxyUrl !== undefined ? { mode: "managed", resolver } : { mode: "ambient", env: ambientEnv ?? EMPTY_PROXY_ENV, active: hasActiveProxy }, { proxyCa, undici: options.undici }), createWebSocketAgent: () => { if (!hasActiveProxy || stopped) { return createDirectNodeAgent(); } return createNodeProxyAgent(resolver, proxyCa); }, explain: (url, explainOptions) => { const decision = stopped ? { kind: "direct", reason: "runtime-stopped", surface: explainOptions?.surface ?? "unknown", url: formatUrl(url), } : resolver.explain(url, explainOptions?.surface ?? "unknown"); emit(options.onEvent, { type: "decision", decision }); return decision; }, registerBypass: (registration) => { if (stopped || proxyUrl === undefined) { return () => { }; } return dynamicBypasses.add(registration); }, stop: () => { if (stopped) { return; } stopped = true; if (runtime !== undefined) { stopRuntime(runtime); } emit(options.onEvent, { type: "runtime.stopped", mode: options.mode }); }, withBypass: (registration, run) => { if (stopped || proxyUrl === undefined) { return run(); } return dynamicBypasses.runScoped(registration, run); }, }; activeHandle = hasActiveProxy ? handle : activeHandle; emit(options.onEvent, { type: "runtime.installed", mode: options.mode, active: hasActiveProxy, ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}), }); return handle; } export const installGlobalProxy = installProxyline;