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/src/h3_server.js CHANGED
@@ -12,10 +12,25 @@
12
12
  * https://github.com/colocohen/quico
13
13
  */
14
14
 
15
- import { Duplex, Readable, Writable } from 'node:stream';
16
- import { Emitter, readVarInt, wt_datagram_payload } from './utils.js';
15
+ import { Duplex } from 'node:stream';
16
+ import { X509Certificate } from 'node:crypto';
17
+ import { readVarInt, writeVarInt, wt_datagram_payload } from './utils.js';
18
+ import { observeH3 } from './h2_fingerprint.js';
19
+ // The Emitter from http_common, not the one in utils.js. Two differences that
20
+ // matter here: it has listenerCount (needed to tell whether anyone wants an
21
+ // 'upgrade'), and it does NOT swallow exceptions thrown by listeners — the
22
+ // utils.js one wraps every call in a try/catch and discards what it catches,
23
+ // which is how a handler that throws becomes silence.
24
+ import { Emitter, isConnectionSpecific, runRequestHandler, validateHeaderName, validateHeaderValue, validateStatus,
25
+ DEFAULT_HEADERS_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, startDeadline, responsePriority,
26
+ sfString, parseSfStringList, maxRequestsPerSocket, trailerFields, statusHasNoBody, runEarlyDataFilter, httpDate, streamResetLimiter } from './http_common.js';
17
27
  import { createQuicServer } from './quic_server.js';
18
- import { H3Connection } from './h3.js';
28
+ import { pickTlsOptions } from './tls_options.js';
29
+ import { _makeSocketStub } from './streams.js';
30
+ import { H3Connection, H3_ERR, wtCloseCapsule, wtDrainCapsule, createCapsuleReader, h3SettingsContext } from './h3.js';
31
+ import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats,
32
+ exporterArgs, exportSessionKeyingMaterial } from './wt_stream.js';
33
+ import { contextZero, datagramCapsule, STREAM_QUEUE_MAX } from './connect_udp.js';
19
34
 
20
35
 
21
36
  // ============================================================
@@ -27,25 +42,129 @@ function createServer(options, handler) {
27
42
 
28
43
  var ev = Emitter();
29
44
 
45
+ // node:http's shouldUpgradeCallback, for extended CONNECT (RFC 9220): taken
46
+ // up as an 'upgrade' (true) or handled as an ordinary request (false).
47
+ // Default: true when something listens for 'upgrade' — counted here, since
48
+ // this Emitter has no listenerCount (so the old "nobody listens" check
49
+ // could never fire).
50
+ var upgradeListeners = 0;
51
+ var shouldUpgradeCallback = typeof options.shouldUpgradeCallback === 'function'
52
+ ? options.shouldUpgradeCallback
53
+ : function () { return upgradeListeners > 0; };
54
+
55
+ // CONNECT-UDP (RFC 9298), from server.js: { wants(req), handle(req,
56
+ // carrier) } — whether a request is to be proxied, and the proxy
57
+ // (connect_udp.js) that takes it with the carrier built here.
58
+ var connectUdp = options.connectUdpHandler || null;
59
+
30
60
  // Transport-only QUIC server owns the UDP socket(s), demux, connection
31
61
  // map and lifecycle. h3_server layers HTTP/3 on top by listening for its
32
62
  // 'connection' event (see below) and wrapping each raw QUICConnection in
33
63
  // an H3Connection. ALPN is 'h3'. External-socket (shared-port) mode and
34
64
  // the handlePacket/hasConnection demux hooks are provided by the
35
65
  // transport server and re-exported on this server's API.
36
- var qserver = createQuicServer({
37
- alpn: 'h3',
66
+ // 'h3' plus whatever else the application asked the handshake to accept.
67
+ // Anything but 'h3' is closed once the handshake settles on it — see the
68
+ // 'connect' handler below.
69
+ var alpn = options.alpn || ['h3'];
70
+
71
+ // Datagrams an application sends between accepting a WebTransport session
72
+ // and its 200 leaving, kept — the oldest dropped past this many.
73
+ var WT_DATAGRAMS_HELD = 256;
74
+
75
+ // ---- How long a request may take to arrive (see http_common) ----
76
+ //
77
+ // headersTimeout: from a request stream's first byte to its whole HEADERS
78
+ // frame. Its streams are independent, so a slow head costs only itself:
79
+ // reset, H3_REQUEST_INCOMPLETE, before any request exists. It was not
80
+ // timed at all — a HEADERS frame sent a byte at a time held its stream, and
81
+ // its share of the connection's stream limit, for as long as it went on.
82
+ // requestTimeout: from the head to the request's last byte; past it the
83
+ // stream is reset both ways and the request ends as a cancelled one does.
84
+ // Not for CONNECT (a WebSocket or a WebTransport session): its stream is
85
+ // meant to stay open.
86
+ var headersTimeout = options.headersTimeout != null ? options.headersTimeout : DEFAULT_HEADERS_TIMEOUT;
87
+ // Rapid Reset (http_common streamResetLimiter): the options checked now.
88
+ streamResetLimiter(options);
89
+ // 0-RTT requests: the application's judge (earlyGate), or the default.
90
+ if (options.earlyDataFilter != null && typeof options.earlyDataFilter !== 'function') {
91
+ var efe = new TypeError('earlyDataFilter must be a function (req, callback)'); efe.code = 'ERR_INVALID_ARG_TYPE'; throw efe;
92
+ }
93
+ var earlyDataFilter = options.earlyDataFilter || null;
94
+ // node:http's maxRequestsPerSocket: GOAWAY after the Nth request (0: none).
95
+ var maxRequests = maxRequestsPerSocket(options.maxRequestsPerSocket);
96
+ var requestTimeout = options.requestTimeout != null ? options.requestTimeout : DEFAULT_REQUEST_TIMEOUT;
97
+
98
+ function armHeadersDeadline(conn, streamId) {
99
+ var t = startDeadline(headersTimeout, function () {
100
+ if (conn.headerDeadlines[streamId] !== t) return;
101
+ delete conn.headerDeadlines[streamId];
102
+ // H3_REQUEST_REJECTED: nothing of the request was processed — no
103
+ // headers to process — so the client may send it again (RFC 9114
104
+ // §4.1.1), as it may not after H3_REQUEST_INCOMPLETE.
105
+ if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_REQUEST_REJECTED);
106
+ });
107
+ if (t) conn.headerDeadlines[streamId] = t;
108
+ }
109
+ function clearHeadersDeadline(conn, streamId) {
110
+ var t = conn.headerDeadlines[streamId];
111
+ if (t) { clearTimeout(t); delete conn.headerDeadlines[streamId]; }
112
+ }
113
+
114
+ function armRequestDeadline(conn, streamId, stream) {
115
+ stream.deadline = startDeadline(requestTimeout, function () {
116
+ stream.deadline = null;
117
+ if (conn.http_streams[streamId] !== stream || stream.req._ended) return;
118
+ if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_REQUEST_INCOMPLETE);
119
+ releaseHttpStream(conn, streamId, true);
120
+ });
121
+ }
122
+ function clearRequestDeadline(stream) {
123
+ if (stream && stream.deadline) { clearTimeout(stream.deadline); stream.deadline = null; }
124
+ }
125
+ if (!Array.isArray(alpn)) alpn = [alpn];
126
+ if (alpn.indexOf('h3') === -1) alpn = ['h3'].concat(alpn);
127
+
128
+ var qserver = createQuicServer(Object.assign({
129
+ alpn: alpn,
38
130
  SNICallback: options.SNICallback || null,
131
+ ticketKeys: options.ticketKeys || null,
132
+ ticketData: options.ticketData || null,
133
+ ech: options.ech || null,
134
+ ocspRequest: options.ocspRequest || null,
135
+ resetTokenSecret: options.resetTokenSecret || null,
136
+ tokenSecret: options.tokenSecret || null,
137
+ validateAddress: options.validateAddress === true,
138
+ handshakeLoadThreshold: options.handshakeLoadThreshold,
139
+ earlyData: options.earlyData, earlyDataMaxAge: options.earlyDataMaxAge, earlyDataReplay: options.earlyDataReplay,
140
+ // 0-RTT is taken only under the SETTINGS this server sends now: a client
141
+ // may send early data under the ones it remembered (RFC 9114 §7.2.4.2).
142
+ earlyDataAppContext: h3SettingsContext({ enableWebTransport: true, maxWebTransportSessions: options.maxWebTransportSessions }),
143
+ maxHandshakesPerAddress: options.maxHandshakesPerAddress,
39
144
  key: options.key,
40
145
  cert: options.cert,
41
146
  ca: options.ca,
42
147
  maxConnections: options.maxConnections || 10000,
148
+ transportParams: options.transportParams || null,
149
+ initialCongestionWindow: options.initialCongestionWindow,
150
+ carefulResume: options.carefulResume,
151
+ startupCap: options.startupCap,
152
+ pmtuDiscovery: options.pmtuDiscovery,
153
+ maxUdpPayloadSize: options.maxUdpPayloadSize,
154
+ initialRtt: options.initialRtt,
155
+ reorderWindow: options.reorderWindow,
156
+ connectionOptions: options.connectionOptions,
157
+ quicVersions: options.quicVersions,
43
158
  socket: options.socket || null,
44
- socket6: options.socket6 || null
45
- });
159
+ socket6: options.socket6 || null,
160
+ greaseQuicBit: options.greaseQuicBit,
161
+ // The server's counters (server_stats.js), when it keeps them.
162
+ stats: options.stats || null
163
+ }, pickTlsOptions(options))); // node:tls's options (tls_options.js)
46
164
 
47
165
  // Surface transport-level (UDP socket / send) errors on this server.
48
166
  qserver.on('error', function (err) { ev.emit('error', err); });
167
+ qserver.on('handshakeLoad', function (e) { ev.emit('handshakeLoad', e); });
49
168
 
50
169
 
51
170
  // ============================================================
@@ -65,40 +184,357 @@ function createServer(options, handler) {
65
184
  }
66
185
  }
67
186
 
