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