@floegence/flowersec-core 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/client-connect/transportSecurity.js +1 -1
- package/dist/endpoint/index.js +185 -33
- package/dist/proxy/appWindow.js +93 -7
- package/dist/proxy/controllerWindow.js +219 -78
- package/dist/proxy/portStream.d.ts +2 -1
- package/dist/proxy/portStream.js +173 -74
- package/dist/proxy/server.d.ts +2 -0
- package/dist/proxy/server.js +346 -105
- package/dist/proxy/windowBridgeProtocol.d.ts +4 -3
- package/dist/proxy/windowBridgeProtocol.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE,
|
|
1
|
+
import { PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, } from "./windowBridgeProtocol.js";
|
|
2
2
|
function normalizeOrigins(origins) {
|
|
3
3
|
const out = [];
|
|
4
4
|
const seen = new Set();
|
|
@@ -56,14 +56,45 @@ function withTrustedExternalOrigin(req, rawOrigin) {
|
|
|
56
56
|
? sanitized
|
|
57
57
|
: { ...sanitized, external_origin: externalOrigin };
|
|
58
58
|
}
|
|
59
|
-
function
|
|
59
|
+
function validWriteId(value) {
|
|
60
|
+
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
61
|
+
}
|
|
62
|
+
function bridgeWebSocket(runtime, msg, port, onTerminal) {
|
|
63
|
+
const capabilities = Array.isArray(msg.capabilities)
|
|
64
|
+
? msg.capabilities.filter((value) => typeof value === "string")
|
|
65
|
+
: [];
|
|
66
|
+
if (!capabilities.includes(PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY)) {
|
|
67
|
+
try {
|
|
68
|
+
port.postMessage({
|
|
69
|
+
type: PROXY_WINDOW_WS_ERROR_MSG_TYPE,
|
|
70
|
+
message: "proxy Window bridge requires bidirectional stream acknowledgements",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Best-effort.
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
port.close();
|
|
78
|
+
}
|
|
79
|
+
onTerminal();
|
|
80
|
+
return { dispose: () => { }, isTerminal: () => true };
|
|
81
|
+
}
|
|
60
82
|
const ac = new AbortController();
|
|
61
83
|
let terminal = false;
|
|
84
|
+
let terminalError = null;
|
|
62
85
|
let acceptingWrites = true;
|
|
63
86
|
let stream = null;
|
|
64
|
-
let
|
|
65
|
-
let
|
|
87
|
+
let pendingInboundWriteId = null;
|
|
88
|
+
let pendingOutboundAcknowledgement = null;
|
|
89
|
+
let nextWriteId = 1;
|
|
66
90
|
const maxBufferedBytes = runtime.limits.maxWsBufferedAmountBytes ?? 4 * (1 << 20);
|
|
91
|
+
let terminalNotified = false;
|
|
92
|
+
const notifyTerminal = () => {
|
|
93
|
+
if (terminalNotified)
|
|
94
|
+
return;
|
|
95
|
+
terminalNotified = true;
|
|
96
|
+
onTerminal();
|
|
97
|
+
};
|
|
67
98
|
const closePort = () => {
|
|
68
99
|
try {
|
|
69
100
|
port.close();
|
|
@@ -72,12 +103,16 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
72
103
|
// Best-effort.
|
|
73
104
|
}
|
|
74
105
|
};
|
|
75
|
-
const failBridge = (error) => {
|
|
106
|
+
const failBridge = (error, notifyPeer = true) => {
|
|
76
107
|
if (terminal)
|
|
77
108
|
return;
|
|
78
109
|
terminal = true;
|
|
79
110
|
acceptingWrites = false;
|
|
80
111
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
112
|
+
terminalError = err;
|
|
113
|
+
pendingInboundWriteId = null;
|
|
114
|
+
pendingOutboundAcknowledgement?.reject(err);
|
|
115
|
+
pendingOutboundAcknowledgement = null;
|
|
81
116
|
if (stream != null) {
|
|
82
117
|
void Promise.resolve(stream.reset(err)).catch(() => {
|
|
83
118
|
// The bridge error is already delivered through the terminal response.
|
|
@@ -89,17 +124,146 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
89
124
|
catch {
|
|
90
125
|
// Best-effort.
|
|
91
126
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
127
|
+
if (notifyPeer) {
|
|
128
|
+
try {
|
|
129
|
+
port.postMessage({
|
|
130
|
+
type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
|
|
131
|
+
message: err.message,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Best-effort.
|
|
136
|
+
}
|
|
100
137
|
}
|
|
101
138
|
closePort();
|
|
139
|
+
notifyTerminal();
|
|
140
|
+
};
|
|
141
|
+
const failProtocol = (message) => {
|
|
142
|
+
failBridge(new Error(`proxy Window stream protocol error: ${message}`));
|
|
143
|
+
};
|
|
144
|
+
const postChunkAndWait = async (chunk) => {
|
|
145
|
+
if (chunk.byteLength > maxBufferedBytes) {
|
|
146
|
+
throw new Error("proxy WebSocket inbound chunk exceeded the buffer limit");
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isSafeInteger(nextWriteId)) {
|
|
149
|
+
throw new Error("proxy Window stream write identifier space exhausted");
|
|
150
|
+
}
|
|
151
|
+
if (pendingOutboundAcknowledgement != null) {
|
|
152
|
+
throw new Error("proxy Window stream already has an unacknowledged outbound chunk");
|
|
153
|
+
}
|
|
154
|
+
const writeId = nextWriteId++;
|
|
155
|
+
const ab = cloneChunk(chunk);
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
pendingOutboundAcknowledgement = { writeId, resolve, reject };
|
|
158
|
+
try {
|
|
159
|
+
port.postMessage({
|
|
160
|
+
type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE,
|
|
161
|
+
data: ab,
|
|
162
|
+
writeId,
|
|
163
|
+
}, [ab]);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
pendingOutboundAcknowledgement = null;
|
|
167
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
port.onmessage = (ev) => {
|
|
172
|
+
const data = ev.data;
|
|
173
|
+
if (data == null || typeof data !== "object")
|
|
174
|
+
return;
|
|
175
|
+
const type = typeof data.type === "string" ? data.type : "";
|
|
176
|
+
if (stream == null) {
|
|
177
|
+
if (type === PROXY_WINDOW_STREAM_RESET_MSG_TYPE) {
|
|
178
|
+
const message = String(data.message ?? "stream reset");
|
|
179
|
+
failBridge(new Error(message), false);
|
|
180
|
+
}
|
|
181
|
+
else if (type === PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE
|
|
182
|
+
|| type === PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE
|
|
183
|
+
|| type === PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE) {
|
|
184
|
+
failProtocol("received stream traffic before the websocket opened");
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
switch (type) {
|
|
189
|
+
case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
|
|
190
|
+
if (!acceptingWrites)
|
|
191
|
+
return;
|
|
192
|
+
const chunkMessage = data;
|
|
193
|
+
const raw = chunkMessage.data;
|
|
194
|
+
if (!(raw instanceof ArrayBuffer) || !validWriteId(chunkMessage.writeId)) {
|
|
195
|
+
failProtocol("invalid stream chunk");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (pendingInboundWriteId != null) {
|
|
199
|
+
failProtocol("received more than one unacknowledged chunk");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (raw.byteLength > maxBufferedBytes) {
|
|
203
|
+
failBridge(new Error("proxy WebSocket outbound buffer exceeded"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const writeId = chunkMessage.writeId;
|
|
207
|
+
const chunk = new Uint8Array(raw);
|
|
208
|
+
pendingInboundWriteId = writeId;
|
|
209
|
+
void stream.write(chunk)
|
|
210
|
+
.then(() => {
|
|
211
|
+
if (terminal)
|
|
212
|
+
return;
|
|
213
|
+
if (pendingInboundWriteId !== writeId) {
|
|
214
|
+
failProtocol("completed write does not match the pending chunk");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
pendingInboundWriteId = null;
|
|
218
|
+
port.postMessage({
|
|
219
|
+
type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
|
|
220
|
+
writeId,
|
|
221
|
+
});
|
|
222
|
+
})
|
|
223
|
+
.catch((error) => failBridge(error));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE: {
|
|
227
|
+
if (!acceptingWrites)
|
|
228
|
+
return;
|
|
229
|
+
if (pendingInboundWriteId != null) {
|
|
230
|
+
failProtocol("stream closed before the pending chunk was acknowledged");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
acceptingWrites = false;
|
|
234
|
+
terminal = true;
|
|
235
|
+
const closeError = new Error("stream is closed");
|
|
236
|
+
pendingInboundWriteId = null;
|
|
237
|
+
pendingOutboundAcknowledgement?.reject(closeError);
|
|
238
|
+
pendingOutboundAcknowledgement = null;
|
|
239
|
+
void Promise.resolve(stream.close()).catch(() => {
|
|
240
|
+
// The peer already closed its bridge endpoint.
|
|
241
|
+
});
|
|
242
|
+
closePort();
|
|
243
|
+
notifyTerminal();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
case PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE: {
|
|
247
|
+
const writeId = data.writeId;
|
|
248
|
+
const pending = pendingOutboundAcknowledgement;
|
|
249
|
+
if (!validWriteId(writeId) || pending == null || pending.writeId !== writeId) {
|
|
250
|
+
failProtocol("unexpected stream write acknowledgement");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
pendingOutboundAcknowledgement = null;
|
|
254
|
+
pending.resolve();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
|
|
258
|
+
const message = String(data.message ?? "stream reset");
|
|
259
|
+
failBridge(new Error(message), false);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
102
265
|
};
|
|
266
|
+
port.start?.();
|
|
103
267
|
void (async () => {
|
|
104
268
|
try {
|
|
105
269
|
const wsOpts = {
|
|
@@ -107,69 +271,15 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
107
271
|
...(msg.protocols === undefined ? {} : { protocols: msg.protocols }),
|
|
108
272
|
};
|
|
109
273
|
const opened = await runtime.openWebSocketStream(msg.path, wsOpts);
|
|
274
|
+
if (terminal) {
|
|
275
|
+
await Promise.resolve(opened.stream.reset(terminalError ?? new Error("proxy controller Window bridge is closed")));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
110
278
|
stream = opened.stream;
|
|
111
|
-
port.onmessage = (ev) => {
|
|
112
|
-
const data = ev.data;
|
|
113
|
-
if (data == null || typeof data !== "object")
|
|
114
|
-
return;
|
|
115
|
-
const type = typeof data.type === "string" ? data.type : "";
|
|
116
|
-
switch (type) {
|
|
117
|
-
case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
|
|
118
|
-
if (!acceptingWrites || stream == null)
|
|
119
|
-
return;
|
|
120
|
-
const raw = data.data;
|
|
121
|
-
if (!(raw instanceof ArrayBuffer))
|
|
122
|
-
return;
|
|
123
|
-
if (pendingWriteBytes + raw.byteLength > maxBufferedBytes) {
|
|
124
|
-
failBridge(new Error("proxy WebSocket outbound buffer exceeded"));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const rawWriteId = data.writeId;
|
|
128
|
-
const writeId = Number.isSafeInteger(rawWriteId) && Number(rawWriteId) > 0
|
|
129
|
-
? Number(rawWriteId)
|
|
130
|
-
: undefined;
|
|
131
|
-
const chunk = new Uint8Array(raw);
|
|
132
|
-
pendingWriteBytes += chunk.byteLength;
|
|
133
|
-
writeChain = writeChain
|
|
134
|
-
.then(async () => {
|
|
135
|
-
if (terminal || stream == null)
|
|
136
|
-
throw new Error("stream is closed");
|
|
137
|
-
await stream.write(chunk);
|
|
138
|
-
if (terminal || writeId === undefined)
|
|
139
|
-
return;
|
|
140
|
-
port.postMessage({
|
|
141
|
-
type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
|
|
142
|
-
writeId,
|
|
143
|
-
});
|
|
144
|
-
})
|
|
145
|
-
.catch((error) => failBridge(error))
|
|
146
|
-
.finally(() => {
|
|
147
|
-
pendingWriteBytes = Math.max(0, pendingWriteBytes - chunk.byteLength);
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE:
|
|
152
|
-
if (!acceptingWrites || stream == null)
|
|
153
|
-
return;
|
|
154
|
-
acceptingWrites = false;
|
|
155
|
-
writeChain = writeChain
|
|
156
|
-
.then(() => stream?.close())
|
|
157
|
-
.catch((error) => failBridge(error));
|
|
158
|
-
return;
|
|
159
|
-
case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
|
|
160
|
-
const message = String(data.message ?? "stream reset");
|
|
161
|
-
failBridge(new Error(message));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
default:
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
port.start?.();
|
|
169
279
|
port.postMessage({
|
|
170
280
|
type: PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE,
|
|
171
281
|
protocol: opened.protocol,
|
|
172
|
-
capabilities: [
|
|
282
|
+
capabilities: [PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY],
|
|
173
283
|
});
|
|
174
284
|
while (!terminal) {
|
|
175
285
|
const chunk = await stream.read();
|
|
@@ -178,22 +288,36 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
178
288
|
acceptingWrites = false;
|
|
179
289
|
port.postMessage({ type: PROXY_WINDOW_STREAM_END_MSG_TYPE });
|
|
180
290
|
closePort();
|
|
291
|
+
notifyTerminal();
|
|
181
292
|
return;
|
|
182
293
|
}
|
|
183
|
-
|
|
184
|
-
port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab }, [ab]);
|
|
294
|
+
await postChunkAndWait(chunk);
|
|
185
295
|
}
|
|
186
296
|
}
|
|
187
297
|
catch (error) {
|
|
298
|
+
if (terminal)
|
|
299
|
+
return;
|
|
188
300
|
const message = error instanceof Error ? error.message : String(error);
|
|
189
301
|
if (stream == null) {
|
|
190
|
-
|
|
191
|
-
|
|
302
|
+
terminal = true;
|
|
303
|
+
acceptingWrites = false;
|
|
304
|
+
terminalError = error instanceof Error ? error : new Error(message);
|
|
305
|
+
try {
|
|
306
|
+
port.postMessage({ type: PROXY_WINDOW_WS_ERROR_MSG_TYPE, message });
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
closePort();
|
|
310
|
+
notifyTerminal();
|
|
311
|
+
}
|
|
192
312
|
return;
|
|
193
313
|
}
|
|
194
314
|
failBridge(error);
|
|
195
315
|
}
|
|
196
316
|
})();
|
|
317
|
+
return {
|
|
318
|
+
dispose: (error) => failBridge(error),
|
|
319
|
+
isTerminal: () => terminal,
|
|
320
|
+
};
|
|
197
321
|
}
|
|
198
322
|
export function registerProxyControllerWindow(opts) {
|
|
199
323
|
const allowedOrigins = normalizeOrigins(opts.allowedOrigins);
|
|
@@ -202,6 +326,8 @@ export function registerProxyControllerWindow(opts) {
|
|
|
202
326
|
}
|
|
203
327
|
const capabilityNonce = normalizeCapabilityNonce(opts.capabilityNonce);
|
|
204
328
|
requireBridgeCapability(opts.expectedSource, capabilityNonce);
|
|
329
|
+
const activeWebSocketBridges = new Set();
|
|
330
|
+
let disposed = false;
|
|
205
331
|
const targetWindow = opts.targetWindow ?? globalThis.window;
|
|
206
332
|
if (targetWindow == null) {
|
|
207
333
|
throw new Error("targetWindow is not available");
|
|
@@ -227,9 +353,18 @@ export function registerProxyControllerWindow(opts) {
|
|
|
227
353
|
opts.runtime.dispatchFetch(withTrustedExternalOrigin(msg.req, eventOrigin), port);
|
|
228
354
|
return;
|
|
229
355
|
}
|
|
230
|
-
case PROXY_WINDOW_WS_OPEN_MSG_TYPE:
|
|
231
|
-
|
|
356
|
+
case PROXY_WINDOW_WS_OPEN_MSG_TYPE: {
|
|
357
|
+
if (disposed)
|
|
358
|
+
return;
|
|
359
|
+
let bridge = null;
|
|
360
|
+
bridge = bridgeWebSocket(opts.runtime, data, port, () => {
|
|
361
|
+
if (bridge != null)
|
|
362
|
+
activeWebSocketBridges.delete(bridge);
|
|
363
|
+
});
|
|
364
|
+
if (!bridge.isTerminal())
|
|
365
|
+
activeWebSocketBridges.add(bridge);
|
|
232
366
|
return;
|
|
367
|
+
}
|
|
233
368
|
default:
|
|
234
369
|
return;
|
|
235
370
|
}
|
|
@@ -237,7 +372,13 @@ export function registerProxyControllerWindow(opts) {
|
|
|
237
372
|
targetWindow.addEventListener("message", onMessage);
|
|
238
373
|
return {
|
|
239
374
|
dispose: () => {
|
|
375
|
+
if (disposed)
|
|
376
|
+
return;
|
|
377
|
+
disposed = true;
|
|
240
378
|
targetWindow.removeEventListener("message", onMessage);
|
|
379
|
+
const error = new Error("proxy controller Window bridge is disposed");
|
|
380
|
+
for (const bridge of [...activeWebSocketBridges])
|
|
381
|
+
bridge.dispose(error);
|
|
241
382
|
},
|
|
242
383
|
};
|
|
243
384
|
}
|