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/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
@@ -17,7 +17,6 @@ import {
17
17
  createSecureContext as _lemonCreateSecureContext,
18
18
  certificateObject
19
19
  } from 'lemon-tls';
20
- import { X509Certificate } from 'node:crypto';
21
20
  import { DEBUG, Emitter } from './utils.js';
22
21
  import { rootCertificates as lemonRoots, parseEcdhCurve as lemonParseEcdhCurve, DEFAULT_SERVER_GROUPS as lemonServerGroups,
23
22
  DEFAULT_SIGNATURE_SCHEMES as lemonSigSchemes } from 'lemon-tls';
@@ -159,6 +158,16 @@ function TLSBridge(options) {
159
158
  var handshakeSecretsEmitted = false;
160
159
  var appSecretsEmitted = false;
161
160
  var helloHandled = false;
161
+ // A handshake refused here, for what QUIC asks of TLS (RFC 9001): the
162
+ // alert closes the connection; nothing more is answered or sent.
163
+ var refused = false;
164
+ function refuse(alert, why) {
165
+ if (refused) return;
166
+ refused = true;
167
+ if (DEBUG) console.log('[tls] refused: ' + why);
168
+ ev.emit('alert', alert);
169
+ ev.emit('error', Object.assign(new Error(why), { code: 'ERR_QUIC_TLS' }));
170
+ }
162
171
  // A client's: what it keeps with each session it saves — the server's
163
172
  // transport parameters (tp), and the application's own (app: HTTP/3's
164
173
  // SETTINGS) once it has them — for 0-RTT next time.
@@ -363,31 +372,44 @@ function TLSBridge(options) {
363
372
  // 'handshakeMessage' with the parsed extensions, so we extract the 0x39 data,
364
373
  // parse it, and hand the params to the QUIC layer (which seeds remote_max_data,
365
374
  // max_ack_delay, ack_delay_exponent, etc. — previously hardcoded defaults).
375
+ //
376
+ // The extension is required (RFC 9001 §8.2): a ClientHello — every one, a
377
+ // second after a HelloRetryRequest included — or EncryptedExtensions
378
+ // without it is refused with missing_extension, before anything is
379
+ // answered. TLS itself has no reason to require it; QUIC does, so here.
366
380
  var peerParamsEmitted = false;
367
381
  session.on('handshakeMessage', function (type, data, message) {
368
- if (peerParamsEmitted || !message || !Array.isArray(message.extensions)) return;
369
- for (var i = 0; i < message.extensions.length; i++) {
370
- var e = message.extensions[i];
371
- if (e && (e.type === 0x39 || e.type === 57) && e.data) {
372
- try {
373
- var parsed = parse_transport_params(e.data, 0);
374
- peerParamsEmitted = true;
375
- if (DEBUG) console.log('[tls] peer transport params parsed (' + e.data.length + 'B)');
376
- // A client keeps, with the sessions this connection saves, the
377
- // server's parameters that 0-RTT must stay within (RFC 9000
378
- // §7.4.1) — given back with the early secret next time.
379
- if (!isServer) keepAppData('tp', rememberedParams(parsed));
380
- ev.emit('peerTransportParams', parsed);
381
- } catch (err) {
382
- if (DEBUG) console.log('[tls] peer transport params parse failed: ' + err.message);
383
- }
384
- break;
385
- }
382
+ var carrier = isServer ? 'client_hello' : 'encrypted_extensions';
383
+ if (type !== carrier || refused) return;
384
+ var ext = null;
385
+ var list = (message && Array.isArray(message.extensions)) ? message.extensions : [];
386
+ for (var i = 0; i < list.length; i++) {
387
+ var e = list[i];
388
+ if (e && (e.type === 0x39 || e.type === 57) && e.data) { ext = e; break; }
389
+ }
390
+ if (!ext) { refuse(109, 'no quic_transport_parameters in the ' + type); return; }
391
+ if (peerParamsEmitted) return;
392
+ var parsed;
393
+ try {
394
+ parsed = parse_transport_params(ext.data, 0);
395
+ } catch (err) {
396
+ // RFC 9000 §7.4: TRANSPORT_PARAMETER_ERROR, not a connection that
397
+ // goes on with the defaults.
398
+ refused = true;
399
+ ev.emit('transportParamsError', 'quic_transport_parameters: ' + err.message);
400
+ return;
386
401
  }
402
+ peerParamsEmitted = true;
403
+ if (DEBUG) console.log('[tls] peer transport params parsed (' + ext.data.length + 'B)');
404
+ // A client keeps, with the sessions this connection saves, the
405
+ // server's parameters that 0-RTT must stay within (RFC 9000
406
+ // §7.4.1) — given back with the early secret next time.
407
+ if (!isServer) keepAppData('tp', rememberedParams(parsed));
408
+ ev.emit('peerTransportParams', parsed);
387
409
  });
388
410
 
389
411
  session.on('hello', function () {
390
- if (helloHandled) return; // prevent double-fire
412
+ if (helloHandled || refused) return; // prevent double-fire
391
413
  helloHandled = true;
392
414
  if (DEBUG) console.log('[tls] hello — configuring TLS');
393
415
 
@@ -435,6 +457,15 @@ function TLSBridge(options) {
435
457
  if (data && data.length >= 2 && data[0] === 2) ev.emit('alert', data[1]);
436
458
  return;
437
459
  }
460
+ // Refused above: nothing more of this handshake goes out.
461
+ if (refused) return;
462
+ // Never sent over QUIC (RFC 9001 §6, §8.3), whatever TLS would do over
463
+ // TCP: a KeyUpdate (QUIC has its own), an EndOfEarlyData. Should TLS
464
+ // make one, the connection ends — internal_error — rather than send it.
465
+ if (type === 'key_update' || type === 'end_of_early_data') {
466
+ refuse(80, 'TLS produced a ' + type + ', which QUIC never sends');
467
+ return;
468
+ }
438
469
  var quicEpoch;
439
470
  if (epoch === 0) quicEpoch = 'initial';
440
471
  else if (epoch === 1) quicEpoch = 'handshake';
@@ -525,7 +556,6 @@ function TLSBridge(options) {
525
556
  on: function (name, fn) { ev.on(name, fn); },
526
557
  off: function (name, fn) { ev.off(name, fn); },
527
558
  feedMessage: feedMessage,
528
- requestKeyUpdate: function () { if (session) session.requestKeyUpdate(true); },
529
559
  getHandshakeSecrets: function () {
530
560
  if (!session) return null;
531
561
  var hs = session.getHandshakeSecrets();
@@ -620,6 +650,10 @@ function TLSBridge(options) {
620
650
  */
621
651
  getJA4: function (opts) { return session && session.getJA4 ? session.getJA4(quicOpts(opts)) : null; },
622
652
 
653
+ /** TLS keying material (RFC 8446 §7.5), as node:tls's exportKeyingMaterial(length, label, context) — or null before the handshake has the secret. */
654
+ exportKeyingMaterial: function (length, label, context) {
655
+ return session && session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
656
+ },
623
657
  /** The peer's certificate chain, leaf first, as DER — or null. */
624
658
  getPeerCertificateChain: function () {
625
659
  var chain = session && session.getPeerCertificate ? session.getPeerCertificate() : null;
@@ -658,27 +692,17 @@ function TLSBridge(options) {
658
692
  * fingerprint, fingerprint256, serialNumber, raw }, or {} when none. On a
659
693
  * server, the one the SNI lookup chose; on a resumed connection, the one
660
694
  * the lookup chose again for the same name.
695
+ *
696
+ * lemon-tls's certificateObject: the one implementation, the one its
697
+ * TLSSocket and getPeerCertificate use. This built it apiece, with
698
+ * subject and issuer as OpenSSL's text ('CN=a\nO=b') where node:tls
699
+ * gives objects ({ CN: 'a', O: 'b' }).
661
700
  */
662
701
  getCertificate: function () {
663
702
  var chain = session && session.getCertificate ? session.getCertificate() : null;
664
703
  if (!chain || chain.length === 0) return {};
665
- try {
666
- var der = chain[0].cert || chain[0];
667
- var x509 = new X509Certificate(Buffer.isBuffer(der) ? der : Buffer.from(der));
668
- return {
669
- subject: x509.subject,
670
- issuer: x509.issuer,
671
- subjectaltname: x509.subjectAltName,
672
- valid_from: x509.validFrom,
673
- valid_to: x509.validTo,
674
- fingerprint: x509.fingerprint,
675
- fingerprint256: x509.fingerprint256,
676
- serialNumber: x509.serialNumber,
677
- raw: der
678
- };
679
- } catch (e) {
680
- return {};
681
- }
704
+ var der = chain[0].cert || chain[0];
705
+ return certificateObject(Buffer.isBuffer(der) ? der : Buffer.from(der)) || {};
682
706
  },
683
707
 
684
708
  close: function () { if (session && session.close) session.close(); }
package/src/transport.js CHANGED
@@ -734,7 +734,9 @@ function parse_quic_frames(buf) {
734
734
  frames.push({ type: 'max_stream_data', id: id.value, max: max.value });
735
735
 
736
736
  } else if (type === 0x12 || type === 0x13) {
737
- var max = safeReadVarInt(); if (!max) break;
737
+ // A count past 2^60 is not one (RFC 9000 §19.11): FRAME_ENCODING_ERROR.
738
+ var at = offset;
739
+ var max = safeReadVarInt(); if (!max || streamCountTooLarge(buf.subarray(at, offset))) break;
738
740
  frames.push({ type: type === 0x12 ? 'max_streams_bidi' : 'max_streams_uni', max: max.value });
739
741
 
740
742
  } else if (type === 0x14) {
@@ -747,7 +749,9 @@ function parse_quic_frames(buf) {
747
749
  frames.push({ type: 'stream_data_blocked', id: id.value, limit: limit.value });
748
750
 
749
751
  } else if (type === 0x16 || type === 0x17) {
750
- var limit = safeReadVarInt(); if (!limit) break;
752
+ // Likewise (§19.14: STREAM_LIMIT_ERROR or FRAME_ENCODING_ERROR).
753
+ var at = offset;
754
+ var limit = safeReadVarInt(); if (!limit || streamCountTooLarge(buf.subarray(at, offset))) break;
751
755
  frames.push({ type: type === 0x16 ? 'streams_blocked_bidi' : 'streams_blocked_uni', limit: limit.value });
752
756
 
753
757
  } else if (type === 0x18) {
package/src/utils.js CHANGED
@@ -410,7 +410,43 @@ function creditAsRead(readable, credit) {
410
410
  return function (n) { received += n; };
411
411
  }
412
412
 
413
+ // ── UDP socket buffers ──
414
+ //
415
+ // The kernel's receive buffer is where datagrams wait for the event loop.
416
+ // The OS default (208 KB on Linux) holds some 180 of them: a burst larger
417
+ // than one turn drains is dropped there, silently — loss to QUIC, so a
418
+ // retransmission and a congestion-window cut on a lossless link. 4 MB by
419
+ // default, for the send buffer too (a full one drops what is sent, the same
420
+ // way). The kernel caps both at net.core.rmem_max / wmem_max.
421
+ // socketBufferSize: bytes; 0 or false — the OS default.
422
+ var DEFAULT_SOCKET_BUFFER_SIZE = 4 << 20;
423
+ function checkedSocketBufferSize(v, what) {
424
+ if (v === undefined || v === null) return DEFAULT_SOCKET_BUFFER_SIZE;
425
+ if (v === 0 || v === false) return 0;
426
+ if (!Number.isInteger(v) || v < 0 || v > 0x7fffffff) {
427
+ var e = new RangeError((what || 'socketBufferSize') + ' must be a positive integer (bytes), 0 or false, got ' + v);
428
+ e.code = 'ERR_OUT_OF_RANGE';
429
+ throw e;
430
+ }
431
+ return v;
432
+ }
433
+ // For dgram.createSocket: applied when the socket is bound.
434
+ function udpSocketOptions(base, size) {
435
+ return size > 0 ? Object.assign({}, base, { recvBufferSize: size, sendBufferSize: size }) : base;
436
+ }
437
+ // For a socket already bound. A platform that refuses the size does not stop
438
+ // the socket from working.
439
+ function setSocketBuffers(sock, size) {
440
+ if (!(size > 0)) return;
441
+ try { sock.setSendBufferSize(size); } catch (e) {}
442
+ try { sock.setRecvBufferSize(size); } catch (e) {}
443
+ }
444
+
413
445
  export {
446
+ DEFAULT_SOCKET_BUFFER_SIZE,
447
+ checkedSocketBufferSize,
448
+ udpSocketOptions,
449
+ setSocketBuffers,
414
450
  createEmitter,
415
451
  mergePriority,
416
452
  creditAsRead,