openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,024 lines (960 loc) • 34.1 kB
text/typescript
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,
type ProxyEnvSnapshot,
} from "./env.js";
import {
bindNodeHttpMethod,
createDirectNodeAgent,
createNodeProxyAgent,
type NodeHttpStackSnapshot,
type ProxylineNodeProxyAgent,
} from "./node-http.js";
import {
formatUrl,
ProxylineError,
redactProxyUrl,
resolveProxyTlsCa,
} from "./shared.js";
import type {
ProxylineBypassRegistration,
ProxylineBypassPolicy,
ProxylineEvent,
ProxylineHandle,
ProxylineOptions,
ProxyResolver,
ProxylineSurface,
ProxylineUndiciOptions,
} from "./types.js";
import { PROXYLINE_DISPATCHER_BRAND } from "./dispatcher-brand.js";
type RuntimeInstall = {
ambientEnv: ProxyEnvSnapshot | undefined;
bypassPolicy: ProxylineBypassPolicy | undefined;
installedDispatcher: Dispatcher;
mode: ProxylineOptions["mode"];
nodeHttpAgent: ProxylineNodeProxyAgent;
nodeHttpsAgent: ProxylineNodeProxyAgent;
originalDispatcher: Dispatcher;
originalFetch: typeof globalThis.fetch;
originalFormData: typeof globalThis.FormData;
originalHeaders: typeof globalThis.Headers;
originalRequest: typeof globalThis.Request;
originalResponse: typeof globalThis.Response;
proxyCa: string | undefined;
proxyUrl: string | undefined;
snapshot: NodeHttpStackSnapshot;
undiciOptions: ProxylineUndiciOptions | undefined;
};
let activeRuntime: RuntimeInstall | undefined;
let activeHandle: ProxylineHandle | undefined;
type ProxylineDispatcher = Dispatcher & {
[PROXYLINE_DISPATCHER_BRAND]?: true;
};
type CompatibleDispatchHandler = Dispatcher.DispatchHandler & {
onError?: (error: Error) => void;
onResponseError?: (controller: Dispatcher.DispatchController | null, error: Error) => void;
};
// 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 as unknown as typeof globalThis.Headers;
const proxylineRequest = UndiciRequest as unknown as typeof globalThis.Request;
const proxylineResponse = UndiciResponse as unknown as typeof globalThis.Response;
const proxylineFormData = UndiciFormData as unknown as typeof globalThis.FormData;
type ProxylineRequestInit = {
body?: unknown;
cache?: unknown;
credentials?: unknown;
dispatcher?: unknown;
duplex?: "half";
headers?: unknown;
integrity?: unknown;
keepalive?: unknown;
method?: string;
mode?: unknown;
redirect?: unknown;
referrer?: unknown;
referrerPolicy?: unknown;
signal?: unknown;
};
type FetchRequestLike = Readonly<{
arrayBuffer: () => Promise<ArrayBuffer>;
body: ReadableStream<Uint8Array> | null;
cache?: unknown;
credentials?: unknown;
headers: InstanceType<typeof globalThis.Headers>;
integrity?: unknown;
keepalive?: unknown;
method: string;
mode?: unknown;
redirect?: unknown;
referrer?: unknown;
referrerPolicy?: unknown;
signal?: unknown;
url: string;
}>;
function getRequestDispatcher(request: FetchRequestLike): unknown {
for (const symbol of Object.getOwnPropertySymbols(request)) {
if (symbol.description !== "dispatcher") {
continue;
}
return Reflect.get(request, symbol);
}
return undefined;
}
function isFetchRequestLike(value: unknown): value is FetchRequestLike {
if (typeof value !== "object" || value === null) {
return false;
}
const record = value as Readonly<Record<string, unknown>>;
return (
typeof record.url === "string" &&
typeof record.method === "string" &&
typeof record.arrayBuffer === "function" &&
record.headers !== undefined
);
}
async function createProxylineRequestFromRequestLike(
request: FetchRequestLike,
options: { includeBody: boolean; preserveDispatcher: boolean },
): Promise<globalThis.Request> {
const init: ProxylineRequestInit = {
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: unknown = 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: Parameters<typeof globalThis.fetch>[1]): boolean {
if (typeof init !== "object" || init === null) {
return false;
}
return "body" in init;
}
async function normalizeFetchInput(
input: Parameters<typeof globalThis.fetch>[0],
init: Parameters<typeof globalThis.fetch>[1],
options: { preserveDispatcher: boolean },
): Promise<Parameters<typeof globalThis.fetch>[0]> {
if ((input instanceof proxylineRequest && options.preserveDispatcher) || !isFetchRequestLike(input)) {
return input;
}
return await createProxylineRequestFromRequestLike(input, {
includeBody: !requestInitOverridesBody(init),
preserveDispatcher: options.preserveDispatcher,
});
}
function withManagedFetchDispatcher(
init: Parameters<typeof globalThis.fetch>[1],
dispatcher: Dispatcher,
): Parameters<typeof globalThis.fetch>[1] {
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: typeof globalThis.fetch = 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: unknown = 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: string | URL | undefined): URL | undefined {
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: ProxylineOptions["onEvent"], event: ProxylineEvent): void {
try {
onEvent?.(event);
} catch {
// Observer failures must not fail install or leave patched globals unstoppable.
}
}
function isProxyableUrlProtocol(protocol: string): boolean {
return protocol === "http:" ||
protocol === "https:" ||
protocol === "ws:" ||
protocol === "wss:";
}
function shouldBypassManagedProxy(
bypassPolicy: ProxylineBypassPolicy | undefined,
bypasses: DynamicBypassRegistry,
url: string | URL,
surface: ProxylineSurface,
): boolean {
if (bypasses.has(url, surface)) {
return true;
}
if (bypassPolicy === undefined) {
return false;
}
return bypassPolicy({ surface, url: formatUrl(url) });
}
type DynamicBypassRegistry = {
add: (registration: ProxylineBypassRegistration) => () => void;
has: (url: string | URL, surface: ProxylineSurface) => boolean;
runScoped: <T>(registration: ProxylineBypassRegistration, run: () => T) => T;
};
function bypassKey(url: string | URL, surface: ProxylineSurface | undefined): string {
return `${surface ?? "*"}\n${formatUrl(url)}`;
}
function createDynamicBypassRegistry(): DynamicBypassRegistry {
const counts = new Map<string, number>();
const scopedBypasses = new AsyncLocalStorage<ReadonlySet<string>>();
const hasScopedBypass = (url: string | URL, surface: ProxylineSurface): boolean => {
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: ProxyEnvSnapshot | undefined): string {
return JSON.stringify(env ?? EMPTY_PROXY_ENV);
}
function createManagedProxyResolver(
proxyUrl: URL,
bypassPolicy: ProxylineBypassPolicy | undefined,
bypasses: DynamicBypassRegistry,
): ProxyResolver {
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
: "";
},
};
}
type UndiciDispatcherOptions = Readonly<{
proxyCa: string | undefined;
undici: ProxylineUndiciOptions | undefined;
}>;
type UndiciProxyAgentOptions = Exclude<ConstructorParameters<typeof UndiciProxyAgent>[0], string | URL>;
type UndiciProxyClientFactory = NonNullable<
UndiciProxyAgentOptions["clientFactory"]
>;
type UnknownFunction = (...args: unknown[]) => unknown;
function finiteNonNegativeInteger(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0
? Math.floor(value)
: undefined;
}
function finitePositiveInteger(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0
? Math.floor(value)
: undefined;
}
function resolveUndiciBaseOptions(
options: ProxylineUndiciOptions | undefined,
): Record<string, unknown> {
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: ProxylineUndiciOptions | undefined): UndiciAgent {
return new UndiciAgent(resolveUndiciBaseOptions(options));
}
function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stripIpServernameFromConnectOptions(options: unknown): unknown {
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: unknown): unknown {
if (typeof connect !== "function") {
return connect;
}
return (options: unknown, callback: unknown): unknown =>
(connect as UnknownFunction)(stripIpServernameFromConnectOptions(options), callback);
}
const checkProxyServerIdentity: typeof tls.checkServerIdentity = (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(): UndiciProxyClientFactory {
return (origin: URL, options: object): Dispatcher => {
const clientOptions = isObjectRecord(options)
? { ...options, connect: stripIpServernameFromConnect(options.connect) }
: options;
return new UndiciPool(
origin,
clientOptions as ConstructorParameters<typeof UndiciPool>[1],
);
};
}
function createUndiciProxyAgent(
proxyUrl: string,
options: UndiciDispatcherOptions,
): UndiciProxyAgent {
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 } : {}),
} as ConstructorParameters<typeof UndiciProxyAgent>[0]);
}
function normalizeUndiciDispatchOptions(
options: Dispatcher.DispatchOptions,
): Dispatcher.DispatchOptions {
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:
| { mode: "managed"; resolver: ProxyResolver }
| { mode: "ambient"; env: ProxyEnvSnapshot; active: boolean },
dispatcherOptions: UndiciDispatcherOptions,
): Dispatcher {
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: Dispatcher.DispatchHandler,
error: Error,
): boolean {
const compatibleHandler = handler as CompatibleDispatchHandler;
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 {
public readonly [PROXYLINE_DISPATCHER_BRAND] = true;
readonly #directDispatcher: UndiciAgent;
readonly #dispatcherOptions: UndiciDispatcherOptions;
readonly #proxyDispatchers = new Map<string, UndiciProxyAgent>();
readonly #resolver: ProxyResolver;
#closedError: Error | undefined;
public constructor(resolver: ProxyResolver, dispatcherOptions: UndiciDispatcherOptions) {
super();
this.#resolver = resolver;
this.#dispatcherOptions = dispatcherOptions;
this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici);
}
public override dispatch(
options: Dispatcher.DispatchOptions,
handler: Dispatcher.DispatchHandler,
): boolean {
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);
}
public override close(callback: () => void): void;
public override close(): Promise<void>;
public override close(callback?: () => void): Promise<void> | void {
const closing = this.#closeAll();
if (callback === undefined) {
return closing;
}
closing.then(callback, callback);
}
public override destroy(): Promise<void>;
public override destroy(error: Error | null): Promise<void>;
public override destroy(callback: () => void): void;
public override destroy(error: Error | null, callback: () => void): void;
public override destroy(
errorOrCallback?: Error | null | (() => void),
callback?: () => void,
): Promise<void> | void {
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: string): UndiciProxyAgent {
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(): Promise<void> {
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: Error | null): Promise<void> {
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 {
public readonly [PROXYLINE_DISPATCHER_BRAND] = true;
readonly #directDispatcher: UndiciAgent;
readonly #dispatcherOptions: UndiciDispatcherOptions;
readonly #env: ProxyEnvSnapshot;
readonly #proxyDispatchers = new Map<string, UndiciProxyAgent>();
#closedError: Error | undefined;
public constructor(env: ProxyEnvSnapshot, dispatcherOptions: UndiciDispatcherOptions) {
super();
this.#env = env;
this.#dispatcherOptions = dispatcherOptions;
this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici);
}
public override dispatch(
options: Dispatcher.DispatchOptions,
handler: Dispatcher.DispatchHandler,
): boolean {
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);
}
public override close(callback: () => void): void;
public override close(): Promise<void>;
public override close(callback?: () => void): Promise<void> | void {
const closing = this.#closeAll();
if (callback === undefined) {
return closing;
}
closing.then(callback, callback);
}
public override destroy(): Promise<void>;
public override destroy(error: Error | null): Promise<void>;
public override destroy(callback: () => void): void;
public override destroy(error: Error | null, callback: () => void): void;
public override destroy(
errorOrCallback?: Error | null | (() => void),
callback?: () => void,
): Promise<void> | void {
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: string): UndiciProxyAgent {
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(): Promise<void> {
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: Error | null): Promise<void> {
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: Dispatcher.DispatchOptions): string | undefined {
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: NodeHttpStackSnapshot): void {
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: ProxyResolver,
dispatcherOptions:
| { mode: "managed"; resolver: ProxyResolver }
| { mode: "ambient"; env: ProxyEnvSnapshot; active: boolean },
proxyCa: string | undefined,
options: {
ambientEnv: ProxyEnvSnapshot | undefined;
bypassPolicy: ProxylineBypassPolicy | undefined;
proxyUrl: URL | undefined;
undici: ProxylineUndiciOptions | undefined;
},
): RuntimeInstall {
if (activeRuntime !== undefined) {
throw new ProxylineError("RUNTIME_ALREADY_ACTIVE", "Proxyline already has an active runtime.");
}
const snapshot: NodeHttpStackSnapshot = {
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: RuntimeInstall = {
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 as unknown as typeof https.globalAgent;
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: RuntimeInstall): void {
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: ProxylineOptions): ProxylineHandle {
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: ProxylineHandle = {
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" as const,
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;