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/streams.js CHANGED
@@ -59,17 +59,38 @@ class IncomingMessage extends Readable {
59
59
  // QUIC has no TCP socket — stub so libraries don't crash
60
60
  this.socket = options.socket || _makeSocketStub();
61
61
  this.connection = this.socket;
62
+ this.client = this.socket; // node:http's older alias
63
+ // node:http's: true on a request handed to 'upgrade' (an HTTP/1.1
64
+ // Upgrade, an extended CONNECT over HTTP/2 and HTTP/3).
65
+ this.upgrade = false;
62
66
 
63
67
  // H3 stream id (internal)
64
68
  this._streamId = options.streamId != null ? options.streamId : null;
65
69
  this._timeoutTimer = null;
66
70
 
67
- // Build rawHeaders
68
- for (var name in this.headers) {
69
- this.rawHeaders.push(name, String(this.headers[name]));
71
+ // Build rawHeaders: one name and value per field line, as node:http has
72
+ // them — the field lines as they came when the decoder kept them
73
+ // (validateFieldSection's _rawHeaders); otherwise from the map, a list
74
+ // (Set-Cookie) one entry per value. String(list) made two cookies one
75
+ // "a=1,b=2".
76
+ var rawList = this.headers._rawHeaders;
77
+ if (Array.isArray(rawList)) {
78
+ for (var ri = 0; ri < rawList.length; ri++) this.rawHeaders.push(rawList[ri]);
79
+ } else {
80
+ for (var name in this.headers) {
81
+ var hv = this.headers[name];
82
+ if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) this.rawHeaders.push(name, String(hv[hi])); }
83
+ else this.rawHeaders.push(name, String(hv));
84
+ }
70
85
  }
71
86
  }
72
87
 
88
+ // node:http's headersDistinct / trailersDistinct: each name (lower case)
89
+ // with every value it came with, as an array, nothing joined — from the
90
+ // field lines as they arrived (rawHeaders, rawTrailers).
91
+ get headersDistinct() { return distinctFields(this.rawHeaders); }
92
+ get trailersDistinct() { return distinctFields(this.rawTrailers); }
93
+
73
94
  // Readable requires _read — data is pushed externally, so no-op.
74
95
  _read(_size) { /* no-op */ }
75
96
 
