@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,4263 @@
|
|
|
1
|
+
// Build a BridgeHandlers map for V8 runtime.
|
|
2
|
+
//
|
|
3
|
+
// Each handler is a plain function that performs the host-side operation.
|
|
4
|
+
// Handler names match HOST_BRIDGE_GLOBAL_KEYS from the bridge contract.
|
|
5
|
+
import * as http from "node:http";
|
|
6
|
+
import * as http2 from "node:http2";
|
|
7
|
+
import * as tls from "node:tls";
|
|
8
|
+
import { Duplex, PassThrough } from "node:stream";
|
|
9
|
+
import { readFileSync, realpathSync, existsSync } from "node:fs";
|
|
10
|
+
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from "node:path";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { serialize } from "node:v8";
|
|
13
|
+
import { randomFillSync, randomUUID, createHash, createHmac, pbkdf2Sync, scryptSync, hkdfSync, createCipheriv, createDecipheriv, sign, verify, generateKeyPairSync, createPrivateKey, createPublicKey, createSecretKey, createDiffieHellman, getDiffieHellman, createECDH, diffieHellman, generateKeySync, generatePrimeSync, publicEncrypt, privateDecrypt, privateEncrypt, publicDecrypt, timingSafeEqual, constants as cryptoConstants, } from "node:crypto";
|
|
14
|
+
import { HOST_BRIDGE_GLOBAL_KEYS, } from "./bridge-contract.js";
|
|
15
|
+
import { AF_INET, AF_INET6, AF_UNIX, SOCK_DGRAM, SOCK_STREAM, mkdir, FDTableManager, O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, FILETYPE_REGULAR_FILE, } from "@secure-exec/core";
|
|
16
|
+
import { normalizeBuiltinSpecifier } from "./builtin-modules.js";
|
|
17
|
+
import { resolveModule, loadFile } from "./package-bundler.js";
|
|
18
|
+
import { transformDynamicImport, isESM } from "@secure-exec/core/internal/shared/esm-utils";
|
|
19
|
+
import { bundlePolyfill, hasPolyfill } from "./polyfills.js";
|
|
20
|
+
import { createBuiltinESMWrapper, getStaticBuiltinWrapperSource, } from "./esm-compiler.js";
|
|
21
|
+
import { checkBridgeBudget, assertPayloadByteLength, assertTextPayloadSize, getBase64EncodedByteLength, getHostBuiltinNamedExports, parseJsonWithLimit, polyfillCodeCache, RESOURCE_BUDGET_ERROR_CODE, } from "./isolate-bootstrap.js";
|
|
22
|
+
const SOL_SOCKET = 1;
|
|
23
|
+
const IPPROTO_TCP = 6;
|
|
24
|
+
const SO_KEEPALIVE = 9;
|
|
25
|
+
const SO_RCVBUF = 8;
|
|
26
|
+
const SO_SNDBUF = 7;
|
|
27
|
+
const TCP_NODELAY = 1;
|
|
28
|
+
function serializeKeyDetails(details) {
|
|
29
|
+
if (!details || typeof details !== "object") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return Object.fromEntries(Object.entries(details).map(([key, value]) => [
|
|
33
|
+
key,
|
|
34
|
+
typeof value === "bigint"
|
|
35
|
+
? { __type: "bigint", value: value.toString() }
|
|
36
|
+
: value,
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
function serializeKeyValue(value) {
|
|
40
|
+
if (Buffer.isBuffer(value)) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "buffer",
|
|
43
|
+
value: value.toString("base64"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
return {
|
|
48
|
+
kind: "string",
|
|
49
|
+
value,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (value &&
|
|
53
|
+
typeof value === "object" &&
|
|
54
|
+
"type" in value &&
|
|
55
|
+
(value.type === "public" ||
|
|
56
|
+
value.type === "private") &&
|
|
57
|
+
typeof value.export === "function") {
|
|
58
|
+
return {
|
|
59
|
+
kind: "keyObject",
|
|
60
|
+
value: serializeSandboxKeyObject(value),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
kind: "object",
|
|
65
|
+
value: value,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function exportAsPem(keyObject) {
|
|
69
|
+
return keyObject.type === "private"
|
|
70
|
+
? keyObject.export({ type: "pkcs8", format: "pem" })
|
|
71
|
+
: keyObject.export({ type: "spki", format: "pem" });
|
|
72
|
+
}
|
|
73
|
+
function serializeSandboxKeyObject(keyObject) {
|
|
74
|
+
let jwk;
|
|
75
|
+
try {
|
|
76
|
+
jwk = keyObject.export({ format: "jwk" });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
jwk = undefined;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: keyObject.type,
|
|
83
|
+
pem: exportAsPem(keyObject),
|
|
84
|
+
asymmetricKeyType: keyObject.asymmetricKeyType ?? undefined,
|
|
85
|
+
asymmetricKeyDetails: serializeKeyDetails(keyObject.asymmetricKeyDetails),
|
|
86
|
+
jwk,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function serializeAnyKeyObject(keyObject) {
|
|
90
|
+
if (keyObject.type === "secret") {
|
|
91
|
+
return {
|
|
92
|
+
type: "secret",
|
|
93
|
+
raw: Buffer.from(keyObject.export()).toString("base64"),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return serializeSandboxKeyObject(keyObject);
|
|
97
|
+
}
|
|
98
|
+
function serializeBridgeValue(value) {
|
|
99
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === "bigint") {
|
|
103
|
+
return {
|
|
104
|
+
__type: "bigint",
|
|
105
|
+
value: value.toString(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (Buffer.isBuffer(value)) {
|
|
109
|
+
return {
|
|
110
|
+
__type: "buffer",
|
|
111
|
+
value: value.toString("base64"),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (value instanceof ArrayBuffer) {
|
|
115
|
+
return {
|
|
116
|
+
__type: "buffer",
|
|
117
|
+
value: Buffer.from(value).toString("base64"),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (ArrayBuffer.isView(value)) {
|
|
121
|
+
return {
|
|
122
|
+
__type: "buffer",
|
|
123
|
+
value: Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
return value.map((entry) => serializeBridgeValue(entry));
|
|
128
|
+
}
|
|
129
|
+
if (value &&
|
|
130
|
+
typeof value === "object" &&
|
|
131
|
+
"type" in value &&
|
|
132
|
+
((value.type === "public" ||
|
|
133
|
+
value.type === "private" ||
|
|
134
|
+
value.type === "secret")) &&
|
|
135
|
+
typeof value.export === "function") {
|
|
136
|
+
return {
|
|
137
|
+
__type: "keyObject",
|
|
138
|
+
value: serializeAnyKeyObject(value),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (value && typeof value === "object") {
|
|
142
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => entry === undefined ? [] : [[key, serializeBridgeValue(entry)]]));
|
|
143
|
+
}
|
|
144
|
+
return String(value);
|
|
145
|
+
}
|
|
146
|
+
function deserializeSandboxKeyObject(serialized) {
|
|
147
|
+
if (serialized.type === "secret") {
|
|
148
|
+
return createSecretKey(Buffer.from(serialized.raw || "", "base64"));
|
|
149
|
+
}
|
|
150
|
+
if (serialized.type === "private") {
|
|
151
|
+
return createPrivateKey(String(serialized.pem || ""));
|
|
152
|
+
}
|
|
153
|
+
return createPublicKey(String(serialized.pem || ""));
|
|
154
|
+
}
|
|
155
|
+
function deserializeBridgeValue(value) {
|
|
156
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
return value.map((entry) => deserializeBridgeValue(entry));
|
|
161
|
+
}
|
|
162
|
+
if ("__type" in value) {
|
|
163
|
+
if (value.__type === "buffer") {
|
|
164
|
+
return Buffer.from(value.value, "base64");
|
|
165
|
+
}
|
|
166
|
+
if (value.__type === "bigint") {
|
|
167
|
+
return BigInt(value.value);
|
|
168
|
+
}
|
|
169
|
+
if (value.__type === "keyObject") {
|
|
170
|
+
return deserializeSandboxKeyObject(value.value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, deserializeBridgeValue(entry)]));
|
|
174
|
+
}
|
|
175
|
+
function parseSerializedOptions(optionsJson) {
|
|
176
|
+
const parsed = JSON.parse(String(optionsJson));
|
|
177
|
+
if (!parsed || parsed.hasOptions !== true) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
return deserializeBridgeValue(parsed.options ?? null);
|
|
181
|
+
}
|
|
182
|
+
function serializeDispatchError(error) {
|
|
183
|
+
if (error instanceof Error) {
|
|
184
|
+
const withCode = error;
|
|
185
|
+
return {
|
|
186
|
+
message: error.message,
|
|
187
|
+
name: error.name,
|
|
188
|
+
code: typeof withCode.code === "string" ? withCode.code : undefined,
|
|
189
|
+
stack: error.stack,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
message: String(error),
|
|
194
|
+
name: "Error",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function restoreDispatchArgument(value) {
|
|
198
|
+
if (!value || typeof value !== "object") {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
if (value.__secureExecDispatchType ===
|
|
202
|
+
"undefined") {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
return value.map((entry) => restoreDispatchArgument(entry));
|
|
207
|
+
}
|
|
208
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, restoreDispatchArgument(entry)]));
|
|
209
|
+
}
|
|
210
|
+
function normalizeBridgeAlgorithm(algorithm) {
|
|
211
|
+
if (algorithm === null || algorithm === undefined || algorithm === "") {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return String(algorithm);
|
|
215
|
+
}
|
|
216
|
+
function decodeBridgeBuffer(data) {
|
|
217
|
+
return Buffer.from(String(data), "base64");
|
|
218
|
+
}
|
|
219
|
+
function sanitizeJsonValue(value) {
|
|
220
|
+
if (typeof value === "bigint") {
|
|
221
|
+
return Number(value);
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(value)) {
|
|
224
|
+
return value.map((entry) => sanitizeJsonValue(entry));
|
|
225
|
+
}
|
|
226
|
+
if (!value || typeof value !== "object") {
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
230
|
+
key,
|
|
231
|
+
sanitizeJsonValue(entry),
|
|
232
|
+
]));
|
|
233
|
+
}
|
|
234
|
+
function serializeCryptoKeyDataFromKeyObject(keyObject, type, algorithm, extractable, usages) {
|
|
235
|
+
if (type === "secret") {
|
|
236
|
+
return {
|
|
237
|
+
type,
|
|
238
|
+
algorithm,
|
|
239
|
+
extractable,
|
|
240
|
+
usages,
|
|
241
|
+
_raw: keyObject.export().toString("base64"),
|
|
242
|
+
_sourceKeyObjectData: {
|
|
243
|
+
type: "secret",
|
|
244
|
+
raw: keyObject.export().toString("base64"),
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
type,
|
|
250
|
+
algorithm,
|
|
251
|
+
extractable,
|
|
252
|
+
usages,
|
|
253
|
+
_pem: type === "private"
|
|
254
|
+
? keyObject.export({ type: "pkcs8", format: "pem" })
|
|
255
|
+
: keyObject.export({ type: "spki", format: "pem" }),
|
|
256
|
+
_sourceKeyObjectData: {
|
|
257
|
+
type,
|
|
258
|
+
pem: type === "private"
|
|
259
|
+
? keyObject.export({ type: "pkcs8", format: "pem" })
|
|
260
|
+
: keyObject.export({ type: "spki", format: "pem" }),
|
|
261
|
+
asymmetricKeyType: keyObject.asymmetricKeyType,
|
|
262
|
+
asymmetricKeyDetails: sanitizeJsonValue(keyObject.asymmetricKeyDetails),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function deserializeCryptoKeyObject(key) {
|
|
267
|
+
if (key.type === "secret") {
|
|
268
|
+
return createSecretKey(decodeBridgeBuffer(key._raw));
|
|
269
|
+
}
|
|
270
|
+
return key.type === "private"
|
|
271
|
+
? createPrivateKey(key._pem ?? "")
|
|
272
|
+
: createPublicKey(key._pem ?? "");
|
|
273
|
+
}
|
|
274
|
+
function normalizeHmacLength(hashName, explicitLength) {
|
|
275
|
+
if (typeof explicitLength === "number") {
|
|
276
|
+
return explicitLength;
|
|
277
|
+
}
|
|
278
|
+
switch (hashName) {
|
|
279
|
+
case "SHA-1":
|
|
280
|
+
case "SHA-256":
|
|
281
|
+
return 512;
|
|
282
|
+
case "SHA-384":
|
|
283
|
+
case "SHA-512":
|
|
284
|
+
return 1024;
|
|
285
|
+
default:
|
|
286
|
+
return 512;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function sliceDerivedBits(secret, length) {
|
|
290
|
+
if (length === undefined || length === null) {
|
|
291
|
+
return Buffer.from(secret);
|
|
292
|
+
}
|
|
293
|
+
const requestedBits = Number(length);
|
|
294
|
+
const maxBits = secret.byteLength * 8;
|
|
295
|
+
if (requestedBits > maxBits) {
|
|
296
|
+
throw new Error("derived bit length is too small");
|
|
297
|
+
}
|
|
298
|
+
const requestedBytes = Math.ceil(requestedBits / 8);
|
|
299
|
+
const derived = Buffer.from(secret.subarray(0, requestedBytes));
|
|
300
|
+
const remainder = requestedBits % 8;
|
|
301
|
+
if (remainder !== 0 && derived.length > 0) {
|
|
302
|
+
derived[derived.length - 1] &= 0xff << (8 - remainder);
|
|
303
|
+
}
|
|
304
|
+
return derived;
|
|
305
|
+
}
|
|
306
|
+
function deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, secret) {
|
|
307
|
+
const normalizedAlgorithm = typeof derivedKeyAlgorithm === "string"
|
|
308
|
+
? { name: derivedKeyAlgorithm }
|
|
309
|
+
: derivedKeyAlgorithm;
|
|
310
|
+
const algorithmName = String(normalizedAlgorithm.name ?? "");
|
|
311
|
+
if (algorithmName === "HMAC") {
|
|
312
|
+
const hashName = typeof normalizedAlgorithm.hash === "string"
|
|
313
|
+
? normalizedAlgorithm.hash
|
|
314
|
+
: String(normalizedAlgorithm.hash?.name ?? "");
|
|
315
|
+
const lengthBits = normalizeHmacLength(hashName, normalizedAlgorithm.length);
|
|
316
|
+
const keyBytes = Buffer.from(secret.subarray(0, Math.ceil(lengthBits / 8)));
|
|
317
|
+
return serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", {
|
|
318
|
+
name: "HMAC",
|
|
319
|
+
hash: { name: hashName },
|
|
320
|
+
length: lengthBits,
|
|
321
|
+
}, extractable, usages);
|
|
322
|
+
}
|
|
323
|
+
const lengthBits = Number(normalizedAlgorithm.length ?? secret.byteLength * 8);
|
|
324
|
+
const keyBytes = Buffer.from(secret.subarray(0, Math.ceil(lengthBits / 8)));
|
|
325
|
+
return serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", {
|
|
326
|
+
...normalizedAlgorithm,
|
|
327
|
+
length: lengthBits,
|
|
328
|
+
}, extractable, usages);
|
|
329
|
+
}
|
|
330
|
+
function resolveDerivedKeyLengthBits(derivedKeyAlgorithm, fallbackBits) {
|
|
331
|
+
const normalizedAlgorithm = typeof derivedKeyAlgorithm === "string"
|
|
332
|
+
? { name: derivedKeyAlgorithm }
|
|
333
|
+
: derivedKeyAlgorithm;
|
|
334
|
+
if (typeof normalizedAlgorithm.length === "number") {
|
|
335
|
+
return normalizedAlgorithm.length;
|
|
336
|
+
}
|
|
337
|
+
if (normalizedAlgorithm.name === "HMAC") {
|
|
338
|
+
const hashName = typeof normalizedAlgorithm.hash === "string"
|
|
339
|
+
? normalizedAlgorithm.hash
|
|
340
|
+
: String(normalizedAlgorithm.hash?.name ?? "");
|
|
341
|
+
return normalizeHmacLength(hashName);
|
|
342
|
+
}
|
|
343
|
+
return fallbackBits;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Build crypto bridge handlers.
|
|
347
|
+
*
|
|
348
|
+
* All handler functions are plain functions (no ivm.Reference wrapping).
|
|
349
|
+
* The V8 runtime registers these by name on the V8 global.
|
|
350
|
+
* Call dispose() when the execution ends to clear stateful cipher sessions.
|
|
351
|
+
*/
|
|
352
|
+
export function buildCryptoBridgeHandlers() {
|
|
353
|
+
const handlers = {};
|
|
354
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
355
|
+
// Stateful cipher sessions — tracks cipher/decipher instances between
|
|
356
|
+
// create/update/final bridge calls (needed for ssh2 streaming AES-GCM).
|
|
357
|
+
const cipherSessions = new Map();
|
|
358
|
+
let nextCipherSessionId = 1;
|
|
359
|
+
const diffieHellmanSessions = new Map();
|
|
360
|
+
let nextDiffieHellmanSessionId = 1;
|
|
361
|
+
// Secure randomness — cap matches Web Crypto API spec (65536 bytes).
|
|
362
|
+
handlers[K.cryptoRandomFill] = (byteLength) => {
|
|
363
|
+
const len = Number(byteLength);
|
|
364
|
+
if (len > 65536) {
|
|
365
|
+
throw new RangeError(`The ArrayBufferView's byte length (${len}) exceeds the number of bytes of entropy available via this API (65536)`);
|
|
366
|
+
}
|
|
367
|
+
const buffer = Buffer.allocUnsafe(len);
|
|
368
|
+
randomFillSync(buffer);
|
|
369
|
+
return buffer.toString("base64");
|
|
370
|
+
};
|
|
371
|
+
handlers[K.cryptoRandomUuid] = () => randomUUID();
|
|
372
|
+
// createHash — guest accumulates update() data, sends base64 to host for digest.
|
|
373
|
+
handlers[K.cryptoHashDigest] = (algorithm, dataBase64) => {
|
|
374
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
375
|
+
const hash = createHash(String(algorithm));
|
|
376
|
+
hash.update(data);
|
|
377
|
+
return hash.digest("base64");
|
|
378
|
+
};
|
|
379
|
+
// createHmac — guest accumulates update() data, sends base64 to host for HMAC digest.
|
|
380
|
+
handlers[K.cryptoHmacDigest] = (algorithm, keyBase64, dataBase64) => {
|
|
381
|
+
const key = Buffer.from(String(keyBase64), "base64");
|
|
382
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
383
|
+
const hmac = createHmac(String(algorithm), key);
|
|
384
|
+
hmac.update(data);
|
|
385
|
+
return hmac.digest("base64");
|
|
386
|
+
};
|
|
387
|
+
// pbkdf2Sync — derive key from password + salt.
|
|
388
|
+
handlers[K.cryptoPbkdf2] = (passwordBase64, saltBase64, iterations, keylen, digest) => {
|
|
389
|
+
const password = Buffer.from(String(passwordBase64), "base64");
|
|
390
|
+
const salt = Buffer.from(String(saltBase64), "base64");
|
|
391
|
+
return pbkdf2Sync(password, salt, Number(iterations), Number(keylen), String(digest)).toString("base64");
|
|
392
|
+
};
|
|
393
|
+
// scryptSync — derive key from password + salt with tunable cost params.
|
|
394
|
+
handlers[K.cryptoScrypt] = (passwordBase64, saltBase64, keylen, optionsJson) => {
|
|
395
|
+
const password = Buffer.from(String(passwordBase64), "base64");
|
|
396
|
+
const salt = Buffer.from(String(saltBase64), "base64");
|
|
397
|
+
const options = JSON.parse(String(optionsJson));
|
|
398
|
+
return scryptSync(password, salt, Number(keylen), options).toString("base64");
|
|
399
|
+
};
|
|
400
|
+
// createCipheriv — guest accumulates update() data, sends base64 to host for encryption.
|
|
401
|
+
// Returns JSON with data (and authTag for GCM modes).
|
|
402
|
+
handlers[K.cryptoCipheriv] = (algorithm, keyBase64, ivBase64, dataBase64, optionsJson) => {
|
|
403
|
+
const key = Buffer.from(String(keyBase64), "base64");
|
|
404
|
+
const iv = ivBase64 === null ? null : Buffer.from(String(ivBase64), "base64");
|
|
405
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
406
|
+
const options = optionsJson ? JSON.parse(String(optionsJson)) : {};
|
|
407
|
+
const cipher = createCipheriv(String(algorithm), key, iv, (options.authTagLength !== undefined
|
|
408
|
+
? { authTagLength: options.authTagLength }
|
|
409
|
+
: undefined));
|
|
410
|
+
if (options.validateOnly) {
|
|
411
|
+
return JSON.stringify({ data: "" });
|
|
412
|
+
}
|
|
413
|
+
if (options.aad) {
|
|
414
|
+
cipher.setAAD(Buffer.from(String(options.aad), "base64"), options.aadOptions);
|
|
415
|
+
}
|
|
416
|
+
if (options.autoPadding !== undefined) {
|
|
417
|
+
cipher.setAutoPadding(Boolean(options.autoPadding));
|
|
418
|
+
}
|
|
419
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
420
|
+
const isAead = /-(gcm|ccm)$/i.test(String(algorithm));
|
|
421
|
+
if (isAead) {
|
|
422
|
+
return JSON.stringify({
|
|
423
|
+
data: encrypted.toString("base64"),
|
|
424
|
+
authTag: cipher.getAuthTag().toString("base64"),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return JSON.stringify({ data: encrypted.toString("base64") });
|
|
428
|
+
};
|
|
429
|
+
// createDecipheriv — guest accumulates update() data, sends base64 to host for decryption.
|
|
430
|
+
// Accepts optionsJson with authTag for GCM modes.
|
|
431
|
+
handlers[K.cryptoDecipheriv] = (algorithm, keyBase64, ivBase64, dataBase64, optionsJson) => {
|
|
432
|
+
const key = Buffer.from(String(keyBase64), "base64");
|
|
433
|
+
const iv = ivBase64 === null ? null : Buffer.from(String(ivBase64), "base64");
|
|
434
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
435
|
+
const options = JSON.parse(String(optionsJson));
|
|
436
|
+
const decipher = createDecipheriv(String(algorithm), key, iv, (options.authTagLength !== undefined
|
|
437
|
+
? { authTagLength: options.authTagLength }
|
|
438
|
+
: undefined));
|
|
439
|
+
if (options.validateOnly) {
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
const isAead = /-(gcm|ccm)$/i.test(String(algorithm));
|
|
443
|
+
if (isAead && options.authTag) {
|
|
444
|
+
decipher.setAuthTag(Buffer.from(options.authTag, "base64"));
|
|
445
|
+
}
|
|
446
|
+
if (options.aad) {
|
|
447
|
+
decipher.setAAD(Buffer.from(String(options.aad), "base64"), options.aadOptions);
|
|
448
|
+
}
|
|
449
|
+
if (options.autoPadding !== undefined) {
|
|
450
|
+
decipher.setAutoPadding(Boolean(options.autoPadding));
|
|
451
|
+
}
|
|
452
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("base64");
|
|
453
|
+
};
|
|
454
|
+
// Stateful cipheriv create — opens a cipher or decipher session on the host.
|
|
455
|
+
// mode: "cipher" | "decipher"; returns sessionId.
|
|
456
|
+
handlers[K.cryptoCipherivCreate] = (mode, algorithm, keyBase64, ivBase64, optionsJson) => {
|
|
457
|
+
const algo = String(algorithm);
|
|
458
|
+
const key = Buffer.from(String(keyBase64), "base64");
|
|
459
|
+
const iv = ivBase64 === null ? null : Buffer.from(String(ivBase64), "base64");
|
|
460
|
+
const options = optionsJson ? JSON.parse(String(optionsJson)) : {};
|
|
461
|
+
const isAead = /-(gcm|ccm)$/i.test(algo);
|
|
462
|
+
let instance;
|
|
463
|
+
if (String(mode) === "decipher") {
|
|
464
|
+
const d = createDecipheriv(algo, key, iv, (options.authTagLength !== undefined
|
|
465
|
+
? { authTagLength: options.authTagLength }
|
|
466
|
+
: undefined));
|
|
467
|
+
if (isAead && options.authTag) {
|
|
468
|
+
d.setAuthTag(Buffer.from(options.authTag, "base64"));
|
|
469
|
+
}
|
|
470
|
+
instance = d;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
instance = createCipheriv(algo, key, iv, (options.authTagLength !== undefined
|
|
474
|
+
? { authTagLength: options.authTagLength }
|
|
475
|
+
: undefined));
|
|
476
|
+
}
|
|
477
|
+
const sessionId = nextCipherSessionId++;
|
|
478
|
+
cipherSessions.set(sessionId, { cipher: instance, algorithm: algo });
|
|
479
|
+
return sessionId;
|
|
480
|
+
};
|
|
481
|
+
// Stateful cipheriv update — feeds data into an open session, returns partial result.
|
|
482
|
+
handlers[K.cryptoCipherivUpdate] = (sessionId, dataBase64) => {
|
|
483
|
+
const id = Number(sessionId);
|
|
484
|
+
const session = cipherSessions.get(id);
|
|
485
|
+
if (!session)
|
|
486
|
+
throw new Error(`Cipher session ${id} not found`);
|
|
487
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
488
|
+
const result = session.cipher.update(data);
|
|
489
|
+
return result.toString("base64");
|
|
490
|
+
};
|
|
491
|
+
// Stateful cipheriv final — finalizes session, returns last block + authTag for GCM.
|
|
492
|
+
// Removes session from map.
|
|
493
|
+
handlers[K.cryptoCipherivFinal] = (sessionId) => {
|
|
494
|
+
const id = Number(sessionId);
|
|
495
|
+
const session = cipherSessions.get(id);
|
|
496
|
+
if (!session)
|
|
497
|
+
throw new Error(`Cipher session ${id} not found`);
|
|
498
|
+
cipherSessions.delete(id);
|
|
499
|
+
const final = session.cipher.final();
|
|
500
|
+
const isAead = /-(gcm|ccm)$/i.test(session.algorithm);
|
|
501
|
+
if (isAead) {
|
|
502
|
+
const authTag = session.cipher.getAuthTag?.();
|
|
503
|
+
return JSON.stringify({
|
|
504
|
+
data: final.toString("base64"),
|
|
505
|
+
authTag: authTag ? authTag.toString("base64") : undefined,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return JSON.stringify({ data: final.toString("base64") });
|
|
509
|
+
};
|
|
510
|
+
// sign — host signs data with a PEM private key.
|
|
511
|
+
handlers[K.cryptoSign] = (algorithm, dataBase64, keyJson) => {
|
|
512
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
513
|
+
const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
|
|
514
|
+
const signature = sign(normalizeBridgeAlgorithm(algorithm), data, key);
|
|
515
|
+
return signature.toString("base64");
|
|
516
|
+
};
|
|
517
|
+
// verify — host verifies signature with a PEM public key.
|
|
518
|
+
handlers[K.cryptoVerify] = (algorithm, dataBase64, keyJson, signatureBase64) => {
|
|
519
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
520
|
+
const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
|
|
521
|
+
const signature = Buffer.from(String(signatureBase64), "base64");
|
|
522
|
+
return verify(normalizeBridgeAlgorithm(algorithm), data, key, signature);
|
|
523
|
+
};
|
|
524
|
+
// Asymmetric encrypt/decrypt — use real Node crypto so DER inputs, encrypted
|
|
525
|
+
// PEM options bags, and sandbox KeyObject handles all follow host semantics.
|
|
526
|
+
handlers[K.cryptoAsymmetricOp] = (operation, keyJson, dataBase64) => {
|
|
527
|
+
const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
|
|
528
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
529
|
+
switch (String(operation)) {
|
|
530
|
+
case "publicEncrypt":
|
|
531
|
+
return publicEncrypt(key, data).toString("base64");
|
|
532
|
+
case "privateDecrypt":
|
|
533
|
+
return privateDecrypt(key, data).toString("base64");
|
|
534
|
+
case "privateEncrypt":
|
|
535
|
+
return privateEncrypt(key, data).toString("base64");
|
|
536
|
+
case "publicDecrypt":
|
|
537
|
+
return publicDecrypt(key, data).toString("base64");
|
|
538
|
+
default:
|
|
539
|
+
throw new Error(`Unsupported asymmetric crypto operation: ${String(operation)}`);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
// createPublicKey/createPrivateKey — import through host crypto so metadata
|
|
543
|
+
// like asymmetricKeyType/asymmetricKeyDetails survives reconstruction.
|
|
544
|
+
handlers[K.cryptoCreateKeyObject] = (operation, keyJson) => {
|
|
545
|
+
const key = deserializeBridgeValue(JSON.parse(String(keyJson)));
|
|
546
|
+
switch (String(operation)) {
|
|
547
|
+
case "createPrivateKey":
|
|
548
|
+
return JSON.stringify(serializeAnyKeyObject(createPrivateKey(key)));
|
|
549
|
+
case "createPublicKey":
|
|
550
|
+
return JSON.stringify(serializeAnyKeyObject(createPublicKey(key)));
|
|
551
|
+
default:
|
|
552
|
+
throw new Error(`Unsupported key creation operation: ${String(operation)}`);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
// generateKeyPairSync — host generates key pair, preserving requested encodings.
|
|
556
|
+
// For KeyObject output, serialize PEM + metadata so the isolate can recreate a
|
|
557
|
+
// Node-compatible KeyObject surface.
|
|
558
|
+
handlers[K.cryptoGenerateKeyPairSync] = (type, optionsJson) => {
|
|
559
|
+
const options = parseSerializedOptions(optionsJson);
|
|
560
|
+
const encodingOptions = options;
|
|
561
|
+
const hasExplicitEncoding = encodingOptions &&
|
|
562
|
+
(encodingOptions.publicKeyEncoding || encodingOptions.privateKeyEncoding);
|
|
563
|
+
const { publicKey, privateKey } = generateKeyPairSync(type, options);
|
|
564
|
+
if (hasExplicitEncoding) {
|
|
565
|
+
return JSON.stringify({
|
|
566
|
+
publicKey: serializeKeyValue(publicKey),
|
|
567
|
+
privateKey: serializeKeyValue(privateKey),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return JSON.stringify({
|
|
571
|
+
publicKey: serializeSandboxKeyObject(publicKey),
|
|
572
|
+
privateKey: serializeSandboxKeyObject(privateKey),
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
// generateKeySync — host generates symmetric KeyObject values with native
|
|
576
|
+
// validation so length/error semantics match Node.
|
|
577
|
+
handlers[K.cryptoGenerateKeySync] = (type, optionsJson) => {
|
|
578
|
+
const options = parseSerializedOptions(optionsJson);
|
|
579
|
+
return JSON.stringify(serializeAnyKeyObject(generateKeySync(type, options)));
|
|
580
|
+
};
|
|
581
|
+
// generatePrimeSync — host generates prime material so bigint/add/rem options
|
|
582
|
+
// follow Node semantics instead of polyfill approximations.
|
|
583
|
+
handlers[K.cryptoGeneratePrimeSync] = (size, optionsJson) => {
|
|
584
|
+
const options = parseSerializedOptions(optionsJson);
|
|
585
|
+
const prime = options === undefined
|
|
586
|
+
? generatePrimeSync(size)
|
|
587
|
+
: generatePrimeSync(size, options);
|
|
588
|
+
return JSON.stringify(serializeBridgeValue(prime));
|
|
589
|
+
};
|
|
590
|
+
// Diffie-Hellman/ECDH — keep native host objects alive by session id so
|
|
591
|
+
// sandbox calls preserve Node's return values, validation, and stateful key material.
|
|
592
|
+
handlers[K.cryptoDiffieHellman] = (optionsJson) => {
|
|
593
|
+
const options = deserializeBridgeValue(JSON.parse(String(optionsJson)));
|
|
594
|
+
return JSON.stringify(serializeBridgeValue(diffieHellman(options)));
|
|
595
|
+
};
|
|
596
|
+
handlers[K.cryptoDiffieHellmanGroup] = (name) => {
|
|
597
|
+
const group = getDiffieHellman(String(name));
|
|
598
|
+
return JSON.stringify({
|
|
599
|
+
prime: serializeBridgeValue(group.getPrime()),
|
|
600
|
+
generator: serializeBridgeValue(group.getGenerator()),
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
handlers[K.cryptoDiffieHellmanSessionCreate] = (requestJson) => {
|
|
604
|
+
const request = JSON.parse(String(requestJson));
|
|
605
|
+
const args = (request.args ?? []).map((value) => deserializeBridgeValue(value));
|
|
606
|
+
let session;
|
|
607
|
+
switch (request.type) {
|
|
608
|
+
case "dh":
|
|
609
|
+
session = createDiffieHellman(...args);
|
|
610
|
+
break;
|
|
611
|
+
case "group":
|
|
612
|
+
session = getDiffieHellman(String(request.name));
|
|
613
|
+
break;
|
|
614
|
+
case "ecdh":
|
|
615
|
+
session = createECDH(String(request.name));
|
|
616
|
+
break;
|
|
617
|
+
default:
|
|
618
|
+
throw new Error(`Unsupported Diffie-Hellman session type: ${String(request.type)}`);
|
|
619
|
+
}
|
|
620
|
+
const sessionId = nextDiffieHellmanSessionId++;
|
|
621
|
+
diffieHellmanSessions.set(sessionId, session);
|
|
622
|
+
return sessionId;
|
|
623
|
+
};
|
|
624
|
+
handlers[K.cryptoDiffieHellmanSessionCall] = (sessionId, requestJson) => {
|
|
625
|
+
const session = diffieHellmanSessions.get(Number(sessionId));
|
|
626
|
+
if (!session) {
|
|
627
|
+
throw new Error(`Diffie-Hellman session ${String(sessionId)} not found`);
|
|
628
|
+
}
|
|
629
|
+
const request = JSON.parse(String(requestJson));
|
|
630
|
+
const args = (request.args ?? []).map((value) => deserializeBridgeValue(value));
|
|
631
|
+
const sessionRecord = session;
|
|
632
|
+
if (request.method === "verifyError") {
|
|
633
|
+
return JSON.stringify({
|
|
634
|
+
result: typeof sessionRecord.verifyError === "number" ? sessionRecord.verifyError : undefined,
|
|
635
|
+
hasResult: typeof sessionRecord.verifyError === "number",
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const method = sessionRecord[request.method];
|
|
639
|
+
if (typeof method !== "function") {
|
|
640
|
+
throw new Error(`Unsupported Diffie-Hellman method: ${request.method}`);
|
|
641
|
+
}
|
|
642
|
+
const result = method.apply(session, args);
|
|
643
|
+
return JSON.stringify({
|
|
644
|
+
result: result === undefined ? null : serializeBridgeValue(result),
|
|
645
|
+
hasResult: result !== undefined,
|
|
646
|
+
});
|
|
647
|
+
};
|
|
648
|
+
// crypto.subtle — single dispatcher for all Web Crypto API operations.
|
|
649
|
+
// Guest-side SandboxSubtle serializes each call as JSON { op, ... }.
|
|
650
|
+
handlers[K.cryptoSubtle] = (opJson) => {
|
|
651
|
+
const req = JSON.parse(String(opJson));
|
|
652
|
+
const normalizeHash = (h) => {
|
|
653
|
+
const n = typeof h === "string" ? h : h.name;
|
|
654
|
+
return n.toLowerCase().replace("-", "");
|
|
655
|
+
};
|
|
656
|
+
switch (req.op) {
|
|
657
|
+
case "digest": {
|
|
658
|
+
const algo = normalizeHash(req.algorithm);
|
|
659
|
+
const data = Buffer.from(req.data, "base64");
|
|
660
|
+
return JSON.stringify({
|
|
661
|
+
data: createHash(algo).update(data).digest("base64"),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
case "generateKey": {
|
|
665
|
+
const algoName = req.algorithm.name;
|
|
666
|
+
if (algoName === "AES-GCM" ||
|
|
667
|
+
algoName === "AES-CBC" ||
|
|
668
|
+
algoName === "AES-CTR" ||
|
|
669
|
+
algoName === "AES-KW") {
|
|
670
|
+
const keyBytes = Buffer.allocUnsafe(req.algorithm.length / 8);
|
|
671
|
+
randomFillSync(keyBytes);
|
|
672
|
+
return JSON.stringify({
|
|
673
|
+
key: serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", req.algorithm, req.extractable, req.usages),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
if (algoName === "HMAC") {
|
|
677
|
+
const hashName = typeof req.algorithm.hash === "string"
|
|
678
|
+
? req.algorithm.hash
|
|
679
|
+
: req.algorithm.hash.name;
|
|
680
|
+
const len = normalizeHmacLength(hashName, req.algorithm.length) / 8;
|
|
681
|
+
const keyBytes = Buffer.allocUnsafe(len);
|
|
682
|
+
randomFillSync(keyBytes);
|
|
683
|
+
return JSON.stringify({
|
|
684
|
+
key: serializeCryptoKeyDataFromKeyObject(createSecretKey(keyBytes), "secret", {
|
|
685
|
+
...req.algorithm,
|
|
686
|
+
hash: { name: hashName },
|
|
687
|
+
length: len * 8,
|
|
688
|
+
}, req.extractable, req.usages),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (algoName === "RSASSA-PKCS1-v1_5" ||
|
|
692
|
+
algoName === "RSA-OAEP" ||
|
|
693
|
+
algoName === "RSA-PSS") {
|
|
694
|
+
let publicExponent = 65537;
|
|
695
|
+
if (req.algorithm.publicExponent) {
|
|
696
|
+
const expBytes = Buffer.from(req.algorithm.publicExponent, "base64");
|
|
697
|
+
publicExponent = 0;
|
|
698
|
+
for (const b of expBytes) {
|
|
699
|
+
publicExponent = (publicExponent << 8) | b;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
703
|
+
modulusLength: req.algorithm.modulusLength || 2048,
|
|
704
|
+
publicExponent,
|
|
705
|
+
publicKeyEncoding: {
|
|
706
|
+
type: "spki",
|
|
707
|
+
format: "pem",
|
|
708
|
+
},
|
|
709
|
+
privateKeyEncoding: {
|
|
710
|
+
type: "pkcs8",
|
|
711
|
+
format: "pem",
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
const publicKeyObject = createPublicKey(publicKey);
|
|
715
|
+
const privateKeyObject = createPrivateKey(privateKey);
|
|
716
|
+
return JSON.stringify({
|
|
717
|
+
publicKey: serializeCryptoKeyDataFromKeyObject(publicKeyObject, "public", req.algorithm, req.extractable, req.usages.filter((u) => ["verify", "encrypt", "wrapKey"].includes(u))),
|
|
718
|
+
privateKey: serializeCryptoKeyDataFromKeyObject(privateKeyObject, "private", req.algorithm, req.extractable, req.usages.filter((u) => ["sign", "decrypt", "unwrapKey"].includes(u))),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
if (algoName === "ECDSA" || algoName === "ECDH") {
|
|
722
|
+
const { publicKey, privateKey } = generateKeyPairSync("ec", {
|
|
723
|
+
namedCurve: String(req.algorithm.namedCurve),
|
|
724
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
725
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
726
|
+
});
|
|
727
|
+
return JSON.stringify({
|
|
728
|
+
publicKey: serializeCryptoKeyDataFromKeyObject(createPublicKey(publicKey), "public", { ...req.algorithm, name: algoName }, req.extractable, req.usages.filter((u) => algoName === "ECDSA"
|
|
729
|
+
? ["verify"].includes(u)
|
|
730
|
+
: ["deriveBits", "deriveKey"].includes(u))),
|
|
731
|
+
privateKey: serializeCryptoKeyDataFromKeyObject(createPrivateKey(privateKey), "private", { ...req.algorithm, name: algoName }, req.extractable, req.usages.filter((u) => algoName === "ECDSA"
|
|
732
|
+
? ["sign"].includes(u)
|
|
733
|
+
: ["deriveBits", "deriveKey"].includes(u))),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (["Ed25519", "Ed448", "X25519", "X448"].includes(algoName)) {
|
|
737
|
+
const keyPair = algoName === "Ed25519"
|
|
738
|
+
? generateKeyPairSync("ed25519")
|
|
739
|
+
: algoName === "Ed448"
|
|
740
|
+
? generateKeyPairSync("ed448")
|
|
741
|
+
: algoName === "X25519"
|
|
742
|
+
? generateKeyPairSync("x25519")
|
|
743
|
+
: generateKeyPairSync("x448");
|
|
744
|
+
const { publicKey, privateKey } = keyPair;
|
|
745
|
+
return JSON.stringify({
|
|
746
|
+
publicKey: serializeCryptoKeyDataFromKeyObject(publicKey, "public", { name: algoName }, req.extractable, req.usages.filter((u) => algoName.startsWith("Ed")
|
|
747
|
+
? ["verify"].includes(u)
|
|
748
|
+
: ["deriveBits", "deriveKey"].includes(u))),
|
|
749
|
+
privateKey: serializeCryptoKeyDataFromKeyObject(privateKey, "private", { name: algoName }, req.extractable, req.usages.filter((u) => algoName.startsWith("Ed")
|
|
750
|
+
? ["sign"].includes(u)
|
|
751
|
+
: ["deriveBits", "deriveKey"].includes(u))),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
throw new Error(`Unsupported key algorithm: ${algoName}`);
|
|
755
|
+
}
|
|
756
|
+
case "importKey": {
|
|
757
|
+
const { format, keyData, algorithm, extractable, usages } = req;
|
|
758
|
+
if (format === "raw") {
|
|
759
|
+
return JSON.stringify({
|
|
760
|
+
key: serializeCryptoKeyDataFromKeyObject(createSecretKey(Buffer.from(keyData, "base64")), "secret", algorithm.name === "HMAC" && !algorithm.length
|
|
761
|
+
? {
|
|
762
|
+
...algorithm,
|
|
763
|
+
hash: typeof algorithm.hash === "string"
|
|
764
|
+
? { name: algorithm.hash }
|
|
765
|
+
: algorithm.hash,
|
|
766
|
+
length: Buffer.from(keyData, "base64").byteLength * 8,
|
|
767
|
+
}
|
|
768
|
+
: algorithm, extractable, usages),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (format === "jwk") {
|
|
772
|
+
const jwk = typeof keyData === "string" ? JSON.parse(keyData) : keyData;
|
|
773
|
+
if (jwk.kty === "oct") {
|
|
774
|
+
const raw = Buffer.from(jwk.k, "base64url");
|
|
775
|
+
return JSON.stringify({
|
|
776
|
+
key: serializeCryptoKeyDataFromKeyObject(createSecretKey(raw), "secret", algorithm, extractable, usages),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (jwk.d) {
|
|
780
|
+
const keyObj = createPrivateKey({ key: jwk, format: "jwk" });
|
|
781
|
+
const pem = keyObj.export({
|
|
782
|
+
type: "pkcs8",
|
|
783
|
+
format: "pem",
|
|
784
|
+
});
|
|
785
|
+
return JSON.stringify({
|
|
786
|
+
key: serializeCryptoKeyDataFromKeyObject(createPrivateKey(pem), "private", algorithm, extractable, usages),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const keyObj = createPublicKey({ key: jwk, format: "jwk" });
|
|
790
|
+
const pem = keyObj.export({ type: "spki", format: "pem" });
|
|
791
|
+
return JSON.stringify({
|
|
792
|
+
key: serializeCryptoKeyDataFromKeyObject(createPublicKey(pem), "public", algorithm, extractable, usages),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (format === "pkcs8") {
|
|
796
|
+
const keyBuf = Buffer.from(keyData, "base64");
|
|
797
|
+
const keyObj = createPrivateKey({
|
|
798
|
+
key: keyBuf,
|
|
799
|
+
format: "der",
|
|
800
|
+
type: "pkcs8",
|
|
801
|
+
});
|
|
802
|
+
const pem = keyObj.export({
|
|
803
|
+
type: "pkcs8",
|
|
804
|
+
format: "pem",
|
|
805
|
+
});
|
|
806
|
+
return JSON.stringify({
|
|
807
|
+
key: serializeCryptoKeyDataFromKeyObject(createPrivateKey(pem), "private", algorithm, extractable, usages),
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
if (format === "spki") {
|
|
811
|
+
const keyBuf = Buffer.from(keyData, "base64");
|
|
812
|
+
const keyObj = createPublicKey({
|
|
813
|
+
key: keyBuf,
|
|
814
|
+
format: "der",
|
|
815
|
+
type: "spki",
|
|
816
|
+
});
|
|
817
|
+
const pem = keyObj.export({ type: "spki", format: "pem" });
|
|
818
|
+
return JSON.stringify({
|
|
819
|
+
key: serializeCryptoKeyDataFromKeyObject(createPublicKey(pem), "public", algorithm, extractable, usages),
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
throw new Error(`Unsupported import format: ${format}`);
|
|
823
|
+
}
|
|
824
|
+
case "exportKey": {
|
|
825
|
+
const { format, key } = req;
|
|
826
|
+
if (format === "raw") {
|
|
827
|
+
if (!key._raw)
|
|
828
|
+
throw new Error("Cannot export asymmetric key as raw");
|
|
829
|
+
return JSON.stringify({
|
|
830
|
+
data: key._raw,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
if (format === "jwk") {
|
|
834
|
+
if (key._raw) {
|
|
835
|
+
const raw = Buffer.from(key._raw, "base64");
|
|
836
|
+
return JSON.stringify({
|
|
837
|
+
jwk: {
|
|
838
|
+
kty: "oct",
|
|
839
|
+
k: raw.toString("base64url"),
|
|
840
|
+
ext: key.extractable,
|
|
841
|
+
key_ops: key.usages,
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
const keyObj = key.type === "private"
|
|
846
|
+
? createPrivateKey(key._pem)
|
|
847
|
+
: createPublicKey(key._pem);
|
|
848
|
+
return JSON.stringify({
|
|
849
|
+
jwk: keyObj.export({ format: "jwk" }),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (format === "pkcs8") {
|
|
853
|
+
if (key.type !== "private")
|
|
854
|
+
throw new Error("Cannot export non-private key as pkcs8");
|
|
855
|
+
const keyObj = createPrivateKey(key._pem);
|
|
856
|
+
const der = keyObj.export({
|
|
857
|
+
type: "pkcs8",
|
|
858
|
+
format: "der",
|
|
859
|
+
});
|
|
860
|
+
return JSON.stringify({ data: der.toString("base64") });
|
|
861
|
+
}
|
|
862
|
+
if (format === "spki") {
|
|
863
|
+
const keyObj = key.type === "private"
|
|
864
|
+
? createPublicKey(createPrivateKey(key._pem))
|
|
865
|
+
: createPublicKey(key._pem);
|
|
866
|
+
const der = keyObj.export({
|
|
867
|
+
type: "spki",
|
|
868
|
+
format: "der",
|
|
869
|
+
});
|
|
870
|
+
return JSON.stringify({ data: der.toString("base64") });
|
|
871
|
+
}
|
|
872
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
873
|
+
}
|
|
874
|
+
case "encrypt": {
|
|
875
|
+
const { algorithm, key, data } = req;
|
|
876
|
+
const rawKey = Buffer.from(key._raw, "base64");
|
|
877
|
+
const plaintext = Buffer.from(data, "base64");
|
|
878
|
+
const algoName = algorithm.name;
|
|
879
|
+
if (algoName === "AES-GCM") {
|
|
880
|
+
const iv = Buffer.from(algorithm.iv, "base64");
|
|
881
|
+
const tagLength = (algorithm.tagLength || 128) / 8;
|
|
882
|
+
const cipher = createCipheriv(`aes-${rawKey.length * 8}-gcm`, rawKey, iv, { authTagLength: tagLength });
|
|
883
|
+
if (algorithm.additionalData) {
|
|
884
|
+
cipher.setAAD(Buffer.from(algorithm.additionalData, "base64"));
|
|
885
|
+
}
|
|
886
|
+
const encrypted = Buffer.concat([
|
|
887
|
+
cipher.update(plaintext),
|
|
888
|
+
cipher.final(),
|
|
889
|
+
]);
|
|
890
|
+
const authTag = cipher.getAuthTag();
|
|
891
|
+
return JSON.stringify({
|
|
892
|
+
data: Buffer.concat([encrypted, authTag]).toString("base64"),
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
if (algoName === "AES-CBC") {
|
|
896
|
+
const iv = Buffer.from(algorithm.iv, "base64");
|
|
897
|
+
const cipher = createCipheriv(`aes-${rawKey.length * 8}-cbc`, rawKey, iv);
|
|
898
|
+
const encrypted = Buffer.concat([
|
|
899
|
+
cipher.update(plaintext),
|
|
900
|
+
cipher.final(),
|
|
901
|
+
]);
|
|
902
|
+
return JSON.stringify({ data: encrypted.toString("base64") });
|
|
903
|
+
}
|
|
904
|
+
throw new Error(`Unsupported encrypt algorithm: ${algoName}`);
|
|
905
|
+
}
|
|
906
|
+
case "decrypt": {
|
|
907
|
+
const { algorithm, key, data } = req;
|
|
908
|
+
const rawKey = Buffer.from(key._raw, "base64");
|
|
909
|
+
const ciphertext = Buffer.from(data, "base64");
|
|
910
|
+
const algoName = algorithm.name;
|
|
911
|
+
if (algoName === "AES-GCM") {
|
|
912
|
+
const iv = Buffer.from(algorithm.iv, "base64");
|
|
913
|
+
const tagLength = (algorithm.tagLength || 128) / 8;
|
|
914
|
+
const encData = ciphertext.subarray(0, ciphertext.length - tagLength);
|
|
915
|
+
const authTag = ciphertext.subarray(ciphertext.length - tagLength);
|
|
916
|
+
const decipher = createDecipheriv(`aes-${rawKey.length * 8}-gcm`, rawKey, iv, { authTagLength: tagLength });
|
|
917
|
+
decipher.setAuthTag(authTag);
|
|
918
|
+
if (algorithm.additionalData) {
|
|
919
|
+
decipher.setAAD(Buffer.from(algorithm.additionalData, "base64"));
|
|
920
|
+
}
|
|
921
|
+
const decrypted = Buffer.concat([
|
|
922
|
+
decipher.update(encData),
|
|
923
|
+
decipher.final(),
|
|
924
|
+
]);
|
|
925
|
+
return JSON.stringify({ data: decrypted.toString("base64") });
|
|
926
|
+
}
|
|
927
|
+
if (algoName === "AES-CBC") {
|
|
928
|
+
const iv = Buffer.from(algorithm.iv, "base64");
|
|
929
|
+
const decipher = createDecipheriv(`aes-${rawKey.length * 8}-cbc`, rawKey, iv);
|
|
930
|
+
const decrypted = Buffer.concat([
|
|
931
|
+
decipher.update(ciphertext),
|
|
932
|
+
decipher.final(),
|
|
933
|
+
]);
|
|
934
|
+
return JSON.stringify({ data: decrypted.toString("base64") });
|
|
935
|
+
}
|
|
936
|
+
throw new Error(`Unsupported decrypt algorithm: ${algoName}`);
|
|
937
|
+
}
|
|
938
|
+
case "sign": {
|
|
939
|
+
const { key, data, algorithm } = req;
|
|
940
|
+
const dataBytes = Buffer.from(data, "base64");
|
|
941
|
+
const algoName = key.algorithm.name;
|
|
942
|
+
if (algoName === "HMAC") {
|
|
943
|
+
const rawKey = Buffer.from(key._raw, "base64");
|
|
944
|
+
const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
|
|
945
|
+
return JSON.stringify({
|
|
946
|
+
data: createHmac(hashAlgo, rawKey)
|
|
947
|
+
.update(dataBytes)
|
|
948
|
+
.digest("base64"),
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
if (algoName === "RSASSA-PKCS1-v1_5") {
|
|
952
|
+
const hashAlgo = normalizeHash(key.algorithm.hash);
|
|
953
|
+
const pkey = createPrivateKey(key._pem);
|
|
954
|
+
return JSON.stringify({
|
|
955
|
+
data: sign(hashAlgo, dataBytes, pkey).toString("base64"),
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
if (algoName === "RSA-PSS") {
|
|
959
|
+
const hashAlgo = normalizeHash(key.algorithm.hash);
|
|
960
|
+
return JSON.stringify({
|
|
961
|
+
data: sign(hashAlgo, dataBytes, {
|
|
962
|
+
key: createPrivateKey(key._pem),
|
|
963
|
+
padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
|
|
964
|
+
saltLength: algorithm.saltLength,
|
|
965
|
+
}).toString("base64"),
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
if (algoName === "ECDSA") {
|
|
969
|
+
const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
|
|
970
|
+
return JSON.stringify({
|
|
971
|
+
data: sign(hashAlgo, dataBytes, createPrivateKey(key._pem)).toString("base64"),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (algoName === "Ed25519" || algoName === "Ed448") {
|
|
975
|
+
if (algoName === "Ed448" &&
|
|
976
|
+
algorithm.context &&
|
|
977
|
+
Buffer.from(algorithm.context, "base64").byteLength > 0) {
|
|
978
|
+
throw new Error("Non zero-length context is not yet supported");
|
|
979
|
+
}
|
|
980
|
+
return JSON.stringify({
|
|
981
|
+
data: sign(null, dataBytes, createPrivateKey(key._pem)).toString("base64"),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
throw new Error(`Unsupported sign algorithm: ${algoName}`);
|
|
985
|
+
}
|
|
986
|
+
case "verify": {
|
|
987
|
+
const { key, signature, data, algorithm } = req;
|
|
988
|
+
const dataBytes = Buffer.from(data, "base64");
|
|
989
|
+
const sigBytes = Buffer.from(signature, "base64");
|
|
990
|
+
const algoName = key.algorithm.name;
|
|
991
|
+
if (algoName === "HMAC") {
|
|
992
|
+
const rawKey = Buffer.from(key._raw, "base64");
|
|
993
|
+
const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
|
|
994
|
+
const expected = createHmac(hashAlgo, rawKey)
|
|
995
|
+
.update(dataBytes)
|
|
996
|
+
.digest();
|
|
997
|
+
if (expected.length !== sigBytes.length)
|
|
998
|
+
return JSON.stringify({ result: false });
|
|
999
|
+
return JSON.stringify({
|
|
1000
|
+
result: timingSafeEqual(expected, sigBytes),
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
if (algoName === "RSASSA-PKCS1-v1_5") {
|
|
1004
|
+
const hashAlgo = normalizeHash(key.algorithm.hash);
|
|
1005
|
+
const pkey = createPublicKey(key._pem);
|
|
1006
|
+
return JSON.stringify({
|
|
1007
|
+
result: verify(hashAlgo, dataBytes, pkey, sigBytes),
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (algoName === "RSA-PSS") {
|
|
1011
|
+
const hashAlgo = normalizeHash(key.algorithm.hash);
|
|
1012
|
+
return JSON.stringify({
|
|
1013
|
+
result: verify(hashAlgo, dataBytes, {
|
|
1014
|
+
key: createPublicKey(key._pem),
|
|
1015
|
+
padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
|
|
1016
|
+
saltLength: algorithm.saltLength,
|
|
1017
|
+
}, sigBytes),
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
if (algoName === "ECDSA") {
|
|
1021
|
+
const hashAlgo = normalizeHash(algorithm.hash ?? key.algorithm.hash);
|
|
1022
|
+
return JSON.stringify({
|
|
1023
|
+
result: verify(hashAlgo, dataBytes, createPublicKey(key._pem), sigBytes),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
if (algoName === "Ed25519" || algoName === "Ed448") {
|
|
1027
|
+
if (algoName === "Ed448" &&
|
|
1028
|
+
algorithm.context &&
|
|
1029
|
+
Buffer.from(algorithm.context, "base64").byteLength > 0) {
|
|
1030
|
+
throw new Error("Non zero-length context is not yet supported");
|
|
1031
|
+
}
|
|
1032
|
+
return JSON.stringify({
|
|
1033
|
+
result: verify(null, dataBytes, createPublicKey(key._pem), sigBytes),
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
throw new Error(`Unsupported verify algorithm: ${algoName}`);
|
|
1037
|
+
}
|
|
1038
|
+
case "deriveBits": {
|
|
1039
|
+
const { algorithm, baseKey, length } = req;
|
|
1040
|
+
const algoName = algorithm.name;
|
|
1041
|
+
if (algoName === "PBKDF2") {
|
|
1042
|
+
const bitLength = Number(length);
|
|
1043
|
+
const byteLength = bitLength / 8;
|
|
1044
|
+
const password = Buffer.from(baseKey._raw, "base64");
|
|
1045
|
+
const salt = Buffer.from(algorithm.salt, "base64");
|
|
1046
|
+
const hash = normalizeHash(algorithm.hash);
|
|
1047
|
+
const derived = pbkdf2Sync(password, salt, algorithm.iterations, byteLength, hash);
|
|
1048
|
+
return JSON.stringify({ data: derived.toString("base64") });
|
|
1049
|
+
}
|
|
1050
|
+
if (algoName === "HKDF") {
|
|
1051
|
+
const bitLength = Number(length);
|
|
1052
|
+
const byteLength = bitLength / 8;
|
|
1053
|
+
const ikm = Buffer.from(baseKey._raw, "base64");
|
|
1054
|
+
const salt = Buffer.from(algorithm.salt, "base64");
|
|
1055
|
+
const info = Buffer.from(algorithm.info, "base64");
|
|
1056
|
+
const hash = normalizeHash(algorithm.hash);
|
|
1057
|
+
const derived = Buffer.from(hkdfSync(hash, ikm, salt, info, byteLength));
|
|
1058
|
+
return JSON.stringify({ data: derived.toString("base64") });
|
|
1059
|
+
}
|
|
1060
|
+
if (algoName === "ECDH" || algoName === "X25519" || algoName === "X448") {
|
|
1061
|
+
const secret = diffieHellman({
|
|
1062
|
+
privateKey: deserializeCryptoKeyObject(baseKey),
|
|
1063
|
+
publicKey: deserializeCryptoKeyObject(algorithm.public),
|
|
1064
|
+
});
|
|
1065
|
+
return JSON.stringify({
|
|
1066
|
+
data: sliceDerivedBits(secret, length).toString("base64"),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error(`Unsupported deriveBits algorithm: ${algoName}`);
|
|
1070
|
+
}
|
|
1071
|
+
case "deriveKey": {
|
|
1072
|
+
const { algorithm, baseKey, derivedKeyAlgorithm, extractable, usages } = req;
|
|
1073
|
+
const algoName = algorithm.name;
|
|
1074
|
+
if (algoName === "PBKDF2") {
|
|
1075
|
+
const keyLengthBits = resolveDerivedKeyLengthBits(derivedKeyAlgorithm, Buffer.from(baseKey._raw, "base64").byteLength * 8);
|
|
1076
|
+
const byteLength = keyLengthBits / 8;
|
|
1077
|
+
const password = Buffer.from(baseKey._raw, "base64");
|
|
1078
|
+
const salt = Buffer.from(algorithm.salt, "base64");
|
|
1079
|
+
const hash = normalizeHash(algorithm.hash);
|
|
1080
|
+
const derived = pbkdf2Sync(password, salt, algorithm.iterations, byteLength, hash);
|
|
1081
|
+
return JSON.stringify({ key: deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, derived) });
|
|
1082
|
+
}
|
|
1083
|
+
if (algoName === "HKDF") {
|
|
1084
|
+
const keyLengthBits = resolveDerivedKeyLengthBits(derivedKeyAlgorithm, Buffer.from(baseKey._raw, "base64").byteLength * 8);
|
|
1085
|
+
const byteLength = keyLengthBits / 8;
|
|
1086
|
+
const ikm = Buffer.from(baseKey._raw, "base64");
|
|
1087
|
+
const salt = Buffer.from(algorithm.salt, "base64");
|
|
1088
|
+
const info = Buffer.from(algorithm.info, "base64");
|
|
1089
|
+
const hash = normalizeHash(algorithm.hash);
|
|
1090
|
+
const derived = Buffer.from(hkdfSync(hash, ikm, salt, info, byteLength));
|
|
1091
|
+
return JSON.stringify({ key: deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, derived) });
|
|
1092
|
+
}
|
|
1093
|
+
if (algoName === "ECDH" || algoName === "X25519" || algoName === "X448") {
|
|
1094
|
+
const secret = diffieHellman({
|
|
1095
|
+
privateKey: deserializeCryptoKeyObject(baseKey),
|
|
1096
|
+
publicKey: deserializeCryptoKeyObject(algorithm.public),
|
|
1097
|
+
});
|
|
1098
|
+
return JSON.stringify({
|
|
1099
|
+
key: deriveSecretKeyData(derivedKeyAlgorithm, extractable, usages, secret),
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
throw new Error(`Unsupported deriveKey algorithm: ${algoName}`);
|
|
1103
|
+
}
|
|
1104
|
+
case "wrapKey": {
|
|
1105
|
+
const { format, key, wrappingKey, wrapAlgorithm } = req;
|
|
1106
|
+
const exported = JSON.parse(handlers[K.cryptoSubtle](JSON.stringify({
|
|
1107
|
+
op: "exportKey",
|
|
1108
|
+
format,
|
|
1109
|
+
key,
|
|
1110
|
+
})));
|
|
1111
|
+
const keyData = format === "jwk"
|
|
1112
|
+
? Buffer.from(JSON.stringify(exported.jwk), "utf8")
|
|
1113
|
+
: decodeBridgeBuffer(exported.data);
|
|
1114
|
+
if (wrapAlgorithm.name === "AES-KW") {
|
|
1115
|
+
const wrappingBytes = decodeBridgeBuffer(wrappingKey._raw);
|
|
1116
|
+
const cipherName = `id-aes${wrappingBytes.byteLength * 8}-wrap`;
|
|
1117
|
+
const cipher = createCipheriv(cipherName, wrappingBytes, Buffer.alloc(8, 0xa6));
|
|
1118
|
+
return JSON.stringify({
|
|
1119
|
+
data: Buffer.concat([cipher.update(keyData), cipher.final()]).toString("base64"),
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
if (wrapAlgorithm.name === "RSA-OAEP") {
|
|
1123
|
+
return JSON.stringify({
|
|
1124
|
+
data: publicEncrypt({
|
|
1125
|
+
key: createPublicKey(wrappingKey._pem),
|
|
1126
|
+
oaepHash: normalizeHash(wrappingKey.algorithm.hash),
|
|
1127
|
+
oaepLabel: wrapAlgorithm.label
|
|
1128
|
+
? decodeBridgeBuffer(wrapAlgorithm.label)
|
|
1129
|
+
: undefined,
|
|
1130
|
+
}, keyData).toString("base64"),
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
if (wrapAlgorithm.name === "AES-CTR" ||
|
|
1134
|
+
wrapAlgorithm.name === "AES-CBC" ||
|
|
1135
|
+
wrapAlgorithm.name === "AES-GCM") {
|
|
1136
|
+
const wrappingBytes = decodeBridgeBuffer(wrappingKey._raw);
|
|
1137
|
+
const algorithmName = wrapAlgorithm.name === "AES-CTR"
|
|
1138
|
+
? `aes-${wrappingBytes.byteLength * 8}-ctr`
|
|
1139
|
+
: wrapAlgorithm.name === "AES-CBC"
|
|
1140
|
+
? `aes-${wrappingBytes.byteLength * 8}-cbc`
|
|
1141
|
+
: `aes-${wrappingBytes.byteLength * 8}-gcm`;
|
|
1142
|
+
const iv = wrapAlgorithm.name === "AES-CTR"
|
|
1143
|
+
? decodeBridgeBuffer(wrapAlgorithm.counter)
|
|
1144
|
+
: decodeBridgeBuffer(wrapAlgorithm.iv);
|
|
1145
|
+
const cipher = createCipheriv(algorithmName, wrappingBytes, iv, wrapAlgorithm.name === "AES-GCM"
|
|
1146
|
+
? { authTagLength: (wrapAlgorithm.tagLength || 128) / 8 }
|
|
1147
|
+
: undefined);
|
|
1148
|
+
if (wrapAlgorithm.name === "AES-GCM" && wrapAlgorithm.additionalData) {
|
|
1149
|
+
cipher.setAAD?.(decodeBridgeBuffer(wrapAlgorithm.additionalData));
|
|
1150
|
+
}
|
|
1151
|
+
const encrypted = Buffer.concat([cipher.update(keyData), cipher.final()]);
|
|
1152
|
+
const payload = wrapAlgorithm.name === "AES-GCM"
|
|
1153
|
+
? Buffer.concat([encrypted, cipher.getAuthTag?.() ?? Buffer.alloc(0)])
|
|
1154
|
+
: encrypted;
|
|
1155
|
+
return JSON.stringify({ data: payload.toString("base64") });
|
|
1156
|
+
}
|
|
1157
|
+
throw new Error(`Unsupported wrap algorithm: ${wrapAlgorithm.name}`);
|
|
1158
|
+
}
|
|
1159
|
+
case "unwrapKey": {
|
|
1160
|
+
const { format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, usages, } = req;
|
|
1161
|
+
let unwrapped;
|
|
1162
|
+
if (unwrapAlgorithm.name === "AES-KW") {
|
|
1163
|
+
const unwrappingBytes = decodeBridgeBuffer(unwrappingKey._raw);
|
|
1164
|
+
const cipherName = `id-aes${unwrappingBytes.byteLength * 8}-wrap`;
|
|
1165
|
+
const decipher = createDecipheriv(cipherName, unwrappingBytes, Buffer.alloc(8, 0xa6));
|
|
1166
|
+
unwrapped = Buffer.concat([
|
|
1167
|
+
decipher.update(decodeBridgeBuffer(wrappedKey)),
|
|
1168
|
+
decipher.final(),
|
|
1169
|
+
]);
|
|
1170
|
+
}
|
|
1171
|
+
else if (unwrapAlgorithm.name === "RSA-OAEP") {
|
|
1172
|
+
unwrapped = privateDecrypt({
|
|
1173
|
+
key: createPrivateKey(unwrappingKey._pem),
|
|
1174
|
+
oaepHash: normalizeHash(unwrappingKey.algorithm.hash),
|
|
1175
|
+
oaepLabel: unwrapAlgorithm.label
|
|
1176
|
+
? decodeBridgeBuffer(unwrapAlgorithm.label)
|
|
1177
|
+
: undefined,
|
|
1178
|
+
}, decodeBridgeBuffer(wrappedKey));
|
|
1179
|
+
}
|
|
1180
|
+
else if (unwrapAlgorithm.name === "AES-CTR" ||
|
|
1181
|
+
unwrapAlgorithm.name === "AES-CBC" ||
|
|
1182
|
+
unwrapAlgorithm.name === "AES-GCM") {
|
|
1183
|
+
const unwrappingBytes = decodeBridgeBuffer(unwrappingKey._raw);
|
|
1184
|
+
const algorithmName = unwrapAlgorithm.name === "AES-CTR"
|
|
1185
|
+
? `aes-${unwrappingBytes.byteLength * 8}-ctr`
|
|
1186
|
+
: unwrapAlgorithm.name === "AES-CBC"
|
|
1187
|
+
? `aes-${unwrappingBytes.byteLength * 8}-cbc`
|
|
1188
|
+
: `aes-${unwrappingBytes.byteLength * 8}-gcm`;
|
|
1189
|
+
const iv = unwrapAlgorithm.name === "AES-CTR"
|
|
1190
|
+
? decodeBridgeBuffer(unwrapAlgorithm.counter)
|
|
1191
|
+
: decodeBridgeBuffer(unwrapAlgorithm.iv);
|
|
1192
|
+
const wrappedBytes = decodeBridgeBuffer(wrappedKey);
|
|
1193
|
+
const decipher = createDecipheriv(algorithmName, unwrappingBytes, iv, unwrapAlgorithm.name === "AES-GCM"
|
|
1194
|
+
? { authTagLength: (unwrapAlgorithm.tagLength || 128) / 8 }
|
|
1195
|
+
: undefined);
|
|
1196
|
+
let ciphertext = wrappedBytes;
|
|
1197
|
+
if (unwrapAlgorithm.name === "AES-GCM") {
|
|
1198
|
+
const tagLength = (unwrapAlgorithm.tagLength || 128) / 8;
|
|
1199
|
+
ciphertext = wrappedBytes.subarray(0, wrappedBytes.byteLength - tagLength);
|
|
1200
|
+
decipher.setAuthTag?.(wrappedBytes.subarray(wrappedBytes.byteLength - tagLength));
|
|
1201
|
+
if (unwrapAlgorithm.additionalData) {
|
|
1202
|
+
decipher.setAAD?.(decodeBridgeBuffer(unwrapAlgorithm.additionalData));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
unwrapped = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
throw new Error(`Unsupported unwrap algorithm: ${unwrapAlgorithm.name}`);
|
|
1209
|
+
}
|
|
1210
|
+
return handlers[K.cryptoSubtle](JSON.stringify({
|
|
1211
|
+
op: "importKey",
|
|
1212
|
+
format,
|
|
1213
|
+
keyData: format === "jwk"
|
|
1214
|
+
? JSON.parse(unwrapped.toString("utf8"))
|
|
1215
|
+
: unwrapped.toString("base64"),
|
|
1216
|
+
algorithm: unwrappedKeyAlgorithm,
|
|
1217
|
+
extractable,
|
|
1218
|
+
usages,
|
|
1219
|
+
}));
|
|
1220
|
+
}
|
|
1221
|
+
default:
|
|
1222
|
+
throw new Error(`Unsupported subtle operation: ${req.op}`);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
const dispose = () => {
|
|
1226
|
+
cipherSessions.clear();
|
|
1227
|
+
diffieHellmanSessions.clear();
|
|
1228
|
+
};
|
|
1229
|
+
return { handlers, dispose };
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Build net socket bridge handlers.
|
|
1233
|
+
*
|
|
1234
|
+
* All TCP operations route through kernel sockets (loopback or external via
|
|
1235
|
+
* the host adapter).
|
|
1236
|
+
* Call dispose() when the execution ends to destroy all open sockets.
|
|
1237
|
+
*/
|
|
1238
|
+
export function buildNetworkSocketBridgeHandlers(deps) {
|
|
1239
|
+
const { socketTable, pid } = deps;
|
|
1240
|
+
if (!socketTable || pid === undefined) {
|
|
1241
|
+
throw new Error("buildNetworkSocketBridgeHandlers requires a kernel socketTable and pid");
|
|
1242
|
+
}
|
|
1243
|
+
return buildKernelSocketBridgeHandlers(deps.dispatch, socketTable, pid);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Build bridge handlers that route net socket operations through the
|
|
1247
|
+
* kernel SocketTable. Data flows through kernel send/recv, connections
|
|
1248
|
+
* route through loopback (paired sockets) or external (host adapter).
|
|
1249
|
+
*/
|
|
1250
|
+
function buildKernelSocketBridgeHandlers(dispatch, socketTable, pid) {
|
|
1251
|
+
const handlers = {};
|
|
1252
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
1253
|
+
const NET_BRIDGE_TIMEOUT_SENTINEL = "__secure_exec_net_timeout__";
|
|
1254
|
+
// Track active kernel socket IDs for cleanup
|
|
1255
|
+
const activeSocketIds = new Set();
|
|
1256
|
+
const activeServerIds = new Set();
|
|
1257
|
+
const activeDgramIds = new Set();
|
|
1258
|
+
// Track TLS-upgraded sockets that bypass kernel recv (host-side TLS)
|
|
1259
|
+
const tlsSockets = new Map();
|
|
1260
|
+
const loopbackTlsTransports = new Map();
|
|
1261
|
+
const loopbackTlsClientHello = new Map();
|
|
1262
|
+
const pendingConnects = new Map();
|
|
1263
|
+
function addressFamily(host) {
|
|
1264
|
+
return host?.includes(":") ? "IPv6" : "IPv4";
|
|
1265
|
+
}
|
|
1266
|
+
function decodeTlsMaterial(value) {
|
|
1267
|
+
if (value === undefined) {
|
|
1268
|
+
return undefined;
|
|
1269
|
+
}
|
|
1270
|
+
const decodeOne = (entry) => entry.kind === "buffer" ? Buffer.from(entry.data, "base64") : entry.data;
|
|
1271
|
+
return Array.isArray(value) ? value.map(decodeOne) : decodeOne(value);
|
|
1272
|
+
}
|
|
1273
|
+
function buildHostTlsOptions(options) {
|
|
1274
|
+
const hostOptions = {};
|
|
1275
|
+
const key = decodeTlsMaterial(options.key);
|
|
1276
|
+
const cert = decodeTlsMaterial(options.cert);
|
|
1277
|
+
const ca = decodeTlsMaterial(options.ca);
|
|
1278
|
+
if (key !== undefined)
|
|
1279
|
+
hostOptions.key = key;
|
|
1280
|
+
if (cert !== undefined)
|
|
1281
|
+
hostOptions.cert = cert;
|
|
1282
|
+
if (ca !== undefined)
|
|
1283
|
+
hostOptions.ca = ca;
|
|
1284
|
+
if (typeof options.passphrase === "string")
|
|
1285
|
+
hostOptions.passphrase = options.passphrase;
|
|
1286
|
+
if (typeof options.ciphers === "string")
|
|
1287
|
+
hostOptions.ciphers = options.ciphers;
|
|
1288
|
+
if (typeof options.session === "string")
|
|
1289
|
+
hostOptions.session = Buffer.from(options.session, "base64");
|
|
1290
|
+
if (Array.isArray(options.ALPNProtocols) && options.ALPNProtocols.length > 0) {
|
|
1291
|
+
hostOptions.ALPNProtocols = [...options.ALPNProtocols];
|
|
1292
|
+
}
|
|
1293
|
+
if (typeof options.minVersion === "string")
|
|
1294
|
+
hostOptions.minVersion = options.minVersion;
|
|
1295
|
+
if (typeof options.maxVersion === "string")
|
|
1296
|
+
hostOptions.maxVersion = options.maxVersion;
|
|
1297
|
+
if (typeof options.servername === "string")
|
|
1298
|
+
hostOptions.servername = options.servername;
|
|
1299
|
+
if (typeof options.requestCert === "boolean")
|
|
1300
|
+
hostOptions.requestCert = options.requestCert;
|
|
1301
|
+
return hostOptions;
|
|
1302
|
+
}
|
|
1303
|
+
function getLoopbackTlsKey(socketId, peerId) {
|
|
1304
|
+
return socketId < peerId ? `${socketId}:${peerId}` : `${peerId}:${socketId}`;
|
|
1305
|
+
}
|
|
1306
|
+
function createTlsTransportEndpoint(readable, writable) {
|
|
1307
|
+
const duplex = new Duplex({
|
|
1308
|
+
read() {
|
|
1309
|
+
let chunk;
|
|
1310
|
+
while ((chunk = readable.read()) !== null) {
|
|
1311
|
+
if (!this.push(chunk)) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
write(chunk, _encoding, callback) {
|
|
1317
|
+
if (!writable.write(chunk)) {
|
|
1318
|
+
writable.once("drain", callback);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
callback();
|
|
1322
|
+
},
|
|
1323
|
+
final(callback) {
|
|
1324
|
+
writable.end();
|
|
1325
|
+
callback();
|
|
1326
|
+
},
|
|
1327
|
+
destroy(error, callback) {
|
|
1328
|
+
readable.destroy(error ?? undefined);
|
|
1329
|
+
writable.destroy(error ?? undefined);
|
|
1330
|
+
callback(error ?? null);
|
|
1331
|
+
},
|
|
1332
|
+
});
|
|
1333
|
+
readable.on("readable", () => {
|
|
1334
|
+
let chunk;
|
|
1335
|
+
while ((chunk = readable.read()) !== null) {
|
|
1336
|
+
if (!duplex.push(chunk)) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
readable.on("end", () => duplex.push(null));
|
|
1342
|
+
readable.on("error", (error) => duplex.destroy(error));
|
|
1343
|
+
return duplex;
|
|
1344
|
+
}
|
|
1345
|
+
function getLoopbackTlsTransport(socket) {
|
|
1346
|
+
if (socket.peerId === undefined) {
|
|
1347
|
+
throw new Error(`Socket ${socket.id} has no loopback peer for TLS upgrade`);
|
|
1348
|
+
}
|
|
1349
|
+
const key = getLoopbackTlsKey(socket.id, socket.peerId);
|
|
1350
|
+
let pair = loopbackTlsTransports.get(key);
|
|
1351
|
+
if (!pair) {
|
|
1352
|
+
const aIn = new PassThrough();
|
|
1353
|
+
const bIn = new PassThrough();
|
|
1354
|
+
pair = {
|
|
1355
|
+
a: createTlsTransportEndpoint(aIn, bIn),
|
|
1356
|
+
b: createTlsTransportEndpoint(bIn, aIn),
|
|
1357
|
+
};
|
|
1358
|
+
loopbackTlsTransports.set(key, pair);
|
|
1359
|
+
}
|
|
1360
|
+
return socket.id < socket.peerId ? pair.a : pair.b;
|
|
1361
|
+
}
|
|
1362
|
+
function cleanupLoopbackTlsTransport(socketId, peerId) {
|
|
1363
|
+
if (peerId === undefined) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (tlsSockets.has(socketId) || tlsSockets.has(peerId)) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const key = getLoopbackTlsKey(socketId, peerId);
|
|
1370
|
+
const pair = loopbackTlsTransports.get(key);
|
|
1371
|
+
if (!pair) {
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
pair.a.destroy();
|
|
1375
|
+
pair.b.destroy();
|
|
1376
|
+
loopbackTlsTransports.delete(key);
|
|
1377
|
+
loopbackTlsClientHello.delete(key);
|
|
1378
|
+
}
|
|
1379
|
+
function serializeTlsState(tlsSocket) {
|
|
1380
|
+
let cipher = null;
|
|
1381
|
+
try {
|
|
1382
|
+
const details = tlsSocket.getCipher();
|
|
1383
|
+
if (details) {
|
|
1384
|
+
const standardName = details.standardName ?? details.name;
|
|
1385
|
+
cipher = {
|
|
1386
|
+
name: details.name,
|
|
1387
|
+
standardName,
|
|
1388
|
+
version: details.version,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
cipher = null;
|
|
1394
|
+
}
|
|
1395
|
+
return JSON.stringify({
|
|
1396
|
+
authorized: tlsSocket.authorized === true,
|
|
1397
|
+
authorizationError: typeof tlsSocket.authorizationError === "string"
|
|
1398
|
+
? tlsSocket.authorizationError
|
|
1399
|
+
: undefined,
|
|
1400
|
+
alpnProtocol: tlsSocket.alpnProtocol || false,
|
|
1401
|
+
servername: tlsSocket.servername,
|
|
1402
|
+
protocol: tlsSocket.getProtocol?.() ?? null,
|
|
1403
|
+
sessionReused: tlsSocket.isSessionReused?.() === true,
|
|
1404
|
+
cipher,
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function serializeTlsBridgeValue(value, seen = new Map()) {
|
|
1408
|
+
if (value === undefined) {
|
|
1409
|
+
return { type: "undefined" };
|
|
1410
|
+
}
|
|
1411
|
+
if (value === null ||
|
|
1412
|
+
typeof value === "boolean" ||
|
|
1413
|
+
typeof value === "number" ||
|
|
1414
|
+
typeof value === "string") {
|
|
1415
|
+
return value;
|
|
1416
|
+
}
|
|
1417
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
1418
|
+
return {
|
|
1419
|
+
type: "buffer",
|
|
1420
|
+
data: Buffer.from(value).toString("base64"),
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
if (Array.isArray(value)) {
|
|
1424
|
+
return {
|
|
1425
|
+
type: "array",
|
|
1426
|
+
value: value.map((entry) => serializeTlsBridgeValue(entry, seen)),
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (typeof value === "object") {
|
|
1430
|
+
const existingId = seen.get(value);
|
|
1431
|
+
if (existingId !== undefined) {
|
|
1432
|
+
return { type: "ref", id: existingId };
|
|
1433
|
+
}
|
|
1434
|
+
const id = seen.size + 1;
|
|
1435
|
+
seen.set(value, id);
|
|
1436
|
+
const serialized = {};
|
|
1437
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1438
|
+
serialized[key] = serializeTlsBridgeValue(entry, seen);
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
type: "object",
|
|
1442
|
+
id,
|
|
1443
|
+
value: serialized,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
return String(value);
|
|
1447
|
+
}
|
|
1448
|
+
function serializeTlsError(error, tlsSocket) {
|
|
1449
|
+
const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
|
|
1450
|
+
const payload = {
|
|
1451
|
+
message: err.message,
|
|
1452
|
+
name: err.name,
|
|
1453
|
+
stack: err.stack,
|
|
1454
|
+
};
|
|
1455
|
+
const code = err.code;
|
|
1456
|
+
if (typeof code === "string") {
|
|
1457
|
+
payload.code = code;
|
|
1458
|
+
}
|
|
1459
|
+
if (tlsSocket) {
|
|
1460
|
+
payload.authorized = tlsSocket.authorized === true;
|
|
1461
|
+
if (typeof tlsSocket.authorizationError === "string") {
|
|
1462
|
+
payload.authorizationError = tlsSocket.authorizationError;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return JSON.stringify(payload);
|
|
1466
|
+
}
|
|
1467
|
+
function serializeSocketInfo(socketId) {
|
|
1468
|
+
const socket = socketTable.get(socketId);
|
|
1469
|
+
const localAddr = socket?.localAddr;
|
|
1470
|
+
const remoteAddr = socket?.remoteAddr;
|
|
1471
|
+
return {
|
|
1472
|
+
localAddress: localAddr && typeof localAddr === "object" && "host" in localAddr
|
|
1473
|
+
? localAddr.host
|
|
1474
|
+
: localAddr && typeof localAddr === "object" && "path" in localAddr
|
|
1475
|
+
? localAddr.path
|
|
1476
|
+
: "0.0.0.0",
|
|
1477
|
+
localPort: localAddr && typeof localAddr === "object" && "port" in localAddr
|
|
1478
|
+
? localAddr.port
|
|
1479
|
+
: 0,
|
|
1480
|
+
localFamily: localAddr && typeof localAddr === "object" && "host" in localAddr
|
|
1481
|
+
? addressFamily(localAddr.host)
|
|
1482
|
+
: localAddr && typeof localAddr === "object" && "path" in localAddr
|
|
1483
|
+
? "Unix"
|
|
1484
|
+
: "IPv4",
|
|
1485
|
+
...(localAddr && typeof localAddr === "object" && "path" in localAddr
|
|
1486
|
+
? { localPath: localAddr.path }
|
|
1487
|
+
: {}),
|
|
1488
|
+
...(remoteAddr && typeof remoteAddr === "object" && "host" in remoteAddr
|
|
1489
|
+
? {
|
|
1490
|
+
remoteAddress: remoteAddr.host,
|
|
1491
|
+
remotePort: remoteAddr.port,
|
|
1492
|
+
remoteFamily: addressFamily(remoteAddr.host),
|
|
1493
|
+
}
|
|
1494
|
+
: remoteAddr && typeof remoteAddr === "object" && "path" in remoteAddr
|
|
1495
|
+
? {
|
|
1496
|
+
remoteAddress: remoteAddr.path,
|
|
1497
|
+
remoteFamily: "Unix",
|
|
1498
|
+
remotePath: remoteAddr.path,
|
|
1499
|
+
}
|
|
1500
|
+
: {}),
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function getBackingSocket(socketId) {
|
|
1504
|
+
const tlsSocket = tlsSockets.get(socketId);
|
|
1505
|
+
if (tlsSocket) {
|
|
1506
|
+
return tlsSocket;
|
|
1507
|
+
}
|
|
1508
|
+
const socket = socketTable.get(socketId);
|
|
1509
|
+
const hostSocket = socket?.hostSocket;
|
|
1510
|
+
return hostSocket?.socket;
|
|
1511
|
+
}
|
|
1512
|
+
function dispatchAsync(socketId, event, data) {
|
|
1513
|
+
setTimeout(() => {
|
|
1514
|
+
dispatch(socketId, event, data);
|
|
1515
|
+
}, 0);
|
|
1516
|
+
}
|
|
1517
|
+
/** Background read pump: polls kernel recv() and dispatches data/end/close. */
|
|
1518
|
+
function startReadPump(socketId) {
|
|
1519
|
+
const pump = async () => {
|
|
1520
|
+
try {
|
|
1521
|
+
while (activeSocketIds.has(socketId)) {
|
|
1522
|
+
// Try to read data
|
|
1523
|
+
let data;
|
|
1524
|
+
try {
|
|
1525
|
+
data = socketTable.recv(socketId, 65536, 0);
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
// Socket closed or error — stop pump
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
if (data !== null) {
|
|
1532
|
+
dispatchAsync(socketId, "data", Buffer.from(data).toString("base64"));
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
// No data — check if EOF
|
|
1536
|
+
const socket = socketTable.get(socketId);
|
|
1537
|
+
if (!socket)
|
|
1538
|
+
break;
|
|
1539
|
+
if (socket.state === "closed" || socket.state === "read-closed") {
|
|
1540
|
+
dispatchAsync(socketId, "end");
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
if (socket.peerWriteClosed || (socket.peerId === undefined && !socket.external)) {
|
|
1544
|
+
dispatchAsync(socketId, "end");
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
// For external sockets, check hostSocket EOF via readBuffer state
|
|
1548
|
+
if (socket.external && socket.readBuffer.length === 0 && socket.peerWriteClosed) {
|
|
1549
|
+
dispatchAsync(socketId, "end");
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
// Wait for data to arrive
|
|
1553
|
+
const handle = socket.readWaiters.enqueue();
|
|
1554
|
+
await handle.wait();
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
catch {
|
|
1558
|
+
// Socket destroyed during pump — expected
|
|
1559
|
+
}
|
|
1560
|
+
// Dispatch close if socket was active
|
|
1561
|
+
if (activeSocketIds.delete(socketId)) {
|
|
1562
|
+
dispatchAsync(socketId, "close");
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
pump();
|
|
1566
|
+
}
|
|
1567
|
+
// Connect — create kernel socket and start async connect + read pump
|
|
1568
|
+
handlers[K.netSocketConnectRaw] = (optionsJson) => {
|
|
1569
|
+
const options = parseJsonWithLimit("net.socket.connect options", String(optionsJson), 128 * 1024);
|
|
1570
|
+
const isUnixPath = typeof options.path === "string" && options.path.length > 0;
|
|
1571
|
+
const host = String(options.host ?? "127.0.0.1");
|
|
1572
|
+
const port = Number(options.port ?? 0);
|
|
1573
|
+
const socketId = socketTable.create(isUnixPath ? AF_UNIX : host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
|
|
1574
|
+
activeSocketIds.add(socketId);
|
|
1575
|
+
// Async connect completion is polled from the isolate via waitConnectRaw.
|
|
1576
|
+
pendingConnects.set(socketId, socketTable.connect(socketId, isUnixPath ? { path: options.path } : { host, port }).then(() => ({ ok: true }), (error) => ({
|
|
1577
|
+
ok: false,
|
|
1578
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1579
|
+
})));
|
|
1580
|
+
return socketId;
|
|
1581
|
+
};
|
|
1582
|
+
handlers[K.netSocketWaitConnectRaw] = async (socketId) => {
|
|
1583
|
+
const id = Number(socketId);
|
|
1584
|
+
const pending = pendingConnects.get(id);
|
|
1585
|
+
try {
|
|
1586
|
+
if (pending) {
|
|
1587
|
+
const result = await pending;
|
|
1588
|
+
if (!result.ok) {
|
|
1589
|
+
throw new Error(result.error);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return JSON.stringify(serializeSocketInfo(id));
|
|
1593
|
+
}
|
|
1594
|
+
finally {
|
|
1595
|
+
pendingConnects.delete(id);
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
handlers[K.netSocketReadRaw] = (socketId) => {
|
|
1599
|
+
const id = Number(socketId);
|
|
1600
|
+
if (!activeSocketIds.has(id)) {
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
try {
|
|
1604
|
+
const chunk = socketTable.recv(id, 65536, 0);
|
|
1605
|
+
if (chunk !== null) {
|
|
1606
|
+
return Buffer.from(chunk).toString("base64");
|
|
1607
|
+
}
|
|
1608
|
+
const socket = socketTable.get(id);
|
|
1609
|
+
if (!socket ||
|
|
1610
|
+
socket.state === "closed" ||
|
|
1611
|
+
socket.state === "read-closed" ||
|
|
1612
|
+
socket.peerWriteClosed) {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
return NET_BRIDGE_TIMEOUT_SENTINEL;
|
|
1616
|
+
}
|
|
1617
|
+
catch (error) {
|
|
1618
|
+
if (error instanceof Error && error.message.includes("EAGAIN")) {
|
|
1619
|
+
return NET_BRIDGE_TIMEOUT_SENTINEL;
|
|
1620
|
+
}
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
handlers[K.netSocketSetNoDelayRaw] = (socketId, enable) => {
|
|
1625
|
+
const id = Number(socketId);
|
|
1626
|
+
socketTable.setsockopt(id, IPPROTO_TCP, TCP_NODELAY, enable ? 1 : 0);
|
|
1627
|
+
getBackingSocket(id)?.setNoDelay(Boolean(enable));
|
|
1628
|
+
};
|
|
1629
|
+
handlers[K.netSocketSetKeepAliveRaw] = (socketId, enable, initialDelaySeconds) => {
|
|
1630
|
+
const id = Number(socketId);
|
|
1631
|
+
const delaySeconds = Math.max(0, Number(initialDelaySeconds) || 0);
|
|
1632
|
+
socketTable.setsockopt(id, SOL_SOCKET, SO_KEEPALIVE, enable ? 1 : 0);
|
|
1633
|
+
getBackingSocket(id)?.setKeepAlive(Boolean(enable), delaySeconds * 1000);
|
|
1634
|
+
};
|
|
1635
|
+
// Write — send data through kernel socket
|
|
1636
|
+
handlers[K.netSocketWriteRaw] = (socketId, dataBase64) => {
|
|
1637
|
+
const id = Number(socketId);
|
|
1638
|
+
// TLS-upgraded sockets write directly to host TLS socket
|
|
1639
|
+
const tlsSocket = tlsSockets.get(id);
|
|
1640
|
+
if (tlsSocket) {
|
|
1641
|
+
tlsSocket.write(Buffer.from(String(dataBase64), "base64"));
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const data = Buffer.from(String(dataBase64), "base64");
|
|
1645
|
+
socketTable.send(id, new Uint8Array(data), 0);
|
|
1646
|
+
};
|
|
1647
|
+
// End — half-close write side
|
|
1648
|
+
handlers[K.netSocketEndRaw] = (socketId) => {
|
|
1649
|
+
const id = Number(socketId);
|
|
1650
|
+
const tlsSocket = tlsSockets.get(id);
|
|
1651
|
+
if (tlsSocket) {
|
|
1652
|
+
tlsSocket.end();
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
socketTable.shutdown(id, "write");
|
|
1657
|
+
}
|
|
1658
|
+
catch {
|
|
1659
|
+
// Socket may already be closed
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
// Destroy — close kernel socket
|
|
1663
|
+
handlers[K.netSocketDestroyRaw] = (socketId) => {
|
|
1664
|
+
const id = Number(socketId);
|
|
1665
|
+
const socket = socketTable.get(id);
|
|
1666
|
+
const tlsSocket = tlsSockets.get(id);
|
|
1667
|
+
if (tlsSocket) {
|
|
1668
|
+
tlsSocket.destroy();
|
|
1669
|
+
tlsSockets.delete(id);
|
|
1670
|
+
}
|
|
1671
|
+
cleanupLoopbackTlsTransport(id, socket?.peerId);
|
|
1672
|
+
socketTable.get(id)?.readWaiters.wakeAll();
|
|
1673
|
+
if (activeSocketIds.has(id)) {
|
|
1674
|
+
activeSocketIds.delete(id);
|
|
1675
|
+
try {
|
|
1676
|
+
socketTable.close(id, pid);
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
// Already closed
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
// TLS upgrade — for external kernel sockets, unwrap the host socket
|
|
1684
|
+
// and wrap with TLS. Loopback sockets cannot be TLS-upgraded (no real TCP).
|
|
1685
|
+
handlers[K.netSocketUpgradeTlsRaw] = (socketId, optionsJson) => {
|
|
1686
|
+
const id = Number(socketId);
|
|
1687
|
+
const socket = socketTable.get(id);
|
|
1688
|
+
if (!socket)
|
|
1689
|
+
throw new Error(`Socket ${id} not found for TLS upgrade`);
|
|
1690
|
+
const options = optionsJson
|
|
1691
|
+
? parseJsonWithLimit("net.socket.upgradeTls options", String(optionsJson), 256 * 1024)
|
|
1692
|
+
: {};
|
|
1693
|
+
const hostTlsOptions = buildHostTlsOptions(options);
|
|
1694
|
+
const peerId = socket.peerId;
|
|
1695
|
+
const loopbackTlsKey = peerId === undefined ? undefined : getLoopbackTlsKey(id, peerId);
|
|
1696
|
+
if (!options.isServer && loopbackTlsKey) {
|
|
1697
|
+
loopbackTlsClientHello.set(loopbackTlsKey, {
|
|
1698
|
+
servername: options.servername,
|
|
1699
|
+
ALPNProtocols: options.ALPNProtocols,
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
let transport;
|
|
1703
|
+
if (socket.external && socket.hostSocket) {
|
|
1704
|
+
const hostSocket = socket.hostSocket;
|
|
1705
|
+
const realSocket = hostSocket.socket;
|
|
1706
|
+
if (!realSocket) {
|
|
1707
|
+
throw new Error(`Socket ${id} has no underlying TCP socket for TLS upgrade`);
|
|
1708
|
+
}
|
|
1709
|
+
socket.hostSocket = undefined;
|
|
1710
|
+
transport = realSocket;
|
|
1711
|
+
}
|
|
1712
|
+
else {
|
|
1713
|
+
transport = getLoopbackTlsTransport(socket);
|
|
1714
|
+
}
|
|
1715
|
+
const tlsSocket = options.isServer
|
|
1716
|
+
? new tls.TLSSocket(transport, {
|
|
1717
|
+
isServer: true,
|
|
1718
|
+
secureContext: tls.createSecureContext(hostTlsOptions),
|
|
1719
|
+
requestCert: options.requestCert === true,
|
|
1720
|
+
rejectUnauthorized: options.rejectUnauthorized === true,
|
|
1721
|
+
})
|
|
1722
|
+
: tls.connect({
|
|
1723
|
+
socket: transport,
|
|
1724
|
+
...hostTlsOptions,
|
|
1725
|
+
rejectUnauthorized: options.rejectUnauthorized !== false,
|
|
1726
|
+
});
|
|
1727
|
+
// Track TLS socket for write/end/destroy bypass
|
|
1728
|
+
tlsSockets.set(id, tlsSocket);
|
|
1729
|
+
tlsSocket.on("secureConnect", () => dispatchAsync(id, "secureConnect", serializeTlsState(tlsSocket)));
|
|
1730
|
+
tlsSocket.on("secure", () => dispatchAsync(id, "secure", serializeTlsState(tlsSocket)));
|
|
1731
|
+
tlsSocket.on("session", (session) => dispatchAsync(id, "session", session.toString("base64")));
|
|
1732
|
+
tlsSocket.on("data", (chunk) => dispatchAsync(id, "data", chunk.toString("base64")));
|
|
1733
|
+
tlsSocket.on("end", () => dispatchAsync(id, "end"));
|
|
1734
|
+
tlsSocket.on("error", (err) => dispatchAsync(id, "error", serializeTlsError(err, tlsSocket)));
|
|
1735
|
+
tlsSocket.on("close", () => {
|
|
1736
|
+
tlsSockets.delete(id);
|
|
1737
|
+
activeSocketIds.delete(id);
|
|
1738
|
+
cleanupLoopbackTlsTransport(id, peerId);
|
|
1739
|
+
dispatchAsync(id, "close");
|
|
1740
|
+
});
|
|
1741
|
+
};
|
|
1742
|
+
handlers[K.netSocketGetTlsClientHelloRaw] = (socketId) => {
|
|
1743
|
+
const id = Number(socketId);
|
|
1744
|
+
const socket = socketTable.get(id);
|
|
1745
|
+
if (!socket || socket.peerId === undefined) {
|
|
1746
|
+
return "{}";
|
|
1747
|
+
}
|
|
1748
|
+
const entry = loopbackTlsClientHello.get(getLoopbackTlsKey(id, socket.peerId));
|
|
1749
|
+
return JSON.stringify(entry ?? {});
|
|
1750
|
+
};
|
|
1751
|
+
handlers[K.netSocketTlsQueryRaw] = (socketId, query, detailed) => {
|
|
1752
|
+
const tlsSocket = tlsSockets.get(Number(socketId));
|
|
1753
|
+
if (!tlsSocket) {
|
|
1754
|
+
return JSON.stringify({ type: "undefined" });
|
|
1755
|
+
}
|
|
1756
|
+
let result;
|
|
1757
|
+
switch (String(query)) {
|
|
1758
|
+
case "getSession":
|
|
1759
|
+
result = tlsSocket.getSession();
|
|
1760
|
+
break;
|
|
1761
|
+
case "isSessionReused":
|
|
1762
|
+
result = tlsSocket.isSessionReused();
|
|
1763
|
+
break;
|
|
1764
|
+
case "getPeerCertificate":
|
|
1765
|
+
result = tlsSocket.getPeerCertificate(Boolean(detailed));
|
|
1766
|
+
break;
|
|
1767
|
+
case "getCertificate":
|
|
1768
|
+
result = tlsSocket.getCertificate();
|
|
1769
|
+
break;
|
|
1770
|
+
case "getProtocol":
|
|
1771
|
+
result = tlsSocket.getProtocol();
|
|
1772
|
+
break;
|
|
1773
|
+
case "getCipher":
|
|
1774
|
+
result = tlsSocket.getCipher();
|
|
1775
|
+
break;
|
|
1776
|
+
default:
|
|
1777
|
+
result = undefined;
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
return JSON.stringify(serializeTlsBridgeValue(result));
|
|
1781
|
+
};
|
|
1782
|
+
handlers[K.tlsGetCiphersRaw] = () => JSON.stringify(tls.getCiphers());
|
|
1783
|
+
handlers[K.netServerListenRaw] = async (optionsJson) => {
|
|
1784
|
+
const options = parseJsonWithLimit("net.server.listen options", String(optionsJson), 128 * 1024);
|
|
1785
|
+
const isUnixPath = typeof options.path === "string" && options.path.length > 0;
|
|
1786
|
+
const host = String(options.host ?? "127.0.0.1");
|
|
1787
|
+
const serverId = socketTable.create(isUnixPath ? AF_UNIX : host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
|
|
1788
|
+
activeServerIds.add(serverId);
|
|
1789
|
+
const socketMode = options.readableAll || options.writableAll
|
|
1790
|
+
? 0o600 |
|
|
1791
|
+
(options.readableAll ? 0o044 : 0) |
|
|
1792
|
+
(options.writableAll ? 0o022 : 0)
|
|
1793
|
+
: undefined;
|
|
1794
|
+
await socketTable.bind(serverId, isUnixPath
|
|
1795
|
+
? { path: options.path }
|
|
1796
|
+
: {
|
|
1797
|
+
host,
|
|
1798
|
+
port: Number(options.port ?? 0),
|
|
1799
|
+
}, socketMode === undefined ? undefined : { mode: socketMode });
|
|
1800
|
+
await socketTable.listen(serverId, Number(options.backlog ?? 511));
|
|
1801
|
+
return JSON.stringify({
|
|
1802
|
+
serverId,
|
|
1803
|
+
address: serializeSocketInfo(serverId),
|
|
1804
|
+
});
|
|
1805
|
+
};
|
|
1806
|
+
handlers[K.netServerAcceptRaw] = (serverId) => {
|
|
1807
|
+
const id = Number(serverId);
|
|
1808
|
+
if (!activeServerIds.has(id)) {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
const listener = socketTable.get(id);
|
|
1812
|
+
if (!listener || listener.state !== "listening") {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
const acceptedId = socketTable.accept(id);
|
|
1816
|
+
if (acceptedId === null) {
|
|
1817
|
+
return NET_BRIDGE_TIMEOUT_SENTINEL;
|
|
1818
|
+
}
|
|
1819
|
+
activeSocketIds.add(acceptedId);
|
|
1820
|
+
return JSON.stringify({
|
|
1821
|
+
socketId: acceptedId,
|
|
1822
|
+
info: serializeSocketInfo(acceptedId),
|
|
1823
|
+
});
|
|
1824
|
+
};
|
|
1825
|
+
handlers[K.netServerCloseRaw] = async (serverId) => {
|
|
1826
|
+
const id = Number(serverId);
|
|
1827
|
+
activeServerIds.delete(id);
|
|
1828
|
+
socketTable.get(id)?.acceptWaiters.wakeAll();
|
|
1829
|
+
try {
|
|
1830
|
+
socketTable.close(id, pid);
|
|
1831
|
+
}
|
|
1832
|
+
catch {
|
|
1833
|
+
// Already closed
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
handlers[K.dgramSocketCreateRaw] = (type) => {
|
|
1837
|
+
const socketType = String(type);
|
|
1838
|
+
const domain = socketType === "udp6" ? AF_INET6 : AF_INET;
|
|
1839
|
+
const socketId = socketTable.create(domain, SOCK_DGRAM, 0, pid);
|
|
1840
|
+
activeDgramIds.add(socketId);
|
|
1841
|
+
return socketId;
|
|
1842
|
+
};
|
|
1843
|
+
handlers[K.dgramSocketBindRaw] = async (socketId, optionsJson) => {
|
|
1844
|
+
const id = Number(socketId);
|
|
1845
|
+
const socket = socketTable.get(id);
|
|
1846
|
+
if (!socket) {
|
|
1847
|
+
throw new Error(`UDP socket ${id} not found`);
|
|
1848
|
+
}
|
|
1849
|
+
const options = parseJsonWithLimit("dgram.socket.bind options", String(optionsJson), 128 * 1024);
|
|
1850
|
+
const host = String(options.address ??
|
|
1851
|
+
(socket.domain === AF_INET6 ? "::" : "0.0.0.0"));
|
|
1852
|
+
await socketTable.bind(id, {
|
|
1853
|
+
host,
|
|
1854
|
+
port: Number(options.port ?? 0),
|
|
1855
|
+
});
|
|
1856
|
+
return JSON.stringify(serializeSocketInfo(id));
|
|
1857
|
+
};
|
|
1858
|
+
handlers[K.dgramSocketRecvRaw] = (socketId) => {
|
|
1859
|
+
const id = Number(socketId);
|
|
1860
|
+
if (!activeDgramIds.has(id)) {
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
try {
|
|
1864
|
+
const socket = socketTable.get(id);
|
|
1865
|
+
if (!socket || socket.state === "closed") {
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
const message = socketTable.recvFrom(id, 65535, 0);
|
|
1869
|
+
if (message === null) {
|
|
1870
|
+
return NET_BRIDGE_TIMEOUT_SENTINEL;
|
|
1871
|
+
}
|
|
1872
|
+
return JSON.stringify({
|
|
1873
|
+
data: Buffer.from(message.data).toString("base64"),
|
|
1874
|
+
rinfo: "path" in message.srcAddr
|
|
1875
|
+
? {
|
|
1876
|
+
address: message.srcAddr.path,
|
|
1877
|
+
family: "unix",
|
|
1878
|
+
port: 0,
|
|
1879
|
+
size: message.data.length,
|
|
1880
|
+
}
|
|
1881
|
+
: {
|
|
1882
|
+
address: message.srcAddr.host,
|
|
1883
|
+
family: addressFamily(message.srcAddr.host),
|
|
1884
|
+
port: message.srcAddr.port,
|
|
1885
|
+
size: message.data.length,
|
|
1886
|
+
},
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
if (error instanceof Error && error.message.includes("EAGAIN")) {
|
|
1891
|
+
return NET_BRIDGE_TIMEOUT_SENTINEL;
|
|
1892
|
+
}
|
|
1893
|
+
return null;
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
handlers[K.dgramSocketSendRaw] = async (socketId, optionsJson) => {
|
|
1897
|
+
const id = Number(socketId);
|
|
1898
|
+
const options = parseJsonWithLimit("dgram.socket.send options", String(optionsJson), 256 * 1024);
|
|
1899
|
+
const data = Buffer.from(options.data, "base64");
|
|
1900
|
+
return socketTable.sendTo(id, new Uint8Array(data), 0, { host: String(options.address), port: Number(options.port) });
|
|
1901
|
+
};
|
|
1902
|
+
handlers[K.dgramSocketCloseRaw] = async (socketId) => {
|
|
1903
|
+
const id = Number(socketId);
|
|
1904
|
+
activeDgramIds.delete(id);
|
|
1905
|
+
socketTable.get(id)?.readWaiters.wakeAll();
|
|
1906
|
+
try {
|
|
1907
|
+
socketTable.close(id, pid);
|
|
1908
|
+
}
|
|
1909
|
+
catch {
|
|
1910
|
+
// Already closed
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
handlers[K.dgramSocketAddressRaw] = (socketId) => {
|
|
1914
|
+
const id = Number(socketId);
|
|
1915
|
+
const socket = socketTable.get(id);
|
|
1916
|
+
if (!socket?.localAddr || "path" in socket.localAddr) {
|
|
1917
|
+
throw new Error("getsockname EBADF");
|
|
1918
|
+
}
|
|
1919
|
+
return JSON.stringify({
|
|
1920
|
+
address: socket.localAddr.host,
|
|
1921
|
+
family: addressFamily(socket.localAddr.host),
|
|
1922
|
+
port: socket.localAddr.port,
|
|
1923
|
+
});
|
|
1924
|
+
};
|
|
1925
|
+
handlers[K.dgramSocketSetBufferSizeRaw] = (socketId, which, size) => {
|
|
1926
|
+
const optname = which === "send" ? SO_SNDBUF : SO_RCVBUF;
|
|
1927
|
+
socketTable.setsockopt(Number(socketId), SOL_SOCKET, optname, Number(size));
|
|
1928
|
+
};
|
|
1929
|
+
handlers[K.dgramSocketGetBufferSizeRaw] = (socketId, which) => {
|
|
1930
|
+
const optname = which === "send" ? SO_SNDBUF : SO_RCVBUF;
|
|
1931
|
+
return socketTable.getsockopt(Number(socketId), SOL_SOCKET, optname) ?? 0;
|
|
1932
|
+
};
|
|
1933
|
+
const dispose = () => {
|
|
1934
|
+
for (const id of activeServerIds) {
|
|
1935
|
+
try {
|
|
1936
|
+
socketTable.close(id, pid);
|
|
1937
|
+
}
|
|
1938
|
+
catch { /* best effort */ }
|
|
1939
|
+
}
|
|
1940
|
+
activeServerIds.clear();
|
|
1941
|
+
for (const id of activeDgramIds) {
|
|
1942
|
+
try {
|
|
1943
|
+
socketTable.close(id, pid);
|
|
1944
|
+
}
|
|
1945
|
+
catch { /* best effort */ }
|
|
1946
|
+
}
|
|
1947
|
+
activeDgramIds.clear();
|
|
1948
|
+
for (const id of activeSocketIds) {
|
|
1949
|
+
try {
|
|
1950
|
+
socketTable.close(id, pid);
|
|
1951
|
+
}
|
|
1952
|
+
catch { /* best effort */ }
|
|
1953
|
+
}
|
|
1954
|
+
activeSocketIds.clear();
|
|
1955
|
+
for (const socket of tlsSockets.values()) {
|
|
1956
|
+
socket.destroy();
|
|
1957
|
+
}
|
|
1958
|
+
tlsSockets.clear();
|
|
1959
|
+
for (const pair of loopbackTlsTransports.values()) {
|
|
1960
|
+
pair.a.destroy();
|
|
1961
|
+
pair.b.destroy();
|
|
1962
|
+
}
|
|
1963
|
+
loopbackTlsTransports.clear();
|
|
1964
|
+
loopbackTlsClientHello.clear();
|
|
1965
|
+
};
|
|
1966
|
+
return { handlers, dispose };
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Convert ESM source to CJS-compatible code for require() loading.
|
|
1970
|
+
* Handles import declarations, export declarations, and re-exports.
|
|
1971
|
+
*/
|
|
1972
|
+
/** Strip // and /* comments from an export/import list string. */
|
|
1973
|
+
function stripComments(s) {
|
|
1974
|
+
return s.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1975
|
+
}
|
|
1976
|
+
function convertEsmToCjs(source, filePath) {
|
|
1977
|
+
if (!isESM(source, filePath))
|
|
1978
|
+
return source;
|
|
1979
|
+
let code = source;
|
|
1980
|
+
// Remove const __filename/dirname declarations (already provided by CJS wrapper)
|
|
1981
|
+
code = code.replace(/^\s*(?:const|let|var)\s+__filename\s*=\s*[^;]+;?\s*$/gm, "// __filename provided by CJS wrapper");
|
|
1982
|
+
code = code.replace(/^\s*(?:const|let|var)\s+__dirname\s*=\s*[^;]+;?\s*$/gm, "// __dirname provided by CJS wrapper");
|
|
1983
|
+
// import X from 'Y' → const X = require('Y')
|
|
1984
|
+
code = code.replace(/^\s*import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?/gm, "const $1 = (function(m) { return m && m.__esModule ? m.default : m; })(require('$2'));");
|
|
1985
|
+
// import { a, b as c } from 'Y' → const { a, b: c } = require('Y')
|
|
1986
|
+
code = code.replace(/^\s*import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]\s*;?/gm, (_match, imports, mod) => {
|
|
1987
|
+
const mapped = stripComments(imports).split(",").map((s) => {
|
|
1988
|
+
const t = s.trim();
|
|
1989
|
+
if (!t)
|
|
1990
|
+
return null;
|
|
1991
|
+
const parts = t.split(/\s+as\s+/);
|
|
1992
|
+
return parts.length === 2 ? `${parts[0].trim()}: ${parts[1].trim()}` : t;
|
|
1993
|
+
}).filter(Boolean).join(", ");
|
|
1994
|
+
return `const { ${mapped} } = require('${mod}');`;
|
|
1995
|
+
});
|
|
1996
|
+
// import * as X from 'Y' → const X = require('Y')
|
|
1997
|
+
code = code.replace(/^\s*import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?/gm, "const $1 = require('$2');");
|
|
1998
|
+
// Side-effect imports: import 'Y' → require('Y')
|
|
1999
|
+
code = code.replace(/^\s*import\s+['"]([^'"]+)['"]\s*;?/gm, "require('$1');");
|
|
2000
|
+
// export { a, b } from 'Y' → re-export
|
|
2001
|
+
code = code.replace(/^\s*export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]\s*;?/gm, (_match, exports, mod) => {
|
|
2002
|
+
return stripComments(exports).split(",").map((s) => {
|
|
2003
|
+
const t = s.trim();
|
|
2004
|
+
if (!t)
|
|
2005
|
+
return "";
|
|
2006
|
+
const parts = t.split(/\s+as\s+/);
|
|
2007
|
+
const local = parts[0].trim();
|
|
2008
|
+
const exported = parts.length === 2 ? parts[1].trim() : local;
|
|
2009
|
+
return `Object.defineProperty(exports, '${exported}', { get: () => require('${mod}').${local}, enumerable: true });`;
|
|
2010
|
+
}).filter(Boolean).join("\n");
|
|
2011
|
+
});
|
|
2012
|
+
// export * from 'Y'
|
|
2013
|
+
code = code.replace(/^\s*export\s+\*\s+from\s+['"]([^'"]+)['"]\s*;?/gm, "Object.assign(exports, require('$1'));");
|
|
2014
|
+
// export default X → module.exports.default = X
|
|
2015
|
+
code = code.replace(/^\s*export\s+default\s+/gm, "module.exports.default = ");
|
|
2016
|
+
// export const/let/var X = ... → const/let/var X = ...; exports.X = X;
|
|
2017
|
+
code = code.replace(/^\s*export\s+(const|let|var)\s+(\w+)\s*=/gm, "$1 $2 =");
|
|
2018
|
+
// Capture the names separately to add exports at the end
|
|
2019
|
+
const exportedVars = [];
|
|
2020
|
+
for (const m of source.matchAll(/^\s*export\s+(?:const|let|var)\s+(\w+)\s*=/gm)) {
|
|
2021
|
+
exportedVars.push(m[1]);
|
|
2022
|
+
}
|
|
2023
|
+
// export function X(...) → function X(...); exports.X = X;
|
|
2024
|
+
code = code.replace(/^\s*export\s+function\s+(\w+)/gm, "function $1");
|
|
2025
|
+
for (const m of source.matchAll(/^\s*export\s+function\s+(\w+)/gm)) {
|
|
2026
|
+
exportedVars.push(m[1]);
|
|
2027
|
+
}
|
|
2028
|
+
// export class X → class X; exports.X = X;
|
|
2029
|
+
code = code.replace(/^\s*export\s+class\s+(\w+)/gm, "class $1");
|
|
2030
|
+
for (const m of source.matchAll(/^\s*export\s+class\s+(\w+)/gm)) {
|
|
2031
|
+
exportedVars.push(m[1]);
|
|
2032
|
+
}
|
|
2033
|
+
// export { a, b } (local re-export without from)
|
|
2034
|
+
code = code.replace(/^\s*export\s+\{([^}]+)\}\s*;?/gm, (_match, exports) => {
|
|
2035
|
+
return stripComments(exports).split(",").map((s) => {
|
|
2036
|
+
const t = s.trim();
|
|
2037
|
+
if (!t)
|
|
2038
|
+
return "";
|
|
2039
|
+
const parts = t.split(/\s+as\s+/);
|
|
2040
|
+
const local = parts[0].trim();
|
|
2041
|
+
const exported = parts.length === 2 ? parts[1].trim() : local;
|
|
2042
|
+
return `Object.defineProperty(exports, '${exported}', { get: () => ${local}, enumerable: true });`;
|
|
2043
|
+
}).filter(Boolean).join("\n");
|
|
2044
|
+
});
|
|
2045
|
+
// Append named exports for exported vars/functions/classes
|
|
2046
|
+
if (exportedVars.length > 0) {
|
|
2047
|
+
const lines = exportedVars.map((name) => `Object.defineProperty(exports, '${name}', { get: () => ${name}, enumerable: true });`);
|
|
2048
|
+
code += "\n" + lines.join("\n");
|
|
2049
|
+
}
|
|
2050
|
+
return code;
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Resolve a package specifier by walking up directories and reading package.json exports.
|
|
2054
|
+
* Handles both root imports ('pkg') and subpath imports ('pkg/sub').
|
|
2055
|
+
*/
|
|
2056
|
+
function resolvePackageExport(req, startDir, mode = "require") {
|
|
2057
|
+
// Split into package name and subpath
|
|
2058
|
+
const parts = req.startsWith("@") ? req.split("/") : [req.split("/")[0], ...req.split("/").slice(1)];
|
|
2059
|
+
const pkgName = req.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
2060
|
+
const subpath = req.startsWith("@")
|
|
2061
|
+
? (parts.length > 2 ? "./" + parts.slice(2).join("/") : ".")
|
|
2062
|
+
: (parts.length > 1 ? "./" + parts.slice(1).join("/") : ".");
|
|
2063
|
+
let cur = startDir;
|
|
2064
|
+
while (cur !== pathDirname(cur)) {
|
|
2065
|
+
const pkgJsonPath = pathJoin(cur, "node_modules", ...pkgName.split("/"), "package.json");
|
|
2066
|
+
if (existsSync(pkgJsonPath)) {
|
|
2067
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
2068
|
+
let entry;
|
|
2069
|
+
if (pkg.exports) {
|
|
2070
|
+
const exportEntry = pkg.exports[subpath];
|
|
2071
|
+
if (typeof exportEntry === "string")
|
|
2072
|
+
entry = exportEntry;
|
|
2073
|
+
else if (exportEntry) {
|
|
2074
|
+
const conditionalEntry = exportEntry;
|
|
2075
|
+
entry =
|
|
2076
|
+
mode === "import"
|
|
2077
|
+
? conditionalEntry.import ?? conditionalEntry.default ?? conditionalEntry.require
|
|
2078
|
+
: conditionalEntry.require ?? conditionalEntry.default ?? conditionalEntry.import;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (!entry && subpath === ".")
|
|
2082
|
+
entry = pkg.main;
|
|
2083
|
+
if (entry)
|
|
2084
|
+
return pathResolve(pathDirname(pkgJsonPath), entry);
|
|
2085
|
+
}
|
|
2086
|
+
cur = pathDirname(cur);
|
|
2087
|
+
}
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
const hostRequire = createRequire(import.meta.url);
|
|
2091
|
+
/**
|
|
2092
|
+
* Build sync module resolution bridge handlers.
|
|
2093
|
+
*
|
|
2094
|
+
* These use Node.js require.resolve() and readFileSync() directly,
|
|
2095
|
+
* avoiding the async VirtualFileSystem path. Needed because the async
|
|
2096
|
+
* applySyncPromise pattern can't nest inside synchronous bridge
|
|
2097
|
+
* callbacks (e.g. net socket data events that trigger require()).
|
|
2098
|
+
*/
|
|
2099
|
+
export function buildModuleResolutionBridgeHandlers(deps) {
|
|
2100
|
+
const handlers = {};
|
|
2101
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2102
|
+
// Sync require.resolve — translates sandbox paths and uses Node.js resolution.
|
|
2103
|
+
// Falls back to realpath + manual package.json resolution for pnpm/ESM packages.
|
|
2104
|
+
handlers[K.resolveModuleSync] = (request, fromDir, requestedMode) => {
|
|
2105
|
+
const req = String(request);
|
|
2106
|
+
const resolveMode = requestedMode === "require" || requestedMode === "import"
|
|
2107
|
+
? requestedMode
|
|
2108
|
+
: "require";
|
|
2109
|
+
// Builtins don't need filesystem resolution
|
|
2110
|
+
const builtin = normalizeBuiltinSpecifier(req);
|
|
2111
|
+
if (builtin)
|
|
2112
|
+
return builtin;
|
|
2113
|
+
// Translate sandbox fromDir to host path for resolution context
|
|
2114
|
+
const sandboxDir = String(fromDir);
|
|
2115
|
+
const hostDir = deps.sandboxToHostPath(sandboxDir) ?? sandboxDir;
|
|
2116
|
+
const resolveFromExports = (dir) => {
|
|
2117
|
+
const resolved = resolvePackageExport(req, dir, resolveMode);
|
|
2118
|
+
return resolved ? deps.hostToSandboxPath(resolved) : null;
|
|
2119
|
+
};
|
|
2120
|
+
if (resolveMode === "import") {
|
|
2121
|
+
const resolved = resolveFromExports(hostDir);
|
|
2122
|
+
if (resolved)
|
|
2123
|
+
return resolved;
|
|
2124
|
+
}
|
|
2125
|
+
// Try require.resolve first
|
|
2126
|
+
try {
|
|
2127
|
+
const resolved = hostRequire.resolve(req, { paths: [hostDir] });
|
|
2128
|
+
return deps.hostToSandboxPath(resolved);
|
|
2129
|
+
}
|
|
2130
|
+
catch { /* CJS resolution failed */ }
|
|
2131
|
+
// Fallback: follow symlinks and try ESM-compatible resolution
|
|
2132
|
+
try {
|
|
2133
|
+
let realDir;
|
|
2134
|
+
try {
|
|
2135
|
+
realDir = realpathSync(hostDir);
|
|
2136
|
+
}
|
|
2137
|
+
catch {
|
|
2138
|
+
realDir = hostDir;
|
|
2139
|
+
}
|
|
2140
|
+
if (resolveMode === "import") {
|
|
2141
|
+
const resolved = resolveFromExports(realDir);
|
|
2142
|
+
if (resolved)
|
|
2143
|
+
return resolved;
|
|
2144
|
+
}
|
|
2145
|
+
// Try require.resolve from real path
|
|
2146
|
+
try {
|
|
2147
|
+
const resolved = hostRequire.resolve(req, { paths: [realDir] });
|
|
2148
|
+
return deps.hostToSandboxPath(resolved);
|
|
2149
|
+
}
|
|
2150
|
+
catch { /* ESM-only, manual resolution */ }
|
|
2151
|
+
// Manual package.json resolution for ESM packages
|
|
2152
|
+
const resolved = resolveFromExports(realDir);
|
|
2153
|
+
if (resolved)
|
|
2154
|
+
return resolved;
|
|
2155
|
+
}
|
|
2156
|
+
catch { /* fallback failed */ }
|
|
2157
|
+
return null;
|
|
2158
|
+
};
|
|
2159
|
+
// Sync file read — translates sandbox path and reads via readFileSync.
|
|
2160
|
+
// Transforms dynamic import() to __dynamicImport() and converts ESM to CJS
|
|
2161
|
+
// for npm packages so require() can load ESM-only dependencies.
|
|
2162
|
+
handlers[K.loadFileSync] = (filePath) => {
|
|
2163
|
+
const sandboxPath = String(filePath);
|
|
2164
|
+
const hostPath = deps.sandboxToHostPath(sandboxPath) ?? sandboxPath;
|
|
2165
|
+
try {
|
|
2166
|
+
let source = readFileSync(hostPath, "utf-8");
|
|
2167
|
+
source = convertEsmToCjs(source, hostPath);
|
|
2168
|
+
return transformDynamicImport(source);
|
|
2169
|
+
}
|
|
2170
|
+
catch {
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
return handlers;
|
|
2175
|
+
}
|
|
2176
|
+
// Env vars that could hijack child processes (library injection, node flags)
|
|
2177
|
+
const DANGEROUS_ENV_KEYS = new Set([
|
|
2178
|
+
"LD_PRELOAD",
|
|
2179
|
+
"LD_LIBRARY_PATH",
|
|
2180
|
+
"NODE_OPTIONS",
|
|
2181
|
+
"DYLD_INSERT_LIBRARIES",
|
|
2182
|
+
]);
|
|
2183
|
+
/** Strip env vars that allow library injection or node flag smuggling. */
|
|
2184
|
+
export function stripDangerousEnv(env) {
|
|
2185
|
+
if (!env)
|
|
2186
|
+
return env;
|
|
2187
|
+
const result = {};
|
|
2188
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2189
|
+
if (!DANGEROUS_ENV_KEYS.has(key)) {
|
|
2190
|
+
result[key] = value;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return result;
|
|
2194
|
+
}
|
|
2195
|
+
export function emitConsoleEvent(onStdio, event) {
|
|
2196
|
+
if (!onStdio)
|
|
2197
|
+
return;
|
|
2198
|
+
try {
|
|
2199
|
+
onStdio(event);
|
|
2200
|
+
}
|
|
2201
|
+
catch {
|
|
2202
|
+
// Keep runtime execution deterministic even when host hooks fail.
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
/** Build console/logging bridge handlers. */
|
|
2206
|
+
export function buildConsoleBridgeHandlers(deps) {
|
|
2207
|
+
const handlers = {};
|
|
2208
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2209
|
+
handlers[K.log] = (msg) => {
|
|
2210
|
+
const str = String(msg);
|
|
2211
|
+
if (deps.maxOutputBytes !== undefined) {
|
|
2212
|
+
const bytes = Buffer.byteLength(str, "utf8");
|
|
2213
|
+
if (deps.budgetState.outputBytes + bytes > deps.maxOutputBytes)
|
|
2214
|
+
return;
|
|
2215
|
+
deps.budgetState.outputBytes += bytes;
|
|
2216
|
+
}
|
|
2217
|
+
emitConsoleEvent(deps.onStdio, { channel: "stdout", message: str });
|
|
2218
|
+
};
|
|
2219
|
+
handlers[K.error] = (msg) => {
|
|
2220
|
+
const str = String(msg);
|
|
2221
|
+
if (deps.maxOutputBytes !== undefined) {
|
|
2222
|
+
const bytes = Buffer.byteLength(str, "utf8");
|
|
2223
|
+
if (deps.budgetState.outputBytes + bytes > deps.maxOutputBytes)
|
|
2224
|
+
return;
|
|
2225
|
+
deps.budgetState.outputBytes += bytes;
|
|
2226
|
+
}
|
|
2227
|
+
emitConsoleEvent(deps.onStdio, { channel: "stderr", message: str });
|
|
2228
|
+
};
|
|
2229
|
+
return handlers;
|
|
2230
|
+
}
|
|
2231
|
+
/** Build module loading bridge handlers (loadPolyfill, resolveModule, loadFile). */
|
|
2232
|
+
export function buildModuleLoadingBridgeHandlers(deps,
|
|
2233
|
+
/** Extra handlers to dispatch through _loadPolyfill for V8 runtime compatibility. */
|
|
2234
|
+
dispatchHandlers) {
|
|
2235
|
+
const handlers = {};
|
|
2236
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2237
|
+
// Polyfill loading — also serves as bridge dispatch multiplexer.
|
|
2238
|
+
// The V8 runtime binary only registers a fixed set of bridge globals.
|
|
2239
|
+
// Newer handlers (crypto, net sockets, etc.) are dispatched through
|
|
2240
|
+
// _loadPolyfill with a "__bd:" prefix.
|
|
2241
|
+
handlers[K.loadPolyfill] = async (moduleName) => {
|
|
2242
|
+
const nameStr = String(moduleName);
|
|
2243
|
+
// Bridge dispatch: "__bd:methodName:base64args"
|
|
2244
|
+
if (nameStr.startsWith("__bd:") && dispatchHandlers) {
|
|
2245
|
+
const colonIdx = nameStr.indexOf(":", 5);
|
|
2246
|
+
const method = nameStr.substring(5, colonIdx > 0 ? colonIdx : undefined);
|
|
2247
|
+
const argsJson = colonIdx > 0 ? nameStr.substring(colonIdx + 1) : "[]";
|
|
2248
|
+
const handler = dispatchHandlers[method];
|
|
2249
|
+
if (!handler)
|
|
2250
|
+
return JSON.stringify({ __bd_error: `No handler: ${method}` });
|
|
2251
|
+
try {
|
|
2252
|
+
const args = restoreDispatchArgument(JSON.parse(argsJson));
|
|
2253
|
+
const result = await handler(...(Array.isArray(args) ? args : [args]));
|
|
2254
|
+
return JSON.stringify({ __bd_result: result });
|
|
2255
|
+
}
|
|
2256
|
+
catch (err) {
|
|
2257
|
+
return JSON.stringify({ __bd_error: serializeDispatchError(err) });
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const name = nameStr.replace(/^node:/, "");
|
|
2261
|
+
if (name === "fs" || name === "child_process" || name === "http" ||
|
|
2262
|
+
name === "https" || name === "http2" || name === "dns" ||
|
|
2263
|
+
name === "os" || name === "module") {
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
if (!hasPolyfill(name))
|
|
2267
|
+
return null;
|
|
2268
|
+
let code = polyfillCodeCache.get(name);
|
|
2269
|
+
if (!code) {
|
|
2270
|
+
code = await bundlePolyfill(name);
|
|
2271
|
+
polyfillCodeCache.set(name, code);
|
|
2272
|
+
}
|
|
2273
|
+
return code;
|
|
2274
|
+
};
|
|
2275
|
+
// Async module path resolution via VFS
|
|
2276
|
+
// V8 ESM module resolve sends the full file path as referrer, not a directory.
|
|
2277
|
+
// Extract dirname when the referrer looks like a file path.
|
|
2278
|
+
// Falls back to Node.js require.resolve() with realpath for pnpm compatibility.
|
|
2279
|
+
handlers[K.resolveModule] = async (request, fromDir, requestedMode) => {
|
|
2280
|
+
const req = String(request);
|
|
2281
|
+
const resolveMode = requestedMode === "require" || requestedMode === "import"
|
|
2282
|
+
? requestedMode
|
|
2283
|
+
: (deps.resolveMode ?? "require");
|
|
2284
|
+
const builtin = normalizeBuiltinSpecifier(req);
|
|
2285
|
+
if (builtin)
|
|
2286
|
+
return builtin;
|
|
2287
|
+
let dir = String(fromDir);
|
|
2288
|
+
if (/\.[cm]?[jt]sx?$/.test(dir)) {
|
|
2289
|
+
const lastSlash = dir.lastIndexOf("/");
|
|
2290
|
+
if (lastSlash > 0)
|
|
2291
|
+
dir = dir.slice(0, lastSlash);
|
|
2292
|
+
}
|
|
2293
|
+
const vfsResult = await resolveModule(req, dir, deps.filesystem, resolveMode, deps.resolutionCache);
|
|
2294
|
+
if (vfsResult)
|
|
2295
|
+
return vfsResult;
|
|
2296
|
+
// Fallback: resolve through real host paths for pnpm symlink compatibility.
|
|
2297
|
+
const hostDir = deps.sandboxToHostPath?.(dir) ?? dir;
|
|
2298
|
+
try {
|
|
2299
|
+
let realDir;
|
|
2300
|
+
try {
|
|
2301
|
+
realDir = realpathSync(hostDir);
|
|
2302
|
+
}
|
|
2303
|
+
catch {
|
|
2304
|
+
realDir = hostDir;
|
|
2305
|
+
}
|
|
2306
|
+
if (resolveMode === "import") {
|
|
2307
|
+
const resolvedImport = resolvePackageExport(req, realDir, "import");
|
|
2308
|
+
if (resolvedImport)
|
|
2309
|
+
return resolvedImport;
|
|
2310
|
+
}
|
|
2311
|
+
// Try require.resolve (works for CJS packages)
|
|
2312
|
+
try {
|
|
2313
|
+
return hostRequire.resolve(req, { paths: [realDir] });
|
|
2314
|
+
}
|
|
2315
|
+
catch { /* ESM-only, try manual resolution */ }
|
|
2316
|
+
// Manual package.json resolution for ESM packages
|
|
2317
|
+
const resolved = resolvePackageExport(req, realDir, resolveMode);
|
|
2318
|
+
if (resolved)
|
|
2319
|
+
return resolved;
|
|
2320
|
+
}
|
|
2321
|
+
catch { /* resolution failed */ }
|
|
2322
|
+
return null;
|
|
2323
|
+
};
|
|
2324
|
+
// Dynamic import bridge — returns null to fall back to require() in the sandbox.
|
|
2325
|
+
// V8 ESM module mode handles static imports natively via module_resolve_callback;
|
|
2326
|
+
// this handler covers the __dynamicImport() path used in exec mode.
|
|
2327
|
+
handlers[K.dynamicImport] = async () => null;
|
|
2328
|
+
// Async file read + dynamic import transform.
|
|
2329
|
+
// Also serves ESM wrappers for built-in modules (fs, path, etc.) when
|
|
2330
|
+
// used from V8's ES module system which calls _loadFile after _resolveModule.
|
|
2331
|
+
handlers[K.loadFile] = async (path, requestedMode) => {
|
|
2332
|
+
const p = String(path);
|
|
2333
|
+
const loadMode = requestedMode === "require" || requestedMode === "import"
|
|
2334
|
+
? requestedMode
|
|
2335
|
+
: (deps.resolveMode ?? "require");
|
|
2336
|
+
// Built-in module ESM wrappers (V8 module system resolves 'fs' then loads it)
|
|
2337
|
+
const bare = p.replace(/^node:/, "");
|
|
2338
|
+
const builtin = getStaticBuiltinWrapperSource(bare);
|
|
2339
|
+
if (builtin)
|
|
2340
|
+
return builtin;
|
|
2341
|
+
// Polyfill-backed builtins (crypto, zlib, etc.)
|
|
2342
|
+
if (hasPolyfill(bare)) {
|
|
2343
|
+
return createBuiltinESMWrapper(`globalThis._requireFrom(${JSON.stringify(bare)}, "/")`, getHostBuiltinNamedExports(bare));
|
|
2344
|
+
}
|
|
2345
|
+
// Regular files load differently for CommonJS require() vs V8's ESM loader.
|
|
2346
|
+
let source = await loadFile(p, deps.filesystem);
|
|
2347
|
+
if (source === null)
|
|
2348
|
+
return null;
|
|
2349
|
+
if (loadMode === "require") {
|
|
2350
|
+
source = convertEsmToCjs(source, p);
|
|
2351
|
+
}
|
|
2352
|
+
return transformDynamicImport(source);
|
|
2353
|
+
};
|
|
2354
|
+
return handlers;
|
|
2355
|
+
}
|
|
2356
|
+
/** Build timer bridge handler. */
|
|
2357
|
+
export function buildTimerBridgeHandlers(deps) {
|
|
2358
|
+
const handlers = {};
|
|
2359
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2360
|
+
handlers[K.scheduleTimer] = (delayMs) => {
|
|
2361
|
+
checkBridgeBudget(deps);
|
|
2362
|
+
return new Promise((resolve) => {
|
|
2363
|
+
const id = globalThis.setTimeout(() => {
|
|
2364
|
+
deps.activeHostTimers.delete(id);
|
|
2365
|
+
resolve();
|
|
2366
|
+
}, Number(delayMs));
|
|
2367
|
+
deps.activeHostTimers.add(id);
|
|
2368
|
+
});
|
|
2369
|
+
};
|
|
2370
|
+
return handlers;
|
|
2371
|
+
}
|
|
2372
|
+
export function buildKernelTimerDispatchHandlers(deps) {
|
|
2373
|
+
const handlers = {};
|
|
2374
|
+
handlers.kernelTimerCreate = (delayMs, repeat) => {
|
|
2375
|
+
checkBridgeBudget(deps);
|
|
2376
|
+
const normalizedDelay = Number(delayMs);
|
|
2377
|
+
return deps.timerTable.createTimer(deps.pid, Number.isFinite(normalizedDelay) && normalizedDelay > 0
|
|
2378
|
+
? Math.floor(normalizedDelay)
|
|
2379
|
+
: 0, Boolean(repeat), () => { });
|
|
2380
|
+
};
|
|
2381
|
+
handlers.kernelTimerArm = (timerId) => {
|
|
2382
|
+
checkBridgeBudget(deps);
|
|
2383
|
+
const timer = deps.timerTable.get(Number(timerId));
|
|
2384
|
+
if (!timer || timer.pid !== deps.pid || timer.cleared) {
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
const dispatchFire = () => {
|
|
2388
|
+
const activeTimer = deps.timerTable.get(timer.id);
|
|
2389
|
+
if (!activeTimer || activeTimer.pid !== deps.pid || activeTimer.cleared) {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
activeTimer.hostHandle = undefined;
|
|
2393
|
+
if (!activeTimer.repeat) {
|
|
2394
|
+
deps.timerTable.clearTimer(activeTimer.id, deps.pid);
|
|
2395
|
+
}
|
|
2396
|
+
deps.sendStreamEvent("timer", Buffer.from(JSON.stringify({ timerId: activeTimer.id })));
|
|
2397
|
+
};
|
|
2398
|
+
if (timer.delayMs <= 0) {
|
|
2399
|
+
queueMicrotask(dispatchFire);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const hostHandle = globalThis.setTimeout(() => {
|
|
2403
|
+
deps.activeHostTimers.delete(hostHandle);
|
|
2404
|
+
dispatchFire();
|
|
2405
|
+
}, timer.delayMs);
|
|
2406
|
+
timer.hostHandle = hostHandle;
|
|
2407
|
+
deps.activeHostTimers.add(hostHandle);
|
|
2408
|
+
};
|
|
2409
|
+
handlers.kernelTimerClear = (timerId) => {
|
|
2410
|
+
checkBridgeBudget(deps);
|
|
2411
|
+
const timer = deps.timerTable.get(Number(timerId));
|
|
2412
|
+
if (!timer || timer.pid !== deps.pid)
|
|
2413
|
+
return;
|
|
2414
|
+
if (timer.hostHandle !== undefined) {
|
|
2415
|
+
clearTimeout(timer.hostHandle);
|
|
2416
|
+
deps.activeHostTimers.delete(timer.hostHandle);
|
|
2417
|
+
timer.hostHandle = undefined;
|
|
2418
|
+
}
|
|
2419
|
+
deps.timerTable.clearTimer(timer.id, deps.pid);
|
|
2420
|
+
};
|
|
2421
|
+
return handlers;
|
|
2422
|
+
}
|
|
2423
|
+
export function buildKernelHandleDispatchHandlers(deps) {
|
|
2424
|
+
const handlers = {};
|
|
2425
|
+
handlers.kernelHandleRegister = (id, description) => {
|
|
2426
|
+
checkBridgeBudget(deps);
|
|
2427
|
+
if (!deps.processTable)
|
|
2428
|
+
return;
|
|
2429
|
+
const handleId = String(id);
|
|
2430
|
+
let activeHandles;
|
|
2431
|
+
try {
|
|
2432
|
+
activeHandles = deps.processTable.getHandles(deps.pid);
|
|
2433
|
+
}
|
|
2434
|
+
catch {
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (activeHandles.has(handleId)) {
|
|
2438
|
+
try {
|
|
2439
|
+
deps.processTable.unregisterHandle(deps.pid, handleId);
|
|
2440
|
+
}
|
|
2441
|
+
catch {
|
|
2442
|
+
// Process exit races turn re-register into a no-op.
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
deps.processTable.registerHandle(deps.pid, handleId, String(description));
|
|
2446
|
+
};
|
|
2447
|
+
handlers.kernelHandleUnregister = (id) => {
|
|
2448
|
+
checkBridgeBudget(deps);
|
|
2449
|
+
if (!deps.processTable)
|
|
2450
|
+
return 0;
|
|
2451
|
+
try {
|
|
2452
|
+
deps.processTable.unregisterHandle(deps.pid, String(id));
|
|
2453
|
+
}
|
|
2454
|
+
catch {
|
|
2455
|
+
// Unknown handles already behave like a no-op at the bridge layer.
|
|
2456
|
+
}
|
|
2457
|
+
try {
|
|
2458
|
+
return deps.processTable.getHandles(deps.pid).size;
|
|
2459
|
+
}
|
|
2460
|
+
catch {
|
|
2461
|
+
return 0;
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
handlers.kernelHandleList = () => {
|
|
2465
|
+
checkBridgeBudget(deps);
|
|
2466
|
+
if (!deps.processTable)
|
|
2467
|
+
return [];
|
|
2468
|
+
try {
|
|
2469
|
+
return Array.from(deps.processTable.getHandles(deps.pid).entries());
|
|
2470
|
+
}
|
|
2471
|
+
catch {
|
|
2472
|
+
return [];
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
return handlers;
|
|
2476
|
+
}
|
|
2477
|
+
/** Build filesystem bridge handlers (readFile, writeFile, stat, etc.). */
|
|
2478
|
+
export function buildFsBridgeHandlers(deps) {
|
|
2479
|
+
const handlers = {};
|
|
2480
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2481
|
+
const fs = deps.filesystem;
|
|
2482
|
+
const base64Limit = deps.bridgeBase64TransferLimitBytes;
|
|
2483
|
+
const jsonLimit = deps.isolateJsonPayloadLimitBytes;
|
|
2484
|
+
handlers[K.fsReadFile] = async (path) => {
|
|
2485
|
+
checkBridgeBudget(deps);
|
|
2486
|
+
const text = await readStandaloneProcAwareTextFile(fs, String(path));
|
|
2487
|
+
assertTextPayloadSize(`fs.readFile ${path}`, text, jsonLimit);
|
|
2488
|
+
return text;
|
|
2489
|
+
};
|
|
2490
|
+
handlers[K.fsWriteFile] = async (path, content) => {
|
|
2491
|
+
checkBridgeBudget(deps);
|
|
2492
|
+
await fs.writeFile(String(path), String(content));
|
|
2493
|
+
};
|
|
2494
|
+
handlers[K.fsReadFileBinary] = async (path) => {
|
|
2495
|
+
checkBridgeBudget(deps);
|
|
2496
|
+
const data = await readStandaloneProcAwareFile(fs, String(path));
|
|
2497
|
+
assertPayloadByteLength(`fs.readFileBinary ${path}`, getBase64EncodedByteLength(data.byteLength), base64Limit);
|
|
2498
|
+
return Buffer.from(data).toString("base64");
|
|
2499
|
+
};
|
|
2500
|
+
handlers[K.fsWriteFileBinary] = async (path, base64Content) => {
|
|
2501
|
+
checkBridgeBudget(deps);
|
|
2502
|
+
const b64 = String(base64Content);
|
|
2503
|
+
assertTextPayloadSize(`fs.writeFileBinary ${path}`, b64, base64Limit);
|
|
2504
|
+
await fs.writeFile(String(path), Buffer.from(b64, "base64"));
|
|
2505
|
+
};
|
|
2506
|
+
handlers[K.fsReadDir] = async (path) => {
|
|
2507
|
+
checkBridgeBudget(deps);
|
|
2508
|
+
const entries = (await fs.readDirWithTypes(String(path))).filter((entry) => entry.name !== "." && entry.name !== "..");
|
|
2509
|
+
const json = JSON.stringify(entries);
|
|
2510
|
+
assertTextPayloadSize(`fs.readDir ${path}`, json, jsonLimit);
|
|
2511
|
+
return json;
|
|
2512
|
+
};
|
|
2513
|
+
handlers[K.fsMkdir] = async (path) => {
|
|
2514
|
+
checkBridgeBudget(deps);
|
|
2515
|
+
await mkdir(fs, String(path));
|
|
2516
|
+
};
|
|
2517
|
+
handlers[K.fsRmdir] = async (path) => {
|
|
2518
|
+
checkBridgeBudget(deps);
|
|
2519
|
+
await fs.removeDir(String(path));
|
|
2520
|
+
};
|
|
2521
|
+
handlers[K.fsExists] = async (path) => {
|
|
2522
|
+
checkBridgeBudget(deps);
|
|
2523
|
+
return standaloneProcAwareExists(fs, String(path));
|
|
2524
|
+
};
|
|
2525
|
+
handlers[K.fsStat] = async (path) => {
|
|
2526
|
+
checkBridgeBudget(deps);
|
|
2527
|
+
const s = await standaloneProcAwareStat(fs, String(path));
|
|
2528
|
+
return JSON.stringify({ mode: s.mode, size: s.size, isDirectory: s.isDirectory,
|
|
2529
|
+
atimeMs: s.atimeMs, mtimeMs: s.mtimeMs, ctimeMs: s.ctimeMs, birthtimeMs: s.birthtimeMs });
|
|
2530
|
+
};
|
|
2531
|
+
handlers[K.fsUnlink] = async (path) => {
|
|
2532
|
+
checkBridgeBudget(deps);
|
|
2533
|
+
await fs.removeFile(String(path));
|
|
2534
|
+
};
|
|
2535
|
+
handlers[K.fsRename] = async (oldPath, newPath) => {
|
|
2536
|
+
checkBridgeBudget(deps);
|
|
2537
|
+
await fs.rename(String(oldPath), String(newPath));
|
|
2538
|
+
};
|
|
2539
|
+
handlers[K.fsChmod] = async (path, mode) => {
|
|
2540
|
+
checkBridgeBudget(deps);
|
|
2541
|
+
await fs.chmod(String(path), Number(mode));
|
|
2542
|
+
};
|
|
2543
|
+
handlers[K.fsChown] = async (path, uid, gid) => {
|
|
2544
|
+
checkBridgeBudget(deps);
|
|
2545
|
+
await fs.chown(String(path), Number(uid), Number(gid));
|
|
2546
|
+
};
|
|
2547
|
+
handlers[K.fsLink] = async (oldPath, newPath) => {
|
|
2548
|
+
checkBridgeBudget(deps);
|
|
2549
|
+
await fs.link(String(oldPath), String(newPath));
|
|
2550
|
+
};
|
|
2551
|
+
handlers[K.fsSymlink] = async (target, linkPath) => {
|
|
2552
|
+
checkBridgeBudget(deps);
|
|
2553
|
+
await fs.symlink(String(target), String(linkPath));
|
|
2554
|
+
};
|
|
2555
|
+
handlers[K.fsReadlink] = async (path) => {
|
|
2556
|
+
checkBridgeBudget(deps);
|
|
2557
|
+
return fs.readlink(String(path));
|
|
2558
|
+
};
|
|
2559
|
+
handlers[K.fsLstat] = async (path) => {
|
|
2560
|
+
checkBridgeBudget(deps);
|
|
2561
|
+
const s = await fs.lstat(String(path));
|
|
2562
|
+
return JSON.stringify({ mode: s.mode, size: s.size, isDirectory: s.isDirectory,
|
|
2563
|
+
isSymbolicLink: s.isSymbolicLink, atimeMs: s.atimeMs, mtimeMs: s.mtimeMs,
|
|
2564
|
+
ctimeMs: s.ctimeMs, birthtimeMs: s.birthtimeMs });
|
|
2565
|
+
};
|
|
2566
|
+
handlers[K.fsTruncate] = async (path, length) => {
|
|
2567
|
+
checkBridgeBudget(deps);
|
|
2568
|
+
await fs.truncate(String(path), Number(length));
|
|
2569
|
+
};
|
|
2570
|
+
handlers[K.fsUtimes] = async (path, atime, mtime) => {
|
|
2571
|
+
checkBridgeBudget(deps);
|
|
2572
|
+
await fs.utimes(String(path), Number(atime), Number(mtime));
|
|
2573
|
+
};
|
|
2574
|
+
return handlers;
|
|
2575
|
+
}
|
|
2576
|
+
/** Build child process bridge handlers. */
|
|
2577
|
+
export function buildChildProcessBridgeHandlers(deps) {
|
|
2578
|
+
const handlers = {};
|
|
2579
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2580
|
+
const jsonLimit = deps.isolateJsonPayloadLimitBytes;
|
|
2581
|
+
let nextSessionId = 1;
|
|
2582
|
+
const sessions = deps.activeChildProcesses;
|
|
2583
|
+
const { processTable, parentPid } = deps;
|
|
2584
|
+
// Map sessionId → kernel PID for kernel-registered processes
|
|
2585
|
+
const sessionToPid = new Map();
|
|
2586
|
+
/** Wrap a SpawnedProcess as a kernel DriverProcess (adds callback stubs). */
|
|
2587
|
+
function wrapAsDriverProcess(proc) {
|
|
2588
|
+
return {
|
|
2589
|
+
writeStdin: (data) => proc.writeStdin(data),
|
|
2590
|
+
closeStdin: () => proc.closeStdin(),
|
|
2591
|
+
kill: (signal) => proc.kill(signal),
|
|
2592
|
+
wait: () => proc.wait(),
|
|
2593
|
+
onStdout: null,
|
|
2594
|
+
onStderr: null,
|
|
2595
|
+
onExit: null,
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
// Serialize a child process event and push it into the V8 isolate
|
|
2599
|
+
const dispatchEvent = (sessionId, type, data) => {
|
|
2600
|
+
try {
|
|
2601
|
+
const payload = JSON.stringify({ sessionId, type, data: data instanceof Uint8Array ? Buffer.from(data).toString("base64") : data });
|
|
2602
|
+
deps.sendStreamEvent("childProcess", Buffer.from(payload));
|
|
2603
|
+
}
|
|
2604
|
+
catch {
|
|
2605
|
+
// Context may be disposed
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
handlers[K.childProcessSpawnStart] = (command, argsJson, optionsJson) => {
|
|
2609
|
+
checkBridgeBudget(deps);
|
|
2610
|
+
if (deps.maxChildProcesses !== undefined && deps.budgetState.childProcesses >= deps.maxChildProcesses) {
|
|
2611
|
+
throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum child processes exceeded`);
|
|
2612
|
+
}
|
|
2613
|
+
deps.budgetState.childProcesses++;
|
|
2614
|
+
const args = parseJsonWithLimit("child_process.spawn args", String(argsJson), jsonLimit);
|
|
2615
|
+
const options = parseJsonWithLimit("child_process.spawn options", String(optionsJson), jsonLimit);
|
|
2616
|
+
const sessionId = nextSessionId++;
|
|
2617
|
+
const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env);
|
|
2618
|
+
const proc = deps.commandExecutor.spawn(String(command), args, {
|
|
2619
|
+
cwd: options.cwd,
|
|
2620
|
+
env: childEnv,
|
|
2621
|
+
onStdout: (data) => dispatchEvent(sessionId, "stdout", data),
|
|
2622
|
+
onStderr: (data) => dispatchEvent(sessionId, "stderr", data),
|
|
2623
|
+
});
|
|
2624
|
+
// Register with kernel process table for cross-runtime visibility
|
|
2625
|
+
if (processTable && parentPid !== undefined) {
|
|
2626
|
+
const childPid = processTable.allocatePid();
|
|
2627
|
+
processTable.register(childPid, "node", String(command), args, {
|
|
2628
|
+
pid: childPid,
|
|
2629
|
+
ppid: parentPid,
|
|
2630
|
+
env: childEnv ?? {},
|
|
2631
|
+
cwd: options.cwd ?? deps.processConfig.cwd ?? "/",
|
|
2632
|
+
fds: { stdin: 0, stdout: 1, stderr: 2 },
|
|
2633
|
+
}, wrapAsDriverProcess(proc));
|
|
2634
|
+
sessionToPid.set(sessionId, childPid);
|
|
2635
|
+
}
|
|
2636
|
+
proc.wait().then((code) => {
|
|
2637
|
+
// Mark exited in kernel process table
|
|
2638
|
+
const childPid = sessionToPid.get(sessionId);
|
|
2639
|
+
if (childPid !== undefined && processTable) {
|
|
2640
|
+
try {
|
|
2641
|
+
processTable.markExited(childPid, code);
|
|
2642
|
+
}
|
|
2643
|
+
catch { /* already exited */ }
|
|
2644
|
+
sessionToPid.delete(sessionId);
|
|
2645
|
+
}
|
|
2646
|
+
dispatchEvent(sessionId, "exit", code);
|
|
2647
|
+
sessions.delete(sessionId);
|
|
2648
|
+
});
|
|
2649
|
+
sessions.set(sessionId, proc);
|
|
2650
|
+
return sessionId;
|
|
2651
|
+
};
|
|
2652
|
+
handlers[K.childProcessStdinWrite] = (sessionId, data) => {
|
|
2653
|
+
const d = data instanceof Uint8Array ? data : Buffer.from(String(data), "base64");
|
|
2654
|
+
sessions.get(Number(sessionId))?.writeStdin(d);
|
|
2655
|
+
};
|
|
2656
|
+
handlers[K.childProcessStdinClose] = (sessionId) => {
|
|
2657
|
+
sessions.get(Number(sessionId))?.closeStdin();
|
|
2658
|
+
};
|
|
2659
|
+
handlers[K.childProcessKill] = (sessionId, signal) => {
|
|
2660
|
+
const id = Number(sessionId);
|
|
2661
|
+
// Route through kernel process table when available
|
|
2662
|
+
const childPid = sessionToPid.get(id);
|
|
2663
|
+
if (childPid !== undefined && processTable) {
|
|
2664
|
+
try {
|
|
2665
|
+
processTable.kill(childPid, Number(signal));
|
|
2666
|
+
}
|
|
2667
|
+
catch { /* already dead */ }
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
sessions.get(id)?.kill(Number(signal));
|
|
2671
|
+
};
|
|
2672
|
+
handlers[K.childProcessSpawnSync] = async (command, argsJson, optionsJson) => {
|
|
2673
|
+
checkBridgeBudget(deps);
|
|
2674
|
+
if (deps.maxChildProcesses !== undefined && deps.budgetState.childProcesses >= deps.maxChildProcesses) {
|
|
2675
|
+
throw new Error(`${RESOURCE_BUDGET_ERROR_CODE}: maximum child processes exceeded`);
|
|
2676
|
+
}
|
|
2677
|
+
deps.budgetState.childProcesses++;
|
|
2678
|
+
const args = parseJsonWithLimit("child_process.spawnSync args", String(argsJson), jsonLimit);
|
|
2679
|
+
const options = parseJsonWithLimit("child_process.spawnSync options", String(optionsJson), jsonLimit);
|
|
2680
|
+
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
|
|
2681
|
+
const stdoutChunks = [];
|
|
2682
|
+
const stderrChunks = [];
|
|
2683
|
+
let stdoutBytes = 0;
|
|
2684
|
+
let stderrBytes = 0;
|
|
2685
|
+
let maxBufferExceeded = false;
|
|
2686
|
+
const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env);
|
|
2687
|
+
const proc = deps.commandExecutor.spawn(String(command), args, {
|
|
2688
|
+
cwd: options.cwd,
|
|
2689
|
+
env: childEnv,
|
|
2690
|
+
onStdout: (data) => {
|
|
2691
|
+
if (maxBufferExceeded)
|
|
2692
|
+
return;
|
|
2693
|
+
stdoutBytes += data.length;
|
|
2694
|
+
if (maxBuffer !== undefined && stdoutBytes > maxBuffer) {
|
|
2695
|
+
maxBufferExceeded = true;
|
|
2696
|
+
proc.kill(15);
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
stdoutChunks.push(data);
|
|
2700
|
+
},
|
|
2701
|
+
onStderr: (data) => {
|
|
2702
|
+
if (maxBufferExceeded)
|
|
2703
|
+
return;
|
|
2704
|
+
stderrBytes += data.length;
|
|
2705
|
+
if (maxBuffer !== undefined && stderrBytes > maxBuffer) {
|
|
2706
|
+
maxBufferExceeded = true;
|
|
2707
|
+
proc.kill(15);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
stderrChunks.push(data);
|
|
2711
|
+
},
|
|
2712
|
+
});
|
|
2713
|
+
// Register sync child with kernel process table
|
|
2714
|
+
let syncChildPid;
|
|
2715
|
+
if (processTable && parentPid !== undefined) {
|
|
2716
|
+
syncChildPid = processTable.allocatePid();
|
|
2717
|
+
processTable.register(syncChildPid, "node", String(command), args, {
|
|
2718
|
+
pid: syncChildPid,
|
|
2719
|
+
ppid: parentPid,
|
|
2720
|
+
env: childEnv ?? {},
|
|
2721
|
+
cwd: options.cwd ?? deps.processConfig.cwd ?? "/",
|
|
2722
|
+
fds: { stdin: 0, stdout: 1, stderr: 2 },
|
|
2723
|
+
}, wrapAsDriverProcess(proc));
|
|
2724
|
+
}
|
|
2725
|
+
const exitCode = await proc.wait();
|
|
2726
|
+
// Mark exited in kernel
|
|
2727
|
+
if (syncChildPid !== undefined && processTable) {
|
|
2728
|
+
try {
|
|
2729
|
+
processTable.markExited(syncChildPid, exitCode);
|
|
2730
|
+
}
|
|
2731
|
+
catch { /* already exited */ }
|
|
2732
|
+
}
|
|
2733
|
+
const decoder = new TextDecoder();
|
|
2734
|
+
const stdout = stdoutChunks.map((c) => decoder.decode(c)).join("");
|
|
2735
|
+
const stderr = stderrChunks.map((c) => decoder.decode(c)).join("");
|
|
2736
|
+
return JSON.stringify({ stdout, stderr, code: exitCode, maxBufferExceeded });
|
|
2737
|
+
};
|
|
2738
|
+
return handlers;
|
|
2739
|
+
}
|
|
2740
|
+
/** Restrict HTTP server hostname to loopback interfaces. */
|
|
2741
|
+
function normalizeLoopbackHostname(hostname) {
|
|
2742
|
+
if (!hostname || hostname === "localhost")
|
|
2743
|
+
return "127.0.0.1";
|
|
2744
|
+
if (hostname === "127.0.0.1" || hostname === "::1")
|
|
2745
|
+
return hostname;
|
|
2746
|
+
if (hostname === "0.0.0.0" || hostname === "::")
|
|
2747
|
+
return "127.0.0.1";
|
|
2748
|
+
throw new Error(`Sandbox HTTP servers are restricted to loopback interfaces. Received hostname: ${hostname}`);
|
|
2749
|
+
}
|
|
2750
|
+
function debugHttpBridge(...args) {
|
|
2751
|
+
if (process.env.SECURE_EXEC_DEBUG_HTTP_BRIDGE === "1") {
|
|
2752
|
+
console.error("[secure-exec http bridge]", ...args);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Create a Duplex stream backed by a kernel socket.
|
|
2757
|
+
* Readable side reads from kernel socket readBuffer; writable side writes via send().
|
|
2758
|
+
*/
|
|
2759
|
+
function createKernelSocketDuplex(socketId, socketTable, pid) {
|
|
2760
|
+
let readPumpStarted = false;
|
|
2761
|
+
const duplex = new Duplex({
|
|
2762
|
+
read() {
|
|
2763
|
+
if (readPumpStarted)
|
|
2764
|
+
return;
|
|
2765
|
+
readPumpStarted = true;
|
|
2766
|
+
runReadPump();
|
|
2767
|
+
},
|
|
2768
|
+
write(chunk, encoding, callback) {
|
|
2769
|
+
try {
|
|
2770
|
+
const data = typeof chunk === "string"
|
|
2771
|
+
? Buffer.from(chunk, encoding)
|
|
2772
|
+
: Buffer.isBuffer(chunk)
|
|
2773
|
+
? chunk
|
|
2774
|
+
: Buffer.from(chunk);
|
|
2775
|
+
debugHttpBridge("socket write", socketId, data.length);
|
|
2776
|
+
socketTable.send(socketId, new Uint8Array(data), 0);
|
|
2777
|
+
callback();
|
|
2778
|
+
}
|
|
2779
|
+
catch (err) {
|
|
2780
|
+
debugHttpBridge("socket write error", socketId, err);
|
|
2781
|
+
callback(err instanceof Error ? err : new Error(String(err)));
|
|
2782
|
+
}
|
|
2783
|
+
},
|
|
2784
|
+
final(callback) {
|
|
2785
|
+
try {
|
|
2786
|
+
socketTable.shutdown(socketId, "write");
|
|
2787
|
+
}
|
|
2788
|
+
catch { /* already closed */ }
|
|
2789
|
+
callback();
|
|
2790
|
+
},
|
|
2791
|
+
destroy(err, callback) {
|
|
2792
|
+
try {
|
|
2793
|
+
socketTable.close(socketId, pid);
|
|
2794
|
+
}
|
|
2795
|
+
catch { /* already closed */ }
|
|
2796
|
+
callback(err);
|
|
2797
|
+
},
|
|
2798
|
+
});
|
|
2799
|
+
// Socket-like properties for Node http module
|
|
2800
|
+
const socket = socketTable.get(socketId);
|
|
2801
|
+
const localAddr = socket?.localAddr;
|
|
2802
|
+
const remoteAddr = socket?.remoteAddr;
|
|
2803
|
+
duplex.remoteAddress =
|
|
2804
|
+
remoteAddr && typeof remoteAddr === "object" && "host" in remoteAddr
|
|
2805
|
+
? remoteAddr.host
|
|
2806
|
+
: "127.0.0.1";
|
|
2807
|
+
duplex.remotePort =
|
|
2808
|
+
remoteAddr && typeof remoteAddr === "object" && "port" in remoteAddr
|
|
2809
|
+
? remoteAddr.port
|
|
2810
|
+
: 0;
|
|
2811
|
+
duplex.localAddress =
|
|
2812
|
+
localAddr && typeof localAddr === "object" && "host" in localAddr
|
|
2813
|
+
? localAddr.host
|
|
2814
|
+
: "127.0.0.1";
|
|
2815
|
+
duplex.localPort =
|
|
2816
|
+
localAddr && typeof localAddr === "object" && "port" in localAddr
|
|
2817
|
+
? localAddr.port
|
|
2818
|
+
: 0;
|
|
2819
|
+
duplex.encrypted = false;
|
|
2820
|
+
duplex.setNoDelay = () => duplex;
|
|
2821
|
+
duplex.setKeepAlive = () => duplex;
|
|
2822
|
+
duplex.setTimeout = (ms, cb) => {
|
|
2823
|
+
if (cb)
|
|
2824
|
+
duplex.once("timeout", cb);
|
|
2825
|
+
return duplex;
|
|
2826
|
+
};
|
|
2827
|
+
duplex.ref = () => duplex;
|
|
2828
|
+
duplex.unref = () => duplex;
|
|
2829
|
+
async function runReadPump() {
|
|
2830
|
+
try {
|
|
2831
|
+
while (true) {
|
|
2832
|
+
let data;
|
|
2833
|
+
try {
|
|
2834
|
+
data = socketTable.recv(socketId, 65536, 0);
|
|
2835
|
+
}
|
|
2836
|
+
catch {
|
|
2837
|
+
break; // socket closed or error
|
|
2838
|
+
}
|
|
2839
|
+
if (data !== null) {
|
|
2840
|
+
debugHttpBridge("socket read", socketId, data.length);
|
|
2841
|
+
if (!duplex.push(Buffer.from(data))) {
|
|
2842
|
+
// Backpressure — wait for drain before continuing
|
|
2843
|
+
readPumpStarted = false;
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
// Check for EOF
|
|
2849
|
+
const sock = socketTable.get(socketId);
|
|
2850
|
+
if (!sock)
|
|
2851
|
+
break;
|
|
2852
|
+
if (sock.state === "closed" || sock.state === "read-closed")
|
|
2853
|
+
break;
|
|
2854
|
+
if (sock.peerWriteClosed || (sock.peerId === undefined && !sock.external))
|
|
2855
|
+
break;
|
|
2856
|
+
if (sock.external && sock.readBuffer.length === 0 && sock.peerWriteClosed)
|
|
2857
|
+
break;
|
|
2858
|
+
// Wait for data
|
|
2859
|
+
const handle = sock.readWaiters.enqueue();
|
|
2860
|
+
await handle.wait();
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
catch {
|
|
2864
|
+
// Socket destroyed during pump
|
|
2865
|
+
}
|
|
2866
|
+
duplex.push(null); // EOF
|
|
2867
|
+
}
|
|
2868
|
+
return duplex;
|
|
2869
|
+
}
|
|
2870
|
+
/** Build network bridge handlers (fetch, httpRequest, dnsLookup, httpServer). */
|
|
2871
|
+
export function buildNetworkBridgeHandlers(deps) {
|
|
2872
|
+
if (!deps.socketTable || deps.pid === undefined) {
|
|
2873
|
+
throw new Error("buildNetworkBridgeHandlers requires a kernel socketTable and pid");
|
|
2874
|
+
}
|
|
2875
|
+
const handlers = {};
|
|
2876
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
2877
|
+
const adapter = deps.networkAdapter;
|
|
2878
|
+
const jsonLimit = deps.isolateJsonPayloadLimitBytes;
|
|
2879
|
+
const ownedHttpServers = new Set();
|
|
2880
|
+
const ownedHttp2Servers = new Set();
|
|
2881
|
+
const { socketTable, pid } = deps;
|
|
2882
|
+
// Track kernel HTTP servers for cleanup
|
|
2883
|
+
const kernelHttpServers = new Map();
|
|
2884
|
+
const kernelHttp2Servers = new Map();
|
|
2885
|
+
const kernelHttp2ClientSessions = new Map();
|
|
2886
|
+
const http2Sessions = new Map();
|
|
2887
|
+
const http2Streams = new Map();
|
|
2888
|
+
const http2ServerSessionIds = new WeakMap();
|
|
2889
|
+
let nextHttp2SessionId = 1;
|
|
2890
|
+
let nextHttp2StreamId = 1;
|
|
2891
|
+
const kernelUpgradeSockets = new Map();
|
|
2892
|
+
let nextKernelUpgradeSocketId = 1;
|
|
2893
|
+
const loopbackAwareAdapter = adapter;
|
|
2894
|
+
const decodeTlsMaterial = (value) => {
|
|
2895
|
+
if (value === undefined) {
|
|
2896
|
+
return undefined;
|
|
2897
|
+
}
|
|
2898
|
+
const decodeOne = (entry) => entry.kind === "buffer" ? Buffer.from(entry.data, "base64") : entry.data;
|
|
2899
|
+
return Array.isArray(value) ? value.map(decodeOne) : decodeOne(value);
|
|
2900
|
+
};
|
|
2901
|
+
const buildHostTlsOptions = (options) => {
|
|
2902
|
+
if (!options) {
|
|
2903
|
+
return {};
|
|
2904
|
+
}
|
|
2905
|
+
const hostOptions = {};
|
|
2906
|
+
const key = decodeTlsMaterial(options.key);
|
|
2907
|
+
const cert = decodeTlsMaterial(options.cert);
|
|
2908
|
+
const ca = decodeTlsMaterial(options.ca);
|
|
2909
|
+
if (key !== undefined)
|
|
2910
|
+
hostOptions.key = key;
|
|
2911
|
+
if (cert !== undefined)
|
|
2912
|
+
hostOptions.cert = cert;
|
|
2913
|
+
if (ca !== undefined)
|
|
2914
|
+
hostOptions.ca = ca;
|
|
2915
|
+
if (typeof options.passphrase === "string")
|
|
2916
|
+
hostOptions.passphrase = options.passphrase;
|
|
2917
|
+
if (typeof options.ciphers === "string")
|
|
2918
|
+
hostOptions.ciphers = options.ciphers;
|
|
2919
|
+
if (typeof options.session === "string")
|
|
2920
|
+
hostOptions.session = Buffer.from(options.session, "base64");
|
|
2921
|
+
if (Array.isArray(options.ALPNProtocols) && options.ALPNProtocols.length > 0) {
|
|
2922
|
+
hostOptions.ALPNProtocols = [...options.ALPNProtocols];
|
|
2923
|
+
}
|
|
2924
|
+
if (typeof options.minVersion === "string")
|
|
2925
|
+
hostOptions.minVersion = options.minVersion;
|
|
2926
|
+
if (typeof options.maxVersion === "string")
|
|
2927
|
+
hostOptions.maxVersion = options.maxVersion;
|
|
2928
|
+
if (typeof options.servername === "string")
|
|
2929
|
+
hostOptions.servername = options.servername;
|
|
2930
|
+
if (typeof options.requestCert === "boolean")
|
|
2931
|
+
hostOptions.requestCert = options.requestCert;
|
|
2932
|
+
if (typeof options.rejectUnauthorized === "boolean") {
|
|
2933
|
+
hostOptions.rejectUnauthorized = options.rejectUnauthorized;
|
|
2934
|
+
}
|
|
2935
|
+
return hostOptions;
|
|
2936
|
+
};
|
|
2937
|
+
const debugHttp2Bridge = (...args) => {
|
|
2938
|
+
if (process.env.SECURE_EXEC_DEBUG_HTTP2_BRIDGE === "1") {
|
|
2939
|
+
console.error("[secure-exec http2 bridge]", ...args);
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2942
|
+
const emitHttp2Event = (...fields) => {
|
|
2943
|
+
const [kind, id, data, extra, extraNumber, extraHeaders, flags] = fields;
|
|
2944
|
+
debugHttp2Bridge("emit", kind, id);
|
|
2945
|
+
deps.sendStreamEvent("http2", Buffer.from(JSON.stringify({
|
|
2946
|
+
kind,
|
|
2947
|
+
id,
|
|
2948
|
+
data,
|
|
2949
|
+
extra,
|
|
2950
|
+
extraNumber,
|
|
2951
|
+
extraHeaders,
|
|
2952
|
+
flags,
|
|
2953
|
+
})));
|
|
2954
|
+
};
|
|
2955
|
+
const serializeHttp2SocketState = (socket) => JSON.stringify({
|
|
2956
|
+
encrypted: socket.encrypted === true,
|
|
2957
|
+
allowHalfOpen: socket.allowHalfOpen === true,
|
|
2958
|
+
localAddress: socket.localAddress,
|
|
2959
|
+
localPort: socket.localPort,
|
|
2960
|
+
localFamily: socket.localAddress?.includes(":") ? "IPv6" : "IPv4",
|
|
2961
|
+
remoteAddress: socket.remoteAddress,
|
|
2962
|
+
remotePort: socket.remotePort,
|
|
2963
|
+
remoteFamily: socket.remoteAddress?.includes(":") ? "IPv6" : "IPv4",
|
|
2964
|
+
servername: typeof socket.servername === "string"
|
|
2965
|
+
? socket.servername
|
|
2966
|
+
: undefined,
|
|
2967
|
+
alpnProtocol: socket.alpnProtocol || false,
|
|
2968
|
+
});
|
|
2969
|
+
const serializeHttp2SessionState = (session) => JSON.stringify({
|
|
2970
|
+
encrypted: session.encrypted === true,
|
|
2971
|
+
alpnProtocol: session.alpnProtocol || (session.encrypted ? "h2" : "h2c"),
|
|
2972
|
+
originSet: Array.isArray(session.originSet) ? [...session.originSet] : undefined,
|
|
2973
|
+
localSettings: session.localSettings && typeof session.localSettings === "object"
|
|
2974
|
+
? session.localSettings
|
|
2975
|
+
: undefined,
|
|
2976
|
+
remoteSettings: session.remoteSettings && typeof session.remoteSettings === "object"
|
|
2977
|
+
? session.remoteSettings
|
|
2978
|
+
: undefined,
|
|
2979
|
+
state: session.state && typeof session.state === "object"
|
|
2980
|
+
? {
|
|
2981
|
+
effectiveLocalWindowSize: typeof session.state.effectiveLocalWindowSize === "number"
|
|
2982
|
+
? session.state.effectiveLocalWindowSize
|
|
2983
|
+
: undefined,
|
|
2984
|
+
localWindowSize: typeof session.state.localWindowSize === "number"
|
|
2985
|
+
? session.state.localWindowSize
|
|
2986
|
+
: undefined,
|
|
2987
|
+
remoteWindowSize: typeof session.state.remoteWindowSize === "number"
|
|
2988
|
+
? session.state.remoteWindowSize
|
|
2989
|
+
: undefined,
|
|
2990
|
+
nextStreamID: typeof session.state.nextStreamID === "number"
|
|
2991
|
+
? session.state.nextStreamID
|
|
2992
|
+
: undefined,
|
|
2993
|
+
outboundQueueSize: typeof session.state.outboundQueueSize === "number"
|
|
2994
|
+
? session.state.outboundQueueSize
|
|
2995
|
+
: undefined,
|
|
2996
|
+
deflateDynamicTableSize: typeof session.state.deflateDynamicTableSize === "number"
|
|
2997
|
+
? session.state.deflateDynamicTableSize
|
|
2998
|
+
: undefined,
|
|
2999
|
+
inflateDynamicTableSize: typeof session.state.inflateDynamicTableSize === "number"
|
|
3000
|
+
? session.state.inflateDynamicTableSize
|
|
3001
|
+
: undefined,
|
|
3002
|
+
}
|
|
3003
|
+
: undefined,
|
|
3004
|
+
socket: session.socket ? JSON.parse(serializeHttp2SocketState(session.socket)) : undefined,
|
|
3005
|
+
});
|
|
3006
|
+
// Let host-side runtime.network.fetch/httpRequest reach only the HTTP
|
|
3007
|
+
// listeners owned by this execution.
|
|
3008
|
+
loopbackAwareAdapter.__setLoopbackPortChecker?.((_hostname, port) => {
|
|
3009
|
+
for (const state of kernelHttpServers.values()) {
|
|
3010
|
+
const socket = socketTable.get(state.listenSocketId);
|
|
3011
|
+
const localAddr = socket?.localAddr;
|
|
3012
|
+
if (localAddr && typeof localAddr === "object" && "port" in localAddr) {
|
|
3013
|
+
if (localAddr.port === port) {
|
|
3014
|
+
return true;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
return false;
|
|
3019
|
+
});
|
|
3020
|
+
const registerKernelUpgradeSocket = (socket) => {
|
|
3021
|
+
const socketId = nextKernelUpgradeSocketId++;
|
|
3022
|
+
kernelUpgradeSockets.set(socketId, socket);
|
|
3023
|
+
socket.on("data", (chunk) => {
|
|
3024
|
+
deps.sendStreamEvent("upgradeSocketData", Buffer.from(JSON.stringify({
|
|
3025
|
+
socketId,
|
|
3026
|
+
dataBase64: Buffer.from(chunk).toString("base64"),
|
|
3027
|
+
})));
|
|
3028
|
+
});
|
|
3029
|
+
socket.on("end", () => {
|
|
3030
|
+
deps.sendStreamEvent("upgradeSocketEnd", Buffer.from(JSON.stringify({ socketId })));
|
|
3031
|
+
});
|
|
3032
|
+
socket.on("close", () => {
|
|
3033
|
+
kernelUpgradeSockets.delete(socketId);
|
|
3034
|
+
});
|
|
3035
|
+
return socketId;
|
|
3036
|
+
};
|
|
3037
|
+
const finalizeKernelServerClose = (serverId, state) => {
|
|
3038
|
+
debugHttpBridge("finalize close check", serverId, state.closeRequested, state.pendingRequests);
|
|
3039
|
+
if (!state.closeRequested || state.pendingRequests > 0) {
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (!state.transportClosed) {
|
|
3043
|
+
state.acceptLoopActive = false;
|
|
3044
|
+
state.transportClosed = true;
|
|
3045
|
+
try {
|
|
3046
|
+
socketTable?.close(state.listenSocketId, pid);
|
|
3047
|
+
}
|
|
3048
|
+
catch { /* already closed */ }
|
|
3049
|
+
try {
|
|
3050
|
+
state.httpServer.close();
|
|
3051
|
+
}
|
|
3052
|
+
catch { /* parser server is never bound */ }
|
|
3053
|
+
}
|
|
3054
|
+
debugHttpBridge("finalize close", serverId);
|
|
3055
|
+
state.resolveClosed();
|
|
3056
|
+
kernelHttpServers.delete(serverId);
|
|
3057
|
+
ownedHttpServers.delete(serverId);
|
|
3058
|
+
deps.activeHttpServerIds.delete(serverId);
|
|
3059
|
+
deps.activeHttpServerClosers.delete(serverId);
|
|
3060
|
+
};
|
|
3061
|
+
const closeKernelServer = async (serverId) => {
|
|
3062
|
+
const state = kernelHttpServers.get(serverId);
|
|
3063
|
+
if (!state)
|
|
3064
|
+
return;
|
|
3065
|
+
debugHttpBridge("close requested", serverId, state.pendingRequests);
|
|
3066
|
+
state.closeRequested = true;
|
|
3067
|
+
finalizeKernelServerClose(serverId, state);
|
|
3068
|
+
};
|
|
3069
|
+
handlers[K.networkFetchRaw] = async (url, optionsJson) => {
|
|
3070
|
+
checkBridgeBudget(deps);
|
|
3071
|
+
const options = parseJsonWithLimit("network.fetch options", String(optionsJson), jsonLimit);
|
|
3072
|
+
const result = await adapter.fetch(String(url), options);
|
|
3073
|
+
const json = JSON.stringify(result);
|
|
3074
|
+
assertTextPayloadSize("network.fetch response", json, jsonLimit);
|
|
3075
|
+
return json;
|
|
3076
|
+
};
|
|
3077
|
+
handlers[K.networkDnsLookupRaw] = async (hostname) => {
|
|
3078
|
+
checkBridgeBudget(deps);
|
|
3079
|
+
const result = await adapter.dnsLookup(String(hostname));
|
|
3080
|
+
return JSON.stringify(result);
|
|
3081
|
+
};
|
|
3082
|
+
handlers[K.networkHttpRequestRaw] = async (url, optionsJson) => {
|
|
3083
|
+
checkBridgeBudget(deps);
|
|
3084
|
+
const options = parseJsonWithLimit("network.httpRequest options", String(optionsJson), jsonLimit);
|
|
3085
|
+
const result = await adapter.httpRequest(String(url), options);
|
|
3086
|
+
const json = JSON.stringify(result);
|
|
3087
|
+
assertTextPayloadSize("network.httpRequest response", json, jsonLimit);
|
|
3088
|
+
return json;
|
|
3089
|
+
};
|
|
3090
|
+
handlers[K.networkHttpServerRespondRaw] = (serverId, requestId, responseJson) => {
|
|
3091
|
+
const numericServerId = Number(serverId);
|
|
3092
|
+
debugHttpBridge("respond callback", numericServerId, requestId);
|
|
3093
|
+
resolveHttpServerResponse({
|
|
3094
|
+
serverId: numericServerId,
|
|
3095
|
+
requestId: Number(requestId),
|
|
3096
|
+
responseJson: String(responseJson),
|
|
3097
|
+
});
|
|
3098
|
+
const state = kernelHttpServers.get(numericServerId);
|
|
3099
|
+
if (!state) {
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
state.pendingRequests = Math.max(0, state.pendingRequests - 1);
|
|
3103
|
+
finalizeKernelServerClose(numericServerId, state);
|
|
3104
|
+
};
|
|
3105
|
+
handlers[K.networkHttpServerWaitRaw] = async (serverId) => {
|
|
3106
|
+
const numericServerId = Number(serverId);
|
|
3107
|
+
debugHttpBridge("wait start", numericServerId);
|
|
3108
|
+
const state = kernelHttpServers.get(numericServerId);
|
|
3109
|
+
if (!state) {
|
|
3110
|
+
debugHttpBridge("wait missing", numericServerId);
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
await state.closedPromise;
|
|
3114
|
+
debugHttpBridge("wait resolved", numericServerId);
|
|
3115
|
+
};
|
|
3116
|
+
// HTTP server listen — always route through the kernel socket table
|
|
3117
|
+
handlers[K.networkHttpServerListenRaw] = (optionsJson) => {
|
|
3118
|
+
const options = parseJsonWithLimit("network.httpServer.listen options", String(optionsJson), jsonLimit);
|
|
3119
|
+
deps.pendingHttpServerStarts.count += 1;
|
|
3120
|
+
return (async () => {
|
|
3121
|
+
try {
|
|
3122
|
+
const host = normalizeLoopbackHostname(options.hostname);
|
|
3123
|
+
debugHttpBridge("listen start", options.serverId, host, options.port ?? 0);
|
|
3124
|
+
const listenSocketId = socketTable.create(AF_INET, SOCK_STREAM, 0, pid);
|
|
3125
|
+
await socketTable.bind(listenSocketId, { host, port: options.port ?? 0 });
|
|
3126
|
+
await socketTable.listen(listenSocketId, 128, { external: true });
|
|
3127
|
+
// Get actual bound address (may differ for ephemeral port)
|
|
3128
|
+
const listenSocket = socketTable.get(listenSocketId);
|
|
3129
|
+
const addr = listenSocket?.localAddr;
|
|
3130
|
+
const address = addr ? {
|
|
3131
|
+
address: addr.host,
|
|
3132
|
+
family: addr.host.includes(":") ? "IPv6" : "IPv4",
|
|
3133
|
+
port: addr.port,
|
|
3134
|
+
} : null;
|
|
3135
|
+
// Create local HTTP server for parsing (not bound to any port)
|
|
3136
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
3137
|
+
try {
|
|
3138
|
+
debugHttpBridge("request start", options.serverId, req.method, req.url);
|
|
3139
|
+
const chunks = [];
|
|
3140
|
+
for await (const chunk of req) {
|
|
3141
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3142
|
+
}
|
|
3143
|
+
const headers = {};
|
|
3144
|
+
Object.entries(req.headers).forEach(([key, value]) => {
|
|
3145
|
+
if (typeof value === "string")
|
|
3146
|
+
headers[key] = value;
|
|
3147
|
+
else if (Array.isArray(value))
|
|
3148
|
+
headers[key] = value[0] ?? "";
|
|
3149
|
+
});
|
|
3150
|
+
if (!headers.host && addr) {
|
|
3151
|
+
headers.host = `${addr.host}:${addr.port}`;
|
|
3152
|
+
}
|
|
3153
|
+
const requestJson = JSON.stringify({
|
|
3154
|
+
method: req.method || "GET",
|
|
3155
|
+
url: req.url || "/",
|
|
3156
|
+
headers,
|
|
3157
|
+
rawHeaders: req.rawHeaders || [],
|
|
3158
|
+
bodyBase64: chunks.length > 0
|
|
3159
|
+
? Buffer.concat(chunks).toString("base64")
|
|
3160
|
+
: undefined,
|
|
3161
|
+
});
|
|
3162
|
+
const requestId = nextHttpRequestId++;
|
|
3163
|
+
// Send request to sandbox and wait for response
|
|
3164
|
+
const responsePromise = new Promise((resolve) => {
|
|
3165
|
+
registerPendingHttpResponse(options.serverId, requestId, resolve);
|
|
3166
|
+
});
|
|
3167
|
+
state.pendingRequests += 1;
|
|
3168
|
+
deps.sendStreamEvent("http_request", serialize({
|
|
3169
|
+
requestId,
|
|
3170
|
+
serverId: options.serverId,
|
|
3171
|
+
request: requestJson,
|
|
3172
|
+
}));
|
|
3173
|
+
const responseJson = await responsePromise;
|
|
3174
|
+
const response = parseJsonWithLimit("network.httpServer response", responseJson, jsonLimit);
|
|
3175
|
+
for (const informational of response.informational || []) {
|
|
3176
|
+
const rawHeaderLines = informational.rawHeaders && informational.rawHeaders.length > 0
|
|
3177
|
+
? informational.rawHeaders
|
|
3178
|
+
: (informational.headers || []).flatMap(([key, value]) => [key, value]);
|
|
3179
|
+
const statusText = informational.statusText ||
|
|
3180
|
+
http.STATUS_CODES[informational.status] ||
|
|
3181
|
+
"";
|
|
3182
|
+
const rawFrame = `HTTP/1.1 ${informational.status} ${statusText}\r\n` +
|
|
3183
|
+
rawHeaderLines.reduce((acc, entry, index) => index % 2 === 0
|
|
3184
|
+
? `${acc}${entry}: ${rawHeaderLines[index + 1] ?? ""}\r\n`
|
|
3185
|
+
: acc, "") +
|
|
3186
|
+
"\r\n";
|
|
3187
|
+
res._writeRaw?.(rawFrame);
|
|
3188
|
+
}
|
|
3189
|
+
res.statusCode = response.status || 200;
|
|
3190
|
+
for (const [key, value] of response.headers || []) {
|
|
3191
|
+
res.setHeader(key, value);
|
|
3192
|
+
}
|
|
3193
|
+
if (response.body !== undefined) {
|
|
3194
|
+
if (response.bodyEncoding === "base64") {
|
|
3195
|
+
debugHttpBridge("response end", options.serverId, response.status, "base64", response.body.length);
|
|
3196
|
+
res.end(Buffer.from(response.body, "base64"));
|
|
3197
|
+
}
|
|
3198
|
+
else {
|
|
3199
|
+
debugHttpBridge("response end", options.serverId, response.status, "utf8", response.body.length);
|
|
3200
|
+
res.end(response.body);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
else {
|
|
3204
|
+
debugHttpBridge("response end", options.serverId, response.status, "empty", 0);
|
|
3205
|
+
res.end();
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
catch {
|
|
3209
|
+
debugHttpBridge("request error", options.serverId, req.method, req.url);
|
|
3210
|
+
res.statusCode = 500;
|
|
3211
|
+
res.end("Internal Server Error");
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
// Handle HTTP upgrades through kernel sockets
|
|
3215
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
3216
|
+
const upgradeHeaders = {};
|
|
3217
|
+
Object.entries(req.headers).forEach(([key, value]) => {
|
|
3218
|
+
if (typeof value === "string")
|
|
3219
|
+
upgradeHeaders[key] = value;
|
|
3220
|
+
else if (Array.isArray(value))
|
|
3221
|
+
upgradeHeaders[key] = value[0] ?? "";
|
|
3222
|
+
});
|
|
3223
|
+
const upgradeSocketId = registerKernelUpgradeSocket(socket);
|
|
3224
|
+
deps.sendStreamEvent("httpServerUpgrade", Buffer.from(JSON.stringify({
|
|
3225
|
+
serverId: options.serverId,
|
|
3226
|
+
request: JSON.stringify({
|
|
3227
|
+
method: req.method || "GET",
|
|
3228
|
+
url: req.url || "/",
|
|
3229
|
+
headers: upgradeHeaders,
|
|
3230
|
+
rawHeaders: req.rawHeaders || [],
|
|
3231
|
+
}),
|
|
3232
|
+
head: head.toString("base64"),
|
|
3233
|
+
socketId: upgradeSocketId,
|
|
3234
|
+
})));
|
|
3235
|
+
});
|
|
3236
|
+
httpServer.on("connect", (req, socket, head) => {
|
|
3237
|
+
const connectHeaders = {};
|
|
3238
|
+
Object.entries(req.headers).forEach(([key, value]) => {
|
|
3239
|
+
if (typeof value === "string")
|
|
3240
|
+
connectHeaders[key] = value;
|
|
3241
|
+
else if (Array.isArray(value))
|
|
3242
|
+
connectHeaders[key] = value[0] ?? "";
|
|
3243
|
+
});
|
|
3244
|
+
const connectSocketId = registerKernelUpgradeSocket(socket);
|
|
3245
|
+
deps.sendStreamEvent("httpServerConnect", Buffer.from(JSON.stringify({
|
|
3246
|
+
serverId: options.serverId,
|
|
3247
|
+
request: JSON.stringify({
|
|
3248
|
+
method: req.method || "CONNECT",
|
|
3249
|
+
url: req.url || "/",
|
|
3250
|
+
headers: connectHeaders,
|
|
3251
|
+
rawHeaders: req.rawHeaders || [],
|
|
3252
|
+
}),
|
|
3253
|
+
head: head.toString("base64"),
|
|
3254
|
+
socketId: connectSocketId,
|
|
3255
|
+
})));
|
|
3256
|
+
});
|
|
3257
|
+
let resolveClosed;
|
|
3258
|
+
const closedPromise = new Promise((resolve) => {
|
|
3259
|
+
resolveClosed = resolve;
|
|
3260
|
+
});
|
|
3261
|
+
const state = {
|
|
3262
|
+
listenSocketId,
|
|
3263
|
+
httpServer,
|
|
3264
|
+
acceptLoopActive: true,
|
|
3265
|
+
closedPromise,
|
|
3266
|
+
resolveClosed,
|
|
3267
|
+
pendingRequests: 0,
|
|
3268
|
+
closeRequested: false,
|
|
3269
|
+
transportClosed: false,
|
|
3270
|
+
};
|
|
3271
|
+
debugHttpBridge("listen ready", options.serverId, address);
|
|
3272
|
+
kernelHttpServers.set(options.serverId, state);
|
|
3273
|
+
ownedHttpServers.add(options.serverId);
|
|
3274
|
+
deps.activeHttpServerIds.add(options.serverId);
|
|
3275
|
+
deps.activeHttpServerClosers.set(options.serverId, () => closeKernelServer(options.serverId));
|
|
3276
|
+
// Start accept loop (fire-and-forget)
|
|
3277
|
+
void startKernelHttpAcceptLoop(state, socketTable, pid);
|
|
3278
|
+
return JSON.stringify({ address });
|
|
3279
|
+
}
|
|
3280
|
+
finally {
|
|
3281
|
+
deps.pendingHttpServerStarts.count -= 1;
|
|
3282
|
+
}
|
|
3283
|
+
})();
|
|
3284
|
+
};
|
|
3285
|
+
// HTTP server close — kernel-owned servers only
|
|
3286
|
+
handlers[K.networkHttpServerCloseRaw] = (serverId) => {
|
|
3287
|
+
const id = Number(serverId);
|
|
3288
|
+
debugHttpBridge("close bridge call", id);
|
|
3289
|
+
if (!ownedHttpServers.has(id)) {
|
|
3290
|
+
throw new Error(`Cannot close server ${id}: not owned by this execution context`);
|
|
3291
|
+
}
|
|
3292
|
+
const kernelState = kernelHttpServers.get(id);
|
|
3293
|
+
if (!kernelState) {
|
|
3294
|
+
throw new Error(`Cannot close server ${id}: kernel server state missing`);
|
|
3295
|
+
}
|
|
3296
|
+
return closeKernelServer(id);
|
|
3297
|
+
};
|
|
3298
|
+
const closeKernelHttp2Server = async (serverId) => {
|
|
3299
|
+
const state = kernelHttp2Servers.get(serverId);
|
|
3300
|
+
if (!state) {
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
state.acceptLoopActive = false;
|
|
3304
|
+
try {
|
|
3305
|
+
socketTable.close(state.listenSocketId, pid);
|
|
3306
|
+
}
|
|
3307
|
+
catch {
|
|
3308
|
+
// Listener already closed.
|
|
3309
|
+
}
|
|
3310
|
+
for (const session of [...state.sessions]) {
|
|
3311
|
+
try {
|
|
3312
|
+
session.close();
|
|
3313
|
+
}
|
|
3314
|
+
catch {
|
|
3315
|
+
// Ignore already-closing sessions.
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
await new Promise((resolve) => {
|
|
3319
|
+
try {
|
|
3320
|
+
state.server.close(() => resolve());
|
|
3321
|
+
}
|
|
3322
|
+
catch {
|
|
3323
|
+
resolve();
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
kernelHttp2Servers.delete(serverId);
|
|
3327
|
+
ownedHttp2Servers.delete(serverId);
|
|
3328
|
+
deps.activeHttpServerIds.delete(serverId);
|
|
3329
|
+
deps.activeHttpServerClosers.delete(serverId);
|
|
3330
|
+
state.resolveClosed();
|
|
3331
|
+
};
|
|
3332
|
+
const startKernelHttp2AcceptLoop = async (state) => {
|
|
3333
|
+
try {
|
|
3334
|
+
while (state.acceptLoopActive) {
|
|
3335
|
+
const listenSocket = socketTable.get(state.listenSocketId);
|
|
3336
|
+
if (!listenSocket || listenSocket.state !== "listening") {
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3339
|
+
const acceptedId = socketTable.accept(state.listenSocketId);
|
|
3340
|
+
if (acceptedId !== null) {
|
|
3341
|
+
const duplex = createKernelSocketDuplex(acceptedId, socketTable, pid);
|
|
3342
|
+
state.server.emit("connection", duplex);
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
const handle = listenSocket.acceptWaiters.enqueue();
|
|
3346
|
+
const acceptedAfterEnqueue = socketTable.accept(state.listenSocketId);
|
|
3347
|
+
if (acceptedAfterEnqueue !== null) {
|
|
3348
|
+
handle.wake();
|
|
3349
|
+
const duplex = createKernelSocketDuplex(acceptedAfterEnqueue, socketTable, pid);
|
|
3350
|
+
state.server.emit("connection", duplex);
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
await handle.wait();
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
catch {
|
|
3357
|
+
// Listener closed.
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
const normalizeHttp2EventHeaders = (headers) => {
|
|
3361
|
+
const normalizedHeaders = {};
|
|
3362
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
3363
|
+
if (value !== undefined) {
|
|
3364
|
+
normalizedHeaders[key] = value;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
return normalizedHeaders;
|
|
3368
|
+
};
|
|
3369
|
+
const emitHttp2SerializedError = (kind, id, error) => {
|
|
3370
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3371
|
+
emitHttp2Event(kind, id, JSON.stringify({
|
|
3372
|
+
message: err.message,
|
|
3373
|
+
name: err.name,
|
|
3374
|
+
code: err.code,
|
|
3375
|
+
}));
|
|
3376
|
+
};
|
|
3377
|
+
const attachHttp2ClientStreamListeners = (streamId, stream) => {
|
|
3378
|
+
stream.on("response", (headers) => {
|
|
3379
|
+
emitHttp2Event("clientResponseHeaders", streamId, JSON.stringify(normalizeHttp2EventHeaders(headers)));
|
|
3380
|
+
});
|
|
3381
|
+
stream.on("push", (headers, flags) => {
|
|
3382
|
+
setImmediate(() => {
|
|
3383
|
+
emitHttp2Event("clientPushHeaders", streamId, JSON.stringify(normalizeHttp2EventHeaders(headers)), undefined, String(flags ?? 0));
|
|
3384
|
+
});
|
|
3385
|
+
});
|
|
3386
|
+
stream.on("data", (chunk) => {
|
|
3387
|
+
emitHttp2Event("clientData", streamId, (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("base64"));
|
|
3388
|
+
});
|
|
3389
|
+
stream.on("end", () => {
|
|
3390
|
+
debugHttp2Bridge("client response end", streamId);
|
|
3391
|
+
setImmediate(() => {
|
|
3392
|
+
emitHttp2Event("clientEnd", streamId);
|
|
3393
|
+
});
|
|
3394
|
+
});
|
|
3395
|
+
stream.on("close", () => {
|
|
3396
|
+
setImmediate(() => {
|
|
3397
|
+
emitHttp2Event("clientClose", streamId, undefined, undefined, String(stream.rstCode ?? 0));
|
|
3398
|
+
http2Streams.delete(streamId);
|
|
3399
|
+
});
|
|
3400
|
+
});
|
|
3401
|
+
stream.on("error", (error) => {
|
|
3402
|
+
emitHttp2SerializedError("clientError", streamId, error);
|
|
3403
|
+
});
|
|
3404
|
+
stream.resume();
|
|
3405
|
+
};
|
|
3406
|
+
const attachHttp2SessionListeners = (sessionId, session, onClose) => {
|
|
3407
|
+
session.on("close", () => {
|
|
3408
|
+
debugHttp2Bridge("session close", sessionId);
|
|
3409
|
+
emitHttp2Event("sessionClose", sessionId);
|
|
3410
|
+
http2Sessions.delete(sessionId);
|
|
3411
|
+
onClose?.();
|
|
3412
|
+
});
|
|
3413
|
+
session.on("error", (error) => {
|
|
3414
|
+
debugHttp2Bridge("session error", sessionId, error instanceof Error ? error.message : String(error));
|
|
3415
|
+
emitHttp2SerializedError("sessionError", sessionId, error);
|
|
3416
|
+
});
|
|
3417
|
+
session.on("localSettings", (settings) => {
|
|
3418
|
+
emitHttp2Event("sessionLocalSettings", sessionId, JSON.stringify(settings));
|
|
3419
|
+
});
|
|
3420
|
+
session.on("remoteSettings", (settings) => {
|
|
3421
|
+
emitHttp2Event("sessionRemoteSettings", sessionId, JSON.stringify(settings));
|
|
3422
|
+
});
|
|
3423
|
+
session.on("goaway", (errorCode, lastStreamID, opaqueData) => {
|
|
3424
|
+
emitHttp2Event("sessionGoaway", sessionId, Buffer.isBuffer(opaqueData) ? opaqueData.toString("base64") : undefined, undefined, String(errorCode), undefined, String(lastStreamID));
|
|
3425
|
+
});
|
|
3426
|
+
};
|
|
3427
|
+
handlers[K.networkHttp2ServerListenRaw] = (optionsJson) => {
|
|
3428
|
+
const options = parseJsonWithLimit("network.http2Server.listen options", String(optionsJson), jsonLimit);
|
|
3429
|
+
return (async () => {
|
|
3430
|
+
debugHttp2Bridge("server listen start", options.serverId, options.secure, options.host, options.port);
|
|
3431
|
+
const host = normalizeLoopbackHostname(options.host);
|
|
3432
|
+
const listenSocketId = socketTable.create(AF_INET, SOCK_STREAM, 0, pid);
|
|
3433
|
+
await socketTable.bind(listenSocketId, { host, port: options.port ?? 0 });
|
|
3434
|
+
await socketTable.listen(listenSocketId, options.backlog ?? 128, { external: true });
|
|
3435
|
+
const listenSocket = socketTable.get(listenSocketId);
|
|
3436
|
+
const addr = listenSocket?.localAddr;
|
|
3437
|
+
const address = addr ? {
|
|
3438
|
+
address: addr.host,
|
|
3439
|
+
family: addr.host.includes(":") ? "IPv6" : "IPv4",
|
|
3440
|
+
port: addr.port,
|
|
3441
|
+
} : null;
|
|
3442
|
+
const server = options.secure
|
|
3443
|
+
? http2.createSecureServer({
|
|
3444
|
+
allowHTTP1: options.allowHTTP1 === true,
|
|
3445
|
+
settings: options.settings,
|
|
3446
|
+
remoteCustomSettings: options.remoteCustomSettings,
|
|
3447
|
+
...buildHostTlsOptions(options.tls),
|
|
3448
|
+
})
|
|
3449
|
+
: http2.createServer({
|
|
3450
|
+
allowHTTP1: options.allowHTTP1 === true,
|
|
3451
|
+
settings: options.settings,
|
|
3452
|
+
remoteCustomSettings: options.remoteCustomSettings,
|
|
3453
|
+
});
|
|
3454
|
+
if (typeof options.timeout === "number" && options.timeout > 0) {
|
|
3455
|
+
server.setTimeout(options.timeout);
|
|
3456
|
+
}
|
|
3457
|
+
server.on("timeout", () => {
|
|
3458
|
+
emitHttp2Event("serverTimeout", options.serverId);
|
|
3459
|
+
});
|
|
3460
|
+
server.on("connection", (socket) => {
|
|
3461
|
+
emitHttp2Event("serverConnection", options.serverId, serializeHttp2SocketState(socket));
|
|
3462
|
+
});
|
|
3463
|
+
if (options.secure) {
|
|
3464
|
+
server.on("secureConnection", (socket) => {
|
|
3465
|
+
emitHttp2Event("serverSecureConnection", options.serverId, serializeHttp2SocketState(socket));
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
server.on("request", (req, res) => {
|
|
3469
|
+
if (req.httpVersionMajor === 2) {
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
void (async () => {
|
|
3473
|
+
const chunks = [];
|
|
3474
|
+
for await (const chunk of req) {
|
|
3475
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3476
|
+
}
|
|
3477
|
+
const headers = {};
|
|
3478
|
+
Object.entries(req.headers).forEach(([key, value]) => {
|
|
3479
|
+
if (typeof value === "string")
|
|
3480
|
+
headers[key] = value;
|
|
3481
|
+
else if (Array.isArray(value))
|
|
3482
|
+
headers[key] = value[0] ?? "";
|
|
3483
|
+
});
|
|
3484
|
+
const requestJson = JSON.stringify({
|
|
3485
|
+
method: req.method || "GET",
|
|
3486
|
+
url: req.url || "/",
|
|
3487
|
+
headers,
|
|
3488
|
+
rawHeaders: req.rawHeaders || [],
|
|
3489
|
+
bodyBase64: chunks.length > 0 ? Buffer.concat(chunks).toString("base64") : undefined,
|
|
3490
|
+
});
|
|
3491
|
+
const requestId = nextHttp2CompatRequestId++;
|
|
3492
|
+
const responsePromise = new Promise((resolve) => {
|
|
3493
|
+
registerPendingHttp2CompatResponse(options.serverId, requestId, resolve);
|
|
3494
|
+
});
|
|
3495
|
+
emitHttp2Event("serverCompatRequest", options.serverId, requestJson, undefined, String(requestId));
|
|
3496
|
+
const responseJson = await responsePromise;
|
|
3497
|
+
const response = parseJsonWithLimit("network.http2Server.compat response", responseJson, jsonLimit);
|
|
3498
|
+
res.statusCode = response.status || 200;
|
|
3499
|
+
for (const [key, value] of response.headers || []) {
|
|
3500
|
+
res.setHeader(key, value);
|
|
3501
|
+
}
|
|
3502
|
+
if (response.bodyEncoding === "base64" && typeof response.body === "string") {
|
|
3503
|
+
res.end(Buffer.from(response.body, "base64"));
|
|
3504
|
+
}
|
|
3505
|
+
else if (typeof response.body === "string") {
|
|
3506
|
+
res.end(response.body);
|
|
3507
|
+
}
|
|
3508
|
+
else {
|
|
3509
|
+
res.end();
|
|
3510
|
+
}
|
|
3511
|
+
})().catch((error) => {
|
|
3512
|
+
try {
|
|
3513
|
+
res.statusCode = 500;
|
|
3514
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
3515
|
+
}
|
|
3516
|
+
catch {
|
|
3517
|
+
// Response already closed.
|
|
3518
|
+
}
|
|
3519
|
+
});
|
|
3520
|
+
});
|
|
3521
|
+
server.on("stream", (stream, headers, flags) => {
|
|
3522
|
+
debugHttp2Bridge("server stream", options.serverId, flags);
|
|
3523
|
+
const streamSession = stream.session;
|
|
3524
|
+
if (!streamSession) {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
let sessionId = http2ServerSessionIds.get(streamSession);
|
|
3528
|
+
if (sessionId === undefined) {
|
|
3529
|
+
sessionId = nextHttp2SessionId++;
|
|
3530
|
+
http2ServerSessionIds.set(streamSession, sessionId);
|
|
3531
|
+
http2Sessions.set(sessionId, streamSession);
|
|
3532
|
+
attachHttp2SessionListeners(sessionId, streamSession);
|
|
3533
|
+
emitHttp2Event("serverSession", options.serverId, serializeHttp2SessionState(streamSession), undefined, String(sessionId));
|
|
3534
|
+
}
|
|
3535
|
+
const streamId = nextHttp2StreamId++;
|
|
3536
|
+
http2Streams.set(streamId, stream);
|
|
3537
|
+
stream.pause();
|
|
3538
|
+
stream.on("data", (chunk) => {
|
|
3539
|
+
emitHttp2Event("serverStreamData", streamId, (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("base64"));
|
|
3540
|
+
});
|
|
3541
|
+
stream.on("end", () => {
|
|
3542
|
+
emitHttp2Event("serverStreamEnd", streamId);
|
|
3543
|
+
});
|
|
3544
|
+
stream.on("drain", () => {
|
|
3545
|
+
emitHttp2Event("serverStreamDrain", streamId);
|
|
3546
|
+
});
|
|
3547
|
+
stream.on("error", (error) => {
|
|
3548
|
+
emitHttp2SerializedError("serverStreamError", streamId, error);
|
|
3549
|
+
});
|
|
3550
|
+
stream.on("close", () => {
|
|
3551
|
+
emitHttp2Event("serverStreamClose", streamId, undefined, undefined, String(stream.rstCode ?? 0));
|
|
3552
|
+
http2Streams.delete(streamId);
|
|
3553
|
+
});
|
|
3554
|
+
emitHttp2Event("serverStream", options.serverId, String(streamId), serializeHttp2SessionState(streamSession), String(sessionId), JSON.stringify(normalizeHttp2EventHeaders(headers)), String(flags ?? 0));
|
|
3555
|
+
});
|
|
3556
|
+
server.on("close", () => {
|
|
3557
|
+
debugHttp2Bridge("server close", options.serverId);
|
|
3558
|
+
emitHttp2Event("serverClose", options.serverId);
|
|
3559
|
+
});
|
|
3560
|
+
let resolveClosed;
|
|
3561
|
+
const closedPromise = new Promise((resolve) => {
|
|
3562
|
+
resolveClosed = resolve;
|
|
3563
|
+
});
|
|
3564
|
+
const state = {
|
|
3565
|
+
listenSocketId,
|
|
3566
|
+
server,
|
|
3567
|
+
sessions: new Set(),
|
|
3568
|
+
acceptLoopActive: true,
|
|
3569
|
+
closedPromise,
|
|
3570
|
+
resolveClosed,
|
|
3571
|
+
};
|
|
3572
|
+
server.on("session", (session) => {
|
|
3573
|
+
state.sessions.add(session);
|
|
3574
|
+
session.once("close", () => {
|
|
3575
|
+
state.sessions.delete(session);
|
|
3576
|
+
});
|
|
3577
|
+
});
|
|
3578
|
+
kernelHttp2Servers.set(options.serverId, state);
|
|
3579
|
+
ownedHttp2Servers.add(options.serverId);
|
|
3580
|
+
deps.activeHttpServerIds.add(options.serverId);
|
|
3581
|
+
deps.activeHttpServerClosers.set(options.serverId, () => closeKernelHttp2Server(options.serverId));
|
|
3582
|
+
void startKernelHttp2AcceptLoop(state);
|
|
3583
|
+
return JSON.stringify({ address });
|
|
3584
|
+
})();
|
|
3585
|
+
};
|
|
3586
|
+
handlers[K.networkHttp2ServerCloseRaw] = (serverId) => {
|
|
3587
|
+
const id = Number(serverId);
|
|
3588
|
+
if (!ownedHttp2Servers.has(id)) {
|
|
3589
|
+
throw new Error(`Cannot close HTTP/2 server ${id}: not owned by this execution context`);
|
|
3590
|
+
}
|
|
3591
|
+
return closeKernelHttp2Server(id);
|
|
3592
|
+
};
|
|
3593
|
+
handlers[K.networkHttp2ServerWaitRaw] = (serverId) => {
|
|
3594
|
+
const state = kernelHttp2Servers.get(Number(serverId));
|
|
3595
|
+
return state?.closedPromise ?? Promise.resolve();
|
|
3596
|
+
};
|
|
3597
|
+
handlers[K.networkHttp2SessionConnectRaw] = (optionsJson) => {
|
|
3598
|
+
const options = parseJsonWithLimit("network.http2Session.connect options", String(optionsJson), jsonLimit);
|
|
3599
|
+
return (async () => {
|
|
3600
|
+
const authority = String(options.authority);
|
|
3601
|
+
debugHttp2Bridge("session connect start", authority, options.socketId ?? null);
|
|
3602
|
+
const sessionId = nextHttp2SessionId++;
|
|
3603
|
+
let transport;
|
|
3604
|
+
if (typeof options.socketId === "number") {
|
|
3605
|
+
transport = createKernelSocketDuplex(options.socketId, socketTable, pid);
|
|
3606
|
+
}
|
|
3607
|
+
else {
|
|
3608
|
+
const host = String(options.host ?? "127.0.0.1");
|
|
3609
|
+
const port = Number(options.port ?? 0);
|
|
3610
|
+
const socketId = socketTable.create(host.includes(":") ? AF_INET6 : AF_INET, SOCK_STREAM, 0, pid);
|
|
3611
|
+
if (typeof options.localAddress === "string" && options.localAddress.length > 0) {
|
|
3612
|
+
await socketTable.bind(socketId, {
|
|
3613
|
+
host: options.localAddress,
|
|
3614
|
+
port: 0,
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
await socketTable.connect(socketId, { host, port });
|
|
3618
|
+
transport = createKernelSocketDuplex(socketId, socketTable, pid);
|
|
3619
|
+
}
|
|
3620
|
+
const session = http2.connect(authority, {
|
|
3621
|
+
settings: options.settings,
|
|
3622
|
+
remoteCustomSettings: options.remoteCustomSettings,
|
|
3623
|
+
createConnection: () => {
|
|
3624
|
+
debugHttp2Bridge("createConnection", authority, options.protocol);
|
|
3625
|
+
if (options.protocol === "https:") {
|
|
3626
|
+
return tls.connect({
|
|
3627
|
+
socket: transport,
|
|
3628
|
+
ALPNProtocols: ["h2"],
|
|
3629
|
+
servername: typeof options.tls?.servername === "string" && options.tls.servername.length > 0
|
|
3630
|
+
? options.tls.servername
|
|
3631
|
+
: undefined,
|
|
3632
|
+
...buildHostTlsOptions(options.tls),
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
return transport;
|
|
3636
|
+
},
|
|
3637
|
+
});
|
|
3638
|
+
let resolveClosed;
|
|
3639
|
+
const closedPromise = new Promise((resolve) => {
|
|
3640
|
+
resolveClosed = resolve;
|
|
3641
|
+
});
|
|
3642
|
+
http2Sessions.set(sessionId, session);
|
|
3643
|
+
kernelHttp2ClientSessions.set(sessionId, {
|
|
3644
|
+
session,
|
|
3645
|
+
closedPromise,
|
|
3646
|
+
resolveClosed,
|
|
3647
|
+
});
|
|
3648
|
+
session.on("connect", () => {
|
|
3649
|
+
debugHttp2Bridge("session connect", sessionId, authority);
|
|
3650
|
+
emitHttp2Event("sessionConnect", sessionId, serializeHttp2SessionState(session));
|
|
3651
|
+
});
|
|
3652
|
+
attachHttp2SessionListeners(sessionId, session, () => {
|
|
3653
|
+
kernelHttp2ClientSessions.get(sessionId)?.resolveClosed();
|
|
3654
|
+
kernelHttp2ClientSessions.delete(sessionId);
|
|
3655
|
+
});
|
|
3656
|
+
session.on("stream", (stream, headers, flags) => {
|
|
3657
|
+
const streamId = nextHttp2StreamId++;
|
|
3658
|
+
http2Streams.set(streamId, stream);
|
|
3659
|
+
attachHttp2ClientStreamListeners(streamId, stream);
|
|
3660
|
+
emitHttp2Event("clientPushStream", sessionId, String(streamId), undefined, undefined, JSON.stringify(normalizeHttp2EventHeaders(headers)), String(flags ?? 0));
|
|
3661
|
+
});
|
|
3662
|
+
return JSON.stringify({
|
|
3663
|
+
sessionId,
|
|
3664
|
+
state: serializeHttp2SessionState(session),
|
|
3665
|
+
});
|
|
3666
|
+
})();
|
|
3667
|
+
};
|
|
3668
|
+
handlers[K.networkHttp2SessionRequestRaw] = (sessionId, headersJson, optionsJson) => {
|
|
3669
|
+
const session = http2Sessions.get(Number(sessionId));
|
|
3670
|
+
if (!session) {
|
|
3671
|
+
throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
|
|
3672
|
+
}
|
|
3673
|
+
const headers = parseJsonWithLimit("network.http2Session.request headers", String(headersJson), jsonLimit);
|
|
3674
|
+
const requestOptions = parseJsonWithLimit("network.http2Session.request options", String(optionsJson), jsonLimit);
|
|
3675
|
+
const stream = session.request(headers, requestOptions);
|
|
3676
|
+
debugHttp2Bridge("session request", sessionId, stream.id);
|
|
3677
|
+
const streamId = nextHttp2StreamId++;
|
|
3678
|
+
http2Streams.set(streamId, stream);
|
|
3679
|
+
attachHttp2ClientStreamListeners(streamId, stream);
|
|
3680
|
+
return streamId;
|
|
3681
|
+
};
|
|
3682
|
+
handlers[K.networkHttp2SessionCloseRaw] = (sessionId) => {
|
|
3683
|
+
http2Sessions.get(Number(sessionId))?.close();
|
|
3684
|
+
};
|
|
3685
|
+
handlers[K.networkHttp2SessionSettingsRaw] = (sessionId, settingsJson) => {
|
|
3686
|
+
const session = http2Sessions.get(Number(sessionId));
|
|
3687
|
+
if (!session) {
|
|
3688
|
+
throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
|
|
3689
|
+
}
|
|
3690
|
+
const settings = parseJsonWithLimit("network.http2Session.settings settings", String(settingsJson), jsonLimit);
|
|
3691
|
+
session.settings(settings, () => {
|
|
3692
|
+
emitHttp2Event("sessionSettingsAck", Number(sessionId));
|
|
3693
|
+
});
|
|
3694
|
+
};
|
|
3695
|
+
handlers[K.networkHttp2SessionSetLocalWindowSizeRaw] = (sessionId, windowSize) => {
|
|
3696
|
+
const session = http2Sessions.get(Number(sessionId));
|
|
3697
|
+
if (!session) {
|
|
3698
|
+
throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
|
|
3699
|
+
}
|
|
3700
|
+
session.setLocalWindowSize(Number(windowSize));
|
|
3701
|
+
return serializeHttp2SessionState(session);
|
|
3702
|
+
};
|
|
3703
|
+
handlers[K.networkHttp2SessionGoawayRaw] = (sessionId, errorCode, lastStreamID, opaqueDataBase64) => {
|
|
3704
|
+
const session = http2Sessions.get(Number(sessionId));
|
|
3705
|
+
if (!session) {
|
|
3706
|
+
throw new Error(`HTTP/2 session ${String(sessionId)} not found`);
|
|
3707
|
+
}
|
|
3708
|
+
session.goaway(Number(errorCode), Number(lastStreamID), typeof opaqueDataBase64 === "string" && opaqueDataBase64.length > 0
|
|
3709
|
+
? Buffer.from(opaqueDataBase64, "base64")
|
|
3710
|
+
: undefined);
|
|
3711
|
+
};
|
|
3712
|
+
handlers[K.networkHttp2SessionDestroyRaw] = (sessionId) => {
|
|
3713
|
+
http2Sessions.get(Number(sessionId))?.destroy();
|
|
3714
|
+
};
|
|
3715
|
+
handlers[K.networkHttp2SessionWaitRaw] = (sessionId) => {
|
|
3716
|
+
const state = kernelHttp2ClientSessions.get(Number(sessionId));
|
|
3717
|
+
return state?.closedPromise ?? Promise.resolve();
|
|
3718
|
+
};
|
|
3719
|
+
handlers[K.networkHttp2StreamRespondRaw] = (streamId, headersJson) => {
|
|
3720
|
+
const stream = http2Streams.get(Number(streamId));
|
|
3721
|
+
if (!stream) {
|
|
3722
|
+
throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
|
|
3723
|
+
}
|
|
3724
|
+
const headers = parseJsonWithLimit("network.http2Stream.respond headers", String(headersJson), jsonLimit);
|
|
3725
|
+
stream.respond(headers);
|
|
3726
|
+
};
|
|
3727
|
+
handlers[K.networkHttp2StreamPushStreamRaw] = async (streamId, headersJson, optionsJson) => {
|
|
3728
|
+
const stream = http2Streams.get(Number(streamId));
|
|
3729
|
+
if (!stream) {
|
|
3730
|
+
throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
|
|
3731
|
+
}
|
|
3732
|
+
const headers = parseJsonWithLimit("network.http2Stream.pushStream headers", String(headersJson), jsonLimit);
|
|
3733
|
+
const options = parseJsonWithLimit("network.http2Stream.pushStream options", String(optionsJson), jsonLimit);
|
|
3734
|
+
return await new Promise((resolve, reject) => {
|
|
3735
|
+
try {
|
|
3736
|
+
stream.pushStream(headers, options, (error, pushStream, pushHeaders) => {
|
|
3737
|
+
if (error) {
|
|
3738
|
+
resolve(JSON.stringify({
|
|
3739
|
+
error: JSON.stringify({
|
|
3740
|
+
message: error.message,
|
|
3741
|
+
name: error.name,
|
|
3742
|
+
code: error.code,
|
|
3743
|
+
}),
|
|
3744
|
+
}));
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
if (!pushStream) {
|
|
3748
|
+
reject(new Error("HTTP/2 push stream callback returned no stream"));
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
const pushStreamId = nextHttp2StreamId++;
|
|
3752
|
+
http2Streams.set(pushStreamId, pushStream);
|
|
3753
|
+
pushStream.on("close", () => {
|
|
3754
|
+
http2Streams.delete(pushStreamId);
|
|
3755
|
+
});
|
|
3756
|
+
resolve(JSON.stringify({
|
|
3757
|
+
streamId: pushStreamId,
|
|
3758
|
+
headers: JSON.stringify(normalizeHttp2EventHeaders(pushHeaders ?? {})),
|
|
3759
|
+
}));
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
catch (error) {
|
|
3763
|
+
reject(error);
|
|
3764
|
+
}
|
|
3765
|
+
});
|
|
3766
|
+
};
|
|
3767
|
+
handlers[K.networkHttp2StreamWriteRaw] = (streamId, dataBase64) => {
|
|
3768
|
+
const stream = http2Streams.get(Number(streamId));
|
|
3769
|
+
if (!stream) {
|
|
3770
|
+
throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
|
|
3771
|
+
}
|
|
3772
|
+
return stream.write(Buffer.from(String(dataBase64), "base64"));
|
|
3773
|
+
};
|
|
3774
|
+
handlers[K.networkHttp2StreamEndRaw] = (streamId, dataBase64) => {
|
|
3775
|
+
const stream = http2Streams.get(Number(streamId));
|
|
3776
|
+
if (!stream) {
|
|
3777
|
+
throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
|
|
3778
|
+
}
|
|
3779
|
+
if (typeof dataBase64 === "string" && dataBase64.length > 0) {
|
|
3780
|
+
stream.end(Buffer.from(dataBase64, "base64"));
|
|
3781
|
+
return;
|
|
3782
|
+
}
|
|
3783
|
+
stream.end();
|
|
3784
|
+
};
|
|
3785
|
+
handlers[K.networkHttp2StreamPauseRaw] = (streamId) => {
|
|
3786
|
+
http2Streams.get(Number(streamId))?.pause();
|
|
3787
|
+
};
|
|
3788
|
+
handlers[K.networkHttp2StreamResumeRaw] = (streamId) => {
|
|
3789
|
+
http2Streams.get(Number(streamId))?.resume();
|
|
3790
|
+
};
|
|
3791
|
+
handlers[K.networkHttp2StreamRespondWithFileRaw] = (streamId, filePath, headersJson, optionsJson) => {
|
|
3792
|
+
const stream = http2Streams.get(Number(streamId));
|
|
3793
|
+
if (!stream) {
|
|
3794
|
+
throw new Error(`HTTP/2 stream ${String(streamId)} not found`);
|
|
3795
|
+
}
|
|
3796
|
+
const headers = parseJsonWithLimit("network.http2Stream.respondWithFile headers", String(headersJson), jsonLimit);
|
|
3797
|
+
const options = parseJsonWithLimit("network.http2Stream.respondWithFile options", String(optionsJson), jsonLimit);
|
|
3798
|
+
stream.respondWithFile(String(filePath), headers, options);
|
|
3799
|
+
};
|
|
3800
|
+
handlers[K.networkHttp2ServerRespondRaw] = (serverId, requestId, responseJson) => {
|
|
3801
|
+
resolveHttp2CompatResponse({
|
|
3802
|
+
serverId: Number(serverId),
|
|
3803
|
+
requestId: Number(requestId),
|
|
3804
|
+
responseJson: String(responseJson),
|
|
3805
|
+
});
|
|
3806
|
+
};
|
|
3807
|
+
handlers[K.upgradeSocketWriteRaw] = (socketId, dataBase64) => {
|
|
3808
|
+
const id = Number(socketId);
|
|
3809
|
+
const socket = kernelUpgradeSockets.get(id);
|
|
3810
|
+
if (socket) {
|
|
3811
|
+
socket.write(Buffer.from(String(dataBase64), "base64"));
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
adapter.upgradeSocketWrite?.(id, String(dataBase64));
|
|
3815
|
+
};
|
|
3816
|
+
handlers[K.upgradeSocketEndRaw] = (socketId) => {
|
|
3817
|
+
const id = Number(socketId);
|
|
3818
|
+
const socket = kernelUpgradeSockets.get(id);
|
|
3819
|
+
if (socket) {
|
|
3820
|
+
socket.end();
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
adapter.upgradeSocketEnd?.(id);
|
|
3824
|
+
};
|
|
3825
|
+
handlers[K.upgradeSocketDestroyRaw] = (socketId) => {
|
|
3826
|
+
const id = Number(socketId);
|
|
3827
|
+
const socket = kernelUpgradeSockets.get(id);
|
|
3828
|
+
if (socket) {
|
|
3829
|
+
kernelUpgradeSockets.delete(id);
|
|
3830
|
+
socket.destroy();
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
adapter.upgradeSocketDestroy?.(id);
|
|
3834
|
+
};
|
|
3835
|
+
// Register upgrade socket callbacks for httpRequest client-side upgrades
|
|
3836
|
+
adapter.setUpgradeSocketCallbacks?.({
|
|
3837
|
+
onData: (socketId, dataBase64) => {
|
|
3838
|
+
deps.sendStreamEvent("upgradeSocketData", Buffer.from(JSON.stringify({ socketId, dataBase64 })));
|
|
3839
|
+
},
|
|
3840
|
+
onEnd: (socketId) => {
|
|
3841
|
+
deps.sendStreamEvent("upgradeSocketEnd", Buffer.from(JSON.stringify({ socketId })));
|
|
3842
|
+
},
|
|
3843
|
+
});
|
|
3844
|
+
// Dispose: close all kernel HTTP servers
|
|
3845
|
+
const dispose = async () => {
|
|
3846
|
+
for (const serverId of Array.from(kernelHttpServers.keys())) {
|
|
3847
|
+
await closeKernelServer(serverId);
|
|
3848
|
+
}
|
|
3849
|
+
for (const serverId of Array.from(kernelHttp2Servers.keys())) {
|
|
3850
|
+
await closeKernelHttp2Server(serverId);
|
|
3851
|
+
}
|
|
3852
|
+
for (const session of http2Sessions.values()) {
|
|
3853
|
+
try {
|
|
3854
|
+
session.destroy();
|
|
3855
|
+
}
|
|
3856
|
+
catch {
|
|
3857
|
+
// Session already closed.
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
kernelHttp2ClientSessions.clear();
|
|
3861
|
+
http2Sessions.clear();
|
|
3862
|
+
http2Streams.clear();
|
|
3863
|
+
for (const socket of kernelUpgradeSockets.values()) {
|
|
3864
|
+
socket.destroy();
|
|
3865
|
+
}
|
|
3866
|
+
kernelUpgradeSockets.clear();
|
|
3867
|
+
};
|
|
3868
|
+
return { handlers, dispose };
|
|
3869
|
+
}
|
|
3870
|
+
/** Accept loop: dequeue connections from kernel listener and feed to http.Server. */
|
|
3871
|
+
async function startKernelHttpAcceptLoop(state, socketTable, pid) {
|
|
3872
|
+
try {
|
|
3873
|
+
while (state.acceptLoopActive) {
|
|
3874
|
+
const listenSocket = socketTable.get(state.listenSocketId);
|
|
3875
|
+
if (!listenSocket || listenSocket.state !== "listening")
|
|
3876
|
+
break;
|
|
3877
|
+
const acceptedId = socketTable.accept(state.listenSocketId);
|
|
3878
|
+
if (acceptedId !== null) {
|
|
3879
|
+
debugHttpBridge("accept backlog", state.listenSocketId, acceptedId);
|
|
3880
|
+
// Wrap kernel socket in Duplex and hand off to http.Server
|
|
3881
|
+
const duplex = createKernelSocketDuplex(acceptedId, socketTable, pid);
|
|
3882
|
+
state.httpServer.emit("connection", duplex);
|
|
3883
|
+
continue;
|
|
3884
|
+
}
|
|
3885
|
+
// Avoid a lost wake-up if a connection arrives between accept() and enqueue().
|
|
3886
|
+
const handle = listenSocket.acceptWaiters.enqueue();
|
|
3887
|
+
const acceptedAfterEnqueue = socketTable.accept(state.listenSocketId);
|
|
3888
|
+
if (acceptedAfterEnqueue !== null) {
|
|
3889
|
+
handle.wake();
|
|
3890
|
+
debugHttpBridge("accept after enqueue", state.listenSocketId, acceptedAfterEnqueue);
|
|
3891
|
+
const duplex = createKernelSocketDuplex(acceptedAfterEnqueue, socketTable, pid);
|
|
3892
|
+
state.httpServer.emit("connection", duplex);
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
// No pending connections — wait for accept waker
|
|
3896
|
+
await handle.wait();
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
catch {
|
|
3900
|
+
// Listener closed — expected
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
// Track request IDs directly, but also keep per-server FIFO queues so older
|
|
3904
|
+
// callbacks that only report serverId still resolve the correct pending waiters.
|
|
3905
|
+
const pendingHttpResponses = new Map();
|
|
3906
|
+
const pendingHttpResponsesByServer = new Map();
|
|
3907
|
+
let nextHttpRequestId = 1;
|
|
3908
|
+
const pendingHttp2CompatResponses = new Map();
|
|
3909
|
+
const pendingHttp2CompatResponsesByServer = new Map();
|
|
3910
|
+
let nextHttp2CompatRequestId = 1;
|
|
3911
|
+
function registerPendingHttpResponse(serverId, requestId, resolve) {
|
|
3912
|
+
pendingHttpResponses.set(requestId, { serverId, resolve });
|
|
3913
|
+
const queue = pendingHttpResponsesByServer.get(serverId);
|
|
3914
|
+
if (queue) {
|
|
3915
|
+
queue.push(requestId);
|
|
3916
|
+
}
|
|
3917
|
+
else {
|
|
3918
|
+
pendingHttpResponsesByServer.set(serverId, [requestId]);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
function removePendingHttpResponse(serverId, requestId) {
|
|
3922
|
+
const pending = pendingHttpResponses.get(requestId);
|
|
3923
|
+
if (!pending)
|
|
3924
|
+
return undefined;
|
|
3925
|
+
pendingHttpResponses.delete(requestId);
|
|
3926
|
+
const queue = pendingHttpResponsesByServer.get(serverId);
|
|
3927
|
+
if (queue) {
|
|
3928
|
+
const index = queue.indexOf(requestId);
|
|
3929
|
+
if (index !== -1)
|
|
3930
|
+
queue.splice(index, 1);
|
|
3931
|
+
if (queue.length === 0)
|
|
3932
|
+
pendingHttpResponsesByServer.delete(serverId);
|
|
3933
|
+
}
|
|
3934
|
+
return pending;
|
|
3935
|
+
}
|
|
3936
|
+
function takePendingHttpResponseByServer(serverId) {
|
|
3937
|
+
const queue = pendingHttpResponsesByServer.get(serverId);
|
|
3938
|
+
if (!queue || queue.length === 0)
|
|
3939
|
+
return undefined;
|
|
3940
|
+
const requestId = queue.shift();
|
|
3941
|
+
if (queue.length === 0)
|
|
3942
|
+
pendingHttpResponsesByServer.delete(serverId);
|
|
3943
|
+
const pending = pendingHttpResponses.get(requestId);
|
|
3944
|
+
if (pending) {
|
|
3945
|
+
pendingHttpResponses.delete(requestId);
|
|
3946
|
+
}
|
|
3947
|
+
return pending;
|
|
3948
|
+
}
|
|
3949
|
+
function registerPendingHttp2CompatResponse(serverId, requestId, resolve) {
|
|
3950
|
+
pendingHttp2CompatResponses.set(requestId, { serverId, resolve });
|
|
3951
|
+
const queue = pendingHttp2CompatResponsesByServer.get(serverId);
|
|
3952
|
+
if (queue) {
|
|
3953
|
+
queue.push(requestId);
|
|
3954
|
+
}
|
|
3955
|
+
else {
|
|
3956
|
+
pendingHttp2CompatResponsesByServer.set(serverId, [requestId]);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
function removePendingHttp2CompatResponse(serverId, requestId) {
|
|
3960
|
+
const pending = pendingHttp2CompatResponses.get(requestId);
|
|
3961
|
+
if (!pending)
|
|
3962
|
+
return undefined;
|
|
3963
|
+
pendingHttp2CompatResponses.delete(requestId);
|
|
3964
|
+
const queue = pendingHttp2CompatResponsesByServer.get(serverId);
|
|
3965
|
+
if (queue) {
|
|
3966
|
+
const index = queue.indexOf(requestId);
|
|
3967
|
+
if (index !== -1)
|
|
3968
|
+
queue.splice(index, 1);
|
|
3969
|
+
if (queue.length === 0)
|
|
3970
|
+
pendingHttp2CompatResponsesByServer.delete(serverId);
|
|
3971
|
+
}
|
|
3972
|
+
return pending;
|
|
3973
|
+
}
|
|
3974
|
+
function takePendingHttp2CompatResponseByServer(serverId) {
|
|
3975
|
+
const queue = pendingHttp2CompatResponsesByServer.get(serverId);
|
|
3976
|
+
if (!queue || queue.length === 0)
|
|
3977
|
+
return undefined;
|
|
3978
|
+
const requestId = queue.shift();
|
|
3979
|
+
if (queue.length === 0)
|
|
3980
|
+
pendingHttp2CompatResponsesByServer.delete(serverId);
|
|
3981
|
+
const pending = pendingHttp2CompatResponses.get(requestId);
|
|
3982
|
+
if (pending) {
|
|
3983
|
+
pendingHttp2CompatResponses.delete(requestId);
|
|
3984
|
+
}
|
|
3985
|
+
return pending;
|
|
3986
|
+
}
|
|
3987
|
+
/** Resolve a pending HTTP server response (called from stream callback handler). */
|
|
3988
|
+
export function resolveHttpServerResponse(options) {
|
|
3989
|
+
const pending = options.requestId !== undefined
|
|
3990
|
+
? removePendingHttpResponse(options.serverId ?? pendingHttpResponses.get(options.requestId)?.serverId ?? -1, options.requestId)
|
|
3991
|
+
: options.serverId !== undefined
|
|
3992
|
+
? takePendingHttpResponseByServer(options.serverId)
|
|
3993
|
+
: undefined;
|
|
3994
|
+
pending?.resolve(options.responseJson);
|
|
3995
|
+
}
|
|
3996
|
+
export function resolveHttp2CompatResponse(options) {
|
|
3997
|
+
const pending = options.requestId !== undefined
|
|
3998
|
+
? removePendingHttp2CompatResponse(options.serverId ?? pendingHttp2CompatResponses.get(options.requestId)?.serverId ?? -1, options.requestId)
|
|
3999
|
+
: options.serverId !== undefined
|
|
4000
|
+
? takePendingHttp2CompatResponseByServer(options.serverId)
|
|
4001
|
+
: undefined;
|
|
4002
|
+
pending?.resolve(options.responseJson);
|
|
4003
|
+
}
|
|
4004
|
+
/** Build PTY bridge handlers. */
|
|
4005
|
+
export function buildPtyBridgeHandlers(deps) {
|
|
4006
|
+
const handlers = {};
|
|
4007
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
4008
|
+
if (deps.stdinIsTTY && deps.onPtySetRawMode) {
|
|
4009
|
+
handlers[K.ptySetRawMode] = (mode) => {
|
|
4010
|
+
deps.onPtySetRawMode(Boolean(mode));
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
return handlers;
|
|
4014
|
+
}
|
|
4015
|
+
const O_ACCMODE = 3;
|
|
4016
|
+
function canRead(flags) {
|
|
4017
|
+
const access = flags & O_ACCMODE;
|
|
4018
|
+
return access === O_RDONLY || access === O_RDWR;
|
|
4019
|
+
}
|
|
4020
|
+
function canWrite(flags) {
|
|
4021
|
+
const access = flags & O_ACCMODE;
|
|
4022
|
+
return access === O_WRONLY || access === O_RDWR;
|
|
4023
|
+
}
|
|
4024
|
+
const PROC_SYS_KERNEL_HOSTNAME_PATH = "/proc/sys/kernel/hostname";
|
|
4025
|
+
function getStandaloneProcFileContent(path) {
|
|
4026
|
+
if (path === PROC_SYS_KERNEL_HOSTNAME_PATH) {
|
|
4027
|
+
return Buffer.from("sandbox\n", "utf8");
|
|
4028
|
+
}
|
|
4029
|
+
return null;
|
|
4030
|
+
}
|
|
4031
|
+
function getStandaloneProcFileStat(path) {
|
|
4032
|
+
const content = getStandaloneProcFileContent(path);
|
|
4033
|
+
if (!content)
|
|
4034
|
+
return null;
|
|
4035
|
+
const now = Date.now();
|
|
4036
|
+
return {
|
|
4037
|
+
mode: 0o100444,
|
|
4038
|
+
size: content.length,
|
|
4039
|
+
isDirectory: false,
|
|
4040
|
+
isSymbolicLink: false,
|
|
4041
|
+
atimeMs: now,
|
|
4042
|
+
mtimeMs: now,
|
|
4043
|
+
ctimeMs: now,
|
|
4044
|
+
birthtimeMs: now,
|
|
4045
|
+
ino: 0xfffe0001,
|
|
4046
|
+
nlink: 1,
|
|
4047
|
+
uid: 0,
|
|
4048
|
+
gid: 0,
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
async function readStandaloneProcAwareFile(vfs, path) {
|
|
4052
|
+
return getStandaloneProcFileContent(path) ?? vfs.readFile(path);
|
|
4053
|
+
}
|
|
4054
|
+
async function readStandaloneProcAwareTextFile(vfs, path) {
|
|
4055
|
+
const content = getStandaloneProcFileContent(path);
|
|
4056
|
+
if (content)
|
|
4057
|
+
return new TextDecoder().decode(content);
|
|
4058
|
+
return vfs.readTextFile(path);
|
|
4059
|
+
}
|
|
4060
|
+
async function standaloneProcAwareExists(vfs, path) {
|
|
4061
|
+
if (getStandaloneProcFileContent(path))
|
|
4062
|
+
return true;
|
|
4063
|
+
return vfs.exists(path);
|
|
4064
|
+
}
|
|
4065
|
+
async function standaloneProcAwareStat(vfs, path) {
|
|
4066
|
+
return getStandaloneProcFileStat(path) ?? vfs.stat(path);
|
|
4067
|
+
}
|
|
4068
|
+
async function standaloneProcAwarePread(vfs, path, offset, length) {
|
|
4069
|
+
const content = getStandaloneProcFileContent(path);
|
|
4070
|
+
if (content) {
|
|
4071
|
+
if (offset >= content.length)
|
|
4072
|
+
return new Uint8Array(0);
|
|
4073
|
+
return content.slice(offset, offset + length);
|
|
4074
|
+
}
|
|
4075
|
+
return vfs.pread(path, offset, length);
|
|
4076
|
+
}
|
|
4077
|
+
/**
|
|
4078
|
+
* Build kernel FD table bridge handlers.
|
|
4079
|
+
*
|
|
4080
|
+
* Creates a ProcessFDTable per execution and routes all FD operations
|
|
4081
|
+
* (open, close, read, write, fstat, ftruncate, fsync) through it.
|
|
4082
|
+
* The FD table tracks file descriptors, cursor positions, and flags.
|
|
4083
|
+
* Actual file I/O is delegated to the VirtualFileSystem.
|
|
4084
|
+
*/
|
|
4085
|
+
export function buildKernelFdBridgeHandlers(deps) {
|
|
4086
|
+
const handlers = {};
|
|
4087
|
+
const K = HOST_BRIDGE_GLOBAL_KEYS;
|
|
4088
|
+
const vfs = deps.filesystem;
|
|
4089
|
+
// Create a per-execution FD table via the kernel FDTableManager
|
|
4090
|
+
const fdManager = new FDTableManager();
|
|
4091
|
+
const pid = 1;
|
|
4092
|
+
const fdTable = fdManager.create(pid);
|
|
4093
|
+
// fdOpen(path, flags, mode?) → fd number
|
|
4094
|
+
handlers[K.fdOpen] = async (path, flags, mode) => {
|
|
4095
|
+
checkBridgeBudget(deps);
|
|
4096
|
+
const pathStr = String(path);
|
|
4097
|
+
const numFlags = Number(flags);
|
|
4098
|
+
const numMode = mode !== undefined && mode !== null ? Number(mode) : undefined;
|
|
4099
|
+
const exists = await standaloneProcAwareExists(vfs, pathStr);
|
|
4100
|
+
// O_CREAT: create if doesn't exist
|
|
4101
|
+
if ((numFlags & O_CREAT) && !exists) {
|
|
4102
|
+
await vfs.writeFile(pathStr, "");
|
|
4103
|
+
}
|
|
4104
|
+
else if (!exists && !(numFlags & O_CREAT)) {
|
|
4105
|
+
throw new Error(`ENOENT: no such file or directory, open '${pathStr}'`);
|
|
4106
|
+
}
|
|
4107
|
+
// O_TRUNC: truncate existing file
|
|
4108
|
+
if ((numFlags & O_TRUNC) && exists) {
|
|
4109
|
+
await vfs.writeFile(pathStr, "");
|
|
4110
|
+
}
|
|
4111
|
+
const fd = fdTable.open(pathStr, numFlags, FILETYPE_REGULAR_FILE);
|
|
4112
|
+
// Store creation mode for umask application
|
|
4113
|
+
if (numMode !== undefined && (numFlags & O_CREAT)) {
|
|
4114
|
+
const entry = fdTable.get(fd);
|
|
4115
|
+
if (entry)
|
|
4116
|
+
entry.description.creationMode = numMode;
|
|
4117
|
+
}
|
|
4118
|
+
return fd;
|
|
4119
|
+
};
|
|
4120
|
+
// fdClose(fd)
|
|
4121
|
+
handlers[K.fdClose] = (fd) => {
|
|
4122
|
+
const fdNum = Number(fd);
|
|
4123
|
+
const ok = fdTable.close(fdNum);
|
|
4124
|
+
if (!ok)
|
|
4125
|
+
throw new Error("EBADF: bad file descriptor, close");
|
|
4126
|
+
};
|
|
4127
|
+
// fdRead(fd, length, position?) → base64 data
|
|
4128
|
+
handlers[K.fdRead] = async (fd, length, position) => {
|
|
4129
|
+
checkBridgeBudget(deps);
|
|
4130
|
+
const fdNum = Number(fd);
|
|
4131
|
+
const len = Number(length);
|
|
4132
|
+
const entry = fdTable.get(fdNum);
|
|
4133
|
+
if (!entry)
|
|
4134
|
+
throw new Error("EBADF: bad file descriptor, read");
|
|
4135
|
+
if (!canRead(entry.description.flags))
|
|
4136
|
+
throw new Error("EBADF: bad file descriptor, read");
|
|
4137
|
+
const pos = (position !== null && position !== undefined)
|
|
4138
|
+
? Number(position)
|
|
4139
|
+
: Number(entry.description.cursor);
|
|
4140
|
+
const data = await standaloneProcAwarePread(vfs, entry.description.path, pos, len);
|
|
4141
|
+
// Update cursor only when no explicit position
|
|
4142
|
+
if (position === null || position === undefined) {
|
|
4143
|
+
entry.description.cursor += BigInt(data.length);
|
|
4144
|
+
}
|
|
4145
|
+
return Buffer.from(data).toString("base64");
|
|
4146
|
+
};
|
|
4147
|
+
// fdWrite(fd, base64data, position?) → bytes written
|
|
4148
|
+
handlers[K.fdWrite] = async (fd, base64data, position) => {
|
|
4149
|
+
checkBridgeBudget(deps);
|
|
4150
|
+
const fdNum = Number(fd);
|
|
4151
|
+
const entry = fdTable.get(fdNum);
|
|
4152
|
+
if (!entry)
|
|
4153
|
+
throw new Error("EBADF: bad file descriptor, write");
|
|
4154
|
+
if (!canWrite(entry.description.flags))
|
|
4155
|
+
throw new Error("EBADF: bad file descriptor, write");
|
|
4156
|
+
const data = Buffer.from(String(base64data), "base64");
|
|
4157
|
+
// Read existing content
|
|
4158
|
+
let content;
|
|
4159
|
+
try {
|
|
4160
|
+
content = await readStandaloneProcAwareFile(vfs, entry.description.path);
|
|
4161
|
+
}
|
|
4162
|
+
catch {
|
|
4163
|
+
content = new Uint8Array(0);
|
|
4164
|
+
}
|
|
4165
|
+
// Determine write position
|
|
4166
|
+
let writePos;
|
|
4167
|
+
if (entry.description.flags & O_APPEND) {
|
|
4168
|
+
writePos = content.length;
|
|
4169
|
+
}
|
|
4170
|
+
else if (position !== null && position !== undefined) {
|
|
4171
|
+
writePos = Number(position);
|
|
4172
|
+
}
|
|
4173
|
+
else {
|
|
4174
|
+
writePos = Number(entry.description.cursor);
|
|
4175
|
+
}
|
|
4176
|
+
// Splice data into content
|
|
4177
|
+
const endPos = writePos + data.length;
|
|
4178
|
+
const newContent = new Uint8Array(Math.max(content.length, endPos));
|
|
4179
|
+
newContent.set(content);
|
|
4180
|
+
newContent.set(data, writePos);
|
|
4181
|
+
await vfs.writeFile(entry.description.path, newContent);
|
|
4182
|
+
// Update cursor only when no explicit position
|
|
4183
|
+
if (position === null || position === undefined) {
|
|
4184
|
+
entry.description.cursor = BigInt(endPos);
|
|
4185
|
+
}
|
|
4186
|
+
return data.length;
|
|
4187
|
+
};
|
|
4188
|
+
// fdFstat(fd) → JSON stat string
|
|
4189
|
+
handlers[K.fdFstat] = async (fd) => {
|
|
4190
|
+
checkBridgeBudget(deps);
|
|
4191
|
+
const fdNum = Number(fd);
|
|
4192
|
+
const entry = fdTable.get(fdNum);
|
|
4193
|
+
if (!entry)
|
|
4194
|
+
throw new Error("EBADF: bad file descriptor, fstat");
|
|
4195
|
+
const stat = await standaloneProcAwareStat(vfs, entry.description.path);
|
|
4196
|
+
return JSON.stringify({
|
|
4197
|
+
dev: 0,
|
|
4198
|
+
ino: stat.ino ?? 0,
|
|
4199
|
+
mode: stat.mode,
|
|
4200
|
+
nlink: stat.nlink ?? 1,
|
|
4201
|
+
uid: stat.uid ?? 0,
|
|
4202
|
+
gid: stat.gid ?? 0,
|
|
4203
|
+
rdev: 0,
|
|
4204
|
+
size: stat.size,
|
|
4205
|
+
blksize: 4096,
|
|
4206
|
+
blocks: Math.ceil(stat.size / 512),
|
|
4207
|
+
atimeMs: stat.atimeMs ?? Date.now(),
|
|
4208
|
+
mtimeMs: stat.mtimeMs ?? Date.now(),
|
|
4209
|
+
ctimeMs: stat.ctimeMs ?? Date.now(),
|
|
4210
|
+
birthtimeMs: stat.birthtimeMs ?? Date.now(),
|
|
4211
|
+
});
|
|
4212
|
+
};
|
|
4213
|
+
// fdFtruncate(fd, len?)
|
|
4214
|
+
handlers[K.fdFtruncate] = async (fd, len) => {
|
|
4215
|
+
checkBridgeBudget(deps);
|
|
4216
|
+
const fdNum = Number(fd);
|
|
4217
|
+
const entry = fdTable.get(fdNum);
|
|
4218
|
+
if (!entry)
|
|
4219
|
+
throw new Error("EBADF: bad file descriptor, ftruncate");
|
|
4220
|
+
const newLen = (len !== undefined && len !== null) ? Number(len) : 0;
|
|
4221
|
+
let content;
|
|
4222
|
+
try {
|
|
4223
|
+
content = await readStandaloneProcAwareFile(vfs, entry.description.path);
|
|
4224
|
+
}
|
|
4225
|
+
catch {
|
|
4226
|
+
content = new Uint8Array(0);
|
|
4227
|
+
}
|
|
4228
|
+
if (content.length > newLen) {
|
|
4229
|
+
await vfs.writeFile(entry.description.path, content.slice(0, newLen));
|
|
4230
|
+
}
|
|
4231
|
+
else if (content.length < newLen) {
|
|
4232
|
+
const padded = new Uint8Array(newLen);
|
|
4233
|
+
padded.set(content);
|
|
4234
|
+
await vfs.writeFile(entry.description.path, padded);
|
|
4235
|
+
}
|
|
4236
|
+
};
|
|
4237
|
+
// fdFsync(fd) — no-op for in-memory VFS, validates FD exists
|
|
4238
|
+
handlers[K.fdFsync] = (fd) => {
|
|
4239
|
+
const fdNum = Number(fd);
|
|
4240
|
+
const entry = fdTable.get(fdNum);
|
|
4241
|
+
if (!entry)
|
|
4242
|
+
throw new Error("EBADF: bad file descriptor, fsync");
|
|
4243
|
+
};
|
|
4244
|
+
// fdGetPath(fd) → path string or null
|
|
4245
|
+
handlers[K.fdGetPath] = (fd) => {
|
|
4246
|
+
const fdNum = Number(fd);
|
|
4247
|
+
const entry = fdTable.get(fdNum);
|
|
4248
|
+
return entry ? entry.description.path : null;
|
|
4249
|
+
};
|
|
4250
|
+
return {
|
|
4251
|
+
handlers,
|
|
4252
|
+
dispose: () => {
|
|
4253
|
+
fdTable.closeAll();
|
|
4254
|
+
},
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
export function createProcessConfigForExecution(processConfig, timingMitigation, frozenTimeMs) {
|
|
4258
|
+
return {
|
|
4259
|
+
...processConfig,
|
|
4260
|
+
timingMitigation: timingMitigation,
|
|
4261
|
+
frozenTimeMs: timingMitigation === "freeze" ? frozenTimeMs : undefined,
|
|
4262
|
+
};
|
|
4263
|
+
}
|