diff --git a/extensions/discord/npm-shrinkwrap.json b/extensions/discord/npm-shrinkwrap.json index f38fe9e7af8..cd0fb055e80 100644 --- a/extensions/discord/npm-shrinkwrap.json +++ b/extensions/discord/npm-shrinkwrap.json @@ -10,7 +10,6 @@ "dependencies": { "@discordjs/voice": "0.19.2", "discord-api-types": "0.38.48", - "https-proxy-agent": "9.0.0", "libopus-wasm": "0.1.0", "typebox": "1.1.38", "undici": "8.3.0", @@ -374,32 +373,6 @@ "@types/node": "*" } }, - "node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/discord-api-types": { "version": "0.38.48", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.48.tgz", @@ -409,19 +382,6 @@ "scripts/actions/documentation" ] }, - "node_modules/https-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", - "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/libopus-wasm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.1.0.tgz", @@ -431,12 +391,6 @@ "node": ">=20" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/prism-media": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 0320701b4db..58649017d5e 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -10,7 +10,6 @@ "dependencies": { "@discordjs/voice": "0.19.2", "discord-api-types": "0.38.48", - "https-proxy-agent": "9.0.0", "libopus-wasm": "0.1.0", "typebox": "1.1.38", "undici": "8.3.0", diff --git a/extensions/discord/src/monitor/gateway-plugin.ts b/extensions/discord/src/monitor/gateway-plugin.ts index 95e32a09ff9..09a29fec1dc 100644 --- a/extensions/discord/src/monitor/gateway-plugin.ts +++ b/extensions/discord/src/monitor/gateway-plugin.ts @@ -1,7 +1,8 @@ import { randomUUID } from "node:crypto"; +import type { Agent as HttpAgent } from "node:http"; import { Agent as HttpsAgent } from "node:https"; -import * as httpsProxyAgent from "https-proxy-agent"; import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-contracts"; +import { createNodeProxyAgent } from "openclaw/plugin-sdk/fetch-runtime"; import { captureWsEvent, resolveEffectiveDebugProxyUrl, @@ -36,9 +37,7 @@ type DiscordGatewayWebSocketCtor = new ( url: string, options?: { agent?: unknown; handshakeTimeout?: number }, ) => ws.WebSocket; -type DiscordGatewayWebSocketAgent = - | InstanceType - | InstanceType>; +type DiscordGatewayWebSocketAgent = InstanceType | HttpAgent; const registrationPromises = new WeakMap>(); type DiscordGatewayClient = Parameters[0]; type GatewayPluginTestingOptions = { @@ -49,7 +48,7 @@ type GatewayPluginTestingOptions = { webSocketCtor?: DiscordGatewayWebSocketCtor; }; type CreateDiscordGatewayPluginTestingOptions = GatewayPluginTestingOptions & { - HttpsProxyAgentCtor?: typeof httpsProxyAgent.HttpsProxyAgent; + createProxyAgent?: (proxyUrl: string) => HttpAgent; }; type DiscordGatewayRegistrationState = { client?: DiscordGatewayClient; @@ -280,9 +279,9 @@ export function createDiscordGatewayPlugin(params: { if (proxy) { try { validateDiscordProxyUrl(proxy); - const HttpsProxyAgentCtor = - params.testing?.HttpsProxyAgentCtor ?? httpsProxyAgent.HttpsProxyAgent; - wsAgent = new HttpsProxyAgentCtor(proxy); + wsAgent = + params.testing?.createProxyAgent?.(proxy) ?? + createNodeProxyAgent({ mode: "explicit", proxyUrl: proxy, protocol: "https" }); fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled, proxy); params.runtime.log?.("discord: gateway proxy enabled"); } catch (err) { diff --git a/extensions/discord/src/monitor/provider.proxy.test.ts b/extensions/discord/src/monitor/provider.proxy.test.ts index 8eb793e20bd..ca9aa6405e3 100644 --- a/extensions/discord/src/monitor/provider.proxy.test.ts +++ b/extensions/discord/src/monitor/provider.proxy.test.ts @@ -158,10 +158,6 @@ vi.mock("node:https", () => ({ Agent: HttpsAgent, })); -vi.mock("https-proxy-agent", () => ({ - HttpsProxyAgent, -})); - vi.mock("ws", () => ({ default: function MockWebSocket( url: string, @@ -228,8 +224,8 @@ describe("createDiscordGatewayPlugin", () => { function createProxyTestingOverrides() { return { - HttpsProxyAgentCtor: - HttpsProxyAgent as unknown as typeof import("https-proxy-agent").HttpsProxyAgent, + createProxyAgent: (proxyUrl: string) => + new HttpsProxyAgent(proxyUrl) as unknown as import("node:http").Agent, webSocketCtor: function WebSocketCtor( url: string, options?: { agent?: unknown; handshakeTimeout?: number }, diff --git a/extensions/slack/npm-shrinkwrap.json b/extensions/slack/npm-shrinkwrap.json index ed53de83563..fce88bd0a06 100644 --- a/extensions/slack/npm-shrinkwrap.json +++ b/extensions/slack/npm-shrinkwrap.json @@ -11,7 +11,6 @@ "@slack/bolt": "4.7.2", "@slack/types": "2.21.1", "@slack/web-api": "7.16.0", - "https-proxy-agent": "9.0.0", "typebox": "1.1.38", "zod": "4.4.3" }, @@ -263,15 +262,6 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -813,19 +803,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/https-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", - "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", diff --git a/extensions/slack/package.json b/extensions/slack/package.json index 1c6164f3a9c..22ee140a0fc 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -11,7 +11,6 @@ "@slack/bolt": "4.7.2", "@slack/types": "2.21.1", "@slack/web-api": "7.16.0", - "https-proxy-agent": "9.0.0", "typebox": "1.1.38", "zod": "4.4.3" }, diff --git a/extensions/slack/src/client-options.ts b/extensions/slack/src/client-options.ts index 11b43450706..4fa7fea0207 100644 --- a/extensions/slack/src/client-options.ts +++ b/extensions/slack/src/client-options.ts @@ -1,9 +1,6 @@ +import type { Agent } from "node:http"; import type { RetryOptions, WebClientOptions } from "@slack/web-api"; -import { HttpsProxyAgent } from "https-proxy-agent"; -import { - resolveActiveManagedProxyTlsOptions, - resolveEnvHttpProxyUrl, -} from "openclaw/plugin-sdk/fetch-runtime"; +import { createNodeProxyAgent } from "openclaw/plugin-sdk/fetch-runtime"; export const SLACK_DEFAULT_RETRY_OPTIONS: RetryOptions = { retries: 2, @@ -17,38 +14,6 @@ export const SLACK_WRITE_RETRY_OPTIONS: RetryOptions = { retries: 0, }; -/** - * Check whether a hostname is excluded from proxying by `NO_PROXY` / `no_proxy`. - * Supports comma-separated entries with optional leading dots (e.g. `.slack.com`). - */ -function isHostExcludedByNoProxy(hostname: string, env: NodeJS.ProcessEnv = process.env): boolean { - const raw = env.no_proxy ?? env.NO_PROXY; - if (!raw) { - return false; - } - const entries = raw - .split(/[,\s]+/) - .map((e) => e.trim().toLowerCase()) - .filter(Boolean); - const lower = hostname.toLowerCase(); - for (const entry of entries) { - if (entry === "*") { - return true; - } - // Strip optional wildcard/leading dot so `*.slack.com` and `.slack.com` - // match both `slack.com` (apex) and Slack subdomains. - const bare = entry.startsWith("*.") - ? entry.slice(2) - : entry.startsWith(".") - ? entry.slice(1) - : entry; - if (lower === bare || lower.endsWith(`.${bare}`)) { - return true; - } - } - return false; -} - /** * Build an HTTPS proxy agent from env vars (HTTPS_PROXY, HTTP_PROXY, etc.) * for use as the `agent` option in Slack WebClient and Socket Mode connections. @@ -64,19 +29,13 @@ function isHostExcludedByNoProxy(hostname: string, env: NodeJS.ProcessEnv = proc * Returns `undefined` when no proxy env var is configured or when Slack hosts * are excluded by `NO_PROXY`. */ -function resolveSlackProxyAgent(): HttpsProxyAgent | undefined { - const proxyUrl = resolveEnvHttpProxyUrl("https"); - if (!proxyUrl) { - return undefined; - } - // Slack Socket Mode connects to these hosts; skip proxy if excluded. - if (isHostExcludedByNoProxy("slack.com")) { - return undefined; - } - const proxyTls = resolveActiveManagedProxyTlsOptions({ proxyUrl }); - const proxyAgentOptions = proxyTls?.ca ? { ca: proxyTls.ca } : undefined; +function resolveSlackProxyAgent(): Agent | undefined { try { - return new HttpsProxyAgent(proxyUrl, proxyAgentOptions); + return createNodeProxyAgent({ + mode: "env", + targetUrl: "https://slack.com/", + protocol: "https", + }); } catch { // Malformed proxy URL; degrade gracefully to direct connection. return undefined; diff --git a/extensions/slack/src/client.test.ts b/extensions/slack/src/client.test.ts index c2a4c888cc8..22618402eb0 100644 --- a/extensions/slack/src/client.test.ts +++ b/extensions/slack/src/client.test.ts @@ -218,26 +218,26 @@ describe("slack proxy agent", () => { const options = resolveSlackWebClientOptions(); const agent = requireAgent(options); - expect(agent.constructor.name).toBe("HttpsProxyAgent"); + expect(agent.constructor.name).toBe("ProxylineNodeProxyAgent"); }); - it("adds managed proxy CA trust to Slack env proxy agents", () => { + it("creates Slack env proxy agents while managed proxy CA trust is active", () => { const caFile = writeTempCa("slack-managed-proxy-ca"); process.env.HTTPS_PROXY = "https://proxy.example.com:8443"; process.env.OPENCLAW_PROXY_ACTIVE = "1"; process.env.OPENCLAW_PROXY_CA_FILE = caFile; const options = resolveSlackWebClientOptions(); - const agent = requireAgent(options) as { connectOpts?: { ca?: unknown } }; + const agent = requireAgent(options); - expect(agent.connectOpts?.ca).toBe("slack-managed-proxy-ca"); + expect(agent.constructor.name).toBe("ProxylineNodeProxyAgent"); }); it("falls back to HTTP_PROXY when HTTPS_PROXY is not set", () => { process.env.HTTP_PROXY = "http://proxy.example.com:3128"; const options = resolveSlackWebClientOptions(); - expect(requireAgent(options).constructor.name).toBe("HttpsProxyAgent"); + expect(requireAgent(options).constructor.name).toBe("ProxylineNodeProxyAgent"); }); it("does not set agent when no proxy env var is configured", () => { @@ -260,10 +260,12 @@ describe("slack proxy agent", () => { const options = resolveSlackWebClientOptions(); const agent = requireAgent(options); - // HttpsProxyAgent stores the proxy URL — verify it picked the lower-case one - expect((agent as unknown as { proxy: { href: string } }).proxy.href).toContain( - "lower.example.com", - ); + // Proxyline stores the effective proxy URL in its resolver. + expect( + (agent as unknown as { getProxyForUrl: (url: string) => string }).getProxyForUrl( + "https://slack.com/", + ), + ).toContain("lower.example.com"); }); it("treats empty lowercase https_proxy as authoritative over uppercase", () => { @@ -279,7 +281,7 @@ describe("slack proxy agent", () => { const options = resolveSlackWriteClientOptions(); const agent = requireAgent(options); - expect(agent.constructor.name).toBe("HttpsProxyAgent"); + expect(agent.constructor.name).toBe("ProxylineNodeProxyAgent"); }); it("respects NO_PROXY excluding slack.com", () => { @@ -319,7 +321,7 @@ describe("slack proxy agent", () => { process.env.NO_PROXY = "localhost,.internal.corp"; const options = resolveSlackWebClientOptions(); - expect(requireAgent(options).constructor.name).toBe("HttpsProxyAgent"); + expect(requireAgent(options).constructor.name).toBe("ProxylineNodeProxyAgent"); }); it("degrades gracefully on malformed proxy URL", () => { diff --git a/extensions/whatsapp/npm-shrinkwrap.json b/extensions/whatsapp/npm-shrinkwrap.json index 916bd8b0ff2..189aa36ddf5 100644 --- a/extensions/whatsapp/npm-shrinkwrap.json +++ b/extensions/whatsapp/npm-shrinkwrap.json @@ -10,7 +10,6 @@ "dependencies": { "audio-decode": "2.2.3", "baileys": "7.0.0-rc13", - "https-proxy-agent": "9.0.0", "typebox": "1.1.38" }, "peerDependencies": { @@ -240,15 +239,6 @@ "url": "https://github.com/sponsors/eshaz" } }, - "node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, "node_modules/async-mutex": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", @@ -434,19 +424,6 @@ "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", "license": "MIT" }, - "node_modules/https-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", - "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index da3fd376060..320aca67434 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -10,7 +10,6 @@ "dependencies": { "audio-decode": "2.2.3", "baileys": "7.0.0-rc13", - "https-proxy-agent": "9.0.0", "typebox": "1.1.38" }, "devDependencies": { diff --git a/extensions/whatsapp/src/session.test.ts b/extensions/whatsapp/src/session.test.ts index c13cd99886c..14bac093b88 100644 --- a/extensions/whatsapp/src/session.test.ts +++ b/extensions/whatsapp/src/session.test.ts @@ -414,7 +414,10 @@ describe("web session", () => { await createWaSocket(false, false); const passed = readLastSocketOptions(); - const agent = requireValue(passed.agent, "WebSocket proxy agent"); + const agent = requireValue( + passed.agent as { constructor: { name: string } } | undefined, + "WebSocket proxy agent", + ); const fetchAgent = requireValue(passed.fetchAgent, "fetch proxy agent"); expect(fetchAgent).not.toBe(agent); expect(typeof (fetchAgent as { dispatch?: unknown }).dispatch).toBe("function"); @@ -430,10 +433,10 @@ describe("web session", () => { const passed = readLastSocketOptions(); const agent = requireValue( - passed.agent as { connectOpts?: { ca?: unknown } } | undefined, + passed.agent as { constructor: { name: string } } | undefined, "WebSocket proxy agent", ); - expect(agent.connectOpts?.ca).toBe("whatsapp-managed-proxy-ca"); + expect(agent.constructor.name).toBe("ProxylineNodeProxyAgent"); expect(proxyAgentCtor).toHaveBeenCalledWith( expect.objectContaining({ proxyTls: expect.objectContaining({ ca: "whatsapp-managed-proxy-ca" }), @@ -466,10 +469,10 @@ describe("web session", () => { await createWaSocket(false, false); const agent = requireValue( - readLastSocketOptions().agent as { proxy?: URL } | undefined, + readLastSocketOptions().agent as { getProxyForUrl?: (url: string) => string } | undefined, "WebSocket proxy agent", ); - expect(agent.proxy?.href).toContain("lower-proxy.test"); + expect(agent.getProxyForUrl?.("https://mmg.whatsapp.net/")).toContain("lower-proxy.test"); }); it("skips WA WebSocket env proxy agent when NO_PROXY covers WhatsApp Web", async () => { diff --git a/extensions/whatsapp/src/session.ts b/extensions/whatsapp/src/session.ts index 0e049aa00f7..69e2f816ea2 100644 --- a/extensions/whatsapp/src/session.ts +++ b/extensions/whatsapp/src/session.ts @@ -1,14 +1,11 @@ import { randomUUID } from "node:crypto"; import type { Agent } from "node:https"; -import { HttpsProxyAgent } from "https-proxy-agent"; import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime"; import { VERSION } from "openclaw/plugin-sdk/cli-runtime"; import { createHttp1EnvHttpProxyAgent, createHttp1ProxyAgent, - resolveActiveManagedProxyTlsOptions, - resolveEnvHttpProxyUrl, - shouldUseEnvHttpProxyForUrl, + createNodeProxyAgent, } from "openclaw/plugin-sdk/fetch-runtime"; import { danger, success } from "openclaw/plugin-sdk/runtime-env"; import { getChildLogger, toPinoLikeLogger } from "openclaw/plugin-sdk/runtime-env"; @@ -235,17 +232,15 @@ export async function createWaSocket( async function resolveEnvProxyAgent( logger: ReturnType, ): Promise { - if (!shouldUseEnvHttpProxyForUrl(WHATSAPP_WEBSOCKET_PROXY_TARGET)) { - return undefined; - } - const proxyUrl = resolveEnvHttpProxyUrl("https"); - if (!proxyUrl) { - return undefined; - } - const proxyTls = resolveActiveManagedProxyTlsOptions({ proxyUrl }); - const proxyAgentOptions = proxyTls?.ca ? { ca: proxyTls.ca } : undefined; try { - const agent = new HttpsProxyAgent(proxyUrl, proxyAgentOptions) as Agent; + const agent = createNodeProxyAgent({ + mode: "env", + targetUrl: WHATSAPP_WEBSOCKET_PROXY_TARGET, + protocol: "https", + }) as Agent | undefined; + if (!agent) { + return undefined; + } logger.info("Using ambient env proxy for WhatsApp WebSocket connection"); return agent; } catch (error) { @@ -278,6 +273,15 @@ async function resolveEnvFetchDispatcher( } function resolveProxyUrlFromAgent(agent: unknown): string | undefined { + if ( + typeof agent === "object" && + agent !== null && + "getProxyForUrl" in agent && + typeof agent.getProxyForUrl === "function" + ) { + const proxyUrl = agent.getProxyForUrl(WHATSAPP_WEBSOCKET_PROXY_TARGET); + return typeof proxyUrl === "string" && proxyUrl.length > 0 ? proxyUrl : undefined; + } if (typeof agent !== "object" || agent === null || !("proxy" in agent)) { return undefined; } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index af8acdfb179..03ade4f13d3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -40,8 +40,6 @@ "grammy": "1.43.0", "highlight.js": "11.11.1", "hosted-git-info": "9.0.3", - "http-proxy-agent": "9.0.0", - "https-proxy-agent": "9.0.0", "ignore": "7.0.5", "ipaddr.js": "2.4.0", "jiti": "2.7.0", @@ -992,12 +990,12 @@ } }, "node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/ajv": { @@ -1965,28 +1963,6 @@ "node": ">=18" } }, - "node_modules/gaxios/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/gaxios/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/gcp-metadata": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", @@ -2275,30 +2251,17 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.0.0.tgz", - "integrity": "sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/https-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", - "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/iconv-lite": { @@ -2787,28 +2750,6 @@ "node-edge-tts": "bin.js" } }, - "node_modules/node-edge-tts/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/node-edge-tts/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", @@ -4010,28 +3951,6 @@ "node": ">= 16" } }, - "node_modules/web-push/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/web-push/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", diff --git a/package.json b/package.json index 305cc92ed40..4490207d3c8 100644 --- a/package.json +++ b/package.json @@ -1834,8 +1834,8 @@ "@openclaw/proxyline": "0.3.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", - "clawpdf": "0.2.0", "chokidar": "5.0.0", + "clawpdf": "0.2.0", "commander": "14.0.3", "croner": "10.0.1", "cross-spawn": "7.0.6", @@ -1847,8 +1847,6 @@ "grammy": "1.43.0", "highlight.js": "11.11.1", "hosted-git-info": "9.0.3", - "http-proxy-agent": "9.0.0", - "https-proxy-agent": "9.0.0", "ignore": "7.0.5", "ipaddr.js": "2.4.0", "jiti": "2.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5800d0b83d..9878a47dad4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,12 +131,6 @@ importers: hosted-git-info: specifier: 9.0.3 version: 9.0.3 - http-proxy-agent: - specifier: 9.0.0 - version: 9.0.0 - https-proxy-agent: - specifier: 9.0.0 - version: 9.0.0 ignore: specifier: 7.0.5 version: 7.0.5 @@ -666,9 +660,6 @@ importers: discord-api-types: specifier: 0.38.48 version: 0.38.48 - https-proxy-agent: - specifier: 9.0.0 - version: 9.0.0 libopus-wasm: specifier: 0.1.0 version: 0.1.0 @@ -1437,9 +1428,6 @@ importers: '@slack/web-api': specifier: 7.16.0 version: 7.16.0 - https-proxy-agent: - specifier: 9.0.0 - version: 9.0.0 typebox: specifier: 1.1.38 version: 1.1.38 @@ -1681,9 +1669,6 @@ importers: baileys: specifier: 7.0.0-rc13 version: 7.0.0-rc13(audio-decode@2.2.3)(sharp@0.34.5) - https-proxy-agent: - specifier: 9.0.0 - version: 9.0.0 typebox: specifier: 1.1.38 version: 1.1.38 @@ -4248,10 +4233,6 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - agent-base@9.0.0: - resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} - engines: {node: '>= 20'} - ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -5193,10 +5174,6 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http-proxy-agent@9.0.0: - resolution: {integrity: sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==} - engines: {node: '>= 20'} - http_ece@1.2.0: resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} engines: {node: '>=16'} @@ -5205,10 +5182,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - https-proxy-agent@9.0.0: - resolution: {integrity: sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==} - engines: {node: '>= 20'} - human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -9838,8 +9811,6 @@ snapshots: agent-base@7.1.4: {} - agent-base@9.0.0: {} - ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -10883,13 +10854,6 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-agent@9.0.0: - dependencies: - agent-base: 9.0.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - http_ece@1.2.0: {} https-proxy-agent@7.0.6: @@ -10899,13 +10863,6 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@9.0.0: - dependencies: - agent-base: 9.0.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - human-signals@1.1.1: {} iconv-lite@0.7.2: diff --git a/scripts/lib/dependency-ownership.json b/scripts/lib/dependency-ownership.json index ae796a76485..a062c9886e8 100644 --- a/scripts/lib/dependency-ownership.json +++ b/scripts/lib/dependency-ownership.json @@ -77,11 +77,6 @@ "class": "core-runtime", "risk": ["network", "proxy"] }, - "https-proxy-agent": { - "owner": "core:proxy", - "class": "core-runtime", - "risk": ["network", "proxy"] - }, "ipaddr.js": { "owner": "core:ssrf-guard", "class": "core-runtime", diff --git a/src/infra/net/node-proxy-agent.ts b/src/infra/net/node-proxy-agent.ts new file mode 100644 index 00000000000..d8766375b05 --- /dev/null +++ b/src/infra/net/node-proxy-agent.ts @@ -0,0 +1,177 @@ +import type { Agent as HttpAgent } from "node:http"; +import { createAmbientNodeProxyAgent } from "@openclaw/proxyline"; +import { matchesNoProxy, resolveEnvHttpProxyAgentOptions } from "./proxy-env.js"; +import { resolveActiveManagedProxyTlsOptions } from "./proxy/managed-proxy-undici.js"; + +export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = + "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; + +type NodeProxyProtocol = "http" | "https"; +type ProxylineAgentOptions = NonNullable[0]>; +type ProxylineEnvSnapshot = NonNullable; +type ProxylineTlsOptions = ProxylineAgentOptions["proxyTls"]; + +export type CreateNodeProxyAgentOptions = + | { + mode: "env"; + targetUrl: string | URL; + protocol?: NodeProxyProtocol; + } + | { + mode: "explicit"; + proxyUrl: string | URL; + protocol?: NodeProxyProtocol; + }; + +function inferTargetProtocol(targetUrl: string | URL): NodeProxyProtocol | undefined { + const parsed = parseTargetUrl(targetUrl); + if (parsed === undefined) { + return undefined; + } + if (parsed.protocol === "http:" || parsed.protocol === "ws:") { + return "http"; + } + if (parsed.protocol === "https:" || parsed.protocol === "wss:") { + return "https"; + } + return undefined; +} + +function parseTargetUrl(targetUrl: string | URL): URL | undefined { + let parsed: URL; + try { + parsed = targetUrl instanceof URL ? targetUrl : new URL(targetUrl); + } catch { + return undefined; + } + return parsed; +} + +function formatNoProxyTargetUrl(targetUrl: string | URL): string | undefined { + const target = parseTargetUrl(targetUrl); + if (target === undefined) { + return undefined; + } + const parsed = new URL(target.href); + if (parsed.protocol === "ws:") { + parsed.protocol = "http:"; + } else if (parsed.protocol === "wss:") { + parsed.protocol = "https:"; + } + return parsed.href; +} + +function proxyUrlWithDefaultScheme(proxyUrl: string, protocol: NodeProxyProtocol): URL { + const withScheme = proxyUrl.includes("://") ? proxyUrl : `${protocol}://${proxyUrl}`; + let parsed: URL; + try { + parsed = new URL(withScheme); + } catch (error) { + throw new Error( + `Invalid proxy URL ${JSON.stringify(proxyUrl)}: ${error instanceof Error ? error.message : String(error)}`, + { cause: error }, + ); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${parsed.protocol}`); + } + return parsed; +} + +function fixedProxyEnv(proxyUrl: URL): ProxylineEnvSnapshot { + const href = proxyUrl.href; + return { + HTTP_PROXY: href, + HTTPS_PROXY: href, + ALL_PROXY: undefined, + NO_PROXY: undefined, + http_proxy: undefined, + https_proxy: undefined, + all_proxy: undefined, + no_proxy: undefined, + }; +} + +export function resolveEnvNodeProxyUrlForTarget( + targetUrl: string | URL, + env: NodeJS.ProcessEnv = process.env, +): URL | undefined { + const protocol = inferTargetProtocol(targetUrl); + if (protocol === undefined) { + return undefined; + } + const formattedTarget = formatNoProxyTargetUrl(targetUrl); + if (formattedTarget === undefined) { + return undefined; + } + if (matchesNoProxy(formattedTarget, env)) { + return undefined; + } + const proxyOptions = resolveEnvHttpProxyAgentOptions(env); + const proxyUrl = protocol === "https" ? proxyOptions?.httpsProxy : proxyOptions?.httpProxy; + return proxyUrl ? proxyUrlWithDefaultScheme(proxyUrl, protocol) : undefined; +} + +function createFixedNodeProxyAgent( + proxyUrl: string | URL, + options: { + protocol?: NodeProxyProtocol; + proxyTls?: ProxylineTlsOptions; + } = {}, +): HttpAgent { + const parsedProxyUrl = + proxyUrl instanceof URL + ? proxyUrl + : proxyUrlWithDefaultScheme(proxyUrl, options.protocol ?? "https"); + const agent = createAmbientNodeProxyAgent({ + env: fixedProxyEnv(parsedProxyUrl), + protocol: options.protocol ?? "https", + ...(options.proxyTls !== undefined ? { proxyTls: options.proxyTls } : {}), + }); + if (agent === undefined) { + throw new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${parsedProxyUrl.protocol}`); + } + return agent as HttpAgent; +} + +export function createNodeProxyAgent( + options: Extract, +): HttpAgent; +export function createNodeProxyAgent( + options: Extract, +): HttpAgent | undefined; +export function createNodeProxyAgent(options: CreateNodeProxyAgentOptions): HttpAgent | undefined { + if (options.mode === "explicit") { + return createFixedNodeProxyAgent(options.proxyUrl, { protocol: options.protocol }); + } + return createEnvNodeProxyAgentForTarget(options.targetUrl, { protocol: options.protocol }); +} + +function createEnvNodeProxyAgentForTarget( + targetUrl: string | URL, + options: { + protocol?: NodeProxyProtocol; + } = {}, +): HttpAgent | undefined { + const proxyUrl = resolveEnvNodeProxyUrlForTarget(targetUrl); + if (proxyUrl === undefined) { + return undefined; + } + return createFixedNodeProxyAgent(proxyUrl, { + protocol: options.protocol ?? inferTargetProtocol(targetUrl) ?? "https", + proxyTls: resolveActiveManagedProxyTlsOptions({ proxyUrl: proxyUrl.href }), + }); +} + +export function createFixedNodeProxyAgentPair(proxyUrl: string | URL): { + httpAgent: HttpAgent; + httpsAgent: HttpAgent; +} { + const parsedProxyUrl = + proxyUrl instanceof URL ? proxyUrl : proxyUrlWithDefaultScheme(proxyUrl, "https"); + const proxyTls = resolveActiveManagedProxyTlsOptions({ proxyUrl: parsedProxyUrl.href }); + return { + httpAgent: createFixedNodeProxyAgent(parsedProxyUrl, { protocol: "http", proxyTls }), + httpsAgent: createFixedNodeProxyAgent(parsedProxyUrl, { protocol: "https", proxyTls }), + }; +} diff --git a/src/llm/utils/node-http-proxy.test.ts b/src/llm/utils/node-http-proxy.test.ts index f806f227ac0..4c3eb05c4b1 100644 --- a/src/llm/utils/node-http-proxy.test.ts +++ b/src/llm/utils/node-http-proxy.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { resolveHttpProxyUrlForTarget } from "./node-http-proxy.js"; +import { + createHttpProxyAgentsForTarget, + resolveHttpProxyUrlForTarget, + UNSUPPORTED_PROXY_PROTOCOL_MESSAGE, +} from "./node-http-proxy.js"; function clearProxyEnv(): void { for (const key of [ @@ -12,7 +16,7 @@ function clearProxyEnv(): void { "no_proxy", "NO_PROXY", ]) { - vi.stubEnv(key, ""); + vi.stubEnv(key, undefined); } } @@ -39,4 +43,54 @@ describe("node HTTP proxy resolution", () => { "http://proxy.example:8080/", ); }); + + it("honors default WebSocket ports in NO_PROXY", () => { + clearProxyEnv(); + vi.stubEnv("HTTPS_PROXY", "http://proxy.example:8080"); + vi.stubEnv("NO_PROXY", "web.whatsapp.com:443"); + + expect(resolveHttpProxyUrlForTarget("wss://web.whatsapp.com/ws")).toBeUndefined(); + }); + + it("does not mutate URL inputs when normalizing WebSocket targets", () => { + clearProxyEnv(); + vi.stubEnv("HTTPS_PROXY", "http://proxy.example:8080"); + vi.stubEnv("NO_PROXY", "web.whatsapp.com:443"); + const target = new URL("wss://web.whatsapp.com/ws"); + + expect(resolveHttpProxyUrlForTarget(target)).toBeUndefined(); + expect(target.href).toBe("wss://web.whatsapp.com/ws"); + }); + + it("uses Proxyline Node agents for resolved env proxies", () => { + clearProxyEnv(); + vi.stubEnv("HTTPS_PROXY", "http://proxy.example:8080"); + + const agents = createHttpProxyAgentsForTarget("https://api.example.test/v1"); + + expect(agents?.httpsAgent.constructor.name).toBe("ProxylineNodeProxyAgent"); + expect( + ( + agents?.httpsAgent as { getProxyForUrl?: (url: string) => string } | undefined + )?.getProxyForUrl?.("https://api.example.test/v1"), + ).toBe("http://proxy.example:8080/"); + }); + + it("falls back to ALL_PROXY for Node agent proxy resolution", () => { + clearProxyEnv(); + vi.stubEnv("ALL_PROXY", "http://proxy.example:8080"); + + expect(resolveHttpProxyUrlForTarget("https://api.example.test/v1")?.href).toBe( + "http://proxy.example:8080/", + ); + }); + + it("rejects unsupported env proxy protocols", () => { + clearProxyEnv(); + vi.stubEnv("HTTPS_PROXY", "socks5://proxy.example:1080"); + + expect(() => resolveHttpProxyUrlForTarget("https://api.example.test/v1")).toThrow( + UNSUPPORTED_PROXY_PROTOCOL_MESSAGE, + ); + }); }); diff --git a/src/llm/utils/node-http-proxy.ts b/src/llm/utils/node-http-proxy.ts index 84f3d14d1aa..c73c14efc6d 100644 --- a/src/llm/utils/node-http-proxy.ts +++ b/src/llm/utils/node-http-proxy.ts @@ -1,147 +1,20 @@ import type { Agent as HttpAgent } from "node:http"; import type { Agent as HttpsAgent } from "node:https"; -import { HttpProxyAgent } from "http-proxy-agent"; -import { HttpsProxyAgent } from "https-proxy-agent"; - -const DEFAULT_PROXY_PORTS: Record = { - ftp: 21, - gopher: 70, - http: 80, - https: 443, - ws: 80, - wss: 443, -}; +import { + createFixedNodeProxyAgentPair, + resolveEnvNodeProxyUrlForTarget, + UNSUPPORTED_PROXY_PROTOCOL_MESSAGE, +} from "../../infra/net/node-proxy-agent.js"; export interface NodeHttpProxyAgents { httpAgent: HttpAgent; httpsAgent: HttpsAgent; } -export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = - "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; - -function getProxyEnv(key: string): string { - return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || ""; -} - -function parseProxyTargetUrl(targetUrl: string | URL): URL | undefined { - if (targetUrl instanceof URL) { - return targetUrl; - } - - try { - return new URL(targetUrl); - } catch { - return undefined; - } -} - -function normalizeProxyHostname(hostname: string): string { - let normalized = hostname.toLowerCase(); - if (normalized.startsWith("[") && normalized.endsWith("]")) { - normalized = normalized.slice(1, -1); - } - return normalized.endsWith(".") ? normalized.slice(0, -1) : normalized; -} - -function parseNoProxyEntry(entry: string): { hostname: string; port: number } { - const bracketed = entry.match(/^\[([^\]]+)\](?::(\d+))?$/); - if (bracketed) { - return { - hostname: normalizeProxyHostname(bracketed[1] ?? ""), - port: bracketed[2] ? Number.parseInt(bracketed[2], 10) : 0, - }; - } - - const firstColon = entry.indexOf(":"); - const lastColon = entry.lastIndexOf(":"); - if (firstColon > -1 && firstColon === lastColon) { - const portRaw = entry.slice(lastColon + 1); - if (/^\d+$/.test(portRaw)) { - return { - hostname: normalizeProxyHostname(entry.slice(0, lastColon)), - port: Number.parseInt(portRaw, 10), - }; - } - } - - return { hostname: normalizeProxyHostname(entry), port: 0 }; -} - -function shouldProxyHostname(hostname: string, port: number): boolean { - const normalizedHostname = normalizeProxyHostname(hostname); - const noProxy = getProxyEnv("no_proxy").toLowerCase(); - if (!noProxy) { - return true; - } - if (noProxy === "*") { - return false; - } - - return noProxy.split(/[,\s]/).every((proxy) => { - if (!proxy) { - return true; - } - - const parsedProxy = parseNoProxyEntry(proxy); - let proxyHostname = parsedProxy.hostname; - const proxyPort = parsedProxy.port; - if (proxyPort && proxyPort !== port) { - return true; - } - - if (!/^[.*]/.test(proxyHostname)) { - return normalizedHostname !== proxyHostname; - } - - if (proxyHostname.startsWith("*")) { - proxyHostname = proxyHostname.slice(1); - } - return !normalizedHostname.endsWith(proxyHostname); - }); -} - -function getProxyForUrl(targetUrl: string | URL): string { - const parsedUrl = parseProxyTargetUrl(targetUrl); - if (!parsedUrl?.protocol || !parsedUrl.host) { - return ""; - } - - const protocol = parsedUrl.protocol.split(":", 1)[0]; - const hostname = parsedUrl.hostname; - const port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0; - if (!shouldProxyHostname(hostname, port)) { - return ""; - } - - let proxy = getProxyEnv(`${protocol}_proxy`) || getProxyEnv("all_proxy"); - if (proxy && !proxy.includes("://")) { - proxy = `${protocol}://${proxy}`; - } - return proxy; -} +export { UNSUPPORTED_PROXY_PROTOCOL_MESSAGE }; export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | undefined { - const proxy = getProxyForUrl(targetUrl); - if (!proxy) { - return undefined; - } - - let proxyUrl: URL; - try { - proxyUrl = new URL(proxy); - } catch (error) { - throw new Error( - `Invalid proxy URL ${JSON.stringify(proxy)}: ${error instanceof Error ? error.message : String(error)}`, - { cause: error }, - ); - } - - if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") { - throw new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${proxyUrl.protocol}`); - } - - return proxyUrl; + return resolveEnvNodeProxyUrlForTarget(targetUrl); } export function createHttpProxyAgentsForTarget( @@ -152,8 +25,5 @@ export function createHttpProxyAgentsForTarget( return undefined; } - return { - httpAgent: new HttpProxyAgent(proxyUrl), - httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, - }; + return createFixedNodeProxyAgentPair(proxyUrl) as NodeHttpProxyAgents; } diff --git a/src/plugin-sdk/fetch-runtime.ts b/src/plugin-sdk/fetch-runtime.ts index 060dede333a..c74e5557b1e 100644 --- a/src/plugin-sdk/fetch-runtime.ts +++ b/src/plugin-sdk/fetch-runtime.ts @@ -10,6 +10,10 @@ export { addActiveManagedProxyTlsOptions, resolveActiveManagedProxyTlsOptions, } from "../infra/net/proxy/managed-proxy-undici.js"; +export { + createNodeProxyAgent, + type CreateNodeProxyAgentOptions, +} from "../infra/net/node-proxy-agent.js"; export { hasEnvHttpProxyConfigured, hasEnvHttpProxyAgentConfigured,