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