quico 0.4.0 → 0.7.0

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