quico 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/h3.js CHANGED
@@ -87,8 +87,12 @@ import {
87
87
  // deliberately NOT part of this move.
88
88
  // ============================================================
89
89
 
90
+ // One decoder for the module: decode() holds no state between calls (no
91
+ // `stream` option), and constructing one per name and per value cost more
92
+ // than the decoding itself.
93
+ var _utf8Decoder = new TextDecoder();
90
94
  function _text(bytes) {
91
- return new TextDecoder().decode(bytes);
95
+ return _utf8Decoder.decode(bytes);
92
96
  }
93
97
 
94
98
  // Every decodeStringLiteral() call below passes strict = true, enabling the
@@ -249,7 +253,10 @@ function parseQpackHeaderBlock(buf, dynamicTable, totalInserts, maxEntries, maxS
249
253
 
250
254
 
251
255
  // ============================================================
252
- // H3 Frame build/parse
256
+ // H3 Frame build
257
+ //
258
+ // Frames are read in place, from each stream's own buffer, as the bytes
259
+ // arrive: processBidiStream and processControlStream in H3Connection.
253
260
  // ============================================================
254
261
 
255
262
  function buildH3Frames(frames) {
@@ -262,43 +269,6 @@ function buildH3Frames(frames) {
262
269
  return concatUint8Arrays(parts);
263
270
  }
264
271
 
265
- function extractH3Frames(chunks, fromOffset) {
266
- if (!chunks || chunks.length === 0) return { frames: [], new_from_offset: fromOffset };
267
-
268
- var buffers = [];
269
- var acc = 0;
270
- for (var i = 0; i < chunks.length; i++) {
271
- var nextAcc = acc + chunks[i].length;
272
- if (fromOffset < nextAcc) {
273
- buffers.push(chunks[i].slice(fromOffset - acc));
274
- for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
275
- break;
276
- }
277
- acc = nextAcc;
278
- }
279
-
280
- if (buffers.length === 0) return { frames: [], new_from_offset: fromOffset };
281
-
282
- var combined = concatUint8Arrays(buffers);
283
- var offset = 0, frames = [];
284
-
285
- while (offset < combined.length) {
286
- var start = offset;
287
- var ft = readVarInt(combined, offset);
288
- if (!ft) break;
289
- offset += ft.byteLength;
290
- var len = readVarInt(combined, offset);
291
- if (!len) { offset = start; break; }
292
- offset += len.byteLength;
293
- if (offset + len.value > combined.length) { offset = start; break; }
294
- frames.push({ frame_type: ft.value, payload: combined.slice(offset, offset + len.value) });
295
- offset += len.value;
296
- }
297
-
298
- return { frames: frames, new_from_offset: fromOffset + offset };
299
- }
300
-
301
-
302
272
  // ============================================================
303
273
  // H3 Settings
304
274
  // ============================================================
@@ -337,8 +307,17 @@ function parseSettingsPayload(buf) {
337
307
  var list = [];
338
308
  var seen = {};
339
309
  while (offset < buf.length) {
340
- var id = readVarInt(buf, offset); if (!id) break; offset += id.byteLength;
341
- var val = readVarInt(buf, offset); if (!val) break; offset += val.byteLength;
310
+ // A payload that ends inside an identifier or before its value is
311
+ // H3_FRAME_ERROR (RFC 9114 §7.1). It was cut short silently: the setting
312
+ // dropped, the connection on.
313
+ var id = readVarInt(buf, offset);
314
+ var val = id ? readVarInt(buf, offset + id.byteLength) : null;
315
+ if (!id || !val) {
316
+ var te = new Error('SETTINGS payload ends inside a setting (' + (buf.length - offset) + ' byte(s) left)');
317
+ te.h3ErrorCode = 0x0106;
318
+ throw te;
319
+ }
320
+ offset += id.byteLength + val.byteLength;
342
321
  // RFC 9114 §7.2.4: an identifier twice, or one of HTTP/2's that HTTP/3
343
322
  // reserves (0x02-0x05), is H3_SETTINGS_ERROR.
344
323
  if (seen[id.value] || (id.value >= 0x02 && id.value <= 0x05)) {
@@ -369,24 +348,14 @@ function parseSettingsPayload(buf) {
369
348
  // QPACK Encoder Instructions (from peer's encoder stream)
370
349
  // ============================================================
371
350
 
372
- function parseQpackEncoderInstructions(chunks, fromOffset, maxLiteral) {
373
- if (!chunks || chunks.length === 0) return { instructions: [], new_from_offset: fromOffset, need: 0 };
351
+ // Parse the encoder-stream instructions in `combined` — the stream's unparsed
352
+ // bytes, from its read position. Returns the complete instructions, how many
353
+ // bytes they took (consumed), and, when the last one is cut short, how many
354
+ // bytes it needs (need). It used to take the stream's chunks and an offset
355
+ // and join them itself; the one caller already hands it a single buffer.
356
+ function parseQpackEncoderInstructions(combined, maxLiteral) {
357
+ if (!combined || combined.length === 0) return { instructions: [], consumed: 0, need: 0 };
374
358
  if (maxLiteral === undefined) maxLiteral = Infinity;
375
-
376
- var buffers = [];
377
- var acc = 0;
378
- for (var i = 0; i < chunks.length; i++) {
379
- var nextAcc = acc + chunks[i].length;
380
- if (fromOffset < nextAcc) {
381
- buffers.push(chunks[i].slice(fromOffset - acc));
382
- for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
383
- break;
384
- }
385
- acc = nextAcc;
386
- }
387
- if (buffers.length === 0) return { instructions: [], new_from_offset: fromOffset, need: 0 };
388
-
389
- var combined = buffers.length === 1 ? buffers[0] : concatUint8Arrays(buffers);
390
359
  var pos = 0;
391
360
  var instructions = [];
392
361
  // When the last instruction is cut short: how many bytes, from its start,
@@ -457,7 +426,7 @@ function parseQpackEncoderInstructions(chunks, fromOffset, maxLiteral) {
457
426
  }
458
427
  }
459
428
 
460
- return { instructions: instructions, new_from_offset: fromOffset + pos,
429
+ return { instructions: instructions, consumed: pos,
461
430
  need: needAbs > pos ? needAbs - pos : 0 };
462
431
  }
463
432
 
@@ -686,6 +655,11 @@ function H3Connection(options) {
686
655
  var decoderStreamId = null;
687
656
  var qpackKnownReceived = 0; // insertions the peer has acknowledged
688
657
  var qpackPendingAcks = 0; // insertions of THEIRS we owe an ack for
658
+ // Field sections we sent with a non-zero Required Insert Count, not yet
659
+ // acknowledged: stream id → [RIC, …] in the order sent (RFC 9204 §2.1.1).
660
+ // The decoder acknowledges each (Section Acknowledgment), or cancels the
661
+ // stream's; an acknowledgment for none is an error (§4.4.1).
662
+ var qpackUnacked = new Map();
689
663
 
690
664
  // ---- SETTINGS remembered for 0-RTT (RFC 9114 §7.2.4.2) ----
691
665
  //
@@ -784,9 +758,10 @@ function H3Connection(options) {
784
758
  * reference — and until an entry is below it, we send a literal instead.
785
759
  * That single number is what makes this encoder non-blocking.
786
760
  *
787
- * Section acknowledgements and stream cancellations are parsed and skipped:
788
- * they drive eviction accounting in an encoder that references
789
- * unacknowledged entries, which this one never does.
761
+ * Each instruction is held to what we actually sent (RFC 9204 §4.4): a
762
+ * Section Acknowledgment for a section we have outstanding on that stream,
763
+ * an Insert Count Increment of at least 1 that does not pass the entries we
764
+ * inserted. Anything else is QPACK_DECODER_STREAM_ERROR.
790
765
  */
791
766
  function processQpackDecoderStream(s) {
792
767
  if (s.buffered === 0) return;
@@ -795,14 +770,41 @@ function H3Connection(options) {
795
770
  var buf = Buffer.from(copyBytes(s, s.buffered));
796
771
 
797
772
  var r = qpackParseDecoderInstructions(buf, 0);
798
- if (r.increments > 0) {
799
- qpackKnownReceived += r.increments;
800
- if (DEBUG) console.log('[h3] qpack acked ' + r.increments +
801
- ' (known=' + qpackKnownReceived + ')');
773
+ for (var k = 0; k < r.instructions.length; k++) {
774
+ var ins = r.instructions[k];
775
+ if (ins.type === 'section_ack') {
776
+ var pend = qpackUnacked.get(ins.value);
777
+ if (!pend || pend.length === 0) {
778
+ throw qpackDecoderStreamError('Section Acknowledgment for stream ' + ins.value +
779
+ ', which has no field section awaiting one');
780
+ }
781
+ var ric = pend.shift();
782
+ if (pend.length === 0) qpackUnacked.delete(ins.value);
783
+ // §4.4.1: the acknowledged section's entries are now known received.
784
+ if (ric > qpackKnownReceived) qpackKnownReceived = ric;
785
+ } else if (ins.type === 'stream_cancel') {
786
+ qpackUnacked.delete(ins.value);
787
+ } else {
788
+ // §4.4.3: zero, or past what was inserted, is an error.
789
+ if (ins.value === 0) throw qpackDecoderStreamError('Insert Count Increment of 0');
790
+ if (qpackKnownReceived + ins.value > encoderTable.insertCount) {
791
+ throw qpackDecoderStreamError('Insert Count Increment to ' + (qpackKnownReceived + ins.value) +
792
+ ', beyond the ' + encoderTable.insertCount + ' entries inserted');
793
+ }
794
+ qpackKnownReceived += ins.value;
795
+ }
802
796
  }
797
+ if (DEBUG && r.instructions.length) console.log('[h3] qpack decoder instructions ' + r.instructions.length +
798
+ ' (known=' + qpackKnownReceived + ')');
803
799
  consumeBytes(s, r.consumed);
804
800
  }
805
801
 
802
+ function qpackDecoderStreamError(msg) {
803
+ var err = new Error('QPACK decoder stream: ' + msg);
804
+ err.h3ErrorCode = H3_ERR.QPACK_DECODER_STREAM_ERROR;
805
+ return err;
806
+ }
807
+
806
808
  function evictQpackTable(limit) {
807
809
  // Oldest first, until what remains fits. Sizes in BYTES: measuring by
808
810
  // string length, as this did, undercounts any non-ASCII header value and
@@ -887,7 +889,15 @@ function H3Connection(options) {
887
889
  if (context.control_streams_sent) return;
888
890
  context.control_streams_sent = true;
889
891
 
890
- var settings = buildSettingsPayload(localSettings(context));
892
+ // A reserved identifier among them, of a random N (RFC 9114 §7.2.4.1,
893
+ // 0x1f·N + 0x21): peers must ignore it — sending one keeps them honest.
894
+ // Not part of localSettings: 0-RTT's context (settingsContext) holds the
895
+ // ones that mean something, and this one changes every connection.
896
+ var greaseId = 0x1f * Math.floor(Math.random() * 0x10000) + 0x21;
897
+ var settings = concatUint8Arrays([
898
+ buildSettingsPayload(localSettings(context)),
899
+ writeVarInt(greaseId), writeVarInt(Math.floor(Math.random() * 64))
900
+ ]);
891
901
 
892
902
  var controlFrame = buildH3Frames([{ frame_type: 0x04, payload: settings }]);
893
903
 
@@ -918,6 +928,11 @@ function H3Connection(options) {
918
928
  }
919
929
 
920
930
 
931
+ // Our control and QPACK streams (sendControlStreams).
932
+ function isOwnCriticalStream(sid) {
933
+ return sid === (context.isServer ? 3 : 2) || sid === encoderStreamId || sid === decoderStreamId;
934
+ }
935
+
921
936
  // ---- Graceful shutdown: GOAWAY (RFC 9114 §5.2) ----
922
937
 
923
938
  function sendGoaway(id) {
@@ -1195,6 +1210,15 @@ function H3Connection(options) {
1195
1210
  // a WT stream, so it can be routed to its session.
1196
1211
  listenQuic('stream_reset', function (streamId, code) {
1197
1212
  var sid = Number(streamId);
1213
+ // The peer's control or QPACK stream, reset: closed, which RFC 9114
1214
+ // §6.2.1 and RFC 9204 §4.2 make a connection error however it happens.
1215
+ // Only a FIN was caught (processStream); a RESET_STREAM left the
1216
+ // connection serving on without them.
1217
+ var crit = context.streams[sid];
1218
+ if (crit && (crit.type === 'control' || crit.type === 'qpack_enc' || crit.type === 'qpack_dec')) {
1219
+ connectionError(H3_ERR.H3_CLOSED_CRITICAL_STREAM, 'the peer reset its ' + crit.type + ' stream');
1220
+ return;
1221
+ }
1198
1222
  var wt = _wtStreams[sid];
1199
1223
  var wasParsing = sid in context.streams;
1200
1224
  // A client's request stream reset before its HEADERS came: there is no
@@ -1219,6 +1243,13 @@ function H3Connection(options) {
1219
1243
  // whole cancellation.
1220
1244
  listenQuic('stop_sending', function (streamId, code) {
1221
1245
  var sid = Number(streamId);
1246
+ // Asked to stop one of OUR critical streams: QUIC has answered with
1247
+ // RESET_STREAM, so it is closed — the same connection error (§6.2.1:
1248
+ // "A receiver MUST NOT request that the sender close the control stream").
1249
+ if (context.control_streams_sent && isOwnCriticalStream(sid)) {
1250
+ connectionError(H3_ERR.H3_CLOSED_CRITICAL_STREAM, 'the peer stopped our ' + (sid === encoderStreamId ? 'QPACK encoder' : sid === decoderStreamId ? 'QPACK decoder' : 'control') + ' stream');
1251
+ return;
1252
+ }
1222
1253
  if (_abortedHere.has(sid)) return;
1223
1254
  ev.emit('stop_sending', sid, code);
1224
1255
  });
@@ -1700,7 +1731,7 @@ function H3Connection(options) {
1700
1731
  // Only the unparsed bytes are copied (once per completed instruction,
1701
1732
  // given `need`), and no literal larger than our table can hold is
1702
1733
  // waited for.
1703
- ext = parseQpackEncoderInstructions([copyBytes(s, s.buffered)], 0,
1734
+ ext = parseQpackEncoderInstructions(copyBytes(s, s.buffered),
1704
1735
  context.local_qpack_max_table_capacity);
1705
1736
  } catch (err) {
1706
1737
  // RFC 9204 §4.2: a malformed encoder stream is QPACK_ENCODER_STREAM_ERROR,
@@ -1708,7 +1739,7 @@ function H3Connection(options) {
1708
1739
  err.h3ErrorCode = H3_ERR.QPACK_ENCODER_STREAM_ERROR;
1709
1740
  throw err;
1710
1741
  }
1711
- consumeBytes(s, ext.new_from_offset);
1742
+ consumeBytes(s, ext.consumed);
1712
1743
  s.need = ext.need;
1713
1744
  if (ext.instructions.length === 0) return;
1714
1745
 
@@ -2000,6 +2031,11 @@ function H3Connection(options) {
2000
2031
  if (avail < headLen + len.value) break;
2001
2032
  // Waits for the encoder stream when it must (see sectionBlocked): the
2002
2033
  // frame stays in the buffer, read again once unblocked.
2034
+ // Already waiting, and the inserts it needs are not here yet: nothing
2035
+ // to do. The frame was copied and its prefix decoded again on every
2036
+ // packet that arrived on the stream meanwhile.
2037
+ if (_blocked.has(Number(streamId)) &&
2038
+ _blocked.get(Number(streamId)) > context.remote_qpack_table_base_index) return;
2003
2039
  var whole = copyBytes(s, headLen + len.value);
2004
2040
  var payload = whole.subarray(headLen);
2005
2041
  if (sectionBlocked(Number(streamId), payload)) return;
@@ -2138,7 +2174,11 @@ function H3Connection(options) {
2138
2174
  var list = [];
2139
2175
  for (var hname in headers) {
2140
2176
  if (Object.prototype.hasOwnProperty.call(headers, hname)) {
2141
- list.push([hname, headers[hname]]);
2177
+ // A list: one field line per value (Set-Cookie above all, which may
2178
+ // not be combined), not String(list).
2179
+ var hv = headers[hname];
2180
+ if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) list.push([hname, String(hv[hi])]); }
2181
+ else list.push([hname, hv]);
2142
2182
  }
2143
2183
  }
2144
2184
  var enc = qpackEncodeHeaderBlock(list, encoderTable, {
@@ -2155,6 +2195,11 @@ function H3Connection(options) {
2155
2195
  // Insertions FIRST, on their own stream: they are for the next request,
2156
2196
  // not this one.
2157
2197
  flushQpackInserts(enc.inserts);
2198
+ if (enc.requiredInsertCount > 0) {
2199
+ var pend = qpackUnacked.get(streamId);
2200
+ if (pend) pend.push(enc.requiredInsertCount);
2201
+ else qpackUnacked.set(streamId, [enc.requiredInsertCount]);
2202
+ }
2158
2203
 
2159
2204
  var frame = buildH3Frames([{ frame_type: 1, payload: new Uint8Array(enc.block) }]);
2160
2205
  if (DEBUG) console.log('[h3] sendHeaders stream=' + streamId + ' len=' + frame.byteLength + ' fin=' + !!fin);
package/src/h3_client.js CHANGED
@@ -248,6 +248,7 @@ function request(options, callback) {
248
248
  udpSocket = createQuicClientSocket({
249
249
  remoteIp: remoteIp,
250
250
  remotePort: remotePort,
251
+ socketBufferSize: options.socketBufferSize,
251
252
  // SNI and the name the certificate is checked for: tls.connect's
252
253
  // servername where given.
253
254
  hostname: options.servername || hostname,
@@ -476,7 +477,11 @@ function request(options, callback) {
476
477
  return e;
477
478
  });
478
479
  });
479
- conn.quic.on('close', function () {
480
+ // On 'closing' — the moment the connection is over (a CONNECTION_CLOSE
481
+ // either way, the idle timeout) — not only on 'close', which waits out
482
+ // the draining period (RFC 9000 §10.2, ~3 s): a response the server cut
483
+ // off hung that long before it failed.
484
+ function connectionOver() {
480
485
  failAll(0, function (sid) {
481
486
  var e = new Error('HTTP/3 connection closed before the response ended');
482
487
  e.code = 'ERR_HTTP3_CONNECTION_CLOSED';
@@ -484,7 +489,9 @@ function request(options, callback) {
484
489
  e.retryable = sid >= conn._goawayId;
485
490
  return e;
486
491
  });
487
- });
492
+ }
493
+ conn.quic.on('closing', connectionOver);
494
+ conn.quic.on('close', connectionOver);
488
495
  }
489
496
 
490
497
 
@@ -662,17 +669,28 @@ function request(options, callback) {
662
669
  else if (reqBody.length) startStreaming();
663
670
  }
664
671
 
665
- function requestHeaders() {
672
+ // contentLength: the body's size when it is known whole (ended before it
673
+ // went) — sent as h2_client and node:https send it, and as browsers do. A
674
+ // server that decides whether there is a body from the headers (Express's
675
+ // body-parser: content-length or transfer-encoding) saw none over HTTP/3,
676
+ // and req.body came out empty.
677
+ function requestHeaders(contentLength) {
666
678
  var h3Headers = {
667
679
  ':method': method,
668
680
  ':scheme': 'https',
669
681
  ':authority': hostname + (port !== 443 ? ':' + port : ''),
670
682
  ':path': path
671
683
  };
672
- for (var name in headers) h3Headers[name.toLowerCase()] = String(headers[name]);
684
+ // A repeated field (an array) stays one line per value: String(list)
685
+ // made two cookies "a=1,b=2", which the server reads as one cookie.
686
+ for (var name in headers) {
687
+ var v = headers[name];
688
+ h3Headers[name.toLowerCase()] = Array.isArray(v) ? v.map(String) : String(v);
689
+ }
673
690
  if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
674
691
  if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
675
692
  if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
693
+ if (contentLength != null && h3Headers['content-length'] === undefined) h3Headers['content-length'] = String(contentLength);
676
694
  return h3Headers;
677
695
  }
678
696
 
@@ -687,7 +705,9 @@ function request(options, callback) {
687
705
  // Early: kept, to go again if the server refuses it.
688
706
  if (earlyTried && !handshaken) earlyBody = { body: body.slice(), trailers: reqTrailers };
689
707
  if (body.length === 0 && !reqTrailers) { h3.sendHeaders(streamId, requestHeaders(), true); return; }
690
- h3.sendHeaders(streamId, requestHeaders(), false);
708
+ var bodyLength = 0;
709
+ for (var bi = 0; bi < body.length; bi++) bodyLength += body[bi].byteLength;
710
+ h3.sendHeaders(streamId, requestHeaders(body.length ? bodyLength : null), false);
691
711
  for (var i = 0; i < body.length; i++) h3.sendBody(streamId, body[i], i === body.length - 1 && !reqTrailers);
692
712
  if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
693
713
  }
package/src/h3_server.js CHANGED
@@ -28,7 +28,8 @@ import { createQuicServer } from './quic_server.js';
28
28
  import { pickTlsOptions } from './tls_options.js';
29
29
  import { _makeSocketStub } from './streams.js';
30
30
  import { H3Connection, H3_ERR, wtCloseCapsule, wtDrainCapsule, createCapsuleReader, h3SettingsContext } from './h3.js';
31
- import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats } from './wt_stream.js';
31
+ import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats,
32
+ exporterArgs, exportSessionKeyingMaterial } from './wt_stream.js';
32
33
  import { contextZero, datagramCapsule, STREAM_QUEUE_MAX } from './connect_udp.js';
33
34
 
34
35
 
@@ -132,6 +133,7 @@ function createServer(options, handler) {
132
133
  ech: options.ech || null,
133
134
  ocspRequest: options.ocspRequest || null,
134
135
  resetTokenSecret: options.resetTokenSecret || null,
136
+ tokenSecret: options.tokenSecret || null,
135
137
  validateAddress: options.validateAddress === true,
136
138
  handshakeLoadThreshold: options.handshakeLoadThreshold,
137
139
  earlyData: options.earlyData, earlyDataMaxAge: options.earlyDataMaxAge, earlyDataReplay: options.earlyDataReplay,
@@ -151,6 +153,7 @@ function createServer(options, handler) {
151
153
  maxUdpPayloadSize: options.maxUdpPayloadSize,
152
154
  initialRtt: options.initialRtt,
153
155
  reorderWindow: options.reorderWindow,
156
+ socketBufferSize: options.socketBufferSize,
154
157
  connectionOptions: options.connectionOptions,
155
158
  quicVersions: options.quicVersions,
156
159
  socket: options.socket || null,
@@ -607,10 +610,15 @@ function createServer(options, handler) {
607
610
  // writeHead, when the status is known.
608
611
  _bodyAllowed: true,
609
612
 
613
+ // A list is kept a list, and goes as one field line per value, as
614
+ // HTTP/1.1 and HTTP/2 send it here and as Node's http does. It was
615
+ // String(list): two cookies became one "a=1,b=2", which a browser
616
+ // reads as a single cookie a with the value "1,b=2" (Set-Cookie
617
+ // never combines, RFC 9110 §5.3).
610
618
  setHeader: function (name, value) {
611
619
  validateHeaderName(name);
612
620
  validateHeaderValue(name, value);
613
- res.headers[name.toLowerCase()] = String(value);
621
+ res.headers[name.toLowerCase()] = Array.isArray(value) ? value.map(String) : String(value);
614
622
  },
615
623
 
616
624
  getHeader: function (name) {
@@ -640,7 +648,7 @@ function createServer(options, handler) {
640
648
  }
641
649
  // Date (RFC 9110 §6.6.1), as HTTP/2 and HTTP/1.1 send it — HTTP/3's
642
650
  // responses had none.
643
- if (!('date' in h3Headers) && statusCode >= 200) h3Headers['date'] = httpDate();
651
+ if (!('date' in h3Headers) && statusCode >= 200 && !(res._wrapper && res._wrapper.sendDate === false)) h3Headers['date'] = httpDate();
644
652
 
645
653
  if (conn.h3) {
646
654
  // Send headers WITHOUT FIN for WT (session stays open)
@@ -703,6 +711,12 @@ function createServer(options, handler) {
703
711
  if (typeof callback === 'function') process.nextTick(callback);
704
712
  },
705
713
 
714
+ // 102 Processing (node:http's writeProcessing): as Early Hints go.
715
+ writeProcessing: function () {
716
+ if (res.headersSent || res._finished || !conn.h3) return;
717
+ try { conn.h3.sendHeaders(streamId, { ':status': '102' }, false); } catch (e) { /* stream gone */ }
718
+ },
719
+
706
720
  write: function (chunk) {
707
721
  if (!res.headersSent) res.writeHead(res.statusCode);
708
722
  if (!res._bodyAllowed) return true;
@@ -868,6 +882,17 @@ function createServer(options, handler) {
868
882
  // datagram's.
869
883
  createSendGroup: function () { return createSendGroup(); },
870
884
 
885
+ // The browser's exportKeyingMaterial, as the client's: this session's
886
+ // keying material from the TLS exporter — the same bytes the client
887
+ // gets for the same label and context.
888
+ exportKeyingMaterial: function (label, context, outputLength) {
889
+ var a;
890
+ try { a = exporterArgs(arguments); } catch (e) { return Promise.reject(e); }
891
+ if (!req._isWebTransport || req._wtClosed) return Promise.reject(new DOMException('the session is closed', 'InvalidStateError'));
892
+ try { return Promise.resolve(exportSessionKeyingMaterial(conn.quic, Number(streamId), a)); }
893
+ catch (e) { return Promise.reject(e); }
894
+ },
895
+
871
896
  // A stream of ours: a Duplex, or a Writable — recorded with its
872
897
  // session, so the session's end destroys it. options { sendOrder,
873
898
  // sendGroup, waitUntilAvailable }, the browser's (wt_stream's
@@ -929,6 +954,13 @@ function createServer(options, handler) {
929
954
  for (var name in params.request_headers) {
930
955
  stream.req.headers[name] = params.request_headers[name];
931
956
  }
957
+ // The field lines as they came (validateFieldSection), which the
958
+ // copy above leaves behind — not enumerable: req.rawHeaders and
959
+ // headersDistinct are built from them. Without it they were rebuilt
960
+ // from the joined values, two x-a lines one "1, 2".
961
+ if (params.request_headers._rawHeaders && !stream.req.headers._rawHeaders) {
962
+ Object.defineProperty(stream.req.headers, '_rawHeaders', { value: params.request_headers._rawHeaders, enumerable: false });
963
+ }
932
964
 
933
965
  // Extract :method and :path
934
966
  if (stream.req.method === null && ':method' in stream.req.headers) {
@@ -1023,6 +1055,7 @@ function createServer(options, handler) {
1023
1055
  releaseHttpStream(conn, streamId, false);
1024
1056
  return;
1025
1057
  }
1058
+ stream.req.upgrade = true;
1026
1059
  ev.emit('upgrade', stream.req, duplex, Buffer.alloc(0));
1027
1060
  return;
1028
1061
  }
@@ -1188,6 +1221,10 @@ function createServer(options, handler) {
1188
1221
  return sock;
1189
1222
  }
1190
1223
 
1224
+ // Every connection, for closing them (closeIdleConnections, close()).
1225
+ var conns = new Set();
1226
+ var closingServer = null; // close()'s callback, once every connection is gone
1227
+
1191
1228
  qserver.on('connection', function (quic, peer) {
1192
1229
  var conn = { quic: quic, h3: null, http_streams: {}, _wtStreamObjects: {}, served: 0,
1193
1230
  headerDeadlines: {}, // request streams whose HEADERS is still arriving
@@ -1208,7 +1245,12 @@ function createServer(options, handler) {
1208
1245
  // 'aborted' on the request and 'close' on the response, a tunnel
1209
1246
  // destroyed, a session closed with its streams. Requests that had
1210
1247
  // finished in both directions are already gone (releaseIfDone).
1248
+ conns.add(conn);
1249
+ // Over for the application ('closing'), or its draining done ('close').
1250
+ var gone = function () { if (conns.delete(conn)) connGone(); };
1251
+ quic.on('closing', gone);
1211
1252
  quic.on('close', function () {
1253
+ gone();
1212
1254
  for (var hid in conn.headerDeadlines) clearHeadersDeadline(conn, hid);
1213
1255
  for (var sid in conn.http_streams) releaseHttpStream(conn, sid, true);
1214
1256
  for (var wid in conn._wtStreamObjects) releaseHttpStream(conn, wid, true);
@@ -1615,9 +1657,63 @@ function createServer(options, handler) {
1615
1657
  // server. This layer adds HTTP/3 request events on top.
1616
1658
  // ============================================================
1617
1659
 
1660
+ // ---- Closing, as node:http (see h1_server) ----
1661
+ //
1662
+ // Idle: no request open on it — before its handshake too, as node:http
1663
+ // counts a connection that has not sent a request. GOAWAY, and closed at
1664
+ // once (closeAfterGoaway: nothing to wait for); before the handshake,
1665
+ // refused (goaway() does that).
1666
+ function isIdle(conn) { return Object.keys(conn.http_streams).length === 0; }
1667
+ // GOAWAY and CONNECTION_CLOSE together (§5.2 allows closing at once when
1668
+ // nothing is in flight): waiting for the GOAWAY's acknowledgement, a
1669
+ // client that had gone held the close until its idle timeout.
1670
+ function closeIdle(conn) {
1671
+ if (!conn.handle) return;
1672
+ conn.handle.goaway();
1673
+ conn.handle.destroy();
1674
+ }
1675
+ function closeIdleConnections() {
1676
+ for (var conn of conns) if (isIdle(conn)) closeIdle(conn);
1677
+ }
1678
+ function closeAllConnections() {
1679
+ for (var conn of conns) if (conn.handle) conn.handle.destroy();
1680
+ }
1681
+ // No new connections; GOAWAY on each — the requests in flight finish, then
1682
+ // the connection closes (closeAfterGoaway) — and once none is left, the
1683
+ // UDP sockets. It closed every connection on the spot: a response being
1684
+ // sent was cut off, and its client waited for the rest until a timeout.
1685
+ function close(callback) {
1686
+ qserver.stopAccepting();
1687
+ closingServer = typeof callback === 'function' ? callback : function () {};
1688
+ for (var conn of conns) {
1689
+ if (isIdle(conn)) closeIdle(conn);
1690
+ else if (conn.handle) conn.handle.goaway();
1691
+ }
1692
+ connGone();
1693
+ }
1694
+ function connGone() {
1695
+ if (!closingServer || conns.size > 0) return;
1696
+ var cb = closingServer;
1697
+ closingServer = null;
1698
+ qserver.close(cb);
1699
+ }
1700
+ function setLimits(o) {
1701
+ if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
1702
+ if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
1703
+ if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
1704
+ }
1705
+
1618
1706
  return {
1619
- listen: qserver.listen,
1620
- close: qserver.close,
1707
+ listen: function (p, h, cb) { closingServer = null; return qserver.listen(p, h, cb); },
1708
+ // The UDP socket's address — after listen(0), the port the OS gave.
1709
+ address: function () { return qserver.address(); },
1710
+ close: close,
1711
+ closeIdleConnections: closeIdleConnections,
1712
+ closeAllConnections: closeAllConnections,
1713
+ setLimits: setLimits,
1714
+ get connectionCount() { return conns.size; },
1715
+ ref: qserver.ref,
1716
+ unref: qserver.unref,
1621
1717
  on: function (name, fn) { if (name === 'upgrade') upgradeListeners++; ev.on(name, fn); },
1622
1718
  setTimeout: function () { /* TODO */ },
1623
1719
 
@@ -1639,7 +1735,9 @@ function createServer(options, handler) {
1639
1735
  getOptions: qserver.getOptions,
1640
1736
  setOptions: qserver.setOptions,
1641
1737
  getResetTokenSecret: qserver.getResetTokenSecret,
1642
- setResetTokenSecret: qserver.setResetTokenSecret
1738
+ setResetTokenSecret: qserver.setResetTokenSecret,
1739
+ getTokenSecret: qserver.getTokenSecret,
1740
+ setTokenSecret: qserver.setTokenSecret
1643
1741
  };
1644
1742
  }
1645
1743