@floegence/flowersec-core 0.19.11 → 0.20.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 +7 -7
- package/dist/browser/reconnectConfig.d.ts +8 -36
- package/dist/browser/reconnectConfig.js +13 -70
- package/dist/client-connect/connectCore.d.ts +14 -5
- package/dist/client-connect/connectCore.js +130 -38
- package/dist/client-connect/transportSecurity.js +13 -15
- package/dist/client.d.ts +2 -0
- package/dist/e2ee/handshake.d.ts +4 -0
- package/dist/e2ee/handshake.js +2 -0
- package/dist/e2ee/secureChannel.d.ts +4 -0
- package/dist/e2ee/secureChannel.js +18 -9
- package/dist/facade.d.ts +3 -0
- package/dist/node/reconnectConfig.d.ts +8 -34
- package/dist/node/reconnectConfig.js +13 -71
- package/dist/node/wsFactory.js +3 -0
- package/dist/observability/observer.d.ts +5 -2
- package/dist/observability/observer.js +3 -0
- package/dist/reconnect/artifactControlplane.d.ts +9 -6
- package/dist/reconnect/artifactControlplane.js +28 -27
- package/dist/reconnect/index.d.ts +2 -0
- package/dist/reconnect/index.js +7 -0
- package/dist/rpc/server.d.ts +29 -4
- package/dist/rpc/server.js +142 -34
- package/dist/tunnel-client/connect.js +6 -6
- package/dist/utils/errors.d.ts +1 -1
- package/dist/ws-client/binaryTransport.d.ts +20 -5
- package/dist/ws-client/binaryTransport.js +110 -8
- package/dist/yamux/byteReader.d.ts +1 -0
- package/dist/yamux/byteReader.js +26 -0
- package/dist/yamux/errors.d.ts +7 -0
- package/dist/yamux/errors.js +15 -0
- package/dist/yamux/session.d.ts +31 -1
- package/dist/yamux/session.js +191 -12
- package/dist/yamux/stream.d.ts +10 -3
- package/dist/yamux/stream.js +61 -19
- package/package.json +1 -1
package/dist/yamux/session.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { ByteReader } from "./byteReader.js";
|
|
2
2
|
import { decodeHeader, encodeHeader, HEADER_LEN } from "./header.js";
|
|
3
|
-
import {
|
|
3
|
+
import { FLAG_ACK, FLAG_RST, FLAG_SYN, TYPE_DATA, TYPE_GO_AWAY, TYPE_PING, TYPE_WINDOW_UPDATE, YAMUX_VERSION } from "./constants.js";
|
|
4
4
|
import { YamuxStream } from "./stream.js";
|
|
5
|
+
import { YamuxResourceExhaustedError } from "./errors.js";
|
|
6
|
+
export const DEFAULT_YAMUX_LIMITS = Object.freeze({
|
|
7
|
+
maxActiveStreams: 64,
|
|
8
|
+
maxInboundStreams: 32,
|
|
9
|
+
maxFrameBytes: 256 * 1024,
|
|
10
|
+
preferredOutboundFrameBytes: 64 * 1024,
|
|
11
|
+
maxStreamReceiveBytes: 256 * 1024,
|
|
12
|
+
maxSessionReceiveBytes: 16 * (1 << 20),
|
|
13
|
+
});
|
|
5
14
|
// YamuxSession multiplexes multiple streams over a single byte stream.
|
|
6
15
|
export class YamuxSession {
|
|
7
16
|
// Underlying byte stream for yamux frames.
|
|
@@ -13,7 +22,8 @@ export class YamuxSession {
|
|
|
13
22
|
// Callback for inbound streams created from SYN frames.
|
|
14
23
|
onIncomingStream;
|
|
15
24
|
// Maximum allowed DATA frame length.
|
|
16
|
-
|
|
25
|
+
limits;
|
|
26
|
+
onDiagnostic;
|
|
17
27
|
// True when this side is the yamux client (odd local stream IDs).
|
|
18
28
|
client;
|
|
19
29
|
// Next stream ID to allocate (odd/even based on role).
|
|
@@ -22,6 +32,11 @@ export class YamuxSession {
|
|
|
22
32
|
closed = false;
|
|
23
33
|
// Writers waiting for send window credits per stream.
|
|
24
34
|
sendWindowWaiters = new Map();
|
|
35
|
+
inboundStreams = new Set();
|
|
36
|
+
sessionReceiveBytes = 0;
|
|
37
|
+
nextPingId = 1;
|
|
38
|
+
pingWaiters = new Map();
|
|
39
|
+
activeProbe;
|
|
25
40
|
constructor(conn, opts) {
|
|
26
41
|
this.conn = conn;
|
|
27
42
|
this.reader = new ByteReader(async () => {
|
|
@@ -33,19 +48,33 @@ export class YamuxSession {
|
|
|
33
48
|
}
|
|
34
49
|
});
|
|
35
50
|
this.onIncomingStream = opts.onIncomingStream;
|
|
36
|
-
this.
|
|
51
|
+
this.limits = normalizeYamuxLimits({ ...opts.limits, ...(opts.maxFrameBytes === undefined ? {} : { maxFrameBytes: opts.maxFrameBytes }) });
|
|
52
|
+
this.onDiagnostic = opts.onDiagnostic;
|
|
37
53
|
this.client = opts.client;
|
|
38
54
|
this.nextStreamId = opts.client ? 1 : 2;
|
|
39
55
|
void this.readLoop();
|
|
40
56
|
}
|
|
41
57
|
// openStream allocates a new stream and performs the SYN handshake.
|
|
42
58
|
async openStream() {
|
|
59
|
+
if (this.closed)
|
|
60
|
+
throw new Error("session closed");
|
|
61
|
+
if (this.streams.size >= this.limits.maxActiveStreams) {
|
|
62
|
+
const error = new YamuxResourceExhaustedError("active_streams", this.streams.size, this.limits.maxActiveStreams);
|
|
63
|
+
this.diagnostic({ code: "resource_limit_reached", resource: error.resource, current: error.current, limit: error.limit });
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
43
66
|
const id = this.nextStreamId;
|
|
44
67
|
this.nextStreamId += 2;
|
|
45
68
|
const s = new YamuxStream(this, id, "init");
|
|
46
69
|
this.streams.set(id, s);
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
try {
|
|
71
|
+
await s.open();
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.onStreamClosed(id);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
49
78
|
}
|
|
50
79
|
// getStream returns the stream for an ID, if any.
|
|
51
80
|
getStream(id) {
|
|
@@ -55,6 +84,57 @@ export class YamuxSession {
|
|
|
55
84
|
async writeRaw(chunk) {
|
|
56
85
|
await this.conn.write(chunk);
|
|
57
86
|
}
|
|
87
|
+
outboundFrameBytes() {
|
|
88
|
+
return this.limits.preferredOutboundFrameBytes;
|
|
89
|
+
}
|
|
90
|
+
releaseReceiveBytes(bytes) {
|
|
91
|
+
this.sessionReceiveBytes = Math.max(0, this.sessionReceiveBytes - Math.max(0, bytes));
|
|
92
|
+
}
|
|
93
|
+
// probeLiveness performs a correlated yamux PING SYN/ACK round trip.
|
|
94
|
+
async probeLiveness(timeoutMs = 10_000) {
|
|
95
|
+
if (this.activeProbe != null)
|
|
96
|
+
return await this.activeProbe;
|
|
97
|
+
const probe = this.startLivenessProbe(timeoutMs);
|
|
98
|
+
this.activeProbe = probe;
|
|
99
|
+
try {
|
|
100
|
+
return await probe;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (this.activeProbe === probe)
|
|
104
|
+
this.activeProbe = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async startLivenessProbe(timeoutMs) {
|
|
108
|
+
if (this.closed)
|
|
109
|
+
throw new Error("session closed");
|
|
110
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
111
|
+
throw new RangeError("timeoutMs must be positive");
|
|
112
|
+
let opaque = this.nextPingId >>> 0;
|
|
113
|
+
do {
|
|
114
|
+
opaque = this.nextPingId++ >>> 0;
|
|
115
|
+
if (this.nextPingId > 0xffffffff)
|
|
116
|
+
this.nextPingId = 1;
|
|
117
|
+
} while (opaque === 0 || this.pingWaiters.has(opaque));
|
|
118
|
+
const startedAt = monotonicMilliseconds();
|
|
119
|
+
return await new Promise((resolve, reject) => {
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
this.pingWaiters.delete(opaque);
|
|
122
|
+
reject(new Error("yamux ping timeout"));
|
|
123
|
+
this.close();
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
timer?.unref?.();
|
|
126
|
+
this.pingWaiters.set(opaque, { startedAt, resolve, reject, timer });
|
|
127
|
+
const hdr = encodeHeader({ type: TYPE_PING, flags: FLAG_SYN, streamId: 0, length: opaque });
|
|
128
|
+
void this.writeRaw(hdr).catch((err) => {
|
|
129
|
+
const waiter = this.pingWaiters.get(opaque);
|
|
130
|
+
if (waiter == null)
|
|
131
|
+
return;
|
|
132
|
+
this.pingWaiters.delete(opaque);
|
|
133
|
+
clearTimeout(waiter.timer);
|
|
134
|
+
reject(err);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
58
138
|
// sendRst sends a reset frame and removes the stream.
|
|
59
139
|
async sendRst(id) {
|
|
60
140
|
const hdr = encodeHeader({ type: TYPE_WINDOW_UPDATE, flags: FLAG_RST, streamId: id, length: 0 });
|
|
@@ -102,7 +182,11 @@ export class YamuxSession {
|
|
|
102
182
|
onStreamEstablished(_streamId) { }
|
|
103
183
|
// onStreamClosed removes the stream and wakes any per-stream waiters.
|
|
104
184
|
onStreamClosed(streamId) {
|
|
185
|
+
const stream = this.streams.get(streamId);
|
|
186
|
+
if (stream != null)
|
|
187
|
+
this.releaseReceiveBytes(stream.releaseBufferedReceiveBytes());
|
|
105
188
|
this.streams.delete(streamId);
|
|
189
|
+
this.inboundStreams.delete(streamId);
|
|
106
190
|
this.notifySendWindow(streamId);
|
|
107
191
|
}
|
|
108
192
|
// close terminates the session and resets all streams.
|
|
@@ -112,6 +196,11 @@ export class YamuxSession {
|
|
|
112
196
|
this.closed = true;
|
|
113
197
|
this.conn.close();
|
|
114
198
|
this.wakeSendWindowWaiters();
|
|
199
|
+
for (const waiter of this.pingWaiters.values()) {
|
|
200
|
+
clearTimeout(waiter.timer);
|
|
201
|
+
waiter.reject(new Error("session closed"));
|
|
202
|
+
}
|
|
203
|
+
this.pingWaiters.clear();
|
|
115
204
|
const streams = Array.from(this.streams.values());
|
|
116
205
|
this.streams.clear();
|
|
117
206
|
for (const s of streams)
|
|
@@ -134,12 +223,47 @@ export class YamuxSession {
|
|
|
134
223
|
return;
|
|
135
224
|
}
|
|
136
225
|
if (h.type === TYPE_DATA) {
|
|
137
|
-
if (
|
|
226
|
+
if (h.length > this.limits.maxFrameBytes) {
|
|
227
|
+
this.close();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (h.streamId === 0) {
|
|
231
|
+
this.close();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const existing = this.streams.get(h.streamId);
|
|
235
|
+
if (existing == null) {
|
|
236
|
+
const inboundAllowed = (h.flags & FLAG_SYN) !== 0 && this.isInboundStreamIdValid(h.streamId);
|
|
237
|
+
const resourceAllowed = this.streams.size < this.limits.maxActiveStreams && this.inboundStreams.size < this.limits.maxInboundStreams;
|
|
238
|
+
if (!inboundAllowed || !resourceAllowed || h.length > this.limits.maxStreamReceiveBytes) {
|
|
239
|
+
await this.reader.discardExactly(h.length);
|
|
240
|
+
this.diagnostic({
|
|
241
|
+
code: "stream_rejected",
|
|
242
|
+
resource: !resourceAllowed ? "inbound_streams" : "stream_receive_bytes",
|
|
243
|
+
current: !resourceAllowed ? this.inboundStreams.size : h.length,
|
|
244
|
+
limit: !resourceAllowed ? this.limits.maxInboundStreams : this.limits.maxStreamReceiveBytes,
|
|
245
|
+
});
|
|
246
|
+
await this.sendRst(h.streamId);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (existing != null && existing.bufferedReceiveBytes() + h.length > this.limits.maxStreamReceiveBytes) {
|
|
251
|
+
await this.reader.discardExactly(h.length);
|
|
252
|
+
this.diagnostic({ code: "resource_limit_reached", resource: "stream_receive_bytes", current: existing.bufferedReceiveBytes() + h.length, limit: this.limits.maxStreamReceiveBytes });
|
|
253
|
+
await this.sendRst(h.streamId);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (this.sessionReceiveBytes + h.length > this.limits.maxSessionReceiveBytes) {
|
|
257
|
+
this.diagnostic({ code: "resource_limit_reached", resource: "session_receive_bytes", current: this.sessionReceiveBytes + h.length, limit: this.limits.maxSessionReceiveBytes });
|
|
138
258
|
this.close();
|
|
139
259
|
return;
|
|
140
260
|
}
|
|
141
261
|
const data = h.length > 0 ? await this.reader.readExactly(h.length) : new Uint8Array();
|
|
142
|
-
|
|
262
|
+
if (data.length > 0)
|
|
263
|
+
this.sessionReceiveBytes += data.length;
|
|
264
|
+
const accepted = await this.handleDataFrame(h.streamId, h.flags, data);
|
|
265
|
+
if (!accepted)
|
|
266
|
+
this.releaseReceiveBytes(data.length);
|
|
143
267
|
continue;
|
|
144
268
|
}
|
|
145
269
|
if (h.type === TYPE_WINDOW_UPDATE) {
|
|
@@ -168,30 +292,43 @@ export class YamuxSession {
|
|
|
168
292
|
await this.writeRaw(hdr);
|
|
169
293
|
return;
|
|
170
294
|
}
|
|
295
|
+
if ((flags & FLAG_ACK) !== 0) {
|
|
296
|
+
const waiter = this.pingWaiters.get(opaque >>> 0);
|
|
297
|
+
if (waiter == null)
|
|
298
|
+
return;
|
|
299
|
+
this.pingWaiters.delete(opaque >>> 0);
|
|
300
|
+
clearTimeout(waiter.timer);
|
|
301
|
+
waiter.resolve(Math.max(0, monotonicMilliseconds() - waiter.startedAt));
|
|
302
|
+
}
|
|
171
303
|
}
|
|
172
304
|
async handleDataFrame(streamId, flags, data) {
|
|
173
305
|
if (streamId === 0) {
|
|
174
306
|
this.close();
|
|
175
|
-
return;
|
|
307
|
+
return false;
|
|
176
308
|
}
|
|
177
309
|
let s = this.streams.get(streamId);
|
|
178
310
|
if (s == null) {
|
|
179
311
|
if ((flags & FLAG_SYN) !== 0) {
|
|
180
312
|
if (!this.isInboundStreamIdValid(streamId)) {
|
|
181
313
|
await this.sendRst(streamId);
|
|
182
|
-
return;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (this.streams.size >= this.limits.maxActiveStreams || this.inboundStreams.size >= this.limits.maxInboundStreams || data.length > this.limits.maxStreamReceiveBytes) {
|
|
317
|
+
await this.sendRst(streamId);
|
|
318
|
+
return false;
|
|
183
319
|
}
|
|
184
320
|
s = new YamuxStream(this, streamId, "synReceived");
|
|
185
321
|
this.streams.set(streamId, s);
|
|
322
|
+
this.inboundStreams.add(streamId);
|
|
186
323
|
await s.open();
|
|
187
324
|
this.onIncomingStream?.(s);
|
|
188
325
|
}
|
|
189
326
|
else {
|
|
190
327
|
await this.sendRst(streamId);
|
|
191
|
-
return;
|
|
328
|
+
return false;
|
|
192
329
|
}
|
|
193
330
|
}
|
|
194
|
-
s.onData(data, flags);
|
|
331
|
+
return s.onData(data, flags);
|
|
195
332
|
}
|
|
196
333
|
async handleWindowUpdateFrame(streamId, flags, delta) {
|
|
197
334
|
if (streamId === 0) {
|
|
@@ -205,8 +342,13 @@ export class YamuxSession {
|
|
|
205
342
|
await this.sendRst(streamId);
|
|
206
343
|
return;
|
|
207
344
|
}
|
|
345
|
+
if (this.streams.size >= this.limits.maxActiveStreams || this.inboundStreams.size >= this.limits.maxInboundStreams) {
|
|
346
|
+
await this.sendRst(streamId);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
208
349
|
s = new YamuxStream(this, streamId, "synReceived");
|
|
209
350
|
this.streams.set(streamId, s);
|
|
351
|
+
this.inboundStreams.add(streamId);
|
|
210
352
|
await s.open();
|
|
211
353
|
this.onIncomingStream?.(s);
|
|
212
354
|
}
|
|
@@ -215,7 +357,9 @@ export class YamuxSession {
|
|
|
215
357
|
return;
|
|
216
358
|
}
|
|
217
359
|
}
|
|
218
|
-
s.onWindowUpdate(delta, flags)
|
|
360
|
+
if (!s.onWindowUpdate(delta, flags)) {
|
|
361
|
+
this.close();
|
|
362
|
+
}
|
|
219
363
|
}
|
|
220
364
|
isInboundStreamIdValid(streamId) {
|
|
221
365
|
// yamux uses stream ID parity to identify the initiator:
|
|
@@ -225,4 +369,39 @@ export class YamuxSession {
|
|
|
225
369
|
// When we are the server, the peer is the client and must initiate odd IDs.
|
|
226
370
|
return (streamId & 1) === (this.client ? 0 : 1);
|
|
227
371
|
}
|
|
372
|
+
diagnostic(event) {
|
|
373
|
+
try {
|
|
374
|
+
this.onDiagnostic?.(event);
|
|
375
|
+
}
|
|
376
|
+
catch { /* Diagnostics cannot affect transport behavior. */ }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function normalizeYamuxLimits(input) {
|
|
380
|
+
const maxFrameBytes = input?.maxFrameBytes ?? DEFAULT_YAMUX_LIMITS.maxFrameBytes;
|
|
381
|
+
const limits = {
|
|
382
|
+
maxActiveStreams: input?.maxActiveStreams ?? DEFAULT_YAMUX_LIMITS.maxActiveStreams,
|
|
383
|
+
maxInboundStreams: input?.maxInboundStreams ?? DEFAULT_YAMUX_LIMITS.maxInboundStreams,
|
|
384
|
+
maxFrameBytes,
|
|
385
|
+
preferredOutboundFrameBytes: input?.preferredOutboundFrameBytes ?? Math.min(DEFAULT_YAMUX_LIMITS.preferredOutboundFrameBytes, maxFrameBytes),
|
|
386
|
+
maxStreamReceiveBytes: input?.maxStreamReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxStreamReceiveBytes,
|
|
387
|
+
maxSessionReceiveBytes: input?.maxSessionReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxSessionReceiveBytes,
|
|
388
|
+
};
|
|
389
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
390
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
391
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
392
|
+
}
|
|
393
|
+
if (limits.maxInboundStreams > limits.maxActiveStreams)
|
|
394
|
+
throw new RangeError("maxInboundStreams must not exceed maxActiveStreams");
|
|
395
|
+
if (limits.preferredOutboundFrameBytes > limits.maxFrameBytes)
|
|
396
|
+
throw new RangeError("preferredOutboundFrameBytes must not exceed maxFrameBytes");
|
|
397
|
+
if (limits.maxFrameBytes > limits.maxStreamReceiveBytes)
|
|
398
|
+
throw new RangeError("maxFrameBytes must not exceed maxStreamReceiveBytes");
|
|
399
|
+
if (limits.maxStreamReceiveBytes < DEFAULT_YAMUX_LIMITS.maxStreamReceiveBytes)
|
|
400
|
+
throw new RangeError("maxStreamReceiveBytes must cover the 256 KiB initial stream window");
|
|
401
|
+
if (limits.maxStreamReceiveBytes > limits.maxSessionReceiveBytes)
|
|
402
|
+
throw new RangeError("maxStreamReceiveBytes must not exceed maxSessionReceiveBytes");
|
|
403
|
+
return Object.freeze(limits);
|
|
404
|
+
}
|
|
405
|
+
function monotonicMilliseconds() {
|
|
406
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
228
407
|
}
|
package/dist/yamux/stream.d.ts
CHANGED
|
@@ -11,14 +11,21 @@ export declare class YamuxStream {
|
|
|
11
11
|
private recvQueueBytes;
|
|
12
12
|
private readWaiters;
|
|
13
13
|
private error;
|
|
14
|
+
private resetTask;
|
|
15
|
+
private writeChain;
|
|
16
|
+
private finalized;
|
|
14
17
|
constructor(session: YamuxSession, id: number, state: StreamState);
|
|
15
18
|
open(): Promise<void>;
|
|
16
|
-
onData(data: Uint8Array, flags: number):
|
|
17
|
-
onWindowUpdate(delta: number, flags: number):
|
|
19
|
+
onData(data: Uint8Array, flags: number): boolean;
|
|
20
|
+
onWindowUpdate(delta: number, flags: number): boolean;
|
|
18
21
|
read(): Promise<Uint8Array | null>;
|
|
19
22
|
write(data: Uint8Array): Promise<void>;
|
|
23
|
+
private writeSerial;
|
|
20
24
|
close(): Promise<void>;
|
|
21
|
-
reset(err: Error): void
|
|
25
|
+
reset(err: Error): Promise<void>;
|
|
26
|
+
private fail;
|
|
27
|
+
bufferedReceiveBytes(): number;
|
|
28
|
+
releaseBufferedReceiveBytes(): number;
|
|
22
29
|
private processFlags;
|
|
23
30
|
private finalizeClosed;
|
|
24
31
|
private shiftRecv;
|
package/dist/yamux/stream.js
CHANGED
|
@@ -22,6 +22,9 @@ export class YamuxStream {
|
|
|
22
22
|
readWaiters = [];
|
|
23
23
|
// Terminal error (reset/overflow) for the stream.
|
|
24
24
|
error = null;
|
|
25
|
+
resetTask;
|
|
26
|
+
writeChain = Promise.resolve();
|
|
27
|
+
finalized = false;
|
|
25
28
|
constructor(session, id, state) {
|
|
26
29
|
this.session = session;
|
|
27
30
|
this.id = id;
|
|
@@ -33,26 +36,35 @@ export class YamuxStream {
|
|
|
33
36
|
}
|
|
34
37
|
// onData handles inbound DATA frames and updates receive window.
|
|
35
38
|
onData(data, flags) {
|
|
36
|
-
this.processFlags(flags);
|
|
37
|
-
if (
|
|
38
|
-
return;
|
|
39
|
-
if (data.length >
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
this.processFlags(flags & ~FLAG_FIN);
|
|
40
|
+
if (this.state === "reset" || this.state === "closed")
|
|
41
|
+
return false;
|
|
42
|
+
if (data.length > 0) {
|
|
43
|
+
if (data.length > this.recvWindow) {
|
|
44
|
+
void this.reset(new Error("recv window exceeded"));
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
this.recvWindow -= data.length;
|
|
48
|
+
this.recvQueue.push(data);
|
|
49
|
+
this.recvQueueBytes += data.length;
|
|
42
50
|
}
|
|
43
|
-
this.
|
|
44
|
-
this.recvQueue.push(data);
|
|
45
|
-
this.recvQueueBytes += data.length;
|
|
51
|
+
this.processFlags(flags & FLAG_FIN);
|
|
46
52
|
const ws = this.readWaiters;
|
|
47
53
|
this.readWaiters = [];
|
|
48
54
|
for (const w of ws)
|
|
49
55
|
w();
|
|
56
|
+
return true;
|
|
50
57
|
}
|
|
51
58
|
// onWindowUpdate applies flow-control credits from peer.
|
|
52
59
|
onWindowUpdate(delta, flags) {
|
|
53
60
|
this.processFlags(flags);
|
|
54
|
-
|
|
61
|
+
const credit = delta >>> 0;
|
|
62
|
+
if (credit > DEFAULT_MAX_STREAM_WINDOW - this.sendWindow) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
this.sendWindow += credit;
|
|
55
66
|
this.session.notifySendWindow(this.id);
|
|
67
|
+
return true;
|
|
56
68
|
}
|
|
57
69
|
// read resolves with the next data chunk, null on EOF, or throws on reset/errors.
|
|
58
70
|
async read() {
|
|
@@ -62,7 +74,10 @@ export class YamuxStream {
|
|
|
62
74
|
const b = this.shiftRecv();
|
|
63
75
|
if (b != null) {
|
|
64
76
|
this.recvQueueBytes -= b.length;
|
|
77
|
+
this.session.releaseReceiveBytes(b.length);
|
|
65
78
|
await this.sendWindowUpdate();
|
|
79
|
+
if (this.recvQueueBytes === 0 && this.state === "closed")
|
|
80
|
+
this.finalizeClosed();
|
|
66
81
|
return b;
|
|
67
82
|
}
|
|
68
83
|
if (this.state === "closed" || this.state === "remoteClose")
|
|
@@ -72,11 +87,17 @@ export class YamuxStream {
|
|
|
72
87
|
}
|
|
73
88
|
// write sends DATA frames, respecting the send window.
|
|
74
89
|
async write(data) {
|
|
90
|
+
const payload = data.slice();
|
|
91
|
+
const write = this.writeChain.then(() => this.writeSerial(payload));
|
|
92
|
+
this.writeChain = write.catch(() => { });
|
|
93
|
+
await write;
|
|
94
|
+
}
|
|
95
|
+
async writeSerial(data) {
|
|
75
96
|
this.ensureWritable();
|
|
76
97
|
let off = 0;
|
|
77
98
|
while (off < data.length) {
|
|
78
99
|
this.ensureWritable();
|
|
79
|
-
const chunk = data.subarray(off);
|
|
100
|
+
const chunk = data.subarray(off, off + this.session.outboundFrameBytes());
|
|
80
101
|
const allowed = await this.waitForSendWindow(chunk.length);
|
|
81
102
|
this.ensureWritable();
|
|
82
103
|
const sendChunk = chunk.subarray(0, allowed);
|
|
@@ -111,26 +132,42 @@ export class YamuxStream {
|
|
|
111
132
|
await this.session.writeRaw(hdr);
|
|
112
133
|
}
|
|
113
134
|
finally {
|
|
114
|
-
if (wasRemoteClose)
|
|
135
|
+
if (wasRemoteClose && this.recvQueueBytes === 0)
|
|
115
136
|
this.finalizeClosed();
|
|
116
137
|
}
|
|
117
138
|
}
|
|
118
139
|
// reset tears down the stream and notifies the peer.
|
|
119
140
|
reset(err) {
|
|
141
|
+
if (this.resetTask != null)
|
|
142
|
+
return this.resetTask;
|
|
143
|
+
if (!this.fail(err))
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
this.resetTask = this.session.sendRst(this.id);
|
|
146
|
+
return this.resetTask;
|
|
147
|
+
}
|
|
148
|
+
fail(err) {
|
|
120
149
|
if (this.state === "reset")
|
|
121
|
-
return;
|
|
150
|
+
return false;
|
|
122
151
|
this.state = "reset";
|
|
123
152
|
this.error = err;
|
|
124
153
|
// Drop any buffered data to free memory on terminal errors.
|
|
125
|
-
this.
|
|
126
|
-
this.recvQueueHead = 0;
|
|
127
|
-
this.recvQueueBytes = 0;
|
|
154
|
+
this.session.releaseReceiveBytes(this.releaseBufferedReceiveBytes());
|
|
128
155
|
const ws = this.readWaiters;
|
|
129
156
|
this.readWaiters = [];
|
|
130
157
|
for (const w of ws)
|
|
131
158
|
w();
|
|
132
159
|
this.session.notifySendWindow(this.id);
|
|
133
|
-
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
bufferedReceiveBytes() {
|
|
163
|
+
return this.recvQueueBytes;
|
|
164
|
+
}
|
|
165
|
+
releaseBufferedReceiveBytes() {
|
|
166
|
+
const bytes = this.recvQueueBytes;
|
|
167
|
+
this.recvQueue.length = 0;
|
|
168
|
+
this.recvQueueHead = 0;
|
|
169
|
+
this.recvQueueBytes = 0;
|
|
170
|
+
return bytes;
|
|
134
171
|
}
|
|
135
172
|
// processFlags updates the state machine for ACK/FIN/RST.
|
|
136
173
|
processFlags(flags) {
|
|
@@ -142,7 +179,8 @@ export class YamuxStream {
|
|
|
142
179
|
if ((flags & FLAG_FIN) !== 0) {
|
|
143
180
|
if (this.state === "localClose") {
|
|
144
181
|
this.state = "closed";
|
|
145
|
-
this.
|
|
182
|
+
if (this.recvQueueBytes === 0)
|
|
183
|
+
this.finalizeClosed();
|
|
146
184
|
}
|
|
147
185
|
else if (this.state === "established" || this.state === "synSent" || this.state === "synReceived") {
|
|
148
186
|
this.state = "remoteClose";
|
|
@@ -153,10 +191,14 @@ export class YamuxStream {
|
|
|
153
191
|
w();
|
|
154
192
|
}
|
|
155
193
|
if ((flags & FLAG_RST) !== 0) {
|
|
156
|
-
this.
|
|
194
|
+
if (this.fail(new Error("rst")))
|
|
195
|
+
this.session.onStreamClosed(this.id);
|
|
157
196
|
}
|
|
158
197
|
}
|
|
159
198
|
finalizeClosed() {
|
|
199
|
+
if (this.finalized)
|
|
200
|
+
return;
|
|
201
|
+
this.finalized = true;
|
|
160
202
|
const ws = this.readWaiters;
|
|
161
203
|
this.readWaiters = [];
|
|
162
204
|
for (const w of ws)
|