quico 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,
@@ -607,10 +609,15 @@ function createServer(options, handler) {
607
609
  // writeHead, when the status is known.
608
610
  _bodyAllowed: true,
609
611
 
612
+ // A list is kept a list, and goes as one field line per value, as
613
+ // HTTP/1.1 and HTTP/2 send it here and as Node's http does. It was
614
+ // String(list): two cookies became one "a=1,b=2", which a browser
615
+ // reads as a single cookie a with the value "1,b=2" (Set-Cookie
616
+ // never combines, RFC 9110 §5.3).
610
617
  setHeader: function (name, value) {
611
618
  validateHeaderName(name);
612
619
  validateHeaderValue(name, value);
613
- res.headers[name.toLowerCase()] = String(value);
620
+ res.headers[name.toLowerCase()] = Array.isArray(value) ? value.map(String) : String(value);
614
621
  },
615
622
 
616
623
  getHeader: function (name) {
@@ -640,7 +647,7 @@ function createServer(options, handler) {
640
647
  }
641
648
  // Date (RFC 9110 §6.6.1), as HTTP/2 and HTTP/1.1 send it — HTTP/3's
642
649
  // responses had none.
643
- if (!('date' in h3Headers) && statusCode >= 200) h3Headers['date'] = httpDate();
650
+ if (!('date' in h3Headers) && statusCode >= 200 && !(res._wrapper && res._wrapper.sendDate === false)) h3Headers['date'] = httpDate();
644
651
 
645
652
  if (conn.h3) {
646
653
  // Send headers WITHOUT FIN for WT (session stays open)
@@ -703,6 +710,12 @@ function createServer(options, handler) {
703
710
  if (typeof callback === 'function') process.nextTick(callback);
704
711
  },
705
712
 
713
+ // 102 Processing (node:http's writeProcessing): as Early Hints go.
714
+ writeProcessing: function () {
715
+ if (res.headersSent || res._finished || !conn.h3) return;
716
+ try { conn.h3.sendHeaders(streamId, { ':status': '102' }, false); } catch (e) { /* stream gone */ }
717
+ },
718
+
706
719
  write: function (chunk) {
707
720
  if (!res.headersSent) res.writeHead(res.statusCode);
708
721
  if (!res._bodyAllowed) return true;
@@ -868,6 +881,17 @@ function createServer(options, handler) {
868
881
  // datagram's.
869
882
  createSendGroup: function () { return createSendGroup(); },
870
883
 
884
+ // The browser's exportKeyingMaterial, as the client's: this session's
885
+ // keying material from the TLS exporter — the same bytes the client
886
+ // gets for the same label and context.
887
+ exportKeyingMaterial: function (label, context, outputLength) {
888
+ var a;
889
+ try { a = exporterArgs(arguments); } catch (e) { return Promise.reject(e); }
890
+ if (!req._isWebTransport || req._wtClosed) return Promise.reject(new DOMException('the session is closed', 'InvalidStateError'));
891
+ try { return Promise.resolve(exportSessionKeyingMaterial(conn.quic, Number(streamId), a)); }
892
+ catch (e) { return Promise.reject(e); }
893
+ },
894
+
871
895
  // A stream of ours: a Duplex, or a Writable — recorded with its
872
896
  // session, so the session's end destroys it. options { sendOrder,
873
897
  // sendGroup, waitUntilAvailable }, the browser's (wt_stream's
@@ -929,6 +953,13 @@ function createServer(options, handler) {
929
953
  for (var name in params.request_headers) {
930
954
  stream.req.headers[name] = params.request_headers[name];
931
955
  }
956
+ // The field lines as they came (validateFieldSection), which the
957
+ // copy above leaves behind — not enumerable: req.rawHeaders and
958
+ // headersDistinct are built from them. Without it they were rebuilt
959
+ // from the joined values, two x-a lines one "1, 2".
960
+ if (params.request_headers._rawHeaders && !stream.req.headers._rawHeaders) {
961
+ Object.defineProperty(stream.req.headers, '_rawHeaders', { value: params.request_headers._rawHeaders, enumerable: false });
962
+ }
932
963
 
933
964
  // Extract :method and :path
934
965
  if (stream.req.method === null && ':method' in stream.req.headers) {
@@ -1023,6 +1054,7 @@ function createServer(options, handler) {
1023
1054
  releaseHttpStream(conn, streamId, false);
1024
1055
  return;
1025
1056
  }
1057
+ stream.req.upgrade = true;
1026
1058
  ev.emit('upgrade', stream.req, duplex, Buffer.alloc(0));
1027
1059
  return;
1028
1060
  }
@@ -1188,6 +1220,10 @@ function createServer(options, handler) {
1188
1220
  return sock;
1189
1221
  }
1190
1222
 
1223
+ // Every connection, for closing them (closeIdleConnections, close()).
1224
+ var conns = new Set();
1225
+ var closingServer = null; // close()'s callback, once every connection is gone
1226
+
1191
1227
  qserver.on('connection', function (quic, peer) {
1192
1228
  var conn = { quic: quic, h3: null, http_streams: {}, _wtStreamObjects: {}, served: 0,
1193
1229
  headerDeadlines: {}, // request streams whose HEADERS is still arriving
@@ -1208,7 +1244,12 @@ function createServer(options, handler) {
1208
1244
  // 'aborted' on the request and 'close' on the response, a tunnel
1209
1245
  // destroyed, a session closed with its streams. Requests that had
1210
1246
  // finished in both directions are already gone (releaseIfDone).
1247
+ conns.add(conn);
1248
+ // Over for the application ('closing'), or its draining done ('close').
1249
+ var gone = function () { if (conns.delete(conn)) connGone(); };
1250
+ quic.on('closing', gone);
1211
1251
  quic.on('close', function () {
1252
+ gone();
1212
1253
  for (var hid in conn.headerDeadlines) clearHeadersDeadline(conn, hid);
1213
1254
  for (var sid in conn.http_streams) releaseHttpStream(conn, sid, true);
1214
1255
  for (var wid in conn._wtStreamObjects) releaseHttpStream(conn, wid, true);
@@ -1615,9 +1656,61 @@ function createServer(options, handler) {
1615
1656
  // server. This layer adds HTTP/3 request events on top.
1616
1657
  // ============================================================
1617
1658
 
1659
+ // ---- Closing, as node:http (see h1_server) ----
1660
+ //
1661
+ // Idle: no request open on it — before its handshake too, as node:http
1662
+ // counts a connection that has not sent a request. GOAWAY, and closed at
1663
+ // once (closeAfterGoaway: nothing to wait for); before the handshake,
1664
+ // refused (goaway() does that).
1665
+ function isIdle(conn) { return Object.keys(conn.http_streams).length === 0; }
1666
+ // GOAWAY and CONNECTION_CLOSE together (§5.2 allows closing at once when
1667
+ // nothing is in flight): waiting for the GOAWAY's acknowledgement, a
1668
+ // client that had gone held the close until its idle timeout.
1669
+ function closeIdle(conn) {
1670
+ if (!conn.handle) return;
1671
+ conn.handle.goaway();
1672
+ conn.handle.destroy();
1673
+ }
1674
+ function closeIdleConnections() {
1675
+ for (var conn of conns) if (isIdle(conn)) closeIdle(conn);
1676
+ }
1677
+ function closeAllConnections() {
1678
+ for (var conn of conns) if (conn.handle) conn.handle.destroy();
1679
+ }
1680
+ // No new connections; GOAWAY on each — the requests in flight finish, then
1681
+ // the connection closes (closeAfterGoaway) — and once none is left, the
1682
+ // UDP sockets. It closed every connection on the spot: a response being
1683
+ // sent was cut off, and its client waited for the rest until a timeout.
1684
+ function close(callback) {
1685
+ qserver.stopAccepting();
1686
+ closingServer = typeof callback === 'function' ? callback : function () {};
1687
+ for (var conn of conns) {
1688
+ if (isIdle(conn)) closeIdle(conn);
1689
+ else if (conn.handle) conn.handle.goaway();
1690
+ }
1691
+ connGone();
1692
+ }
1693
+ function connGone() {
1694
+ if (!closingServer || conns.size > 0) return;
1695
+ var cb = closingServer;
1696
+ closingServer = null;
1697
+ qserver.close(cb);
1698
+ }
1699
+ function setLimits(o) {
1700
+ if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
1701
+ if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
1702
+ if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
1703
+ }
1704
+
1618
1705
  return {
1619
- listen: qserver.listen,
1620
- close: qserver.close,
1706
+ listen: function (p, h, cb) { closingServer = null; return qserver.listen(p, h, cb); },
1707
+ close: close,
1708
+ closeIdleConnections: closeIdleConnections,
1709
+ closeAllConnections: closeAllConnections,
1710
+ setLimits: setLimits,
1711
+ get connectionCount() { return conns.size; },
1712
+ ref: qserver.ref,
1713
+ unref: qserver.unref,
1621
1714
  on: function (name, fn) { if (name === 'upgrade') upgradeListeners++; ev.on(name, fn); },
1622
1715
  setTimeout: function () { /* TODO */ },
1623
1716
 
@@ -1639,7 +1732,9 @@ function createServer(options, handler) {
1639
1732
  getOptions: qserver.getOptions,
1640
1733
  setOptions: qserver.setOptions,
1641
1734
  getResetTokenSecret: qserver.getResetTokenSecret,
1642
- setResetTokenSecret: qserver.setResetTokenSecret
1735
+ setResetTokenSecret: qserver.setResetTokenSecret,
1736
+ getTokenSecret: qserver.getTokenSecret,
1737
+ setTokenSecret: qserver.setTokenSecret
1643
1738
  };
1644
1739
  }
1645
1740
 
@@ -84,6 +84,7 @@ var HUFF_BITS = new Uint8Array([
84
84
  ]);
85
85
 
86
86
  var HUFF_EOS = 256; // index of the EOS symbol; decoding it is an error
87
+ var _HD_HAS_SYM = 1 << 17, _HD_INVALID = 1 << 18; // huffDecode table flags
87
88
 
88
89
  var _huffRoot = (function () {
89
90
  var root = {};
@@ -100,6 +101,51 @@ var _huffRoot = (function () {
100
101
  })();
101
102
 
102
103
 
104
+ // Table-driven decoding, four bits at a time.
105
+ //
106
+ // The decoder used to walk the code tree one bit per step: eight object
107
+ // lookups per input byte. Here every internal node of that same tree is a
108
+ // state, and for each state and each 4-bit nibble a table holds what four
109
+ // steps of the walk would have done — the node it ends on, the symbol it
110
+ // completed (at most one: the shortest code is 5 bits), and whether it fell
111
+ // off the tree. Two lookups per byte, same walk, same results.
112
+ //
113
+ // The padding checks at the end need only the state the input stopped in:
114
+ // the bits since the last complete symbol are that node's depth, and they
115
+ // are all ones exactly when its path from the root is.
116
+ var _huffDec = (function () {
117
+ var nodes = [_huffRoot], depth = [0], ones = [1];
118
+ var idOf = new Map([[_huffRoot, 0]]);
119
+ for (var q = 0; q < nodes.length; q++) {
120
+ for (var b = 0; b <= 1; b++) {
121
+ var child = nodes[q][b];
122
+ if (child && child.sym === undefined && !idOf.has(child)) {
123
+ idOf.set(child, nodes.length);
124
+ nodes.push(child);
125
+ depth.push(depth[q] + 1);
126
+ ones.push(ones[q] && b === 1 ? 1 : 0);
127
+ }
128
+ }
129
+ }
130
+ // entry = next | sym << 8 | HAS_SYM | INVALID ; sym is 0..256
131
+ var table = new Int32Array(nodes.length * 16);
132
+ for (var st = 0; st < nodes.length; st++) {
133
+ for (var nib = 0; nib < 16; nib++) {
134
+ var node = nodes[st], entry = 0;
135
+ for (var k = 3; k >= 0; k--) {
136
+ node = node[(nib >> k) & 1];
137
+ if (!node) { entry |= _HD_INVALID; node = _huffRoot; break; }
138
+ if (node.sym !== undefined) {
139
+ entry |= _HD_HAS_SYM | (node.sym << 8);
140
+ node = _huffRoot;
141
+ }
142
+ }
143
+ table[st * 16 + nib] = entry | idOf.get(node);
144
+ }
145
+ }
146
+ return { table: table, depth: Uint8Array.from(depth), ones: Uint8Array.from(ones) };
147
+ })();
148
+
103
149
  /**
104
150
  * Decode a Huffman-coded byte string (RFC 7541 §5.2). Returns the raw
105
151
  * Uint8Array, NOT a string.
@@ -117,42 +163,35 @@ var _huffRoot = (function () {
117
163
  * decoders that must answer a malformed peer with COMPRESSION_ERROR pass true.
118
164
  */
119
165
  function huffDecode(buf, strict) {
120
- var out = [];
121
- var node = _huffRoot;
122
- var nbits = 0, current = 0;
123
- var sincePadStart = 0; // bits consumed since the last complete symbol
124
- var padIsOnes = true; // were all of those bits 1?
166
+ var table = _huffDec.table;
167
+ var out = new Uint8Array(((buf.length * 8) / 5) | 0);
168
+ var n = 0, state = 0;
125
169
 
126
170
  for (var i = 0; i < buf.length; i++) {
127
- current = (current << 8) | buf[i];
128
- nbits += 8;
129
- while (nbits > 0) {
130
- var bit = (current >> (nbits - 1)) & 1;
131
- node = node[bit];
132
- if (!node) throw new Error('Invalid Huffman code');
133
- nbits--;
134
- sincePadStart++;
135
- if (bit === 0) padIsOnes = false;
136
- if (node.sym !== undefined) {
137
- if (strict && node.sym === HUFF_EOS) {
171
+ var byte = buf[i];
172
+ for (var half = 0; half < 2; half++) {
173
+ var e = table[(state << 4) | (half === 0 ? byte >> 4 : byte & 15)];
174
+ if (e & _HD_INVALID) throw new Error('Invalid Huffman code');
175
+ if (e & _HD_HAS_SYM) {
176
+ var sym = (e >> 8) & 0x1FF;
177
+ if (strict && sym === HUFF_EOS) {
138
178
  throw new Error('Huffman: EOS symbol in input');
139
179
  }
140
- out.push(node.sym);
141
- node = _huffRoot;
142
- sincePadStart = 0;
143
- padIsOnes = true;
180
+ out[n++] = sym; // EOS (256) stores as 0, as Uint8Array.from did
144
181
  }
182
+ state = e & 0xFF;
145
183
  }
146
184
  }
147
185
 
148
- if (strict && node !== _huffRoot) {
186
+ if (strict && state !== 0) {
149
187
  // Trailing bits that did not complete a symbol are padding. They must be
150
188
  // at most 7 bits and must be the most-significant bits of the EOS code.
151
- if (sincePadStart > 7) throw new Error('Huffman: padding longer than 7 bits');
152
- if (!padIsOnes) throw new Error('Huffman: padding is not the EOS prefix');
189
+ if (_huffDec.depth[state] > 7) throw new Error('Huffman: padding longer than 7 bits');
190
+ if (!_huffDec.ones[state]) throw new Error('Huffman: padding is not the EOS prefix');
153
191
  }
154
192
 
155
- return Uint8Array.from(out);
193
+ // A view, not a copy: every caller turns the bytes into a string at once.
194
+ return out.subarray(0, n);
156
195
  }
157
196
 
158
197
 
@@ -517,4 +556,4 @@ export {
517
556
 
518
557
  // Text
519
558
  bytesToLatin1
520
- };
559
+ };
@@ -458,6 +458,10 @@ function validateFieldSection(list, opts) {
458
458
  // m,p,a,s, Safari m,s,p,a. See h2_fingerprint.js. Collected here because
459
459
  // this loop is the only place that sees the list in its original sequence.
460
460
  var pseudoOrder = [];
461
+ // The fields as they came, one name and value per field line, pseudo-headers
462
+ // first: for rawHeaders, which node:http2 keeps that way (the map combines
463
+ // repeated ones).
464
+ var raw = [];
461
465
  var bad = function (m) { var e = new Error(m); e.malformed = true; return e; };
462
466
 
463
467
  for (var i = 0; i < list.length; i++) {
@@ -477,6 +481,7 @@ function validateFieldSection(list, opts) {
477
481
  if (!pseudo[name]) throw bad('unknown or misdirected pseudo-header ' + name);
478
482
  if (hasOwn.call(headers, name)) throw bad('duplicate pseudo-header ' + name);
479
483
  headers[name] = value;
484
+ raw.push(name, value); // node:http2 keeps them in rawHeaders too
480
485
  continue;
481
486
  }
482
487
  seenRegular = true;
@@ -491,6 +496,7 @@ function validateFieldSection(list, opts) {
491
496
  // Repeat-combining is RFC 9110 §5.3: set-cookie never joins, cookie joins
492
497
  // with "; " (which h2 and h3 rely on: a sender may split it to compress).
493
498
  addHeader(headers, name, value);
499
+ raw.push(name, value);
494
500
  }
495
501
 
496
502
  var protocol = null;
@@ -543,6 +549,7 @@ function validateFieldSection(list, opts) {
543
549
  }
544
550
  if (!isServer && !trailers && !headers[':status']) throw bad('response is missing :status');
545
551
 
552
+ Object.defineProperty(headers, '_rawHeaders', { value: raw, enumerable: false });
546
553
  return { headers: headers, pseudoOrder: pseudoOrder, protocol: protocol };
547
554
  }
548
555