@floegence/flowersec-core 2.5.2 → 2.5.4
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/dist/framing/jsonframe.d.ts +1 -1
- package/dist/framing/jsonframe.js +3 -1
- package/dist/node/acceptor.js +33 -11
- package/dist/proxy/runtime.js +39 -6
- package/dist/proxy/serviceWorker.d.ts +2 -0
- package/dist/proxy/serviceWorker.js +63 -7
- package/dist/proxy/windowBridge.d.ts +2 -0
- package/dist/proxy/windowBridge.js +143 -42
- package/dist/proxy/wsPatch.d.ts +1 -0
- package/dist/proxy/wsPatch.js +39 -6
- package/dist/rpc/client.js +9 -7
- package/dist/rpc/server.js +44 -20
- package/dist/rpc/validate.d.ts +1 -0
- package/dist/rpc/validate.js +18 -6
- package/dist/transport/webTransportAdapter.js +17 -2
- package/dist/v2/publicSession.js +4 -0
- package/dist/v2/session.d.ts +1 -1
- package/dist/v2/session.js +38 -11
- package/dist/yamux/stream.d.ts +3 -0
- package/dist/yamux/stream.js +17 -4
- package/package.json +3 -3
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
|
@@ -9,6 +9,6 @@ type ReadExactlyFn = (n: number) => Promise<Uint8Array>;
|
|
|
9
9
|
type ReadExactlyLike = Readonly<{
|
|
10
10
|
readExactly: (n: number) => Promise<Uint8Array>;
|
|
11
11
|
}>;
|
|
12
|
-
export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown): Promise<void>;
|
|
12
|
+
export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown, maxBytes?: number): Promise<void>;
|
|
13
13
|
export declare function readJsonFrame(readExactly: ReadExactlyFn | ReadExactlyLike, maxBytes: number): Promise<unknown>;
|
|
14
14
|
export {};
|
|
@@ -13,8 +13,10 @@ function normalizeReadExactly(readExactly) {
|
|
|
13
13
|
return typeof readExactly === "function" ? readExactly : (n) => readExactly.readExactly(n);
|
|
14
14
|
}
|
|
15
15
|
// writeJsonFrame encodes a JSON payload with a 4-byte length prefix.
|
|
16
|
-
export async function writeJsonFrame(write, v) {
|
|
16
|
+
export async function writeJsonFrame(write, v, maxBytes = 0) {
|
|
17
17
|
const json = te.encode(JSON.stringify(v));
|
|
18
|
+
if (maxBytes > 0 && json.length > maxBytes)
|
|
19
|
+
throw new JsonFramingError("frame too large");
|
|
18
20
|
const hdr = u32be(json.length);
|
|
19
21
|
const out = new Uint8Array(4 + json.length);
|
|
20
22
|
out.set(hdr, 0);
|
package/dist/node/acceptor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RpcRouter } from "../rpc/server.js";
|
|
2
|
-
import { assertRpcError } from "../rpc/validate.js";
|
|
2
|
+
import { assertRpcError, assertRpcTypeId } from "../rpc/validate.js";
|
|
3
3
|
import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
|
|
4
4
|
import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
|
|
5
5
|
import { startNodeWebSocketServer, } from "./webSocketServer.js";
|
|
@@ -85,10 +85,13 @@ function registerNotification(state, typeId, handler) {
|
|
|
85
85
|
state.notifications.set(typeId, handler);
|
|
86
86
|
}
|
|
87
87
|
function validateRPCRegistration(typeId, handler) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
try {
|
|
89
|
+
assertRpcTypeId(typeId);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
93
|
+
}
|
|
94
|
+
if (typeof handler !== "function") {
|
|
92
95
|
throw new HandlerRegistrationError("invalid_handler");
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -443,12 +446,31 @@ export async function createAcceptor(options) {
|
|
|
443
446
|
const abort = () => controller.abort(operation.signal?.reason);
|
|
444
447
|
operation.signal?.addEventListener("abort", abort, { once: true });
|
|
445
448
|
try {
|
|
446
|
-
return await Promise
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
449
|
+
return await new Promise((resolve, reject) => {
|
|
450
|
+
let settled = false;
|
|
451
|
+
let remaining = listeners.length;
|
|
452
|
+
const errors = [];
|
|
453
|
+
listeners.forEach((listener, index) => {
|
|
454
|
+
const selected = listeners[(index + cursor) % listeners.length];
|
|
455
|
+
void selected.accept({ signal: controller.signal }).then((carrier) => {
|
|
456
|
+
if (!settled) {
|
|
457
|
+
settled = true;
|
|
458
|
+
cursor = (cursor + 1) % listeners.length;
|
|
459
|
+
controller.abort();
|
|
460
|
+
resolve(carrier);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
carrier.abort({ code: 1000, reason: "accept race lost" });
|
|
464
|
+
}
|
|
465
|
+
}, (error) => {
|
|
466
|
+
errors.push(error);
|
|
467
|
+
remaining -= 1;
|
|
468
|
+
if (!settled && remaining === 0) {
|
|
469
|
+
settled = true;
|
|
470
|
+
reject(new AggregateError(errors, "all listeners failed to accept"));
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
});
|
|
452
474
|
});
|
|
453
475
|
}
|
|
454
476
|
finally {
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -45,11 +45,34 @@ function normalizeTimeout(input) {
|
|
|
45
45
|
}
|
|
46
46
|
return input;
|
|
47
47
|
}
|
|
48
|
+
class InvalidProxyPathError extends TypeError {
|
|
49
|
+
}
|
|
48
50
|
function normalizePath(input) {
|
|
49
|
-
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://")) {
|
|
50
|
-
throw new
|
|
51
|
+
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://") || input.includes("#")) {
|
|
52
|
+
throw new InvalidProxyPathError("proxy path must be an origin-relative path");
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(input, "https://flowersec.invalid/");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new InvalidProxyPathError("proxy path must be an origin-relative path");
|
|
60
|
+
}
|
|
61
|
+
const pathname = normalizePercentEscapes(parsed.pathname, true).replace(/\/{2,}/gu, "/");
|
|
62
|
+
const search = normalizePercentEscapes(parsed.search, false);
|
|
63
|
+
return pathname + search;
|
|
64
|
+
}
|
|
65
|
+
function normalizePercentEscapes(input, rejectEncodedSeparators) {
|
|
66
|
+
if (/%(?![0-9a-f]{2})/iu.test(input))
|
|
67
|
+
throw new InvalidProxyPathError("proxy path contains invalid percent encoding");
|
|
68
|
+
return input.replace(/%([0-9a-f]{2})/giu, (_match, hex) => {
|
|
69
|
+
const value = Number.parseInt(hex, 16);
|
|
70
|
+
if (rejectEncodedSeparators && (value === 0x2f || value === 0x5c)) {
|
|
71
|
+
throw new InvalidProxyPathError("proxy path contains an encoded separator");
|
|
72
|
+
}
|
|
73
|
+
const character = String.fromCharCode(value);
|
|
74
|
+
return /[A-Za-z0-9\-._~]/u.test(character) ? character : `%${hex.toUpperCase()}`;
|
|
75
|
+
});
|
|
53
76
|
}
|
|
54
77
|
function pathName(path) {
|
|
55
78
|
const query = path.indexOf("?");
|
|
@@ -176,11 +199,19 @@ class StreamAdmission {
|
|
|
176
199
|
return Promise.reject(new SessionError("resource_exhausted"));
|
|
177
200
|
}
|
|
178
201
|
return new Promise((resolve, reject) => {
|
|
202
|
+
let cleaned = false;
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
if (cleaned)
|
|
205
|
+
return;
|
|
206
|
+
cleaned = true;
|
|
207
|
+
signal?.removeEventListener("abort", onAbort);
|
|
208
|
+
};
|
|
179
209
|
const entry = {
|
|
180
210
|
bytes,
|
|
181
211
|
...(signal === undefined ? {} : { signal }),
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
cleanup,
|
|
213
|
+
resolve: (release) => { cleanup(); resolve(release); },
|
|
214
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
184
215
|
};
|
|
185
216
|
const onAbort = () => {
|
|
186
217
|
const index = this.queue.indexOf(entry);
|
|
@@ -188,7 +219,7 @@ class StreamAdmission {
|
|
|
188
219
|
return;
|
|
189
220
|
this.queue.splice(index, 1);
|
|
190
221
|
this.queuedBytes -= bytes;
|
|
191
|
-
reject(new SessionError("canceled"));
|
|
222
|
+
entry.reject(new SessionError("canceled"));
|
|
192
223
|
};
|
|
193
224
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
225
|
this.queue.push(entry);
|
|
@@ -251,6 +282,8 @@ async function* readChunks(reader, maxChunkBytes, maxBodyBytes) {
|
|
|
251
282
|
function publicFailure(error) {
|
|
252
283
|
if (error instanceof ProxyPolicyError)
|
|
253
284
|
return { status: 403, code: "policy_denied", message: "proxy request denied" };
|
|
285
|
+
if (error instanceof InvalidProxyPathError)
|
|
286
|
+
return { status: 400, code: "invalid_request", message: "invalid proxy request" };
|
|
254
287
|
if (error instanceof SessionError && (error.code === "resource_exhausted" || error.code === "closed" || error.code === "going_away")) {
|
|
255
288
|
return { status: 503, code: error.code, message: "proxy service unavailable" };
|
|
256
289
|
}
|
|
@@ -15,6 +15,8 @@ export type ProxyServiceWorkerScriptOptions = Readonly<{
|
|
|
15
15
|
sameOriginOnly?: boolean;
|
|
16
16
|
maxRequestBodyBytes?: number;
|
|
17
17
|
maxInjectHTMLBytes?: number;
|
|
18
|
+
responseMetadataTimeoutMs?: number;
|
|
19
|
+
responseBodyInactivityTimeoutMs?: number;
|
|
18
20
|
passthrough?: ProxyServiceWorkerPassthroughOptions;
|
|
19
21
|
proxyPathPrefix?: string;
|
|
20
22
|
stripProxyPathPrefix?: boolean;
|
|
@@ -17,6 +17,12 @@ function bounded(name, value, fallback, maximum) {
|
|
|
17
17
|
throw new TypeError(`${name} is invalid`);
|
|
18
18
|
return result;
|
|
19
19
|
}
|
|
20
|
+
function optionalBounded(name, value, maximum) {
|
|
21
|
+
const result = value ?? 0;
|
|
22
|
+
if (!Number.isSafeInteger(result) || result < 0 || result > maximum)
|
|
23
|
+
throw new TypeError(`${name} is invalid`);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
20
26
|
function token(name, value, fallback = "") {
|
|
21
27
|
const result = value ?? fallback;
|
|
22
28
|
if (result !== result.trim() || /[\u0000-\u0020\u007f]/u.test(result) || result.length > 512)
|
|
@@ -42,6 +48,8 @@ function normalizeOptions(options) {
|
|
|
42
48
|
sameOriginOnly: options.sameOriginOnly ?? true,
|
|
43
49
|
maxRequestBodyBytes: bounded("maxRequestBodyBytes", options.maxRequestBodyBytes, 64 * 1024 * 1024, 256 * 1024 * 1024),
|
|
44
50
|
maxInjectHTMLBytes: bounded("maxInjectHTMLBytes", options.maxInjectHTMLBytes, 8 * 1024 * 1024, 32 * 1024 * 1024),
|
|
51
|
+
responseMetadataTimeoutMs: bounded("responseMetadataTimeoutMs", options.responseMetadataTimeoutMs, 10_000, 300_000),
|
|
52
|
+
responseBodyInactivityTimeoutMs: optionalBounded("responseBodyInactivityTimeoutMs", options.responseBodyInactivityTimeoutMs, 300_000),
|
|
45
53
|
passthroughPaths: strings("passthrough.paths", options.passthrough?.paths),
|
|
46
54
|
passthroughPrefixes: strings("passthrough.prefixes", options.passthrough?.prefixes),
|
|
47
55
|
proxyPathPrefix,
|
|
@@ -127,6 +135,8 @@ function serviceWorkerMain(config) {
|
|
|
127
135
|
return new Response("proxy runtime unavailable", { status: 503 });
|
|
128
136
|
}
|
|
129
137
|
let body;
|
|
138
|
+
if (request.signal.aborted)
|
|
139
|
+
return new Response("proxy request canceled", { status: 499 });
|
|
130
140
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
131
141
|
const requestBody = await request.clone().arrayBuffer();
|
|
132
142
|
if (requestBody.byteLength > config.maxRequestBodyBytes)
|
|
@@ -138,16 +148,54 @@ function serviceWorkerMain(config) {
|
|
|
138
148
|
let metadata = null;
|
|
139
149
|
let controller = null;
|
|
140
150
|
let finished = false;
|
|
141
|
-
|
|
151
|
+
let remoteAbortSent = false;
|
|
152
|
+
let bodyInactivityTimer;
|
|
153
|
+
const abortRemote = () => {
|
|
154
|
+
if (remoteAbortSent)
|
|
155
|
+
return;
|
|
156
|
+
remoteAbortSent = true;
|
|
157
|
+
try {
|
|
158
|
+
channel.port1.postMessage({ type: "flowersec-proxy:abort" });
|
|
159
|
+
}
|
|
160
|
+
catch { /* Port already failed. */ }
|
|
161
|
+
};
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
clearTimeout(metadataTimer);
|
|
164
|
+
clearTimeout(bodyInactivityTimer);
|
|
165
|
+
clearInterval(runtimeWatchdog);
|
|
166
|
+
request.signal.removeEventListener("abort", requestAborted);
|
|
167
|
+
channel.port1.close();
|
|
168
|
+
};
|
|
169
|
+
const finishError = (status, message, cancelRemote = false) => {
|
|
142
170
|
if (finished)
|
|
143
171
|
return;
|
|
144
172
|
finished = true;
|
|
173
|
+
if (cancelRemote)
|
|
174
|
+
abortRemote();
|
|
145
175
|
if (controller !== null)
|
|
146
176
|
controller.error(new Error(message));
|
|
147
177
|
else
|
|
148
178
|
resolve(new Response(message, { status }));
|
|
149
|
-
|
|
179
|
+
cleanup();
|
|
180
|
+
};
|
|
181
|
+
const requestAborted = () => finishError(499, "proxy request canceled", true);
|
|
182
|
+
const armBodyInactivityTimer = () => {
|
|
183
|
+
clearTimeout(bodyInactivityTimer);
|
|
184
|
+
if (config.responseBodyInactivityTimeoutMs === 0)
|
|
185
|
+
return;
|
|
186
|
+
bodyInactivityTimer = setTimeout(() => finishError(504, "proxy response body timed out", true), config.responseBodyInactivityTimeoutMs);
|
|
150
187
|
};
|
|
188
|
+
const metadataTimer = setTimeout(() => finishError(504, "proxy response timed out", true), config.responseMetadataTimeoutMs);
|
|
189
|
+
const runtimeWatchdog = setInterval(() => {
|
|
190
|
+
void worker.clients.get(target.id).then((current) => {
|
|
191
|
+
if (current !== null || finished)
|
|
192
|
+
return;
|
|
193
|
+
if (config.windowTarget === "registered_runtime")
|
|
194
|
+
runtimeClientId = "";
|
|
195
|
+
finishError(503, "proxy runtime unavailable", true);
|
|
196
|
+
}).catch(() => finishError(503, "proxy runtime unavailable", true));
|
|
197
|
+
}, 250);
|
|
198
|
+
request.signal.addEventListener("abort", requestAborted, { once: true });
|
|
151
199
|
channel.port1.onmessage = (message) => {
|
|
152
200
|
const value = message.data;
|
|
153
201
|
if (value === null || typeof value !== "object" || finished)
|
|
@@ -162,13 +210,21 @@ function serviceWorkerMain(config) {
|
|
|
162
210
|
headers.append(entry.name, entry.value);
|
|
163
211
|
}
|
|
164
212
|
metadata = { status: value.status, headers };
|
|
213
|
+
clearTimeout(metadataTimer);
|
|
214
|
+
armBodyInactivityTimer();
|
|
165
215
|
const stream = new ReadableStream({
|
|
166
216
|
start(valueController) {
|
|
167
217
|
controller = valueController;
|
|
168
218
|
channel.port1.postMessage({ type: "flowersec-proxy:response_credit" });
|
|
169
219
|
},
|
|
170
220
|
pull() { channel.port1.postMessage({ type: "flowersec-proxy:response_credit" }); },
|
|
171
|
-
cancel() {
|
|
221
|
+
cancel() {
|
|
222
|
+
if (finished)
|
|
223
|
+
return;
|
|
224
|
+
finished = true;
|
|
225
|
+
abortRemote();
|
|
226
|
+
cleanup();
|
|
227
|
+
},
|
|
172
228
|
});
|
|
173
229
|
resolve(new Response(stream, { status: metadata.status, headers: metadata.headers }));
|
|
174
230
|
return;
|
|
@@ -177,19 +233,20 @@ function serviceWorkerMain(config) {
|
|
|
177
233
|
if (controller === null || !(value.data instanceof ArrayBuffer))
|
|
178
234
|
return finishError(502, "invalid proxy response");
|
|
179
235
|
controller.enqueue(new Uint8Array(value.data));
|
|
236
|
+
armBodyInactivityTimer();
|
|
180
237
|
return;
|
|
181
238
|
}
|
|
182
239
|
if (value.type === "flowersec-proxy:response_end") {
|
|
183
240
|
finished = true;
|
|
184
241
|
controller?.close();
|
|
185
|
-
|
|
242
|
+
cleanup();
|
|
186
243
|
return;
|
|
187
244
|
}
|
|
188
245
|
if (value.type === "flowersec-proxy:response_error") {
|
|
189
246
|
finishError(Number.isInteger(value.status) ? value.status : 502, typeof value.message === "string" ? value.message : "proxy request failed");
|
|
190
247
|
}
|
|
191
248
|
};
|
|
192
|
-
channel.port1.onmessageerror = () => finishError(502, "proxy request failed");
|
|
249
|
+
channel.port1.onmessageerror = () => finishError(502, "proxy request failed", true);
|
|
193
250
|
const headers = Array.from(request.headers.entries()).map(([name, value]) => ({ name, value }));
|
|
194
251
|
try {
|
|
195
252
|
target.postMessage({
|
|
@@ -207,8 +264,7 @@ function serviceWorkerMain(config) {
|
|
|
207
264
|
catch {
|
|
208
265
|
if (config.windowTarget === "registered_runtime")
|
|
209
266
|
runtimeClientId = "";
|
|
210
|
-
|
|
211
|
-
resolve(new Response("proxy runtime unavailable", { status: 503 }));
|
|
267
|
+
finishError(503, "proxy runtime unavailable");
|
|
212
268
|
}
|
|
213
269
|
});
|
|
214
270
|
const injection = config.injectHTML;
|
|
@@ -12,6 +12,7 @@ export declare class MessagePortByteStream implements ByteStream {
|
|
|
12
12
|
private buffered;
|
|
13
13
|
private readBuffered;
|
|
14
14
|
private ended;
|
|
15
|
+
private writeClosed;
|
|
15
16
|
private closed;
|
|
16
17
|
constructor(port: MessagePort);
|
|
17
18
|
read(options?: OperationOptions): Promise<Uint8Array | null>;
|
|
@@ -22,6 +23,7 @@ export declare class MessagePortByteStream implements ByteStream {
|
|
|
22
23
|
private handle;
|
|
23
24
|
private finish;
|
|
24
25
|
private fail;
|
|
26
|
+
private settleWrite;
|
|
25
27
|
}
|
|
26
28
|
export type RegisterProxyAppWindowOptions = Readonly<{
|
|
27
29
|
controllerOrigin: string;
|
|
@@ -18,6 +18,7 @@ export class MessagePortByteStream {
|
|
|
18
18
|
buffered = 0;
|
|
19
19
|
readBuffered = 0;
|
|
20
20
|
ended = false;
|
|
21
|
+
writeClosed = false;
|
|
21
22
|
closed = false;
|
|
22
23
|
constructor(port) {
|
|
23
24
|
this.port = port;
|
|
@@ -38,12 +39,17 @@ export class MessagePortByteStream {
|
|
|
38
39
|
if (this.terminalError !== undefined)
|
|
39
40
|
throw this.terminalError;
|
|
40
41
|
return await new Promise((resolve, reject) => {
|
|
41
|
-
const
|
|
42
|
+
const cleanup = () => options.signal?.removeEventListener("abort", onAbort);
|
|
43
|
+
const waiter = {
|
|
44
|
+
resolve: (value) => { cleanup(); resolve(value); },
|
|
45
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
46
|
+
};
|
|
42
47
|
const onAbort = () => {
|
|
43
48
|
const index = this.readWaiters.indexOf(waiter);
|
|
44
|
-
if (index
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
if (index < 0)
|
|
50
|
+
return;
|
|
51
|
+
this.readWaiters.splice(index, 1);
|
|
52
|
+
waiter.reject(new SessionError("canceled"));
|
|
47
53
|
};
|
|
48
54
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
49
55
|
this.readWaiters.push(waiter);
|
|
@@ -52,6 +58,8 @@ export class MessagePortByteStream {
|
|
|
52
58
|
async write(data, options = {}) {
|
|
53
59
|
if (this.closed || this.terminalError !== undefined)
|
|
54
60
|
throw this.terminalError ?? new SessionError("closed");
|
|
61
|
+
if (this.writeClosed)
|
|
62
|
+
throw new SessionError("operation_failed");
|
|
55
63
|
if (options.signal?.aborted === true)
|
|
56
64
|
throw new SessionError("canceled");
|
|
57
65
|
if (data.length === 0)
|
|
@@ -63,24 +71,27 @@ export class MessagePortByteStream {
|
|
|
63
71
|
const copy = data.slice();
|
|
64
72
|
this.buffered += copy.length;
|
|
65
73
|
await new Promise((resolve, reject) => {
|
|
74
|
+
const onAbort = () => this.settleWrite(id, new SessionError("canceled"));
|
|
66
75
|
this.writeWaiters.set(id, {
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
bytes: copy.length,
|
|
77
|
+
...(options.signal === undefined ? {} : { signal: options.signal, onAbort }),
|
|
78
|
+
resolve,
|
|
79
|
+
reject,
|
|
69
80
|
});
|
|
81
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
70
82
|
try {
|
|
71
83
|
this.port.postMessage({ type: "chunk", id, data: copy.buffer }, [copy.buffer]);
|
|
72
84
|
}
|
|
73
85
|
catch {
|
|
74
|
-
this.
|
|
75
|
-
this.buffered -= copy.length;
|
|
76
|
-
reject(new SessionError("operation_failed"));
|
|
86
|
+
this.settleWrite(id, new SessionError("operation_failed"));
|
|
77
87
|
}
|
|
78
88
|
});
|
|
79
89
|
return data.length;
|
|
80
90
|
}
|
|
81
91
|
async closeWrite() {
|
|
82
|
-
if (this.closed)
|
|
92
|
+
if (this.closed || this.writeClosed)
|
|
83
93
|
return;
|
|
94
|
+
this.writeClosed = true;
|
|
84
95
|
this.port.postMessage({ type: "end" });
|
|
85
96
|
}
|
|
86
97
|
async reset() {
|
|
@@ -100,6 +111,10 @@ export class MessagePortByteStream {
|
|
|
100
111
|
return;
|
|
101
112
|
const message = value;
|
|
102
113
|
if (message.type === "chunk") {
|
|
114
|
+
if (this.ended || this.closed) {
|
|
115
|
+
this.fail(new SessionError("operation_failed"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
103
118
|
if (!Number.isSafeInteger(message.id) || !(message.data instanceof ArrayBuffer) || message.data.byteLength > MAX_BRIDGE_CHUNK_BYTES) {
|
|
104
119
|
this.fail(new SessionError("operation_failed"));
|
|
105
120
|
return;
|
|
@@ -122,14 +137,12 @@ export class MessagePortByteStream {
|
|
|
122
137
|
return;
|
|
123
138
|
}
|
|
124
139
|
if (message.type === "ack" && Number.isSafeInteger(message.id)) {
|
|
125
|
-
|
|
126
|
-
if (waiter !== undefined) {
|
|
127
|
-
this.writeWaiters.delete(message.id);
|
|
128
|
-
waiter.resolve();
|
|
129
|
-
}
|
|
140
|
+
this.settleWrite(message.id);
|
|
130
141
|
return;
|
|
131
142
|
}
|
|
132
143
|
if (message.type === "end") {
|
|
144
|
+
if (this.ended)
|
|
145
|
+
return;
|
|
133
146
|
this.ended = true;
|
|
134
147
|
for (const waiter of this.readWaiters.splice(0))
|
|
135
148
|
waiter.resolve(null);
|
|
@@ -149,9 +162,8 @@ export class MessagePortByteStream {
|
|
|
149
162
|
this.readBuffered = 0;
|
|
150
163
|
for (const waiter of this.readWaiters.splice(0))
|
|
151
164
|
waiter.resolve(null);
|
|
152
|
-
for (const
|
|
153
|
-
|
|
154
|
-
this.writeWaiters.clear();
|
|
165
|
+
for (const id of [...this.writeWaiters.keys()])
|
|
166
|
+
this.settleWrite(id, new SessionError("closed"));
|
|
155
167
|
this.port.close();
|
|
156
168
|
}
|
|
157
169
|
fail(error) {
|
|
@@ -163,11 +175,22 @@ export class MessagePortByteStream {
|
|
|
163
175
|
this.readBuffered = 0;
|
|
164
176
|
for (const waiter of this.readWaiters.splice(0))
|
|
165
177
|
waiter.reject(error);
|
|
166
|
-
for (const
|
|
167
|
-
|
|
168
|
-
this.writeWaiters.clear();
|
|
178
|
+
for (const id of [...this.writeWaiters.keys()])
|
|
179
|
+
this.settleWrite(id, error);
|
|
169
180
|
this.port.close();
|
|
170
181
|
}
|
|
182
|
+
settleWrite(id, error) {
|
|
183
|
+
const waiter = this.writeWaiters.get(id);
|
|
184
|
+
if (waiter === undefined)
|
|
185
|
+
return;
|
|
186
|
+
this.writeWaiters.delete(id);
|
|
187
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
188
|
+
this.buffered = Math.max(0, this.buffered - waiter.bytes);
|
|
189
|
+
if (error === undefined)
|
|
190
|
+
waiter.resolve();
|
|
191
|
+
else
|
|
192
|
+
waiter.reject(error);
|
|
193
|
+
}
|
|
171
194
|
}
|
|
172
195
|
function bridgeLimits(maxWsFrameBytes, maxWsBufferedAmountBytes) {
|
|
173
196
|
const wsFrame = maxWsFrameBytes ?? SDK_DEFAULTS.proxy.maxWsFrameBytes;
|
|
@@ -219,20 +242,39 @@ export function registerProxyAppWindow(options) {
|
|
|
219
242
|
openWebSocketStream: async (path, openOptions = {}) => {
|
|
220
243
|
if (disposed)
|
|
221
244
|
throw new SessionError("closed");
|
|
245
|
+
if (openOptions.signal?.aborted === true)
|
|
246
|
+
throw new SessionError("canceled");
|
|
222
247
|
const channel = new MessageChannel();
|
|
223
248
|
const response = new Promise((resolve, reject) => {
|
|
224
|
-
|
|
249
|
+
let settled = false;
|
|
250
|
+
const finish = (error, opened) => {
|
|
251
|
+
if (settled)
|
|
252
|
+
return;
|
|
253
|
+
settled = true;
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
openOptions.signal?.removeEventListener("abort", abort);
|
|
256
|
+
if (error !== undefined) {
|
|
257
|
+
channel.port1.close();
|
|
258
|
+
reject(error);
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
resolve(opened);
|
|
262
|
+
};
|
|
263
|
+
const abort = () => {
|
|
264
|
+
channel.port1.postMessage({ type: "reset" });
|
|
265
|
+
finish(new SessionError("canceled"));
|
|
266
|
+
};
|
|
267
|
+
const timer = setTimeout(() => finish(new SessionError("timeout")), 10_000);
|
|
225
268
|
channel.port1.onmessage = (event) => {
|
|
226
269
|
if (event.data?.type !== WEBSOCKET_ACK_MESSAGE)
|
|
227
270
|
return;
|
|
228
|
-
clearTimeout(timer);
|
|
229
271
|
if (event.data.ok !== true) {
|
|
230
|
-
|
|
231
|
-
reject(new SessionError("operation_failed"));
|
|
272
|
+
finish(new SessionError("operation_failed"));
|
|
232
273
|
return;
|
|
233
274
|
}
|
|
234
|
-
|
|
275
|
+
finish(undefined, Object.freeze({ stream: new MessagePortByteStream(channel.port1), protocol: typeof event.data.protocol === "string" ? event.data.protocol : "" }));
|
|
235
276
|
};
|
|
277
|
+
openOptions.signal?.addEventListener("abort", abort, { once: true });
|
|
236
278
|
});
|
|
237
279
|
controller.postMessage({
|
|
238
280
|
type: WEBSOCKET_OPEN_MESSAGE,
|
|
@@ -241,9 +283,6 @@ export function registerProxyAppWindow(options) {
|
|
|
241
283
|
protocols: openOptions.protocols ?? [],
|
|
242
284
|
...(nonce === undefined ? {} : { capabilityNonce: nonce }),
|
|
243
285
|
}, origin, [channel.port2]);
|
|
244
|
-
if (openOptions.signal !== undefined) {
|
|
245
|
-
openOptions.signal.addEventListener("abort", () => channel.port1.close(), { once: true });
|
|
246
|
-
}
|
|
247
286
|
return await response;
|
|
248
287
|
},
|
|
249
288
|
dispose: () => { disposed = true; },
|
|
@@ -253,33 +292,65 @@ export function registerProxyAppWindow(options) {
|
|
|
253
292
|
dispose: () => { disposed = true; },
|
|
254
293
|
});
|
|
255
294
|
}
|
|
256
|
-
async function bridgeStreams(runtimeStream, port) {
|
|
295
|
+
async function bridgeStreams(runtimeStream, port, signal) {
|
|
257
296
|
const bridge = new MessagePortByteStream(port);
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
let resetTask;
|
|
299
|
+
const resetBoth = () => {
|
|
300
|
+
resetTask ??= Promise.allSettled([
|
|
301
|
+
Promise.resolve().then(async () => await runtimeStream.reset()),
|
|
302
|
+
Promise.resolve().then(async () => await bridge.reset()),
|
|
303
|
+
]);
|
|
304
|
+
return resetTask;
|
|
305
|
+
};
|
|
306
|
+
const abort = () => {
|
|
307
|
+
controller.abort(signal?.reason ?? new SessionError("canceled"));
|
|
308
|
+
void resetBoth();
|
|
309
|
+
};
|
|
310
|
+
if (signal?.aborted === true)
|
|
311
|
+
abort();
|
|
312
|
+
else
|
|
313
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
258
314
|
const left = (async () => {
|
|
259
315
|
while (true) {
|
|
260
|
-
const chunk = await runtimeStream.read();
|
|
316
|
+
const chunk = await runtimeStream.read({ signal: controller.signal });
|
|
261
317
|
if (chunk === null) {
|
|
262
318
|
await bridge.closeWrite();
|
|
263
319
|
return;
|
|
264
320
|
}
|
|
265
|
-
await bridge.write(chunk);
|
|
321
|
+
await bridge.write(chunk, { signal: controller.signal });
|
|
266
322
|
}
|
|
267
|
-
})();
|
|
323
|
+
})().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
|
|
268
324
|
const right = (async () => {
|
|
269
325
|
while (true) {
|
|
270
|
-
const chunk = await bridge.read();
|
|
326
|
+
const chunk = await bridge.read({ signal: controller.signal });
|
|
271
327
|
if (chunk === null) {
|
|
272
328
|
await runtimeStream.closeWrite();
|
|
273
329
|
return;
|
|
274
330
|
}
|
|
275
331
|
let offset = 0;
|
|
276
332
|
while (offset < chunk.length)
|
|
277
|
-
offset += await runtimeStream.write(chunk.subarray(offset));
|
|
333
|
+
offset += await runtimeStream.write(chunk.subarray(offset), { signal: controller.signal });
|
|
278
334
|
}
|
|
279
|
-
})();
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
335
|
+
})().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
|
|
336
|
+
try {
|
|
337
|
+
const settled = await Promise.allSettled([left, right]);
|
|
338
|
+
const failure = settled.find((result) => result.status === "rejected");
|
|
339
|
+
if (failure !== undefined) {
|
|
340
|
+
await resetBoth();
|
|
341
|
+
throw failure.reason;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
await Promise.all([runtimeStream.close(), bridge.close()]);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
await resetBoth();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
signal?.removeEventListener("abort", abort);
|
|
353
|
+
}
|
|
283
354
|
}
|
|
284
355
|
export function registerProxyControllerWindow(options) {
|
|
285
356
|
const target = options.targetWindow ?? globalThis.window;
|
|
@@ -289,6 +360,7 @@ export function registerProxyControllerWindow(options) {
|
|
|
289
360
|
}
|
|
290
361
|
const nonce = capability(options.capabilityNonce);
|
|
291
362
|
let disposed = false;
|
|
363
|
+
const active = new Set();
|
|
292
364
|
const onMessage = (event) => {
|
|
293
365
|
if (disposed || !allowed.has(event.origin) || (options.expectedSource !== undefined && event.source !== options.expectedSource))
|
|
294
366
|
return;
|
|
@@ -303,22 +375,51 @@ export function registerProxyControllerWindow(options) {
|
|
|
303
375
|
}
|
|
304
376
|
if (event.data?.type === WEBSOCKET_OPEN_MESSAGE && event.data.version === 2) {
|
|
305
377
|
void (async () => {
|
|
378
|
+
let canceled = false;
|
|
379
|
+
const openController = new AbortController();
|
|
380
|
+
active.add(openController);
|
|
381
|
+
port.onmessage = (message) => {
|
|
382
|
+
if (message.data?.type === "reset" ||
|
|
383
|
+
message.data?.type === "close") {
|
|
384
|
+
canceled = true;
|
|
385
|
+
openController.abort();
|
|
386
|
+
port.close();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
306
389
|
try {
|
|
307
390
|
const opened = await options.runtime.openWebSocketStream(String(event.data.path ?? ""), {
|
|
308
391
|
protocols: Array.isArray(event.data.protocols) ? event.data.protocols.map(String) : [],
|
|
392
|
+
signal: openController.signal,
|
|
309
393
|
});
|
|
394
|
+
if (canceled) {
|
|
395
|
+
await opened.stream.reset();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
310
398
|
port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: true, protocol: opened.protocol });
|
|
311
|
-
await bridgeStreams(opened.stream, port);
|
|
399
|
+
await bridgeStreams(opened.stream, port, openController.signal);
|
|
312
400
|
}
|
|
313
401
|
catch {
|
|
314
|
-
|
|
402
|
+
try {
|
|
403
|
+
port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: false });
|
|
404
|
+
}
|
|
405
|
+
catch { /* Port is already closed. */ }
|
|
315
406
|
port.close();
|
|
316
407
|
}
|
|
408
|
+
finally {
|
|
409
|
+
active.delete(openController);
|
|
410
|
+
}
|
|
317
411
|
})();
|
|
318
412
|
}
|
|
319
413
|
};
|
|
320
414
|
target.addEventListener("message", onMessage);
|
|
321
|
-
return Object.freeze({
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
dispose: () => {
|
|
417
|
+
disposed = true;
|
|
418
|
+
target.removeEventListener("message", onMessage);
|
|
419
|
+
for (const controller of active)
|
|
420
|
+
controller.abort(new SessionError("closed"));
|
|
421
|
+
},
|
|
422
|
+
});
|
|
322
423
|
}
|
|
323
424
|
export async function registerProxyAppWindowWithServiceWorkerControl(options) {
|
|
324
425
|
await registerServiceWorkerAndEnsureControl({
|