quico 0.4.0 → 0.8.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 +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -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 +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/wt_stream.js
ADDED
|
@@ -0,0 +1,1075 @@
|
|
|
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
|
+
// A group of another session's (the browser's: InvalidStateError).
|
|
131
|
+
if (g != null && g._owner && stream._owner && g._owner !== stream._owner) {
|
|
132
|
+
throw new DOMException('this send group belongs to another WebTransport session', 'InvalidStateError');
|
|
133
|
+
}
|
|
134
|
+
if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
|
|
135
|
+
sendGroup = g || null;
|
|
136
|
+
if (sendGroup && sendGroup._add) sendGroup._add(sendStats);
|
|
137
|
+
schedule();
|
|
138
|
+
} });
|
|
139
|
+
}
|
|
140
|
+
function schedule() {
|
|
141
|
+
if (sends && sched && !sendDone) quic.setStreamPriority(wtStreamId, sched.forStream(sendGroup, sendOrder));
|
|
142
|
+
}
|
|
143
|
+
schedule();
|
|
144
|
+
|
|
145
|
+
var stream = kind === 'bidi' ? new Duplex({ read: function () {}, write: write, final: final })
|
|
146
|
+
: kind === 'uni-in' ? new Readable({ read: function () {} })
|
|
147
|
+
: new Writable({ write: write, final: final });
|
|
148
|
+
|
|
149
|
+
stream._streamId = wtStreamId;
|
|
150
|
+
stream._sessionId = sessionId;
|
|
151
|
+
if (kind === 'bidi') Object.defineProperty(stream, '_wtBidirectional', { value: true }); // instanceof WebTransportBidirectionalStream
|
|
152
|
+
|
|
153
|
+
// The code what is still open goes with when the stream is destroyed:
|
|
154
|
+
// destroy(err) with err.streamErrorCode — the browser's
|
|
155
|
+
// WebTransportError({ streamErrorCode }) — that application code;
|
|
156
|
+
// its session gone (_sessionGone) — WT_SESSION_GONE, an HTTP/3 code of
|
|
157
|
+
// its own, not an application one (draft-ietf-webtrans-http3 §4.6:
|
|
158
|
+
// "MUST reset ... with WT_SESSION_GONE");
|
|
159
|
+
// otherwise application code 0.
|
|
160
|
+
// destroy() always sent code 0: the application's code was dropped, and a
|
|
161
|
+
// session's end reached its streams as an ordinary cancel.
|
|
162
|
+
var closeH3Code = null;
|
|
163
|
+
var WT_SESSION_GONE = 0x170d7b68;
|
|
164
|
+
// At once, not when 'close' comes a tick later: the session's end is
|
|
165
|
+
// often the connection's (a WebTransport client's own), whose
|
|
166
|
+
// CONNECTION_CLOSE would go first and these with it.
|
|
167
|
+
//
|
|
168
|
+
// `err` is what the browser's views fail with (the WHATWG readable and
|
|
169
|
+
// writable): the session's own error — the very object its `closed`
|
|
170
|
+
// rejects with when the session was lost, an AbortError when we closed it
|
|
171
|
+
// (W3C WebTransport, "cleanup"). Without one, a WebTransportError, source
|
|
172
|
+
// 'session'. The views failed only on the next read or write, and a
|
|
173
|
+
// reader or writer waiting on `closed` waited for good.
|
|
174
|
+
stream._sessionGone = function (err) {
|
|
175
|
+
if (!stream._sessionError) stream._sessionError = err || new WebTransportError('the session is gone', { source: 'session' });
|
|
176
|
+
failViews(stream._sessionError);
|
|
177
|
+
if (closeH3Code === null) closeH3Code = WT_SESSION_GONE;
|
|
178
|
+
if (!sendDone) { sendDone = true; sendReset = true; resetSending(closeH3Code, 0); }
|
|
179
|
+
if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, closeH3Code); }
|
|
180
|
+
};
|
|
181
|
+
var baseDestroy = stream.destroy;
|
|
182
|
+
stream.destroy = function (err) {
|
|
183
|
+
if (closeH3Code === null && err && typeof err.streamErrorCode === 'number') closeH3Code = wtErrorToH3(err.streamErrorCode);
|
|
184
|
+
return baseDestroy.apply(this, arguments);
|
|
185
|
+
};
|
|
186
|
+
stream.streamErrorCode = null;
|
|
187
|
+
stream._bytesWritten = function () { return stats.bytesWritten; }; // a writer's commit()
|
|
188
|
+
// Its QUIC stream ID and its session's — read-only, for logs and maps.
|
|
189
|
+
Object.defineProperty(stream, 'id', { enumerable: true, get: function () { return wtStreamId; } });
|
|
190
|
+
Object.defineProperty(stream, 'sessionId', { enumerable: true, get: function () { return sessionId; } });
|
|
191
|
+
|
|
192
|
+
// The WHATWG views (see above), made on first use — with what the
|
|
193
|
+
// browser's WebTransportReceiveStream / WebTransportSendStream add.
|
|
194
|
+
//
|
|
195
|
+
// Each view fails at once when its direction is given up on — the peer's
|
|
196
|
+
// RESET_STREAM fails the readable, its STOP_SENDING the writable, the
|
|
197
|
+
// session's end both — so a reader or writer waiting on `closed` hears it,
|
|
198
|
+
// as in the browser; not only the next read or write. One made after that
|
|
199
|
+
// starts failed.
|
|
200
|
+
var webReadable = null, webWritable = null;
|
|
201
|
+
var readView = null, writeView = null; // { fail(err) } of each, once made
|
|
202
|
+
function failRead(err) { if (readView) readView.fail(err); }
|
|
203
|
+
function failWrite(err) { if (writeView) writeView.fail(err); }
|
|
204
|
+
function failViews(err) { failRead(err); failWrite(err); }
|
|
205
|
+
if (reads) {
|
|
206
|
+
Object.defineProperty(stream, 'readable', { configurable: true, get: function () {
|
|
207
|
+
if (!webReadable) {
|
|
208
|
+
readView = webReadableOf(stream, recvStats);
|
|
209
|
+
webReadable = readView.stream;
|
|
210
|
+
if (stream._sessionError) failRead(stream._sessionError);
|
|
211
|
+
else if (stream._readReset && !stream.readableLength) failRead(endedError(stream));
|
|
212
|
+
}
|
|
213
|
+
return webReadable;
|
|
214
|
+
} });
|
|
215
|
+
}
|
|
216
|
+
if (sends) {
|
|
217
|
+
sendProps(stream);
|
|
218
|
+
Object.defineProperty(stream, 'writable', { configurable: true, get: function () {
|
|
219
|
+
if (!webWritable) {
|
|
220
|
+
writeView = webWritableOf(stream, sendStats, quic, wtStreamId);
|
|
221
|
+
webWritable = writeView.stream;
|
|
222
|
+
sendProps(webWritable);
|
|
223
|
+
if (stream._sessionError) failWrite(stream._sessionError);
|
|
224
|
+
else if (stream._writeStopped) failWrite(endedError(stream));
|
|
225
|
+
}
|
|
226
|
+
return webWritable;
|
|
227
|
+
} });
|
|
228
|
+
}
|
|
229
|
+
stream.getStats = function () {
|
|
230
|
+
return Promise.resolve(Object.assign({}, sends ? sendStats() : {}, reads ? recvStats() : {}));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Credit for what the application has read (utils' creditAsRead): a
|
|
234
|
+
// stream nobody reads holds one window, not a window and Node's buffer.
|
|
235
|
+
var received = reads ? creditAsRead(stream, function (n) { quic.creditStream(wtStreamId, n); }) : null;
|
|
236
|
+
stream._pushData = function (data) {
|
|
237
|
+
if (recvDone) return;
|
|
238
|
+
if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
239
|
+
stats.bytesReceived += data.byteLength;
|
|
240
|
+
received(data.byteLength);
|
|
241
|
+
stream.push(data);
|
|
242
|
+
};
|
|
243
|
+
stream._endData = function () {
|
|
244
|
+
if (recvDone) return;
|
|
245
|
+
recvDone = true;
|
|
246
|
+
stream.push(null);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// The stream closes once both directions are over — each ended by a FIN
|
|
250
|
+
// (sent, or received and read to the end), a reset, or a stop, by either
|
|
251
|
+
// side, in any order. That was checked at some of those points and not
|
|
252
|
+
// others: a stream whose reading we had stopped stayed open for good once
|
|
253
|
+
// its writing finished after.
|
|
254
|
+
var readStopped = false; // we stopped reading it (stopSending)
|
|
255
|
+
var sendReset = false; // we reset it (resetStream; or the stream went with its direction open)
|
|
256
|
+
stream._readReset = false; // the peer reset it (RESET_STREAM)
|
|
257
|
+
stream._writeStopped = false; // the peer stopped reading it (STOP_SENDING)
|
|
258
|
+
|
|
259
|
+
// Each direction's state, read-only — null for the one a unidirectional
|
|
260
|
+
// stream does not have. A 'drain' says the writer may write; sendState
|
|
261
|
+
// says whether what it writes still goes anywhere.
|
|
262
|
+
// sendState: 'open', 'finished' (our FIN is queued), 'reset' (we gave up
|
|
263
|
+
// sending), 'stopped' (the peer asked us to stop: STOP_SENDING)
|
|
264
|
+
// recvState: 'open', 'finished' (the peer's FIN arrived), 'reset' (the
|
|
265
|
+
// peer gave up sending: RESET_STREAM), 'stopped' (we stopped
|
|
266
|
+
// reading: stopSending)
|
|
267
|
+
Object.defineProperty(stream, 'sendState', { enumerable: true, get: function () {
|
|
268
|
+
if (!sends) return null;
|
|
269
|
+
return stream._writeStopped ? 'stopped' : sendReset ? 'reset' : committed ? 'committed' : finSent ? 'finished' : 'open';
|
|
270
|
+
} });
|
|
271
|
+
Object.defineProperty(stream, 'recvState', { enumerable: true, get: function () {
|
|
272
|
+
if (!reads) return null;
|
|
273
|
+
return stream._readReset ? 'reset' : readStopped ? 'stopped' : recvDone ? 'finished' : 'open';
|
|
274
|
+
} });
|
|
275
|
+
function readOver() { return !reads || readStopped || stream._readReset || stream.readableEnded; }
|
|
276
|
+
function writeOver() { return !sends || sendDone; }
|
|
277
|
+
function closeIfDone(err) {
|
|
278
|
+
if (!stream.destroyed && readOver() && writeOver()) stream.destroy(err || null);
|
|
279
|
+
}
|
|
280
|
+
if (reads) stream.on('end', function () { closeIfDone(); });
|
|
281
|
+
if (sends) stream.on('finish', function () { closeIfDone(); });
|
|
282
|
+
|
|
283
|
+
// options.reliableSize: the first bytes the application wrote that still
|
|
284
|
+
// reach the peer, whole, before the reset (RESET_STREAM_AT, draft-ietf-
|
|
285
|
+
// quic-reliable-stream-reset) — a MOQT subgroup's header, say. More than
|
|
286
|
+
// was written: what was written. Only with a peer that takes
|
|
287
|
+
// RESET_STREAM_AT (stream.reliableResetSupported); without, a RESET_STREAM.
|
|
288
|
+
//
|
|
289
|
+
// After end() too (RFC 9000 §3.1: "Data Sent" → "Reset Sent"), for as long
|
|
290
|
+
// as what was written is not all acknowledged: a MOQT subgroup past its
|
|
291
|
+
// delivery timeout, ended but still on its way, is dropped instead of sent
|
|
292
|
+
// to the end. It did nothing once end() had been called — the FIN queued
|
|
293
|
+
// counted as the direction being over — and the stream went on being sent
|
|
294
|
+
// and resent until it had all arrived. Everything acknowledged already:
|
|
295
|
+
// nothing to reset (false). Returns whether a reset was sent.
|
|
296
|
+
stream.resetStream = function (code, options) {
|
|
297
|
+
if (!sends || sendReset || stream._writeStopped) return false;
|
|
298
|
+
var reliable = options && options.reliableSize != null ? Number(options.reliableSize) : 0;
|
|
299
|
+
if (!(reliable >= 0) || Math.floor(reliable) !== reliable) throw new RangeError('reliableSize must be a non-negative integer, got ' + (options && options.reliableSize));
|
|
300
|
+
var wasOpen = !sendDone;
|
|
301
|
+
if (!resetSending(wtErrorToH3(code || 0), reliable) && !wasOpen) return false; // all delivered
|
|
302
|
+
sendDone = true;
|
|
303
|
+
sendReset = true;
|
|
304
|
+
if (stream._onClosePending) stream._onClosePending(true); // given up before it was open
|
|
305
|
+
if (wasOpen) closeIfDone();
|
|
306
|
+
return true;
|
|
307
|
+
};
|
|
308
|
+
// The sending part reset. Our stream's header (signal value and session
|
|
309
|
+
// ID) is always delivered (draft-ietf-webtrans-http3 §4.4: "MUST use the
|
|
310
|
+
// RESET_STREAM_AT frame with a Reliable Size set to at least the size of
|
|
311
|
+
// the WebTransport header") — without it the peer cannot tell which
|
|
312
|
+
// session the reset is for — and `reliable` bytes of the application's
|
|
313
|
+
// after it. QUIC sends RESET_STREAM_AT when the peer takes it, RESET_STREAM
|
|
314
|
+
// otherwise.
|
|
315
|
+
function resetSending(h3code, reliable) {
|
|
316
|
+
return quic.resetStream(wtStreamId, h3code, prefixLength + (reliable || 0));
|
|
317
|
+
}
|
|
318
|
+
Object.defineProperty(stream, 'reliableResetSupported', { enumerable: true,
|
|
319
|
+
get: function () { return !!quic.resetStreamAtSupported; } });
|
|
320
|
+
stream.stopSending = function (code) {
|
|
321
|
+
if (recvDone) return;
|
|
322
|
+
recvDone = true;
|
|
323
|
+
readStopped = true;
|
|
324
|
+
quic.stopSending(wtStreamId, wtErrorToH3(code || 0)); // and its held credit goes
|
|
325
|
+
closeIfDone();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// The peer gave up on one direction, with the application's code:
|
|
329
|
+
// RESET_STREAM ends our reading, STOP_SENDING our sending (QUIC has
|
|
330
|
+
// answered that one with RESET_STREAM already). The other direction goes
|
|
331
|
+
// on, as in the browser — a bidirectional stream's two halves are
|
|
332
|
+
// independent; the peer giving up on one used to destroy the stream, and
|
|
333
|
+
// reset the half it had not touched. 'peerReset' / 'peerStopSending' (code)
|
|
334
|
+
// say which ended, stream.streamErrorCode holds the code, and the stream
|
|
335
|
+
// closes once both directions are over: at once when the other one already
|
|
336
|
+
// is — then with an Error carrying the code, for an 'error' listener only
|
|
337
|
+
// (a cancelled stream is ordinary traffic, not a crash).
|
|
338
|
+
//
|
|
339
|
+
// Reading reset: what was read stands, and the readable side ends (its
|
|
340
|
+
// WHATWG view fails with the code instead — a reset is not a FIN). Sending
|
|
341
|
+
// stopped: writes from then on are dropped (the WHATWG view's fail).
|
|
342
|
+
stream._peerCancelled = function (how, h3code) {
|
|
343
|
+
if (stream.destroyed) return;
|
|
344
|
+
// WT_SESSION_GONE is not the peer giving up on this stream: it is its
|
|
345
|
+
// session ending (draft-ietf-webtrans-http3 §4.6 — every stream of a
|
|
346
|
+
// closed session is reset or stopped with it). The stream ends as its
|
|
347
|
+
// session's streams end here (_sessionGone): both directions, the views
|
|
348
|
+
// failed with the session's error, closed without an 'error'. Taken as a
|
|
349
|
+
// cancel, it was destroyed with one whenever an 'error' listener was there
|
|
350
|
+
// — and pipe() adds one of its own, which re-throws: a server echoing a
|
|
351
|
+
// stream with s.pipe(s) crashed whenever a client closed its session.
|
|
352
|
+
if (h3code === WT_SESSION_GONE) {
|
|
353
|
+
stream._sessionGone();
|
|
354
|
+
stream.destroy();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
var stop = how === 'stop_sending';
|
|
358
|
+
if (stop ? stream._writeStopped : stream._readReset) return;
|
|
359
|
+
// The RESET_STREAM that answers our own STOP_SENDING (RFC 9000 §3.5): our
|
|
360
|
+
// doing, not the peer giving up — reported as 'peerReset' it was.
|
|
361
|
+
if (!stop && readStopped) return;
|
|
362
|
+
stream.streamErrorCode = h3ErrorToWt(h3code);
|
|
363
|
+
if (stop) { sendDone = true; stream._writeStopped = true; }
|
|
364
|
+
else { recvDone = true; stream._readReset = true; }
|
|
365
|
+
stream.emit(stop ? 'peerStopSending' : 'peerReset', stream.streamErrorCode);
|
|
366
|
+
// The writable fails at once. The readable too, unless bytes received
|
|
367
|
+
// before the reset are still unread — those a RESET_STREAM_AT sent
|
|
368
|
+
// reliably (its Reliable Size) must reach the reader whole: the view
|
|
369
|
+
// reads them, then fails (see its pull).
|
|
370
|
+
if (stop) failWrite(endedError(stream));
|
|
371
|
+
else if (!stream.readableLength) failRead(endedError(stream));
|
|
372
|
+
if (readOver() && writeOver()) {
|
|
373
|
+
var err = new Error(stop ? 'the peer stopped reading this stream' : 'the peer reset this stream');
|
|
374
|
+
err.code = stop ? 'ERR_WEBTRANSPORT_STOP_SENDING' : 'ERR_WEBTRANSPORT_STREAM_RESET';
|
|
375
|
+
err.streamErrorCode = stream.streamErrorCode;
|
|
376
|
+
closeIfDone(stream.listenerCount('error') > 0 ? err : null);
|
|
377
|
+
} else if (!stop) {
|
|
378
|
+
stream.push(null);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
stream.once('close', function () {
|
|
383
|
+
if (sends) sendStats(); // the last figures, while QUIC still has them
|
|
384
|
+
if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
|
|
385
|
+
var code = closeH3Code !== null ? closeH3Code : wtErrorToH3(0);
|
|
386
|
+
if (!sendDone) { sendDone = true; sendReset = true; resetSending(code, 0); }
|
|
387
|
+
if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, code); }
|
|
388
|
+
// Still waiting to be opened (openWtStream): given up if its sending was
|
|
389
|
+
// cut short; ended normally (end() before it opened), its bytes still go
|
|
390
|
+
// once it opens.
|
|
391
|
+
if (stream._onClosePending) stream._onClosePending(sendReset);
|
|
392
|
+
quic.releaseStreamCredit(wtStreamId);
|
|
393
|
+
if (onClose) onClose();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return stream;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---- The WHATWG views ----
|
|
400
|
+
|
|
401
|
+
// Made only here: the browser's interfaces have no constructor a page can
|
|
402
|
+
// call ("Illegal constructor"), and neither do these.
|
|
403
|
+
var INTERNAL = Symbol('quico.webtransport.internal');
|
|
404
|
+
function illegal() { throw new TypeError('Illegal constructor'); }
|
|
405
|
+
|
|
406
|
+
// WebIDL [Clamp] unsigned long: clamped to 0 – 2^32-1, rounded half to even.
|
|
407
|
+
function clampUint32(v) {
|
|
408
|
+
v = Number(v);
|
|
409
|
+
if (isNaN(v)) return 0;
|
|
410
|
+
v = Math.min(Math.max(v, 0), 4294967295);
|
|
411
|
+
var f = Math.floor(v), d = v - f;
|
|
412
|
+
return d > 0.5 || (d === 0.5 && f % 2 === 1) ? f + 1 : f;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// The browser's WebTransportError — a DOMException, as the spec has it (so
|
|
416
|
+
// `err instanceof DOMException`, and `err.name` is 'WebTransportError'): what
|
|
417
|
+
// a stream's reads and writes fail with when the peer reset or stopped it
|
|
418
|
+
// (source 'stream', its streamErrorCode), what a session's end fails things
|
|
419
|
+
// with (source 'session'), and what an application passes to cancel() /
|
|
420
|
+
// abort() to send a code. It was an Error.
|
|
421
|
+
//
|
|
422
|
+
// Takes either form: the spec's (message, { source, streamErrorCode }) or the
|
|
423
|
+
// one browsers implement, a single { message, source, streamErrorCode }. A
|
|
424
|
+
// Node code (err.code, 'ERR_…') is set with setErrorCode: a DOMException's
|
|
425
|
+
// own `code` is a read-only number.
|
|
426
|
+
class WebTransportError extends DOMException {
|
|
427
|
+
constructor(message, options) {
|
|
428
|
+
if (message && typeof message === 'object') { options = message; message = message.message; }
|
|
429
|
+
super(message === undefined || message === null ? '' : String(message), 'WebTransportError');
|
|
430
|
+
var source = options && options.source != null ? String(options.source) : 'stream';
|
|
431
|
+
if (source !== 'stream' && source !== 'session') throw new TypeError("WebTransportError source must be 'stream' or 'session', got " + JSON.stringify(source));
|
|
432
|
+
var code = options && options.streamErrorCode != null ? clampUint32(options.streamErrorCode) : null;
|
|
433
|
+
Object.defineProperty(this, '_wt', { value: { source: source, streamErrorCode: code } });
|
|
434
|
+
}
|
|
435
|
+
get source() { return this._wt.source; }
|
|
436
|
+
get streamErrorCode() { return this._wt.streamErrorCode; }
|
|
437
|
+
}
|
|
438
|
+
function setErrorCode(err, code) {
|
|
439
|
+
Object.defineProperty(err, 'code', { value: code, writable: true, configurable: true, enumerable: false });
|
|
440
|
+
return err;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// The browser's stream classes. A bidirectional stream here is a Node Duplex
|
|
444
|
+
// (with the browser's .readable and .writable), so WebTransportBidirectionalStream
|
|
445
|
+
// recognises it rather than being its class; the other two are the
|
|
446
|
+
// WHATWG streams themselves.
|
|
447
|
+
class WebTransportBidirectionalStream {
|
|
448
|
+
constructor() { illegal(); }
|
|
449
|
+
static [Symbol.hasInstance](o) { return o != null && o._wtBidirectional === true; }
|
|
450
|
+
}
|
|
451
|
+
class WebTransportReceiveStream extends ReadableStream {
|
|
452
|
+
constructor(token, source, strategy, stats) {
|
|
453
|
+
if (token !== INTERNAL) illegal();
|
|
454
|
+
super(source, strategy);
|
|
455
|
+
Object.defineProperty(this, '_wtStats', { value: stats });
|
|
456
|
+
}
|
|
457
|
+
/** { bytesReceived, bytesRead } */
|
|
458
|
+
getStats() { return Promise.resolve(this._wtStats()); }
|
|
459
|
+
}
|
|
460
|
+
class WebTransportSendStream extends WritableStream {
|
|
461
|
+
constructor(token, sink, strategy, hooks) {
|
|
462
|
+
if (token !== INTERNAL) illegal();
|
|
463
|
+
super(sink, strategy);
|
|
464
|
+
Object.defineProperty(this, '_wtHooks', { value: hooks });
|
|
465
|
+
}
|
|
466
|
+
/** A WebTransportWriter: the browser's writer, with atomicWrite() and commit(). */
|
|
467
|
+
getWriter() { return new WebTransportWriter(this); }
|
|
468
|
+
/** { bytesWritten, bytesSent, bytesAcknowledged } */
|
|
469
|
+
getStats() { return Promise.resolve(this._wtHooks.stats()); }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The browser's WebTransportWriter (getWriter() of a send stream):
|
|
473
|
+
// atomicWrite(chunk) written only if all of it fits the flow-control
|
|
474
|
+
// window as it is when its turn comes — otherwise not
|
|
475
|
+
// sent at all, and the Promise rejects (AbortError),
|
|
476
|
+
// with any atomic write queued behind it; a plain write
|
|
477
|
+
// queued behind it then fails the stream, as the spec
|
|
478
|
+
// has it. The stream itself goes on.
|
|
479
|
+
// commit() what was written so far is delivered even if the
|
|
480
|
+
// stream is aborted later: abort() then resets it with
|
|
481
|
+
// RESET_STREAM_AT, that many bytes reliable (a MOQT
|
|
482
|
+
// subgroup's header, say) — with a peer that takes it;
|
|
483
|
+
// without, a plain RESET_STREAM.
|
|
484
|
+
class WebTransportWriter extends WritableStreamDefaultWriter {
|
|
485
|
+
constructor(stream) {
|
|
486
|
+
super(stream);
|
|
487
|
+
Object.defineProperty(this, '_wtHooks', { value: stream && stream._wtHooks ? stream._wtHooks : null });
|
|
488
|
+
}
|
|
489
|
+
write(chunk) {
|
|
490
|
+
var h = this._wtHooks, p = super.write(chunk);
|
|
491
|
+
if (!h) return p;
|
|
492
|
+
h.plainPending++;
|
|
493
|
+
var done = function () { h.plainPending--; };
|
|
494
|
+
p.then(done, done);
|
|
495
|
+
return p;
|
|
496
|
+
}
|
|
497
|
+
atomicWrite(chunk) {
|
|
498
|
+
var h = this._wtHooks;
|
|
499
|
+
if (!h) return super.write(chunk);
|
|
500
|
+
var ticket = h.markAtomic(chunk);
|
|
501
|
+
var p = super.write(chunk);
|
|
502
|
+
return p.then(function () {
|
|
503
|
+
if (ticket.aborted) throw new DOMException('the chunk does not fit the flow-control window', 'AbortError');
|
|
504
|
+
}, function (err) { ticket.aborted = true; throw err; });
|
|
505
|
+
}
|
|
506
|
+
commit() {
|
|
507
|
+
if (this._wtHooks) this._wtHooks.commit();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// The browser's WebTransportSendGroup: streams (and datagram writables) that
|
|
512
|
+
// share a send-order space; getStats() sums its streams' figures.
|
|
513
|
+
class WebTransportSendGroup {
|
|
514
|
+
constructor(token, owner) {
|
|
515
|
+
if (token !== INTERNAL) illegal();
|
|
516
|
+
Object.defineProperty(this, '_owner', { value: owner || null });
|
|
517
|
+
Object.defineProperty(this, '_members', { value: new Set() });
|
|
518
|
+
}
|
|
519
|
+
_add(f) { this._members.add(f); }
|
|
520
|
+
_remove(f) { this._members.delete(f); }
|
|
521
|
+
getStats() {
|
|
522
|
+
var t = { bytesWritten: 0, bytesSent: 0, bytesAcknowledged: 0 };
|
|
523
|
+
this._members.forEach(function (f) { var s = f(); t.bytesWritten += s.bytesWritten; t.bytesSent += s.bytesSent; t.bytesAcknowledged += s.bytesAcknowledged; });
|
|
524
|
+
return Promise.resolve(t);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
class WebTransportDatagramsWritable extends WritableStream {
|
|
529
|
+
constructor(token, sink, strategy) {
|
|
530
|
+
if (token !== INTERNAL) illegal();
|
|
531
|
+
super(sink, strategy);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
class WebTransportDatagramDuplexStream {
|
|
535
|
+
constructor() { illegal(); }
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// The code a cancel() or abort() reason carries; 0 for any other reason.
|
|
539
|
+
function codeOf(reason) {
|
|
540
|
+
return reason && typeof reason.streamErrorCode === 'number' ? reason.streamErrorCode : 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Why the stream ended early: the peer's code if it gave one.
|
|
544
|
+
function endedError(stream) {
|
|
545
|
+
return new WebTransportError(stream.streamErrorCode != null ? 'the peer gave up on this stream' : 'stream closed',
|
|
546
|
+
{ streamErrorCode: stream.streamErrorCode });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// What a write into a WHATWG view may carry: a BufferSource — an ArrayBuffer
|
|
550
|
+
// or a view onto one, not shared memory — and not an empty one (as the
|
|
551
|
+
// browsers reject it; WPT sendstream-bad-chunk, datagram-bad-chunk). A string
|
|
552
|
+
// was turned into whatever its missing .buffer gave. Copied: the caller may
|
|
553
|
+
// reuse its buffer once write() returns.
|
|
554
|
+
function bufferSourceBytes(chunk, what) {
|
|
555
|
+
var u8 = null;
|
|
556
|
+
if (chunk instanceof ArrayBuffer) u8 = new Uint8Array(chunk);
|
|
557
|
+
else if (ArrayBuffer.isView(chunk) && !(typeof SharedArrayBuffer !== 'undefined' && chunk.buffer instanceof SharedArrayBuffer)) {
|
|
558
|
+
u8 = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
559
|
+
}
|
|
560
|
+
if (!u8) throw new TypeError(what + ' must be an ArrayBuffer or an ArrayBufferView');
|
|
561
|
+
if (u8.byteLength === 0) throw new TypeError(what + ' must not be empty');
|
|
562
|
+
return u8.slice();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Each returns { stream, fail(err) }: the WHATWG stream, and how its owner
|
|
566
|
+
// fails it from outside — the peer giving up on that direction, or the
|
|
567
|
+
// session's end — while nothing is reading or writing.
|
|
568
|
+
//
|
|
569
|
+
// The readable is a byte stream, as the browser's WebTransportReceiveStream:
|
|
570
|
+
// a BYOB reader (getReader({ mode: 'byob' })) fills its own buffer, what does
|
|
571
|
+
// not fit kept for the next read. A default reader gets each chunk in a
|
|
572
|
+
// buffer of its own — a copy: enqueue() takes the buffer over, and a chunk
|
|
573
|
+
// read here can share its memory with others.
|
|
574
|
+
function webReadableOf(stream, stats) {
|
|
575
|
+
var controller = null, failed = false;
|
|
576
|
+
var rs = new WebTransportReceiveStream(INTERNAL, {
|
|
577
|
+
type: 'bytes',
|
|
578
|
+
start: function (c) { controller = c; },
|
|
579
|
+
pull: function (controller) {
|
|
580
|
+
return new Promise(function (resolve) {
|
|
581
|
+
function attempt() {
|
|
582
|
+
if (failed) return done();
|
|
583
|
+
var chunk = stream.read();
|
|
584
|
+
if (chunk !== null) {
|
|
585
|
+
var req = controller.byobRequest;
|
|
586
|
+
if (req && req.view) {
|
|
587
|
+
var v = req.view, n = Math.min(v.byteLength, chunk.length);
|
|
588
|
+
new Uint8Array(v.buffer, v.byteOffset, n).set(chunk.subarray(0, n));
|
|
589
|
+
if (chunk.length > n) stream.unshift(chunk.subarray(n));
|
|
590
|
+
req.respond(n);
|
|
591
|
+
} else {
|
|
592
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
593
|
+
}
|
|
594
|
+
return done();
|
|
595
|
+
}
|
|
596
|
+
if (stream._readReset) { fail(endedError(stream)); return done(); }
|
|
597
|
+
if (stream.readableEnded) {
|
|
598
|
+
controller.close();
|
|
599
|
+
if (controller.byobRequest) controller.byobRequest.respond(0);
|
|
600
|
+
return done();
|
|
601
|
+
}
|
|
602
|
+
if (stream.destroyed) { fail(stream._sessionError || endedError(stream)); return done(); }
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
function done() {
|
|
606
|
+
stream.removeListener('readable', attempt);
|
|
607
|
+
stream.removeListener('end', attempt);
|
|
608
|
+
stream.removeListener('close', attempt);
|
|
609
|
+
resolve();
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
if (attempt()) return;
|
|
613
|
+
stream.on('readable', attempt);
|
|
614
|
+
stream.on('end', attempt);
|
|
615
|
+
stream.on('close', attempt);
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
cancel: function (reason) { failed = true; stream.stopSending(codeOf(reason)); }
|
|
619
|
+
}, { highWaterMark: 0 }, stats); // read only when the reader asks: backpressure
|
|
620
|
+
function fail(err) {
|
|
621
|
+
if (failed) return;
|
|
622
|
+
failed = true;
|
|
623
|
+
try { controller.error(err); } catch (e) {} // already closed: nothing to fail
|
|
624
|
+
}
|
|
625
|
+
return { stream: rs, fail: fail };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function webWritableOf(stream, stats, quic, streamId) {
|
|
629
|
+
var controller = null, failed = false;
|
|
630
|
+
var committed = 0; // commit(): bytes delivered even if aborted
|
|
631
|
+
var atomic = new Map(); // chunk → its atomic tickets, in write order
|
|
632
|
+
var hooks = {
|
|
633
|
+
stats: stats,
|
|
634
|
+
plainPending: 0,
|
|
635
|
+
markAtomic: function (chunk) {
|
|
636
|
+
var t = { aborted: false };
|
|
637
|
+
var list = atomic.get(chunk);
|
|
638
|
+
if (list) list.push(t); else atomic.set(chunk, [t]);
|
|
639
|
+
return t;
|
|
640
|
+
},
|
|
641
|
+
commit: function () { committed = stream._bytesWritten(); }
|
|
642
|
+
};
|
|
643
|
+
// The sink is told a chunk is atomic by its ticket (markAtomic, just before
|
|
644
|
+
// the writer queues it); plain writes are counted by the writer's write().
|
|
645
|
+
function takeTicket(chunk) {
|
|
646
|
+
var list = atomic.get(chunk);
|
|
647
|
+
if (!list) return null;
|
|
648
|
+
var t = list.shift();
|
|
649
|
+
if (list.length === 0) atomic.delete(chunk);
|
|
650
|
+
return t;
|
|
651
|
+
}
|
|
652
|
+
// An atomic write that does not fit: it, and every atomic write still
|
|
653
|
+
// queued, are not sent; a plain write queued behind it fails the stream.
|
|
654
|
+
function abortAtomic(t) {
|
|
655
|
+
t.aborted = true;
|
|
656
|
+
atomic.forEach(function (list) { list.forEach(function (x) { x.aborted = true; }); });
|
|
657
|
+
if (hooks.plainPending > 0) fail(new DOMException('a write queued behind an atomic write that did not fit', 'AbortError'));
|
|
658
|
+
}
|
|
659
|
+
var ws = new WebTransportSendStream(INTERNAL, {
|
|
660
|
+
start: function (c) { controller = c; },
|
|
661
|
+
write: function (chunk) {
|
|
662
|
+
var ticket = takeTicket(chunk);
|
|
663
|
+
var u8 = bufferSourceBytes(chunk, 'a chunk written to a WebTransport stream');
|
|
664
|
+
if (ticket) {
|
|
665
|
+
if (ticket.aborted) return;
|
|
666
|
+
if (!quic.streamSendWindow || quic.streamSendWindow(streamId) < u8.byteLength) { abortAtomic(ticket); return; }
|
|
667
|
+
}
|
|
668
|
+
return new Promise(function (resolve, reject) {
|
|
669
|
+
if (stream.destroyed || stream._writeStopped) { reject(stream._sessionError || endedError(stream)); return; }
|
|
670
|
+
stream.write(Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength), function (err) {
|
|
671
|
+
if (err || stream.destroyed || stream._writeStopped) reject(stream._sessionError || endedError(stream)); else resolve();
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
},
|
|
675
|
+
close: function () {
|
|
676
|
+
return new Promise(function (resolve, reject) {
|
|
677
|
+
if (stream.destroyed || stream._writeStopped) { reject(stream._sessionError || endedError(stream)); return; }
|
|
678
|
+
stream.end(resolve);
|
|
679
|
+
});
|
|
680
|
+
},
|
|
681
|
+
// RESET_STREAM_AT with what was committed, when anything was.
|
|
682
|
+
abort: function (reason) {
|
|
683
|
+
failed = true;
|
|
684
|
+
stream.resetStream(codeOf(reason), committed > 0 ? { reliableSize: committed } : undefined);
|
|
685
|
+
}
|
|
686
|
+
}, undefined, hooks);
|
|
687
|
+
function fail(err) {
|
|
688
|
+
if (failed) return;
|
|
689
|
+
failed = true;
|
|
690
|
+
try { controller.error(err); } catch (e) {} // already closed: nothing to fail
|
|
691
|
+
}
|
|
692
|
+
return { stream: ws, fail: fail };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ---- Opening a stream of our own ----
|
|
696
|
+
//
|
|
697
|
+
// The browser's way, for both sides (the server's res.create…Stream, the
|
|
698
|
+
// client's wt.create…Stream): options { sendOrder, sendGroup,
|
|
699
|
+
// waitUntilAvailable }. Without waitUntilAvailable, a stream past the peer's
|
|
700
|
+
// MAX_STREAMS is refused at once — null here; the browser rejects — rather
|
|
701
|
+
// than opened to wait unseen; with it, it is opened and its bytes wait for
|
|
702
|
+
// the peer to allow more. make(streamId, kind, prefixLength) builds the
|
|
703
|
+
// stream object, recorded by its owner.
|
|
704
|
+
//
|
|
705
|
+
// Opened is not open yet: past the peer's limit the stream waits for it to
|
|
706
|
+
// allow more. stream.pending says so (as net.Socket's does) until it is
|
|
707
|
+
// open; then 'ready', and callback(null, stream) — or callback(err) if it
|
|
708
|
+
// closes first (its session or connection did). Both after this returns,
|
|
709
|
+
// as Node's are, whether or not it had to wait. The client's Promise is
|
|
710
|
+
// settled from the same callback: resolved once the stream is open, as the
|
|
711
|
+
// browser's is — it was resolved at once, open or not.
|
|
712
|
+
function openWtStream(h3, quic, sessionId, bidi, options, make, callback) {
|
|
713
|
+
options = options || {};
|
|
714
|
+
if (!options.waitUntilAvailable && !quic.canOpenStream(bidi)) return null;
|
|
715
|
+
var streamId = h3.createWebTransportStream(sessionId, bidi);
|
|
716
|
+
var stream = make(streamId, bidi ? 'bidi' : 'uni-out', wt_stream_header(bidi, sessionId).byteLength);
|
|
717
|
+
if (options.sendGroup) stream.sendGroup = options.sendGroup;
|
|
718
|
+
if (options.sendOrder != null) stream.sendOrder = options.sendOrder;
|
|
719
|
+
stream.pending = true;
|
|
720
|
+
var settled = false;
|
|
721
|
+
function settle(err) {
|
|
722
|
+
if (settled) return;
|
|
723
|
+
settled = true;
|
|
724
|
+
stream._onClosePending = null;
|
|
725
|
+
stream.pending = false;
|
|
726
|
+
if (err) { err = new Error('the stream closed before it was open'); err.code = 'ERR_WEBTRANSPORT_STREAM_NOT_OPEN'; }
|
|
727
|
+
process.nextTick(function () {
|
|
728
|
+
if (!err) stream.emit('ready');
|
|
729
|
+
if (typeof callback === 'function') callback(err || null, stream);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
stream._onClosePending = function (aborted) { if (aborted) settle(true); };
|
|
733
|
+
quic.whenStreamPermitted(streamId, settle);
|
|
734
|
+
return stream;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ---- A session's place in QUIC's scheduler ----
|
|
738
|
+
//
|
|
739
|
+
// The browser's model (W3C WebTransport, sendGroup / sendOrder): the streams
|
|
740
|
+
// of a send group share the group's turns, the highest sendOrder with
|
|
741
|
+
// something to send taking them, equal orders taking turns; the groups share
|
|
742
|
+
// the session's sending equally. A stream in no group is in its session's own
|
|
743
|
+
// group. QUIC's scheduler has exactly that as `group` and `order`
|
|
744
|
+
// (setStreamPriority); this maps one onto the other. Every stream and group
|
|
745
|
+
// of the session is at the urgency of its CONNECT stream — RFC 9218's
|
|
746
|
+
// `priority` on the CONNECT request, or what the application set for it —
|
|
747
|
+
// taken as each stream is placed.
|
|
748
|
+
//
|
|
749
|
+
// Datagrams go first by default, ahead of any stream (sendDatagram). Sent
|
|
750
|
+
// with a sendGroup or a sendOrder other than 0, they take their place among
|
|
751
|
+
// the streams instead, as the browser's createWritable({ sendOrder,
|
|
752
|
+
// sendGroup }) has it: a member of that group (or of the session's), at
|
|
753
|
+
// that order.
|
|
754
|
+
function wtScheduling(quic, sessionStreamId) {
|
|
755
|
+
var sessionGroup = {};
|
|
756
|
+
function urgency() {
|
|
757
|
+
var p = quic.getStreamPriority(sessionStreamId);
|
|
758
|
+
return p && p.urgency != null ? p.urgency : 3;
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
forStream: function (group, order) {
|
|
762
|
+
return { urgency: urgency(), incremental: true, group: group || sessionGroup, order: Number(order) || 0 };
|
|
763
|
+
},
|
|
764
|
+
forDatagram: function (options) {
|
|
765
|
+
if (!options || (!options.sendGroup && !Number(options.sendOrder))) return null;
|
|
766
|
+
return { urgency: urgency(), group: options.sendGroup || sessionGroup, order: Number(options.sendOrder) || 0 };
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ---- A session's datagrams and statistics, for both sides ----
|
|
772
|
+
//
|
|
773
|
+
// The client's WebTransport and the server's res share these, so a session
|
|
774
|
+
// looks the same from either end. `session` is what they hand in:
|
|
775
|
+
// quic the QUIC connection (a getter on the client:
|
|
776
|
+
// made after the session object)
|
|
777
|
+
// connected() whether the session is up
|
|
778
|
+
// send(u8, options) its sendDatagram (options: sendOrder, sendGroup)
|
|
779
|
+
// maxDatagramSize() the largest payload it can send
|
|
780
|
+
// owner { maxAge, expired } — its datagrams in QUIC's
|
|
781
|
+
// queue: their age limit (outgoingMaxAge) and how
|
|
782
|
+
// many were dropped unsent (sendDatagram's owner)
|
|
783
|
+
// dgStats { droppedIncoming, expiredIncoming }
|
|
784
|
+
// connecting() (the client's) the session is still being
|
|
785
|
+
// set up: datagrams written meanwhile wait
|
|
786
|
+
// transport (the client's) its WebTransport — a send group
|
|
787
|
+
// of another session's is refused
|
|
788
|
+
|
|
789
|
+
// WebIDL's unsigned long, as a setter takes it: a number, its fraction
|
|
790
|
+
// dropped, modulo 2^32 (-1 is 4294967295); NaN and ±Infinity are 0.
|
|
791
|
+
function toUint32(v) {
|
|
792
|
+
v = Number(v);
|
|
793
|
+
if (!isFinite(v)) return 0;
|
|
794
|
+
v = Math.trunc(v) % 4294967296;
|
|
795
|
+
return v < 0 ? v + 4294967296 : v;
|
|
796
|
+
}
|
|
797
|
+
// An age limit, as the browser's setters take it: null, or milliseconds —
|
|
798
|
+
// negative or NaN is a RangeError, 0 means none (null).
|
|
799
|
+
function maxAgeOf(v, name) {
|
|
800
|
+
if (v === null || v === undefined) return null;
|
|
801
|
+
v = Number(v);
|
|
802
|
+
if (isNaN(v) || v < 0) throw new RangeError(name + ' must be null or a non-negative number, got ' + v);
|
|
803
|
+
return v === 0 ? null : v;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// The browser's WebTransportDatagramDuplexStream (W3C WebTransport):
|
|
807
|
+
// readable the incoming datagrams, read only when asked;
|
|
808
|
+
// past incomingMaxBufferedDatagrams the oldest
|
|
809
|
+
// goes, one older than incomingMaxAge is skipped
|
|
810
|
+
// createWritable({ sendOrder, sendGroup })
|
|
811
|
+
// a WebTransportDatagramsWritable of its own:
|
|
812
|
+
// each has its queue, its place among the
|
|
813
|
+
// session's streams, and its backpressure —
|
|
814
|
+
// writer.ready waits while outgoingMaxBufferedDatagrams
|
|
815
|
+
// of its datagrams wait to go
|
|
816
|
+
// maxDatagramSize, incomingMaxAge, outgoingMaxAge,
|
|
817
|
+
// incomingMaxBufferedDatagrams, outgoingMaxBufferedDatagrams
|
|
818
|
+
// What is written before the session is up waits for it (and goes once it
|
|
819
|
+
// is, those past outgoingMaxAge dropped); one larger than maxDatagramSize is
|
|
820
|
+
// dropped, as the browser drops it. outgoingMaxAge is this session's alone (a
|
|
821
|
+
// session on a shared connection used to set the whole connection's).
|
|
822
|
+
//
|
|
823
|
+
// Also, as before the browser's API changed, and for code written then:
|
|
824
|
+
// `writable` (one writable, made on first use), and incomingHighWaterMark /
|
|
825
|
+
// outgoingHighWaterMark — the same settings as …MaxBufferedDatagrams.
|
|
826
|
+
function createDatagrams(session) {
|
|
827
|
+
var queue = [], controller = null, pull = null, readable = null, legacy = null;
|
|
828
|
+
var inMax = 256, outMax = 256, inAge = null;
|
|
829
|
+
var writers = new Set(); // each writable's { w, q, release, fail }
|
|
830
|
+
var ended = null; // the session's end: { error } (null error: closed cleanly)
|
|
831
|
+
var flushing = false;
|
|
832
|
+
var dg = Object.create(WebTransportDatagramDuplexStream.prototype);
|
|
833
|
+
var bytesType = session.readableType === 'bytes'; // datagramsReadableType: a BYOB reader, as the browser's
|
|
834
|
+
var pullReject = null;
|
|
835
|
+
|
|
836
|
+
function deliver() {
|
|
837
|
+
if (!pull) return;
|
|
838
|
+
var now = Date.now();
|
|
839
|
+
while (queue.length > 0) {
|
|
840
|
+
var d = queue.shift();
|
|
841
|
+
if (inAge != null && now - d.t > inAge) { session.dgStats.expiredIncoming++; continue; }
|
|
842
|
+
var p = pull, r = pullReject; pull = pullReject = null;
|
|
843
|
+
var req = bytesType ? controller.byobRequest : null;
|
|
844
|
+
if (req && req.view) {
|
|
845
|
+
// A BYOB read: the whole datagram into the reader's buffer — one too
|
|
846
|
+
// small is a RangeError, a view of elements wider than a byte a
|
|
847
|
+
// TypeError (W3C pullDatagrams); either fails the readable.
|
|
848
|
+
var v = req.view;
|
|
849
|
+
if (v.byteLength < d.data.byteLength) { r(new RangeError('the buffer (' + v.byteLength + ' bytes) is smaller than the datagram (' + d.data.byteLength + ')')); return; }
|
|
850
|
+
if (!(v instanceof Uint8Array || v instanceof Int8Array || v instanceof Uint8ClampedArray)) { r(new TypeError('a datagram is read into a view of bytes')); return; }
|
|
851
|
+
new Uint8Array(v.buffer, v.byteOffset, d.data.byteLength).set(d.data);
|
|
852
|
+
req.respond(d.data.byteLength);
|
|
853
|
+
} else {
|
|
854
|
+
controller.enqueue(d.data);
|
|
855
|
+
}
|
|
856
|
+
p();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function connecting() { return !!(session.connecting && session.connecting()); }
|
|
862
|
+
|
|
863
|
+
// What the writables hold goes to QUIC — on the next turn, so that a burst
|
|
864
|
+
// of writes queues up to outgoingMaxBufferedDatagrams and its writer feels
|
|
865
|
+
// it (writer.ready), as the browser's does; or, while the session is being
|
|
866
|
+
// set up, waits for it. Older than outgoingMaxAge: dropped unsent.
|
|
867
|
+
function scheduleFlush() {
|
|
868
|
+
if (flushing) return;
|
|
869
|
+
flushing = true;
|
|
870
|
+
setImmediate(flush);
|
|
871
|
+
}
|
|
872
|
+
function flush() {
|
|
873
|
+
flushing = false;
|
|
874
|
+
var up = session.connected(), now = Date.now(), age = session.owner.maxAge;
|
|
875
|
+
writers.forEach(function (r) {
|
|
876
|
+
while (r.q.length > 0 && age != null && now - r.q[0].t > age) { r.q.shift(); session.owner.expired = (session.owner.expired || 0) + 1; }
|
|
877
|
+
if (!up) return;
|
|
878
|
+
while (r.q.length > 0) {
|
|
879
|
+
var u8 = r.q.shift().data;
|
|
880
|
+
if (u8.byteLength <= session.maxDatagramSize()) session.send(u8, r.w); // its sendOrder / sendGroup, as they are now
|
|
881
|
+
}
|
|
882
|
+
r.release();
|
|
883
|
+
if (r.closing) writers.delete(r);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function foreignGroup(g) {
|
|
888
|
+
return g != null && g._owner && session.transport && g._owner !== session.transport;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function makeWritable(options) {
|
|
892
|
+
options = options || {};
|
|
893
|
+
var r = { q: [], bp: null, bpResolve: null, controller: null };
|
|
894
|
+
r.release = function () {
|
|
895
|
+
if (r.bpResolve && r.q.length < outMax) { var f = r.bpResolve; r.bp = r.bpResolve = null; f(); }
|
|
896
|
+
};
|
|
897
|
+
r.fail = function (err) { try { r.controller.error(err); } catch (e) {} };
|
|
898
|
+
var w = new WebTransportDatagramsWritable(INTERNAL, {
|
|
899
|
+
start: function (c) { r.controller = c; },
|
|
900
|
+
write: function (chunk) {
|
|
901
|
+
var t = Date.now();
|
|
902
|
+
var u8 = bufferSourceBytes(chunk, 'a datagram');
|
|
903
|
+
if (ended) {
|
|
904
|
+
if (ended.error) throw ended.error;
|
|
905
|
+
throw new WebTransportError('the session is closed', { source: 'session' });
|
|
906
|
+
}
|
|
907
|
+
if (!session.connected() && !connecting()) throw new WebTransportError('the session is not connected', { source: 'session' });
|
|
908
|
+
// Larger than can be sent: dropped, the write resolves (as the browser's).
|
|
909
|
+
if (session.connected() && u8.byteLength > session.maxDatagramSize()) return;
|
|
910
|
+
r.q.push({ data: u8, t: t });
|
|
911
|
+
scheduleFlush();
|
|
912
|
+
if (r.q.length >= outMax) {
|
|
913
|
+
if (!r.bp) r.bp = new Promise(function (resolve) { r.bpResolve = resolve; });
|
|
914
|
+
return r.bp;
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
// Closed: what it holds still goes, then it is forgotten (flush).
|
|
918
|
+
close: function () { r.closing = true; scheduleFlush(); },
|
|
919
|
+
abort: function () { r.q = []; writers.delete(r); }
|
|
920
|
+
}, { highWaterMark: 1 });
|
|
921
|
+
var sendOrder = Number(options.sendOrder) || 0, sendGroup = options.sendGroup || null;
|
|
922
|
+
Object.defineProperty(w, 'sendOrder', { enumerable: true,
|
|
923
|
+
get: function () { return sendOrder; }, set: function (v) { sendOrder = Number(v) || 0; } });
|
|
924
|
+
Object.defineProperty(w, 'sendGroup', { enumerable: true,
|
|
925
|
+
get: function () { return sendGroup; },
|
|
926
|
+
set: function (g) {
|
|
927
|
+
if (foreignGroup(g)) throw new DOMException('this send group belongs to another WebTransport session', 'InvalidStateError');
|
|
928
|
+
sendGroup = g || null;
|
|
929
|
+
} });
|
|
930
|
+
r.w = w;
|
|
931
|
+
writers.add(r);
|
|
932
|
+
return w;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
Object.defineProperty(dg, 'readable', { enumerable: true, get: function () {
|
|
936
|
+
if (!readable) {
|
|
937
|
+
var source = {
|
|
938
|
+
start: function (c) {
|
|
939
|
+
controller = c;
|
|
940
|
+
if (ended) { if (ended.error) c.error(ended.error); else c.close(); }
|
|
941
|
+
},
|
|
942
|
+
pull: function () { return new Promise(function (resolve, reject) { pull = resolve; pullReject = reject; deliver(); }); }
|
|
943
|
+
};
|
|
944
|
+
if (bytesType) source.type = 'bytes';
|
|
945
|
+
readable = new ReadableStream(source, { highWaterMark: 0 });
|
|
946
|
+
}
|
|
947
|
+
return readable;
|
|
948
|
+
} });
|
|
949
|
+
// The browser's createWritable: a group of another session's is a
|
|
950
|
+
// TypeError, a session that is over an InvalidStateError — in that order.
|
|
951
|
+
dg.createWritable = function (options) {
|
|
952
|
+
if (options && foreignGroup(options.sendGroup)) throw new TypeError('options.sendGroup belongs to another WebTransport session');
|
|
953
|
+
if (ended || (!session.connected() && !connecting())) throw new DOMException('the session is closed', 'InvalidStateError');
|
|
954
|
+
return makeWritable(options);
|
|
955
|
+
};
|
|
956
|
+
Object.defineProperty(dg, 'writable', { enumerable: true, get: function () {
|
|
957
|
+
return legacy || (legacy = makeWritable());
|
|
958
|
+
} });
|
|
959
|
+
Object.defineProperty(dg, 'maxDatagramSize', { enumerable: true, get: function () { return session.maxDatagramSize(); } });
|
|
960
|
+
Object.defineProperty(dg, 'incomingMaxAge', { enumerable: true,
|
|
961
|
+
get: function () { return inAge; },
|
|
962
|
+
set: function (v) { inAge = maxAgeOf(v, 'incomingMaxAge'); } });
|
|
963
|
+
Object.defineProperty(dg, 'outgoingMaxAge', { enumerable: true,
|
|
964
|
+
get: function () { return session.owner.maxAge; },
|
|
965
|
+
set: function (v) { session.owner.maxAge = maxAgeOf(v, 'outgoingMaxAge'); } });
|
|
966
|
+
function inMaxProp() {
|
|
967
|
+
return { enumerable: true, get: function () { return inMax; },
|
|
968
|
+
set: function (v) { inMax = Math.max(1, toUint32(v)); } };
|
|
969
|
+
}
|
|
970
|
+
function outMaxProp() {
|
|
971
|
+
return { enumerable: true, get: function () { return outMax; },
|
|
972
|
+
set: function (v) { outMax = Math.max(1, toUint32(v)); writers.forEach(function (r) { r.release(); }); } };
|
|
973
|
+
}
|
|
974
|
+
Object.defineProperty(dg, 'incomingMaxBufferedDatagrams', inMaxProp());
|
|
975
|
+
Object.defineProperty(dg, 'outgoingMaxBufferedDatagrams', outMaxProp());
|
|
976
|
+
Object.defineProperty(dg, 'incomingHighWaterMark', inMaxProp());
|
|
977
|
+
Object.defineProperty(dg, 'outgoingHighWaterMark', outMaxProp());
|
|
978
|
+
|
|
979
|
+
// A datagram arrived: for the readable, once anything reads it.
|
|
980
|
+
dg._push = function (data) {
|
|
981
|
+
if (!readable || ended) return;
|
|
982
|
+
queue.push({ data: data, t: Date.now() });
|
|
983
|
+
while (queue.length > inMax) { queue.shift(); session.dgStats.droppedIncoming++; }
|
|
984
|
+
deliver();
|
|
985
|
+
};
|
|
986
|
+
// The session is up: what was written meanwhile goes.
|
|
987
|
+
dg._flush = function () { scheduleFlush(); };
|
|
988
|
+
// The session is over (W3C "cleanup"): the queues emptied, each writer
|
|
989
|
+
// released; closed cleanly, the readable closes; lost (`err`), the readable
|
|
990
|
+
// and every writable fail with the session's error.
|
|
991
|
+
dg._close = function (err) {
|
|
992
|
+
if (ended) return;
|
|
993
|
+
ended = { error: err || null };
|
|
994
|
+
queue = [];
|
|
995
|
+
writers.forEach(function (r) { r.q = []; r.release(); if (err) r.fail(err); });
|
|
996
|
+
if (controller) {
|
|
997
|
+
try {
|
|
998
|
+
if (err) controller.error(err);
|
|
999
|
+
else { controller.close(); if (controller.byobRequest) controller.byobRequest.respond(0); }
|
|
1000
|
+
} catch (e) {}
|
|
1001
|
+
}
|
|
1002
|
+
if (pull) { var p = pull; pull = pullReject = null; p(); }
|
|
1003
|
+
};
|
|
1004
|
+
return dg;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// The browser's getStats(): the connection's figures (QUIC's getStats — shared
|
|
1008
|
+
// by every session on it, as in the browser), and the session's datagrams.
|
|
1009
|
+
function sessionStats(session) {
|
|
1010
|
+
var q = session.quic && session.quic.getStats ? session.quic.getStats() : {};
|
|
1011
|
+
var out = { timestamp: Date.now() };
|
|
1012
|
+
['packetsSent', 'packetsLost', 'bytesLost', 'bytesReceived', 'packetsReceived', 'smoothedRtt', 'rttVariation',
|
|
1013
|
+
'minRtt', 'estimatedSendRate', 'atSendCapacity', 'congestionWindow', 'bytesInFlight'].forEach(function (k) {
|
|
1014
|
+
out[k] = q[k] !== undefined ? q[k] : null;
|
|
1015
|
+
});
|
|
1016
|
+
// As the spec counts them: bytesSent the application's bytes (new stream
|
|
1017
|
+
// data and datagram payloads — no framing, no retransmissions),
|
|
1018
|
+
// bytesSentOverhead what the packets carried besides, bytesAcknowledged
|
|
1019
|
+
// the payload the peer acknowledged. (bytesSent was the packets' size.)
|
|
1020
|
+
out.bytesSent = q.payloadBytesSent !== undefined ? q.payloadBytesSent : null;
|
|
1021
|
+
out.bytesSentOverhead = q.payloadBytesSent !== undefined && q.bytesSent !== undefined ? Math.max(0, q.bytesSent - q.payloadBytesSent) : null;
|
|
1022
|
+
out.bytesAcknowledged = q.payloadBytesAcknowledged !== undefined ? q.payloadBytesAcknowledged : null;
|
|
1023
|
+
out.datagrams = {
|
|
1024
|
+
droppedIncoming: session.dgStats.droppedIncoming,
|
|
1025
|
+
expiredIncoming: session.dgStats.expiredIncoming,
|
|
1026
|
+
expiredOutgoing: session.owner.expired || 0,
|
|
1027
|
+
lostOutgoing: null // a datagram's packet lost is not traced back to it
|
|
1028
|
+
};
|
|
1029
|
+
return Promise.resolve(out);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// owner: the client's WebTransport, so that a group is not used on another
|
|
1033
|
+
// session (its streams and datagram writables check it).
|
|
1034
|
+
function createSendGroup(owner) { return new WebTransportSendGroup(INTERNAL, owner); }
|
|
1035
|
+
|
|
1036
|
+
// ---- Keying material (the browser's exportKeyingMaterial) ----
|
|
1037
|
+
//
|
|
1038
|
+
// draft-ietf-webtrans-http3 ("Exporting Keying Material"): the TLS exporter
|
|
1039
|
+
// (RFC 8446 §7.5) with the label "EXPORTER-WebTransport" and, as its context,
|
|
1040
|
+
// WebTransport Session ID (64), Label Length (8), Label, Context Length (8), Context
|
|
1041
|
+
// — so a session's keys are its own, not the connection's. Checked as the
|
|
1042
|
+
// browser checks them: all three arguments required (TypeError), the label
|
|
1043
|
+
// and the context BufferSources of at most 255 bytes, the length 1 to 4096
|
|
1044
|
+
// (RangeError).
|
|
1045
|
+
function bufferSourceView(v, name) {
|
|
1046
|
+
var shared = typeof SharedArrayBuffer !== 'undefined' &&
|
|
1047
|
+
(v instanceof SharedArrayBuffer || (ArrayBuffer.isView(v) && v.buffer instanceof SharedArrayBuffer));
|
|
1048
|
+
if (!shared && v instanceof ArrayBuffer) return new Uint8Array(v);
|
|
1049
|
+
if (!shared && ArrayBuffer.isView(v)) return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
1050
|
+
throw new TypeError(name + ' must be an ArrayBuffer or an ArrayBufferView');
|
|
1051
|
+
}
|
|
1052
|
+
function exporterArgs(args) {
|
|
1053
|
+
if (args.length < 3) throw new TypeError('exportKeyingMaterial(label, context, outputLength): all three are required');
|
|
1054
|
+
var label = bufferSourceView(args[0], 'label'), context = bufferSourceView(args[1], 'context');
|
|
1055
|
+
if (label.byteLength > 255) throw new RangeError('the label is longer than 255 bytes');
|
|
1056
|
+
if (context.byteLength > 255) throw new RangeError('the context is longer than 255 bytes');
|
|
1057
|
+
var length = toUint32(args[2]);
|
|
1058
|
+
if (length === 0 || length > 4096) throw new RangeError('outputLength must be 1 to 4096, got ' + args[2]);
|
|
1059
|
+
return { label: label, context: context, length: length };
|
|
1060
|
+
}
|
|
1061
|
+
function exportSessionKeyingMaterial(quic, sessionId, a) {
|
|
1062
|
+
var L = a.label.byteLength, C = a.context.byteLength;
|
|
1063
|
+
var ctx = new Uint8Array(8 + 1 + L + 1 + C);
|
|
1064
|
+
new DataView(ctx.buffer).setBigUint64(0, BigInt(sessionId));
|
|
1065
|
+
ctx[8] = L; ctx.set(a.label, 9);
|
|
1066
|
+
ctx[9 + L] = C; ctx.set(a.context, 10 + L);
|
|
1067
|
+
var out = quic && quic.exportKeyingMaterial ? quic.exportKeyingMaterial(a.length, 'EXPORTER-WebTransport', ctx) : null;
|
|
1068
|
+
if (!out) throw new DOMException('the connection cannot export keying material', 'InvalidStateError');
|
|
1069
|
+
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength).slice();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, setErrorCode,
|
|
1073
|
+
exporterArgs, exportSessionKeyingMaterial,
|
|
1074
|
+
WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
|
|
1075
|
+
WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream };
|