@jameskh/proxywire 1.0.0
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/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetcher.d.ts +48 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +147 -0
- package/dist/fetcher.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/list.d.ts +18 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +44 -0
- package/dist/list.js.map +1 -0
- package/dist/parse.d.ts +38 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +185 -0
- package/dist/parse.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/errors.ts +61 -0
- package/src/fetcher.ts +185 -0
- package/src/index.ts +28 -0
- package/src/list.ts +58 -0
- package/src/parse.ts +246 -0
- package/src/types.ts +71 -0
package/dist/parse.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describeLocation, ProxyParseError } from "./errors.js";
|
|
2
|
+
/** Schemes that can actually be dialled by `makeProxyDispatcher`. */
|
|
3
|
+
export const SUPPORTED_PROXY_PROTOCOLS = ["http", "https", "socks4", "socks5"];
|
|
4
|
+
const SUPPORTED_PROTOCOL_SET = new Set(SUPPORTED_PROXY_PROTOCOLS.map((scheme) => `${scheme}:`));
|
|
5
|
+
/**
|
|
6
|
+
* Residential providers commonly export schemeless lists, leaving the caller to
|
|
7
|
+
* decide whether to dial them over SOCKS or HTTP. SOCKS5 is the safer default:
|
|
8
|
+
* providers that support both advertise SOCKS5, and an HTTP-only endpoint fails
|
|
9
|
+
* loudly on connect rather than silently downgrading.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_PROXY_PROTOCOL = "socks5";
|
|
12
|
+
const PORT_PATTERN = /^\d+$/u;
|
|
13
|
+
const SCHEME_PATTERN = /^[a-z0-9+.-]+:\/\//iu;
|
|
14
|
+
/**
|
|
15
|
+
* Placeholder rows that ship inside example proxy files. A list still holding
|
|
16
|
+
* these has not been filled in, and connecting to `host:port` produces a
|
|
17
|
+
* confusing DNS error rather than an obvious "you forgot to configure this".
|
|
18
|
+
*/
|
|
19
|
+
const PLACEHOLDER_PROXIES = new Set([
|
|
20
|
+
"socks5://user:pass@host:port",
|
|
21
|
+
"socks5://host:port:user:pass",
|
|
22
|
+
"http://user:pass@host:port",
|
|
23
|
+
"http://host:port:user:pass",
|
|
24
|
+
"login:password@ip:port",
|
|
25
|
+
"ip:port@login:password",
|
|
26
|
+
"login:password:ip:port",
|
|
27
|
+
"ip:port:login:password",
|
|
28
|
+
]);
|
|
29
|
+
const PLACEHOLDER_FRAGMENTS = [
|
|
30
|
+
"user:pass@host:port",
|
|
31
|
+
"login:password@ip:port",
|
|
32
|
+
"ip:port@login:password",
|
|
33
|
+
];
|
|
34
|
+
/** True when a line is one of the template entries rather than a real proxy. */
|
|
35
|
+
export function isPlaceholderProxy(proxy) {
|
|
36
|
+
const normalized = proxy.trim().toLowerCase();
|
|
37
|
+
return (PLACEHOLDER_PROXIES.has(normalized) ||
|
|
38
|
+
PLACEHOLDER_FRAGMENTS.some((fragment) => normalized.includes(fragment)));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A proxy reduced to `host:port`, safe to put in a log line or an error
|
|
42
|
+
* message. Credentials in a proxy URL are as sensitive as any other password
|
|
43
|
+
* and must never reach a log file.
|
|
44
|
+
*/
|
|
45
|
+
export function maskProxy(proxy) {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(proxy).host;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return proxy;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Fisher-Yates. Spreads load across a pool instead of hammering the first entry. */
|
|
54
|
+
export function shuffleProxies(proxies) {
|
|
55
|
+
const copy = [...proxies];
|
|
56
|
+
for (let index = copy.length - 1; index > 0; index -= 1) {
|
|
57
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
58
|
+
[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]];
|
|
59
|
+
}
|
|
60
|
+
return copy;
|
|
61
|
+
}
|
|
62
|
+
function resolveProtocol(defaultProtocol) {
|
|
63
|
+
const scheme = (typeof defaultProtocol === "function" ? defaultProtocol() : defaultProtocol)?.toLowerCase();
|
|
64
|
+
if (scheme === undefined) {
|
|
65
|
+
return DEFAULT_PROXY_PROTOCOL;
|
|
66
|
+
}
|
|
67
|
+
if (!SUPPORTED_PROTOCOL_SET.has(`${scheme}:`)) {
|
|
68
|
+
throw new ProxyParseError(`Unsupported proxy protocol "${scheme}". Supported schemes are ${SUPPORTED_PROXY_PROTOCOLS.join(", ")}.`);
|
|
69
|
+
}
|
|
70
|
+
return scheme;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parses `scheme://host:port:username:password`, the shape several providers
|
|
74
|
+
* hand out when they prefix their colon-separated export with a scheme. The
|
|
75
|
+
* credentials go through `encodeURIComponent` because provider usernames
|
|
76
|
+
* routinely carry `_`, `-` and session markers, and passwords can contain
|
|
77
|
+
* characters that would otherwise break `new URL`.
|
|
78
|
+
*/
|
|
79
|
+
function normalizeProviderProxyUrl(proxy) {
|
|
80
|
+
const match = /^(https?|socks[45]):\/\/(.+)$/iu.exec(proxy);
|
|
81
|
+
if (!match) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const [, scheme, authority] = match;
|
|
85
|
+
if (!scheme || !authority) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const [host, port, username, ...passwordParts] = authority.split(":");
|
|
89
|
+
const password = passwordParts.join(":");
|
|
90
|
+
if (!host || !port || !username || !password || !PORT_PATTERN.test(port)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return `${scheme.toLowerCase()}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parses the four schemeless shapes providers export, told apart by which
|
|
97
|
+
* field looks like a port:
|
|
98
|
+
* login:password@ip:port
|
|
99
|
+
* ip:port@login:password
|
|
100
|
+
* login:password:ip:port
|
|
101
|
+
* ip:port:login:password
|
|
102
|
+
* The scheme is supplied separately - see `defaultProtocol`.
|
|
103
|
+
*/
|
|
104
|
+
export function parseSchemelessProxy(proxy) {
|
|
105
|
+
// Anything carrying a scheme prefix is not a schemeless provider line.
|
|
106
|
+
if (SCHEME_PATTERN.test(proxy)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (proxy.includes("@")) {
|
|
110
|
+
const atIndex = proxy.indexOf("@");
|
|
111
|
+
const left = proxy.slice(0, atIndex).split(":");
|
|
112
|
+
const right = proxy.slice(atIndex + 1).split(":");
|
|
113
|
+
if (left.length !== 2 || right.length !== 2) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const [leftFirst, leftSecond] = left;
|
|
117
|
+
const [rightFirst, rightSecond] = right;
|
|
118
|
+
// ip:port@login:password
|
|
119
|
+
if (PORT_PATTERN.test(leftSecond) && !PORT_PATTERN.test(rightSecond)) {
|
|
120
|
+
return { host: leftFirst, port: leftSecond, username: rightFirst, password: rightSecond };
|
|
121
|
+
}
|
|
122
|
+
// login:password@ip:port
|
|
123
|
+
if (PORT_PATTERN.test(rightSecond)) {
|
|
124
|
+
return { host: rightFirst, port: rightSecond, username: leftFirst, password: leftSecond };
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const parts = proxy.split(":");
|
|
129
|
+
if (parts.length !== 4) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const [first, second, third, fourth] = parts;
|
|
133
|
+
// ip:port:login:password
|
|
134
|
+
if (PORT_PATTERN.test(second) && !PORT_PATTERN.test(fourth)) {
|
|
135
|
+
return { host: first, port: second, username: third, password: fourth };
|
|
136
|
+
}
|
|
137
|
+
// login:password:ip:port
|
|
138
|
+
if (PORT_PATTERN.test(fourth)) {
|
|
139
|
+
return { host: third, port: fourth, username: first, password: second };
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
function normalizeSchemelessProxyUrl(proxy, defaultProtocol) {
|
|
144
|
+
const parsed = parseSchemelessProxy(proxy);
|
|
145
|
+
if (!parsed) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const { host, port, username, password } = parsed;
|
|
149
|
+
return `${resolveProtocol(defaultProtocol)}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
|
|
150
|
+
}
|
|
151
|
+
function assertSupportedProxyUrl(proxy, location) {
|
|
152
|
+
let url;
|
|
153
|
+
try {
|
|
154
|
+
url = new URL(proxy);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new ProxyParseError(`Invalid proxy URL${describeLocation(location)}. Use user:pass@host:port URL format or provider host:port:user:pass format.`, location);
|
|
158
|
+
}
|
|
159
|
+
if (!SUPPORTED_PROTOCOL_SET.has(url.protocol) || !url.hostname) {
|
|
160
|
+
throw new ProxyParseError(`Invalid proxy URL${describeLocation(location)}. Supported schemes are ${SUPPORTED_PROXY_PROTOCOLS.join(", ")}.`, location);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Turns any supported proxy notation into a dialable URL, leaving an already
|
|
165
|
+
* valid URL untouched. Throws a `ProxyParseError` naming the source and line
|
|
166
|
+
* when nothing matches.
|
|
167
|
+
*/
|
|
168
|
+
export function normalizeProxy(proxy, options = {}) {
|
|
169
|
+
const location = { source: options.source, lineNumber: options.lineNumber };
|
|
170
|
+
const trimmed = proxy.trim();
|
|
171
|
+
try {
|
|
172
|
+
assertSupportedProxyUrl(trimmed, location);
|
|
173
|
+
return trimmed;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const normalized = normalizeProviderProxyUrl(trimmed) ??
|
|
177
|
+
normalizeSchemelessProxyUrl(trimmed, options.defaultProtocol);
|
|
178
|
+
if (!normalized) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
assertSupportedProxyUrl(normalized, location);
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SecureVersion } from "node:tls";
|
|
2
|
+
import type { Agent, ProxyAgent } from "undici";
|
|
3
|
+
/**
|
|
4
|
+
* An undici dispatcher that routes requests through a proxy. SOCKS proxies get
|
|
5
|
+
* a plain `Agent` with a custom `connect`; HTTP/HTTPS proxies get a `ProxyAgent`.
|
|
6
|
+
*/
|
|
7
|
+
export type ProxyDispatcher = Agent | ProxyAgent;
|
|
8
|
+
/** A `fetch` bound to a particular proxy (or to no proxy at all). */
|
|
9
|
+
export type ProxyFetch = (url: string, options?: RequestInit) => Promise<Response>;
|
|
10
|
+
/**
|
|
11
|
+
* Anywhere this library would otherwise be silent about something worth
|
|
12
|
+
* knowing - a transport retry, for instance - it calls into this instead of
|
|
13
|
+
* writing to the console, so the host application keeps control of its output.
|
|
14
|
+
* Every method is optional; missing ones are simply not called.
|
|
15
|
+
*/
|
|
16
|
+
export interface ProxyLogger {
|
|
17
|
+
debug?(message: string): void;
|
|
18
|
+
warn?(message: string): void;
|
|
19
|
+
}
|
|
20
|
+
export interface FetcherOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Wall-clock budget for a single request, applied via `AbortSignal.timeout`
|
|
23
|
+
* whenever the caller has not supplied a signal of its own.
|
|
24
|
+
*/
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** How long the SOCKS handshake may take before the connection is abandoned. */
|
|
27
|
+
socksConnectTimeoutMs?: number;
|
|
28
|
+
/** Lowest TLS version accepted when tunnelling through a SOCKS proxy. */
|
|
29
|
+
minTlsVersion?: SecureVersion;
|
|
30
|
+
/**
|
|
31
|
+
* Bun's `fetch` cannot tunnel SOCKS, and silently ignoring that would send
|
|
32
|
+
* traffic from the machine's own address while the caller believes it is
|
|
33
|
+
* proxied. By default a SOCKS proxy under Bun throws instead. Set this to
|
|
34
|
+
* fall back to the undici dispatcher path - correct on Node, a no-op on Bun.
|
|
35
|
+
*/
|
|
36
|
+
allowSocksUnderBun?: boolean;
|
|
37
|
+
logger?: ProxyLogger;
|
|
38
|
+
}
|
|
39
|
+
export interface ProxyParseOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Scheme applied to schemeless provider lines such as `1.2.3.4:8080:user:pass`.
|
|
42
|
+
* Pass a function to resolve it lazily - useful when it comes from an
|
|
43
|
+
* environment variable that may be set after this module is imported.
|
|
44
|
+
* Defaults to `socks5`.
|
|
45
|
+
*/
|
|
46
|
+
defaultProtocol?: string | (() => string);
|
|
47
|
+
/**
|
|
48
|
+
* Where these proxies came from - a file path, a URL, an env var name. Used
|
|
49
|
+
* only to make error messages point somewhere useful.
|
|
50
|
+
*/
|
|
51
|
+
source?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ProxyListOptions extends ProxyParseOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Lines starting with this prefix are ignored. Defaults to `#`. Set to an
|
|
56
|
+
* empty string to treat every non-blank line as a proxy.
|
|
57
|
+
*/
|
|
58
|
+
commentPrefix?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Throw when the list still contains the placeholder entries that ship in
|
|
61
|
+
* example files (`user:pass@host:port` and friends). Defaults to `true`:
|
|
62
|
+
* running against a template is a configuration mistake, not a proxy list.
|
|
63
|
+
*/
|
|
64
|
+
rejectPlaceholders?: boolean;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEhD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,UAAU,CAAC;AAEjD,qEAAqE;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;IAC1C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jameskh/proxywire",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Proxy list parsing and proxy-aware fetch for Node and Bun. Normalizes provider export formats, builds HTTP/SOCKS dispatchers, and returns a fetch that actually goes through the proxy on both runtimes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"proxy",
|
|
12
|
+
"socks5",
|
|
13
|
+
"socks4",
|
|
14
|
+
"http-proxy",
|
|
15
|
+
"fetch",
|
|
16
|
+
"undici",
|
|
17
|
+
"dispatcher",
|
|
18
|
+
"bun",
|
|
19
|
+
"proxy-list"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"bun": "./src/index.ts",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"socks": "^2.8.0",
|
|
44
|
+
"undici": "^8.3.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.3.14",
|
|
48
|
+
"@types/node": "^26.2.0",
|
|
49
|
+
"typescript": "^6.0.3"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"main": "./dist/index.js",
|
|
55
|
+
"types": "./dist/index.d.ts",
|
|
56
|
+
"sideEffects": false
|
|
57
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Thrown when a proxy string cannot be understood, with the offending location. */
|
|
2
|
+
export class ProxyParseError extends Error {
|
|
3
|
+
readonly source?: string;
|
|
4
|
+
readonly lineNumber?: number;
|
|
5
|
+
|
|
6
|
+
constructor(message: string, location: { source?: string; lineNumber?: number } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ProxyParseError";
|
|
9
|
+
this.source = location.source;
|
|
10
|
+
this.lineNumber = location.lineNumber;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Renders `in <source> on line <n>` for error messages, omitting what is unknown. */
|
|
15
|
+
export function describeLocation(location: { source?: string; lineNumber?: number }): string {
|
|
16
|
+
const parts: string[] = [];
|
|
17
|
+
if (location.source) {
|
|
18
|
+
parts.push(` in ${location.source}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (location.lineNumber !== undefined) {
|
|
22
|
+
parts.push(` on line ${location.lineNumber}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return parts.join("");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Flattens an error and its `cause` chain into one readable line.
|
|
30
|
+
*
|
|
31
|
+
* `fetch` throws a bare `TypeError: fetch failed` for every underlying network
|
|
32
|
+
* failure - DNS, TLS, proxy handshake, timeout, refused connection - and puts
|
|
33
|
+
* the real reason in `cause`. Without walking that chain there is no way to
|
|
34
|
+
* tell a dead proxy from a bad credential from a slow host, which is exactly
|
|
35
|
+
* the distinction anyone debugging a proxy pool needs.
|
|
36
|
+
*/
|
|
37
|
+
export function describeError(error: unknown): string {
|
|
38
|
+
const parts: string[] = [];
|
|
39
|
+
let current: unknown = error;
|
|
40
|
+
const seen = new Set<unknown>();
|
|
41
|
+
|
|
42
|
+
while (current && !seen.has(current)) {
|
|
43
|
+
seen.add(current);
|
|
44
|
+
|
|
45
|
+
if (current instanceof Error) {
|
|
46
|
+
const code = (current as NodeJS.ErrnoException).code;
|
|
47
|
+
const detail =
|
|
48
|
+
code && !current.message.includes(code) ? `${current.message} (${code})` : current.message;
|
|
49
|
+
if (!parts.includes(detail)) {
|
|
50
|
+
parts.push(detail);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
current = current.cause;
|
|
54
|
+
} else {
|
|
55
|
+
parts.push(String(current));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parts.length > 0 ? parts.join(" -> ") : "Unknown error";
|
|
61
|
+
}
|
package/src/fetcher.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as tls from "node:tls";
|
|
2
|
+
import { SocksClient } from "socks";
|
|
3
|
+
import { Agent, fetch as undiciFetch, ProxyAgent } from "undici";
|
|
4
|
+
import { describeError } from "./errors.js";
|
|
5
|
+
import { maskProxy } from "./parse.js";
|
|
6
|
+
import type { FetcherOptions, ProxyDispatcher, ProxyFetch } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
9
|
+
const DEFAULT_SOCKS_CONNECT_TIMEOUT_MS = 15_000;
|
|
10
|
+
const DEFAULT_SOCKS_PORT = 1080;
|
|
11
|
+
const DEFAULT_DESTINATION_PORT = 443;
|
|
12
|
+
|
|
13
|
+
/** True when running under Bun rather than Node. */
|
|
14
|
+
export const IS_BUN = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds an undici dispatcher for a proxy URL.
|
|
18
|
+
*
|
|
19
|
+
* SOCKS gets a plain `Agent` whose `connect` opens the SOCKS tunnel by hand and
|
|
20
|
+
* then starts TLS over it, because undici has no SOCKS support of its own.
|
|
21
|
+
* HTTP/HTTPS proxies go to `ProxyAgent`, which already speaks CONNECT.
|
|
22
|
+
*/
|
|
23
|
+
export function makeProxyDispatcher(proxy: string, options: FetcherOptions = {}): ProxyDispatcher {
|
|
24
|
+
if (proxy.startsWith("socks5://") || proxy.startsWith("socks4://")) {
|
|
25
|
+
const url = new URL(proxy);
|
|
26
|
+
const proxyHost = url.hostname;
|
|
27
|
+
const proxyPort = Number(url.port) || DEFAULT_SOCKS_PORT;
|
|
28
|
+
const proxyType = proxy.startsWith("socks5://") ? 5 : 4;
|
|
29
|
+
const auth = url.username
|
|
30
|
+
? {
|
|
31
|
+
username: decodeURIComponent(url.username),
|
|
32
|
+
password: decodeURIComponent(url.password),
|
|
33
|
+
}
|
|
34
|
+
: undefined;
|
|
35
|
+
|
|
36
|
+
return new Agent({
|
|
37
|
+
connect: async (connectOptions, callback) => {
|
|
38
|
+
try {
|
|
39
|
+
const destinationPort = Number(connectOptions.port) || DEFAULT_DESTINATION_PORT;
|
|
40
|
+
const { socket } = await SocksClient.createConnection({
|
|
41
|
+
proxy: {
|
|
42
|
+
host: proxyHost,
|
|
43
|
+
port: proxyPort,
|
|
44
|
+
type: proxyType as 4 | 5,
|
|
45
|
+
...(auth
|
|
46
|
+
? {
|
|
47
|
+
userId: auth.username,
|
|
48
|
+
password: auth.password,
|
|
49
|
+
}
|
|
50
|
+
: {}),
|
|
51
|
+
},
|
|
52
|
+
command: "connect",
|
|
53
|
+
destination: {
|
|
54
|
+
host: connectOptions.hostname,
|
|
55
|
+
port: destinationPort,
|
|
56
|
+
},
|
|
57
|
+
timeout: options.socksConnectTimeoutMs ?? DEFAULT_SOCKS_CONNECT_TIMEOUT_MS,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const tlsSocket = tls.connect({
|
|
61
|
+
socket: socket as tls.TLSSocket,
|
|
62
|
+
servername: connectOptions.hostname,
|
|
63
|
+
minVersion: options.minTlsVersion ?? "TLSv1.2",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
tlsSocket.on("secureConnect", () => callback(null, tlsSocket));
|
|
67
|
+
tlsSocket.on("error", (error: Error) => callback(error, null));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
callback(error as Error, null);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new ProxyAgent(proxy);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Applies the request budget, but never overrides a signal the caller supplied. */
|
|
79
|
+
export function withRequestTimeout(options: RequestInit, timeoutMs: number): RequestInit {
|
|
80
|
+
return options.signal ? options : { ...options, signal: AbortSignal.timeout(timeoutMs) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runs `run` again once if it fails.
|
|
85
|
+
*
|
|
86
|
+
* Proxy pools drop the occasional connection for reasons that have nothing to
|
|
87
|
+
* do with the request - a rotating exit going away mid-handshake, most often -
|
|
88
|
+
* and a single immediate retry turns most of those into a success.
|
|
89
|
+
*/
|
|
90
|
+
export async function retryOnTransportFailure<T>(
|
|
91
|
+
describe: string,
|
|
92
|
+
run: () => Promise<T>,
|
|
93
|
+
options: FetcherOptions = {},
|
|
94
|
+
): Promise<T> {
|
|
95
|
+
try {
|
|
96
|
+
return await run();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const detail = describeError(error);
|
|
99
|
+
options.logger?.debug?.(
|
|
100
|
+
`${describe} failed at the transport level, retrying once - ${detail.slice(0, 120)}`,
|
|
101
|
+
);
|
|
102
|
+
return run();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Fetcher {
|
|
107
|
+
/** A `fetch` that routes through the proxy this fetcher was built for. */
|
|
108
|
+
fetch: ProxyFetch;
|
|
109
|
+
/** The dispatcher backing it, absent when proxyless or on Bun's native path. */
|
|
110
|
+
dispatcher?: ProxyDispatcher;
|
|
111
|
+
/** Releases the dispatcher's socket pool. Safe to call more than once. */
|
|
112
|
+
close(): Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A `fetch` bound to one proxy, or an unproxied one when `proxy` is omitted.
|
|
117
|
+
*
|
|
118
|
+
* The two runtimes need different mechanisms and this is the whole reason the
|
|
119
|
+
* function exists. Bun ignores undici's `dispatcher` option outright, so a
|
|
120
|
+
* request aimed at a dead proxy still succeeds and reports the machine's own
|
|
121
|
+
* address - every "proxied" call silently goes out unproxied. Bun's own `fetch`
|
|
122
|
+
* takes a `proxy` option that it does honour, so that path is used there, and
|
|
123
|
+
* the dispatcher path is kept for Node.
|
|
124
|
+
*/
|
|
125
|
+
export function makeFetcher(proxy?: string, options: FetcherOptions = {}): Fetcher {
|
|
126
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
127
|
+
|
|
128
|
+
if (proxy && IS_BUN && !options.allowSocksUnderBun) {
|
|
129
|
+
if (!/^https?:\/\//iu.test(proxy)) {
|
|
130
|
+
// Bun's fetch answers UnsupportedProxyProtocol for socks, and the
|
|
131
|
+
// dispatcher fallback below does nothing there, so refuse rather than
|
|
132
|
+
// run unproxied without saying so.
|
|
133
|
+
throw new Error(
|
|
134
|
+
`SOCKS proxies are not supported when running under Bun (${maskProxy(proxy)}). ` +
|
|
135
|
+
`Use an http:// proxy, or set allowSocksUnderBun to opt out of this check.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fetcher: ProxyFetch = async (url, requestOptions = {}) =>
|
|
140
|
+
fetch(url, { ...withRequestTimeout(requestOptions, timeoutMs), proxy } as RequestInit & {
|
|
141
|
+
proxy: string;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return { fetch: fetcher, close: async () => {} };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const dispatcher = proxy ? makeProxyDispatcher(proxy, options) : undefined;
|
|
148
|
+
|
|
149
|
+
const fetcher: ProxyFetch = async (url, requestOptions = {}) => {
|
|
150
|
+
const withTimeout = withRequestTimeout(requestOptions, timeoutMs);
|
|
151
|
+
if (dispatcher) {
|
|
152
|
+
// On Node the global fetch is backed by whatever undici ships inside the
|
|
153
|
+
// running binary, which is often a different major from the "undici"
|
|
154
|
+
// package the dispatcher came from, and the handler interfaces are not
|
|
155
|
+
// compatible across majors. Using undici's own fetch keeps the
|
|
156
|
+
// dispatcher and the implementation on the same version.
|
|
157
|
+
const response = await undiciFetch(url, { ...withTimeout, dispatcher } as Parameters<
|
|
158
|
+
typeof undiciFetch
|
|
159
|
+
>[1]);
|
|
160
|
+
return response as unknown as Response;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return fetch(url, withTimeout);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return { fetch: fetcher, dispatcher, close: () => closeDispatcher(dispatcher) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Releases a dispatcher's connection pool.
|
|
171
|
+
*
|
|
172
|
+
* Without this every request cycle leaks an `Agent`/`ProxyAgent` and its
|
|
173
|
+
* sockets, which exhausts file descriptors in any long-running process.
|
|
174
|
+
*/
|
|
175
|
+
export async function closeDispatcher(dispatcher?: ProxyDispatcher): Promise<void> {
|
|
176
|
+
if (!dispatcher) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await dispatcher.close();
|
|
182
|
+
} catch {
|
|
183
|
+
// Already closed, or could not close cleanly - nothing actionable either way.
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { describeError, ProxyParseError } from "./errors.js";
|
|
2
|
+
export {
|
|
3
|
+
closeDispatcher,
|
|
4
|
+
IS_BUN,
|
|
5
|
+
makeFetcher,
|
|
6
|
+
makeProxyDispatcher,
|
|
7
|
+
retryOnTransportFailure,
|
|
8
|
+
withRequestTimeout,
|
|
9
|
+
} from "./fetcher.js";
|
|
10
|
+
export type { Fetcher } from "./fetcher.js";
|
|
11
|
+
export { loadProxyFile, parseProxyList } from "./list.js";
|
|
12
|
+
export {
|
|
13
|
+
isPlaceholderProxy,
|
|
14
|
+
maskProxy,
|
|
15
|
+
normalizeProxy,
|
|
16
|
+
parseSchemelessProxy,
|
|
17
|
+
shuffleProxies,
|
|
18
|
+
SUPPORTED_PROXY_PROTOCOLS,
|
|
19
|
+
} from "./parse.js";
|
|
20
|
+
export type { ProxyProtocol } from "./parse.js";
|
|
21
|
+
export type {
|
|
22
|
+
FetcherOptions,
|
|
23
|
+
ProxyDispatcher,
|
|
24
|
+
ProxyFetch,
|
|
25
|
+
ProxyListOptions,
|
|
26
|
+
ProxyLogger,
|
|
27
|
+
ProxyParseOptions,
|
|
28
|
+
} from "./types.js";
|
package/src/list.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { ProxyParseError } from "./errors.js";
|
|
3
|
+
import { isPlaceholderProxy, normalizeProxy } from "./parse.js";
|
|
4
|
+
import type { ProxyListOptions } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_COMMENT_PREFIX = "#";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses a newline-separated proxy list into dialable URLs.
|
|
10
|
+
*
|
|
11
|
+
* Line numbers are captured before blanks and comments are filtered out, so an
|
|
12
|
+
* error still points at the right line of the original text. Comments are
|
|
13
|
+
* skipped for a practical reason: example files are usually all comments, and
|
|
14
|
+
* the placeholder text inside them would otherwise read as proxies and trip the
|
|
15
|
+
* placeholder check. Skipping them also lets a real list carry notes.
|
|
16
|
+
*/
|
|
17
|
+
export function parseProxyList(text: string, options: ProxyListOptions = {}): string[] {
|
|
18
|
+
const commentPrefix = options.commentPrefix ?? DEFAULT_COMMENT_PREFIX;
|
|
19
|
+
|
|
20
|
+
const entries = text
|
|
21
|
+
.split(/\r?\n/u)
|
|
22
|
+
.map((line, index) => ({ proxy: line.trim(), lineNumber: index + 1 }))
|
|
23
|
+
.filter(
|
|
24
|
+
({ proxy }) =>
|
|
25
|
+
proxy.length > 0 && (commentPrefix === "" || !proxy.startsWith(commentPrefix)),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (entries.length === 0) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (options.rejectPlaceholders !== false) {
|
|
33
|
+
const placeholders = entries.filter(({ proxy }) => isPlaceholderProxy(proxy));
|
|
34
|
+
if (placeholders.length > 0) {
|
|
35
|
+
const location = { source: options.source, lineNumber: placeholders[0]!.lineNumber };
|
|
36
|
+
throw new ProxyParseError(
|
|
37
|
+
`Placeholder proxies detected${options.source ? ` in ${options.source}` : ""}. ` +
|
|
38
|
+
`Replace the placeholder entries with real proxies.`,
|
|
39
|
+
location,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return entries.map(({ proxy, lineNumber }) => normalizeProxy(proxy, { ...options, lineNumber }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads and parses a proxy list file, returning an empty list when the file is
|
|
49
|
+
* absent. A missing file means "no proxies configured", which is a valid state
|
|
50
|
+
* for anything that treats proxying as optional.
|
|
51
|
+
*/
|
|
52
|
+
export function loadProxyFile(path: string, options: ProxyListOptions = {}): string[] {
|
|
53
|
+
if (!existsSync(path)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parseProxyList(readFileSync(path, "utf8"), { source: path, ...options });
|
|
58
|
+
}
|