@@ -184,6 +205,11 @@ class ServerResponse extends Writable {
184
205
  this.finished = false;
185
206
  this._headers = {};
186
207
  this._headerNames = {}; // lowercase → original-case
208
+ // node:http's: a Date header unless false (RFC 9110 §6.6.1); and, when
209
+ // true, a body that does not match its Content-Length throws.
210
+ this.sendDate = true;
211
+ this.strictContentLength = false;
212
+ this._bodyBytes = 0;
187
213
 
188
214
  // H3 references (set by server when creating the response)
189
215
  this._streamId = options.streamId != null ? options.streamId : null;
@@ -210,6 +236,9 @@ class ServerResponse extends Writable {
210
236
  this.getHeaderNames = this.getHeaderNames.bind(this);
211
237
  this.hasHeader = this.hasHeader.bind(this);
212
238
  this.removeHeader = this.removeHeader.bind(this);
239
+ this.appendHeader = this.appendHeader.bind(this);
240
+ this.setHeaders = this.setHeaders.bind(this);
241
+ this.getRawHeaderNames = this.getRawHeaderNames.bind(this);
213
242
  this.writeHead = this.writeHead.bind(this);
214
243
  this.flushHeaders = this.flushHeaders.bind(this);
215
244
  this._flushHeaders = this._flushHeaders.bind(this);
@@ -270,8 +299,10 @@ class ServerResponse extends Writable {
270
299
  return this._headers[name.toLowerCase()];
271
300
  }
272
301
 
302
+ // An object with no prototype, as node:http's: a header named
303
+ // "constructor" or "__proto__" is a header, not a property it inherits.
273
304
  getHeaders() {
274
- var out = {};
305
+ var out = Object.create(null);
275
306
  for (var k in this._headers) out[k] = this._headers[k];
276
307
  return out;
277
308
  }
@@ -280,6 +311,38 @@ class ServerResponse extends Writable {
280
311
  return Object.keys(this._headers);
281
312
  }
282
313
 
314
+ // node:http's: the names as they were set, in their case.
315
+ getRawHeaderNames() {
316
+ var out = [];
317
+ for (var k in this._headers) out.push(this._headerNames[k] || k);
318
+ return out;
319
+ }
320
+
321
+ // node:http's: a value added to a header's (both kept), not in its place.
322
+ appendHeader(name, value) {
323
+ if (this.headersSent) throw headersSentError('append', this.req && this.req.httpVersion);
324
+ return this._appendHeader(name, value);
325
+ }
326
+
327
+ // node:http's: several at once, from a Headers or a Map — each replacing
328
+ // that name's value (Set-Cookie: every one of a Headers' cookies).
329
+ setHeaders(headers) {
330
+ if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
331
+ var isHeaders = typeof Headers !== 'undefined' && headers instanceof Headers;
332
+ if (!isHeaders && !(headers instanceof Map)) {
333
+ var e = new TypeError('The "headers" argument must be an instance of Headers or Map');
334
+ e.code = 'ERR_INVALID_ARG_TYPE';
335
+ throw e;
336
+ }
337
+ var self = this, cookies = null;
338
+ headers.forEach(function (value, name) {
339
+ if (isHeaders && String(name).toLowerCase() === 'set-cookie') { cookies = headers.getSetCookie(); return; }
340
+ self.setHeader(name, value);
341
+ });
342
+ if (cookies) this.setHeader('set-cookie', cookies);
343
+ return this;
344
+ }
345
+
283
346
  hasHeader(name) {
284
347
  return name.toLowerCase() in this._headers;
285
348
  }
@@ -292,6 +355,16 @@ class ServerResponse extends Writable {
292
355
  return this;
293
356
  }
294
357
 
358
+ // A value added to what a name has (kept as a list), not in its place.
359
+ _appendHeader(name, value) {
360
+ var lower = String(name).toLowerCase();
361
+ if (!(lower in this._headers)) return this.setHeader(name, value);
362
+ validateHeaderName(name);
363
+ validateHeaderValue(name, value);
364
+ this._headers[lower] = [].concat(this._headers[lower], value);
365
+ return this;
366
+ }
367
+
295
368
  /**
296
369
  * node:http's res.addTrailers(headers): fields sent after the body — an
297
370
  * object, or an array of [name, value] pairs. Over HTTP/1.1 they go in the
@@ -325,7 +398,22 @@ class ServerResponse extends Writable {
325
398
  this.statusCode = statusCode;
326
399
  if (statusMessage !== undefined) this.statusMessage = String(statusMessage);
327
400
 
328
- if (typeof headers === 'object' && headers !== null) {
401
+ // node:http's three forms: an object; a flat array [name, value, name,
402
+ // value]; an array of [name, value] pairs. The arrays may repeat a name
403
+ // and every value is kept (two Set-Cookie lines). An array went through
404
+ // for…in: headers named "0", "1", … — over HTTP/3 the response failed.
405
+ if (Array.isArray(headers)) {
406
+ var tuples = headers.length > 0 && Array.isArray(headers[0]);
407
+ if (!tuples && headers.length % 2 !== 0) {
408
+ var bad = new TypeError('The argument \'headers\' is invalid: an array of names and values must have an even length');
409
+ bad.code = 'ERR_INVALID_ARG_VALUE';
410
+ throw bad;
411
+ }
412
+ for (var i = 0; i < headers.length; i += tuples ? 1 : 2) {
413
+ var hn = tuples ? headers[i][0] : headers[i], hv = tuples ? headers[i][1] : headers[i + 1];
414
+ if (hn) this._appendHeader(hn, hv);
415
+ }
416
+ } else if (typeof headers === 'object' && headers !== null) {
329
417
  for (var name in headers) this.setHeader(name, headers[name]);
330
418
  }
331
419
 
@@ -372,12 +460,38 @@ class ServerResponse extends Writable {
372
460
  write(chunk, encoding, callback) {
373
461
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
374
462
  this._wrote = true;
463
+ this._countBody(chunk, encoding, false);
375
464
  return super.write(chunk, encoding, callback);
376
465
  }
377
466
 
467
+ // strictContentLength (node:http's): the body may not go past its
468
+ // Content-Length, nor end short of it — ERR_HTTP_CONTENT_LENGTH_MISMATCH,
469
+ // thrown from the write() or end() that shows it.
470
+ // Counted only once checked: a write that threw sent nothing.
471
+ _countBody(chunk, encoding, ending) {
472
+ var total = this._bodyBytes;
473
+ if (chunk != null && !this.writableEnded) {
474
+ total += typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : (chunk.byteLength || 0);
475
+ }
476
+ var declared = this.strictContentLength ? this.getHeader('content-length') : undefined;
477
+ if (declared !== undefined && !Array.isArray(declared) && !(this.req && this.req.method === 'HEAD') && Number(declared) >= 0) {
478
+ declared = Number(declared);
479
+ if (total > declared || (ending && total !== declared)) {
480
+ var e = new Error('Response body\'s content-length of ' + total + ' byte(s) does not match the content-length of ' + declared + ' byte(s) set in header');
481
+ e.code = 'ERR_HTTP_CONTENT_LENGTH_MISMATCH';
482
+ throw e;
483
+ }
484
+ }
485
+ this._bodyBytes = total;
486
+ }
487
+
378
488
  end(data, encoding, callback) {
379
489
  if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
380
490
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
491
+ if (!this.writableEnded) {
492
+ if (data != null) this._countBody(data, encoding, true);
493
+ else this._countBody(null, null, true);
494
+ }
381
495
  // The whole body, when nothing was written before it and the head has
382
496
  // not gone: its length, for HTTP/1.1 to send as Content-Length rather
383
497
  // than chunk it (node:http's end() does the same). HTTP/2 and HTTP/3
@@ -388,8 +502,22 @@ class ServerResponse extends Writable {
388
502
  : data.byteLength;
389
503
  if (typeof len === 'number') this._endLength = len; // else write() rejects it
390
504
  }
391
- if (data != null) this.write(data, encoding);
392
- return super.end(callback);
505
+ if (data != null) this._writeCounted(data, encoding);
506
+ // The callback runs at 'finish', after the listeners already on it — as
507
+ // node:http's end(cb), a once('finish'). Given to Writable's end() it
508
+ // ran before them.
509
+ if (typeof callback === 'function') {
510
+ if (this.writableFinished) process.nextTick(callback);
511
+ else this.once('finish', callback);
512
+ }
513
+ super.end();
514
+ return this;
515
+ }
516
+
517
+ // The last write, its bytes already counted by end().
518
+ _writeCounted(data, encoding) {
519
+ this._wrote = true;
520
+ return super.write(data, encoding);
393
521
  }
394
522
 
395
523
  writeContinue() { /* HTTP/3 doesn't use 100-continue — stub */ }
@@ -405,6 +533,12 @@ class ServerResponse extends Writable {
405
533
  if (typeof callback === 'function') process.nextTick(callback);
406
534
  }
407
535
 
536
+ /**
537
+ * 102 Processing — node:http's response.writeProcessing(). Replaced by the
538
+ * layer that made the response, as writeEarlyHints is; here, nothing.
539
+ */
540
+ writeProcessing() {}
541
+
408
542
  /**
409
543
  * The response's priority, { urgency, incremental } (RFC 9218). HTTP/2 and
410
544
  * HTTP/3 replace this with the real thing (h2_server, h3_server); over
@@ -593,6 +727,21 @@ class ClientRequest extends Writable {
593
727
  if (Buffer.isBuffer(chunk)) {
594
728
  chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
595
729
  }
730
+ if (this._sink && this._wholeBody) {
731
+ // end(data) with nothing written before it: the body whole, handed to
732
+ // the transport as its end — sent with a content-length, as node:http
733
+ // sends end(data). Written, then ended, a transport already able to
734
+ // send had begun streaming on the write: no length, and a server that
735
+ // goes by the headers (Express's body-parser) saw no body. It happened
736
+ // whenever the connection was ready at once — a pooled one.
737
+ this._wholeBody = false;
738
+ this._sinkEnded = true;
739
+ this._remember(chunk);
740
+ this._sink.end(chunk, this._trailers);
741
+ callback();
742
+ return;
743
+ }
744
+ this._wholeBody = false;
596
745
  if (this._sink) { this._toSink(chunk, callback); return; }
597
746
  // No transport yet (still connecting): held — and the writer told to wait
598
747
  // past the high-water mark, as a socket not yet connected would.
@@ -604,7 +753,7 @@ class ClientRequest extends Writable {
604
753
 
605
754
  _final(callback) {
606
755
  this.finished = true;
607
- if (this._sink) this._sink.end(null, this._trailers);
756
+ if (this._sink && !this._sinkEnded) { this._sinkEnded = true; this._sink.end(null, this._trailers); }
608
757
  callback();
609
758
  }
610
759
 
@@ -647,12 +796,21 @@ class ClientRequest extends Writable {
647
796
  });
648
797
  var held = this._bodyChunks, more = true;
649
798
  this._bodyChunks = []; this._bodyBuffered = 0;
799
+ this._sinkEnded = false;
800
+ if (this.finished) {
801
+ // Ended before a transport took it: the body is known whole — one
802
+ // end(), so it goes with its length rather than streamed.
803
+ for (var j = 0; j < held.length; j++) this._remember(held[j]);
804
+ var whole = held.length === 0 ? null : held.length === 1 ? held[0] : Buffer.concat(held);
805
+ this._sinkEnded = true;
806
+ sink.end(whole, this._trailers);
807
+ return;
808
+ }
650
809
  for (var i = 0; i < held.length; i++) { this._remember(held[i]); more = sink.write(held[i]); }
651
810
  if (this._pendingWriteCb) {
652
811
  var cb = this._pendingWriteCb; this._pendingWriteCb = null;
653
812
  if (more) cb(); else this._sinkWaitCb = cb;
654
813
  }
655
- if (this.finished) sink.end(null, this._trailers);
656
814
  }
657
815
 
658
816
  // The transport failed before the server processed the request: the body
@@ -684,13 +842,18 @@ class ClientRequest extends Writable {
684
842
 
685
843
  write(chunk, encoding, callback) {
686
844
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
845
+ this._wroteBody = true;
687
846
  return super.write(chunk, encoding, callback);
688
847
  }
689
848
 
690
849
  end(data, encoding, callback) {
691
850
  if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
692
851
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
693
- if (data != null) this.write(data, encoding);
852
+ if (data != null) {
853
+ // Nothing written before: this chunk is the body whole (_write).
854
+ this._wholeBody = !this._wroteBody && !this.writableEnded;
855
+ this.write(data, encoding);
856
+ }
694
857
  return super.end(callback);
695
858
  }
696
859
 
@@ -778,6 +941,16 @@ function reparent(obj, Base) {
778
941
  Object.setPrototypeOf(obj, top);
779
942
  }
780
943
 
944
+ // rawHeaders' pairs as node:http's headersDistinct: name → [values].
945
+ function distinctFields(raw) {
946
+ var out = {};
947
+ for (var i = 0; raw && i + 1 < raw.length; i += 2) {
948
+ var n = String(raw[i]).toLowerCase();
949
+ (out[n] || (out[n] = [])).push(String(raw[i + 1]));
950
+ }
951
+ return out;
952
+ }
953
+
781
954
  function _makeSocketStub() {
782
955
  var sock = new Duplex({
783
956
  read(_size) { /* no-op — no data from QUIC socket */ },
