@secure-exec/nodejs 0.2.0-rc.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/LICENSE +191 -0
- package/README.md +7 -0
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +67 -0
- package/dist/bridge/active-handles.d.ts +22 -0
- package/dist/bridge/active-handles.js +112 -0
- package/dist/bridge/child-process.d.ts +99 -0
- package/dist/bridge/child-process.js +672 -0
- package/dist/bridge/dispatch.d.ts +2 -0
- package/dist/bridge/dispatch.js +40 -0
- package/dist/bridge/fs.d.ts +502 -0
- package/dist/bridge/fs.js +3307 -0
- package/dist/bridge/index.d.ts +10 -0
- package/dist/bridge/index.js +41 -0
- package/dist/bridge/module.d.ts +75 -0
- package/dist/bridge/module.js +325 -0
- package/dist/bridge/network.d.ts +1093 -0
- package/dist/bridge/network.js +8651 -0
- package/dist/bridge/os.d.ts +13 -0
- package/dist/bridge/os.js +256 -0
- package/dist/bridge/polyfills.d.ts +9 -0
- package/dist/bridge/polyfills.js +67 -0
- package/dist/bridge/process.d.ts +121 -0
- package/dist/bridge/process.js +1382 -0
- package/dist/bridge/whatwg-url.d.ts +67 -0
- package/dist/bridge/whatwg-url.js +712 -0
- package/dist/bridge-contract.d.ts +774 -0
- package/dist/bridge-contract.js +172 -0
- package/dist/bridge-handlers.d.ts +199 -0
- package/dist/bridge-handlers.js +4263 -0
- package/dist/bridge-loader.d.ts +9 -0
- package/dist/bridge-loader.js +87 -0
- package/dist/bridge-setup.d.ts +1 -0
- package/dist/bridge-setup.js +3 -0
- package/dist/bridge.js +21652 -0
- package/dist/builtin-modules.d.ts +25 -0
- package/dist/builtin-modules.js +312 -0
- package/dist/default-network-adapter.d.ts +13 -0
- package/dist/default-network-adapter.js +351 -0
- package/dist/driver.d.ts +87 -0
- package/dist/driver.js +191 -0
- package/dist/esm-compiler.d.ts +14 -0
- package/dist/esm-compiler.js +68 -0
- package/dist/execution-driver.d.ts +37 -0
- package/dist/execution-driver.js +977 -0
- package/dist/host-network-adapter.d.ts +7 -0
- package/dist/host-network-adapter.js +279 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/isolate-bootstrap.d.ts +86 -0
- package/dist/isolate-bootstrap.js +125 -0
- package/dist/ivm-compat.d.ts +7 -0
- package/dist/ivm-compat.js +31 -0
- package/dist/kernel-runtime.d.ts +58 -0
- package/dist/kernel-runtime.js +535 -0
- package/dist/module-access.d.ts +75 -0
- package/dist/module-access.js +606 -0
- package/dist/module-resolver.d.ts +8 -0
- package/dist/module-resolver.js +150 -0
- package/dist/os-filesystem.d.ts +42 -0
- package/dist/os-filesystem.js +161 -0
- package/dist/package-bundler.d.ts +36 -0
- package/dist/package-bundler.js +497 -0
- package/dist/polyfills.d.ts +17 -0
- package/dist/polyfills.js +97 -0
- package/dist/worker-adapter.d.ts +21 -0
- package/dist/worker-adapter.js +34 -0
- package/package.json +123 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module classification and resolution helpers.
|
|
3
|
+
*
|
|
4
|
+
* Node built-ins are split into three tiers:
|
|
5
|
+
* - Bridge modules: fully polyfilled by the bridge (fs, process, http, etc.)
|
|
6
|
+
* - Deferred core modules: known but not yet bridge-supported; surfaced via
|
|
7
|
+
* deferred stubs in require paths and polyfills/wrappers in ESM paths
|
|
8
|
+
* - Unsupported core modules: known but intentionally unimplemented
|
|
9
|
+
*
|
|
10
|
+
* Everything else falls through to node-stdlib-browser polyfills or node_modules.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Known named exports for each built-in module. Used by the ESM wrapper
|
|
14
|
+
* generator to create `export const X = _builtin.X;` re-exports so that
|
|
15
|
+
* `import { readFile } from 'fs'` works inside the isolate.
|
|
16
|
+
*/
|
|
17
|
+
export declare const BUILTIN_NAMED_EXPORTS: Record<string, string[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a module specifier to its canonical form if it's a known built-in.
|
|
20
|
+
* Returns null for non-builtin specifiers.
|
|
21
|
+
* Preserves the `node:` prefix when present, strips it otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeBuiltinSpecifier(request: string): string | null;
|
|
24
|
+
/** Extract the directory portion of a path (lightweight dirname without node:path). */
|
|
25
|
+
export declare function getPathDir(path: string): string;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module classification and resolution helpers.
|
|
3
|
+
*
|
|
4
|
+
* Node built-ins are split into three tiers:
|
|
5
|
+
* - Bridge modules: fully polyfilled by the bridge (fs, process, http, etc.)
|
|
6
|
+
* - Deferred core modules: known but not yet bridge-supported; surfaced via
|
|
7
|
+
* deferred stubs in require paths and polyfills/wrappers in ESM paths
|
|
8
|
+
* - Unsupported core modules: known but intentionally unimplemented
|
|
9
|
+
*
|
|
10
|
+
* Everything else falls through to node-stdlib-browser polyfills or node_modules.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Static set of Node.js stdlib module names that have browser polyfills
|
|
14
|
+
* available via node-stdlib-browser. Hardcoded to avoid importing
|
|
15
|
+
* node-stdlib-browser at runtime (its ESM entry crashes on missing
|
|
16
|
+
* mock/empty.js in published builds).
|
|
17
|
+
*/
|
|
18
|
+
const STDLIB_BROWSER_MODULES = new Set([
|
|
19
|
+
"assert",
|
|
20
|
+
"buffer",
|
|
21
|
+
"child_process",
|
|
22
|
+
"cluster",
|
|
23
|
+
"console",
|
|
24
|
+
"constants",
|
|
25
|
+
"crypto",
|
|
26
|
+
"dgram",
|
|
27
|
+
"dns",
|
|
28
|
+
"domain",
|
|
29
|
+
"events",
|
|
30
|
+
"fs",
|
|
31
|
+
"http",
|
|
32
|
+
"https",
|
|
33
|
+
"http2",
|
|
34
|
+
"module",
|
|
35
|
+
"net",
|
|
36
|
+
"os",
|
|
37
|
+
"path",
|
|
38
|
+
"punycode",
|
|
39
|
+
"process",
|
|
40
|
+
"querystring",
|
|
41
|
+
"readline",
|
|
42
|
+
"repl",
|
|
43
|
+
"stream",
|
|
44
|
+
"_stream_duplex",
|
|
45
|
+
"_stream_passthrough",
|
|
46
|
+
"_stream_readable",
|
|
47
|
+
"_stream_transform",
|
|
48
|
+
"_stream_writable",
|
|
49
|
+
"string_decoder",
|
|
50
|
+
"sys",
|
|
51
|
+
"timers/promises",
|
|
52
|
+
"timers",
|
|
53
|
+
"tls",
|
|
54
|
+
"tty",
|
|
55
|
+
"url",
|
|
56
|
+
"util",
|
|
57
|
+
"vm",
|
|
58
|
+
"zlib",
|
|
59
|
+
]);
|
|
60
|
+
/** Check if a module has a polyfill available via node-stdlib-browser. */
|
|
61
|
+
function hasPolyfill(moduleName) {
|
|
62
|
+
const name = moduleName.replace(/^node:/, "");
|
|
63
|
+
return STDLIB_BROWSER_MODULES.has(name);
|
|
64
|
+
}
|
|
65
|
+
/** Modules with full bridge implementations injected into the isolate. */
|
|
66
|
+
const BRIDGE_MODULES = [
|
|
67
|
+
"fs",
|
|
68
|
+
"fs/promises",
|
|
69
|
+
"module",
|
|
70
|
+
"os",
|
|
71
|
+
"http",
|
|
72
|
+
"https",
|
|
73
|
+
"http2",
|
|
74
|
+
"dns",
|
|
75
|
+
"child_process",
|
|
76
|
+
"process",
|
|
77
|
+
"v8",
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Recognized built-ins that lack bridge support.
|
|
81
|
+
* Runtime handling differs by path (require stubs vs ESM/polyfill handling).
|
|
82
|
+
*/
|
|
83
|
+
const DEFERRED_CORE_MODULES = [
|
|
84
|
+
"net",
|
|
85
|
+
"tls",
|
|
86
|
+
"readline",
|
|
87
|
+
"perf_hooks",
|
|
88
|
+
"async_hooks",
|
|
89
|
+
"worker_threads",
|
|
90
|
+
"diagnostics_channel",
|
|
91
|
+
];
|
|
92
|
+
/** Built-ins that are intentionally unimplemented (throw on use). */
|
|
93
|
+
const UNSUPPORTED_CORE_MODULES = [
|
|
94
|
+
"dgram",
|
|
95
|
+
"cluster",
|
|
96
|
+
"wasi",
|
|
97
|
+
"inspector",
|
|
98
|
+
"repl",
|
|
99
|
+
"trace_events",
|
|
100
|
+
"domain",
|
|
101
|
+
];
|
|
102
|
+
const KNOWN_BUILTIN_MODULES = new Set([
|
|
103
|
+
...BRIDGE_MODULES,
|
|
104
|
+
...DEFERRED_CORE_MODULES,
|
|
105
|
+
...UNSUPPORTED_CORE_MODULES,
|
|
106
|
+
"assert",
|
|
107
|
+
"buffer",
|
|
108
|
+
"constants",
|
|
109
|
+
"crypto",
|
|
110
|
+
"events",
|
|
111
|
+
"path",
|
|
112
|
+
"querystring",
|
|
113
|
+
"stream",
|
|
114
|
+
"stream/web",
|
|
115
|
+
"string_decoder",
|
|
116
|
+
"timers",
|
|
117
|
+
"tty",
|
|
118
|
+
"url",
|
|
119
|
+
"util",
|
|
120
|
+
"vm",
|
|
121
|
+
"zlib",
|
|
122
|
+
]);
|
|
123
|
+
/**
|
|
124
|
+
* Known named exports for each built-in module. Used by the ESM wrapper
|
|
125
|
+
* generator to create `export const X = _builtin.X;` re-exports so that
|
|
126
|
+
* `import { readFile } from 'fs'` works inside the isolate.
|
|
127
|
+
*/
|
|
128
|
+
export const BUILTIN_NAMED_EXPORTS = {
|
|
129
|
+
fs: [
|
|
130
|
+
"promises",
|
|
131
|
+
"readFileSync",
|
|
132
|
+
"writeFileSync",
|
|
133
|
+
"appendFileSync",
|
|
134
|
+
"existsSync",
|
|
135
|
+
"statSync",
|
|
136
|
+
"mkdirSync",
|
|
137
|
+
"readdirSync",
|
|
138
|
+
"createReadStream",
|
|
139
|
+
"createWriteStream",
|
|
140
|
+
],
|
|
141
|
+
"fs/promises": [
|
|
142
|
+
"access",
|
|
143
|
+
"readFile",
|
|
144
|
+
"writeFile",
|
|
145
|
+
"appendFile",
|
|
146
|
+
"copyFile",
|
|
147
|
+
"cp",
|
|
148
|
+
"open",
|
|
149
|
+
"opendir",
|
|
150
|
+
"mkdir",
|
|
151
|
+
"mkdtemp",
|
|
152
|
+
"readdir",
|
|
153
|
+
"rename",
|
|
154
|
+
"stat",
|
|
155
|
+
"lstat",
|
|
156
|
+
"chmod",
|
|
157
|
+
"chown",
|
|
158
|
+
"utimes",
|
|
159
|
+
"truncate",
|
|
160
|
+
"unlink",
|
|
161
|
+
"rm",
|
|
162
|
+
"rmdir",
|
|
163
|
+
"realpath",
|
|
164
|
+
"readlink",
|
|
165
|
+
"symlink",
|
|
166
|
+
"link",
|
|
167
|
+
],
|
|
168
|
+
module: [
|
|
169
|
+
"createRequire",
|
|
170
|
+
"Module",
|
|
171
|
+
"isBuiltin",
|
|
172
|
+
"builtinModules",
|
|
173
|
+
"SourceMap",
|
|
174
|
+
"syncBuiltinESMExports",
|
|
175
|
+
],
|
|
176
|
+
os: [
|
|
177
|
+
"arch",
|
|
178
|
+
"platform",
|
|
179
|
+
"tmpdir",
|
|
180
|
+
"homedir",
|
|
181
|
+
"hostname",
|
|
182
|
+
"type",
|
|
183
|
+
"release",
|
|
184
|
+
"constants",
|
|
185
|
+
],
|
|
186
|
+
http: [
|
|
187
|
+
"request",
|
|
188
|
+
"get",
|
|
189
|
+
"createServer",
|
|
190
|
+
"Server",
|
|
191
|
+
"IncomingMessage",
|
|
192
|
+
"ServerResponse",
|
|
193
|
+
"Agent",
|
|
194
|
+
"validateHeaderName",
|
|
195
|
+
"validateHeaderValue",
|
|
196
|
+
"METHODS",
|
|
197
|
+
"STATUS_CODES",
|
|
198
|
+
],
|
|
199
|
+
https: ["request", "get", "createServer", "Agent", "globalAgent"],
|
|
200
|
+
dns: ["lookup", "resolve", "resolve4", "resolve6", "promises"],
|
|
201
|
+
child_process: [
|
|
202
|
+
"spawn",
|
|
203
|
+
"spawnSync",
|
|
204
|
+
"exec",
|
|
205
|
+
"execSync",
|
|
206
|
+
"execFile",
|
|
207
|
+
"execFileSync",
|
|
208
|
+
"fork",
|
|
209
|
+
],
|
|
210
|
+
process: [
|
|
211
|
+
"argv",
|
|
212
|
+
"env",
|
|
213
|
+
"cwd",
|
|
214
|
+
"chdir",
|
|
215
|
+
"exit",
|
|
216
|
+
"pid",
|
|
217
|
+
"platform",
|
|
218
|
+
"version",
|
|
219
|
+
"versions",
|
|
220
|
+
"stdout",
|
|
221
|
+
"stderr",
|
|
222
|
+
"stdin",
|
|
223
|
+
"nextTick",
|
|
224
|
+
],
|
|
225
|
+
path: [
|
|
226
|
+
"sep",
|
|
227
|
+
"delimiter",
|
|
228
|
+
"basename",
|
|
229
|
+
"dirname",
|
|
230
|
+
"extname",
|
|
231
|
+
"format",
|
|
232
|
+
"isAbsolute",
|
|
233
|
+
"join",
|
|
234
|
+
"normalize",
|
|
235
|
+
"parse",
|
|
236
|
+
"relative",
|
|
237
|
+
"resolve",
|
|
238
|
+
],
|
|
239
|
+
async_hooks: [
|
|
240
|
+
"AsyncLocalStorage",
|
|
241
|
+
"AsyncResource",
|
|
242
|
+
"createHook",
|
|
243
|
+
"executionAsyncId",
|
|
244
|
+
"triggerAsyncId",
|
|
245
|
+
],
|
|
246
|
+
perf_hooks: [
|
|
247
|
+
"performance",
|
|
248
|
+
"PerformanceObserver",
|
|
249
|
+
"PerformanceEntry",
|
|
250
|
+
"monitorEventLoopDelay",
|
|
251
|
+
"createHistogram",
|
|
252
|
+
"constants",
|
|
253
|
+
],
|
|
254
|
+
diagnostics_channel: [
|
|
255
|
+
"channel",
|
|
256
|
+
"hasSubscribers",
|
|
257
|
+
"tracingChannel",
|
|
258
|
+
"Channel",
|
|
259
|
+
],
|
|
260
|
+
stream: [
|
|
261
|
+
"Readable",
|
|
262
|
+
"Writable",
|
|
263
|
+
"Duplex",
|
|
264
|
+
"Transform",
|
|
265
|
+
"PassThrough",
|
|
266
|
+
"Stream",
|
|
267
|
+
"pipeline",
|
|
268
|
+
"finished",
|
|
269
|
+
"promises",
|
|
270
|
+
"addAbortSignal",
|
|
271
|
+
"compose",
|
|
272
|
+
],
|
|
273
|
+
"stream/web": [
|
|
274
|
+
"ReadableStream",
|
|
275
|
+
"ReadableStreamDefaultReader",
|
|
276
|
+
"ReadableStreamBYOBReader",
|
|
277
|
+
"ReadableStreamBYOBRequest",
|
|
278
|
+
"ReadableByteStreamController",
|
|
279
|
+
"ReadableStreamDefaultController",
|
|
280
|
+
"TransformStream",
|
|
281
|
+
"TransformStreamDefaultController",
|
|
282
|
+
"WritableStream",
|
|
283
|
+
"WritableStreamDefaultWriter",
|
|
284
|
+
"WritableStreamDefaultController",
|
|
285
|
+
"ByteLengthQueuingStrategy",
|
|
286
|
+
"CountQueuingStrategy",
|
|
287
|
+
"TextEncoderStream",
|
|
288
|
+
"TextDecoderStream",
|
|
289
|
+
"CompressionStream",
|
|
290
|
+
"DecompressionStream",
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Normalize a module specifier to its canonical form if it's a known built-in.
|
|
295
|
+
* Returns null for non-builtin specifiers.
|
|
296
|
+
* Preserves the `node:` prefix when present, strips it otherwise.
|
|
297
|
+
*/
|
|
298
|
+
export function normalizeBuiltinSpecifier(request) {
|
|
299
|
+
const moduleName = request.replace(/^node:/, "");
|
|
300
|
+
if (KNOWN_BUILTIN_MODULES.has(moduleName) || hasPolyfill(moduleName)) {
|
|
301
|
+
return request.startsWith("node:") ? `node:${moduleName}` : moduleName;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
/** Extract the directory portion of a path (lightweight dirname without node:path). */
|
|
306
|
+
export function getPathDir(path) {
|
|
307
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
308
|
+
const lastSlash = normalizedPath.lastIndexOf("/");
|
|
309
|
+
if (lastSlash <= 0)
|
|
310
|
+
return "/";
|
|
311
|
+
return normalizedPath.slice(0, lastSlash);
|
|
312
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NetworkAdapter } from "@secure-exec/core";
|
|
2
|
+
export interface DefaultNetworkAdapterOptions {
|
|
3
|
+
/** Pre-seed loopback ports that should bypass SSRF checks (e.g. host-managed servers). */
|
|
4
|
+
initialExemptPorts?: Iterable<number>;
|
|
5
|
+
}
|
|
6
|
+
/** Check whether an IP address falls in a private/reserved range (SSRF protection). */
|
|
7
|
+
export declare function isPrivateIp(ip: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Create a Node.js network adapter that provides real fetch, DNS, and HTTP
|
|
10
|
+
* client support. Binary responses are base64-encoded with an
|
|
11
|
+
* `x-body-encoding` header so the bridge can decode them.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createDefaultNetworkAdapter(options?: DefaultNetworkAdapterOptions): NetworkAdapter;
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import * as dns from "node:dns";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as http from "node:http";
|
|
4
|
+
import * as https from "node:https";
|
|
5
|
+
import * as zlib from "node:zlib";
|
|
6
|
+
/** Check whether an IP address falls in a private/reserved range (SSRF protection). */
|
|
7
|
+
export function isPrivateIp(ip) {
|
|
8
|
+
// Normalize IPv4-mapped IPv6 (::ffff:a.b.c.d → a.b.c.d)
|
|
9
|
+
const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
10
|
+
if (net.isIPv4(normalized)) {
|
|
11
|
+
const parts = normalized.split(".").map(Number);
|
|
12
|
+
const [a, b] = parts;
|
|
13
|
+
return (a === 10 ||
|
|
14
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
15
|
+
(a === 192 && b === 168) ||
|
|
16
|
+
a === 127 ||
|
|
17
|
+
(a === 169 && b === 254) ||
|
|
18
|
+
a === 0 ||
|
|
19
|
+
(a >= 224 && a <= 239) ||
|
|
20
|
+
(a >= 240));
|
|
21
|
+
}
|
|
22
|
+
if (net.isIPv6(normalized)) {
|
|
23
|
+
const lower = normalized.toLowerCase();
|
|
24
|
+
return (lower === "::1" ||
|
|
25
|
+
lower === "::" ||
|
|
26
|
+
lower.startsWith("fc") ||
|
|
27
|
+
lower.startsWith("fd") ||
|
|
28
|
+
lower.startsWith("fe80") ||
|
|
29
|
+
lower.startsWith("ff"));
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/** Check whether a hostname is a loopback address (127.x.x.x, ::1, localhost). */
|
|
34
|
+
function isLoopbackHost(hostname) {
|
|
35
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]")
|
|
36
|
+
? hostname.slice(1, -1)
|
|
37
|
+
: hostname;
|
|
38
|
+
if (bare === "localhost" || bare === "::1")
|
|
39
|
+
return true;
|
|
40
|
+
if (net.isIPv4(bare) && bare.startsWith("127."))
|
|
41
|
+
return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function getUrlPort(parsed) {
|
|
45
|
+
return parsed.port
|
|
46
|
+
? Number(parsed.port)
|
|
47
|
+
: parsed.protocol === "https:" ? 443 : 80;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve hostname to IP and block private/reserved ranges (SSRF protection).
|
|
51
|
+
*
|
|
52
|
+
* Loopback requests are allowed only when an explicit exemption or the
|
|
53
|
+
* runtime-provided kernel listener checker claims the requested port.
|
|
54
|
+
*/
|
|
55
|
+
async function assertNotPrivateHost(url, allowLoopbackPort) {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (parsed.protocol === "data:" || parsed.protocol === "blob:")
|
|
58
|
+
return;
|
|
59
|
+
const hostname = parsed.hostname;
|
|
60
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]")
|
|
61
|
+
? hostname.slice(1, -1)
|
|
62
|
+
: hostname;
|
|
63
|
+
if (isLoopbackHost(hostname)) {
|
|
64
|
+
const port = getUrlPort(parsed);
|
|
65
|
+
if (allowLoopbackPort?.(hostname, port)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (net.isIP(bare)) {
|
|
70
|
+
if (isPrivateIp(bare)) {
|
|
71
|
+
throw new Error(`SSRF blocked: ${hostname} resolves to private IP`);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const address = await new Promise((resolve, reject) => {
|
|
76
|
+
dns.lookup(bare, (err, addr) => {
|
|
77
|
+
if (err)
|
|
78
|
+
reject(err);
|
|
79
|
+
else
|
|
80
|
+
resolve(addr);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
if (isPrivateIp(address)) {
|
|
84
|
+
throw new Error(`SSRF blocked: ${hostname} resolves to private IP ${address}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const MAX_REDIRECTS = 20;
|
|
88
|
+
/**
|
|
89
|
+
* Create a Node.js network adapter that provides real fetch, DNS, and HTTP
|
|
90
|
+
* client support. Binary responses are base64-encoded with an
|
|
91
|
+
* `x-body-encoding` header so the bridge can decode them.
|
|
92
|
+
*/
|
|
93
|
+
export function createDefaultNetworkAdapter(options) {
|
|
94
|
+
const upgradeSockets = new Map();
|
|
95
|
+
const initialExemptPorts = new Set(options?.initialExemptPorts);
|
|
96
|
+
let nextUpgradeSocketId = 1;
|
|
97
|
+
let onUpgradeSocketData = null;
|
|
98
|
+
let onUpgradeSocketEnd = null;
|
|
99
|
+
let dynamicLoopbackPortChecker;
|
|
100
|
+
const allowLoopbackPort = (hostname, port) => {
|
|
101
|
+
if (initialExemptPorts.has(port))
|
|
102
|
+
return true;
|
|
103
|
+
if (dynamicLoopbackPortChecker?.(hostname, port))
|
|
104
|
+
return true;
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
const adapter = {
|
|
108
|
+
__setLoopbackPortChecker(checker) {
|
|
109
|
+
dynamicLoopbackPortChecker = checker;
|
|
110
|
+
},
|
|
111
|
+
upgradeSocketWrite(socketId, dataBase64) {
|
|
112
|
+
const socket = upgradeSockets.get(socketId);
|
|
113
|
+
if (socket && !socket.destroyed) {
|
|
114
|
+
socket.write(Buffer.from(dataBase64, "base64"));
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
upgradeSocketEnd(socketId) {
|
|
118
|
+
const socket = upgradeSockets.get(socketId);
|
|
119
|
+
if (socket && !socket.destroyed) {
|
|
120
|
+
socket.end();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
upgradeSocketDestroy(socketId) {
|
|
124
|
+
const socket = upgradeSockets.get(socketId);
|
|
125
|
+
if (socket) {
|
|
126
|
+
socket.destroy();
|
|
127
|
+
upgradeSockets.delete(socketId);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
setUpgradeSocketCallbacks(callbacks) {
|
|
131
|
+
onUpgradeSocketData = callbacks.onData;
|
|
132
|
+
onUpgradeSocketEnd = callbacks.onEnd;
|
|
133
|
+
},
|
|
134
|
+
async fetch(url, requestOptions) {
|
|
135
|
+
let currentUrl = url;
|
|
136
|
+
let redirected = false;
|
|
137
|
+
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
138
|
+
await assertNotPrivateHost(currentUrl, allowLoopbackPort);
|
|
139
|
+
const response = await fetch(currentUrl, {
|
|
140
|
+
method: requestOptions?.method || "GET",
|
|
141
|
+
headers: requestOptions?.headers,
|
|
142
|
+
body: requestOptions?.body,
|
|
143
|
+
redirect: "manual",
|
|
144
|
+
});
|
|
145
|
+
const status = response.status;
|
|
146
|
+
if (status === 301 || status === 302 || status === 303 || status === 307 || status === 308) {
|
|
147
|
+
const location = response.headers.get("location");
|
|
148
|
+
if (!location)
|
|
149
|
+
break;
|
|
150
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
151
|
+
redirected = true;
|
|
152
|
+
if (status === 301 || status === 302 || status === 303) {
|
|
153
|
+
requestOptions = { ...requestOptions, method: "GET", body: undefined };
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const headers = {};
|
|
158
|
+
response.headers.forEach((value, key) => {
|
|
159
|
+
headers[key] = value;
|
|
160
|
+
});
|
|
161
|
+
delete headers["content-encoding"];
|
|
162
|
+
const contentType = response.headers.get("content-type") || "";
|
|
163
|
+
const isBinary = contentType.includes("octet-stream") ||
|
|
164
|
+
contentType.includes("gzip") ||
|
|
165
|
+
currentUrl.endsWith(".tgz");
|
|
166
|
+
let body;
|
|
167
|
+
if (isBinary) {
|
|
168
|
+
const buffer = await response.arrayBuffer();
|
|
169
|
+
body = Buffer.from(buffer).toString("base64");
|
|
170
|
+
headers["x-body-encoding"] = "base64";
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
body = await response.text();
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: response.ok,
|
|
177
|
+
status: response.status,
|
|
178
|
+
statusText: response.statusText,
|
|
179
|
+
headers,
|
|
180
|
+
body,
|
|
181
|
+
url: currentUrl,
|
|
182
|
+
redirected,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
throw new Error("Too many redirects");
|
|
186
|
+
},
|
|
187
|
+
async dnsLookup(hostname) {
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
dns.lookup(hostname, (err, address, family) => {
|
|
190
|
+
if (err) {
|
|
191
|
+
resolve({ error: err.message, code: err.code || "ENOTFOUND" });
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
resolve({ address, family });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
async httpRequest(url, requestOptions) {
|
|
200
|
+
await assertNotPrivateHost(url, allowLoopbackPort);
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const urlObj = new URL(url);
|
|
203
|
+
const isHttps = urlObj.protocol === "https:";
|
|
204
|
+
const transport = isHttps ? https : http;
|
|
205
|
+
const reqOptions = {
|
|
206
|
+
hostname: urlObj.hostname,
|
|
207
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
208
|
+
path: urlObj.pathname + urlObj.search,
|
|
209
|
+
method: requestOptions?.method || "GET",
|
|
210
|
+
headers: requestOptions?.headers || {},
|
|
211
|
+
// Keep host-side pooling disabled so sandbox http.Agent semantics
|
|
212
|
+
// are controlled entirely by the bridge layer.
|
|
213
|
+
agent: false,
|
|
214
|
+
...(isHttps && requestOptions?.rejectUnauthorized !== undefined && {
|
|
215
|
+
rejectUnauthorized: requestOptions.rejectUnauthorized,
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
const req = transport.request(reqOptions, (res) => {
|
|
219
|
+
const chunks = [];
|
|
220
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
221
|
+
res.on("end", async () => {
|
|
222
|
+
let buffer = Buffer.concat(chunks);
|
|
223
|
+
const contentEncoding = res.headers["content-encoding"];
|
|
224
|
+
if (contentEncoding === "gzip" || contentEncoding === "deflate") {
|
|
225
|
+
try {
|
|
226
|
+
buffer = await new Promise((responseResolve, responseReject) => {
|
|
227
|
+
const decompress = contentEncoding === "gzip" ? zlib.gunzip : zlib.inflate;
|
|
228
|
+
decompress(buffer, (err, result) => {
|
|
229
|
+
if (err)
|
|
230
|
+
responseReject(err);
|
|
231
|
+
else
|
|
232
|
+
responseResolve(result);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Preserve the original buffer when decompression fails.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const contentType = res.headers["content-type"] || "";
|
|
241
|
+
const isBinary = contentType.includes("octet-stream") ||
|
|
242
|
+
contentType.includes("gzip") ||
|
|
243
|
+
url.endsWith(".tgz");
|
|
244
|
+
const headers = {};
|
|
245
|
+
const rawHeaders = [...res.rawHeaders];
|
|
246
|
+
Object.entries(res.headers).forEach(([key, value]) => {
|
|
247
|
+
if (typeof value === "string")
|
|
248
|
+
headers[key] = value;
|
|
249
|
+
else if (Array.isArray(value))
|
|
250
|
+
headers[key] = value.join(", ");
|
|
251
|
+
});
|
|
252
|
+
delete headers["content-encoding"];
|
|
253
|
+
const trailers = {};
|
|
254
|
+
if (res.trailers) {
|
|
255
|
+
Object.entries(res.trailers).forEach(([key, value]) => {
|
|
256
|
+
if (typeof value === "string")
|
|
257
|
+
trailers[key] = value;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const hasTrailers = Object.keys(trailers).length > 0;
|
|
261
|
+
const base = {
|
|
262
|
+
status: res.statusCode || 200,
|
|
263
|
+
statusText: res.statusMessage || "OK",
|
|
264
|
+
headers,
|
|
265
|
+
rawHeaders,
|
|
266
|
+
url,
|
|
267
|
+
...(hasTrailers ? { trailers } : {}),
|
|
268
|
+
};
|
|
269
|
+
if (isBinary) {
|
|
270
|
+
headers["x-body-encoding"] = "base64";
|
|
271
|
+
resolve({ ...base, body: buffer.toString("base64") });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
resolve({ ...base, body: buffer.toString("utf-8") });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
res.on("error", reject);
|
|
278
|
+
});
|
|
279
|
+
req.on("upgrade", (res, socket, head) => {
|
|
280
|
+
const headers = {};
|
|
281
|
+
const rawHeaders = [...res.rawHeaders];
|
|
282
|
+
Object.entries(res.headers).forEach(([key, value]) => {
|
|
283
|
+
if (typeof value === "string")
|
|
284
|
+
headers[key] = value;
|
|
285
|
+
else if (Array.isArray(value))
|
|
286
|
+
headers[key] = value.join(", ");
|
|
287
|
+
});
|
|
288
|
+
const socketId = nextUpgradeSocketId++;
|
|
289
|
+
upgradeSockets.set(socketId, socket);
|
|
290
|
+
socket.on("data", (chunk) => {
|
|
291
|
+
if (onUpgradeSocketData) {
|
|
292
|
+
onUpgradeSocketData(socketId, chunk.toString("base64"));
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
socket.on("close", () => {
|
|
296
|
+
if (onUpgradeSocketEnd) {
|
|
297
|
+
onUpgradeSocketEnd(socketId);
|
|
298
|
+
}
|
|
299
|
+
upgradeSockets.delete(socketId);
|
|
300
|
+
});
|
|
301
|
+
resolve({
|
|
302
|
+
status: res.statusCode || 101,
|
|
303
|
+
statusText: res.statusMessage || "Switching Protocols",
|
|
304
|
+
headers,
|
|
305
|
+
rawHeaders,
|
|
306
|
+
body: head.toString("base64"),
|
|
307
|
+
url,
|
|
308
|
+
upgradeSocketId: socketId,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
req.on("connect", (res, socket, head) => {
|
|
312
|
+
const headers = {};
|
|
313
|
+
const rawHeaders = [...res.rawHeaders];
|
|
314
|
+
Object.entries(res.headers).forEach(([key, value]) => {
|
|
315
|
+
if (typeof value === "string")
|
|
316
|
+
headers[key] = value;
|
|
317
|
+
else if (Array.isArray(value))
|
|
318
|
+
headers[key] = value.join(", ");
|
|
319
|
+
});
|
|
320
|
+
const socketId = nextUpgradeSocketId++;
|
|
321
|
+
upgradeSockets.set(socketId, socket);
|
|
322
|
+
socket.on("data", (chunk) => {
|
|
323
|
+
if (onUpgradeSocketData) {
|
|
324
|
+
onUpgradeSocketData(socketId, chunk.toString("base64"));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
socket.on("close", () => {
|
|
328
|
+
if (onUpgradeSocketEnd) {
|
|
329
|
+
onUpgradeSocketEnd(socketId);
|
|
330
|
+
}
|
|
331
|
+
upgradeSockets.delete(socketId);
|
|
332
|
+
});
|
|
333
|
+
resolve({
|
|
334
|
+
status: res.statusCode || 200,
|
|
335
|
+
statusText: res.statusMessage || "Connection established",
|
|
336
|
+
headers,
|
|
337
|
+
rawHeaders,
|
|
338
|
+
body: head.toString("base64"),
|
|
339
|
+
url,
|
|
340
|
+
upgradeSocketId: socketId,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
req.on("error", reject);
|
|
344
|
+
if (requestOptions?.body)
|
|
345
|
+
req.write(requestOptions.body);
|
|
346
|
+
req.end();
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
return adapter;
|
|
351
|
+
}
|