mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 22:16:32 +00:00
refactor: route node proxy agents through proxyline
This commit is contained in:
46
extensions/discord/npm-shrinkwrap.json
generated
46
extensions/discord/npm-shrinkwrap.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<typeof HttpsAgent>
|
||||
| InstanceType<typeof httpsProxyAgent.HttpsProxyAgent<string>>;
|
||||
type DiscordGatewayWebSocketAgent = InstanceType<typeof HttpsAgent> | HttpAgent;
|
||||
const registrationPromises = new WeakMap<discordGateway.GatewayPlugin, Promise<void>>();
|
||||
type DiscordGatewayClient = Parameters<discordGateway.GatewayPlugin["registerClient"]>[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<string>(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) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
23
extensions/slack/npm-shrinkwrap.json
generated
23
extensions/slack/npm-shrinkwrap.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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<string> | 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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
23
extensions/whatsapp/npm-shrinkwrap.json
generated
23
extensions/whatsapp/npm-shrinkwrap.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<typeof getChildLogger>,
|
||||
): Promise<Agent | undefined> {
|
||||
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;
|
||||
}
|
||||
|
||||
101
npm-shrinkwrap.json
generated
101
npm-shrinkwrap.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
177
src/infra/net/node-proxy-agent.ts
Normal file
177
src/infra/net/node-proxy-agent.ts
Normal file
@@ -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<Parameters<typeof createAmbientNodeProxyAgent>[0]>;
|
||||
type ProxylineEnvSnapshot = NonNullable<ProxylineAgentOptions["env"]>;
|
||||
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<CreateNodeProxyAgentOptions, { mode: "explicit" }>,
|
||||
): HttpAgent;
|
||||
export function createNodeProxyAgent(
|
||||
options: Extract<CreateNodeProxyAgentOptions, { mode: "env" }>,
|
||||
): 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 }),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, number> = {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user