@openclaw/proxyline 0.3.3 → 0.3.5
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 +11 -0
- package/README.md +5 -1
- package/dist/connect.d.ts +1 -0
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +19 -5
- package/dist/node-http.d.ts.map +1 -1
- package/dist/node-http.js +15 -18
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +44 -6
- package/dist/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +9 -0
- package/docs/api-reference.md +2 -0
- package/docs/surfaces.md +2 -0
- package/package.json +15 -9
- package/scripts/prepack-build.mjs +6 -1
- package/src/connect.ts +20 -5
- package/src/node-http.ts +15 -18
- package/src/runtime.ts +48 -6
- package/src/shared.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5 - 2026-08-01
|
|
4
|
+
|
|
5
|
+
- Fixed malformed percent-encoding in proxy credentials to fail through CONNECT promises and Node request errors with `INVALID_PROXY_USERINFO`. Thanks @SebTardif.
|
|
6
|
+
- Updated Undici, WebSocket and TypeScript tooling, pnpm, and pinned GitHub Actions to their latest stable releases.
|
|
7
|
+
|
|
8
|
+
## 0.3.4 - 2026-07-20
|
|
9
|
+
|
|
10
|
+
- Added `AbortSignal` cancellation to explicit CONNECT tunnels, including active socket cleanup.
|
|
11
|
+
- Fixed managed HTTPS proxy connections to IPv6 literal endpoints so equivalent compressed and expanded certificate addresses validate without sending IP-literal SNI.
|
|
12
|
+
- Updated Undici to 8.5.0 and raised the compatible peer floor to pick up current security fixes; refreshed pnpm, Node types, and transitive esbuild tooling.
|
|
13
|
+
|
|
3
14
|
## 0.3.3 - 2026-05-18
|
|
4
15
|
|
|
5
16
|
- Fixed managed `globalThis.fetch` so later Undici global dispatcher replacement cannot bypass the active Proxyline dispatcher. Thanks @jesse-merhi.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 🌐 Proxyline
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/@openclaw/proxyline)
|
|
4
6
|
[](https://nodejs.org/)
|
|
5
7
|
[](./LICENSE)
|
|
@@ -31,7 +33,7 @@ pnpm add @openclaw/proxyline
|
|
|
31
33
|
npm install @openclaw/proxyline
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Requires Node 22.19.0+ and a host `undici` dependency compatible with `>=8.
|
|
36
|
+
Requires Node 22.19.0+ and a host `undici` dependency compatible with `>=8.5.0 <9`.
|
|
35
37
|
|
|
36
38
|
## Quick start
|
|
37
39
|
|
|
@@ -76,12 +78,14 @@ const socket = new WebSocket("wss://events.example.com/", {
|
|
|
76
78
|
```ts
|
|
77
79
|
import { openProxyConnectTunnel } from "@openclaw/proxyline";
|
|
78
80
|
|
|
81
|
+
const controller = new AbortController();
|
|
79
82
|
const socket = await openProxyConnectTunnel({
|
|
80
83
|
proxyUrl: "https://proxy.corp.example:8443",
|
|
81
84
|
proxyTls: { caFile: "/etc/proxy-ca.pem" },
|
|
82
85
|
targetHost: "api.example.com",
|
|
83
86
|
targetPort: 443,
|
|
84
87
|
timeoutMs: 2_000,
|
|
88
|
+
signal: controller.signal,
|
|
85
89
|
});
|
|
86
90
|
```
|
|
87
91
|
|
package/dist/connect.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
7
7
|
targetHost: string;
|
|
8
8
|
targetPort: number;
|
|
9
9
|
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
10
11
|
}>;
|
|
11
12
|
type ProxySocket = net.Socket | tls.TLSSocket;
|
|
12
13
|
export declare function formatConnectAuthority(targetHost: string, targetPort: number): string;
|
package/dist/connect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAgD,KAAK,mBAAmB,EAAqC,MAAM,aAAa,CAAC;AAExI,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC;IACvB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC,CAAC;AAMH,KAAK,WAAW,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC;AAsB9C,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAqBrF;AAkDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,WAAW,CAAC,CA4HtB"}
|
package/dist/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import tls from "node:tls";
|
|
3
|
-
import { ProxylineError, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
|
|
3
|
+
import { ProxylineError, decodeProxyUserinfoComponent, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
|
|
4
4
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
5
5
|
const INVALID_CONNECT_AUTHORITY_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
6
6
|
const INVALID_CONNECT_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
@@ -17,8 +17,8 @@ function resolveProxyAuthorization(proxy) {
|
|
|
17
17
|
if (!proxy.username && !proxy.password) {
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
|
-
const username =
|
|
21
|
-
const password =
|
|
20
|
+
const username = decodeProxyUserinfoComponent(proxy.username);
|
|
21
|
+
const password = decodeProxyUserinfoComponent(proxy.password);
|
|
22
22
|
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
23
23
|
}
|
|
24
24
|
export function formatConnectAuthority(targetHost, targetPort) {
|
|
@@ -100,6 +100,7 @@ export async function openProxyConnectTunnel(options) {
|
|
|
100
100
|
socket?.off("close", onClosed);
|
|
101
101
|
socket?.off("connect", onConnected);
|
|
102
102
|
socket?.off("secureConnect", onConnected);
|
|
103
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
103
104
|
};
|
|
104
105
|
const fail = (error) => {
|
|
105
106
|
if (settled) {
|
|
@@ -108,7 +109,7 @@ export async function openProxyConnectTunnel(options) {
|
|
|
108
109
|
settled = true;
|
|
109
110
|
cleanup();
|
|
110
111
|
socket?.destroy();
|
|
111
|
-
reject(failConnect(proxy, error));
|
|
112
|
+
reject(error instanceof ProxylineError ? error : failConnect(proxy, error));
|
|
112
113
|
};
|
|
113
114
|
const succeed = (connectedSocket, tunneledBytes) => {
|
|
114
115
|
if (settled) {
|
|
@@ -127,7 +128,12 @@ export async function openProxyConnectTunnel(options) {
|
|
|
127
128
|
fail(new Error("proxy socket missing after connect"));
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
try {
|
|
132
|
+
writeConnectRequest(socket, proxy, target);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
fail(error);
|
|
136
|
+
}
|
|
131
137
|
};
|
|
132
138
|
const onData = (chunk) => {
|
|
133
139
|
responseBuffer = Buffer.concat([responseBuffer, chunk]);
|
|
@@ -162,7 +168,15 @@ export async function openProxyConnectTunnel(options) {
|
|
|
162
168
|
const onClosed = () => {
|
|
163
169
|
fail(new Error("proxy socket closed before CONNECT response"));
|
|
164
170
|
};
|
|
171
|
+
const onAbort = () => {
|
|
172
|
+
fail(options.signal?.reason ?? new Error("proxy CONNECT aborted"));
|
|
173
|
+
};
|
|
165
174
|
try {
|
|
175
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
176
|
+
if (options.signal?.aborted) {
|
|
177
|
+
onAbort();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
166
180
|
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
167
181
|
timeout = setTimeout(() => {
|
|
168
182
|
fail(new Error(`proxy CONNECT timed out after ${Math.trunc(options.timeoutMs ?? 0)}ms`));
|
package/dist/node-http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,MAAM,YAAY,CAAC;AAI/B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAElB,OAAO,
|
|
1
|
+
{"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"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"}
|
package/dist/node-http.js
CHANGED
|
@@ -5,7 +5,7 @@ import tls from "node:tls";
|
|
|
5
5
|
import { domainToASCII } from "node:url";
|
|
6
6
|
import { readProxyEnv, resolveAmbientProxyForUrl, } from "./env.js";
|
|
7
7
|
import { formatConnectAuthority } from "./connect.js";
|
|
8
|
-
import { ProxylineError, resolveProxyTlsCa } from "./shared.js";
|
|
8
|
+
import { ProxylineError, decodeProxyUserinfoComponent, resolveProxyTlsCa } from "./shared.js";
|
|
9
9
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
10
10
|
const INVALID_PROXY_TARGET_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
|
|
11
11
|
const INVALID_PROXY_TARGET_HOST_CONTROL_PATTERN = /[\u0000-\u0020\u007f]/;
|
|
@@ -133,8 +133,8 @@ function proxyAuthorization(proxy) {
|
|
|
133
133
|
if (!proxy.username && !proxy.password) {
|
|
134
134
|
return undefined;
|
|
135
135
|
}
|
|
136
|
-
const username =
|
|
137
|
-
const password =
|
|
136
|
+
const username = decodeProxyUserinfoComponent(proxy.username);
|
|
137
|
+
const password = decodeProxyUserinfoComponent(proxy.password);
|
|
138
138
|
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
139
139
|
}
|
|
140
140
|
function proxyConnectOptions(proxy, proxyTls) {
|
|
@@ -445,26 +445,23 @@ class ProxylineConnectAgent extends http.Agent {
|
|
|
445
445
|
finish(error, proxySocket);
|
|
446
446
|
};
|
|
447
447
|
const onConnected = () => {
|
|
448
|
-
let target;
|
|
449
448
|
try {
|
|
450
|
-
|
|
449
|
+
const { host, port } = connectTarget(options);
|
|
450
|
+
const authority = formatConnectAuthority(host, port);
|
|
451
|
+
const headers = [
|
|
452
|
+
`CONNECT ${authority} HTTP/1.1`,
|
|
453
|
+
`Host: ${authority}`,
|
|
454
|
+
`Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
|
|
455
|
+
];
|
|
456
|
+
const authorization = proxyAuthorization(this.#proxy);
|
|
457
|
+
if (authorization !== undefined) {
|
|
458
|
+
headers.push(`Proxy-Authorization: ${authorization}`);
|
|
459
|
+
}
|
|
460
|
+
proxySocket.write([...headers, "", ""].join("\r\n"));
|
|
451
461
|
}
|
|
452
462
|
catch (error) {
|
|
453
463
|
fail(error instanceof Error ? error : new Error(String(error)));
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
const { host, port } = target;
|
|
457
|
-
const authority = formatConnectAuthority(host, port);
|
|
458
|
-
const headers = [
|
|
459
|
-
`CONNECT ${authority} HTTP/1.1`,
|
|
460
|
-
`Host: ${authority}`,
|
|
461
|
-
`Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
|
|
462
|
-
];
|
|
463
|
-
const authorization = proxyAuthorization(this.#proxy);
|
|
464
|
-
if (authorization !== undefined) {
|
|
465
|
-
headers.push(`Proxy-Authorization: ${authorization}`);
|
|
466
464
|
}
|
|
467
|
-
proxySocket.write([...headers, "", ""].join("\r\n"));
|
|
468
465
|
};
|
|
469
466
|
const onData = (chunk) => {
|
|
470
467
|
responseBuffer = Buffer.concat([responseBuffer, chunk]);
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AAs0BpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,CAAC"}
|
package/dist/runtime.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { X509Certificate } from "node:crypto";
|
|
4
5
|
import net from "node:net";
|
|
6
|
+
import tls from "node:tls";
|
|
5
7
|
import { Agent as UndiciAgent, Dispatcher, FormData as UndiciFormData, Headers as UndiciHeaders, Pool as UndiciPool, Request as UndiciRequest, Response as UndiciResponse, errors as undiciErrors, fetch as undiciFetch, getGlobalDispatcher, ProxyAgent as UndiciProxyAgent, setGlobalDispatcher, } from "undici";
|
|
6
8
|
import { createAmbientProxyResolver, EMPTY_PROXY_ENV, resolveAmbientProxyForUrl, readProxyEnv, } from "./env.js";
|
|
7
9
|
import { bindNodeHttpMethod, createDirectNodeAgent, createNodeProxyAgent, } from "./node-http.js";
|
|
@@ -301,6 +303,21 @@ function stripIpServernameFromConnect(connect) {
|
|
|
301
303
|
}
|
|
302
304
|
return (options, callback) => connect(stripIpServernameFromConnectOptions(options), callback);
|
|
303
305
|
}
|
|
306
|
+
const checkProxyServerIdentity = (hostname, certificate) => {
|
|
307
|
+
const normalizedHostname = hostname.replace(/^\[|\]$/g, "");
|
|
308
|
+
if (net.isIP(normalizedHostname) === 6) {
|
|
309
|
+
try {
|
|
310
|
+
// Compare binary addresses so compressed and expanded IPv6 SAN forms remain equivalent.
|
|
311
|
+
if (new X509Certificate(certificate.raw).checkIP(normalizedHostname) !== undefined) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Let Node return its canonical certificate validation error below.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return tls.checkServerIdentity(normalizedHostname, certificate);
|
|
320
|
+
};
|
|
304
321
|
function createProxyClientFactory() {
|
|
305
322
|
return (origin, options) => {
|
|
306
323
|
const clientOptions = isObjectRecord(options)
|
|
@@ -310,13 +327,32 @@ function createProxyClientFactory() {
|
|
|
310
327
|
};
|
|
311
328
|
}
|
|
312
329
|
function createUndiciProxyAgent(proxyUrl, options) {
|
|
330
|
+
const proxyHostname = new URL(proxyUrl).hostname.replace(/^\[|\]$/g, "");
|
|
331
|
+
const proxyTls = options.proxyCa !== undefined || net.isIP(proxyHostname) === 6
|
|
332
|
+
? {
|
|
333
|
+
...(options.proxyCa !== undefined ? { ca: options.proxyCa } : {}),
|
|
334
|
+
...(net.isIP(proxyHostname) === 6 ? { checkServerIdentity: checkProxyServerIdentity } : {}),
|
|
335
|
+
}
|
|
336
|
+
: undefined;
|
|
337
|
+
const dispatcherFactory = createProxyClientFactory();
|
|
313
338
|
return new UndiciProxyAgent({
|
|
314
339
|
...resolveUndiciBaseOptions(options.undici),
|
|
315
340
|
uri: proxyUrl,
|
|
316
|
-
clientFactory:
|
|
317
|
-
|
|
341
|
+
clientFactory: dispatcherFactory,
|
|
342
|
+
factory: dispatcherFactory,
|
|
343
|
+
...(proxyTls !== undefined ? { proxyTls } : {}),
|
|
318
344
|
});
|
|
319
345
|
}
|
|
346
|
+
function normalizeUndiciDispatchOptions(options) {
|
|
347
|
+
if (options.origin === undefined || !/^https?:\/\//i.test(options.path)) {
|
|
348
|
+
return options;
|
|
349
|
+
}
|
|
350
|
+
const pathUrl = new URL(options.path);
|
|
351
|
+
return {
|
|
352
|
+
...options,
|
|
353
|
+
path: `${pathUrl.pathname}${pathUrl.search}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
320
356
|
function createUndiciProxyDispatcher(options, dispatcherOptions) {
|
|
321
357
|
if (options.mode === "ambient") {
|
|
322
358
|
if (!options.active) {
|
|
@@ -355,10 +391,11 @@ class ManagedUndiciDispatcher extends Dispatcher {
|
|
|
355
391
|
if (this.#closedError !== undefined) {
|
|
356
392
|
return reportClosedDispatchError(handler, this.#closedError);
|
|
357
393
|
}
|
|
358
|
-
const
|
|
394
|
+
const normalizedOptions = normalizeUndiciDispatchOptions(options);
|
|
395
|
+
const url = resolveUndiciDispatchUrl(normalizedOptions);
|
|
359
396
|
const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
|
|
360
397
|
const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
|
|
361
|
-
return dispatcher.dispatch(
|
|
398
|
+
return dispatcher.dispatch(normalizedOptions, handler);
|
|
362
399
|
}
|
|
363
400
|
close(callback) {
|
|
364
401
|
const closing = this.#closeAll();
|
|
@@ -421,10 +458,11 @@ class AmbientUndiciDispatcher extends Dispatcher {
|
|
|
421
458
|
if (this.#closedError !== undefined) {
|
|
422
459
|
return reportClosedDispatchError(handler, this.#closedError);
|
|
423
460
|
}
|
|
424
|
-
const
|
|
461
|
+
const normalizedOptions = normalizeUndiciDispatchOptions(options);
|
|
462
|
+
const url = resolveUndiciDispatchUrl(normalizedOptions);
|
|
425
463
|
const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
|
|
426
464
|
const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
|
|
427
|
-
return dispatcher.dispatch(
|
|
465
|
+
return dispatcher.dispatch(normalizedOptions, handler);
|
|
428
466
|
}
|
|
429
467
|
close(callback) {
|
|
430
468
|
const closing = this.#closeAll();
|
package/dist/shared.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare class ProxylineError extends Error {
|
|
|
6
6
|
readonly code: string;
|
|
7
7
|
constructor(code: string, message: string);
|
|
8
8
|
}
|
|
9
|
+
/** Decode proxy userinfo; invalid percent-encoding must not throw URIError mid-CONNECT. */
|
|
10
|
+
export declare function decodeProxyUserinfoComponent(value: string): string;
|
|
9
11
|
export declare function resolveProxyTlsCa(options: ProxylineTlsOptions | undefined): string | undefined;
|
|
10
12
|
export declare function formatUrl(value: string | URL): string;
|
|
11
13
|
export declare function redactProxyUrl(value: string | URL): string;
|
package/dist/shared.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B,YAAmB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAI/C;CACF;AAED,2FAA2F;AAC3F,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASlE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW9F;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAO1D"}
|
package/dist/shared.js
CHANGED
|
@@ -7,6 +7,15 @@ export class ProxylineError extends Error {
|
|
|
7
7
|
this.code = code;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
/** Decode proxy userinfo; invalid percent-encoding must not throw URIError mid-CONNECT. */
|
|
11
|
+
export function decodeProxyUserinfoComponent(value) {
|
|
12
|
+
try {
|
|
13
|
+
return decodeURIComponent(value);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new ProxylineError("INVALID_PROXY_USERINFO", "Proxy username or password contains invalid percent-encoding.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
10
19
|
export function resolveProxyTlsCa(options) {
|
|
11
20
|
if (!options) {
|
|
12
21
|
return undefined;
|
package/docs/api-reference.md
CHANGED
|
@@ -257,6 +257,7 @@ type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
257
257
|
targetHost: string;
|
|
258
258
|
targetPort: number;
|
|
259
259
|
timeoutMs?: number;
|
|
260
|
+
signal?: AbortSignal;
|
|
260
261
|
}>;
|
|
261
262
|
```
|
|
262
263
|
|
|
@@ -264,6 +265,7 @@ type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
264
265
|
- `proxyTls` — CA trust for HTTPS proxies. See [Proxy TLS](./proxy-tls.md).
|
|
265
266
|
- `targetHost` / `targetPort` — what to ask the proxy to connect to.
|
|
266
267
|
- `timeoutMs` — overall budget for the CONNECT handshake.
|
|
268
|
+
- `signal` — optional caller cancellation; aborting rejects the handshake and destroys its active proxy socket.
|
|
267
269
|
|
|
268
270
|
### `AmbientNodeProxyAgentOptions`
|
|
269
271
|
|
package/docs/surfaces.md
CHANGED
|
@@ -83,6 +83,7 @@ const socket = await openProxyConnectTunnel({
|
|
|
83
83
|
targetHost: "api.example.com",
|
|
84
84
|
targetPort: 443,
|
|
85
85
|
timeoutMs: 2_000,
|
|
86
|
+
signal: AbortSignal.timeout(2_000),
|
|
86
87
|
});
|
|
87
88
|
```
|
|
88
89
|
|
|
@@ -93,6 +94,7 @@ Properties:
|
|
|
93
94
|
- Userinfo in the `proxyUrl` becomes a `Proxy-Authorization: Basic ...` header.
|
|
94
95
|
- A bounded `16 KiB` header buffer protects against malicious or runaway proxy responses.
|
|
95
96
|
- `timeoutMs` is enforced and emits a `CONNECT_FAILED` error on expiry.
|
|
97
|
+
- `signal` aborts an in-progress handshake and destroys its active proxy socket.
|
|
96
98
|
- Bytes the proxy sends after the response headers are re-injected with `socket.unshift()` so the caller sees the full target stream.
|
|
97
99
|
- Non-2xx status lines, header overrun, premature close, and socket errors are all surfaced as `ProxylineError` with code `CONNECT_FAILED`.
|
|
98
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/proxyline",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Process-global proxy routing for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "https://github.com/openclaw/proxyline/issues"
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://proxyline.dev",
|
|
20
|
-
"packageManager": "pnpm@11.
|
|
20
|
+
"packageManager": "pnpm@11.18.0",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
@@ -53,20 +53,26 @@
|
|
|
53
53
|
"prepare": "node scripts/prepack-build.mjs",
|
|
54
54
|
"publish:dry-run": "pnpm publish --dry-run --access public",
|
|
55
55
|
"test": "pnpm build && tsx --test test/index.test.ts test/e2e.test.ts test/package.test.ts test/scripts.test.ts && tsx --test test/package-artifact.test.ts",
|
|
56
|
-
"typecheck": "pnpm build && tsc --noEmit"
|
|
56
|
+
"typecheck": "pnpm build && tsc --noEmit",
|
|
57
|
+
"check:changed": "pnpm run check",
|
|
58
|
+
"test:changed": "pnpm run test",
|
|
59
|
+
"crabbox:hydrate": "crabbox actions hydrate",
|
|
60
|
+
"crabbox:run": "crabbox run",
|
|
61
|
+
"crabbox:stop": "crabbox stop",
|
|
62
|
+
"crabbox:warmup": "crabbox warmup"
|
|
57
63
|
},
|
|
58
64
|
"devDependencies": {
|
|
59
|
-
"@types/node": "
|
|
65
|
+
"@types/node": "26.1.2",
|
|
60
66
|
"@types/ws": "8.18.1",
|
|
61
|
-
"tsx": "4.
|
|
62
|
-
"typescript": "
|
|
63
|
-
"undici": "8.
|
|
64
|
-
"ws": "8.
|
|
67
|
+
"tsx": "4.23.1",
|
|
68
|
+
"typescript": "^7.0.2",
|
|
69
|
+
"undici": "8.9.0",
|
|
70
|
+
"ws": "8.21.1"
|
|
65
71
|
},
|
|
66
72
|
"engines": {
|
|
67
73
|
"node": ">=22.19.0"
|
|
68
74
|
},
|
|
69
75
|
"peerDependencies": {
|
|
70
|
-
"undici": ">=8.
|
|
76
|
+
"undici": ">=8.5.0 <9"
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
4
6
|
|
|
5
7
|
const require = createRequire(import.meta.url);
|
|
6
8
|
|
|
7
9
|
function resolveTypeScriptCompiler() {
|
|
8
10
|
try {
|
|
9
|
-
|
|
11
|
+
const packageJsonPath = require.resolve("typescript/package.json");
|
|
12
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
13
|
+
const compiler = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.tsc;
|
|
14
|
+
return typeof compiler === "string" ? resolve(dirname(packageJsonPath), compiler) : undefined;
|
|
10
15
|
} catch {
|
|
11
16
|
return undefined;
|
|
12
17
|
}
|
package/src/connect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import tls from "node:tls";
|
|
3
|
-
import { ProxylineError, type ProxylineTlsOptions, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
|
|
3
|
+
import { ProxylineError, decodeProxyUserinfoComponent, type ProxylineTlsOptions, redactProxyUrl, resolveProxyTlsCa } from "./shared.js";
|
|
4
4
|
|
|
5
5
|
export type OpenProxyConnectTunnelOptions = Readonly<{
|
|
6
6
|
proxyUrl: string | URL;
|
|
@@ -8,6 +8,7 @@ export type OpenProxyConnectTunnelOptions = Readonly<{
|
|
|
8
8
|
targetHost: string;
|
|
9
9
|
targetPort: number;
|
|
10
10
|
timeoutMs?: number;
|
|
11
|
+
signal?: AbortSignal;
|
|
11
12
|
}>;
|
|
12
13
|
|
|
13
14
|
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
|
|
@@ -31,8 +32,8 @@ function resolveProxyAuthorization(proxy: URL): string | undefined {
|
|
|
31
32
|
if (!proxy.username && !proxy.password) {
|
|
32
33
|
return undefined;
|
|
33
34
|
}
|
|
34
|
-
const username =
|
|
35
|
-
const password =
|
|
35
|
+
const username = decodeProxyUserinfoComponent(proxy.username);
|
|
36
|
+
const password = decodeProxyUserinfoComponent(proxy.password);
|
|
36
37
|
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -131,6 +132,7 @@ export async function openProxyConnectTunnel(
|
|
|
131
132
|
socket?.off("close", onClosed);
|
|
132
133
|
socket?.off("connect", onConnected);
|
|
133
134
|
socket?.off("secureConnect", onConnected);
|
|
135
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
134
136
|
};
|
|
135
137
|
|
|
136
138
|
const fail = (error: unknown): void => {
|
|
@@ -140,7 +142,7 @@ export async function openProxyConnectTunnel(
|
|
|
140
142
|
settled = true;
|
|
141
143
|
cleanup();
|
|
142
144
|
socket?.destroy();
|
|
143
|
-
reject(failConnect(proxy, error));
|
|
145
|
+
reject(error instanceof ProxylineError ? error : failConnect(proxy, error));
|
|
144
146
|
};
|
|
145
147
|
|
|
146
148
|
const succeed = (connectedSocket: ProxySocket, tunneledBytes: Buffer | undefined): void => {
|
|
@@ -161,7 +163,11 @@ export async function openProxyConnectTunnel(
|
|
|
161
163
|
fail(new Error("proxy socket missing after connect"));
|
|
162
164
|
return;
|
|
163
165
|
}
|
|
164
|
-
|
|
166
|
+
try {
|
|
167
|
+
writeConnectRequest(socket, proxy, target);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
fail(error);
|
|
170
|
+
}
|
|
165
171
|
};
|
|
166
172
|
|
|
167
173
|
const onData = (chunk: Buffer): void => {
|
|
@@ -203,7 +209,16 @@ export async function openProxyConnectTunnel(
|
|
|
203
209
|
fail(new Error("proxy socket closed before CONNECT response"));
|
|
204
210
|
};
|
|
205
211
|
|
|
212
|
+
const onAbort = (): void => {
|
|
213
|
+
fail(options.signal?.reason ?? new Error("proxy CONNECT aborted"));
|
|
214
|
+
};
|
|
215
|
+
|
|
206
216
|
try {
|
|
217
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
218
|
+
if (options.signal?.aborted) {
|
|
219
|
+
onAbort();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
207
222
|
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
208
223
|
timeout = setTimeout(() => {
|
|
209
224
|
fail(new Error(`proxy CONNECT timed out after ${Math.trunc(options.timeoutMs ?? 0)}ms`));
|
package/src/node-http.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ProxyEnvSnapshot,
|
|
10
10
|
} from "./env.js";
|
|
11
11
|
import { formatConnectAuthority } from "./connect.js";
|
|
12
|
-
import { ProxylineError, resolveProxyTlsCa, type ProxylineTlsOptions } from "./shared.js";
|
|
12
|
+
import { ProxylineError, decodeProxyUserinfoComponent, resolveProxyTlsCa, type ProxylineTlsOptions } from "./shared.js";
|
|
13
13
|
import type { ProxylineSurface, ProxyResolver } from "./types.js";
|
|
14
14
|
|
|
15
15
|
export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions & {
|
|
@@ -198,8 +198,8 @@ function proxyAuthorization(proxy: URL): string | undefined {
|
|
|
198
198
|
if (!proxy.username && !proxy.password) {
|
|
199
199
|
return undefined;
|
|
200
200
|
}
|
|
201
|
-
const username =
|
|
202
|
-
const password =
|
|
201
|
+
const username = decodeProxyUserinfoComponent(proxy.username);
|
|
202
|
+
const password = decodeProxyUserinfoComponent(proxy.password);
|
|
203
203
|
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -591,25 +591,22 @@ class ProxylineConnectAgent extends http.Agent {
|
|
|
591
591
|
};
|
|
592
592
|
|
|
593
593
|
const onConnected = (): void => {
|
|
594
|
-
let target: { host: string; port: number };
|
|
595
594
|
try {
|
|
596
|
-
|
|
595
|
+
const { host, port } = connectTarget(options);
|
|
596
|
+
const authority = formatConnectAuthority(host, port);
|
|
597
|
+
const headers = [
|
|
598
|
+
`CONNECT ${authority} HTTP/1.1`,
|
|
599
|
+
`Host: ${authority}`,
|
|
600
|
+
`Proxy-Connection: ${this.#keepAlive ? "Keep-Alive" : "close"}`,
|
|
601
|
+
];
|
|
602
|
+
const authorization = proxyAuthorization(this.#proxy);
|
|
603
|
+
if (authorization !== undefined) {
|
|
604
|
+
headers.push(`Proxy-Authorization: ${authorization}`);
|
|
605
|
+
}
|
|
606
|
+
proxySocket.write([...headers, "", ""].join("\r\n"));
|
|
597
607
|
} catch (error) {
|
|
598
608
|
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
609
|
}
|
|
612
|
-
proxySocket.write([...headers, "", ""].join("\r\n"));
|
|
613
610
|
};
|
|
614
611
|
|
|
615
612
|
const onData = (chunk: Buffer): void => {
|
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { X509Certificate } from "node:crypto";
|
|
4
5
|
import net from "node:net";
|
|
6
|
+
import tls from "node:tls";
|
|
5
7
|
import {
|
|
6
8
|
Agent as UndiciAgent,
|
|
7
9
|
Dispatcher,
|
|
@@ -483,6 +485,21 @@ function stripIpServernameFromConnect(connect: unknown): unknown {
|
|
|
483
485
|
(connect as UnknownFunction)(stripIpServernameFromConnectOptions(options), callback);
|
|
484
486
|
}
|
|
485
487
|
|
|
488
|
+
const checkProxyServerIdentity: typeof tls.checkServerIdentity = (hostname, certificate) => {
|
|
489
|
+
const normalizedHostname = hostname.replace(/^\[|\]$/g, "");
|
|
490
|
+
if (net.isIP(normalizedHostname) === 6) {
|
|
491
|
+
try {
|
|
492
|
+
// Compare binary addresses so compressed and expanded IPv6 SAN forms remain equivalent.
|
|
493
|
+
if (new X509Certificate(certificate.raw).checkIP(normalizedHostname) !== undefined) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
// Let Node return its canonical certificate validation error below.
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return tls.checkServerIdentity(normalizedHostname, certificate);
|
|
501
|
+
};
|
|
502
|
+
|
|
486
503
|
function createProxyClientFactory(): UndiciProxyClientFactory {
|
|
487
504
|
return (origin: URL, options: object): Dispatcher => {
|
|
488
505
|
const clientOptions = isObjectRecord(options)
|
|
@@ -499,14 +516,37 @@ function createUndiciProxyAgent(
|
|
|
499
516
|
proxyUrl: string,
|
|
500
517
|
options: UndiciDispatcherOptions,
|
|
501
518
|
): UndiciProxyAgent {
|
|
519
|
+
const proxyHostname = new URL(proxyUrl).hostname.replace(/^\[|\]$/g, "");
|
|
520
|
+
const proxyTls =
|
|
521
|
+
options.proxyCa !== undefined || net.isIP(proxyHostname) === 6
|
|
522
|
+
? {
|
|
523
|
+
...(options.proxyCa !== undefined ? { ca: options.proxyCa } : {}),
|
|
524
|
+
...(net.isIP(proxyHostname) === 6 ? { checkServerIdentity: checkProxyServerIdentity } : {}),
|
|
525
|
+
}
|
|
526
|
+
: undefined;
|
|
527
|
+
const dispatcherFactory = createProxyClientFactory();
|
|
502
528
|
return new UndiciProxyAgent({
|
|
503
529
|
...resolveUndiciBaseOptions(options.undici),
|
|
504
530
|
uri: proxyUrl,
|
|
505
|
-
clientFactory:
|
|
506
|
-
|
|
531
|
+
clientFactory: dispatcherFactory,
|
|
532
|
+
factory: dispatcherFactory,
|
|
533
|
+
...(proxyTls !== undefined ? { proxyTls } : {}),
|
|
507
534
|
} as ConstructorParameters<typeof UndiciProxyAgent>[0]);
|
|
508
535
|
}
|
|
509
536
|
|
|
537
|
+
function normalizeUndiciDispatchOptions(
|
|
538
|
+
options: Dispatcher.DispatchOptions,
|
|
539
|
+
): Dispatcher.DispatchOptions {
|
|
540
|
+
if (options.origin === undefined || !/^https?:\/\//i.test(options.path)) {
|
|
541
|
+
return options;
|
|
542
|
+
}
|
|
543
|
+
const pathUrl = new URL(options.path);
|
|
544
|
+
return {
|
|
545
|
+
...options,
|
|
546
|
+
path: `${pathUrl.pathname}${pathUrl.search}`,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
510
550
|
function createUndiciProxyDispatcher(
|
|
511
551
|
options:
|
|
512
552
|
| { mode: "managed"; resolver: ProxyResolver }
|
|
@@ -560,10 +600,11 @@ class ManagedUndiciDispatcher extends Dispatcher {
|
|
|
560
600
|
if (this.#closedError !== undefined) {
|
|
561
601
|
return reportClosedDispatchError(handler, this.#closedError);
|
|
562
602
|
}
|
|
563
|
-
const
|
|
603
|
+
const normalizedOptions = normalizeUndiciDispatchOptions(options);
|
|
604
|
+
const url = resolveUndiciDispatchUrl(normalizedOptions);
|
|
564
605
|
const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
|
|
565
606
|
const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
|
|
566
|
-
return dispatcher.dispatch(
|
|
607
|
+
return dispatcher.dispatch(normalizedOptions, handler);
|
|
567
608
|
}
|
|
568
609
|
|
|
569
610
|
public override close(callback: () => void): void;
|
|
@@ -646,10 +687,11 @@ class AmbientUndiciDispatcher extends Dispatcher {
|
|
|
646
687
|
if (this.#closedError !== undefined) {
|
|
647
688
|
return reportClosedDispatchError(handler, this.#closedError);
|
|
648
689
|
}
|
|
649
|
-
const
|
|
690
|
+
const normalizedOptions = normalizeUndiciDispatchOptions(options);
|
|
691
|
+
const url = resolveUndiciDispatchUrl(normalizedOptions);
|
|
650
692
|
const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
|
|
651
693
|
const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
|
|
652
|
-
return dispatcher.dispatch(
|
|
694
|
+
return dispatcher.dispatch(normalizedOptions, handler);
|
|
653
695
|
}
|
|
654
696
|
|
|
655
697
|
public override close(callback: () => void): void;
|
package/src/shared.ts
CHANGED
|
@@ -15,6 +15,18 @@ export class ProxylineError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Decode proxy userinfo; invalid percent-encoding must not throw URIError mid-CONNECT. */
|
|
19
|
+
export function decodeProxyUserinfoComponent(value: string): string {
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(value);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new ProxylineError(
|
|
24
|
+
"INVALID_PROXY_USERINFO",
|
|
25
|
+
"Proxy username or password contains invalid percent-encoding.",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export function resolveProxyTlsCa(options: ProxylineTlsOptions | undefined): string | undefined {
|
|
19
31
|
if (!options) {
|
|
20
32
|
return undefined;
|