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/index.js CHANGED
@@ -28,8 +28,10 @@ import lemonTls from 'lemon-tls';
28
28
 
29
29
  // Encrypted Client Hello keys (RFC 9849) — LemonTLS's; for createServer's `ech`.
30
30
  const generateEchKey = lemonTls.generateEchKey;
31
- import { WebTransport, WebTransportError } from './src/webtransport.js';
31
+ import { WebTransport, WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
32
+ WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream } from './src/webtransport.js';
32
33
  import { WebSocket } from './src/ws_client.js';
34
+ import { WebSocketStream, WebSocketError } from './src/ws_stream.js';
33
35
  import { connectUdp } from './src/connect_udp_client.js';
34
36
  import { WebSocketServer, upgradeWebSocket } from './src/ws_server.js';
35
37
  import { ja4h } from './src/ja4h.js';
@@ -52,7 +54,8 @@ import { request as h3Request } from './src/h3_client.js';
52
54
  const api = {
53
55
  createServer, request, get, Agent, globalAgent,
54
56
  IncomingMessage, ServerResponse, ClientRequest,
55
- connectUdp, createSecureContext, generateEchKey, WebTransport, WebTransportError, WebSocket, WebSocketServer, upgradeWebSocket,
57
+ connectUdp, createSecureContext, generateEchKey, WebTransport, WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
58
+ WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream, WebSocket, WebSocketStream, WebSocketError, WebSocketServer, upgradeWebSocket,
56
59
  ja4h, h2Fingerprint, h3Fingerprint, observeH2, profileRequest,
57
60
  createQuicServer, createQuicClientSocket, createTcpListener,
58
61
  createH1Server, createH2Server, createH3Server,
@@ -62,7 +65,8 @@ const api = {
62
65
  export {
63
66
  createServer, request, get, Agent, globalAgent,
64
67
  IncomingMessage, ServerResponse, ClientRequest,
65
- connectUdp, createSecureContext, generateEchKey, WebTransport, WebTransportError, WebSocket, WebSocketServer, upgradeWebSocket,
68
+ connectUdp, createSecureContext, generateEchKey, WebTransport, WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
69
+ WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream, WebSocket, WebSocketStream, WebSocketError, WebSocketServer, upgradeWebSocket,
66
70
  ja4h, h2Fingerprint, h3Fingerprint, observeH2, profileRequest,
67
71
  createQuicServer, createQuicClientSocket, createTcpListener,
68
72
  createH1Server, createH2Server, createH3Server,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quico",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
5
5
  "type": "module",
6
6
  "main": "index.cjs",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "engines": {
15
- "node": ">=18.0.0"
15
+ "node": ">=24.0.0"
16
16
  },
17
17
  "types": "index.d.ts",
18
18
  "files": [
@@ -37,6 +37,9 @@
37
37
  "http3-client",
38
38
  "h3-node",
39
39
  "webtransport",
40
+ "moqt",
41
+ "masque",
42
+ "connect-udp",
40
43
  "websocket",
41
44
  "datagram",
42
45
  "udp",
@@ -65,7 +68,7 @@
65
68
  "email": "support@quicojs.dev",
66
69
  "url": "https://github.com/colocohen/quico/issues"
67
70
  },
68
- "homepage": "https://github.com/colocohen/quico#readme",
71
+ "homepage": "https://quicojs.dev",
69
72
  "funding": [
70
73
  {
71
74
  "type": "github",
@@ -77,7 +80,8 @@
77
80
  }
78
81
  ],
79
82
  "dependencies": {
83
+ "cryptographic-formats": "^0.1.0",
80
84
  "flat-ranges": "^2.2.0",
81
- "lemon-tls": "^0.9.0"
85
+ "lemon-tls": "^0.11.0"
82
86
  }
83
- }
87
+ }
package/src/client.js CHANGED
@@ -479,6 +479,7 @@ function request(url, options, callback) {
479
479
  startupCap: options.startupCap, pmtuDiscovery: options.pmtuDiscovery,
480
480
  maxUdpPayloadSize: options.maxUdpPayloadSize, initialRtt: options.initialRtt,
481
481
  reorderWindow: options.reorderWindow, initialCongestionWindow: options.initialCongestionWindow,
482
+ socketBufferSize: options.socketBufferSize, // the UDP socket's buffers (utils.js)
482
483
  // 0-RTT with the saved session (below), off unless asked for.
483
484
  earlyData: options.earlyData === true
484
485
  };
@@ -53,7 +53,7 @@ import { EventEmitter } from 'node:events';
53
53
  import dgram from 'node:dgram';
54
54
  import dns from 'node:dns';
55
55
  import net from 'node:net';
56
- import { readVarInt, writeVarInt } from './utils.js';
56
+ import { readVarInt, writeVarInt, checkedSocketBufferSize, udpSocketOptions } from './utils.js';
57
57
 
58
58
  var DEFAULT_TEMPLATE = '/.well-known/masque/udp/{target_host}/{target_port}/';
59
59
  var MIN_IDLE_MS = 120000; // §3.1: "SHOULD NOT use a period lower than two minutes"
@@ -369,6 +369,9 @@ function createConnectUdp(options) {
369
369
  }
370
370
  var headers = opts.headers || null;
371
371
  if (opts.relay === false) { this._open(headers); return this; }
372
+ // The socket to the target, when this tunnel makes it (no opts.socket):
373
+ // its buffers (utils.js), 4 MB by default; socketBufferSize 0, the OS's.
374
+ var bufferSize = checkedSocketBufferSize(opts.socketBufferSize, 'accept({ socketBufferSize })');
372
375
 
373
376
  var allowPrivate = opts.allowPrivate != null ? !!opts.allowPrivate : allowPrivateDefault;
374
377
  var host = this.target.host;
@@ -390,7 +393,7 @@ function createConnectUdp(options) {
390
393
  if (opts.socket) self._useSocket(opts.socket, false, pick.address, headers);
391
394
  else {
392
395
  var sock;
393
- try { sock = dgram.createSocket(family === 6 ? 'udp6' : 'udp4'); }
396
+ try { sock = dgram.createSocket(udpSocketOptions({ type: family === 6 ? 'udp6' : 'udp4' }, bufferSize)); }
394
397
  catch (e2) { self._refuse(502, proxyStatus('proxy_internal_error'), e2); return; }
395
398
  sock.once('error', function (e3) {
396
399
  if (self.state === 'pending') self._refuse(502, proxyStatus('destination_ip_unroutable', e3.code), e3);
@@ -395,6 +395,7 @@ function ProxyH3Connection(key, proxy, o, ech) {
395
395
  if (self.state !== 'connecting') return;
396
396
  self.udp = createQuicClientSocket({
397
397
  remoteIp: ip, remotePort: proxy.port, hostname: o.servername || proxy.hostname,
398
+ socketBufferSize: o.socketBufferSize,
398
399
  rejectUnauthorized: o.rejectUnauthorized !== false, ca: o.ca || null,
399
400
  key: o.key || null, cert: o.cert || null, passphrase: o.passphrase,
400
401
  quicVersions: o.quicVersions,
@@ -407,6 +408,15 @@ function ProxyH3Connection(key, proxy, o, ech) {
407
408
  // Each ticket the proxy sends: the agent's, and each tunnel's 'session'.
408
409
  onSocket: function (q) {
409
410
  self.quic = q;
411
+ // The connection is over the moment either side's CONNECTION_CLOSE is
412
+ // known ('closing'), not when its draining period (RFC 9000 §10.2,
413
+ // some 3 s) ends and 'close' comes: each tunnel on it is told at
414
+ // once, and the connection leaves the pool at once — a tunnel opened
415
+ // meanwhile joined a connection already closed, and failed. As the
416
+ // WebTransport client hears it (r69). Only once open: a handshake
417
+ // that ends is onError's to report — an ECH rejection is retried
418
+ // there with the server's new keys, not failed.
419
+ q.on('closing', function () { if (self.quic === q && self.state === 'open') self._gone(); });
410
420
  q.on('session', function (s) {
411
421
  self.sessions.keep(s);
412
422
  self.tunnels.forEach(function (t) { if (t.session) t.session(s); });
package/src/crypto.js CHANGED
@@ -607,6 +607,12 @@ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn,
607
607
  return {
608
608
  packet_number: packetNumber,
609
609
  key_phase: keyPhase,
610
+ // The first byte with header protection removed: its reserved bits are
611
+ // the caller's to check — only once the AEAD has passed the packet
612
+ // (RFC 9000 §17.2, §17.3.1), since before that anyone could set them.
613
+ first_byte: array[0],
614
+ // null: the AEAD refused the packet. An empty Uint8Array: it passed, and
615
+ // there was nothing in it — a different thing (RFC 9000 §12.4).
610
616
  plaintext: plaintext
611
617
  };
612
618
  }
package/src/h1_server.js CHANGED
@@ -155,6 +155,8 @@ function createServer(options, handler) {
155
155
 
156
156
  socket.on('data', function (chunk) {
157
157
  if (destroyed) return;
158
+ // A request is arriving: not idle (closeIdleConnections, close()).
159
+ socket._quicoBusy = true;
158
160
 
159
161
  try {
160
162
  parser.execute(chunk);
@@ -231,6 +233,7 @@ function createServer(options, handler) {
231
233
  });
232
234
 
233
235
  parser.on('headers', function (info) {
236
+ socket._quicoBusy = true;
234
237
  clearTimer();
235
238
  setTimer(requestTimeout, 'request');
236
239
  served++;
@@ -359,6 +362,7 @@ function createServer(options, handler) {
359
362
  socket: socket
360
363
  });
361
364
  req.rawHeaders = info.rawHeaders;
365
+ req.upgrade = true;
362
366
  req._endData();
363
367
 
364
368
  // Hand the raw socket over, exactly as node:http does. `head` is the
@@ -452,7 +456,7 @@ function createServer(options, handler) {
452
456
  willClose = !keepAlive;
453
457
 
454
458
  if (keepAlive && info.httpVersion === '1.0') headers['keep-alive'] = 'timeout=5';
455
- if (!('date' in headers)) headers['date'] = httpDate();
459
+ if (!('date' in headers) && res.sendDate !== false) headers['date'] = httpDate();
456
460
  // A function when the server was built before its port was known —
457
461
  // the unified server builds its layers at creation and learns the
458
462
  // port from listen(), or from options.port when fed from outside.
@@ -484,6 +488,13 @@ function createServer(options, handler) {
484
488
  if (typeof callback === 'function') process.nextTick(callback);
485
489
  };
486
490
 
491
+ // 102 Processing (node:http's writeProcessing): an interim response,
492
+ // as Early Hints are.
493
+ res.writeProcessing = function () {
494
+ if (res.headersSent || info.httpVersion === '1.0' || destroyed) return;
495
+ try { socket.write(buildResponseHead(102, null, {})); } catch (e) {}
496
+ };
497
+
487
498
  res._write = function (chunk, encoding, callback) {
488
499
  if (!res.headersSent) res._flushHeaders();
489
500
  if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
@@ -526,6 +537,7 @@ function createServer(options, handler) {
526
537
  function onResponseComplete() {
527
538
  clearTimer();
528
539
  current = null;
540
+ socket._quicoBusy = false;
529
541
  // Whatever of the body is still to come is dropped from here on (the
530
542
  // parser reads it, to find the next request): the reader no longer
531
543
  // holds the socket back.
@@ -573,7 +585,7 @@ function createServer(options, handler) {
573
585
  ca: options.ca,
574
586
  SNICallback: options.SNICallback
575
587
  });
576
- tcpServer.register('http/1.1', handleConnection);
588
+ tcpServer.register('http/1.1', handleConnection, { managesClose: true });
577
589
  tcpServer.on('error', function (err) { ev.emit('error', err); });
578
590
  tcpServer.on('clientError', function (err, sock) { ev.emit('clientError', err, sock); });
579
591
  tcpServer.on('tlsClientError', function (err, sock) { ev.emit('tlsClientError', err, sock); });
@@ -584,17 +596,42 @@ function createServer(options, handler) {
584
596
  return server;
585
597
  }
586
598
 
587
- function close(callback) {
599
+ // ---- Closing, as node:http ----
600
+ //
601
+ // Idle: a connection between requests (or before its first) — no request
602
+ // arriving, no response being written. Closing one costs nothing.
603
+ function closeIdleConnections() {
604
+ for (var s of connections) if (!s._quicoBusy) { try { s.destroy(); } catch (e) {} }
605
+ }
606
+ // All, mid-request too. Not the sockets handed to 'upgrade' (a WebSocket):
607
+ // those left this server at the upgrade, as in node:http.
608
+ function closeAllConnections() {
609
+ for (var s of connections) { try { s.destroy(); } catch (e) {} }
610
+ }
611
+ // No new connections, the idle ones closed; each busy one closes as its
612
+ // response completes (onResponseComplete). It used to end every socket at
613
+ // once: a response being written was cut off.
614
+ function beginClose() {
588
615
  closing = true;
589
- if (!tcpServer) {
590
- // Sockets we were handed rather than accepted: still ours to close.
591
- for (var s of connections) { try { s.end(); } catch (e) {} }
592
- if (callback) callback();
593
- return;
594
- }
616
+ closeIdleConnections();
617
+ }
618
+
619
+ function close(callback) {
620
+ beginClose();
621
+ if (!tcpServer) { if (callback) callback(); return; }
595
622
  tcpServer.close(callback);
596
623
  }
597
624
 
625
+ // node:http's timeouts and limits, changed while the server runs: from
626
+ // the next request (or connection) on.
627
+ function setLimits(o) {
628
+ if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
629
+ if (o.keepAliveTimeout !== undefined) keepAliveTimeout = o.keepAliveTimeout;
630
+ if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
631
+ if (o.maxHeaderCount !== undefined) maxHeaderCount = o.maxHeaderCount;
632
+ if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
633
+ }
634
+
598
635
  function address() {
599
636
  return tcpServer && tcpServer.address ? tcpServer.address() : null;
600
637
  }
@@ -610,6 +647,11 @@ function createServer(options, handler) {
610
647
  emit: function () { ev.emit.apply(ev, arguments); return server; },
611
648
  listenerCount: function (n) { return ev.listenerCount(n); },
612
649
  setTimeout: function (ms) { headersTimeout = ms; return server; },
650
+ closeIdleConnections: closeIdleConnections,
651
+ closeAllConnections: closeAllConnections,
652
+ beginClose: beginClose,
653
+ setLimits: setLimits,
654
+ get connectionCount() { return connections.size; },
613
655
 
614
656
  /** Attach an already-connected Duplex, bypassing the listener. Lets the
615
657
  * unified server share one TCP port, and makes the whole stack testable
package/src/h2_server.js CHANGED
@@ -255,6 +255,20 @@ function createServer(options, handler) {
255
255
  });
256
256
 
257
257
  connections.add(socket);
258
+ socket._quicoH2 = {
259
+ idle: function () { return h2.activeStreams === 0; },
260
+ drain: function () {
261
+ if (destroyed || draining) return;
262
+ draining = true;
263
+ try { h2.sendGoaway(F.ERROR.NO_ERROR); } catch (e) {}
264
+ finishIfDone();
265
+ },
266
+ destroy: function () {
267
+ if (destroyed) return;
268
+ try { h2.sendGoaway(F.ERROR.NO_ERROR); } catch (e) {}
269
+ destroy(null, true);
270
+ }
271
+ };
258
272
 
259
273
  // `linger`: end the socket rather than destroy it — what was written last
260
274
  // (the GOAWAY saying why) leaves first — and destroy it if the peer has
@@ -340,8 +354,12 @@ function createServer(options, handler) {
340
354
  // only (HTTP/1.1 ends its socket with the client's, as node:http does).
341
355
  socket.allowHalfOpen = true;
342
356
  var peerEnded = false;
357
+ // Closing (server.close(), closeIdleConnections): GOAWAY sent — no new
358
+ // streams — and the connection ends with its last stream, as when the
359
+ // peer half-closes.
360
+ var draining = false;
343
361
  function finishIfDone() {
344
- if (destroyed || !peerEnded || h2.activeStreams > 0) return;
362
+ if (destroyed || !(peerEnded || draining) || h2.activeStreams > 0) return;
345
363
  try { h2.sendGoaway(F.ERROR.NO_ERROR); } catch (e) {}
346
364
  destroy(null, true);
347
365
  }
@@ -375,7 +393,7 @@ function createServer(options, handler) {
375
393
  // a long-lived connection kept every WebSocket it ever carried).
376
394
  var st = streams[id];
377
395
  if (st && st.duplex) delete streams[id];
378
- if (peerEnded) process.nextTick(finishIfDone);
396
+ if (peerEnded || draining) process.nextTick(finishIfDone);
379
397
  });
380
398
  // A response that waits for the client to open a window, from a client
381
399
  // that has half-closed: it sends nothing more — no WINDOW_UPDATE either —
@@ -435,6 +453,7 @@ function createServer(options, handler) {
435
453
  delete streams[streamId];
436
454
  return;
437
455
  }
456
+ creq.upgrade = true;
438
457
  ev.emit('upgrade', creq, stream, Buffer.alloc(0));
439
458
  return;
440
459
  }
@@ -686,7 +705,7 @@ function createServer(options, handler) {
686
705
 
687
706
  bodyAllowed = !statusHasNoBody(code) && req.method !== 'HEAD';
688
707
 
689
- if (!('date' in headers)) headers['date'] = httpDate();
708
+ if (!('date' in headers) && res.sendDate !== false) headers['date'] = httpDate();
690
709
  // A function when the server was built before its port was known —
691
710
  // the unified server builds its layers at creation and learns the
692
711
  // port from listen(), or from options.port when fed from outside.
@@ -767,6 +786,12 @@ function createServer(options, handler) {
767
786
  if (typeof callback === 'function') process.nextTick(callback);
768
787
  };
769
788
 
789
+ // 102 Processing (node:http's writeProcessing), as Early Hints go.
790
+ res.writeProcessing = function () {
791
+ if (res.headersSent || res._aborted || destroyed) return;
792
+ try { h2.sendHeaders(streamId, buildResponsePseudoHeaders(102, {}), false); } catch (e) {}
793
+ };
794
+
770
795
  /**
771
796
  * The response's priority, as the server decides it: { urgency (0..7,
772
797
  * 0 first), incremental } — RFC 9218, as h3_server's. It replaces what
@@ -821,7 +846,7 @@ function createServer(options, handler) {
821
846
  ca: options.ca,
822
847
  SNICallback: options.SNICallback
823
848
  });
824
- listener.register('h2', handleConnection);
849
+ listener.register('h2', handleConnection, { managesClose: true });
825
850
  listener.on('error', function (err) { ev.emit('error', err); });
826
851
  listener.on('clientError', function (err, sock) { ev.emit('clientError', err, sock); });
827
852
  listener.on('tlsClientError', function (err, sock) { ev.emit('tlsClientError', err, sock); });
@@ -832,16 +857,35 @@ function createServer(options, handler) {
832
857
  return server;
833
858
  }
834
859
 
835
- function close(callback) {
860
+ // ---- Closing, as node:http (see h1_server) ----
861
+ // Idle: no stream open. Closed with GOAWAY first, so the client knows
862
+ // nothing it sent was lost.
863
+ function closeIdleConnections() {
864
+ for (var s of connections) if (s._quicoH2 && s._quicoH2.idle()) s._quicoH2.drain();
865
+ }
866
+ function closeAllConnections() {
867
+ for (var s of connections) if (s._quicoH2) s._quicoH2.destroy();
868
+ }
869
+ // GOAWAY on every connection: those idle end now, the others when their
870
+ // last stream does. It used to end every socket at once — responses in
871
+ // progress were cut off.
872
+ function beginClose() {
836
873
  closing = true;
837
- if (!listener) {
838
- for (var s of connections) { try { s.end(); } catch (e) {} }
839
- if (callback) callback();
840
- return;
841
- }
874
+ for (var s of connections) if (s._quicoH2) s._quicoH2.drain();
875
+ }
876
+
877
+ function close(callback) {
878
+ beginClose();
879
+ if (!listener) { if (callback) callback(); return; }
842
880
  listener.close(callback);
843
881
  }
844
882
 
883
+ function setLimits(o) {
884
+ if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
885
+ if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
886
+ if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
887
+ }
888
+
845
889
 
846
890
  var server = {
847
891
  listen: listen,
@@ -852,7 +896,12 @@ function createServer(options, handler) {
852
896
  once: function (n, f) { ev.once(n, f); return server; },
853
897
  off: function (n, f) { ev.off(n, f); return server; },
854
898
  emit: function () { ev.emit.apply(ev, arguments); return server; },
855
- listenerCount: function (n) { return ev.listenerCount(n); }
899
+ listenerCount: function (n) { return ev.listenerCount(n); },
900
+ closeIdleConnections: closeIdleConnections,
901
+ closeAllConnections: closeAllConnections,
902
+ beginClose: beginClose,
903
+ setLimits: setLimits,
904
+ get connectionCount() { return connections.size; }
856
905
  };
857
906
 
858
907
  return server;