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/README.md +245 -31
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +7 -4
- package/src/connect_udp_client.js +9 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +29 -60
- package/src/h3_client.js +24 -5
- package/src/h3_server.js +101 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/quic_connection.js +200 -23
- package/src/quic_server.js +94 -13
- package/src/server.js +266 -22
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +4 -0
- package/src/webtransport.js +424 -54
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
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
|
|
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,
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.8.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": ">=
|
|
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://
|
|
71
|
+
"homepage": "https://quicojs.dev",
|
|
69
72
|
"funding": [
|
|
70
73
|
{
|
|
71
74
|
"type": "github",
|
|
@@ -78,6 +81,6 @@
|
|
|
78
81
|
],
|
|
79
82
|
"dependencies": {
|
|
80
83
|
"flat-ranges": "^2.2.0",
|
|
81
|
-
"lemon-tls": "^0.
|
|
84
|
+
"lemon-tls": "^0.10.0"
|
|
82
85
|
}
|
|
83
86
|
}
|
|
@@ -407,6 +407,15 @@ function ProxyH3Connection(key, proxy, o, ech) {
|
|
|
407
407
|
// Each ticket the proxy sends: the agent's, and each tunnel's 'session'.
|
|
408
408
|
onSocket: function (q) {
|
|
409
409
|
self.quic = q;
|
|
410
|
+
// The connection is over the moment either side's CONNECTION_CLOSE is
|
|
411
|
+
// known ('closing'), not when its draining period (RFC 9000 §10.2,
|
|
412
|
+
// some 3 s) ends and 'close' comes: each tunnel on it is told at
|
|
413
|
+
// once, and the connection leaves the pool at once — a tunnel opened
|
|
414
|
+
// meanwhile joined a connection already closed, and failed. As the
|
|
415
|
+
// WebTransport client hears it (r69). Only once open: a handshake
|
|
416
|
+
// that ends is onError's to report — an ECH rejection is retried
|
|
417
|
+
// there with the server's new keys, not failed.
|
|
418
|
+
q.on('closing', function () { if (self.quic === q && self.state === 'open') self._gone(); });
|
|
410
419
|
q.on('session', function (s) {
|
|
411
420
|
self.sessions.keep(s);
|
|
412
421
|
self.tunnels.forEach(function (t) { if (t.session) t.session(s); });
|
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
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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 (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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;
|
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
|
|
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
|
|
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
|
// ============================================================
|
|
@@ -369,24 +339,14 @@ function parseSettingsPayload(buf) {
|
|
|
369
339
|
// QPACK Encoder Instructions (from peer's encoder stream)
|
|
370
340
|
// ============================================================
|
|
371
341
|
|
|
372
|
-
|
|
373
|
-
|
|
342
|
+
// Parse the encoder-stream instructions in `combined` — the stream's unparsed
|
|
343
|
+
// bytes, from its read position. Returns the complete instructions, how many
|
|
344
|
+
// bytes they took (consumed), and, when the last one is cut short, how many
|
|
345
|
+
// bytes it needs (need). It used to take the stream's chunks and an offset
|
|
346
|
+
// and join them itself; the one caller already hands it a single buffer.
|
|
347
|
+
function parseQpackEncoderInstructions(combined, maxLiteral) {
|
|
348
|
+
if (!combined || combined.length === 0) return { instructions: [], consumed: 0, need: 0 };
|
|
374
349
|
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
350
|
var pos = 0;
|
|
391
351
|
var instructions = [];
|
|
392
352
|
// When the last instruction is cut short: how many bytes, from its start,
|
|
@@ -457,7 +417,7 @@ function parseQpackEncoderInstructions(chunks, fromOffset, maxLiteral) {
|
|
|
457
417
|
}
|
|
458
418
|
}
|
|
459
419
|
|
|
460
|
-
return { instructions: instructions,
|
|
420
|
+
return { instructions: instructions, consumed: pos,
|
|
461
421
|
need: needAbs > pos ? needAbs - pos : 0 };
|
|
462
422
|
}
|
|
463
423
|
|
|
@@ -1700,7 +1660,7 @@ function H3Connection(options) {
|
|
|
1700
1660
|
// Only the unparsed bytes are copied (once per completed instruction,
|
|
1701
1661
|
// given `need`), and no literal larger than our table can hold is
|
|
1702
1662
|
// waited for.
|
|
1703
|
-
ext = parseQpackEncoderInstructions(
|
|
1663
|
+
ext = parseQpackEncoderInstructions(copyBytes(s, s.buffered),
|
|
1704
1664
|
context.local_qpack_max_table_capacity);
|
|
1705
1665
|
} catch (err) {
|
|
1706
1666
|
// RFC 9204 §4.2: a malformed encoder stream is QPACK_ENCODER_STREAM_ERROR,
|
|
@@ -1708,7 +1668,7 @@ function H3Connection(options) {
|
|
|
1708
1668
|
err.h3ErrorCode = H3_ERR.QPACK_ENCODER_STREAM_ERROR;
|
|
1709
1669
|
throw err;
|
|
1710
1670
|
}
|
|
1711
|
-
consumeBytes(s, ext.
|
|
1671
|
+
consumeBytes(s, ext.consumed);
|
|
1712
1672
|
s.need = ext.need;
|
|
1713
1673
|
if (ext.instructions.length === 0) return;
|
|
1714
1674
|
|
|
@@ -2000,6 +1960,11 @@ function H3Connection(options) {
|
|
|
2000
1960
|
if (avail < headLen + len.value) break;
|
|
2001
1961
|
// Waits for the encoder stream when it must (see sectionBlocked): the
|
|
2002
1962
|
// frame stays in the buffer, read again once unblocked.
|
|
1963
|
+
// Already waiting, and the inserts it needs are not here yet: nothing
|
|
1964
|
+
// to do. The frame was copied and its prefix decoded again on every
|
|
1965
|
+
// packet that arrived on the stream meanwhile.
|
|
1966
|
+
if (_blocked.has(Number(streamId)) &&
|
|
1967
|
+
_blocked.get(Number(streamId)) > context.remote_qpack_table_base_index) return;
|
|
2003
1968
|
var whole = copyBytes(s, headLen + len.value);
|
|
2004
1969
|
var payload = whole.subarray(headLen);
|
|
2005
1970
|
if (sectionBlocked(Number(streamId), payload)) return;
|
|
@@ -2138,7 +2103,11 @@ function H3Connection(options) {
|
|
|
2138
2103
|
var list = [];
|
|
2139
2104
|
for (var hname in headers) {
|
|
2140
2105
|
if (Object.prototype.hasOwnProperty.call(headers, hname)) {
|
|
2141
|
-
list
|
|
2106
|
+
// A list: one field line per value (Set-Cookie above all, which may
|
|
2107
|
+
// not be combined), not String(list).
|
|
2108
|
+
var hv = headers[hname];
|
|
2109
|
+
if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) list.push([hname, String(hv[hi])]); }
|
|
2110
|
+
else list.push([hname, hv]);
|
|
2142
2111
|
}
|
|
2143
2112
|
}
|
|
2144
2113
|
var enc = qpackEncodeHeaderBlock(list, encoderTable, {
|
package/src/h3_client.js
CHANGED
|
@@ -476,7 +476,11 @@ function request(options, callback) {
|
|
|
476
476
|
return e;
|
|
477
477
|
});
|
|
478
478
|
});
|
|
479
|
-
|
|
479
|
+
// On 'closing' — the moment the connection is over (a CONNECTION_CLOSE
|
|
480
|
+
// either way, the idle timeout) — not only on 'close', which waits out
|
|
481
|
+
// the draining period (RFC 9000 §10.2, ~3 s): a response the server cut
|
|
482
|
+
// off hung that long before it failed.
|
|
483
|
+
function connectionOver() {
|
|
480
484
|
failAll(0, function (sid) {
|
|
481
485
|
var e = new Error('HTTP/3 connection closed before the response ended');
|
|
482
486
|
e.code = 'ERR_HTTP3_CONNECTION_CLOSED';
|
|
@@ -484,7 +488,9 @@ function request(options, callback) {
|
|
|
484
488
|
e.retryable = sid >= conn._goawayId;
|
|
485
489
|
return e;
|
|
486
490
|
});
|
|
487
|
-
}
|
|
491
|
+
}
|
|
492
|
+
conn.quic.on('closing', connectionOver);
|
|
493
|
+
conn.quic.on('close', connectionOver);
|
|
488
494
|
}
|
|
489
495
|
|
|
490
496
|
|
|
@@ -662,17 +668,28 @@ function request(options, callback) {
|
|
|
662
668
|
else if (reqBody.length) startStreaming();
|
|
663
669
|
}
|
|
664
670
|
|
|
665
|
-
|
|
671
|
+
// contentLength: the body's size when it is known whole (ended before it
|
|
672
|
+
// went) — sent as h2_client and node:https send it, and as browsers do. A
|
|
673
|
+
// server that decides whether there is a body from the headers (Express's
|
|
674
|
+
// body-parser: content-length or transfer-encoding) saw none over HTTP/3,
|
|
675
|
+
// and req.body came out empty.
|
|
676
|
+
function requestHeaders(contentLength) {
|
|
666
677
|
var h3Headers = {
|
|
667
678
|
':method': method,
|
|
668
679
|
':scheme': 'https',
|
|
669
680
|
':authority': hostname + (port !== 443 ? ':' + port : ''),
|
|
670
681
|
':path': path
|
|
671
682
|
};
|
|
672
|
-
|
|
683
|
+
// A repeated field (an array) stays one line per value: String(list)
|
|
684
|
+
// made two cookies "a=1,b=2", which the server reads as one cookie.
|
|
685
|
+
for (var name in headers) {
|
|
686
|
+
var v = headers[name];
|
|
687
|
+
h3Headers[name.toLowerCase()] = Array.isArray(v) ? v.map(String) : String(v);
|
|
688
|
+
}
|
|
673
689
|
if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
|
|
674
690
|
if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
|
|
675
691
|
if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
|
|
692
|
+
if (contentLength != null && h3Headers['content-length'] === undefined) h3Headers['content-length'] = String(contentLength);
|
|
676
693
|
return h3Headers;
|
|
677
694
|
}
|
|
678
695
|
|
|
@@ -687,7 +704,9 @@ function request(options, callback) {
|
|
|
687
704
|
// Early: kept, to go again if the server refuses it.
|
|
688
705
|
if (earlyTried && !handshaken) earlyBody = { body: body.slice(), trailers: reqTrailers };
|
|
689
706
|
if (body.length === 0 && !reqTrailers) { h3.sendHeaders(streamId, requestHeaders(), true); return; }
|
|
690
|
-
|
|
707
|
+
var bodyLength = 0;
|
|
708
|
+
for (var bi = 0; bi < body.length; bi++) bodyLength += body[bi].byteLength;
|
|
709
|
+
h3.sendHeaders(streamId, requestHeaders(body.length ? bodyLength : null), false);
|
|
691
710
|
for (var i = 0; i < body.length; i++) h3.sendBody(streamId, body[i], i === body.length - 1 && !reqTrailers);
|
|
692
711
|
if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
|
|
693
712
|
}
|