quico 0.3.0 → 0.7.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 +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/wt_stream.js
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// A WebTransport stream, as a Node stream — one for both sides
|
|
17
|
+
// ============================================================
|
|
18
|
+
//
|
|
19
|
+
// The server (h3_server) and the client (webtransport.js) build their
|
|
20
|
+
// streams here, so they behave alike: a Duplex for a bidirectional stream,
|
|
21
|
+
// a Readable for the peer's unidirectional one ('uni-in'), a Writable for
|
|
22
|
+
// ours ('uni-out'). The client had classes of its own, which had none of
|
|
23
|
+
// what follows.
|
|
24
|
+
//
|
|
25
|
+
// Reading: what the peer sends is credited back to it as the application
|
|
26
|
+
// reads (_read), not as it arrives — as for a request body. The peer can be
|
|
27
|
+
// one stream window ahead of the reader, no more; a stream nobody read was
|
|
28
|
+
// taken in whole.
|
|
29
|
+
//
|
|
30
|
+
// Each direction can be given up on its own, with a code of the
|
|
31
|
+
// application's (draft-ietf-webtrans-http3 §4.4, mapped by wtErrorToH3):
|
|
32
|
+
// stream.resetStream(code) — stop sending: RESET_STREAM; what was written
|
|
33
|
+
// and not yet sent is dropped;
|
|
34
|
+
// stream.stopSending(code) — stop reading: STOP_SENDING; the peer stops.
|
|
35
|
+
// destroy() with a direction unfinished does the same with code 0 — the peer
|
|
36
|
+
// is told, as a browser is when a WebTransport stream is aborted or
|
|
37
|
+
// cancelled. It was told nothing: what was queued kept going out, and the
|
|
38
|
+
// peer waited for a FIN that would not come.
|
|
39
|
+
//
|
|
40
|
+
// The peer giving up on a direction (RESET_STREAM or STOP_SENDING, through
|
|
41
|
+
// stream._peerCancelled) ends that direction: 'peerReset' / 'peerStopSending'
|
|
42
|
+
// with the application's code (stream.streamErrorCode; null when the peer
|
|
43
|
+
// sent none). The other direction of a bidirectional stream goes on; the
|
|
44
|
+
// stream closes ('close') once both are over — with an Error carrying the
|
|
45
|
+
// code only to an 'error' listener, since a cancelled stream is ordinary
|
|
46
|
+
// traffic, not a crash.
|
|
47
|
+
//
|
|
48
|
+
// And the browser's shape too: stream.readable and stream.writable are WHATWG
|
|
49
|
+
// ReadableStream / WritableStream views of it, as a browser's
|
|
50
|
+
// WebTransportBidirectionalStream has them — so code written against the
|
|
51
|
+
// browser API runs here: getReader(), getWriter(), pipeTo(); reader.cancel()
|
|
52
|
+
// and writer.abort() with a WebTransportError({ streamErrorCode }) send
|
|
53
|
+
// STOP_SENDING / RESET_STREAM with that code; a stream the peer reset or
|
|
54
|
+
// stopped fails reads or writes with a WebTransportError carrying its code.
|
|
55
|
+
// The reader reads only when asked, so one that does not read holds the peer
|
|
56
|
+
// back as a Node consumer does (the client's classes read with on('data'),
|
|
57
|
+
// which drained the stream whatever the reader did). Node's own converters
|
|
58
|
+
// (Duplex.toWeb) would not do for a bidirectional stream: their reader ends
|
|
59
|
+
// only once our side has finished writing too, where the browser's ends when
|
|
60
|
+
// the peer finishes. Node's stream utilities — pipe(), finished(),
|
|
61
|
+
// pipeline() — are unaffected by the two names.
|
|
62
|
+
|
|
63
|
+
import { Duplex, Readable, Writable } from 'node:stream';
|
|
64
|
+
import { wtErrorToH3, h3ErrorToWt } from './h3.js';
|
|
65
|
+
import { wt_stream_header, creditAsRead } from './utils.js';
|
|
66
|
+
|
|
67
|
+
// quic: the QUIC connection. kind: 'bidi', 'uni-in' or 'uni-out'. onClose:
|
|
68
|
+
// called once, when the stream is done with — for the owner's bookkeeping.
|
|
69
|
+
// prefixLength: the bytes of WebTransport prefix we sent on it ourselves (on a
|
|
70
|
+
// stream we opened), which are not the application's in its statistics.
|
|
71
|
+
// sched: its session's wtScheduling (below) — where its sending part goes in
|
|
72
|
+
// QUIC's scheduler. sessionId: its session's (the CONNECT stream's) ID.
|
|
73
|
+
function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, sessionId) {
|
|
74
|
+
var sends = kind !== 'uni-in', reads = kind !== 'uni-out';
|
|
75
|
+
var sendDone = !sends; // the FIN queued, or the direction reset
|
|
76
|
+
var recvDone = !reads; // the FIN received, or reading stopped
|
|
77
|
+
var finSent = false;
|
|
78
|
+
// For getStats(), as the browser's streams report them.
|
|
79
|
+
var stats = { bytesWritten: 0, bytesSent: 0, bytesAcknowledged: 0, bytesReceived: 0 };
|
|
80
|
+
prefixLength = prefixLength || 0;
|
|
81
|
+
|
|
82
|
+
function write(chunk, encoding, cb) {
|
|
83
|
+
if (sendDone) { cb(); return; } // reset: nothing more goes out
|
|
84
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
|
|
85
|
+
stats.bytesWritten += chunk.byteLength;
|
|
86
|
+
quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
|
|
87
|
+
quic.whenStreamSent(wtStreamId, stream.writableHighWaterMark, cb); // backpressure
|
|
88
|
+
}
|
|
89
|
+
// The FIN queued; 'committed' once it and every byte are acknowledged.
|
|
90
|
+
function final(cb) {
|
|
91
|
+
if (!sendDone) {
|
|
92
|
+
sendDone = true; finSent = true;
|
|
93
|
+
quic.sendStream(wtStreamId, new Uint8Array(0), true);
|
|
94
|
+
if (quic.whenStreamCommitted) quic.whenStreamCommitted(wtStreamId, function () {
|
|
95
|
+
if (sendReset || stream._writeStopped) return;
|
|
96
|
+
committed = true;
|
|
97
|
+
stream.emit('committed');
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
cb();
|
|
101
|
+
}
|
|
102
|
+
var committed = false;
|
|
103
|
+
|
|
104
|
+
// What QUIC has sent and had acknowledged of it, in the application's bytes.
|
|
105
|
+
// A stream whose every byte and FIN were acknowledged is gone from QUIC's
|
|
106
|
+
// table — all of it, then.
|
|
107
|
+
function sendStats() {
|
|
108
|
+
var st = quic.context && quic.context.send_streams[wtStreamId];
|
|
109
|
+
if (st) {
|
|
110
|
+
stats.bytesSent = Math.max(stats.bytesSent, Math.min(stats.bytesWritten, st.max_sent_offset - prefixLength));
|
|
111
|
+
var acked = st.acked_ranges.length >= 2 && st.acked_ranges[0] === 0 ? st.acked_ranges[1] - prefixLength : 0;
|
|
112
|
+
stats.bytesAcknowledged = Math.max(stats.bytesAcknowledged, Math.min(stats.bytesWritten, acked));
|
|
113
|
+
} else if (finSent) {
|
|
114
|
+
stats.bytesSent = stats.bytesAcknowledged = stats.bytesWritten;
|
|
115
|
+
}
|
|
116
|
+
return { bytesWritten: stats.bytesWritten, bytesSent: stats.bytesSent, bytesAcknowledged: stats.bytesAcknowledged };
|
|
117
|
+
}
|
|
118
|
+
function recvStats() {
|
|
119
|
+
return { bytesReceived: stats.bytesReceived, bytesRead: stats.bytesReceived - (stream.readableLength || 0) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The browser's send order and group (WebTransportSendStream), handed to
|
|
123
|
+
// QUIC's scheduler as they change (see wtScheduling).
|
|
124
|
+
var sendOrder = 0, sendGroup = null;
|
|
125
|
+
function sendProps(target) {
|
|
126
|
+
Object.defineProperty(target, 'sendOrder', { configurable: true, enumerable: true,
|
|
127
|
+
get: function () { return sendOrder; }, set: function (v) { sendOrder = Number(v) || 0; schedule(); } });
|
|
128
|
+
Object.defineProperty(target, 'sendGroup', { configurable: true, enumerable: true,
|
|
129
|
+
get: function () { return sendGroup; }, set: function (g) {
|
|
130
|
+
if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
|
|
131
|
+
sendGroup = g || null;
|
|
132
|
+
if (sendGroup && sendGroup._add) sendGroup._add(sendStats);
|
|
133
|
+
schedule();
|
|
134
|
+
} });
|
|
135
|
+
}
|
|
136
|
+
function schedule() {
|
|
137
|
+
if (sends && sched && !sendDone) quic.setStreamPriority(wtStreamId, sched.forStream(sendGroup, sendOrder));
|
|
138
|
+
}
|
|
139
|
+
schedule();
|
|
140
|
+
|
|
141
|
+
var stream = kind === 'bidi' ? new Duplex({ read: function () {}, write: write, final: final })
|
|
142
|
+
: kind === 'uni-in' ? new Readable({ read: function () {} })
|
|
143
|
+
: new Writable({ write: write, final: final });
|
|
144
|
+
|
|
145
|
+
stream._streamId = wtStreamId;
|
|
146
|
+
stream._sessionId = sessionId;
|
|
147
|
+
|
|
148
|
+
// The code what is still open goes with when the stream is destroyed:
|
|
149
|
+
// destroy(err) with err.streamErrorCode — the browser's
|
|
150
|
+
// WebTransportError({ streamErrorCode }) — that application code;
|
|
151
|
+
// its session gone (_sessionGone) — WT_SESSION_GONE, an HTTP/3 code of
|
|
152
|
+
// its own, not an application one (draft-ietf-webtrans-http3 §4.6:
|
|
153
|
+
// "MUST reset ... with WT_SESSION_GONE");
|
|
154
|
+
// otherwise application code 0.
|
|
155
|
+
// destroy() always sent code 0: the application's code was dropped, and a
|
|
156
|
+
// session's end reached its streams as an ordinary cancel.
|
|
157
|
+
var closeH3Code = null;
|
|
158
|
+
var WT_SESSION_GONE = 0x170d7b68;
|
|
159
|
+
// At once, not when 'close' comes a tick later: the session's end is
|
|
160
|
+
// often the connection's (a WebTransport client's own), whose
|
|
161
|
+
// CONNECTION_CLOSE would go first and these with it.
|
|
162
|
+
stream._sessionGone = function () {
|
|
163
|
+
if (closeH3Code === null) closeH3Code = WT_SESSION_GONE;
|
|
164
|
+
if (!sendDone) { sendDone = true; sendReset = true; resetSending(closeH3Code, 0); }
|
|
165
|
+
if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, closeH3Code); }
|
|
166
|
+
};
|
|
167
|
+
var baseDestroy = stream.destroy;
|
|
168
|
+
stream.destroy = function (err) {
|
|
169
|
+
if (closeH3Code === null && err && typeof err.streamErrorCode === 'number') closeH3Code = wtErrorToH3(err.streamErrorCode);
|
|
170
|
+
return baseDestroy.apply(this, arguments);
|
|
171
|
+
};
|
|
172
|
+
stream.streamErrorCode = null;
|
|
173
|
+
// Its QUIC stream ID and its session's — read-only, for logs and maps.
|
|
174
|
+
Object.defineProperty(stream, 'id', { enumerable: true, get: function () { return wtStreamId; } });
|
|
175
|
+
Object.defineProperty(stream, 'sessionId', { enumerable: true, get: function () { return sessionId; } });
|
|
176
|
+
|
|
177
|
+
// The WHATWG views (see above), made on first use — with what the
|
|
178
|
+
// browser's WebTransportReceiveStream / WebTransportSendStream add.
|
|
179
|
+
var webReadable = null, webWritable = null;
|
|
180
|
+
if (reads) {
|
|
181
|
+
Object.defineProperty(stream, 'readable', { configurable: true, get: function () {
|
|
182
|
+
if (!webReadable) {
|
|
183
|
+
webReadable = webReadableOf(stream);
|
|
184
|
+
webReadable.getStats = function () { return Promise.resolve(recvStats()); };
|
|
185
|
+
}
|
|
186
|
+
return webReadable;
|
|
187
|
+
} });
|
|
188
|
+
}
|
|
189
|
+
if (sends) {
|
|
190
|
+
sendProps(stream);
|
|
191
|
+
Object.defineProperty(stream, 'writable', { configurable: true, get: function () {
|
|
192
|
+
if (!webWritable) {
|
|
193
|
+
webWritable = webWritableOf(stream);
|
|
194
|
+
sendProps(webWritable);
|
|
195
|
+
webWritable.getStats = function () { return Promise.resolve(sendStats()); };
|
|
196
|
+
}
|
|
197
|
+
return webWritable;
|
|
198
|
+
} });
|
|
199
|
+
}
|
|
200
|
+
stream.getStats = function () {
|
|
201
|
+
return Promise.resolve(Object.assign({}, sends ? sendStats() : {}, reads ? recvStats() : {}));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Credit for what the application has read (utils' creditAsRead): a
|
|
205
|
+
// stream nobody reads holds one window, not a window and Node's buffer.
|
|
206
|
+
var received = reads ? creditAsRead(stream, function (n) { quic.creditStream(wtStreamId, n); }) : null;
|
|
207
|
+
stream._pushData = function (data) {
|
|
208
|
+
if (recvDone) return;
|
|
209
|
+
if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
210
|
+
stats.bytesReceived += data.byteLength;
|
|
211
|
+
received(data.byteLength);
|
|
212
|
+
stream.push(data);
|
|
213
|
+
};
|
|
214
|
+
stream._endData = function () {
|
|
215
|
+
if (recvDone) return;
|
|
216
|
+
recvDone = true;
|
|
217
|
+
stream.push(null);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// The stream closes once both directions are over — each ended by a FIN
|
|
221
|
+
// (sent, or received and read to the end), a reset, or a stop, by either
|
|
222
|
+
// side, in any order. That was checked at some of those points and not
|
|
223
|
+
// others: a stream whose reading we had stopped stayed open for good once
|
|
224
|
+
// its writing finished after.
|
|
225
|
+
var readStopped = false; // we stopped reading it (stopSending)
|
|
226
|
+
var sendReset = false; // we reset it (resetStream; or the stream went with its direction open)
|
|
227
|
+
stream._readReset = false; // the peer reset it (RESET_STREAM)
|
|
228
|
+
stream._writeStopped = false; // the peer stopped reading it (STOP_SENDING)
|
|
229
|
+
|
|
230
|
+
// Each direction's state, read-only — null for the one a unidirectional
|
|
231
|
+
// stream does not have. A 'drain' says the writer may write; sendState
|
|
232
|
+
// says whether what it writes still goes anywhere.
|
|
233
|
+
// sendState: 'open', 'finished' (our FIN is queued), 'reset' (we gave up
|
|
234
|
+
// sending), 'stopped' (the peer asked us to stop: STOP_SENDING)
|
|
235
|
+
// recvState: 'open', 'finished' (the peer's FIN arrived), 'reset' (the
|
|
236
|
+
// peer gave up sending: RESET_STREAM), 'stopped' (we stopped
|
|
237
|
+
// reading: stopSending)
|
|
238
|
+
Object.defineProperty(stream, 'sendState', { enumerable: true, get: function () {
|
|
239
|
+
if (!sends) return null;
|
|
240
|
+
return stream._writeStopped ? 'stopped' : sendReset ? 'reset' : committed ? 'committed' : finSent ? 'finished' : 'open';
|
|
241
|
+
} });
|
|
242
|
+
Object.defineProperty(stream, 'recvState', { enumerable: true, get: function () {
|
|
243
|
+
if (!reads) return null;
|
|
244
|
+
return stream._readReset ? 'reset' : readStopped ? 'stopped' : recvDone ? 'finished' : 'open';
|
|
245
|
+
} });
|
|
246
|
+
function readOver() { return !reads || readStopped || stream._readReset || stream.readableEnded; }
|
|
247
|
+
function writeOver() { return !sends || sendDone; }
|
|
248
|
+
function closeIfDone(err) {
|
|
249
|
+
if (!stream.destroyed && readOver() && writeOver()) stream.destroy(err || null);
|
|
250
|
+
}
|
|
251
|
+
if (reads) stream.on('end', function () { closeIfDone(); });
|
|
252
|
+
if (sends) stream.on('finish', function () { closeIfDone(); });
|
|
253
|
+
|
|
254
|
+
// options.reliableSize: the first bytes the application wrote that still
|
|
255
|
+
// reach the peer, whole, before the reset (RESET_STREAM_AT, draft-ietf-
|
|
256
|
+
// quic-reliable-stream-reset) — a MOQT subgroup's header, say. More than
|
|
257
|
+
// was written: what was written. Only with a peer that takes
|
|
258
|
+
// RESET_STREAM_AT (stream.reliableResetSupported); without, a RESET_STREAM.
|
|
259
|
+
//
|
|
260
|
+
// After end() too (RFC 9000 §3.1: "Data Sent" → "Reset Sent"), for as long
|
|
261
|
+
// as what was written is not all acknowledged: a MOQT subgroup past its
|
|
262
|
+
// delivery timeout, ended but still on its way, is dropped instead of sent
|
|
263
|
+
// to the end. It did nothing once end() had been called — the FIN queued
|
|
264
|
+
// counted as the direction being over — and the stream went on being sent
|
|
265
|
+
// and resent until it had all arrived. Everything acknowledged already:
|
|
266
|
+
// nothing to reset (false). Returns whether a reset was sent.
|
|
267
|
+
stream.resetStream = function (code, options) {
|
|
268
|
+
if (!sends || sendReset || stream._writeStopped) return false;
|
|
269
|
+
var reliable = options && options.reliableSize != null ? Number(options.reliableSize) : 0;
|
|
270
|
+
if (!(reliable >= 0) || Math.floor(reliable) !== reliable) throw new RangeError('reliableSize must be a non-negative integer, got ' + (options && options.reliableSize));
|
|
271
|
+
var wasOpen = !sendDone;
|
|
272
|
+
if (!resetSending(wtErrorToH3(code || 0), reliable) && !wasOpen) return false; // all delivered
|
|
273
|
+
sendDone = true;
|
|
274
|
+
sendReset = true;
|
|
275
|
+
if (stream._onClosePending) stream._onClosePending(true); // given up before it was open
|
|
276
|
+
if (wasOpen) closeIfDone();
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
// The sending part reset. Our stream's header (signal value and session
|
|
280
|
+
// ID) is always delivered (draft-ietf-webtrans-http3 §4.4: "MUST use the
|
|
281
|
+
// RESET_STREAM_AT frame with a Reliable Size set to at least the size of
|
|
282
|
+
// the WebTransport header") — without it the peer cannot tell which
|
|
283
|
+
// session the reset is for — and `reliable` bytes of the application's
|
|
284
|
+
// after it. QUIC sends RESET_STREAM_AT when the peer takes it, RESET_STREAM
|
|
285
|
+
// otherwise.
|
|
286
|
+
function resetSending(h3code, reliable) {
|
|
287
|
+
return quic.resetStream(wtStreamId, h3code, prefixLength + (reliable || 0));
|
|
288
|
+
}
|
|
289
|
+
Object.defineProperty(stream, 'reliableResetSupported', { enumerable: true,
|
|
290
|
+
get: function () { return !!quic.resetStreamAtSupported; } });
|
|
291
|
+
stream.stopSending = function (code) {
|
|
292
|
+
if (recvDone) return;
|
|
293
|
+
recvDone = true;
|
|
294
|
+
readStopped = true;
|
|
295
|
+
quic.stopSending(wtStreamId, wtErrorToH3(code || 0)); // and its held credit goes
|
|
296
|
+
closeIfDone();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// The peer gave up on one direction, with the application's code:
|
|
300
|
+
// RESET_STREAM ends our reading, STOP_SENDING our sending (QUIC has
|
|
301
|
+
// answered that one with RESET_STREAM already). The other direction goes
|
|
302
|
+
// on, as in the browser — a bidirectional stream's two halves are
|
|
303
|
+
// independent; the peer giving up on one used to destroy the stream, and
|
|
304
|
+
// reset the half it had not touched. 'peerReset' / 'peerStopSending' (code)
|
|
305
|
+
// say which ended, stream.streamErrorCode holds the code, and the stream
|
|
306
|
+
// closes once both directions are over: at once when the other one already
|
|
307
|
+
// is — then with an Error carrying the code, for an 'error' listener only
|
|
308
|
+
// (a cancelled stream is ordinary traffic, not a crash).
|
|
309
|
+
//
|
|
310
|
+
// Reading reset: what was read stands, and the readable side ends (its
|
|
311
|
+
// WHATWG view fails with the code instead — a reset is not a FIN). Sending
|
|
312
|
+
// stopped: writes from then on are dropped (the WHATWG view's fail).
|
|
313
|
+
stream._peerCancelled = function (how, h3code) {
|
|
314
|
+
if (stream.destroyed) return;
|
|
315
|
+
var stop = how === 'stop_sending';
|
|
316
|
+
if (stop ? stream._writeStopped : stream._readReset) return;
|
|
317
|
+
// The RESET_STREAM that answers our own STOP_SENDING (RFC 9000 §3.5): our
|
|
318
|
+
// doing, not the peer giving up — reported as 'peerReset' it was.
|
|
319
|
+
if (!stop && readStopped) return;
|
|
320
|
+
stream.streamErrorCode = h3ErrorToWt(h3code);
|
|
321
|
+
if (stop) { sendDone = true; stream._writeStopped = true; }
|
|
322
|
+
else { recvDone = true; stream._readReset = true; }
|
|
323
|
+
stream.emit(stop ? 'peerStopSending' : 'peerReset', stream.streamErrorCode);
|
|
324
|
+
if (readOver() && writeOver()) {
|
|
325
|
+
var err = new Error(stop ? 'the peer stopped reading this stream' : 'the peer reset this stream');
|
|
326
|
+
err.code = stop ? 'ERR_WEBTRANSPORT_STOP_SENDING' : 'ERR_WEBTRANSPORT_STREAM_RESET';
|
|
327
|
+
err.streamErrorCode = stream.streamErrorCode;
|
|
328
|
+
closeIfDone(stream.listenerCount('error') > 0 ? err : null);
|
|
329
|
+
} else if (!stop) {
|
|
330
|
+
stream.push(null);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
stream.once('close', function () {
|
|
335
|
+
if (sends) sendStats(); // the last figures, while QUIC still has them
|
|
336
|
+
if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
|
|
337
|
+
var code = closeH3Code !== null ? closeH3Code : wtErrorToH3(0);
|
|
338
|
+
if (!sendDone) { sendDone = true; sendReset = true; resetSending(code, 0); }
|
|
339
|
+
if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, code); }
|
|
340
|
+
// Still waiting to be opened (openWtStream): given up if its sending was
|
|
341
|
+
// cut short; ended normally (end() before it opened), its bytes still go
|
|
342
|
+
// once it opens.
|
|
343
|
+
if (stream._onClosePending) stream._onClosePending(sendReset);
|
|
344
|
+
quic.releaseStreamCredit(wtStreamId);
|
|
345
|
+
if (onClose) onClose();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return stream;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- The WHATWG views ----
|
|
352
|
+
|
|
353
|
+
// The browser's WebTransportError: what a stream's reads and writes fail with
|
|
354
|
+
// when the peer reset or stopped it (source 'stream', its streamErrorCode),
|
|
355
|
+
// and what an application passes to cancel() / abort() to send a code.
|
|
356
|
+
//
|
|
357
|
+
// Takes either form: the spec's (message, { source, streamErrorCode }) or the
|
|
358
|
+
// one browsers implement, a single { message, source, streamErrorCode }.
|
|
359
|
+
class WebTransportError extends Error {
|
|
360
|
+
constructor(message, options) {
|
|
361
|
+
if (message && typeof message === 'object') { options = message; message = message.message; }
|
|
362
|
+
super(message || '');
|
|
363
|
+
this.name = 'WebTransportError';
|
|
364
|
+
this.source = (options && options.source) || 'stream';
|
|
365
|
+
this.streamErrorCode = options && options.streamErrorCode != null ? options.streamErrorCode : null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The code a cancel() or abort() reason carries; 0 for any other reason.
|
|
370
|
+
function codeOf(reason) {
|
|
371
|
+
return reason && typeof reason.streamErrorCode === 'number' ? reason.streamErrorCode : 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Why the stream ended early: the peer's code if it gave one.
|
|
375
|
+
function endedError(stream) {
|
|
376
|
+
return new WebTransportError(stream.streamErrorCode != null ? 'the peer gave up on this stream' : 'stream closed',
|
|
377
|
+
{ streamErrorCode: stream.streamErrorCode });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function webReadableOf(stream) {
|
|
381
|
+
return new ReadableStream({
|
|
382
|
+
pull: function (controller) {
|
|
383
|
+
return new Promise(function (resolve) {
|
|
384
|
+
function attempt() {
|
|
385
|
+
var chunk = stream.read();
|
|
386
|
+
if (chunk !== null) { controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)); return done(); }
|
|
387
|
+
if (stream._readReset) { controller.error(endedError(stream)); return done(); }
|
|
388
|
+
if (stream.readableEnded) { controller.close(); return done(); }
|
|
389
|
+
if (stream.destroyed) { controller.error(endedError(stream)); return done(); }
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function done() {
|
|
393
|
+
stream.removeListener('readable', attempt);
|
|
394
|
+
stream.removeListener('end', attempt);
|
|
395
|
+
stream.removeListener('close', attempt);
|
|
396
|
+
resolve();
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
if (attempt()) return;
|
|
400
|
+
stream.on('readable', attempt);
|
|
401
|
+
stream.on('end', attempt);
|
|
402
|
+
stream.on('close', attempt);
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
cancel: function (reason) { stream.stopSending(codeOf(reason)); }
|
|
406
|
+
}, { highWaterMark: 0 }); // read only when the reader asks: backpressure
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function webWritableOf(stream) {
|
|
410
|
+
return new WritableStream({
|
|
411
|
+
write: function (chunk) {
|
|
412
|
+
return new Promise(function (resolve, reject) {
|
|
413
|
+
if (stream.destroyed || stream._writeStopped) { reject(endedError(stream)); return; }
|
|
414
|
+
stream.write(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength), function (err) {
|
|
415
|
+
if (err || stream.destroyed || stream._writeStopped) reject(endedError(stream)); else resolve();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
close: function () {
|
|
420
|
+
return new Promise(function (resolve, reject) {
|
|
421
|
+
if (stream.destroyed || stream._writeStopped) { reject(endedError(stream)); return; }
|
|
422
|
+
stream.end(resolve);
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
abort: function (reason) { stream.resetStream(codeOf(reason)); }
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---- Opening a stream of our own ----
|
|
430
|
+
//
|
|
431
|
+
// The browser's way, for both sides (the server's res.create…Stream, the
|
|
432
|
+
// client's wt.create…Stream): options { sendOrder, sendGroup,
|
|
433
|
+
// waitUntilAvailable }. Without waitUntilAvailable, a stream past the peer's
|
|
434
|
+
// MAX_STREAMS is refused at once — null here; the browser rejects — rather
|
|
435
|
+
// than opened to wait unseen; with it, it is opened and its bytes wait for
|
|
436
|
+
// the peer to allow more. make(streamId, kind, prefixLength) builds the
|
|
437
|
+
// stream object, recorded by its owner.
|
|
438
|
+
//
|
|
439
|
+
// Opened is not open yet: past the peer's limit the stream waits for it to
|
|
440
|
+
// allow more. stream.pending says so (as net.Socket's does) until it is
|
|
441
|
+
// open; then 'ready', and callback(null, stream) — or callback(err) if it
|
|
442
|
+
// closes first (its session or connection did). Both after this returns,
|
|
443
|
+
// as Node's are, whether or not it had to wait. The client's Promise is
|
|
444
|
+
// settled from the same callback: resolved once the stream is open, as the
|
|
445
|
+
// browser's is — it was resolved at once, open or not.
|
|
446
|
+
function openWtStream(h3, quic, sessionId, bidi, options, make, callback) {
|
|
447
|
+
options = options || {};
|
|
448
|
+
if (!options.waitUntilAvailable && !quic.canOpenStream(bidi)) return null;
|
|
449
|
+
var streamId = h3.createWebTransportStream(sessionId, bidi);
|
|
450
|
+
var stream = make(streamId, bidi ? 'bidi' : 'uni-out', wt_stream_header(bidi, sessionId).byteLength);
|
|
451
|
+
if (options.sendGroup) stream.sendGroup = options.sendGroup;
|
|
452
|
+
if (options.sendOrder != null) stream.sendOrder = options.sendOrder;
|
|
453
|
+
stream.pending = true;
|
|
454
|
+
var settled = false;
|
|
455
|
+
function settle(err) {
|
|
456
|
+
if (settled) return;
|
|
457
|
+
settled = true;
|
|
458
|
+
stream._onClosePending = null;
|
|
459
|
+
stream.pending = false;
|
|
460
|
+
if (err) { err = new Error('the stream closed before it was open'); err.code = 'ERR_WEBTRANSPORT_STREAM_NOT_OPEN'; }
|
|
461
|
+
process.nextTick(function () {
|
|
462
|
+
if (!err) stream.emit('ready');
|
|
463
|
+
if (typeof callback === 'function') callback(err || null, stream);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
stream._onClosePending = function (aborted) { if (aborted) settle(true); };
|
|
467
|
+
quic.whenStreamPermitted(streamId, settle);
|
|
468
|
+
return stream;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- A session's place in QUIC's scheduler ----
|
|
472
|
+
//
|
|
473
|
+
// The browser's model (W3C WebTransport, sendGroup / sendOrder): the streams
|
|
474
|
+
// of a send group share the group's turns, the highest sendOrder with
|
|
475
|
+
// something to send taking them, equal orders taking turns; the groups share
|
|
476
|
+
// the session's sending equally. A stream in no group is in its session's own
|
|
477
|
+
// group. QUIC's scheduler has exactly that as `group` and `order`
|
|
478
|
+
// (setStreamPriority); this maps one onto the other. Every stream and group
|
|
479
|
+
// of the session is at the urgency of its CONNECT stream — RFC 9218's
|
|
480
|
+
// `priority` on the CONNECT request, or what the application set for it —
|
|
481
|
+
// taken as each stream is placed.
|
|
482
|
+
//
|
|
483
|
+
// Datagrams go first by default, ahead of any stream (sendDatagram). Sent
|
|
484
|
+
// with a sendGroup or a sendOrder other than 0, they take their place among
|
|
485
|
+
// the streams instead, as the browser's createWritable({ sendOrder,
|
|
486
|
+
// sendGroup }) has it: a member of that group (or of the session's), at
|
|
487
|
+
// that order.
|
|
488
|
+
function wtScheduling(quic, sessionStreamId) {
|
|
489
|
+
var sessionGroup = {};
|
|
490
|
+
function urgency() {
|
|
491
|
+
var p = quic.getStreamPriority(sessionStreamId);
|
|
492
|
+
return p && p.urgency != null ? p.urgency : 3;
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
forStream: function (group, order) {
|
|
496
|
+
return { urgency: urgency(), incremental: true, group: group || sessionGroup, order: Number(order) || 0 };
|
|
497
|
+
},
|
|
498
|
+
forDatagram: function (options) {
|
|
499
|
+
if (!options || (!options.sendGroup && !Number(options.sendOrder))) return null;
|
|
500
|
+
return { urgency: urgency(), group: options.sendGroup || sessionGroup, order: Number(options.sendOrder) || 0 };
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---- A session's datagrams and statistics, for both sides ----
|
|
506
|
+
//
|
|
507
|
+
// The client's WebTransport and the server's res share these, so a session
|
|
508
|
+
// looks the same from either end. `session` is what they hand in:
|
|
509
|
+
// quic the QUIC connection (a getter on the client:
|
|
510
|
+
// made after the session object)
|
|
511
|
+
// connected() whether the session is up
|
|
512
|
+
// send(u8, options) its sendDatagram (options: sendOrder, sendGroup)
|
|
513
|
+
// maxDatagramSize() the largest payload it can send
|
|
514
|
+
// owner { maxAge, expired } — its datagrams in QUIC's
|
|
515
|
+
// queue: their age limit (outgoingMaxAge) and how
|
|
516
|
+
// many were dropped unsent (sendDatagram's owner)
|
|
517
|
+
// dgStats { droppedIncoming, expiredIncoming }
|
|
518
|
+
|
|
519
|
+
// The browser's WebTransportDatagramDuplexStream: readable, writable,
|
|
520
|
+
// createWritable({ sendOrder, sendGroup }), maxDatagramSize, the age limits
|
|
521
|
+
// and high-water marks. The readable reads only when asked; past
|
|
522
|
+
// incomingHighWaterMark the oldest goes, and one older than incomingMaxAge
|
|
523
|
+
// when read is skipped. outgoingMaxAge is this session's alone (a session
|
|
524
|
+
// on a shared connection used to set the whole connection's).
|
|
525
|
+
function createDatagrams(session) {
|
|
526
|
+
var queue = [], controller = null, pull = null, readable = null, writable = null;
|
|
527
|
+
var dg = { incomingMaxAge: null, incomingHighWaterMark: 256, outgoingHighWaterMark: 256 };
|
|
528
|
+
|
|
529
|
+
function deliver() {
|
|
530
|
+
if (!pull) return;
|
|
531
|
+
var now = Date.now();
|
|
532
|
+
while (queue.length > 0) {
|
|
533
|
+
var d = queue.shift();
|
|
534
|
+
if (dg.incomingMaxAge != null && now - d.t > dg.incomingMaxAge) { session.dgStats.expiredIncoming++; continue; }
|
|
535
|
+
controller.enqueue(d.data);
|
|
536
|
+
var p = pull; pull = null; p();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function makeWritable(options) {
|
|
541
|
+
var w = new WritableStream({
|
|
542
|
+
write: function (chunk) {
|
|
543
|
+
if (!session.connected()) throw new WebTransportError('the session is not connected', { source: 'session' });
|
|
544
|
+
var u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
545
|
+
if (u8.byteLength <= session.maxDatagramSize()) session.send(u8, w); // its sendOrder / sendGroup, as they are now
|
|
546
|
+
}
|
|
547
|
+
}, { highWaterMark: dg.outgoingHighWaterMark });
|
|
548
|
+
w.sendOrder = (options && options.sendOrder) || 0;
|
|
549
|
+
w.sendGroup = (options && options.sendGroup) || null;
|
|
550
|
+
return w;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
Object.defineProperty(dg, 'readable', { enumerable: true, get: function () {
|
|
554
|
+
if (!readable) {
|
|
555
|
+
readable = new ReadableStream({
|
|
556
|
+
start: function (c) { controller = c; },
|
|
557
|
+
pull: function () { return new Promise(function (resolve) { pull = resolve; deliver(); }); }
|
|
558
|
+
}, { highWaterMark: 0 });
|
|
559
|
+
}
|
|
560
|
+
return readable;
|
|
561
|
+
} });
|
|
562
|
+
Object.defineProperty(dg, 'writable', { enumerable: true, get: function () {
|
|
563
|
+
return writable || (writable = makeWritable());
|
|
564
|
+
} });
|
|
565
|
+
Object.defineProperty(dg, 'maxDatagramSize', { enumerable: true, get: function () { return session.maxDatagramSize(); } });
|
|
566
|
+
Object.defineProperty(dg, 'outgoingMaxAge', { enumerable: true,
|
|
567
|
+
get: function () { return session.owner.maxAge; },
|
|
568
|
+
set: function (v) { session.owner.maxAge = v == null ? null : Number(v); } });
|
|
569
|
+
dg.createWritable = function (options) { return makeWritable(options); };
|
|
570
|
+
// A datagram arrived: for the readable, once anything reads it.
|
|
571
|
+
dg._push = function (data) {
|
|
572
|
+
if (!readable) return;
|
|
573
|
+
queue.push({ data: data, t: Date.now() });
|
|
574
|
+
while (queue.length > dg.incomingHighWaterMark) { queue.shift(); session.dgStats.droppedIncoming++; }
|
|
575
|
+
deliver();
|
|
576
|
+
};
|
|
577
|
+
dg._close = function () {
|
|
578
|
+
queue = [];
|
|
579
|
+
if (controller) { try { controller.close(); } catch (e) {} }
|
|
580
|
+
if (pull) { var p = pull; pull = null; p(); }
|
|
581
|
+
};
|
|
582
|
+
return dg;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// The browser's getStats(): the connection's figures (QUIC's getStats — shared
|
|
586
|
+
// by every session on it, as in the browser), and the session's datagrams.
|
|
587
|
+
function sessionStats(session) {
|
|
588
|
+
var q = session.quic && session.quic.getStats ? session.quic.getStats() : {};
|
|
589
|
+
var out = { timestamp: Date.now() };
|
|
590
|
+
['bytesSent', 'packetsSent', 'packetsLost', 'bytesReceived', 'packetsReceived', 'smoothedRtt', 'rttVariation',
|
|
591
|
+
'minRtt', 'estimatedSendRate', 'atSendCapacity', 'congestionWindow', 'bytesInFlight'].forEach(function (k) {
|
|
592
|
+
out[k] = q[k] !== undefined ? q[k] : null;
|
|
593
|
+
});
|
|
594
|
+
out.datagrams = {
|
|
595
|
+
droppedIncoming: session.dgStats.droppedIncoming,
|
|
596
|
+
expiredIncoming: session.dgStats.expiredIncoming,
|
|
597
|
+
expiredOutgoing: session.owner.expired || 0,
|
|
598
|
+
lostOutgoing: null // a datagram's packet lost is not traced back to it
|
|
599
|
+
};
|
|
600
|
+
return Promise.resolve(out);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// The browser's WebTransportSendGroup: streams that share a send order space.
|
|
604
|
+
// getStats() sums its streams' figures.
|
|
605
|
+
function createSendGroup() {
|
|
606
|
+
var members = new Set();
|
|
607
|
+
return {
|
|
608
|
+
_add: function (f) { members.add(f); },
|
|
609
|
+
_remove: function (f) { members.delete(f); },
|
|
610
|
+
getStats: function () {
|
|
611
|
+
var t = { bytesWritten: 0, bytesSent: 0, bytesAcknowledged: 0 };
|
|
612
|
+
members.forEach(function (f) { var s = f(); t.bytesWritten += s.bytesWritten; t.bytesSent += s.bytesSent; t.bytesAcknowledged += s.bytesAcknowledged; });
|
|
613
|
+
return Promise.resolve(t);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, WebTransportError };
|