68
- function _createWtStream(conn, quic, wtStreamId, sessionId, isBidi) {
69
- if (!conn._wtStreamObjects) conn._wtStreamObjects = {};
70
-
71
- var stream;
72
- if (isBidi) {
73
- stream = new Duplex({
74
- read: function () {},
75
- write: function (chunk, encoding, cb) {
76
- if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
77
- quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
78
- cb();
79
- },
80
- final: function (cb) {
81
- quic.sendStream(wtStreamId, new Uint8Array(0), true);
82
- cb();
83
- }
84
- });
85
- } else {
86
- stream = new Readable({ read: function () {} });
187
+ // ── Stream lifecycle: one exit ────────────────────────────────────────
188
+ //
189
+ // releaseHttpStream is the ONLY place this server drops what it holds for
190
+ // a stream — an entry in conn.http_streams (a request, a tunnel, a
191
+ // WebTransport session) or in conn._wtStreamObjects (a WebTransport
192
+ // stream) — and every way a stream can end comes through it:
193
+ //
194
+ // normally (aborted = false)
195
+ // a request over in both directions → releaseIfDone
196
+ // a tunnel's FIN, or a refused tunnel → tunnel_data / reject / 501
197
+ // a WebTransport stream's FIN → wt_stream / wt_data
198
+ // our own WT stream finished → its 'close'
199
+ // a session's FIN (CONNECT stream) → request_end
200
+ // cut short (aborted = true)
201
+ // the peer cancelled (RESET / STOP_SENDING) → reportCancel
202
+ // we ended it over what the peer sent → 'stream_error'
203
+ // its WebTransport session ended → releaseSessionStreams
204
+ // the connection closed → quic 'close'
205
+ //
206
+ // What each kind hears is decided here, once:
207
+ // a request cut short → 'aborted' — the counterpart of h2_server's
208
+ // abortStream; the unified server turns it into
209
+ // IncomingMessage 'aborted' and ServerResponse
210
+ // 'close'. Never an Error: a browser cancelling
211
+ // a request is ordinary traffic, and an uncaught
212
+ // error for it would take the process down.
213
+ // a tunnel cut short → its Duplex destroyed
214
+ // a WT stream cut short → destroyed
215
+ // a WT session, however → 'close' once (and 'aborted' if cut short), and
216
+ // every stream still open in it released too
217
+ // Idempotent: a second end for the same stream finds nothing.
218
+ function releaseHttpStream(conn, streamId, aborted) {
219
+ var wtStream = conn._wtStreamObjects[streamId];
220
+ if (wtStream) {
221
+ delete conn._wtStreamObjects[streamId];
222
+ if (aborted) { try { wtStream.destroy(); } catch (e) {} }
87
223
  }
88
224
 
89
- stream._streamId = wtStreamId;
90
- stream._sessionId = sessionId;
91
- stream._pushData = function (data) {
92
- if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
93
- stream.push(data);
94
- };
95
- stream._endData = function () { stream.push(null); };
225
+ var stream = conn.http_streams[streamId];
226
+ if (!stream) return;
227
+ delete conn.http_streams[streamId];
228
+ clearRequestDeadline(stream);
229
+ var req = stream.req;
230
+ // Its body's credit, if the layer above was returning it as it read: the
231
+ // request is over, so nothing more is waited for.
232
+ if (conn.h3) conn.h3.releaseBody(streamId);
233
+
234
+ if (stream.tunnel) {
235
+ if (aborted) { try { stream.tunnel.destroy(); } catch (e) {} }
236
+ if (conn.goaway) closeAfterGoaway(conn);
237
+ return;
238
+ }
239
+ if (stream.udp) {
240
+ var quarter = Math.floor(Number(streamId) / 4);
241
+ if (conn.udpByQuarter[quarter] === Number(streamId)) delete conn.udpByQuarter[quarter];
242
+ // Cut short under the tunnel (a reset, the connection gone): it closes.
243
+ if (aborted && stream.udp.sink) stream.udp.sink.abort();
244
+ if (conn.goaway) closeAfterGoaway(conn);
245
+ return;
246
+ }
247
+ if (aborted && !req.aborted) {
248
+ req.aborted = true;
249
+ var listeners = req._listeners['aborted'] || [];
250
+ for (var i = 0; i < listeners.length; i++) {
251
+ try { listeners[i](); } catch (e) {}
252
+ }
253
+ }
254
+ if (req._isWebTransport) {
255
+ if (conn.wtByQuarter[Math.floor(Number(streamId) / 4)] === Number(streamId)) delete conn.wtByQuarter[Math.floor(Number(streamId) / 4)];
256
+ // FIN on the CONNECT stream ends the WebTransport session — it is what
257
+ // a browser's wt.close() sends — as does a reset of it, or the
258
+ // connection going. 'close' used to come only with the whole QUIC
259
+ // connection, and a browser that closed the session but kept the
260
+ // connection for its fetches left the session standing here.
261
+ if (!req._wtClosed) {
262
+ req._wtClosed = true;
263
+ if (req._wtDatagrams) req._wtDatagrams._close();
264
+ _emitOnReq(req, 'close');
265
+ }
266
+ releaseSessionStreams(conn, Number(streamId));
267
+ }
268
+ if (conn.goaway) closeAfterGoaway(conn);
269
+ }
270
+
271
+ // ---- Graceful shutdown (RFC 9114 §5.2) ----
272
+ //
273
+ // The intents shared with h2_server's handle (CONN_OVERLOADED is
274
+ // 'overloaded'), as HTTP/3 connection error codes: one piece of limiting
275
+ // code drives both versions.
276
+ function h3ConnCode(code) {
277
+ if (typeof code === 'number') return code;
278
+ if (code === 'overloaded') return H3_ERR.H3_EXCESSIVE_LOAD;
279
+ return H3_ERR.H3_NO_ERROR;
280
+ }
281
+
282
+ // After GOAWAY, once the last request accepted has finished and everything
283
+ // sent on the connection has been acknowledged, the connection is closed
284
+ // ("MAY initiate an immediate closure", §5.2). Not at the last res.end():
285
+ // CONNECTION_CLOSE stops all sending, and the last responses would be cut
286
+ // off in flight.
287
+ function closeAfterGoaway(conn) {
288
+ if (conn.goaway.closing || Object.keys(conn.http_streams).length > 0) return;
289
+ conn.goaway.closing = true;
290
+ conn.quic.whenAllSent(function () {
291
+ try { conn.quic.close(conn.goaway.code, 'goaway', true); } catch (e) {}
292
+ });
293
+ }
294
+
295
+ // Every WebTransport stream still open in a session that has ended.
296
+ // Each goes with WT_SESSION_GONE (draft-ietf-webtrans-http3 §4.6) — not
297
+ // with application code 0, which it got when its session's end reached it
298
+ // here before h3's endWtSession did (the peer's STOP_SENDING on the CONNECT
299
+ // stream: released first, then stopped).
300
+ function releaseSessionStreams(conn, sessionId) {
301
+ for (var k in conn._wtStreamObjects) {
302
+ var child = conn._wtStreamObjects[k];
303
+ if (child && child._sessionId === sessionId) {
304
+ if (child._sessionGone) child._sessionGone();
305
+ releaseHttpStream(conn, k, true);
306
+ }
307
+ }
308
+ }
96
309
 
310
+ /**
311
+ * Release a request's entry once it is over in both directions: the
312
+ * request fully received (FIN, 'request_end') and the response ended
313
+ * (res.end).
314
+ *
315
+ * Nothing removed it before, except a cancel, a tunnel's end, or a 501 —
316
+ * so every request served stayed here, with the IncomingMessage and
317
+ * ServerResponse wrapped around it, for as long as the QUIC connection
318
+ * lived. A browser keeps that connection open while a WebTransport session
319
+ * runs on it, and sends every fetch for the origin over it too: the map grew
320
+ * without bound, and the datagram handler below walks it for every datagram.
321
+ *
322
+ * A tunnel and a WebTransport session have their own ends (tunnel_data FIN,
323
+ * 'request_end' on the CONNECT stream) and are left to them.
324
+ */
325
+ function releaseIfDone(conn, streamId) {
326
+ var stream = conn.http_streams[streamId];
327
+ if (!stream || stream.tunnel || stream.udp || stream.req._isWebTransport) return;
328
+ if (stream.req._ended && stream.res._finished) releaseHttpStream(conn, streamId, false);
329
+ }
330
+
331
+ // A WebTransport stream (wt_stream.js — the client builds its own the same
332
+ // way), recorded on its connection so a session's end can find it, and
333
+ // forgotten when it closes.
334
+ // prefixLength: on a stream we opened, the prefix h3 wrote on it.
335
+ function _createWtStream(conn, quic, wtStreamId, sessionId, kind, prefixLength) {
336
+ var session = conn.http_streams[sessionId];
337
+ var stream = createWtStream(quic, wtStreamId, kind, function () {
338
+ if (conn._wtStreamObjects[wtStreamId] === stream) delete conn._wtStreamObjects[wtStreamId];
339
+ if (conn.h3) conn.h3.releaseWebTransportStream(wtStreamId);
340
+ }, prefixLength, session && session.req._wtSched, sessionId);
97
341
  conn._wtStreamObjects[wtStreamId] = stream;
98
342
  return stream;
99
343
  }
100
344
 
101
345
 
346
+ /**
347
+ * A Duplex over one QUIC stream, for an extended-CONNECT tunnel.
348
+ *
349
+ * The same shape h2_server builds over an HTTP/2 stream, and for the same
350
+ * reason: ws_server was written against a socket, so handing it one of these
351
+ * is the whole integration — the WebSocket framing never learns whether its
352
+ * bytes are TCP, HTTP/2 DATA frames, or QUIC stream bytes.
353
+ *
354
+ * What QUIC adds for free is worth naming: each of these is an independent
355
+ * QUIC stream, so a WebSocket that stalls does not hold up the others on the
356
+ * connection. Over HTTP/2 they all share one TCP byte stream and it does.
357
+ */
358
+ function _createTunnelDuplex(conn, streamId, req) {
359
+ var accepted = false;
360
+
361
+ var duplex = new Duplex({
362
+ read: function () {},
363
+ // Destroyed by the application (ws.terminate(), socket.destroy()) while
364
+ // the tunnel is open: the stream cancelled both ways. It was left open
365
+ // — the peer never heard the tunnel had gone. Destroyed because the
366
+ // stream already ended (releaseHttpStream), it is no longer listed.
367
+ destroy: function (err, cb) {
368
+ if (conn.http_streams[streamId] && conn.http_streams[streamId].tunnel === duplex) {
369
+ if (conn.h3) conn.h3.abortStream(streamId, err ? H3_ERR.H3_INTERNAL_ERROR : H3_ERR.H3_REQUEST_CANCELLED);
370
+ releaseHttpStream(conn, streamId, false);
371
+ }
372
+ cb(err);
373
+ },
374
+ write: function (chunk, encoding, cb) {
375
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
376
+ var u8 = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
377
+ if (conn.h3) conn.h3.sendTunnelData(streamId, u8, false);
378
+ // Backpressure, as for a response (whenStreamSent in quic_connection).
379
+ if (conn.quic) conn.quic.whenStreamSent(Number(streamId), duplex.writableHighWaterMark, cb); else cb();
380
+ },
381
+ final: function (cb) {
382
+ if (conn.h3) conn.h3.sendTunnelData(streamId, new Uint8Array(0), true);
383
+ cb();
384
+ }
385
+ });
386
+
387
+ duplex._streamId = streamId;
388
+ duplex.encrypted = true; // QUIC is always encrypted
389
+ duplex.setNoDelay = function () { return duplex; };
390
+ duplex.setKeepAlive = function () { return duplex; };
391
+ duplex.setTimeout = function () { return duplex; };
392
+
393
+ /**
394
+ * Accept the tunnel.
395
+ *
396
+ * HTTP/3 has no 101 any more than HTTP/2 does: a 2xx accepts (RFC 9220
397
+ * §3). The headers go out WITHOUT FIN — the stream stays open in both
398
+ * directions, which is the entire point — and only then does the stream
399
+ * leave HTTP/3 framing.
400
+ */
401
+ duplex._h2Accept = function (extraHeaders) {
402
+ if (accepted) return;
403
+ accepted = true;
404
+ var h = { ':status': '200' };
405
+ for (var n in (extraHeaders || {})) h[String(n).toLowerCase()] = extraHeaders[n];
406
+ if (conn.h3) {
407
+ conn.h3.sendHeaders(streamId, h, false);
408
+ conn.h3.registerTunnelStream(streamId);
409
+ }
410
+ };
411
+
412
+ /** Refuse it, with a status. */
413
+ duplex._h2Reject = function (status, message) {
414
+ if (accepted) return;
415
+ accepted = true;
416
+ if (conn.h3) {
417
+ conn.h3.sendHeaders(streamId, { ':status': String(status) }, true);
418
+ // Whatever the client sent ahead of our answer is held for a tunnel
419
+ // that will now never open: drop it and stop reading.
420
+ conn.h3.stopReading(streamId, H3_ERR.H3_REQUEST_REJECTED);
421
+ }
422
+ releaseHttpStream(conn, streamId, false);
423
+ };
424
+
425
+ return duplex;
426
+ }
427
+
428
+
429
+ // ============================================================
430
+ // CONNECT-UDP over HTTP/3 — the carrier (see connect_udp.js)
431
+ //
432
+ // UDP payloads travel as HTTP Datagrams: a QUIC DATAGRAM frame each —
433
+ // Quarter Stream ID, Context ID 0, the payload — when both sides
434
+ // negotiated them (SETTINGS_H3_DATAGRAM, and QUIC's max_datagram_frame_
435
+ // size); otherwise as DATAGRAM capsules in the stream's DATA frames. A
436
+ // payload larger than a datagram can carry is dropped, not moved to the
437
+ // stream (RFC 9298 §5: "SHOULD NOT send the UDP payload in a DATAGRAM
438
+ // capsule") — a reliable, ordered copy of a UDP packet is not one.
439
+ // ============================================================
440
+
441
+ var UDP_HELD_MAX = 64; // datagrams waiting for the 2xx to leave
442
+
443
+ function startConnectUdp(conn, streamId, stream) {
444
+ var sid = Number(streamId);
445
+ var quarter = Math.floor(sid / 4);
446
+ var accepted = false, closed = false, busy = false;
447
+ var held = null; // until the 2xx has left: a datagram before it is one the client drops
448
+
449
+ function datagramsOn() {
450
+ return !!(conn.h3 && conn.h3.context.remote_datagram_support === true && conn.quic.maxDatagramSize() > 0);
451
+ }
452
+ function done() {
453
+ if (conn.http_streams[streamId] === stream) releaseHttpStream(conn, streamId, false);
454
+ }
455
+
456
+ var carrier = {
457
+ httpVersion: '3.0',
458
+ connKey: conn,
459
+ sink: null, // set by the proxy: datagram(u8), data(u8), end(), abort()
460
+ peerEnded: false, // the client's FIN came
461
+ mode: function () { return datagramsOn() ? 'datagram' : 'capsule'; },
462
+
463
+ accept: function (headers) {
464
+ if (accepted || closed || !conn.h3) return;
465
+ accepted = true;
466
+ var h = { ':status': '200' };
467
+ for (var n in (headers || {})) h[n] = String(headers[n]);
468
+ conn.h3.sendHeaders(streamId, h, false);
469
+ held = [];
470
+ conn.quic.whenStreamSent(sid, 0, function () {
471
+ var list = held; held = null;
472
+ for (var i = 0; list && i < list.length; i++) conn.quic.sendDatagram(list[i], null, null);
473
+ });
474
+ },
475
+
476
+ reject: function (status, headers) {
477
+ if (accepted || closed || !conn.h3) return;
478
+ closed = true;
479
+ var h = { ':status': String(status) };
480
+ for (var n in (headers || {})) h[n] = String(headers[n]);
481
+ conn.h3.sendHeaders(streamId, h, true);
482
+ if (!carrier.peerEnded) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
483
+ done();
484
+ },
485
+
486
+ sendPayload: function (u8) {
487
+ if (!accepted || closed || !conn.h3) return false;
488
+ if (datagramsOn()) {
489
+ var ctx = contextZero(u8);
490
+ var q = writeVarInt(quarter);
491
+ var dg = new Uint8Array(q.byteLength + ctx.byteLength);
492
+ dg.set(q, 0); dg.set(ctx, q.byteLength);
493
+ if (dg.byteLength > conn.quic.maxDatagramSize()) return false;
494
+ if (held) { if (held.length >= UDP_HELD_MAX) return false; held.push(dg); return true; }
495
+ return conn.quic.sendDatagram(dg, null, null);
496
+ }
497
+ // Capsules: as much waiting on the stream as STREAM_QUEUE_MAX, and
498
+ // past it dropped, as a full UDP queue drops.
499
+ if (busy) return false;
500
+ conn.h3.sendBody(streamId, datagramCapsule(u8), false);
501
+ busy = true;
502
+ conn.quic.whenStreamSent(sid, STREAM_QUEUE_MAX, function () { busy = false; });
503
+ return true;
504
+ },
505
+
506
+ // Ours ends: FIN — and, if the client has not ended its side, it is told
507
+ // to stop sending (H3_NO_ERROR: nothing went wrong). Over.
508
+ end: function () {
509
+ if (closed || !conn.h3) return;
510
+ closed = true;
511
+ if (!accepted) conn.h3.sendHeaders(streamId, { ':status': '503' }, true);
512
+ else conn.h3.sendBody(streamId, null, true);
513
+ if (!carrier.peerEnded) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
514
+ done();
515
+ },
516
+
517
+ // Broken: the stream reset both ways (H3_CONNECT_ERROR, RFC 9114 §4.4).
518
+ abort: function () {
519
+ if (closed) return;
520
+ closed = true;
521
+ if (conn.h3 && conn.http_streams[streamId] === stream) {
522
+ try { conn.h3.abortStream(streamId, H3_ERR.H3_CONNECT_ERROR); } catch (e) {}
523
+ }
524
+ done();
525
+ }
526
+ };
527
+
528
+ stream.udp = carrier;
529
+ // The stream's bytes are capsules, credited as they arrive (as a
530
+ // WebTransport session's).
531
+ if (conn.h3) conn.h3.releaseBody(sid);
532
+ conn.udpByQuarter[quarter] = sid;
533
+ connectUdp.handle(stream.req, carrier);
534
+ }
535
+
536
+
537
+
102
538
  // ============================================================
103
539
  // set_http_stream — reactive HTTP stream builder
104
540
  // Creates req/res on first call, fires handler when ready.
@@ -110,8 +546,21 @@ function createServer(options, handler) {
110
546
 
111
547
  var is_new = false;
112
548
 
113
- // Create req/res on first encounter of this stream
549
+ // Create req/res on first encounter of this stream — and only for its
550
+ // HEADERS. Body bytes or an end for a stream with no entry belong to a
551
+ // request that does not exist (never started, already cancelled, or
552
+ // already done); creating one for them built a request with no method
553
+ // and no URL, called the handler, and left an entry nothing would remove.
114
554
  if (!(streamId in conn.http_streams)) {
555
+ if (!params || typeof params !== 'object' || !('request_headers' in params)) return;
556
+
557
+ // After GOAWAY: a request at or above its stream id is not processed
558
+ // (§5.2) — refused with H3_REQUEST_REJECTED, which tells the client it
559
+ // may safely retry it on another connection.
560
+ if (conn.goaway && Number(streamId) >= conn.goaway.id) {
561
+ if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_REQUEST_REJECTED);
562
+ return;
563
+ }
115
564
 
116
565
  var req = {
117
566
  method: null,
@@ -122,9 +571,29 @@ function createServer(options, handler) {
122
571
  _listeners: {},
123
572
  _isWebTransport: false,
124
573
 
574
+ // The connection's socket — one object for every request on it, as
575
+ // over TCP (see makeConnSocket).
576
+ h3Fingerprint: function () {
577
+ return conn.getH3Fingerprint ? conn.getH3Fingerprint() : null;
578
+ },
579
+
580
+ socket: conn.socket,
581
+
125
582
  on: function (event, fn) {
126
583
  (req._listeners[event] = req._listeners[event] || []).push(fn);
127
- }
584
+ },
585
+
586
+ // The body's flow-control credit, returned as the layer above reads
587
+ // it rather than as it arrives (h3's holdBody): what that layer has
588
+ // not read yet bounds what the client may send.
589
+ _holdBody: function () { if (conn.h3) conn.h3.holdBody(streamId); },
590
+ _creditBody: function (n) { if (conn.h3) conn.h3.creditBody(streamId, n); },
591
+ _releaseBody: function () { if (conn.h3) conn.h3.releaseBody(streamId); },
592
+
593
+ // Set by the layer above: whether it still wants the rest of the body
594
+ // once the response is complete (see res.end). Unset, the rest is
595
+ // wanted if anything listens for it.
596
+ _wantsRest: null
128
597
  };
129
598
 
130
599
  var res = {
@@ -132,9 +601,23 @@ function createServer(options, handler) {
132
601
  headers: {},
133
602
  headersSent: false,
134
603
  _finished: false,
135
-
604
+ // A response to HEAD, or a 204 or 304, has no content (RFC 9110 §9.3.2,
605
+ // §15.3.5, §15.4.5): what the application writes is dropped, as Node
606
+ // does over HTTP/1.1 and HTTP/2 (and these layers did already). Sent,
607
+ // it is DATA a client must reject: nghttp3 (curl, the h3 checkers)
608
+ // failed the request with ERR_MALFORMED_HTTP_MESSAGING. Set at
609
+ // writeHead, when the status is known.
610
+ _bodyAllowed: true,
611
+
612
+ // A list is kept a list, and goes as one field line per value, as
613
+ // HTTP/1.1 and HTTP/2 send it here and as Node's http does. It was
614
+ // String(list): two cookies became one "a=1,b=2", which a browser
615
+ // reads as a single cookie a with the value "1,b=2" (Set-Cookie
616
+ // never combines, RFC 9110 §5.3).
136
617
  setHeader: function (name, value) {
137
- res.headers[name.toLowerCase()] = String(value);
618
+ validateHeaderName(name);
619
+ validateHeaderValue(name, value);
620
+ res.headers[name.toLowerCase()] = Array.isArray(value) ? value.map(String) : String(value);
138
621
  },
139
622
 
140
623
  getHeader: function (name) {
@@ -143,18 +626,28 @@ function createServer(options, handler) {
143
626
 
144
627
  writeHead: function (statusCode, headers) {
145
628
  if (res.headersSent) return res;
629
+ validateStatus(statusCode);
146
630
  res.statusCode = statusCode;
631
+ res._bodyAllowed = !statusHasNoBody(statusCode) && req.headers[':method'] !== 'HEAD';
147
632
 
148
633
  if (typeof headers === 'object') {
149
- for (var name in headers) {
150
- res.headers[name.toLowerCase()] = String(headers[name]);
151
- }
634
+ for (var name in headers) res.setHeader(name, headers[name]);
152
635
  }
153
636
 
637
+ // Connection-specific fields are dropped, as HTTP/2's responses
638
+ // drop them (buildResponsePseudoHeaders): RFC 9114 §4.2 makes a
639
+ // message carrying one malformed, and a client may fail the request
640
+ // over it — an application's `Connection: close` or a WebSocket
641
+ // `Upgrade` meant for HTTP/1.1 cost the response. nginx fixed the
642
+ // same in 1.31.3.
154
643
  var h3Headers = { ':status': String(statusCode) };
155
644
  for (var name in res.headers) {
645
+ if (isConnectionSpecific(name) || name.charCodeAt(0) === 58) continue;
156
646
  h3Headers[name] = res.headers[name];
157
647
  }
648
+ // Date (RFC 9110 §6.6.1), as HTTP/2 and HTTP/1.1 send it — HTTP/3's
649
+ // responses had none.
650
+ if (!('date' in h3Headers) && statusCode >= 200 && !(res._wrapper && res._wrapper.sendDate === false)) h3Headers['date'] = httpDate();
158
651
 
159
652
  if (conn.h3) {
160
653
  // Send headers WITHOUT FIN for WT (session stays open)
@@ -163,70 +656,291 @@ function createServer(options, handler) {
163
656
  }
164
657
  res.headersSent = true;
165
658
 
659
+ // A WT CONNECT turned down: streams it already had are refused.
660
+ if (req.headers[':protocol'] === 'webtransport' && !(statusCode >= 200 && statusCode < 300) && conn.h3) {
661
+ conn.h3.rejectWebTransportSession(Number(streamId));
662
+ }
166
663
  // If this is a WT CONNECT with 200 → register session
167
664
  if (req.headers[':protocol'] === 'webtransport' && statusCode === 200 && conn.h3) {
168
665
  req._isWebTransport = true;
666
+ req._wtSched = wtScheduling(conn.quic, Number(streamId));
667
+ // The session as wt_stream's createDatagrams / sessionStats take
668
+ // it — the client's WebTransport hands in the same.
669
+ req._wtOwner = { maxAge: null, expired: 0 };
670
+ req._wtSession = {
671
+ quic: conn.quic,
672
+ connected: function () { return req._isWebTransport && !req._wtClosed; },
673
+ send: function (u8, options) { return res.sendDatagram(u8, options); },
674
+ maxDatagramSize: function () { return res.maxDatagramSize(); },
675
+ owner: req._wtOwner,
676
+ dgStats: { droppedIncoming: 0, expiredIncoming: 0 }
677
+ };
169
678
  conn.h3.registerWebTransportSession(Number(streamId));
679
+ // Its datagrams carry Number(streamId) / 4 (RFC 9297 §2.1).
680
+ conn.wtByQuarter[Math.floor(Number(streamId) / 4)] = Number(streamId);
681
+ // From here the CONNECT stream carries capsules, not a body: read
682
+ // here (onSessionCapsules), and credited as they arrive.
683
+ conn.h3.releaseBody(Number(streamId));
684
+ req._capsules = createCapsuleReader(function (info) { sessionClosed(info, false); },
685
+ function () { _emitOnReq(req, 'draining'); });
686
+ // Datagrams wait until the 200 has left (see sendDatagram).
687
+ req._wtDatagramsHeld = [];
688
+ conn.quic.whenStreamSent(Number(streamId), 0, function () {
689
+ var held = req._wtDatagramsHeld;
690
+ req._wtDatagramsHeld = null;
691
+ for (var i = 0; held && i < held.length; i++) conn.quic.sendDatagram(held[i].payload, held[i].prio, req._wtOwner);
692
+ });
170
693
  }
171
694
 
172
695
  return res;
173
696
  },
174
697
 
698
+ // 103 Early Hints (RFC 8297): a HEADERS frame with :status 103 and
699
+ // no FIN — the final response follows on the same stream. A list of
700
+ // links goes as one field, comma-joined, which Link allows (RFC 8288).
701
+ writeEarlyHints: function (hints, callback) {
702
+ if (!res.headersSent && !res._finished && conn.h3) {
703
+ var h = { ':status': '103' };
704
+ for (var n in (hints || {})) {
705
+ if (!Object.prototype.hasOwnProperty.call(hints, n) || hints[n] == null) continue;
706
+ h[String(n).toLowerCase()] = Array.isArray(hints[n]) ? hints[n].join(', ') : String(hints[n]);
707
+ }
708
+ try { conn.h3.sendHeaders(streamId, h, false); } catch (e) { /* stream gone */ }
709
+ }
710
+ if (typeof callback === 'function') process.nextTick(callback);
711
+ },
712
+
713
+ // 102 Processing (node:http's writeProcessing): as Early Hints go.
714
+ writeProcessing: function () {
715
+ if (res.headersSent || res._finished || !conn.h3) return;
716
+ try { conn.h3.sendHeaders(streamId, { ':status': '102' }, false); } catch (e) { /* stream gone */ }
717
+ },
718
+
175
719
  write: function (chunk) {
176
720
  if (!res.headersSent) res.writeHead(res.statusCode);
721
+ if (!res._bodyAllowed) return true;
177
722
  if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
178
723
  if (conn.h3) conn.h3.sendBody(streamId, chunk, false);
179
724
  return true;
180
725
  },
181
726
 
182
- end: function (chunk) {
727
+ // Backpressure: cb once the stream has sent all but `bytes` of what
728
+ // was written (see whenStreamSent in quic_connection).
729
+ whenSent: function (bytes, cb) {
730
+ if (!conn.quic || !conn.quic.whenStreamSent) { cb(); return; }
731
+ conn.quic.whenStreamSent(Number(streamId), bytes, cb);
732
+ },
733
+
734
+ // `trailers`: fields after the body (res.addTrailers) — a HEADERS
735
+ // frame closing the stream (RFC 9114 §4.1). What gRPC answers with:
736
+ // grpc-status and grpc-message come only there.
737
+ end: function (chunk, trailers) {
183
738
  if (res._finished) return;
184
739
  if (!res.headersSent) res.writeHead(res.statusCode);
740
+ if (!res._bodyAllowed) { chunk = null; trailers = null; }
741
+ trailers = trailerFields(trailers);
742
+ var withTrailers = !!trailers;
185
743
  if (conn.h3) {
186
744
  if (chunk) {
187
745
  if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
188
- conn.h3.sendBody(streamId, chunk, true);
189
- } else {
746
+ conn.h3.sendBody(streamId, chunk, !withTrailers);
747
+ } else if (!withTrailers) {
190
748
  conn.h3.sendBody(streamId, null, true);
191
749
  }
750
+ if (withTrailers) conn.h3.sendHeaders(streamId, trailers, true);
192
751
  }
193
752
  res._finished = true;
753
+
754
+ // The response is complete and the request is not (RFC 9114
755
+ // §4.1.2): a 413, a 401, a refusal answered before the upload is
756
+ // read. Unless something still reads the body, the client is told
757
+ // to stop sending it — STOP_SENDING with H3_NO_ERROR, which does
758
+ // not touch the response — and the request is over. It used to
759
+ // stay open: the client went on uploading into a window nobody
760
+ // would open again, holding its stream until it gave up.
761
+ if (!req._ended && !req._isWebTransport && conn.h3 && conn.http_streams[streamId] && !conn.http_streams[streamId].tunnel) {
762
+ var wants = typeof req._wantsRest === 'function' ? req._wantsRest()
763
+ : !!(req._listeners['data'] && req._listeners['data'].length);
764
+ if (!wants) {
765
+ conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
766
+ releaseHttpStream(conn, streamId, false);
767
+ return;
768
+ }
769
+ }
770
+ releaseIfDone(conn, streamId);
771
+ },
772
+
773
+ // The application gave the exchange up (res.destroy(), req.destroy()
774
+ // — unified server): the response not finished, the stream is
775
+ // cancelled both ways (RFC 9114 §4.1.1), `code` H3_REQUEST_CANCELLED
776
+ // or, for a failure of ours, H3_INTERNAL_ERROR; the response already
777
+ // complete, only what is left of the request body is refused
778
+ // (STOP_SENDING H3_NO_ERROR, §4.1.2), the response left to arrive.
779
+ // The request is then over here, as at any other end. Without this
780
+ // the client waited for the rest of a response that would never come
781
+ // and the stream stayed listed until the connection closed.
782
+ abort: function (code) {
783
+ if (!conn.http_streams[streamId]) return;
784
+ if (!res._finished) {
785
+ res._finished = true;
786
+ if (conn.h3) conn.h3.abortStream(streamId, code);
787
+ } else if (!req._ended) {
788
+ if (conn.h3) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
789
+ } else {
790
+ return;
791
+ }
792
+ releaseHttpStream(conn, streamId, false);
793
+ },
794
+
795
+ // The response's priority, as the server decides it: { urgency
796
+ // (0..7, 0 first), incremental } — RFC 9218's parameters. It replaces
797
+ // what the client asked for (its `priority` header, PRIORITY_UPDATE),
798
+ // and later requests from the client do not override it. For a
799
+ // WebTransport session, its urgency is its streams' too.
800
+ setPriority: function (p) {
801
+ if (conn.quic) conn.quic.setStreamPriority(Number(streamId), responsePriority(p));
194
802
  },
195
803
 
196
804
  // ---- WebTransport methods on res ----
197
805
 
198
- sendDatagram: function (data) {
199
- if (!req._isWebTransport) return;
806
+ // true: queued, and sent if room comes in time (quic_connection's
807
+ // sendDatagram — never retransmitted, the oldest dropped first when
808
+ // the path cannot keep up). false: it cannot be — no session, or
809
+ // larger than the peer takes (res.maxDatagramSize()).
810
+ //
811
+ // Held until the 200 that opens the session has left: a datagram the
812
+ // browser receives before it knows the session exists is dropped.
813
+ // Sent at once, they all were — every one the application sent right
814
+ // after accepting the session, 3000 of 3000, since the 200 waits for
815
+ // the end of the tick and a datagram did not.
816
+ //
817
+ // options { sendOrder, sendGroup }: as the browser's
818
+ // datagrams.createWritable(options) — its place among the session's
819
+ // streams (wtScheduling); without them, ahead of every stream.
820
+ sendDatagram: function (data, options) {
821
+ if (!req._isWebTransport || !conn.quic) return false;
200
822
  if (typeof data === 'string') data = Buffer.from(data);
201
823
  if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
202
- conn.quic.sendDatagram(wt_datagram_payload(Number(streamId), data));
824
+ var payload = wt_datagram_payload(Number(streamId), data);
825
+ if (payload.byteLength > conn.quic.maxDatagramSize()) return false;
826
+ var prio = req._wtSched.forDatagram(options);
827
+ if (req._wtDatagramsHeld) {
828
+ req._wtDatagramsHeld.push({ payload: payload, prio: prio });
829
+ if (req._wtDatagramsHeld.length > WT_DATAGRAMS_HELD) req._wtDatagramsHeld.shift(); // the oldest goes
830
+ return true;
831
+ }
832
+ return conn.quic.sendDatagram(payload, prio, req._wtOwner);
203
833
  },
204
834
 
205
- createBidirectionalStream: function () {
206
- if (!req._isWebTransport || !conn.h3) return null;
207
- var wtStreamId = conn.h3.createWebTransportStream(Number(streamId), true);
208
- return _createWtStream(conn, conn.quic, wtStreamId, Number(streamId), true);
835
+ // The largest datagram this session can send, in bytes of payload.
836
+ maxDatagramSize: function () {
837
+ if (!req._isWebTransport || !conn.quic) return 0;
838
+ return Math.max(0, conn.quic.maxDatagramSize() - (wt_datagram_payload(Number(streamId), new Uint8Array(0)).byteLength));
209
839
  },
210
840
 
211
- createUnidirectionalStream: function () {
212
- if (!req._isWebTransport || !conn.h3) return null;
213
- var wtStreamId = conn.h3.createWebTransportStream(Number(streamId), false);
214
- var stream = new Writable({
215
- write: function (chunk, encoding, cb) {
216
- if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
217
- conn.quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
218
- cb();
219
- },
220
- final: function (cb) {
221
- conn.quic.sendStream(wtStreamId, new Uint8Array(0), true);
222
- cb();
223
- }
224
- });
225
- stream._streamId = wtStreamId;
226
- return stream;
841
+ // Close the session with a code (32-bit) and a reason (UTF-8, up to
842
+ // 1024 bytes): CLOSE_WEBTRANSPORT_SESSION, then the CONNECT stream's
843
+ // end — what a browser's wt.close({ closeCode, reason }) sends, and
844
+ // what its wt.closed resolves with. Its streams are reset with
845
+ // WEBTRANSPORT_SESSION_GONE. There was no way to close a session but
846
+ // to close the whole QUIC connection under it.
847
+ closeSession: function (code, reason) {
848
+ if (!req._isWebTransport || req._wtClosed || !conn.h3) return;
849
+ conn.h3.sendBody(streamId, wtCloseCapsule(code || 0, reason), true);
850
+ sessionClosed({ closeCode: Number(code) || 0, reason: reason == null ? '' : String(reason) }, true);
851
+ },
852
+
853
+ // DRAIN_WEBTRANSPORT_SESSION: the peer should finish what it is doing
854
+ // and start nothing new; the session stays open.
855
+ drainSession: function () {
856
+ if (!req._isWebTransport || req._wtClosed || !conn.h3) return;
857
+ conn.h3.sendBody(streamId, wtDrainCapsule(), false);
858
+ },
859
+
860
+ // The application protocol chosen for the session, one of those the
861
+ // client offered (req.webTransportProtocols): sent as WT-Protocol
862
+ // with the 200, and what the browser's wt.protocol then says. Before
863
+ // writeHead; a name the client did not offer throws — the client
864
+ // would fail the session over it.
865
+ setWebTransportProtocol: function (name) {
866
+ if (res.headersSent) throw new Error('setWebTransportProtocol() after the response head was sent');
867
+ var offered = req.webTransportProtocols || [];
868
+ if (offered.indexOf(name) < 0) throw new TypeError('the client did not offer the protocol ' + JSON.stringify(name));
869
+ res.setHeader('wt-protocol', sfString(name));
870
+ },
871
+
872
+ // The browser's getStats(), as the client's wt.getStats(): the
873
+ // connection's figures and this session's datagrams (wt_stream's
874
+ // sessionStats).
875
+ getStats: function () {
876
+ if (!req._wtSession) return Promise.resolve(null);
877
+ return sessionStats(req._wtSession);
878
+ },
879
+
880
+ // The browser's WebTransportSendGroup, for a stream's sendGroup or a
881
+ // datagram's.
882
+ createSendGroup: function () { return createSendGroup(); },
883
+
884
+ // The browser's exportKeyingMaterial, as the client's: this session's
885
+ // keying material from the TLS exporter — the same bytes the client
886
+ // gets for the same label and context.
887
+ exportKeyingMaterial: function (label, context, outputLength) {
888
+ var a;
889
+ try { a = exporterArgs(arguments); } catch (e) { return Promise.reject(e); }
890
+ if (!req._isWebTransport || req._wtClosed) return Promise.reject(new DOMException('the session is closed', 'InvalidStateError'));
891
+ try { return Promise.resolve(exportSessionKeyingMaterial(conn.quic, Number(streamId), a)); }
892
+ catch (e) { return Promise.reject(e); }
893
+ },
894
+
895
+ // A stream of ours: a Duplex, or a Writable — recorded with its
896
+ // session, so the session's end destroys it. options { sendOrder,
897
+ // sendGroup, waitUntilAvailable }, the browser's (wt_stream's
898
+ // openWtStream): null, without waitUntilAvailable, when the client
899
+ // allows no more streams now (res.canOpenStream()).
900
+ //
901
+ // callback(err, stream): once it is open — at once, or (with
902
+ // waitUntilAvailable) when the client allows it; err if it closed
903
+ // first. stream.pending and its 'ready' say the same.
904
+ createBidirectionalStream: function (options, callback) { return openSessionStream(true, options, callback); },
905
+ createUnidirectionalStream: function (options, callback) { return openSessionStream(false, options, callback); },
906
+
907
+ // Whether a stream of ours could be opened now, within the client's
908
+ // MAX_STREAMS. options { bidirectional }.
909
+ canOpenStream: function (options) {
910
+ return !!(req._isWebTransport && !req._wtClosed && conn.quic && conn.quic.canOpenStream(!!(options && options.bidirectional)));
227
911
  }
228
912
  };
229
913
 
914
+ // The browser's datagrams object (readable, writable, createWritable,
915
+ // the age limits), as the client's wt.datagrams — made on first use.
916
+ Object.defineProperty(res, 'datagrams', { enumerable: true, get: function () {
917
+ if (!req._wtSession) return null;
918
+ return req._wtDatagrams || (req._wtDatagrams = createDatagrams(req._wtSession));
919
+ } });
920
+
921
+ var openSessionStream = function (bidi, options, callback) {
922
+ if (typeof options === 'function') { callback = options; options = null; }
923
+ if (!req._isWebTransport || req._wtClosed || !conn.h3) return null;
924
+ return openWtStream(conn.h3, conn.quic, Number(streamId), bidi, options, function (id, kind, prefix) {
925
+ return _createWtStream(conn, conn.quic, id, Number(streamId), kind, prefix);
926
+ }, callback);
927
+ };
928
+
929
+ // A WebTransport session ended by CLOSE_WEBTRANSPORT_SESSION — the
930
+ // peer's capsule, or ours (res.closeSession). req.closeInfo holds the
931
+ // code and reason, as a browser's wt.closed resolves with them; then
932
+ // 'close', and the session's streams go (WEBTRANSPORT_SESSION_GONE).
933
+ // After the peer's, our side of the CONNECT stream ends too (§5).
934
+ var sessionClosed = function (info, byUs) {
935
+ if (req._wtClosed || !req._isWebTransport) return;
936
+ req.closeInfo = info;
937
+ if (req._wrapper) req._wrapper.closeInfo = info;
938
+ req._capsules = null;
939
+ if (!byUs && conn.h3) conn.h3.sendBody(streamId, null, true);
940
+ if (conn.h3) conn.h3.endWebTransportSession(Number(streamId));
941
+ releaseHttpStream(conn, streamId, false);
942
+ };
943
+
230
944
  conn.http_streams[streamId] = { req: req, res: res };
231
945
  is_new = true;
232
946
  }
@@ -239,6 +953,13 @@ function createServer(options, handler) {
239
953
  for (var name in params.request_headers) {
240
954
  stream.req.headers[name] = params.request_headers[name];
241
955
  }
956
+ // The field lines as they came (validateFieldSection), which the
957
+ // copy above leaves behind — not enumerable: req.rawHeaders and
958
+ // headersDistinct are built from them. Without it they were rebuilt
959
+ // from the joined values, two x-a lines one "1, 2".
960
+ if (params.request_headers._rawHeaders && !stream.req.headers._rawHeaders) {
961
+ Object.defineProperty(stream.req.headers, '_rawHeaders', { value: params.request_headers._rawHeaders, enumerable: false });
962
+ }
242
963
 
243
964
  // Extract :method and :path
244
965
  if (stream.req.method === null && ':method' in stream.req.headers) {
@@ -247,10 +968,21 @@ function createServer(options, handler) {
247
968
  if (stream.req.url === null && ':path' in stream.req.headers) {
248
969
  stream.req.url = stream.req.headers[':path'];
249
970
  }
971
+ // A WebTransport CONNECT: the application protocols the client
972
+ // offers (WT-Available-Protocols), in its order of preference.
973
+ if (stream.req.headers[':protocol'] === 'webtransport') {
974
+ stream.req.webTransportProtocols = parseSfStringList(stream.req.headers['wt-available-protocols']);
975
+ }
250
976
  }
251
977
 
252
978
  if ('request_body_chunk' in params) {
253
979
  var stream = conn.http_streams[streamId];
980
+ // A 0-RTT request being judged (earlyGate): later.
981
+ if (stream.req._paused) { stream.req._paused.push({ request_body_chunk: params.request_body_chunk }); return; }
982
+ // An open WebTransport session's CONNECT stream: capsules, not a body.
983
+ if (stream.req._capsules) { stream.req._capsules(params.request_body_chunk); return; }
984
+ // A CONNECT-UDP tunnel's: capsules too, for the proxy.
985
+ if (stream.udp) { if (stream.udp.sink) stream.udp.sink.data(params.request_body_chunk); return; }
254
986
  var listeners = stream.req._listeners['data'] || [];
255
987
  for (var i = 0; i < listeners.length; i++) {
256
988
  try { listeners[i](params.request_body_chunk); } catch (e) { }
@@ -259,21 +991,126 @@ function createServer(options, handler) {
259
991
 
260
992
  if ('request_end' in params) {
261
993
  var stream = conn.http_streams[streamId];
994
+ if (stream.req._paused) { stream.req._paused.push({ request_end: true }); return; }
995
+ clearRequestDeadline(stream);
996
+ // The client closed the tunnel (§3.1 of RFC 9298).
997
+ if (stream.udp) { stream.udp.peerEnded = true; if (stream.udp.sink) stream.udp.sink.end(); return; }
262
998
  var listeners = stream.req._listeners['end'] || [];
263
999
  for (var i = 0; i < listeners.length; i++) {
264
1000
  try { listeners[i](); } catch (e) { }
265
1001
  }
1002
+ stream.req._ended = true;
1003
+
1004
+ // A session ends with its CONNECT stream's FIN (releaseHttpStream
1005
+ // says so and closes its streams); a request, once its response is
1006
+ // done too.
1007
+ if (stream.req._isWebTransport) releaseHttpStream(conn, streamId, false);
1008
+ else releaseIfDone(conn, streamId);
266
1009
  }
267
1010
  }
268
1011
 
269
- // Fire handler on new stream (after headers are set)
1012
+ // Fire handler on new stream (after headers are set). A request that
1013
+ // came in 0-RTT is judged first (earlyGate): it may be a replay.
270
1014
  if (is_new) {
271
- var stream = conn.http_streams[streamId];
272
- if (typeof handler === 'function') {
273
- handler(stream.req, stream.res);
1015
+ var newStream = conn.http_streams[streamId];
1016
+ newStream.req.earlyData = !!(conn.quic && conn.quic.isEarlyStream && conn.quic.isEarlyStream(streamId));
1017
+ if (newStream.req.earlyData) { earlyGate(conn, streamId, newStream, function () { dispatchNewRequest(newStream); }); return; }
1018
+ dispatchNewRequest(newStream);
1019
+ }
1020
+
1021
+ function dispatchNewRequest(stream) {
1022
+
1023
+ // ---- Extended CONNECT for something other than WebTransport ----
1024
+ //
1025
+ // RFC 9220 bootstraps WebSocket over HTTP/3 exactly as RFC 8441 does
1026
+ // over HTTP/2: a CONNECT carrying :protocol, answered with a 2xx. The
1027
+ // two differ only in what runs on the stream afterwards — WebTransport
1028
+ // turns it into a session that later streams join, WebSocket just sends
1029
+ // frames on it — so a WebSocket must NOT be registered as a session, or
1030
+ // unrelated streams carrying its id would be mistaken for members of it.
1031
+ var proto = stream.req.headers[':protocol'];
1032
+ // CONNECT-UDP, when the application proxies UDP: not a tunnel of raw
1033
+ // bytes — after the 2xx the stream still carries HTTP/3 DATA frames,
1034
+ // holding capsules (RFC 9297 §3.2), as a WebTransport session's does.
1035
+ if (proto && connectUdp && String(proto).toLowerCase() === 'connect-udp' && connectUdp.wants(stream.req)) {
1036
+ startConnectUdp(conn, streamId, stream);
1037
+ return;
274
1038
  }
275
- ev.emit('request', stream.req, stream.res);
1039
+ if (proto && String(proto).toLowerCase() !== 'webtransport' && shouldUpgradeCallback(stream.req)) {
1040
+ // Stop HTTP/3 framing on this stream NOW, before the application has
1041
+ // decided anything: a client may already be sending WebSocket frames,
1042
+ // and they must not reach the frame parser.
1043
+ if (conn.h3) conn.h3.beginTunnelStream(streamId);
1044
+ var duplex = _createTunnelDuplex(conn, streamId, stream.req);
1045
+ stream.tunnel = duplex;
1046
+
1047
+ if (upgradeListeners === 0) {
1048
+ // Accepted by shouldUpgradeCallback, yet nobody listens. Answer
1049
+ // rather than leave the peer waiting for a 2xx that will never come.
1050
+ if (conn.h3) {
1051
+ conn.h3.sendHeaders(streamId, { ':status': '501' }, true);
1052
+ conn.h3.stopReading(streamId, H3_ERR.H3_REQUEST_REJECTED);
1053
+ }
1054
+ releaseHttpStream(conn, streamId, false);
1055
+ return;
1056
+ }
1057
+ stream.req.upgrade = true;
1058
+ ev.emit('upgrade', stream.req, duplex, Buffer.alloc(0));
1059
+ return;
1060
+ }
1061
+
1062
+ if (stream.req.method !== 'CONNECT' && !stream.req._ended) armRequestDeadline(conn, streamId, stream);
1063
+
1064
+ // A handler that throws loses its own stream — reset both ways,
1065
+ // H3_INTERNAL_ERROR — not the connection (see runRequestHandler). A
1066
+ // response it had already ended is left to finish.
1067
+ // maxRequestsPerSocket: after the Nth request, GOAWAY — those in flight
1068
+ // finish, the client takes the next to a new connection (as HTTP/2).
1069
+ if (maxRequests > 0 && ++conn.served === maxRequests && conn.handle) conn.handle.goaway();
1070
+ runRequestHandler(function () {
1071
+ if (typeof handler === 'function') handler(stream.req, stream.res);
1072
+ ev.emit('request', stream.req, stream.res);
1073
+ }, function () {
1074
+ var w = stream.res && stream.res._wrapper;
1075
+ if (w ? w.writableEnded : stream.res._finished) return;
1076
+ if (conn.http_streams[streamId] !== stream) return;
1077
+ if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_INTERNAL_ERROR);
1078
+ releaseHttpStream(conn, streamId, true);
1079
+ });
1080
+ }
1081
+ }
1082
+
1083
+ // ── 0-RTT requests (RFC 8470) ──────────────────────────────────────────
1084
+ //
1085
+ // A request that came in 0-RTT (req.earlyData) may be a replay: whoever
1086
+ // captured the client's first flight can send it again. Each one is asked
1087
+ // about first — earlyDataFilter(req, callback(err, allow)), the
1088
+ // application's, or by default: GET, HEAD and OPTIONS go on, anything else
1089
+ // is answered 425 Too Early, which a client sends again once its handshake
1090
+ // is done (the browsers do, and quico's client). No answer within
1091
+ // EARLY_FILTER_TIMEOUT_MS, an error, a throw: 425 — the request is not
1092
+ // lost, only later. Meanwhile what more arrives on the stream (a body, its
1093
+ // end) waits, for the handler to find.
1094
+ // (The filter and its timeout: http_common's runEarlyDataFilter.)
1095
+ function earlyGate(conn, streamId, stream, proceed) {
1096
+ var req = stream.req;
1097
+ req._paused = [];
1098
+ runEarlyDataFilter(earlyDataFilter, req, function (allow) {
1099
+ var queued = req._paused;
1100
+ req._paused = null;
1101
+ if (conn.http_streams[streamId] !== stream) return; // gone meanwhile (reset, closed)
1102
+ if (!allow) { tooEarly(conn, streamId, stream); return; }
1103
+ proceed();
1104
+ for (var i = 0; i < queued.length; i++) set_http_stream(conn, streamId, queued[i]);
1105
+ });
1106
+ }
1107
+ function tooEarly(conn, streamId, stream) {
1108
+ if (options.stats) options.stats.earlyData.tooEarly++;
1109
+ if (conn.h3) {
1110
+ conn.h3.sendHeaders(streamId, { ':status': '425', 'content-length': '0' }, true);
1111
+ if (!stream.req._ended) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
276
1112
  }
1113
+ releaseHttpStream(conn, streamId, false);
277
1114
  }
278
1115
 
279
1116
 
@@ -286,19 +1123,355 @@ function createServer(options, handler) {
286
1123
  // socket, demux and connection lifecycle.
287
1124
  // ============================================================
288
1125
 
1126
+ /**
1127
+ * A stand-in for the TLSSocket that TCP requests carry — ONE per QUIC
1128
+ * connection, shared by every request on it.
1129
+ *
1130
+ * Over TCP, req.socket is the connection: every request on it gets the
1131
+ * same object, which is how an application tells that two requests came in
1132
+ * on one connection, and hears 'close' when it goes. It used to be built
1133
+ * per request here, so over HTTP/3 no two requests shared one and nothing
1134
+ * ever closed.
1135
+ *
1136
+ * There is no real socket: the TLS handshake happens inside the QUIC
1137
+ * connection, so what an application reaches for on req.socket is answered
1138
+ * from it. `encrypted` is always true — QUIC has no unencrypted mode.
1139
+ * The address follows the connection when the client moves (migration,
1140
+ * RFC 9000 §9) — once the new address is validated.
1141
+ */
1142
+ function makeConnSocket(conn, quic, peer) {
1143
+ // A real Duplex (streams.js), not a bare emitter: Express reads
1144
+ // socket.readable / socket.writable to decide whether a request is still
1145
+ // open (on-finished — body-parser, express.static), and listens on it with
1146
+ // on/removeListener. Without them every HTTP/3 request looked finished
1147
+ // before it started: a POST body was skipped and a static file never sent.
1148
+ var sock = _makeSocketStub();
1149
+ sock.alpnProtocol = 'h3';
1150
+ // The connection's QUIC version (1, or 0x6b3343cf for v2) — after
1151
+ // compatible version negotiation, the one negotiated.
1152
+ Object.defineProperty(sock, 'quicVersion', { configurable: true, enumerable: true,
1153
+ get: function () { return quic.version; } });
1154
+ // Live, as net.Socket's are: a client whose NAT changes its port is
1155
+ // followed by the connection (RFC 9000 §9.3), and a copy taken here
1156
+ // would go on naming the address it left.
1157
+ Object.defineProperty(sock, 'remoteAddress', { configurable: true, enumerable: true,
1158
+ get: function () { return quic.remoteAddress || (peer && peer.address) || null; } });
1159
+ Object.defineProperty(sock, 'remotePort', { configurable: true, enumerable: true,
1160
+ get: function () { return quic.remotePort || (peer && peer.port) || null; } });
1161
+ // TLS fingerprints and the certificate presented, as on a TLSSocket.
1162
+ sock.getJA4 = function (opts) { return quic.getJA4 ? quic.getJA4(opts) : null; };
1163
+ sock.getJA5 = function (opts) { return quic.getJA5 ? quic.getJA5(opts) : null; };
1164
+ sock.getJA4S = function (opts) { return quic.getJA4S ? quic.getJA4S(opts) : null; };
1165
+ // What the handshake settled, as a lemon-tls TLSSocket answers over
1166
+ // HTTP/2 and HTTP/1.1 — among it whether the certificates went compressed.
1167
+ sock.getNegotiationResult = function () { return quic.getNegotiationResult ? quic.getNegotiationResult() : null; };
1168
+ sock.getCertificate = function () { return quic.getCertificate ? quic.getCertificate() : {}; };
1169
+ // The client's certificate and its verification, as a TLSSocket's
1170
+ // (requestCert). The stub said `authorized: true` and `{}` for every
1171
+ // connection: harmless while QUIC could not ask for a certificate, and
1172
+ // an open door once it could — an application checking
1173
+ // req.socket.authorized would have let in a client that sent none.
1174
+ Object.defineProperty(sock, 'authorized', { configurable: true, enumerable: true,
1175
+ get: function () { return !!(quic.isAuthorized && quic.isAuthorized()); } });
1176
+ Object.defineProperty(sock, 'authorizationError', { configurable: true, enumerable: true,
1177
+ get: function () { return quic.authorizationError ? quic.authorizationError() : null; } });
1178
+ sock.getPeerCertificate = function () { return quic.getPeerCertificate ? quic.getPeerCertificate() : {}; };
1179
+ sock.getPeerX509Certificate = function () {
1180
+ var c = sock.getPeerCertificate();
1181
+ try { return c && c.raw ? new X509Certificate(c.raw) : undefined; } catch (e) { return undefined; }
1182
+ };
1183
+ // The session ticket, { data, resumed, offered } — the same method a
1184
+ // TLSSocket has over TCP, so req.socket answers it on every HTTP version.
1185
+ sock.getTicketInfo = function () { return quic.getTicketInfo ? quic.getTicketInfo() : null; };
1186
+ // This connection's options while it runs (quic_connection's
1187
+ // setOptions): every request on the connection shares them.
1188
+ sock.setOptions = function (o) { quic.setOptions(o); return sock; };
1189
+ sock.getOptions = function () { return quic.getOptions(); };
1190
+ // And the rest of a TLSSocket's session questions.
1191
+ sock.isSessionReused = function () { return !!(quic.isSessionReused && quic.isSessionReused()); };
1192
+ sock.getProtocol = function () { return quic.getProtocol ? quic.getProtocol() : null; };
1193
+ sock.getCipher = function () { return quic.getCipher ? quic.getCipher() : null; };
1194
+ // The protocol-layer fingerprint, alongside the TLS one above. Named to
1195
+ // match req.h2Fingerprint on the TCP side so an application can reach for
1196
+ // whichever is present without branching on version.
1197
+ sock.h3Fingerprint = function () { return conn.getH3Fingerprint ? conn.getH3Fingerprint() : null; };
1198
+ // The SNI the client sent, or false — known once the ClientHello is in,
1199
+ // which is before any request, so read live rather than copied now.
1200
+ Object.defineProperty(sock, 'servername', {
1201
+ get: function () { return quic.servername || false; },
1202
+ enumerable: true
1203
+ });
1204
+ // Encrypted Client Hello, as over TCP (lemon-tls TLSSocket): null when
1205
+ // not offered, 'accepted' — servername is then the name that was hidden
1206
+ // — or 'rejected' (servername is the public name).
1207
+ Object.defineProperty(sock, 'echStatus', {
1208
+ get: function () { return quic.echStatus; },
1209
+ enumerable: true
1210
+ });
1211
+ Object.defineProperty(sock, 'echAccepted', {
1212
+ get: function () { return quic.echStatus === 'accepted'; },
1213
+ enumerable: true
1214
+ });
1215
+ // Gone with the connection: destroy() turns readable/writable off, sets
1216
+ // destroyed, and emits 'close' — as a socket does.
1217
+ quic.on('close', function () {
1218
+ if (!sock.destroyed) sock.destroy();
1219
+ });
1220
+ return sock;
1221
+ }
1222
+
1223
+ // Every connection, for closing them (closeIdleConnections, close()).
1224
+ var conns = new Set();
1225
+ var closingServer = null; // close()'s callback, once every connection is gone
1226
+
289
1227
  qserver.on('connection', function (quic, peer) {
290
- var conn = { quic: quic, h3: null, http_streams: {} };
1228
+ var conn = { quic: quic, h3: null, http_streams: {}, _wtStreamObjects: {}, served: 0,
1229
+ headerDeadlines: {}, // request streams whose HEADERS is still arriving
1230
+ wtByQuarter: {}, // WebTransport sessions by Quarter Stream ID → stream id
1231
+ udpByQuarter: {} }; // CONNECT-UDP tunnels, the same way
1232
+ conn.socket = makeConnSocket(conn, quic, peer);
1233
+
1234
+ // The connection is gone — CONNECTION_CLOSE from either side, or the
1235
+ // idle timeout. Every WebTransport session on it ends with it, and says
1236
+ // so with 'close' on its request, as a session the browser closes does.
1237
+ // Without this the application never heard of a peer that vanished.
1238
+ //
1239
+ // So does everything else still open on it. A request whose body was
1240
+ // still arriving, or whose response was still being written, or a
1241
+ // WebSocket tunnel, used to hear nothing at all: the application waited
1242
+ // for an end that could not come, until a timeout of its own. Each is
1243
+ // ended the way a cancelled stream is — releaseHttpStream, aborted:
1244
+ // 'aborted' on the request and 'close' on the response, a tunnel
1245
+ // destroyed, a session closed with its streams. Requests that had
1246
+ // finished in both directions are already gone (releaseIfDone).
1247
+ conns.add(conn);
1248
+ // Over for the application ('closing'), or its draining done ('close').
1249
+ var gone = function () { if (conns.delete(conn)) connGone(); };
1250
+ quic.on('closing', gone);
1251
+ quic.on('close', function () {
1252
+ gone();
1253
+ for (var hid in conn.headerDeadlines) clearHeadersDeadline(conn, hid);
1254
+ for (var sid in conn.http_streams) releaseHttpStream(conn, sid, true);
1255
+ for (var wid in conn._wtStreamObjects) releaseHttpStream(conn, wid, true);
1256
+ });
291
1257
 
292
- // QUIC handshake complete → set up H3
1258
+ // Registered HERE, not after the handshake completes.
1259
+ //
1260
+ // The peer's QUIC transport parameters arrive inside its ClientHello,
1261
+ // which is processed long before 'connect' fires — a listener attached
1262
+ // afterwards has already missed them, and the transport section of the
1263
+ // fingerprint comes out empty. The HTTP/3 SETTINGS arrive later still, on
1264
+ // the control stream, so observeH3 is given the QUIC connection now and
1265
+ // the H3 connection as soon as it exists.
1266
+ var h3Observer = observeH3(null, quic);
1267
+ conn.getH3Fingerprint = function () { return h3Observer(); };
1268
+
1269
+ /**
1270
+ * The same handle h2_server builds, so that one piece of rate-limiting
1271
+ * code works against both protocols without branching on version.
1272
+ */
1273
+ var connHandle = {
1274
+ httpVersion: '3.0',
1275
+ // quic_server attaches the address to the connection, but the listener
1276
+ // is handed it directly too — `peer` — and that one does not depend on
1277
+ // which version of quic_server is installed. Without the fallback an
1278
+ // older quic_server gives null here, and an IP screen that reads it
1279
+ // skips the connection silently rather than failing.
1280
+ get remoteAddress() { return quic.remoteAddress || (peer && peer.address) || null; },
1281
+ get remotePort() { return quic.remotePort || (peer && peer.port) || null; },
1282
+
1283
+ // Read by anything that checks before it acts, the way checkIpBlacklist
1284
+ // does with a TCP socket: its answer arrives asynchronously, and by then
1285
+ // the peer may already have gone.
1286
+ get destroyed() {
1287
+ var st = quic.state;
1288
+ return st === 'closed' || st === 'draining' || st === 'closing';
1289
+ },
1290
+ encrypted: true,
1291
+ fingerprint: function () { return h3Observer(); },
1292
+ getTicketInfo: function () { return quic.getTicketInfo ? quic.getTicketInfo() : null; },
1293
+ isSessionReused: function () { return !!(quic.isSessionReused && quic.isSessionReused()); },
1294
+ // This connection's options while it runs: { startupCap, reorderWindow,
1295
+ // pmtuDiscovery, maxUdpPayloadSize, initialRtt, initialCongestionWindow,
1296
+ // carefulResume } (quic_connection's setOptions).
1297
+ setOptions: function (o) { quic.setOptions(o); return connHandle; },
1298
+ getOptions: function () { return quic.getOptions(); },
1299
+
1300
+ // Refuse or cancel a stream: RESET_STREAM and STOP_SENDING both ways,
1301
+ // as RFC 9114 §4.1.1 describes. The intents are h2_server's — 'refused'
1302
+ // for a request never processed, which the client may safely retry
1303
+ // (H3_REQUEST_REJECTED), 'cancelled' for one abandoned part way
1304
+ // (H3_REQUEST_CANCELLED); a number is passed through as is.
1305
+ //
1306
+ // It used to answer 429 instead: QUIC could not reset a stream until
1307
+ // control frames became reliable. A 429 still costs a response, and
1308
+ // leaves the stream open until the client reads it.
1309
+ sendRstStream: function (streamId, code) {
1310
+ var h3code = typeof code === 'number' ? code
1311
+ : code === 'cancelled' ? H3_ERR.H3_REQUEST_CANCELLED
1312
+ : H3_ERR.H3_REQUEST_REJECTED;
1313
+ if (conn.h3) conn.h3.abortStream(streamId, h3code);
1314
+ clearHeadersDeadline(conn, streamId);
1315
+ releaseHttpStream(conn, streamId, true);
1316
+ },
1317
+
1318
+ // Stop new requests; the ones in flight finish — as h2_server's
1319
+ // goaway(), whose semantics a rate limiter is written against. This
1320
+ // closed the connection on the spot, so the same limiter code that let
1321
+ // HTTP/2 requests in progress complete cut HTTP/3 ones off mid-response.
1322
+ //
1323
+ // The GOAWAY carries the first request stream id NOT processed: one past
1324
+ // the highest the client has opened, so every request already started —
1325
+ // even one whose HEADERS are still arriving — is answered. When they
1326
+ // have all finished and been delivered, the connection is closed:
1327
+ // H3_NO_ERROR, or H3_EXCESSIVE_LOAD for goaway('overloaded') (HTTP/2's
1328
+ // ENHANCE_YOUR_CALM). Before the handshake there is no HTTP/3 to speak
1329
+ // of: refused, as destroy() does.
1330
+ goaway: function (code) {
1331
+ if (conn.goaway) return;
1332
+ if (!conn.h3) { try { quic.close(0x02, 'refused', false); } catch (e) {} return; }
1333
+ conn.goaway = { id: (quic.context.peer_max_opened_bidi + 1) * 4, code: h3ConnCode(code), closing: false };
1334
+ conn.h3.sendGoaway(conn.goaway.id);
1335
+ closeAfterGoaway(conn);
1336
+ },
1337
+
1338
+ destroy: function (code) {
1339
+ // Two different closes, because the right one depends on WHEN.
1340
+ //
1341
+ // Before the handshake completes there is no HTTP/3 yet — an
1342
+ // application error code has no meaning to the peer, and RFC 9000
1343
+ // §10.2.3 forbids sending one in the Initial or Handshake packet
1344
+ // spaces. CONNECTION_REFUSED (0x02) is the transport error that says
1345
+ // exactly what happened: the server declined the connection. That is
1346
+ // the case for an IP screen, which runs on the very first packet.
1347
+ //
1348
+ // After it, the application space, with the intent as for goaway():
1349
+ // H3_NO_ERROR, H3_EXCESSIVE_LOAD for 'overloaded', or a number. It sent
1350
+ // 0x10c — H3_REQUEST_CANCELLED, a stream's code, which says nothing
1351
+ // about why a connection ended.
1352
+ try {
1353
+ if (!conn.h3) quic.close(0x02, 'refused', false);
1354
+ else quic.close(h3ConnCode(code), 'closed', true);
1355
+ } catch (e) {}
1356
+ }
1357
+ };
1358
+ conn.handle = connHandle;
1359
+
1360
+ // Announced upward, before the handshake — the QUIC counterpart of
1361
+ // tcp_listener's 'connection', and there for the same reason.
1362
+ //
1363
+ // It is the one point where a server can refuse a peer before any crypto
1364
+ // is done on its behalf. It fires on the first Initial packet, so a
1365
+ // listener that calls destroy() here stops the connection before the TLS
1366
+ // handshake, just as destroying a TCP socket in 'connection' does.
1367
+ //
1368
+ // Without it, nothing above this file learns that a QUIC connection
1369
+ // exists, and screening that is wired to 'connection' — an IP blacklist,
1370
+ // a rate limit on new connections — applies to HTTP/1.1 and HTTP/2 and
1371
+ // silently not to HTTP/3.
1372
+ //
1373
+ // The handle carries remoteAddress, destroyed and destroy(), which is what
1374
+ // such a listener uses on a TCP socket, so the same function serves both.
1375
+ ev.emit('connection', connHandle);
1376
+
1377
+ // A handshake that failed — a TLS alert (a client certificate refused, no
1378
+ // common cipher, a size limit) or the handshake timeout: node:tls's
1379
+ // 'tlsClientError', with the handle in place of the socket. The QUIC
1380
+ // connection reports 'error' for exactly these and nothing else; it was
1381
+ // not listened to, so a failed handshake left no trace.
1382
+ //
1383
+ // Also when it is the client that ended the handshake with an alert (a
1384
+ // browser that does not trust the certificate): in QUIC that arrives as
1385
+ // CONNECTION_CLOSE with CRYPTO_ERROR, 0x100 + the alert (RFC 9001 §4.8),
1386
+ // where node:tls would report the alert itself — even after 'connect',
1387
+ // which a server reaches with its own Finished, before the client has
1388
+ // checked anything. A CRYPTO_ERROR can only come from TLS, and TLS in
1389
+ // QUIC is the handshake. Once per connection.
1390
+ var tlsErrorReported = false;
1391
+ function reportTlsError(err) {
1392
+ if (tlsErrorReported) return;
1393
+ tlsErrorReported = true;
1394
+ ev.emit('tlsClientError', err, connHandle);
1395
+ }
1396
+ quic.on('error', reportTlsError);
1397
+ quic.on('close', function () {
1398
+ var pc = quic.context && quic.context.peer_close;
1399
+ // The transport's CONNECTION_CLOSE only (0x1c): in the application's
1400
+ // (0x1d) the same numbers are HTTP/3's — 0x100 is H3_NO_ERROR, a
1401
+ // client closing a connection it is done with. Reported as TLS alert 0
1402
+ // since HTTP/3 clients began to say goodbye (h3_client's
1403
+ // CONNECTION_CLOSE used to be lost with its socket).
1404
+ if (pc && !pc.application && pc.error >= 0x100 && pc.error < 0x200) {
1405
+ var err = new Error('the client ended the handshake: TLS alert ' + (pc.error - 0x100) + (pc.reason ? ' (' + pc.reason + ')' : ''));
1406
+ err.code = 'ERR_TLS_ALERT_RECEIVED';
1407
+ err.alert = pc.error - 0x100;
1408
+ reportTlsError(err);
1409
+ }
1410
+ });
1411
+
1412
+ // 'secureConnection': the handshake is complete — the client's Finished
1413
+ // verified, the name and the moment node:tls uses (it was emitted when
1414
+ // the server's own Finished went, a round trip early, before the client
1415
+ // had proven anything). 'connection' fired before any TLS, knowing only
1416
+ // an address; here the connection can be asked what the handshake
1417
+ // decided (isSessionReused(), getTicketInfo()), which is what a caller
1418
+ // needs to tell a returning client from a new one. Before any request:
1419
+ // the client's are held until now. A listener may destroy() it. The same
1420
+ // handle as 'connection' carries — over QUIC there is no socket to hand
1421
+ // out.
1422
+ quic.on('handshake_ended', function (confirmed) {
1423
+ if (confirmed) ev.emit('secureConnection', connHandle);
1424
+ });
1425
+
1426
+ // Our side of the handshake done (our Finished sent): HTTP/3 set up —
1427
+ // its SETTINGS go with the handshake's last datagram.
293
1428
  quic.on('connect', function () {
294
- var h3 = new H3Connection({ quicConnection: quic, enableWebTransport: true });
1429
+ if (connHandle.destroyed) return;
1430
+
1431
+ // The handshake may have settled on something other than HTTP/3 when
1432
+ // the server accepts more than one ALPN name. Such a connection is not
1433
+ // ours to speak: the application only asked that the handshake agree to
1434
+ // the name. Close it instead of treating its streams as HTTP/3.
1435
+ var negotiated = quic.alpnProtocol;
1436
+ if (negotiated && negotiated !== 'h3') {
1437
+ try { quic.close(0, 'alpn ' + negotiated + ' is not served here'); } catch (e) {}
1438
+ return;
1439
+ }
1440
+
1441
+ var h3 = new H3Connection({
1442
+ quicConnection: quic,
1443
+ enableWebTransport: true,
1444
+ maxWebTransportSessions: options.maxWebTransportSessions,
1445
+ // Report-only mode for the checks added with the error layer. See the
1446
+ // note in h3.js: these turn conditions that were previously ignored
1447
+ // into connection errors, so a bug they expose looks like a regression.
1448
+ qpackLenient: options.qpackLenient === true
1449
+ });
295
1450
  conn.h3 = h3;
1451
+ // The H3 half of the observer, now that the connection exists. The QUIC
1452
+ // half was attached before the handshake, above.
1453
+ h3Observer = observeH3(h3, quic, h3Observer.state);
1454
+
1455
+ // Surface HTTP/3-level failures on the server. Without this the error
1456
+ // raised by connectionError() is emitted into a connection object nobody
1457
+ // listens to, and the peer's ERR_QUIC_PROTOCOL_ERROR is the only sign
1458
+ // anything happened — invisible on this side, which is the exact failure
1459
+ // mode the error layer exists to remove.
1460
+ h3.on('error', function (err) { ev.emit('error', err); });
296
1461
 
297
1462
  // HTTP request headers received → set_http_stream (reactive)
298
1463
  h3.on('http_headers', function (streamId, headers) {
1464
+ clearHeadersDeadline(conn, streamId);
299
1465
  set_http_stream(conn, streamId, { request_headers: headers });
300
1466
  });
301
1467
 
1468
+ // The request's trailers (after its body, before its end): on req, as
1469
+ // node:http's req.trailers — read once 'end' has fired.
1470
+ h3.on('http_trailers', function (streamId, trailers) {
1471
+ var st = conn.http_streams[streamId];
1472
+ if (st && st.req) st.req.trailers = trailers;
1473
+ });
1474
+
302
1475
  // HTTP request body chunk → set_http_stream
303
1476
  h3.on('http_body', function (streamId, data) {
304
1477
  set_http_stream(conn, streamId, { request_body_chunk: data });
@@ -309,19 +1482,120 @@ function createServer(options, handler) {
309
1482
  set_http_stream(conn, streamId, { request_end: true });
310
1483
  });
311
1484
 
1485
+ // Bytes on an accepted extended-CONNECT tunnel: WebSocket frames, not
1486
+ // HTTP/3 frames. They go straight to the Duplex the upgrade handler was
1487
+ // given.
1488
+ h3.on('stream_open', function (streamId) {
1489
+ armHeadersDeadline(conn, streamId);
1490
+ ev.emit('stream_open', connHandle, streamId);
1491
+ });
1492
+ // The client cancelled a stream. Same event and arguments as
1493
+ // h2_server's 'stream_reset', so a Rapid Reset detector is written once
1494
+ // for both protocols.
1495
+ //
1496
+ // HTTP/2 has one frame for it (RST_STREAM); HTTP/3 has two, one per
1497
+ // direction, and which ones arrive depends on the request. A browser
1498
+ // cancelling a GET sends only STOP_SENDING — its side of the stream is
1499
+ // already finished, so there is nothing to reset — while cancelling an
1500
+ // upload sends both. Reporting only RESET_STREAM made every cancelled
1501
+ // GET invisible, and GET-then-cancel is exactly what a Rapid Reset
1502
+ // attack sends. So: either frame reports the cancellation, once per
1503
+ // stream.
1504
+ var cancelReported = new Set();
1505
+ // Rapid Reset: past streamResetBurst / streamResetRate cancelled
1506
+ // request streams, the connection is closed with H3_EXCESSIVE_LOAD.
1507
+ var resetTooMany = streamResetLimiter(options);
1508
+ function reportCancel(streamId, code, kind) {
1509
+ clearHeadersDeadline(conn, streamId);
1510
+ // A WebTransport stream: the direction the peer gave up on ends, with
1511
+ // the application's code (wt_stream's _peerCancelled); the stream
1512
+ // object decides what that leaves of it, and is forgotten when it
1513
+ // closes. Its two directions are independent — destroying it here
1514
+ // reset the one the peer had not touched.
1515
+ var wtCancelled = conn._wtStreamObjects[streamId];
1516
+ var isWtStream = !!(wtCancelled && wtCancelled._peerCancelled);
1517
+ if (isWtStream) {
1518
+ wtCancelled._peerCancelled(kind, code);
1519
+ } else {
1520
+ var hadRequest = streamId in conn.http_streams;
1521
+ var cancelled = conn.http_streams[streamId];
1522
+ var sendingOpen = !!cancelled && !(cancelled.res && cancelled.res._finished);
1523
+ releaseHttpStream(conn, streamId, true);
1524
+ // The client reset its request (RESET_STREAM alone — an upload
1525
+ // abandoned): the request is aborted above, and with it whatever
1526
+ // response was still to come (a tunnel, a WebTransport session, a
1527
+ // response not yet ended). Our sending part is reset with it
1528
+ // (H3_REQUEST_CANCELLED): left open, nothing would ever end it —
1529
+ // the stream never closed, and the client's stream credit with it.
1530
+ // A response already ended goes on to arrive.
1531
+ if (kind === 'stream_reset' && sendingOpen && conn.quic && conn.quic.resetStream) {
1532
+ try { conn.quic.resetStream(Number(streamId), H3_ERR.H3_REQUEST_CANCELLED); } catch (e) {}
1533
+ }
1534
+ // The client will not read the response (STOP_SENDING): the request
1535
+ // is cancelled, so stop reading its stream too (RFC 9114 §4.1.1).
1536
+ // Body or trailers still parsed on it would otherwise reach
1537
+ // set_http_stream for a request no longer there. Our sending part
1538
+ // was already reset by the transport, in answer to the STOP_SENDING.
1539
+ //
1540
+ // Not for RESET_STREAM: nothing more is read after it anyway, and
1541
+ // a client that only cut its request short may still want the
1542
+ // response (§4.1.1's early response) — resetting it would discard a
1543
+ // reply the handler already wrote.
1544
+ if (kind === 'stop_sending' && hadRequest && conn.h3) {
1545
+ try { conn.h3.stopReading(streamId, H3_ERR.H3_REQUEST_CANCELLED); } catch (e) {}
1546
+ }
1547
+ }
1548
+ if (cancelReported.has(streamId)) return;
1549
+ cancelReported.add(streamId);
1550
+ if (cancelReported.size > 4096) cancelReported.delete(cancelReported.values().next().value);
1551
+ ev.emit('stream_reset', connHandle, streamId, code);
1552
+ // Request streams only: a WebTransport application cancels its own
1553
+ // streams as a matter of course (a media frame gone stale), and those
1554
+ // reach no request handler.
1555
+ if (!isWtStream && resetTooMany()) {
1556
+ try { quic.close(H3_ERR.H3_EXCESSIVE_LOAD, 'too many stream resets', true); } catch (e) {}
1557
+ }
1558
+ }
1559
+ h3.on('stream_reset', function (sid, code) { reportCancel(sid, code, 'stream_reset'); });
1560
+ h3.on('stop_sending', function (sid, code) { reportCancel(sid, code, 'stop_sending'); });
1561
+
1562
+ // We ended a stream over what the peer sent on it (h3 streamError):
1563
+ // whatever request was built for it is aborted, as for a cancel.
1564
+ h3.on('stream_error', function (streamId) {
1565
+ clearHeadersDeadline(conn, streamId);
1566
+ releaseHttpStream(conn, streamId, true);
1567
+ });
1568
+
1569
+ // A WebTransport session ended in the HTTP/3 layer — its CONNECT stream
1570
+ // finished or was reset — and its streams were reset there. Here, the
1571
+ // same end: the session's streams are released (destroyed).
1572
+ h3.on('wt_session_closed', function (sessionId) {
1573
+ releaseSessionStreams(conn, Number(sessionId));
1574
+ });
1575
+
1576
+ h3.on('tunnel_data', function (streamId, data, fin) {
1577
+ var stream = conn.http_streams[streamId];
1578
+ if (!stream || !stream.tunnel) return;
1579
+ if (data && data.byteLength) {
1580
+ stream.tunnel.push(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
1581
+ }
1582
+ if (fin) {
1583
+ stream.tunnel.push(null);
1584
+ releaseHttpStream(conn, streamId, false);
1585
+ }
1586
+ });
1587
+
312
1588
  // ---- WebTransport stream events ----
313
1589
 
314
1590
  // New WT stream (bidi or uni from client)
315
1591
  h3.on('wt_stream', function (sessionId, wtStreamId, data, fin, isBidi) {
316
1592
  var session = conn.http_streams[sessionId];
317
- if (!session || !session.req._isWebTransport) return;
1593
+ // No session to give it to: nothing will read it, so nothing holds
1594
+ // its credit either.
1595
+ if (!session || !session.req._isWebTransport) { quic.releaseStreamCredit(wtStreamId); return; }
318
1596
 
319
1597
  // Create a Duplex-like stream for the WT stream
320
- var wtStream = _createWtStream(conn, quic, wtStreamId, sessionId, isBidi);
321
-
322
- // Push initial data
323
- if (data.byteLength > 0) _emitOnReq(session.req, 'data:' + wtStreamId, data);
324
- if (fin) _emitOnReq(session.req, 'end:' + wtStreamId);
1598
+ var wtStream = _createWtStream(conn, quic, wtStreamId, sessionId, isBidi ? 'bidi' : 'uni-in');
325
1599
 
326
1600
  // Emit on req
327
1601
  if (isBidi) {
@@ -332,34 +1606,44 @@ function createServer(options, handler) {
332
1606
 
333
1607
  // If initial data, push to stream
334
1608
  if (data.byteLength > 0) wtStream._pushData(data);
335
- if (fin) wtStream._endData();
1609
+ // Released at FIN, as 'wt_data' below does — a stream whose whole
1610
+ // content came in its first delivery never reaches that handler.
1611
+ if (fin) { wtStream._endData(); releaseHttpStream(conn, wtStreamId, false); }
336
1612
  });
337
1613
 
338
1614
  // Subsequent data on WT stream
339
1615
  h3.on('wt_data', function (sessionId, wtStreamId, data, fin) {
340
- if (!conn._wtStreamObjects) return;
341
1616
  var wtStream = conn._wtStreamObjects[wtStreamId];
342
1617
  if (!wtStream) return;
343
1618
  if (data.byteLength > 0) wtStream._pushData(data);
344
- if (fin) { wtStream._endData(); delete conn._wtStreamObjects[wtStreamId]; }
1619
+ if (fin) { wtStream._endData(); releaseHttpStream(conn, wtStreamId, false); }
345
1620
  });
346
1621
 
347
- // Datagrams
1622
+ // Datagrams: to the session their Quarter Stream ID names (RFC 9297
1623
+ // §2.1), looked up — not searched for among every request on the
1624
+ // connection, once per datagram.
348
1625
  quic.on('datagram', function (rawData) {
349
1626
  if (!rawData || rawData.byteLength === 0) return;
350
1627
  var result = readVarInt(rawData, 0);
351
1628
  if (!result) return;
352
- var quarterSid = result.value;
353
- var payload = rawData.slice(result.byteLength);
354
-
355
- // Find the session: quarter_stream_id = session_stream_id / 4
356
- for (var sid in conn.http_streams) {
357
- var session = conn.http_streams[sid];
358
- if (session.req._isWebTransport && Math.floor(Number(sid) / 4) === quarterSid) {
359
- _emitOnReq(session.req, 'datagram', Buffer.from(payload));
360
- break;
361
- }
1629
+ // A CONNECT-UDP tunnel's: the rest is Context ID and payload.
1630
+ var udpSid = conn.udpByQuarter[result.value];
1631
+ if (udpSid !== undefined) {
1632
+ var ust = conn.http_streams[udpSid];
1633
+ if (ust && ust.udp && ust.udp.sink) ust.udp.sink.datagram(rawData.subarray(result.byteLength));
1634
+ return;
362
1635
  }
1636
+ var session = conn.http_streams[conn.wtByQuarter[result.value]];
1637
+ if (!session || !session.req._isWebTransport) return;
1638
+ // One copy, into a buffer of its own: exact size, byteOffset 0.
1639
+ // This was slice() then Buffer.from() — two copies, and a
1640
+ // Buffer.from() under 4KB is carved out of Node's shared 8KB
1641
+ // pool: a consumer that hands the datagram to another thread
1642
+ // (postMessage) then clones the whole 8KB pool for every one.
1643
+ var payload = new Uint8Array(rawData.byteLength - result.byteLength);
1644
+ payload.set(rawData.subarray(result.byteLength));
1645
+ if (session.req._wtDatagrams) session.req._wtDatagrams._push(payload);
1646
+ _emitOnReq(session.req, 'datagram', Buffer.from(payload.buffer));
363
1647
  });
364
1648
  });
365
1649
  });
@@ -372,10 +1656,62 @@ function createServer(options, handler) {
372
1656
  // server. This layer adds HTTP/3 request events on top.
373
1657
  // ============================================================
374
1658
 
1659
+ // ---- Closing, as node:http (see h1_server) ----
1660
+ //
1661
+ // Idle: no request open on it — before its handshake too, as node:http
1662
+ // counts a connection that has not sent a request. GOAWAY, and closed at
1663
+ // once (closeAfterGoaway: nothing to wait for); before the handshake,
1664
+ // refused (goaway() does that).
1665
+ function isIdle(conn) { return Object.keys(conn.http_streams).length === 0; }
1666
+ // GOAWAY and CONNECTION_CLOSE together (§5.2 allows closing at once when
1667
+ // nothing is in flight): waiting for the GOAWAY's acknowledgement, a
1668
+ // client that had gone held the close until its idle timeout.
1669
+ function closeIdle(conn) {
1670
+ if (!conn.handle) return;
1671
+ conn.handle.goaway();
1672
+ conn.handle.destroy();
1673
+ }
1674
+ function closeIdleConnections() {
1675
+ for (var conn of conns) if (isIdle(conn)) closeIdle(conn);
1676
+ }
1677
+ function closeAllConnections() {
1678
+ for (var conn of conns) if (conn.handle) conn.handle.destroy();
1679
+ }
1680
+ // No new connections; GOAWAY on each — the requests in flight finish, then
1681
+ // the connection closes (closeAfterGoaway) — and once none is left, the
1682
+ // UDP sockets. It closed every connection on the spot: a response being
1683
+ // sent was cut off, and its client waited for the rest until a timeout.
1684
+ function close(callback) {
1685
+ qserver.stopAccepting();
1686
+ closingServer = typeof callback === 'function' ? callback : function () {};
1687
+ for (var conn of conns) {
1688
+ if (isIdle(conn)) closeIdle(conn);
1689
+ else if (conn.handle) conn.handle.goaway();
1690
+ }
1691
+ connGone();
1692
+ }
1693
+ function connGone() {
1694
+ if (!closingServer || conns.size > 0) return;
1695
+ var cb = closingServer;
1696
+ closingServer = null;
1697
+ qserver.close(cb);
1698
+ }
1699
+ function setLimits(o) {
1700
+ if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
1701
+ if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
1702
+ if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
1703
+ }
1704
+
375
1705
  return {
376
- listen: qserver.listen,
377
- close: qserver.close,
378
- on: function (name, fn) { ev.on(name, fn); },
1706
+ listen: function (p, h, cb) { closingServer = null; return qserver.listen(p, h, cb); },
1707
+ close: close,
1708
+ closeIdleConnections: closeIdleConnections,
1709
+ closeAllConnections: closeAllConnections,
1710
+ setLimits: setLimits,
1711
+ get connectionCount() { return conns.size; },
1712
+ ref: qserver.ref,
1713
+ unref: qserver.unref,
1714
+ on: function (name, fn) { if (name === 'upgrade') upgradeListeners++; ev.on(name, fn); },
379
1715
  setTimeout: function () { /* TODO */ },
380
1716
 
381
1717
  /** Feed an incoming UDP packet when running in external-socket mode.
@@ -384,7 +1720,21 @@ function createServer(options, handler) {
384
1720
 
385
1721
  /** Returns true if the 5-tuple matches an active QUIC connection.
386
1722
  * Used by demuxers for routing decisions on shared UDP ports. */
387
- hasConnection: qserver.hasConnection
1723
+ hasConnection: qserver.hasConnection,
1724
+
1725
+ getTicketKeys: qserver.getTicketKeys,
1726
+ setTicketKeys: qserver.setTicketKeys,
1727
+ getEchKeys: qserver.getEchKeys,
1728
+ setEchKeys: qserver.setEchKeys,
1729
+ getValidateAddress: qserver.getValidateAddress,
1730
+ setValidateAddress: qserver.setValidateAddress,
1731
+ getPendingHandshakes: qserver.getPendingHandshakes,
1732
+ getOptions: qserver.getOptions,
1733
+ setOptions: qserver.setOptions,
1734
+ getResetTokenSecret: qserver.getResetTokenSecret,
1735
+ setResetTokenSecret: qserver.setResetTokenSecret,
1736
+ getTokenSecret: qserver.getTokenSecret,
1737
+ setTokenSecret: qserver.setTokenSecret
388
1738
  };
389
1739
  }
390
1740