@@ -67,6 +67,7 @@ function createTcpListener(options) {
67
67
  var order = []; // registration order = ALPN preference order
68
68
  var sockets = new Set();
69
69
  var closing = false;
70
+ var refed = true;
70
71
 
71
72
  var tls = options.tls || null;
72
73
  var cachedContext = null;
@@ -134,6 +135,7 @@ function createTcpListener(options) {
134
135
  if (stats) stats.protocol(protocol);
135
136
 
136
137
  sockets.add(socket);
138
+ socket._quicoProtocol = protocol;
137
139
  socket.on('close', function () { sockets.delete(socket); });
138
140
  if (handlerOpts[protocol] && handlerOpts[protocol].paused) socket.pause();
139
141
  handler(socket, protocol);
@@ -501,6 +503,7 @@ function createTcpListener(options) {
501
503
  if (order.length === 0) throw new Error('no protocol handlers registered');
502
504
 
503
505
  server = net.createServer({ allowHalfOpen: false }, handleConnection);
506
+ if (!refed) server.unref();
504
507
  server.on('error', function (err) { ev.emit('error', err); });
505
508
  server.listen(port, host, function () {
506
509
  ev.emit('listening');
@@ -521,7 +524,12 @@ function createTcpListener(options) {
521
524
  // — sockets handed in through handleConnection — there is no net.Server
522
525
  // here to close, and returning early (as this did) left every one of
523
526
  // those connections open after shutdown.
527
+ // A protocol that closes its own connections (registered with
528
+ // managesClose: HTTP/1.1 and HTTP/2 finish what is in flight first) is
529
+ // left to it.
524
530
  for (var socket of sockets) {
531
+ var po = handlerOpts[socket._quicoProtocol];
532
+ if (po && po.managesClose) continue;
525
533
  if (!socket._quicoBusy) { try { socket.end(); } catch (e) {} }
526
534
  }
527
535
 
@@ -540,6 +548,9 @@ function createTcpListener(options) {
540
548
  off: function (n, f) { ev.off(n, f); return listener; },
541
549
  get protocols() { return order.slice(); },
542
550
  get connections() { return sockets.size; },
551
+ // node:net's: whether the process may exit while this listens.
552
+ ref: function () { refed = true; if (server && server.ref) server.ref(); return listener; },
553
+ unref: function () { refed = false; if (server && server.unref) server.unref(); return listener; },
543
554
 
544
555
  // The session ticket keys — tls.Server's names. A new key takes effect
545
556
  // for the next connection; tickets sealed with the one before no longer
package/src/tls_bridge.js CHANGED
@@ -620,6 +620,10 @@ function TLSBridge(options) {
620
620
  */
621
621
  getJA4: function (opts) { return session && session.getJA4 ? session.getJA4(quicOpts(opts)) : null; },
622
622
 
623
+ /** TLS keying material (RFC 8446 §7.5), as node:tls's exportKeyingMaterial(length, label, context) — or null before the handshake has the secret. */
624
+ exportKeyingMaterial: function (length, label, context) {
625
+ return session && session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
626
+ },
623
627
  /** The peer's certificate chain, leaf first, as DER — or null. */
624
628
  getPeerCertificateChain: function () {
625
629
  var chain = session && session.getPeerCertificate ? session.getPeerCertificate() : null;