quico 0.7.0 → 0.9.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 +247 -33
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +9 -5
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +10 -0
- package/src/crypto.js +6 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +115 -70
- package/src/h3_client.js +25 -5
- package/src/h3_server.js +104 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +379 -63
- package/src/quic_server.js +213 -72
- package/src/quic_socket.js +5 -5
- package/src/server.js +277 -24
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +62 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +432 -57
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
package/src/webtransport.js
CHANGED
|
@@ -17,12 +17,16 @@ import dns from 'node:dns';
|
|
|
17
17
|
import { H3Connection, wtCloseCapsule, createCapsuleReader } from './h3.js';
|
|
18
18
|
import { createQuicClientSocket } from './quic_socket.js';
|
|
19
19
|
import lemonTls from 'lemon-tls';
|
|
20
|
-
import { createHash
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { x509 } from 'cryptographic-formats';
|
|
21
22
|
import { readVarInt, wt_datagram_payload } from './utils.js';
|
|
22
|
-
import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats,
|
|
23
|
+
import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, setErrorCode,
|
|
24
|
+
exporterArgs, exportSessionKeyingMaterial,
|
|
25
|
+
WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
|
|
26
|
+
WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream } from './wt_stream.js';
|
|
23
27
|
import { defaultTransportParams } from './tls_bridge.js';
|
|
24
28
|
import { initialCongestionWindow, checkConnectionOptions } from './quic_connection.js';
|
|
25
|
-
import { sfString, parseSfString } from './http_common.js';
|
|
29
|
+
import { sfString, parseSfString, validateHeaderName, validateHeaderValue, isConnectionSpecific } from './http_common.js';
|
|
26
30
|
import { parseVersionList } from './quic_version.js';
|
|
27
31
|
|
|
28
32
|
|
|
@@ -50,6 +54,8 @@ import { parseVersionList } from './quic_version.js';
|
|
|
50
54
|
// Options: the browser's — serverCertificateHashes, protocols,
|
|
51
55
|
// congestionControl, allowPooling, requireUnreliable, anticipatedConcurrent…
|
|
52
56
|
// — and Node's, as for https.request: rejectUnauthorized (default true),
|
|
57
|
+
// origin and headers (fields for the CONNECT request, as the WebSocket
|
|
58
|
+
// client takes them: a browser sends its page's Origin, Node has none),
|
|
53
59
|
// ca, key / cert / passphrase (a client certificate), and session (one
|
|
54
60
|
// saved from the 'session' event, to resume the TLS session). The
|
|
55
61
|
// server's certificate is verified against the platform roots
|
|
@@ -68,25 +74,213 @@ var UNI_METHODS = ['write', 'end', 'destroy', 'cork', 'uncork', 'setDefaultEncod
|
|
|
68
74
|
var UNI_STATE = ['_streamId', 'id', 'sessionId', 'sendState', 'pending', 'streamErrorCode', 'destroyed', 'writableLength', 'writableHighWaterMark',
|
|
69
75
|
'writableNeedDrain', 'writableEnded', 'writableFinished', 'writableCorked', 'writableObjectMode', 'closed', 'errored'];
|
|
70
76
|
|
|
77
|
+
// The CONNECT request's own fields, besides those the session sets:
|
|
78
|
+
// options.headers — the browser's HeadersInit: a Headers, a sequence of
|
|
79
|
+
// [name, value] pairs, or an object of them — and Origin (options.origin):
|
|
80
|
+
// a browser always sends its page's, and servers check it (WPT's
|
|
81
|
+
// WebTransport server refuses a CONNECT without one); Node has no page.
|
|
82
|
+
//
|
|
83
|
+
// As the browser's Headers takes them: names lower-cased, values with their
|
|
84
|
+
// leading and trailing whitespace removed (a field value may not begin or end
|
|
85
|
+
// with it — RFC 9110 §5.5 — and a server refuses the request), a name given
|
|
86
|
+
// twice sent once with its values joined by ", ". An invalid name or value,
|
|
87
|
+
// or a field the session sets itself, throws a TypeError now, not later on
|
|
88
|
+
// the wire:
|
|
89
|
+
// a pseudo-header (not a token: TypeError from validateHeaderName);
|
|
90
|
+
// wt-available-protocols (that is options.protocols).
|
|
91
|
+
// Fields that cannot go on an HTTP/3 request at all are dropped, as the
|
|
92
|
+
// browser drops them: connection-specific ones (RFC 9114 §4.2), host (the
|
|
93
|
+
// authority is :authority, §4.3.1), te, and content-length and trailer,
|
|
94
|
+
// which do not apply to a CONNECT whose stream carries capsules. The fields
|
|
95
|
+
// a browser keeps from a page for its own safety — cookie, referer, sec-* —
|
|
96
|
+
// are sent: Node is the user agent itself here, as with Node's fetch.
|
|
97
|
+
var CONNECT_DROPPED = { 'host': 1, 'te': 1, 'content-length': 1, 'trailer': 1 };
|
|
98
|
+
var HTTP_WHITESPACE = /^[\t\n\r ]+|[\t\n\r ]+$/g;
|
|
99
|
+
function headersInitEntries(h) {
|
|
100
|
+
if (h == null) return [];
|
|
101
|
+
if (typeof h !== 'object' && typeof h !== 'function') {
|
|
102
|
+
throw new TypeError('options.headers must be a Headers, a sequence of [name, value] pairs, or an object');
|
|
103
|
+
}
|
|
104
|
+
var out = [];
|
|
105
|
+
if (typeof Headers !== 'undefined' && h instanceof Headers) {
|
|
106
|
+
h.forEach(function (value, name) { out.push([name, value]); });
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
if (typeof h[Symbol.iterator] === 'function') {
|
|
110
|
+
for (var pair of h) {
|
|
111
|
+
if (pair == null || typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') {
|
|
112
|
+
throw new TypeError('options.headers: each item must be a [name, value] pair');
|
|
113
|
+
}
|
|
114
|
+
var p = Array.from(pair);
|
|
115
|
+
if (p.length !== 2) throw new TypeError('options.headers: each pair must have exactly two items, a name and a value; got ' + p.length);
|
|
116
|
+
out.push([p[0], p[1]]);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
for (var name in h) {
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(h, name)) continue;
|
|
122
|
+
var v = h[name];
|
|
123
|
+
if (v === undefined || v === null) continue;
|
|
124
|
+
// A list, as Node's http takes it: each value a field of its own.
|
|
125
|
+
if (Array.isArray(v)) { for (var i = 0; i < v.length; i++) out.push([name, v[i]]); }
|
|
126
|
+
else out.push([name, v]);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function connectRequestFields(options) {
|
|
131
|
+
var out = {};
|
|
132
|
+
var entries = headersInitEntries(options.headers);
|
|
133
|
+
for (var i = 0; i < entries.length; i++) {
|
|
134
|
+
var name = String(entries[i][0]);
|
|
135
|
+
validateHeaderName(name);
|
|
136
|
+
var lower = name.toLowerCase();
|
|
137
|
+
if (lower === 'wt-available-protocols') {
|
|
138
|
+
throw new TypeError('options.headers: wt-available-protocols cannot be set; it is options.protocols');
|
|
139
|
+
}
|
|
140
|
+
var value = String(entries[i][1]).replace(HTTP_WHITESPACE, '');
|
|
141
|
+
validateHeaderValue(lower, value);
|
|
142
|
+
if (isConnectionSpecific(lower) || CONNECT_DROPPED[lower]) continue;
|
|
143
|
+
out[lower] = Object.prototype.hasOwnProperty.call(out, lower) ? out[lower] + ', ' + value : value;
|
|
144
|
+
}
|
|
145
|
+
if (options.origin != null) {
|
|
146
|
+
var origin = String(options.origin).replace(HTTP_WHITESPACE, '');
|
|
147
|
+
validateHeaderValue('origin', origin);
|
|
148
|
+
out['origin'] = origin;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The URL, as the browser's constructor checks it: one that does not parse,
|
|
154
|
+
// whose scheme is not https, or that has a fragment, is a SyntaxError
|
|
155
|
+
// (DOMException) — as Node's own WebSocket throws for its URL. An http: URL
|
|
156
|
+
// and a fragment were taken, and a port of 0 became 443.
|
|
157
|
+
function parseWebTransportUrl(url) {
|
|
158
|
+
var u;
|
|
159
|
+
try { u = new URL(String(url)); }
|
|
160
|
+
catch (e) { throw new DOMException('Invalid URL: ' + url, 'SyntaxError'); }
|
|
161
|
+
if (u.protocol !== 'https:') throw new DOMException('A WebTransport URL\'s scheme must be https:, not ' + u.protocol, 'SyntaxError');
|
|
162
|
+
if (u.href.indexOf('#') >= 0) throw new DOMException('A WebTransport URL must not have a fragment', 'SyntaxError');
|
|
163
|
+
return u;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// options.protocols, as the browser checks them: a list of strings, each
|
|
167
|
+
// one a valid structured-field string (printable ASCII), 1 to 512 long, none
|
|
168
|
+
// twice — otherwise a SyntaxError. They were sent as given: a CR or LF in
|
|
169
|
+
// one went into the field.
|
|
170
|
+
function checkProtocols(list) {
|
|
171
|
+
if (list == null) return [];
|
|
172
|
+
if (typeof list !== 'object' || typeof list[Symbol.iterator] !== 'function') {
|
|
173
|
+
throw new TypeError('options.protocols must be a sequence of strings');
|
|
174
|
+
}
|
|
175
|
+
var out = [];
|
|
176
|
+
for (var p of list) {
|
|
177
|
+
p = String(p);
|
|
178
|
+
if (p.length === 0 || p.length > 512 || !/^[\x20-\x7e]*$/.test(p) || out.indexOf(p) >= 0) {
|
|
179
|
+
throw new DOMException('Invalid protocol in options.protocols: ' + JSON.stringify(p) +
|
|
180
|
+
' (each must be printable ASCII, 1 to 512 characters, and given once)', 'SyntaxError');
|
|
181
|
+
}
|
|
182
|
+
out.push(p);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// options.serverCertificateHashes, as the browser's WebIDL takes it: each an
|
|
188
|
+
// object with an algorithm and a value — a BufferSource, not shared memory.
|
|
189
|
+
// A malformed one was skipped, and the connection then failed for a reason
|
|
190
|
+
// that did not say why.
|
|
191
|
+
function checkCertificateHashes(list) {
|
|
192
|
+
if (list == null) return;
|
|
193
|
+
if (typeof list !== 'object' || typeof list[Symbol.iterator] !== 'function') {
|
|
194
|
+
throw new TypeError('options.serverCertificateHashes must be a sequence of { algorithm, value }');
|
|
195
|
+
}
|
|
196
|
+
for (var h of list) {
|
|
197
|
+
if (h == null || typeof h !== 'object') throw new TypeError('options.serverCertificateHashes: each must be an object { algorithm, value }');
|
|
198
|
+
if (h.algorithm === undefined) throw new TypeError('options.serverCertificateHashes: algorithm is required');
|
|
199
|
+
var v = h.value;
|
|
200
|
+
if (v === undefined) throw new TypeError('options.serverCertificateHashes: value is required');
|
|
201
|
+
var shared = typeof SharedArrayBuffer !== 'undefined' &&
|
|
202
|
+
(v instanceof SharedArrayBuffer || (ArrayBuffer.isView(v) && v.buffer instanceof SharedArrayBuffer));
|
|
203
|
+
if (shared || !(v instanceof ArrayBuffer || ArrayBuffer.isView(v))) {
|
|
204
|
+
throw new TypeError('options.serverCertificateHashes: value must be an ArrayBuffer or an ArrayBufferView, not shared memory');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The anticipatedConcurrentIncoming… hints, as WebIDL's [EnforceRange]
|
|
210
|
+
// unsigned short? takes them: null, or an integer 0 to 65535 (a fraction
|
|
211
|
+
// dropped); anything else a TypeError.
|
|
212
|
+
function streamHint(v, name) {
|
|
213
|
+
if (v === null || v === undefined) return null;
|
|
214
|
+
var n = Number(v);
|
|
215
|
+
if (!isFinite(n)) throw new TypeError(name + ' must be null or an integer from 0 to 65535, got ' + v);
|
|
216
|
+
n = Math.trunc(n);
|
|
217
|
+
if (n < 0 || n > 65535) throw new TypeError(name + ' must be null or an integer from 0 to 65535, got ' + v);
|
|
218
|
+
return n;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var CONGESTION_CONTROL = ['default', 'throughput', 'low-latency'];
|
|
222
|
+
|
|
223
|
+
// The browser's responseHeaders: the server's answer to the CONNECT, as a
|
|
224
|
+
// Headers that cannot be changed — without the pseudo-headers, and without
|
|
225
|
+
// wt-protocol, which is negotiation (its outcome is wt.protocol). A browser
|
|
226
|
+
// also keeps Set-Cookie from its page; here it is there, as with Node's
|
|
227
|
+
// fetch: the program is the user agent (getSetCookie() reads it).
|
|
228
|
+
class ImmutableHeaders extends Headers {
|
|
229
|
+
set() { throw new TypeError('responseHeaders cannot be changed'); }
|
|
230
|
+
append() { throw new TypeError('responseHeaders cannot be changed'); }
|
|
231
|
+
delete() { throw new TypeError('responseHeaders cannot be changed'); }
|
|
232
|
+
}
|
|
233
|
+
function responseHeadersOf(headers) {
|
|
234
|
+
var list = headers && headers._list ? headers._list
|
|
235
|
+
: Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
|
|
236
|
+
var pairs = [];
|
|
237
|
+
for (var i = 0; i < list.length; i++) {
|
|
238
|
+
var name = String(list[i][0]).toLowerCase(), value = list[i][1];
|
|
239
|
+
if (name.charAt(0) === ':' || name === 'wt-protocol') continue;
|
|
240
|
+
var values = Array.isArray(value) ? value : [value];
|
|
241
|
+
for (var j = 0; j < values.length; j++) {
|
|
242
|
+
var pair = [name, String(values[j])];
|
|
243
|
+
try { new Headers([pair]); pairs.push(pair); }
|
|
244
|
+
catch (e) {} // a field Headers cannot hold is left out, not the whole answer
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return new ImmutableHeaders(pairs);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function invalidState(message) { return new DOMException(message, 'InvalidStateError'); }
|
|
251
|
+
|
|
71
252
|
class WebTransport extends EventEmitter {
|
|
72
253
|
|
|
73
254
|
constructor(url, options) {
|
|
74
255
|
super();
|
|
75
256
|
|
|
76
|
-
//
|
|
77
|
-
var parsed =
|
|
78
|
-
this._hostname = parsed.hostname;
|
|
79
|
-
this.
|
|
257
|
+
// The URL and the browser's options, checked as the browser checks them.
|
|
258
|
+
var parsed = parseWebTransportUrl(url);
|
|
259
|
+
this._hostname = parsed.hostname.replace(/^\[(.*)\]$/, '$1'); // an IPv6 literal, without its brackets
|
|
260
|
+
this._authority = parsed.host; // with them, and the port unless 443
|
|
261
|
+
this._port = parsed.port === '' ? 443 : Number(parsed.port);
|
|
80
262
|
this._path = parsed.pathname + (parsed.search || '');
|
|
81
263
|
this._options = options || {};
|
|
264
|
+
this._protocols = checkProtocols(this._options.protocols);
|
|
265
|
+
checkCertificateHashes(this._options.serverCertificateHashes);
|
|
266
|
+
if (this._options.datagramsReadableType != null && String(this._options.datagramsReadableType) !== 'bytes') {
|
|
267
|
+
throw new TypeError("options.datagramsReadableType must be 'bytes'");
|
|
268
|
+
}
|
|
269
|
+
if (this._options.congestionControl != null && CONGESTION_CONTROL.indexOf(String(this._options.congestionControl)) < 0) {
|
|
270
|
+
throw new TypeError('options.congestionControl must be one of ' + CONGESTION_CONTROL.join(', '));
|
|
271
|
+
}
|
|
272
|
+
this._hintUni = streamHint(this._options.anticipatedConcurrentIncomingUnidirectionalStreams, 'anticipatedConcurrentIncomingUnidirectionalStreams');
|
|
273
|
+
this._hintBidi = streamHint(this._options.anticipatedConcurrentIncomingBidirectionalStreams, 'anticipatedConcurrentIncomingBidirectionalStreams');
|
|
82
274
|
// Checked here, as the browser checks its options: a bad value throws now,
|
|
83
275
|
// not later inside the connection.
|
|
84
276
|
defaultTransportParams(this._options.transportParams);
|
|
277
|
+
this._transportParams = this._hintedTransportParams();
|
|
85
278
|
if (this._options.ecdhCurve != null) lemonTls.parseEcdhCurve(this._options.ecdhCurve);
|
|
86
279
|
if (this._options.sigalgs != null) lemonTls.parseSigalgs(this._options.sigalgs);
|
|
87
280
|
initialCongestionWindow(this._options.initialCongestionWindow);
|
|
88
281
|
(function (o) { var c = {}; ['startupCap', 'pmtuDiscovery', 'maxUdpPayloadSize', 'initialRtt', 'reorderWindow'].forEach(function (k) { if (o[k] != null) c[k] = o[k]; }); checkConnectionOptions(c, 'WebTransport'); })(this._options);
|
|
89
282
|
parseVersionList(this._options.quicVersions, 'quicVersions');
|
|
283
|
+
this._connectFields = connectRequestFields(this._options);
|
|
90
284
|
|
|
91
285
|
// State
|
|
92
286
|
this._state = 'connecting'; // connecting → connected → closed, or connecting → failed
|
|
@@ -127,8 +321,7 @@ class WebTransport extends EventEmitter {
|
|
|
127
321
|
this.reliability = 'pending'; // then 'supports-unreliable' or 'reliable-only'
|
|
128
322
|
this.congestionControl = 'default';
|
|
129
323
|
this.protocol = ''; // the server's choice among options.protocols
|
|
130
|
-
this.
|
|
131
|
-
this.anticipatedConcurrentIncomingBidirectionalStreams = o.anticipatedConcurrentIncomingBidirectionalStreams != null ? o.anticipatedConcurrentIncomingBidirectionalStreams : null;
|
|
324
|
+
this._responseHeaders = null; // responseHeaders: the server's, once the session is up
|
|
132
325
|
this._incoming = { stream: null, unidirectionalStream: null }; // the browser API's queues, once read
|
|
133
326
|
// The session, as wt_stream's createDatagrams / sessionStats take it —
|
|
134
327
|
// the server's res hands in the same.
|
|
@@ -138,6 +331,9 @@ class WebTransport extends EventEmitter {
|
|
|
138
331
|
this._session = {
|
|
139
332
|
get quic() { return self._quic; },
|
|
140
333
|
connected: function () { return self._state === 'connected'; },
|
|
334
|
+
connecting: function () { return self._state === 'connecting'; },
|
|
335
|
+
readableType: this._options.datagramsReadableType != null ? 'bytes' : undefined,
|
|
336
|
+
get transport() { return self; },
|
|
141
337
|
send: function (u8, options) { return self.sendDatagram(u8, options); },
|
|
142
338
|
maxDatagramSize: function () { return self.maxDatagramSize(); },
|
|
143
339
|
owner: this._dgOwner,
|
|
@@ -172,29 +368,45 @@ class WebTransport extends EventEmitter {
|
|
|
172
368
|
_failSetup(err) {
|
|
173
369
|
if (this._state !== 'connecting') return;
|
|
174
370
|
this._state = 'failed';
|
|
371
|
+
this._attempt = (this._attempt || 0) + 1; // whatever is still on its way is ignored from now
|
|
175
372
|
// As the browser's: a WebTransportError, source 'session' — keeping the
|
|
176
373
|
// cause's message and code (node:tls's, say) for a Node caller.
|
|
177
374
|
if (!(err instanceof WebTransportError)) {
|
|
178
375
|
var we = new WebTransportError(err.message, { source: 'session' });
|
|
179
|
-
if (err.code) we
|
|
376
|
+
if (err.code) setErrorCode(we, err.code);
|
|
180
377
|
we.cause = err;
|
|
181
378
|
err = we;
|
|
182
379
|
}
|
|
183
380
|
this._readyReject(err);
|
|
184
381
|
this._closedReject(err);
|
|
382
|
+
this._destroyStreams(err, false);
|
|
185
383
|
if (this.listenerCount('error') > 0) this.emit('error', err);
|
|
186
|
-
|
|
384
|
+
// Over: 'close' now — the connection's own end is ignored from here.
|
|
385
|
+
if (!this._closedDone) { this._closedDone = true; this.emit('close'); }
|
|
386
|
+
// The connection goes: the one the CONNECT was sent on, or the one still
|
|
387
|
+
// in its handshake (close() before `ready`: it went on, and `ready`
|
|
388
|
+
// resolved once the server answered).
|
|
389
|
+
var q = this._quic || this._handshaking;
|
|
390
|
+
if (q) { try { q.close(0x0100, 'WebTransport session not established', true); } catch (e) {} } // H3_NO_ERROR
|
|
187
391
|
}
|
|
188
392
|
|
|
189
393
|
_connect() {
|
|
190
394
|
var host = this._hostname;
|
|
191
395
|
var self = this;
|
|
192
396
|
|
|
397
|
+
// Port 0 names no server (RFC 6335 §6): nothing can answer, and the
|
|
398
|
+
// attempt waited for a handshake timeout — the browser fails it.
|
|
399
|
+
if (this._port === 0) {
|
|
400
|
+
process.nextTick(function () { self._failSetup(new WebTransportError('port 0 cannot be connected to', { source: 'session' })); });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
193
404
|
if (/^[\d.]+$/.test(host) || host.indexOf(':') >= 0 || host === 'localhost') {
|
|
194
405
|
var ip = (host === 'localhost') ? '127.0.0.1' : host;
|
|
195
406
|
this._startQuic(ip);
|
|
196
407
|
} else {
|
|
197
408
|
dns.lookup(host, function (err, address) {
|
|
409
|
+
if (self._state !== 'connecting') return; // closed meanwhile
|
|
198
410
|
if (err) { self._failSetup(err); return; }
|
|
199
411
|
self._startQuic(address);
|
|
200
412
|
});
|
|
@@ -214,6 +426,7 @@ class WebTransport extends EventEmitter {
|
|
|
214
426
|
this._udpSocket = createQuicClientSocket({
|
|
215
427
|
remoteIp: remoteIp,
|
|
216
428
|
remotePort: remotePort,
|
|
429
|
+
socketBufferSize: this._options.socketBufferSize,
|
|
217
430
|
hostname: this._hostname,
|
|
218
431
|
// serverCertificateHashes pins the certificate instead of a CA (checked
|
|
219
432
|
// in onConnect, before anything is sent on the connection).
|
|
@@ -225,9 +438,10 @@ class WebTransport extends EventEmitter {
|
|
|
225
438
|
// no certificate to pin, and the pin is the only check there is.
|
|
226
439
|
session: this._options.serverCertificateHashes ? null : (this._options.session || null),
|
|
227
440
|
onSocket: function (q) {
|
|
441
|
+
if (attempt === self._attempt) self._handshaking = q;
|
|
228
442
|
q.on('session', function (buf) { self.emit('session', buf); });
|
|
229
443
|
},
|
|
230
|
-
transportParams: this.
|
|
444
|
+
transportParams: this._transportParams,
|
|
231
445
|
quicVersions: this._options.quicVersions,
|
|
232
446
|
initialCongestionWindow: this._options.initialCongestionWindow,
|
|
233
447
|
startupCap: this._options.startupCap,
|
|
@@ -257,7 +471,9 @@ class WebTransport extends EventEmitter {
|
|
|
257
471
|
onConnect: function (q, s) {
|
|
258
472
|
if (attempt !== self._attempt) { try { q.close(0x100, 'superseded', true); } catch (e) {} return; }
|
|
259
473
|
self._quic = q;
|
|
474
|
+
self._handshaking = null;
|
|
260
475
|
self._udpSocket = s;
|
|
476
|
+
q.on('closing', function () { if (attempt === self._attempt) self._connectionEnded(); });
|
|
261
477
|
var pinErr = self._checkCertificateHashes();
|
|
262
478
|
if (pinErr) { self._failSetup(pinErr); return; }
|
|
263
479
|
self._h3 = new H3Connection({ quicConnection: self._quic, isServer: false, enableWebTransport: true });
|
|
@@ -267,16 +483,7 @@ class WebTransport extends EventEmitter {
|
|
|
267
483
|
|
|
268
484
|
onClose: function () {
|
|
269
485
|
if (attempt !== self._attempt) return;
|
|
270
|
-
|
|
271
|
-
// onError did not see it. `ready` must settle (it rejects in the
|
|
272
|
-
// browser too); left pending, the caller would wait forever.
|
|
273
|
-
if (self._state === 'connecting') {
|
|
274
|
-
var err = new Error('WebTransport connection closed before the session was established');
|
|
275
|
-
err.code = 'ERR_WEBTRANSPORT_SESSION_FAILED';
|
|
276
|
-
self._failSetup(err);
|
|
277
|
-
}
|
|
278
|
-
if (self._state !== 'failed') self._state = 'closed';
|
|
279
|
-
self._sessionClosed();
|
|
486
|
+
self._connectionEnded();
|
|
280
487
|
// The helper closes the UDP socket.
|
|
281
488
|
}
|
|
282
489
|
});
|
|
@@ -293,7 +500,7 @@ class WebTransport extends EventEmitter {
|
|
|
293
500
|
// down ('draining').
|
|
294
501
|
var readCapsules = createCapsuleReader(function (info) {
|
|
295
502
|
if (!self.closeInfo) self.closeInfo = info;
|
|
296
|
-
self.
|
|
503
|
+
self._close(info);
|
|
297
504
|
}, function () { self._drainingResolve(); self.emit('draining'); });
|
|
298
505
|
this._h3.on('http_body', function (streamId, data) {
|
|
299
506
|
if (streamId === self._sessionId && self._state === 'connected') readCapsules(data);
|
|
@@ -305,7 +512,9 @@ class WebTransport extends EventEmitter {
|
|
|
305
512
|
var status = parseInt(headers[':status']) || 0;
|
|
306
513
|
// 1xx (103 Early Hints) is not the answer to the CONNECT: it follows.
|
|
307
514
|
if (status >= 100 && status < 200) return;
|
|
308
|
-
|
|
515
|
+
// Any 2xx establishes the session (draft-ietf-webtrans-http3 §3.3,
|
|
516
|
+
// as in the browser): a 204 failed here as "CONNECT failed".
|
|
517
|
+
if (status >= 200 && status < 300) {
|
|
309
518
|
var reliability = self._quic.maxDatagramSize() > 0 ? 'supports-unreliable' : 'reliable-only';
|
|
310
519
|
if (self._options.requireUnreliable && reliability !== 'supports-unreliable') {
|
|
311
520
|
self._failSetup(new Error('the server does not take datagrams (requireUnreliable)'));
|
|
@@ -316,10 +525,12 @@ class WebTransport extends EventEmitter {
|
|
|
316
525
|
// The server's choice among options.protocols (WT-Protocol, a
|
|
317
526
|
// structured-field string), if it made one from them.
|
|
318
527
|
var chosen = parseSfString(headers['wt-protocol']);
|
|
319
|
-
if (chosen !== null &&
|
|
528
|
+
if (chosen !== null && self._protocols.indexOf(chosen) >= 0) self.protocol = chosen;
|
|
529
|
+
self._responseHeaders = responseHeadersOf(headers);
|
|
320
530
|
self._readyResolve();
|
|
321
531
|
self.emit('ready');
|
|
322
532
|
self._handOver();
|
|
533
|
+
self.datagrams._flush(); // what was written before `ready`
|
|
323
534
|
} else {
|
|
324
535
|
self._failSetup(new Error('WebTransport CONNECT failed: HTTP ' + status));
|
|
325
536
|
return;
|
|
@@ -352,10 +563,24 @@ class WebTransport extends EventEmitter {
|
|
|
352
563
|
this._h3.on('stream_reset', function (streamId, code) { self._peerCancelled(streamId, 'stream_reset', code); });
|
|
353
564
|
this._h3.on('stop_sending', function (streamId, code) { self._peerCancelled(streamId, 'stop_sending', code); });
|
|
354
565
|
|
|
566
|
+
// The server ended the CONNECT stream with a FIN and no
|
|
567
|
+
// CLOSE_WEBTRANSPORT_SESSION: a clean close, code 0 and an empty reason
|
|
568
|
+
// (draft-ietf-webtrans-http3 §5: "semantically equivalent to
|
|
569
|
+
// terminating it with a CLOSE_WEBTRANSPORT_SESSION capsule that has an
|
|
570
|
+
// error code of 0 and an empty error string"). `closed` waited for the
|
|
571
|
+
// QUIC idle timeout, and 'close' never came.
|
|
572
|
+
this._h3.on('http_end', function (streamId) {
|
|
573
|
+
if (streamId === self._sessionId && self._state === 'connected') self._close(self.closeInfo || { closeCode: 0, reason: '' });
|
|
574
|
+
});
|
|
575
|
+
|
|
355
576
|
// The session is over in HTTP/3 (its CONNECT stream ended): its streams,
|
|
356
|
-
// already reset there, end here too.
|
|
577
|
+
// already reset there, end here too. Still 'connected' here means it
|
|
578
|
+
// ended with neither side closing it — no capsule, no FIN (a FIN closes
|
|
579
|
+
// it cleanly just above): the server reset the CONNECT stream. As in
|
|
580
|
+
// the browser, the session was lost, not closed.
|
|
357
581
|
this._h3.on('wt_session_closed', function (sessionId) {
|
|
358
|
-
if (sessionId
|
|
582
|
+
if (sessionId !== self._sessionId) return;
|
|
583
|
+
if (self._state === 'connected') self._sessionLost('the server reset the session\'s CONNECT stream');
|
|
359
584
|
});
|
|
360
585
|
|
|
361
586
|
// Datagrams — QUIC DATAGRAM payload = VarInt(quarter_stream_id) + app data (RFC 9297)
|
|
@@ -388,6 +613,7 @@ class WebTransport extends EventEmitter {
|
|
|
388
613
|
if (self._streams[streamId] === stream) delete self._streams[streamId];
|
|
389
614
|
if (self._h3) self._h3.releaseWebTransportStream(streamId);
|
|
390
615
|
}, prefixLength, this._sched, this._sessionId);
|
|
616
|
+
stream._owner = this; // its sendGroup must be one of this session's
|
|
391
617
|
this._streams[streamId] = stream;
|
|
392
618
|
return stream;
|
|
393
619
|
}
|
|
@@ -421,22 +647,33 @@ class WebTransport extends EventEmitter {
|
|
|
421
647
|
_incomingStreams(event) {
|
|
422
648
|
var self = this;
|
|
423
649
|
var rs = new ReadableStream({
|
|
424
|
-
start: function (controller) {
|
|
650
|
+
start: function (controller) {
|
|
651
|
+
self._incoming[event] = controller;
|
|
652
|
+
if (self._incomingEnd) { if (self._incomingEnd.error) controller.error(self._incomingEnd.error); else controller.close(); }
|
|
653
|
+
}
|
|
425
654
|
});
|
|
426
655
|
queueMicrotask(function () { self._handOver(); });
|
|
427
656
|
return rs;
|
|
428
657
|
}
|
|
429
658
|
|
|
430
|
-
// The session is over: each stream still open
|
|
431
|
-
// (draft-ietf-webtrans-http3 §4.6), not an
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
659
|
+
// The session is over (the browser's "cleanup"): each stream still open
|
|
660
|
+
// goes with WT_SESSION_GONE (draft-ietf-webtrans-http3 §4.6), not an
|
|
661
|
+
// application code, and its browser views fail with `err` — the session's
|
|
662
|
+
// error when it was lost or never established, the same object `closed`
|
|
663
|
+
// rejects with; an AbortError when we closed it; a WebTransportError when
|
|
664
|
+
// the server did. Closed (`closed` true), the incoming-stream queues and
|
|
665
|
+
// the datagrams' readable close; otherwise they fail with `err` too.
|
|
666
|
+
_destroyStreams(err, closed) {
|
|
667
|
+
if (this._cleanedUp) return;
|
|
668
|
+
this._cleanedUp = true;
|
|
669
|
+
for (var id in this._streams) { this._streams[id]._sessionGone(err); this._streams[id].destroy(); }
|
|
670
|
+
for (var i = 0; i < this._waiting.length; i++) { var w = this._waiting[i][1]; if (w._sessionGone) w._sessionGone(err); w.destroy(); }
|
|
435
671
|
this._waiting = [];
|
|
436
672
|
for (var ev in this._incoming) {
|
|
437
|
-
if (this._incoming[ev]) { try { this._incoming[ev].close(); } catch (e) {} }
|
|
673
|
+
if (this._incoming[ev]) { try { if (closed) this._incoming[ev].close(); else this._incoming[ev].error(err); } catch (e) {} }
|
|
438
674
|
}
|
|
439
|
-
this.
|
|
675
|
+
this._incomingEnd = { error: closed ? null : err }; // for a queue first read after this
|
|
676
|
+
this.datagrams._close(closed ? null : err);
|
|
440
677
|
}
|
|
441
678
|
|
|
442
679
|
// serverCertificateHashes (the browser's way to trust a certificate it
|
|
@@ -458,8 +695,11 @@ class WebTransport extends EventEmitter {
|
|
|
458
695
|
return v.length === digest.length && v.equals(digest);
|
|
459
696
|
});
|
|
460
697
|
if (!match) return new Error('serverCertificateHashes: the certificate matches none of them');
|
|
461
|
-
|
|
462
|
-
var
|
|
698
|
+
// Its dates, read by cryptographic-formats
|
|
699
|
+
var cert;
|
|
700
|
+
try { cert = x509.from_der(der); } catch (e) { return new Error('serverCertificateHashes: the certificate does not parse: ' + e.message); }
|
|
701
|
+
var now = Date.now();
|
|
702
|
+
var from = cert.validity.not_before.date.getTime(), to = cert.validity.not_after.date.getTime();
|
|
463
703
|
if (!(now >= from && now <= to)) return new Error('serverCertificateHashes: the certificate is not valid now');
|
|
464
704
|
if (to - from > 14 * 24 * 3600 * 1000) return new Error('serverCertificateHashes: the certificate is valid for more than 14 days');
|
|
465
705
|
return null;
|
|
@@ -482,13 +722,14 @@ class WebTransport extends EventEmitter {
|
|
|
482
722
|
':method': 'CONNECT',
|
|
483
723
|
':protocol': 'webtransport',
|
|
484
724
|
':scheme': 'https',
|
|
485
|
-
':authority': this.
|
|
725
|
+
':authority': this._authority,
|
|
486
726
|
':path': this._path
|
|
487
727
|
};
|
|
728
|
+
for (var f in this._connectFields) headers[f] = this._connectFields[f];
|
|
488
729
|
// The browser's `protocols`: the application protocols it offers, as a
|
|
489
730
|
// structured-field list of strings; the server answers with WT-Protocol.
|
|
490
|
-
if (
|
|
491
|
-
headers['wt-available-protocols'] = this.
|
|
731
|
+
if (this._protocols.length > 0) {
|
|
732
|
+
headers['wt-available-protocols'] = this._protocols.map(sfString).join(', ');
|
|
492
733
|
}
|
|
493
734
|
|
|
494
735
|
this._h3.sendHeaders(this._sessionId, headers, false);
|
|
@@ -501,18 +742,37 @@ class WebTransport extends EventEmitter {
|
|
|
501
742
|
// ============================================================
|
|
502
743
|
|
|
503
744
|
// The browser's stream options — sendOrder, sendGroup, waitUntilAvailable
|
|
504
|
-
// (wt_stream's openWtStream, as the server's)
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
745
|
+
// (wt_stream's openWtStream, as the server's). As the browser's
|
|
746
|
+
// createBidirectionalStream / createUnidirectionalStream:
|
|
747
|
+
// a sendGroup of another session's rejects, TypeError;
|
|
748
|
+
// the session over ('closed', 'failed') rejects, InvalidStateError —
|
|
749
|
+
// also when it ends while the stream waits to be opened;
|
|
750
|
+
// still connecting waits for `ready` (it was refused);
|
|
751
|
+
// past the server's limit, without waitUntilAvailable
|
|
752
|
+
// rejects, QuotaExceededError;
|
|
753
|
+
// otherwise resolved once the stream is open — at once if the server allows
|
|
754
|
+
// it, or when it does (waitUntilAvailable).
|
|
508
755
|
_openStream(bidi, options) {
|
|
509
|
-
if (this._state !== 'connected') return Promise.reject(new WebTransportError('the session is not connected', { source: 'session' }));
|
|
510
756
|
var self = this;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
757
|
+
options = options || {};
|
|
758
|
+
if (options.sendGroup != null && options.sendGroup._owner !== this) {
|
|
759
|
+
return Promise.reject(new TypeError('options.sendGroup belongs to another WebTransport session'));
|
|
760
|
+
}
|
|
761
|
+
if (this._state === 'closed' || this._state === 'failed') return Promise.reject(invalidState('the session is ' + this._state));
|
|
762
|
+
var up = this._state === 'connected' ? Promise.resolve() : this.ready;
|
|
763
|
+
return up.then(function () {
|
|
764
|
+
if (self._state !== 'connected') throw invalidState('the session is ' + self._state);
|
|
765
|
+
return new Promise(function (resolve, reject) {
|
|
766
|
+
var stream = openWtStream(self._h3, self._quic, self._sessionId, bidi, options,
|
|
767
|
+
function (id, kind, prefix) { return self._newStream(id, kind, prefix); },
|
|
768
|
+
function (err, st) {
|
|
769
|
+
if (!err) resolve(st);
|
|
770
|
+
else reject(self._state !== 'connected' ? invalidState('the session is ' + self._state) : new WebTransportError(err.message, { source: 'session' }));
|
|
771
|
+
});
|
|
772
|
+
if (!stream) reject(new DOMException('the server allows no more streams now', 'QuotaExceededError'));
|
|
773
|
+
});
|
|
774
|
+
}, function () {
|
|
775
|
+
throw invalidState('the session was not established');
|
|
516
776
|
});
|
|
517
777
|
}
|
|
518
778
|
|
|
@@ -558,18 +818,70 @@ class WebTransport extends EventEmitter {
|
|
|
558
818
|
return this._incomingUni || (this._incomingUni = this._incomingStreams('unidirectionalStream'));
|
|
559
819
|
}
|
|
560
820
|
|
|
561
|
-
/** The
|
|
821
|
+
/** The server's answer to the CONNECT (an immutable Headers) — null until the session is up. */
|
|
822
|
+
get responseHeaders() { return this._responseHeaders; }
|
|
823
|
+
|
|
824
|
+
/** As the browser's: no WebTransport over HTTP/2 (reliable-only) here — HTTP/3 alone. */
|
|
825
|
+
static get supportsReliableOnly() { return false; }
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* The browser's exportKeyingMaterial(label, context, outputLength): keying
|
|
829
|
+
* material of this session's own, from the TLS exporter (see wt_stream's
|
|
830
|
+
* exportSessionKeyingMaterial) — a Promise of a Uint8Array. While the
|
|
831
|
+
* session is being set up, it waits for it; a session that is over (or
|
|
832
|
+
* ends meanwhile) is an InvalidStateError.
|
|
833
|
+
*/
|
|
834
|
+
exportKeyingMaterial(label, context, outputLength) {
|
|
835
|
+
var self = this, a;
|
|
836
|
+
try { a = exporterArgs(arguments); } catch (e) { return Promise.reject(e); }
|
|
837
|
+
if (this._state === 'closed' || this._state === 'failed') return Promise.reject(invalidState('the session is ' + this._state));
|
|
838
|
+
var up = this._state === 'connected' ? Promise.resolve() : this.ready;
|
|
839
|
+
return up.then(function () {
|
|
840
|
+
if (self._state !== 'connected') throw invalidState('the session is ' + self._state);
|
|
841
|
+
return exportSessionKeyingMaterial(self._quic, self._sessionId, a);
|
|
842
|
+
}, function () { throw invalidState('the session was not established'); });
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** The browser's WebTransportSendGroup — of this session's alone. */
|
|
562
846
|
createSendGroup() {
|
|
563
|
-
return createSendGroup();
|
|
847
|
+
return createSendGroup(this);
|
|
564
848
|
}
|
|
565
849
|
|
|
566
850
|
/**
|
|
567
851
|
* The browser's getStats(): the connection's figures, as QUIC has them.
|
|
852
|
+
* A session that failed has none: InvalidStateError, as the browser's.
|
|
568
853
|
*/
|
|
569
854
|
getStats() {
|
|
855
|
+
if (this._state === 'failed') return Promise.reject(invalidState('the session failed'));
|
|
570
856
|
return sessionStats(this._session); // wt_stream: as the server's res.getStats()
|
|
571
857
|
}
|
|
572
858
|
|
|
859
|
+
// The browser's anticipatedConcurrentIncoming… hints: null or 0 to 65535
|
|
860
|
+
// (a TypeError otherwise, the value unchanged). Given to the constructor,
|
|
861
|
+
// they raise the stream limits we grant the server at the start, if they
|
|
862
|
+
// are above them (see _hintedTransportParams).
|
|
863
|
+
get anticipatedConcurrentIncomingUnidirectionalStreams() { return this._hintUni; }
|
|
864
|
+
set anticipatedConcurrentIncomingUnidirectionalStreams(v) { this._hintUni = streamHint(v, 'anticipatedConcurrentIncomingUnidirectionalStreams'); }
|
|
865
|
+
get anticipatedConcurrentIncomingBidirectionalStreams() { return this._hintBidi; }
|
|
866
|
+
set anticipatedConcurrentIncomingBidirectionalStreams(v) { this._hintBidi = streamHint(v, 'anticipatedConcurrentIncomingBidirectionalStreams'); }
|
|
867
|
+
|
|
868
|
+
// options.transportParams, with the hints applied: the server may open as
|
|
869
|
+
// many streams as anticipated from the start (unidirectional ones on top
|
|
870
|
+
// of the three HTTP/3 keeps for itself). A limit set in transportParams is
|
|
871
|
+
// the caller's and stays.
|
|
872
|
+
_hintedTransportParams() {
|
|
873
|
+
var given = this._options.transportParams || {};
|
|
874
|
+
var base = defaultTransportParams(given);
|
|
875
|
+
var tp = Object.assign({}, given);
|
|
876
|
+
if (this._hintBidi !== null && given.initialMaxStreamsBidi == null && this._hintBidi > base.initial_max_streams_bidi) {
|
|
877
|
+
tp.initialMaxStreamsBidi = this._hintBidi;
|
|
878
|
+
}
|
|
879
|
+
if (this._hintUni !== null && given.initialMaxStreamsUni == null && this._hintUni + 3 > base.initial_max_streams_uni) {
|
|
880
|
+
tp.initialMaxStreamsUni = this._hintUni + 3;
|
|
881
|
+
}
|
|
882
|
+
return tp;
|
|
883
|
+
}
|
|
884
|
+
|
|
573
885
|
/**
|
|
574
886
|
* Send an unreliable datagram.
|
|
575
887
|
*/
|
|
@@ -595,21 +907,82 @@ class WebTransport extends EventEmitter {
|
|
|
595
907
|
/**
|
|
596
908
|
* Close the WebTransport session.
|
|
597
909
|
*/
|
|
910
|
+
// The QUIC connection is over: 'closing' (the peer's CONNECTION_CLOSE, or
|
|
911
|
+
// ours) — as the browser hears it, at once — or 'close' at the end of the
|
|
912
|
+
// closing/draining period, whichever comes first; the second finds it done.
|
|
913
|
+
_connectionEnded() {
|
|
914
|
+
// Gone before the CONNECT was answered — after the QUIC handshake, so
|
|
915
|
+
// onError did not see it. `ready` must settle (it rejects in the
|
|
916
|
+
// browser too); left pending, the caller would wait forever.
|
|
917
|
+
if (this._state === 'connecting') {
|
|
918
|
+
var err = new Error('WebTransport connection closed before the session was established');
|
|
919
|
+
err.code = 'ERR_WEBTRANSPORT_SESSION_FAILED';
|
|
920
|
+
this._failSetup(err);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
// Lost while established: neither a CLOSE_WEBTRANSPORT_SESSION, nor
|
|
924
|
+
// the CONNECT stream's FIN, nor our close() — the connection went
|
|
925
|
+
// (an error, the idle timeout, the server gone). `closed` rejects,
|
|
926
|
+
// as the browser's does; it used to resolve { closeCode: 0 }, which
|
|
927
|
+
// is a close the peer asked for.
|
|
928
|
+
if (this._state === 'connected') { this._sessionLost('the connection was lost'); return; }
|
|
929
|
+
this._sessionClosed();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// The session ended without being closed — the connection lost, or the
|
|
933
|
+
// CONNECT stream reset. As the browser's: the state is 'failed', `closed`
|
|
934
|
+
// rejects with a WebTransportError (source 'session'), and every stream,
|
|
935
|
+
// the incoming-stream queues and the datagrams fail with that same error
|
|
936
|
+
// object; 'close' is emitted all the same, so a Node listener hears the end
|
|
937
|
+
// either way. The QUIC connection, this session's alone, goes with it.
|
|
938
|
+
_sessionLost(message) {
|
|
939
|
+
if (this._closedDone) return;
|
|
940
|
+
this._state = 'failed';
|
|
941
|
+
this._closedDone = true;
|
|
942
|
+
var err = new WebTransportError('WebTransport session lost: ' + message, { source: 'session' });
|
|
943
|
+
setErrorCode(err, 'ERR_WEBTRANSPORT_SESSION_LOST');
|
|
944
|
+
var pc = this._quic && this._quic.context && this._quic.context.peer_close;
|
|
945
|
+
if (pc) err.quicError = { code: pc.error, application: !!pc.application, reason: pc.reason || '' };
|
|
946
|
+
this._destroyStreams(err, false);
|
|
947
|
+
this._closedReject(err);
|
|
948
|
+
this.emit('close');
|
|
949
|
+
if (this._quic) { try { this._quic.close(0x0100, 'WebTransport session lost', true); } catch (e) {} }
|
|
950
|
+
}
|
|
951
|
+
|
|
598
952
|
// The session is over: `closed` resolves and 'close' is emitted, once. As
|
|
599
953
|
// in the browser, closed resolves with the code and reason the session was
|
|
600
954
|
// closed with — the server's CLOSE_WEBTRANSPORT_SESSION, or ours — and
|
|
601
955
|
// { closeCode: 0, reason: '' } when there was none. At the session's close,
|
|
602
956
|
// not the QUIC connection's: that one lingers in its closing period
|
|
603
957
|
// (RFC 9000 §10.2) for three PTOs after, and `closed` waited it out.
|
|
604
|
-
|
|
958
|
+
// streamError: what the streams' browser views fail with (see close()).
|
|
959
|
+
_sessionClosed(streamError) {
|
|
605
960
|
if (this._closedDone) return;
|
|
606
961
|
this._closedDone = true;
|
|
607
|
-
this._destroyStreams();
|
|
962
|
+
this._destroyStreams(streamError || new WebTransportError('the session is closed', { source: 'session' }), true);
|
|
608
963
|
this._closedResolve(this.closeInfo || { closeCode: 0, reason: '' });
|
|
609
964
|
this.emit('close');
|
|
610
965
|
}
|
|
611
966
|
|
|
967
|
+
// The browser's close(): before the session is up, it is abandoned —
|
|
968
|
+
// `ready` and `closed` reject with a WebTransportError (source 'session')
|
|
969
|
+
// and the connection goes; `ready` used to resolve all the same once the
|
|
970
|
+
// server answered. Once up, CLOSE_WEBTRANSPORT_SESSION with the code and
|
|
971
|
+
// reason, and `closed` resolves with them (the reason as given; what goes
|
|
972
|
+
// on the wire is cut at 1024 bytes); the streams' views fail with an
|
|
973
|
+
// AbortError, as the browser's.
|
|
612
974
|
close(info) {
|
|
975
|
+
if (this._state === 'closed' || this._state === 'failed') return;
|
|
976
|
+
if (this._state === 'connecting') {
|
|
977
|
+
this._failSetup(new WebTransportError('the session was closed before it was established', { source: 'session' }));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this._close(info, new DOMException('the session was closed', 'AbortError'));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Closed: by us (close()), or by the server (its CLOSE_WEBTRANSPORT_SESSION
|
|
984
|
+
// or the CONNECT stream's FIN — then a WebTransportError for the streams).
|
|
985
|
+
_close(info, streamError) {
|
|
613
986
|
if (this._state === 'closed' || this._state === 'failed') return;
|
|
614
987
|
this._state = 'closed';
|
|
615
988
|
|
|
@@ -630,7 +1003,7 @@ class WebTransport extends EventEmitter {
|
|
|
630
1003
|
closeQuic();
|
|
631
1004
|
}
|
|
632
1005
|
}
|
|
633
|
-
this._sessionClosed();
|
|
1006
|
+
this._sessionClosed(streamError || new WebTransportError('the server closed the session', { source: 'session' }));
|
|
634
1007
|
}
|
|
635
1008
|
|
|
636
1009
|
/**
|
|
@@ -644,9 +1017,11 @@ class WebTransport extends EventEmitter {
|
|
|
644
1017
|
* URL this session connected to.
|
|
645
1018
|
*/
|
|
646
1019
|
get url() {
|
|
647
|
-
|
|
1020
|
+
var h = this._hostname.indexOf(':') >= 0 ? '[' + this._hostname + ']' : this._hostname;
|
|
1021
|
+
return 'https://' + h + ':' + this._port + this._path;
|
|
648
1022
|
}
|
|
649
1023
|
}
|
|
650
1024
|
|
|
651
1025
|
|
|
652
|
-
export { WebTransport, WebTransportError
|
|
1026
|
+
export { WebTransport, WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
|
|
1027
|
+
WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream };
|