@openclaw/proxyline 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +11 -13
- package/dist/node-http.d.ts.map +1 -1
- package/dist/node-http.js +8 -5
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +18 -19
- package/docs/CNAME +1 -0
- package/docs/README.md +34 -0
- package/docs/api-reference.md +280 -0
- package/docs/environment-variables.md +70 -0
- package/docs/getting-started.md +82 -0
- package/docs/index.md +46 -0
- package/docs/modes.md +68 -0
- package/docs/observability.md +98 -0
- package/docs/proxy-tls.md +66 -0
- package/docs/security.md +83 -0
- package/docs/surfaces.md +116 -0
- package/docs/testing.md +78 -0
- package/docs/troubleshooting.md +71 -0
- package/package.json +9 -3
- package/scripts/prepack-build.mjs +32 -0
- package/src/connect.ts +222 -0
- package/src/dispatcher-brand.ts +13 -0
- package/src/env.ts +250 -0
- package/src/index.ts +27 -0
- package/src/node-http.ts +901 -0
- package/src/runtime.ts +906 -0
- package/src/shared.ts +42 -0
- package/src/types.ts +98 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +22 -0
package/src/node-http.ts
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import tls from "node:tls";
|
|
5
|
+
import { domainToASCII } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
readProxyEnv,
|
|
8
|
+
resolveAmbientProxyForUrl,
|
|
9
|
+
type ProxyEnvSnapshot,
|
|
10
|
+
} from "./env.js";
|
|
11
|
+
import { formatConnectAuthority } from "./connect.js";
|
|
12
|
+
import { ProxylineError, resolveProxyTlsCa, type ProxylineTlsOptions } from "./shared.js";
|
|
13
|
+
import type { ProxylineSurface, ProxyResolver } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions & {
|
|
16
|
+
agent?: http.Agent | false;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type NodeHttpMethod = typeof http.request;
|
|
20
|
+
type NodeAgentFactory = (options: NodeHttpRequestOptions) => http.Agent;
|
|
21
|
+
type NodeAgentOptions = http.AgentOptions & https.AgentOptions;
|
|
22
|
+
type NodeAgentRequestOptions = http.RequestOptions & https.RequestOptions & {
|
|
23
|
+
secureEndpoint?: boolean;
|
|
24
|
+
};
|
|
25
|
+
type NodeAddRequestAgent = http.Agent & {
|
|
26
|
+
addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void;
|
|
27
|
+
};
|
|
28
|
+
type RequestSetTimeout = (
|
|
29
|
+
this: http.ClientRequest,
|
|
30
|
+
timeout: number,
|
|
31
|
+
callback?: () => void,
|
|
32
|
+
) => http.ClientRequest;
|
|
33
|
+
type NodeAgentWithOptions = http.Agent & {
|
|
34
|
+
options?: NodeAgentOptions;
|
|
35
|
+
};
|
|
36
|
+
type NodeProxyAgentOptions = NodeAgentOptions & {
|
|
37
|
+
defaultProtocol?: "http" | "https";
|
|
38
|
+
getProxyForUrl: (
|
|
39
|
+
url: string,
|
|
40
|
+
surface?: ProxylineSurface,
|
|
41
|
+
request?: http.ClientRequest,
|
|
42
|
+
) => string;
|
|
43
|
+
proxyTls?: ProxylineTlsOptions;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
47
|
+
const INVALID_PROXY_TARGET_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
48
|
+
const INVALID_PROXY_TARGET_HOST_CONTROL_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
49
|
+
const nodeAgentDefaultPorts = new WeakMap<object, number>();
|
|
50
|
+
|
|
51
|
+
export const CALLER_AGENT_TLS_OPTION_KEYS = [
|
|
52
|
+
"ca",
|
|
53
|
+
"cert",
|
|
54
|
+
"ciphers",
|
|
55
|
+
"clientCertEngine",
|
|
56
|
+
"crl",
|
|
57
|
+
"dhparam",
|
|
58
|
+
"ecdhCurve",
|
|
59
|
+
"honorCipherOrder",
|
|
60
|
+
"key",
|
|
61
|
+
"maxVersion",
|
|
62
|
+
"minVersion",
|
|
63
|
+
"passphrase",
|
|
64
|
+
"pfx",
|
|
65
|
+
"rejectUnauthorized",
|
|
66
|
+
"secureOptions",
|
|
67
|
+
"secureProtocol",
|
|
68
|
+
"sessionIdContext",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
export type NodeHttpStackSnapshot = {
|
|
72
|
+
httpRequest: typeof http.request;
|
|
73
|
+
httpGet: typeof http.get;
|
|
74
|
+
httpGlobalAgent: typeof http.globalAgent;
|
|
75
|
+
httpsRequest: typeof https.request;
|
|
76
|
+
httpsGet: typeof https.get;
|
|
77
|
+
httpsGlobalAgent: typeof https.globalAgent;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function copyNodeHttpOptions(value: unknown): NodeHttpRequestOptions {
|
|
81
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
return { ...(value as NodeHttpRequestOptions) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readAgentOptions(agent: http.Agent | false | undefined): NodeAgentOptions | undefined {
|
|
88
|
+
if (agent === undefined || agent === false) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return (agent as NodeAgentWithOptions).options;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function preserveCallerAgentOptions(options: NodeHttpRequestOptions): void {
|
|
95
|
+
const agentOptions = readAgentOptions(options.agent);
|
|
96
|
+
if (agentOptions === undefined) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const key of CALLER_AGENT_TLS_OPTION_KEYS) {
|
|
100
|
+
const value = agentOptions[key];
|
|
101
|
+
if (value !== undefined && options[key as keyof NodeHttpRequestOptions] === undefined) {
|
|
102
|
+
options[key as keyof NodeHttpRequestOptions] = value as never;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function unbracketHostname(hostname: string): string {
|
|
108
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inferDestinationHostname(
|
|
112
|
+
url: string | URL | undefined,
|
|
113
|
+
options: NodeHttpRequestOptions,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
if (typeof options.hostname === "string") {
|
|
116
|
+
return unbracketHostname(options.hostname);
|
|
117
|
+
}
|
|
118
|
+
if (url !== undefined) {
|
|
119
|
+
return unbracketHostname(url instanceof URL ? url.hostname : new URL(url).hostname);
|
|
120
|
+
}
|
|
121
|
+
if (typeof options.host === "string") {
|
|
122
|
+
return unbracketHostname(splitHostPort(options.host).host);
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function preserveDestinationTlsIdentity(
|
|
128
|
+
url: string | URL | undefined,
|
|
129
|
+
options: NodeHttpRequestOptions,
|
|
130
|
+
): void {
|
|
131
|
+
if (options.servername !== undefined) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const hostname = inferDestinationHostname(url, options);
|
|
135
|
+
if (!hostname) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (net.isIP(hostname) === 0) {
|
|
139
|
+
options.servername = hostname;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function bindNodeHttpMethod<TMethod extends NodeHttpMethod>(
|
|
144
|
+
originalMethod: TMethod,
|
|
145
|
+
createAgent: NodeAgentFactory,
|
|
146
|
+
): TMethod {
|
|
147
|
+
return ((...args: unknown[]) => {
|
|
148
|
+
let url: string | URL | undefined;
|
|
149
|
+
let options: NodeHttpRequestOptions;
|
|
150
|
+
let callback: unknown;
|
|
151
|
+
const firstArg = args[0];
|
|
152
|
+
if (typeof firstArg === "string" || firstArg instanceof URL) {
|
|
153
|
+
url = firstArg;
|
|
154
|
+
if (typeof args[1] === "function") {
|
|
155
|
+
options = {};
|
|
156
|
+
callback = args[1];
|
|
157
|
+
} else {
|
|
158
|
+
options = copyNodeHttpOptions(args[1]);
|
|
159
|
+
callback = args[2];
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
options = copyNodeHttpOptions(firstArg);
|
|
163
|
+
callback = args[1];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
preserveCallerAgentOptions(options);
|
|
167
|
+
preserveDestinationTlsIdentity(url, options);
|
|
168
|
+
const agent = createAgent(options);
|
|
169
|
+
options.agent = agent;
|
|
170
|
+
delete options.createConnection;
|
|
171
|
+
if (url !== undefined) {
|
|
172
|
+
const request = originalMethod(url, options, callback as (res: http.IncomingMessage) => void);
|
|
173
|
+
request.once("close", () => {
|
|
174
|
+
agent.destroy();
|
|
175
|
+
});
|
|
176
|
+
return request;
|
|
177
|
+
}
|
|
178
|
+
const request = originalMethod(options, callback as (res: http.IncomingMessage) => void);
|
|
179
|
+
request.once("close", () => {
|
|
180
|
+
agent.destroy();
|
|
181
|
+
});
|
|
182
|
+
return request;
|
|
183
|
+
}) as TMethod;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function proxyHost(proxy: URL): string {
|
|
187
|
+
return (proxy.hostname || proxy.host).replace(/^\[|\]$/g, "");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function proxyPort(proxy: URL): number {
|
|
191
|
+
if (proxy.port) {
|
|
192
|
+
return Number(proxy.port);
|
|
193
|
+
}
|
|
194
|
+
return proxy.protocol === "https:" ? 443 : 80;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function proxyAuthorization(proxy: URL): string | undefined {
|
|
198
|
+
if (!proxy.username && !proxy.password) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const username = decodeURIComponent(proxy.username);
|
|
202
|
+
const password = decodeURIComponent(proxy.password);
|
|
203
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function proxyConnectOptions(
|
|
207
|
+
proxy: URL,
|
|
208
|
+
proxyTls: ProxylineTlsOptions | undefined,
|
|
209
|
+
): net.TcpNetConnectOpts | tls.ConnectionOptions {
|
|
210
|
+
const host = proxyHost(proxy);
|
|
211
|
+
const base = {
|
|
212
|
+
host,
|
|
213
|
+
port: proxyPort(proxy),
|
|
214
|
+
};
|
|
215
|
+
if (proxy.protocol !== "https:") {
|
|
216
|
+
return base;
|
|
217
|
+
}
|
|
218
|
+
const ca = resolveProxyTlsCa(proxyTls);
|
|
219
|
+
return {
|
|
220
|
+
...base,
|
|
221
|
+
ALPNProtocols: ["http/1.1"],
|
|
222
|
+
...(net.isIP(host) === 0 ? { servername: host } : {}),
|
|
223
|
+
...(ca !== undefined ? { ca } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function assertSupportedNodeProxyProtocol(proxy: URL): void {
|
|
228
|
+
if (proxy.protocol !== "http:" && proxy.protocol !== "https:") {
|
|
229
|
+
throw new ProxylineError(
|
|
230
|
+
"UNSUPPORTED_PROXY_PROTOCOL",
|
|
231
|
+
`Node HTTP agents support http:// and https:// proxy endpoints: ${proxy.protocol}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function requestProtocol(
|
|
237
|
+
req: http.ClientRequest,
|
|
238
|
+
options: NodeAgentRequestOptions,
|
|
239
|
+
stackProtocol: "http" | "https" | undefined,
|
|
240
|
+
): string {
|
|
241
|
+
const isWebSocket = isWebSocketRequest(req);
|
|
242
|
+
if (isSecureEndpoint(options, stackProtocol)) {
|
|
243
|
+
return isWebSocket ? "wss:" : "https:";
|
|
244
|
+
}
|
|
245
|
+
return isWebSocket ? "ws:" : "http:";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizedPort(value: unknown): number | undefined {
|
|
249
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const port = Number(value);
|
|
253
|
+
return Number.isInteger(port) && port >= 1 && port <= 65_535 ? port : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizedPositiveInteger(value: unknown): number | undefined {
|
|
257
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const integer = Number(value);
|
|
261
|
+
return Number.isInteger(integer) && integer > 0 ? integer : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function requestAuthority(options: NodeAgentRequestOptions): string {
|
|
265
|
+
const rawHost = options.hostname ?? options.host ?? "localhost";
|
|
266
|
+
const parsed = splitHostPort(String(rawHost));
|
|
267
|
+
const host = normalizeProxyTargetHost(parsed.host || "localhost");
|
|
268
|
+
const port = parsed.port ?? normalizedPort(options.port);
|
|
269
|
+
const authorityHost = net.isIPv6(host) ? `[${host}]` : host;
|
|
270
|
+
return port === undefined ? authorityHost : `${authorityHost}:${port}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function requestDestinationUrl(
|
|
274
|
+
req: http.ClientRequest,
|
|
275
|
+
options: NodeAgentRequestOptions,
|
|
276
|
+
stackProtocol: "http" | "https" | undefined,
|
|
277
|
+
): string {
|
|
278
|
+
const path = req.path.startsWith("/") ? req.path : `/${req.path}`;
|
|
279
|
+
return `${requestProtocol(req, options, stackProtocol)}//${requestAuthority(options)}${path}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function proxyForwardRequestPath(req: http.ClientRequest, options: NodeAgentRequestOptions): string {
|
|
283
|
+
if (/^(?:https?|wss?):\/\//i.test(req.path)) {
|
|
284
|
+
return new URL(req.path).href;
|
|
285
|
+
}
|
|
286
|
+
return requestDestinationUrl(req, options, undefined);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function setProxyRequestHeaders(req: http.ClientRequest, proxy: URL, keepAlive: boolean): void {
|
|
290
|
+
const authorization = proxyAuthorization(proxy);
|
|
291
|
+
if (authorization !== undefined) {
|
|
292
|
+
req.setHeader("Proxy-Authorization", authorization);
|
|
293
|
+
}
|
|
294
|
+
if (!req.hasHeader("Proxy-Connection")) {
|
|
295
|
+
req.setHeader("Proxy-Connection", keepAlive ? "Keep-Alive" : "close");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function setForwardProxyRequestPath(
|
|
300
|
+
req: http.ClientRequest & { _header?: string | null },
|
|
301
|
+
options: NodeAgentRequestOptions,
|
|
302
|
+
): void {
|
|
303
|
+
req._header = null;
|
|
304
|
+
req.path = proxyForwardRequestPath(req, options);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function connectToProxy(
|
|
308
|
+
proxy: URL,
|
|
309
|
+
proxyTls: ProxylineTlsOptions | undefined,
|
|
310
|
+
): net.Socket | tls.TLSSocket {
|
|
311
|
+
const options = proxyConnectOptions(proxy, proxyTls);
|
|
312
|
+
return proxy.protocol === "https:"
|
|
313
|
+
? tls.connect(options as tls.ConnectionOptions)
|
|
314
|
+
: net.connect(options as net.NetConnectOpts);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isWebSocketRequest(req: http.ClientRequest): boolean {
|
|
318
|
+
return String(req.getHeader("upgrade") ?? "").toLowerCase() === "websocket";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isSecureEndpoint(
|
|
322
|
+
options: NodeAgentRequestOptions,
|
|
323
|
+
stackProtocol?: "http" | "https",
|
|
324
|
+
): boolean {
|
|
325
|
+
return (
|
|
326
|
+
stackProtocol === "https" ||
|
|
327
|
+
options.secureEndpoint === true ||
|
|
328
|
+
options.protocol === "https:" ||
|
|
329
|
+
options.protocol === "wss:" ||
|
|
330
|
+
options.defaultPort === 443
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function shouldTunnelRequest(
|
|
335
|
+
req: http.ClientRequest,
|
|
336
|
+
options: NodeAgentRequestOptions,
|
|
337
|
+
stackProtocol: "http" | "https" | undefined,
|
|
338
|
+
): boolean {
|
|
339
|
+
return isSecureEndpoint(options, stackProtocol) || isWebSocketRequest(req);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function requestSurface(
|
|
343
|
+
req: http.ClientRequest,
|
|
344
|
+
options: NodeAgentRequestOptions,
|
|
345
|
+
stackProtocol: "http" | "https" | undefined,
|
|
346
|
+
): ProxylineSurface {
|
|
347
|
+
if (isWebSocketRequest(req)) {
|
|
348
|
+
return "websocket";
|
|
349
|
+
}
|
|
350
|
+
return isSecureEndpoint(options, stackProtocol) ? "node-https" : "node-http";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function splitHostPort(value: string): { host: string; port?: number } {
|
|
354
|
+
const bracketed = value.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
355
|
+
if (bracketed) {
|
|
356
|
+
return {
|
|
357
|
+
host: bracketed[1] ?? "",
|
|
358
|
+
...(bracketed[2] !== undefined ? { port: Number(bracketed[2]) } : {}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const lastColon = value.lastIndexOf(":");
|
|
362
|
+
const hasSingleColon = lastColon !== -1 && value.indexOf(":") === lastColon;
|
|
363
|
+
if (hasSingleColon) {
|
|
364
|
+
const possiblePort = value.slice(lastColon + 1);
|
|
365
|
+
if (/^\d+$/.test(possiblePort)) {
|
|
366
|
+
return { host: value.slice(0, lastColon), port: Number(possiblePort) };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { host: value };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeProxyTargetHost(host: string): string {
|
|
373
|
+
const unbracketedHost =
|
|
374
|
+
host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
375
|
+
if (net.isIP(unbracketedHost) !== 0) {
|
|
376
|
+
return unbracketedHost;
|
|
377
|
+
}
|
|
378
|
+
if (INVALID_PROXY_TARGET_HOST_DELIMITER_PATTERN.test(host)) {
|
|
379
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target host contains unsafe delimiters.");
|
|
380
|
+
}
|
|
381
|
+
if (INVALID_PROXY_TARGET_HOST_CONTROL_PATTERN.test(host)) {
|
|
382
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target host contains unsafe characters.");
|
|
383
|
+
}
|
|
384
|
+
const asciiHost = domainToASCII(host);
|
|
385
|
+
if (!asciiHost) {
|
|
386
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target host is not a valid host name.");
|
|
387
|
+
}
|
|
388
|
+
return asciiHost;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function connectTarget(options: NodeAgentRequestOptions): { host: string; port: number } {
|
|
392
|
+
const rawHost = options.hostname ?? options.host;
|
|
393
|
+
if (typeof rawHost !== "string") {
|
|
394
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target is missing host.");
|
|
395
|
+
}
|
|
396
|
+
const parsed = splitHostPort(rawHost);
|
|
397
|
+
const port = parsed.port ?? Number(options.port);
|
|
398
|
+
if (!parsed.host || !Number.isInteger(port)) {
|
|
399
|
+
throw new ProxylineError("INVALID_CONNECT_TARGET", "CONNECT target is missing host or port.");
|
|
400
|
+
}
|
|
401
|
+
const host = normalizeProxyTargetHost(parsed.host);
|
|
402
|
+
formatConnectAuthority(host, port);
|
|
403
|
+
return { host, port };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function destinationTlsConnectOptions(
|
|
407
|
+
options: NodeAgentRequestOptions,
|
|
408
|
+
socket: net.Socket,
|
|
409
|
+
): tls.ConnectionOptions {
|
|
410
|
+
const target = connectTarget(options);
|
|
411
|
+
const tlsOptions = { ...options, socket } as tls.ConnectionOptions & Record<string, unknown>;
|
|
412
|
+
tlsOptions.host = target.host;
|
|
413
|
+
delete tlsOptions.path;
|
|
414
|
+
delete tlsOptions.port;
|
|
415
|
+
delete tlsOptions.secureEndpoint;
|
|
416
|
+
delete tlsOptions.agent;
|
|
417
|
+
return tlsOptions;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
class ProxylineHttpForwardAgent extends http.Agent {
|
|
421
|
+
public readonly options: NodeAgentOptions;
|
|
422
|
+
readonly #keepAlive: boolean;
|
|
423
|
+
readonly #proxy: URL;
|
|
424
|
+
readonly #proxyTls: ProxylineTlsOptions | undefined;
|
|
425
|
+
|
|
426
|
+
public constructor(proxy: URL, options: NodeAgentOptions, proxyTls: ProxylineTlsOptions | undefined) {
|
|
427
|
+
super(options);
|
|
428
|
+
this.options = options;
|
|
429
|
+
this.#keepAlive = options.keepAlive === true;
|
|
430
|
+
this.#proxy = proxy;
|
|
431
|
+
this.#proxyTls = proxyTls;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
435
|
+
setForwardProxyRequestPath(req as http.ClientRequest & { _header?: string | null }, options);
|
|
436
|
+
setProxyRequestHeaders(req, this.#proxy, this.#keepAlive);
|
|
437
|
+
(http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
public override createConnection(
|
|
441
|
+
_options: NodeAgentRequestOptions,
|
|
442
|
+
callback?: (error: Error | null, socket: net.Socket) => void,
|
|
443
|
+
): net.Socket {
|
|
444
|
+
const socket = connectToProxy(this.#proxy, this.#proxyTls);
|
|
445
|
+
if (callback !== undefined) {
|
|
446
|
+
const onError = (error: Error): void => {
|
|
447
|
+
callback(error, socket);
|
|
448
|
+
};
|
|
449
|
+
const onConnected = (): void => {
|
|
450
|
+
socket.off("error", onError);
|
|
451
|
+
callback(null, socket);
|
|
452
|
+
};
|
|
453
|
+
socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
454
|
+
socket.once("error", onError);
|
|
455
|
+
}
|
|
456
|
+
return socket;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
class ProxylineConnectAgent extends http.Agent {
|
|
461
|
+
public readonly options: NodeAgentOptions;
|
|
462
|
+
readonly #keepAlive: boolean;
|
|
463
|
+
readonly #pendingConnectSockets = new Set<net.Socket>();
|
|
464
|
+
readonly #pendingRequests = new WeakMap<NodeAgentRequestOptions, http.ClientRequest>();
|
|
465
|
+
readonly #pendingRequestQueue: http.ClientRequest[] = [];
|
|
466
|
+
readonly #proxy: URL;
|
|
467
|
+
readonly #proxyTls: ProxylineTlsOptions | undefined;
|
|
468
|
+
|
|
469
|
+
public constructor(proxy: URL, options: NodeAgentOptions, proxyTls: ProxylineTlsOptions | undefined) {
|
|
470
|
+
super(options);
|
|
471
|
+
this.options = options;
|
|
472
|
+
this.#keepAlive = options.keepAlive === true;
|
|
473
|
+
this.#proxy = proxy;
|
|
474
|
+
this.#proxyTls = proxyTls;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
478
|
+
this.#pendingRequests.set(options, req);
|
|
479
|
+
this.#pendingRequestQueue.push(req);
|
|
480
|
+
req.once("socket", () => this.#removePendingRequest(req));
|
|
481
|
+
req.once("close", () => this.#removePendingRequest(req));
|
|
482
|
+
try {
|
|
483
|
+
(http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
this.#pendingRequests.delete(options);
|
|
486
|
+
this.#removePendingRequest(req);
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
#removePendingRequest(req: http.ClientRequest): void {
|
|
492
|
+
const index = this.#pendingRequestQueue.indexOf(req);
|
|
493
|
+
if (index !== -1) {
|
|
494
|
+
this.#pendingRequestQueue.splice(index, 1);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
public override createConnection(
|
|
499
|
+
options: NodeAgentRequestOptions,
|
|
500
|
+
callback?: (error: Error | null, socket: net.Socket) => void,
|
|
501
|
+
): net.Socket {
|
|
502
|
+
const mappedRequest = this.#pendingRequests.get(options);
|
|
503
|
+
const request = mappedRequest ?? this.#pendingRequestQueue.shift();
|
|
504
|
+
this.#pendingRequests.delete(options);
|
|
505
|
+
if (mappedRequest !== undefined) {
|
|
506
|
+
this.#removePendingRequest(mappedRequest);
|
|
507
|
+
}
|
|
508
|
+
if (callback === undefined) {
|
|
509
|
+
throw new ProxylineError("INVALID_CONNECT_CALLBACK", "CONNECT agents require an async socket callback.");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const proxySocket = connectToProxy(this.#proxy, this.#proxyTls);
|
|
513
|
+
this.#pendingConnectSockets.add(proxySocket);
|
|
514
|
+
let pendingTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
515
|
+
let settled = false;
|
|
516
|
+
let responseBuffer = Buffer.alloc(0);
|
|
517
|
+
let originalRequestSetTimeout: RequestSetTimeout | undefined;
|
|
518
|
+
let hookedRequestSetTimeout: RequestSetTimeout | undefined;
|
|
519
|
+
let tlsSocket: tls.TLSSocket | undefined;
|
|
520
|
+
|
|
521
|
+
const startPendingTimeout = (timeoutMs: number): void => {
|
|
522
|
+
if (pendingTimeout !== undefined) {
|
|
523
|
+
clearTimeout(pendingTimeout);
|
|
524
|
+
}
|
|
525
|
+
pendingTimeout = setTimeout(() => {
|
|
526
|
+
request?.emit("timeout");
|
|
527
|
+
if (!settled) {
|
|
528
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy CONNECT timed out"));
|
|
529
|
+
}
|
|
530
|
+
}, timeoutMs);
|
|
531
|
+
pendingTimeout.unref?.();
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const clearPendingTimeout = (): void => {
|
|
535
|
+
if (pendingTimeout !== undefined) {
|
|
536
|
+
clearTimeout(pendingTimeout);
|
|
537
|
+
pendingTimeout = undefined;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const restoreRequestTimeoutHook = (): void => {
|
|
542
|
+
if (
|
|
543
|
+
request !== undefined &&
|
|
544
|
+
originalRequestSetTimeout !== undefined &&
|
|
545
|
+
request.setTimeout === hookedRequestSetTimeout
|
|
546
|
+
) {
|
|
547
|
+
request.setTimeout = originalRequestSetTimeout;
|
|
548
|
+
}
|
|
549
|
+
originalRequestSetTimeout = undefined;
|
|
550
|
+
hookedRequestSetTimeout = undefined;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const cleanupProxyHandshakeListeners = (): void => {
|
|
554
|
+
proxySocket.off("data", onData);
|
|
555
|
+
proxySocket.off("error", onError);
|
|
556
|
+
proxySocket.off("end", onClosed);
|
|
557
|
+
proxySocket.off("close", onClosed);
|
|
558
|
+
proxySocket.off("connect", onConnected);
|
|
559
|
+
proxySocket.off("secureConnect", onConnected);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const cleanup = (): void => {
|
|
563
|
+
clearPendingTimeout();
|
|
564
|
+
this.#pendingConnectSockets.delete(proxySocket);
|
|
565
|
+
if (tlsSocket !== undefined) {
|
|
566
|
+
this.#pendingConnectSockets.delete(tlsSocket);
|
|
567
|
+
}
|
|
568
|
+
restoreRequestTimeoutHook();
|
|
569
|
+
cleanupProxyHandshakeListeners();
|
|
570
|
+
request?.off("abort", onRequestClosed);
|
|
571
|
+
request?.off("close", onRequestClosed);
|
|
572
|
+
request?.off("error", onRequestClosed);
|
|
573
|
+
request?.off("timeout", onRequestTimedOut);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const finish = (error: Error | null, socket: net.Socket): void => {
|
|
577
|
+
if (settled) {
|
|
578
|
+
if (error === null) {
|
|
579
|
+
socket.destroy();
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
settled = true;
|
|
584
|
+
cleanup();
|
|
585
|
+
callback(error, socket);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const fail = (error: Error): void => {
|
|
589
|
+
proxySocket.destroy();
|
|
590
|
+
finish(error, proxySocket);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const onConnected = (): void => {
|
|
594
|
+
let target: { host: string; port: number };
|
|
595
|
+
try {
|
|
596
|
+
target = connectTarget(options);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const { host, port } = target;
|
|
602
|
+
const authority = formatConnectAuthority(host, port);
|
|
603
|
+
const headers = [
|
|
604
|
+
`CONNECT ${authority} HTTP/1.1`,
|
|
605
|
+
`Host: ${authority}`,
|
|
606
|
+
`Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
|
|
607
|
+
];
|
|
608
|
+
const authorization = proxyAuthorization(this.#proxy);
|
|
609
|
+
if (authorization !== undefined) {
|
|
610
|
+
headers.push(`Proxy-Authorization: ${authorization}`);
|
|
611
|
+
}
|
|
612
|
+
proxySocket.write([...headers, "", ""].join("\r\n"));
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const onData = (chunk: Buffer): void => {
|
|
616
|
+
responseBuffer = Buffer.concat([responseBuffer, chunk]);
|
|
617
|
+
const headerEnd = responseBuffer.indexOf("\r\n\r\n");
|
|
618
|
+
if (headerEnd === -1) {
|
|
619
|
+
if (responseBuffer.length > MAX_CONNECT_RESPONSE_HEADER_BYTES) {
|
|
620
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy CONNECT response headers were too large"));
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const bodyOffset = headerEnd + 4;
|
|
625
|
+
if (bodyOffset > MAX_CONNECT_RESPONSE_HEADER_BYTES) {
|
|
626
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy CONNECT response headers were too large"));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const statusLine = responseBuffer.subarray(0, bodyOffset).toString("latin1").split("\r\n", 1)[0] ?? "";
|
|
630
|
+
if (!/^HTTP\/1\.[01] 2\d\d\b/.test(statusLine)) {
|
|
631
|
+
fail(new ProxylineError("CONNECT_FAILED", statusLine || "proxy returned an invalid CONNECT response"));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const tunneledBytes = responseBuffer.subarray(bodyOffset);
|
|
635
|
+
cleanupProxyHandshakeListeners();
|
|
636
|
+
if (tunneledBytes.length > 0) {
|
|
637
|
+
proxySocket.unshift(tunneledBytes);
|
|
638
|
+
}
|
|
639
|
+
if (!isSecureEndpoint(options)) {
|
|
640
|
+
finish(null, proxySocket);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const currentTlsSocket = tls.connect(destinationTlsConnectOptions(options, proxySocket));
|
|
644
|
+
tlsSocket = currentTlsSocket;
|
|
645
|
+
this.#pendingConnectSockets.add(currentTlsSocket);
|
|
646
|
+
const onTlsError = (error: Error): void => {
|
|
647
|
+
currentTlsSocket.off("close", onTlsClosed);
|
|
648
|
+
finish(error, currentTlsSocket);
|
|
649
|
+
};
|
|
650
|
+
const onTlsSecureConnect = (): void => {
|
|
651
|
+
currentTlsSocket.off("error", onTlsError);
|
|
652
|
+
currentTlsSocket.off("close", onTlsClosed);
|
|
653
|
+
finish(null, currentTlsSocket);
|
|
654
|
+
};
|
|
655
|
+
const onTlsClosed = (): void => {
|
|
656
|
+
finish(
|
|
657
|
+
new ProxylineError("CONNECT_FAILED", "destination TLS socket closed before secureConnect"),
|
|
658
|
+
currentTlsSocket,
|
|
659
|
+
);
|
|
660
|
+
};
|
|
661
|
+
currentTlsSocket.once("secureConnect", onTlsSecureConnect);
|
|
662
|
+
currentTlsSocket.once("error", onTlsError);
|
|
663
|
+
currentTlsSocket.once("close", onTlsClosed);
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const onError = (error: Error): void => {
|
|
667
|
+
fail(error);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const onClosed = (): void => {
|
|
671
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy socket closed before CONNECT completed"));
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const onRequestClosed = (): void => {
|
|
675
|
+
if (!settled) {
|
|
676
|
+
fail(new ProxylineError("CONNECT_FAILED", "request closed before proxy CONNECT completed"));
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const onRequestTimedOut = (): void => {
|
|
681
|
+
if (!settled) {
|
|
682
|
+
fail(new ProxylineError("CONNECT_FAILED", "proxy CONNECT timed out"));
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
if (request !== undefined) {
|
|
687
|
+
originalRequestSetTimeout = request.setTimeout;
|
|
688
|
+
hookedRequestSetTimeout = function hookedSetTimeout(timeout, callback) {
|
|
689
|
+
const result = originalRequestSetTimeout?.call(this, timeout, callback) ?? this;
|
|
690
|
+
const timeoutMs = normalizedPositiveInteger(timeout);
|
|
691
|
+
if (timeoutMs !== undefined) {
|
|
692
|
+
startPendingTimeout(timeoutMs);
|
|
693
|
+
} else {
|
|
694
|
+
clearPendingTimeout();
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
697
|
+
};
|
|
698
|
+
request.setTimeout = hookedRequestSetTimeout;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const requestTimeout = (request as { timeout?: unknown } | undefined)?.timeout;
|
|
702
|
+
const timeoutMs = normalizedPositiveInteger(options.timeout ?? requestTimeout);
|
|
703
|
+
if (timeoutMs !== undefined) {
|
|
704
|
+
startPendingTimeout(timeoutMs);
|
|
705
|
+
}
|
|
706
|
+
request?.once("abort", onRequestClosed);
|
|
707
|
+
request?.once("close", onRequestClosed);
|
|
708
|
+
request?.once("error", onRequestClosed);
|
|
709
|
+
request?.once("timeout", onRequestTimedOut);
|
|
710
|
+
|
|
711
|
+
proxySocket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
|
|
712
|
+
proxySocket.on("data", onData);
|
|
713
|
+
proxySocket.once("error", onError);
|
|
714
|
+
proxySocket.once("end", onClosed);
|
|
715
|
+
proxySocket.once("close", onClosed);
|
|
716
|
+
|
|
717
|
+
return undefined as unknown as net.Socket;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
public override destroy(): void {
|
|
721
|
+
for (const socket of this.#pendingConnectSockets) {
|
|
722
|
+
socket.destroy();
|
|
723
|
+
}
|
|
724
|
+
this.#pendingConnectSockets.clear();
|
|
725
|
+
super.destroy();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export class ProxylineNodeProxyAgent extends http.Agent {
|
|
730
|
+
public readonly options: NodeAgentOptions;
|
|
731
|
+
readonly #agents = new Map<string, NodeAddRequestAgent>();
|
|
732
|
+
readonly #defaultProtocol: "http" | "https";
|
|
733
|
+
readonly #getProxyForUrl: (
|
|
734
|
+
url: string,
|
|
735
|
+
surface?: ProxylineSurface,
|
|
736
|
+
request?: http.ClientRequest,
|
|
737
|
+
) => string;
|
|
738
|
+
readonly #httpAgent: NodeAddRequestAgent;
|
|
739
|
+
readonly #httpsAgent: NodeAddRequestAgent;
|
|
740
|
+
readonly #proxyTls: ProxylineTlsOptions | undefined;
|
|
741
|
+
|
|
742
|
+
public constructor(options: NodeProxyAgentOptions) {
|
|
743
|
+
const { defaultProtocol = "http", getProxyForUrl, proxyTls, ...agentOptions } = options;
|
|
744
|
+
super(agentOptions);
|
|
745
|
+
if (nodeAgentDefaultPorts.get(this) === 80) {
|
|
746
|
+
nodeAgentDefaultPorts.delete(this);
|
|
747
|
+
}
|
|
748
|
+
this.options = agentOptions;
|
|
749
|
+
this.#defaultProtocol = defaultProtocol;
|
|
750
|
+
this.#getProxyForUrl = getProxyForUrl;
|
|
751
|
+
this.#proxyTls = proxyTls;
|
|
752
|
+
this.#httpAgent = new http.Agent(agentOptions) as NodeAddRequestAgent;
|
|
753
|
+
this.#httpsAgent = new https.Agent(agentOptions) as unknown as NodeAddRequestAgent;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
public get defaultPort(): number {
|
|
757
|
+
const stackProtocol = this.#callStackProtocol();
|
|
758
|
+
return nodeAgentDefaultPorts.get(this) ??
|
|
759
|
+
((stackProtocol ?? this.#defaultProtocol) === "https" ? 443 : 80);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
public set defaultPort(value: number) {
|
|
763
|
+
nodeAgentDefaultPorts.set(this, value);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
public get protocol(): string {
|
|
767
|
+
return `${this.#callStackProtocol() ?? this.#defaultProtocol}:`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
public set protocol(_value: string) {
|
|
771
|
+
// Node's http.Agent constructor assigns this, but this wrapper is dual-use.
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
public getProxyForUrl(url: string, request?: http.ClientRequest): string {
|
|
775
|
+
return this.#getProxyForUrl(url, undefined, request);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#callStackProtocol(): "http" | "https" | undefined {
|
|
779
|
+
const originalStackTraceLimit = Error.stackTraceLimit;
|
|
780
|
+
const errorConstructor = Error as unknown as { prepareStackTrace?: unknown };
|
|
781
|
+
const originalPrepareStackTrace = errorConstructor.prepareStackTrace;
|
|
782
|
+
if (typeof originalStackTraceLimit !== "number" || originalStackTraceLimit < 20) {
|
|
783
|
+
// Node reads agent.protocol/defaultPort before addRequest, so this is the only caller signal.
|
|
784
|
+
Error.stackTraceLimit = 20;
|
|
785
|
+
}
|
|
786
|
+
let stack: string | undefined;
|
|
787
|
+
try {
|
|
788
|
+
delete errorConstructor.prepareStackTrace;
|
|
789
|
+
stack = new Error().stack;
|
|
790
|
+
} finally {
|
|
791
|
+
if (originalPrepareStackTrace === undefined) {
|
|
792
|
+
delete errorConstructor.prepareStackTrace;
|
|
793
|
+
} else {
|
|
794
|
+
errorConstructor.prepareStackTrace = originalPrepareStackTrace;
|
|
795
|
+
}
|
|
796
|
+
Error.stackTraceLimit = originalStackTraceLimit;
|
|
797
|
+
}
|
|
798
|
+
if (typeof stack !== "string") {
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
for (const line of stack.split("\n")) {
|
|
802
|
+
if (line.includes("node:https:")) {
|
|
803
|
+
return "https";
|
|
804
|
+
}
|
|
805
|
+
if (line.includes("node:http:")) {
|
|
806
|
+
return "http";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
|
|
813
|
+
const stackProtocol = this.#callStackProtocol();
|
|
814
|
+
const agentOptions =
|
|
815
|
+
stackProtocol === "https" && options.secureEndpoint !== true
|
|
816
|
+
? { ...options, secureEndpoint: true }
|
|
817
|
+
: options;
|
|
818
|
+
const url = requestDestinationUrl(req, agentOptions, stackProtocol);
|
|
819
|
+
const surface = requestSurface(req, agentOptions, stackProtocol);
|
|
820
|
+
const proxy = this.#getProxyForUrl(url, surface, req);
|
|
821
|
+
if (!proxy) {
|
|
822
|
+
(isSecureEndpoint(agentOptions, stackProtocol) ? this.#httpsAgent : this.#httpAgent)
|
|
823
|
+
.addRequest(req, agentOptions);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const proxyUrl = new URL(proxy);
|
|
827
|
+
assertSupportedNodeProxyProtocol(proxyUrl);
|
|
828
|
+
const tunnel = shouldTunnelRequest(req, agentOptions, stackProtocol);
|
|
829
|
+
const key = `${tunnel ? "connect" : "forward"}:${proxyUrl.href}`;
|
|
830
|
+
let agent = this.#agents.get(key);
|
|
831
|
+
if (agent === undefined) {
|
|
832
|
+
const newAgent = tunnel
|
|
833
|
+
? new ProxylineConnectAgent(proxyUrl, this.options, this.#proxyTls)
|
|
834
|
+
: new ProxylineHttpForwardAgent(proxyUrl, this.options, this.#proxyTls);
|
|
835
|
+
agent = newAgent;
|
|
836
|
+
this.#agents.set(key, agent);
|
|
837
|
+
}
|
|
838
|
+
agent.addRequest(req, agentOptions);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
public override destroy(): void {
|
|
842
|
+
for (const agent of this.#agents.values()) {
|
|
843
|
+
agent.destroy();
|
|
844
|
+
}
|
|
845
|
+
this.#agents.clear();
|
|
846
|
+
this.#httpAgent.destroy();
|
|
847
|
+
this.#httpsAgent.destroy();
|
|
848
|
+
super.destroy();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function createNodeProxyAgent(
|
|
853
|
+
resolver: ProxyResolver,
|
|
854
|
+
proxyCa: string | undefined,
|
|
855
|
+
defaultProtocol: "http" | "https" = "http",
|
|
856
|
+
): ProxylineNodeProxyAgent {
|
|
857
|
+
return new ProxylineNodeProxyAgent({
|
|
858
|
+
defaultProtocol,
|
|
859
|
+
getProxyForUrl: resolver.getProxyForUrl,
|
|
860
|
+
...(proxyCa !== undefined ? { proxyTls: { ca: proxyCa } } : {}),
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export function createDirectNodeAgent(): ProxylineNodeProxyAgent {
|
|
865
|
+
return new ProxylineNodeProxyAgent({
|
|
866
|
+
getProxyForUrl: () => "",
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export type AmbientNodeProxyAgentOptions = {
|
|
871
|
+
env?: ProxyEnvSnapshot;
|
|
872
|
+
protocol?: "http" | "https";
|
|
873
|
+
proxyTls?: ProxylineTlsOptions;
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
function ambientProbeUrl(protocol: "http" | "https"): string {
|
|
877
|
+
return `${protocol}://proxyline.invalid/`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export function hasAmbientNodeProxyConfigured(
|
|
881
|
+
options: AmbientNodeProxyAgentOptions = {},
|
|
882
|
+
): boolean {
|
|
883
|
+
const env = options.env ?? readProxyEnv();
|
|
884
|
+
const protocol = options.protocol ?? "https";
|
|
885
|
+
return resolveAmbientProxyForUrl(ambientProbeUrl(protocol), env) !== undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function createAmbientNodeProxyAgent(
|
|
889
|
+
options: AmbientNodeProxyAgentOptions = {},
|
|
890
|
+
): ProxylineNodeProxyAgent | undefined {
|
|
891
|
+
const env = options.env ?? readProxyEnv();
|
|
892
|
+
const protocol = options.protocol ?? "https";
|
|
893
|
+
if (resolveAmbientProxyForUrl(ambientProbeUrl(protocol), env) === undefined) {
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
return new ProxylineNodeProxyAgent({
|
|
897
|
+
defaultProtocol: protocol,
|
|
898
|
+
getProxyForUrl: (url) => resolveAmbientProxyForUrl(url, env) ?? "",
|
|
899
|
+
...(options.proxyTls !== undefined ? { proxyTls: options.proxyTls } : {}),
|
|
900
|
+
});
|
|
901
|
+
}
|