quico 0.2.0 → 0.4.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 +854 -125
- package/index.js +8 -2
- package/package.json +2 -2
- package/src/agent.js +67 -1
- package/src/client.js +40 -6
- package/src/crypto.js +37 -5
- package/src/h3.js +52 -43
- package/src/h3_client.js +103 -79
- package/src/h3_server.js +52 -183
- package/src/quic_connection.js +1397 -123
- package/src/quic_server.js +372 -0
- package/src/quic_socket.js +127 -0
- package/src/server.js +30 -22
- package/src/streams.js +15 -1
- package/src/tls_bridge.js +116 -14
- package/src/transport.js +143 -34
- package/src/utils.js +59 -4
- package/src/webtransport.js +25 -78
package/src/tls_bridge.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { TLSSession, createSecureContext as _lemonCreateSecureContext } from 'lemon-tls';
|
|
16
16
|
import nodeTls from 'node:tls';
|
|
17
|
-
import { Emitter } from './utils.js';
|
|
18
|
-
import { build_transport_params } from './transport.js';
|
|
17
|
+
import { DEBUG, Emitter } from './utils.js';
|
|
18
|
+
import { build_transport_params, parse_transport_params } from './transport.js';
|
|
19
19
|
import {
|
|
20
20
|
TLS_CIPHER_SUITES
|
|
21
21
|
} from './crypto.js';
|
|
@@ -52,10 +52,28 @@ function TLSBridge(options) {
|
|
|
52
52
|
var originalDcid = options.originalDcid || new Uint8Array(0);
|
|
53
53
|
var localCid = options.localCid || new Uint8Array(0); // client's own SCID
|
|
54
54
|
var hostname = options.hostname || null;
|
|
55
|
+
// Peer certificate verification. Defaults to true, matching node:tls — but
|
|
56
|
+
// until now the option was never plumbed through at all, so quico could not
|
|
57
|
+
// talk to a server whose certificate wasn't signed by a publicly trusted CA:
|
|
58
|
+
// no local development against self-signed certs, no internal PKI, and no
|
|
59
|
+
// interop (the QUIC interop runner mints its own CA and addresses hosts as
|
|
60
|
+
// server4 / server6, so every implementation disables verification there).
|
|
61
|
+
var rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
62
|
+
var ca = options.ca || null;
|
|
63
|
+
// ALPN protocol(s) to advertise. Default ['h3']; normalize string → array.
|
|
64
|
+
var supportedAlpns = (function () {
|
|
65
|
+
var a = options.alpn || ['h3'];
|
|
66
|
+
return Array.isArray(a) ? a : [a];
|
|
67
|
+
})();
|
|
55
68
|
|
|
56
69
|
function feedMessage(data) {
|
|
57
70
|
if (!session) {
|
|
58
|
-
var sessionOpts = {
|
|
71
|
+
var sessionOpts = {
|
|
72
|
+
isServer: isServer,
|
|
73
|
+
SNICallback: options.SNICallback,
|
|
74
|
+
rejectUnauthorized: rejectUnauthorized
|
|
75
|
+
};
|
|
76
|
+
if (ca) sessionOpts.ca = ca;
|
|
59
77
|
if (!isServer) {
|
|
60
78
|
sessionOpts.sessionId = new Uint8Array(0); // QUIC: no compatibility mode
|
|
61
79
|
if (hostname) sessionOpts.servername = hostname;
|
|
@@ -71,24 +89,76 @@ function TLSBridge(options) {
|
|
|
71
89
|
|
|
72
90
|
session.set_context({
|
|
73
91
|
local_supported_versions: [0x0304],
|
|
74
|
-
local_supported_alpns:
|
|
92
|
+
local_supported_alpns: supportedAlpns,
|
|
75
93
|
local_supported_groups: [29, 23, 24],
|
|
76
94
|
local_supported_cipher_suites: [0x1301, 0x1302],
|
|
77
95
|
local_extensions: [{ type: 0x39, data: quicParams }],
|
|
78
|
-
|
|
96
|
+
// Signature schemes offered to the peer (client: in the ClientHello;
|
|
97
|
+
// server: in CertificateRequest). Full modern list, ordered PSS/ECDSA/
|
|
98
|
+
// EdDSA first, legacy pkcs1 last (TLS 1.2 compat only — TLS 1.3 never
|
|
99
|
+
// signs pkcs1). Two bugs lived in the old pkcs1-only list:
|
|
100
|
+
// 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
|
|
101
|
+
// outright (alert 40 handshake_failure — seen live against
|
|
102
|
+
// speed.cloudflare.com; claude.ai worked only because that zone
|
|
103
|
+
// also carries an RSA cert).
|
|
104
|
+
// 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
|
|
105
|
+
// §4.1.2 requires CH2 to match CH1 exactly — offering a different
|
|
106
|
+
// list in CH1 made every HRR flow non-compliant.
|
|
107
|
+
local_supported_signature_algorithms: [
|
|
108
|
+
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
109
|
+
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
|
110
|
+
0x0807, 0x0808, // ed25519, ed448
|
|
111
|
+
0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
|
|
112
|
+
],
|
|
79
113
|
});
|
|
80
114
|
helloHandled = true; // ClientHello will be generated from this set_context
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
|
-
|
|
117
|
+
// Only feed real handshake bytes. QUICConnection.connect() calls
|
|
118
|
+
// feedMessage(new Uint8Array(0)) as a "trigger" to start the handshake,
|
|
119
|
+
// but the ClientHello is actually built by lemon-tls from set_context()
|
|
120
|
+
// above, on its own timer — the empty feed was always a no-op in intent.
|
|
121
|
+
//
|
|
122
|
+
// It stopped being harmless once lemon-tls hardened parse failures into
|
|
123
|
+
// fatalAlert(): an empty buffer fails as "truncated u8 at 0", which sets
|
|
124
|
+
// the session state to 'error' permanently, so the ClientHello timer then
|
|
125
|
+
// early-returns and NO handshake is ever sent. Guard here rather than
|
|
126
|
+
// making lemon-tls lenient — rejecting malformed handshake input is the
|
|
127
|
+
// correct behavior for a TLS stack facing a hostile peer.
|
|
128
|
+
if (data && data.length > 0) session.message(data);
|
|
84
129
|
}
|
|
85
130
|
|
|
86
131
|
function setupSessionEvents() {
|
|
87
132
|
|
|
133
|
+
// Peer's QUIC transport parameters ride in the TLS quic_transport_parameters
|
|
134
|
+
// extension (type 0x39): client's in the ClientHello, server's in the
|
|
135
|
+
// EncryptedExtensions. lemon-tls surfaces every received handshake message via
|
|
136
|
+
// 'handshakeMessage' with the parsed extensions, so we extract the 0x39 data,
|
|
137
|
+
// parse it, and hand the params to the QUIC layer (which seeds remote_max_data,
|
|
138
|
+
// max_ack_delay, ack_delay_exponent, etc. — previously hardcoded defaults).
|
|
139
|
+
var peerParamsEmitted = false;
|
|
140
|
+
session.on('handshakeMessage', function (type, data, message) {
|
|
141
|
+
if (peerParamsEmitted || !message || !Array.isArray(message.extensions)) return;
|
|
142
|
+
for (var i = 0; i < message.extensions.length; i++) {
|
|
143
|
+
var e = message.extensions[i];
|
|
144
|
+
if (e && (e.type === 0x39 || e.type === 57) && e.data) {
|
|
145
|
+
try {
|
|
146
|
+
var parsed = parse_transport_params(e.data, 0);
|
|
147
|
+
peerParamsEmitted = true;
|
|
148
|
+
if (DEBUG) console.log('[tls] peer transport params parsed (' + e.data.length + 'B)');
|
|
149
|
+
ev.emit('peerTransportParams', parsed);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (DEBUG) console.log('[tls] peer transport params parse failed: ' + err.message);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
88
158
|
session.on('hello', function () {
|
|
89
159
|
if (helloHandled) return; // prevent double-fire
|
|
90
160
|
helloHandled = true;
|
|
91
|
-
console.log('[tls] hello — configuring TLS');
|
|
161
|
+
if (DEBUG) console.log('[tls] hello — configuring TLS');
|
|
92
162
|
|
|
93
163
|
// Transport params — shared defaults + role-specific fields
|
|
94
164
|
var tp = defaultTransportParams();
|
|
@@ -107,13 +177,29 @@ function TLSBridge(options) {
|
|
|
107
177
|
|
|
108
178
|
var tlsContext = {
|
|
109
179
|
local_supported_versions: [0x0304],
|
|
110
|
-
local_supported_alpns:
|
|
180
|
+
local_supported_alpns: supportedAlpns,
|
|
111
181
|
local_supported_groups: [29, 23, 24],
|
|
112
182
|
local_supported_cipher_suites: [0x1301, 0x1302, 0xc02f, 0xc030, 0xcca8],
|
|
113
183
|
local_extensions: [
|
|
114
184
|
{ type: 0x39, data: quicParams }
|
|
115
185
|
],
|
|
116
|
-
|
|
186
|
+
// Signature schemes offered to the peer (client: in the ClientHello;
|
|
187
|
+
// server: in CertificateRequest). Full modern list, ordered PSS/ECDSA/
|
|
188
|
+
// EdDSA first, legacy pkcs1 last (TLS 1.2 compat only — TLS 1.3 never
|
|
189
|
+
// signs pkcs1). Two bugs lived in the old pkcs1-only list:
|
|
190
|
+
// 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
|
|
191
|
+
// outright (alert 40 handshake_failure — seen live against
|
|
192
|
+
// speed.cloudflare.com; claude.ai worked only because that zone
|
|
193
|
+
// also carries an RSA cert).
|
|
194
|
+
// 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
|
|
195
|
+
// §4.1.2 requires CH2 to match CH1 exactly — offering a different
|
|
196
|
+
// list in CH1 made every HRR flow non-compliant.
|
|
197
|
+
local_supported_signature_algorithms: [
|
|
198
|
+
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
199
|
+
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
|
200
|
+
0x0807, 0x0808, // ed25519, ed448
|
|
201
|
+
0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
|
|
202
|
+
],
|
|
117
203
|
};
|
|
118
204
|
|
|
119
205
|
session.set_context(tlsContext);
|
|
@@ -125,7 +211,7 @@ function TLSBridge(options) {
|
|
|
125
211
|
else if (epoch === 1) quicEpoch = 'handshake';
|
|
126
212
|
else quicEpoch = 'app';
|
|
127
213
|
|
|
128
|
-
console.log('[tls] outgoing: epoch=' + quicEpoch + ' type=' + type + ' len=' + data.length);
|
|
214
|
+
if (DEBUG) console.log('[tls] outgoing: epoch=' + quicEpoch + ' type=' + type + ' len=' + data.length);
|
|
129
215
|
ev.emit('send', quicEpoch, data);
|
|
130
216
|
});
|
|
131
217
|
|
|
@@ -135,7 +221,7 @@ function TLSBridge(options) {
|
|
|
135
221
|
handshakeSecretsEmitted = true;
|
|
136
222
|
var cipher = session.getCipher();
|
|
137
223
|
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
138
|
-
console.log('[tls] handshake secrets ready');
|
|
224
|
+
if (DEBUG) console.log('[tls] handshake secrets ready');
|
|
139
225
|
ev.emit('handshakeSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
|
|
140
226
|
});
|
|
141
227
|
|
|
@@ -144,18 +230,34 @@ function TLSBridge(options) {
|
|
|
144
230
|
appSecretsEmitted = true;
|
|
145
231
|
var cipher = session.getCipher();
|
|
146
232
|
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
147
|
-
console.log('[tls] app secrets ready');
|
|
233
|
+
if (DEBUG) console.log('[tls] app secrets ready');
|
|
148
234
|
ev.emit('appSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
|
|
149
235
|
});
|
|
150
236
|
|
|
151
237
|
session.on('secureConnect', function () {
|
|
152
|
-
console.log('[tls] secureConnect');
|
|
238
|
+
if (DEBUG) console.log('[tls] secureConnect');
|
|
153
239
|
ev.emit('secureConnect');
|
|
154
240
|
});
|
|
155
241
|
|
|
156
242
|
session.on('keyUpdate', function (info) {
|
|
157
243
|
ev.emit('keyUpdate', info.direction, info.secret);
|
|
158
244
|
});
|
|
245
|
+
|
|
246
|
+
// NSS SSLKEYLOGFILE lines, forwarded to whoever owns the connection.
|
|
247
|
+
//
|
|
248
|
+
// lemon-tls already formats these correctly for TLS 1.3 — including
|
|
249
|
+
// picking client_random from the right side based on role — and QUIC uses
|
|
250
|
+
// the identical label set (CLIENT_HANDSHAKE_TRAFFIC_SECRET,
|
|
251
|
+
// SERVER_TRAFFIC_SECRET_0, ...), so no translation is needed here.
|
|
252
|
+
//
|
|
253
|
+
// Note that subscribing has a cost: lemon-tls skips building these strings
|
|
254
|
+
// entirely while listenerCount('keylog') is 0. We accept that, matching
|
|
255
|
+
// node:tls, which also always emits 'keylog'. Two short buffers per
|
|
256
|
+
// handshake is not a measurable expense, and having key logs available
|
|
257
|
+
// without a rebuild is worth far more when debugging a live capture.
|
|
258
|
+
session.on('keylog', function (line) {
|
|
259
|
+
ev.emit('keylog', line);
|
|
260
|
+
});
|
|
159
261
|
}
|
|
160
262
|
|
|
161
263
|
return {
|
|
@@ -208,4 +310,4 @@ function createSecureContext(options) {
|
|
|
208
310
|
return ctx;
|
|
209
311
|
}
|
|
210
312
|
|
|
211
|
-
export { TLSBridge, createSecureContext };
|
|
313
|
+
export { TLSBridge, createSecureContext };
|
package/src/transport.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
+
DEBUG,
|
|
16
17
|
concatUint8Arrays,
|
|
17
18
|
writeVarInt,
|
|
18
19
|
readVarInt
|
|
@@ -142,7 +143,7 @@ function parse_quic_datagram(array) {
|
|
|
142
143
|
: array.slice(start, end);
|
|
143
144
|
|
|
144
145
|
if (packets.length > 0) {
|
|
145
|
-
console.log('[quic] coalesced pkt #' + packets.length + ' type=' + pkt.type + ' offset=' + start + ' len=' + pkt.totalLength + ' firstByte=0x' + array[start].toString(16).padStart(2,'0'));
|
|
146
|
+
if (DEBUG) console.log('[quic] coalesced pkt #' + packets.length + ' type=' + pkt.type + ' offset=' + start + ' len=' + pkt.totalLength + ' firstByte=0x' + array[start].toString(16).padStart(2,'0'));
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
packets.push(pkt);
|
|
@@ -153,6 +154,94 @@ function parse_quic_datagram(array) {
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
|
|
157
|
+
// ============================================================
|
|
158
|
+
// Version Negotiation (RFC 9000 §6, §17.2.1)
|
|
159
|
+
// ============================================================
|
|
160
|
+
|
|
161
|
+
/** QUIC versions this implementation can actually speak. */
|
|
162
|
+
var SUPPORTED_VERSIONS = [0x00000001];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Is this a version we can handle?
|
|
166
|
+
* Version 0 identifies a Version Negotiation packet, not a real version.
|
|
167
|
+
*/
|
|
168
|
+
function is_supported_version(version) {
|
|
169
|
+
return SUPPORTED_VERSIONS.indexOf(version >>> 0) !== -1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a Version Negotiation packet (RFC 9000 §17.2.1).
|
|
174
|
+
*
|
|
175
|
+
* A server MUST send one when it receives a long-header packet carrying a
|
|
176
|
+
* version it does not support — it is how a client discovers what the server
|
|
177
|
+
* speaks, and how any QUIC prober checks whether a server is alive without
|
|
178
|
+
* completing a handshake. The interop runner's simulator does exactly that
|
|
179
|
+
* before starting a test, so without this the server looks dead.
|
|
180
|
+
*
|
|
181
|
+
* The packet is unusual: no encryption, no packet number, no frames, and no
|
|
182
|
+
* connection state. It is valid only as an immediate response.
|
|
183
|
+
*
|
|
184
|
+
* IMPORTANT: the connection IDs are swapped. The DCID we send is the SCID we
|
|
185
|
+
* received, and vice versa — the client matches the reply against its own
|
|
186
|
+
* Source CID, so getting this backwards makes the packet be ignored.
|
|
187
|
+
*
|
|
188
|
+
* byte 0 1 (long header) + 7 unpredictable bits
|
|
189
|
+
* 1..4 version 0x00000000
|
|
190
|
+
* 5 DCID length, then DCID
|
|
191
|
+
* ... SCID length, then SCID
|
|
192
|
+
* ... list of supported versions, 4 bytes each
|
|
193
|
+
*
|
|
194
|
+
* @param {Uint8Array} recvDcid DCID from the client's packet (becomes our SCID)
|
|
195
|
+
* @param {Uint8Array} recvScid SCID from the client's packet (becomes our DCID)
|
|
196
|
+
*/
|
|
197
|
+
function build_version_negotiation(recvDcid, recvScid) {
|
|
198
|
+
var dcid = recvScid || new Uint8Array(0);
|
|
199
|
+
var scid = recvDcid || new Uint8Array(0);
|
|
200
|
+
|
|
201
|
+
// RFC 9000 §17.2.1: the low 7 bits are unused and SHOULD be random, so that
|
|
202
|
+
// clients don't come to depend on a fixed value.
|
|
203
|
+
var first = 0x80 | (Math.floor(Math.random() * 128) & 0x7f);
|
|
204
|
+
|
|
205
|
+
var versions = SUPPORTED_VERSIONS.slice();
|
|
206
|
+
|
|
207
|
+
// A reserved ("greased") version, per RFC 9000 §6.3. Its pattern is
|
|
208
|
+
// 0x?a?a?a?a, and no endpoint will ever support it. Including one keeps
|
|
209
|
+
// clients honest: any client that assumes it understands every version in
|
|
210
|
+
// the list will break here rather than in the field.
|
|
211
|
+
var grease = ((Math.floor(Math.random() * 16) << 28) |
|
|
212
|
+
(0x0a << 24) |
|
|
213
|
+
(Math.floor(Math.random() * 16) << 20) |
|
|
214
|
+
(0x0a << 16) |
|
|
215
|
+
(Math.floor(Math.random() * 16) << 12) |
|
|
216
|
+
(0x0a << 8) |
|
|
217
|
+
(Math.floor(Math.random() * 16) << 4) |
|
|
218
|
+
0x0a) >>> 0;
|
|
219
|
+
versions.push(grease);
|
|
220
|
+
|
|
221
|
+
var out = new Uint8Array(1 + 4 + 1 + dcid.length + 1 + scid.length + versions.length * 4);
|
|
222
|
+
var o = 0;
|
|
223
|
+
|
|
224
|
+
out[o++] = first;
|
|
225
|
+
out[o++] = 0; out[o++] = 0; out[o++] = 0; out[o++] = 0; // version 0
|
|
226
|
+
|
|
227
|
+
out[o++] = dcid.length;
|
|
228
|
+
out.set(dcid, o); o += dcid.length;
|
|
229
|
+
|
|
230
|
+
out[o++] = scid.length;
|
|
231
|
+
out.set(scid, o); o += scid.length;
|
|
232
|
+
|
|
233
|
+
for (var i = 0; i < versions.length; i++) {
|
|
234
|
+
var v = versions[i] >>> 0;
|
|
235
|
+
out[o++] = (v >>> 24) & 0xff;
|
|
236
|
+
out[o++] = (v >>> 16) & 0xff;
|
|
237
|
+
out[o++] = (v >>> 8) & 0xff;
|
|
238
|
+
out[o++] = v & 0xff;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
156
245
|
// ============================================================
|
|
157
246
|
// QUIC Frame encoding (RFC 9000 §12.4)
|
|
158
247
|
// ============================================================
|
|
@@ -201,18 +290,25 @@ function encode_quic_frames(frames) {
|
|
|
201
290
|
} else if (frame.type === 'stream') {
|
|
202
291
|
var typeByte = 0x08;
|
|
203
292
|
var hasOffset = (frame.offset != null && frame.offset > 0);
|
|
204
|
-
var hasLen = (frame.data && frame.data.length > 0);
|
|
205
293
|
var hasFin = !!frame.fin;
|
|
206
|
-
|
|
294
|
+
var dataLen = (frame.data && frame.data.length) ? frame.data.length : 0;
|
|
295
|
+
|
|
296
|
+
// ALWAYS emit the LEN field. A STREAM frame without the LEN bit means
|
|
297
|
+
// "data runs to the end of the packet"; if anything follows it in the
|
|
298
|
+
// packet (another frame — e.g. a zero-length FIN-only batched with
|
|
299
|
+
// others — or padding), the peer reads those trailing bytes as stream
|
|
300
|
+
// data and the offset overshoots the declared final size
|
|
301
|
+
// (FINAL_SIZE / PROTOCOL_VIOLATION 0xa). An explicit length is always
|
|
302
|
+
// safe and self-delimiting.
|
|
207
303
|
if (hasOffset) typeByte |= 0x04;
|
|
208
|
-
|
|
304
|
+
typeByte |= 0x02; // LEN — always present
|
|
209
305
|
if (hasFin) typeByte |= 0x01;
|
|
210
306
|
|
|
211
307
|
parts.push(concatUint8Arrays([
|
|
212
308
|
new Uint8Array([typeByte]),
|
|
213
309
|
writeVarInt(frame.id),
|
|
214
310
|
hasOffset ? writeVarInt(frame.offset) : new Uint8Array(0),
|
|
215
|
-
|
|
311
|
+
writeVarInt(dataLen),
|
|
216
312
|
frame.data || new Uint8Array(0)
|
|
217
313
|
]));
|
|
218
314
|
|
|
@@ -315,18 +411,14 @@ function encode_quic_frames(frames) {
|
|
|
315
411
|
]));
|
|
316
412
|
|
|
317
413
|
} else if (frame.type === 'datagram') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
])
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
new Uint8Array([0x30]),
|
|
327
|
-
frame.data
|
|
328
|
-
]));
|
|
329
|
-
}
|
|
414
|
+
// Emit the 0x30 form (no Length): the datagram data runs to the end of
|
|
415
|
+
// the packet. QUICO sends each DATAGRAM as the sole/last frame of its
|
|
416
|
+
// packet, so the length-prefixed 0x31 form is not needed here. (The
|
|
417
|
+
// parser above accepts both forms on receive.)
|
|
418
|
+
parts.push(concatUint8Arrays([
|
|
419
|
+
new Uint8Array([0x30]),
|
|
420
|
+
frame.data
|
|
421
|
+
]));
|
|
330
422
|
}
|
|
331
423
|
}
|
|
332
424
|
|
|
@@ -498,13 +590,21 @@ function parse_quic_frames(buf) {
|
|
|
498
590
|
frames.push({ type: 'handshake_done' });
|
|
499
591
|
|
|
500
592
|
} else if (type === 0x30 || type === 0x31) {
|
|
501
|
-
|
|
593
|
+
// RFC 9221 DATAGRAM frame. The 0x31 form carries an explicit Length
|
|
594
|
+
// varint and the datagram data is exactly that many bytes; the 0x30 form
|
|
595
|
+
// has no Length and the data runs to the end of the packet (so a 0x30
|
|
596
|
+
// DATAGRAM must be the last frame). The optional field is a Length — not
|
|
597
|
+
// a "context ID" (that draft-era concept was removed before RFC 9221).
|
|
598
|
+
var data;
|
|
502
599
|
if (type === 0x31) {
|
|
503
|
-
var
|
|
504
|
-
|
|
600
|
+
var len = safeReadVarInt(); if (!len) break;
|
|
601
|
+
var end = offset + len.value;
|
|
602
|
+
if (end > buf.length) break; // truncated / invalid length
|
|
603
|
+
data = buf.slice(offset, end); offset = end;
|
|
604
|
+
} else {
|
|
605
|
+
data = buf.slice(offset); offset = buf.length;
|
|
505
606
|
}
|
|
506
|
-
|
|
507
|
-
frames.push({ type: 'datagram', contextId: contextId, data: data });
|
|
607
|
+
frames.push({ type: 'datagram', data: data });
|
|
508
608
|
|
|
509
609
|
} else {
|
|
510
610
|
// Unknown frame — stop
|
|
@@ -604,24 +704,28 @@ function parse_transport_params(buf, start) {
|
|
|
604
704
|
|
|
605
705
|
var id = idVar.value;
|
|
606
706
|
|
|
707
|
+
// Safe inner varint: a malformed param could carry an empty/short value,
|
|
708
|
+
// in which case readVarInt returns null — never dereference .value blindly.
|
|
709
|
+
function vi(bytes) { var r = readVarInt(bytes, 0); return r ? r.value : 0; }
|
|
710
|
+
|
|
607
711
|
switch (id) {
|
|
608
712
|
case 0x00: out.original_destination_connection_id = valueBytes; break;
|
|
609
|
-
case 0x01: out.max_idle_timeout =
|
|
713
|
+
case 0x01: out.max_idle_timeout = vi(valueBytes); break;
|
|
610
714
|
case 0x02: out.stateless_reset_token = valueBytes; break;
|
|
611
|
-
case 0x03: out.max_udp_payload_size =
|
|
612
|
-
case 0x04: out.initial_max_data =
|
|
613
|
-
case 0x05: out.initial_max_stream_data_bidi_local =
|
|
614
|
-
case 0x06: out.initial_max_stream_data_bidi_remote =
|
|
615
|
-
case 0x07: out.initial_max_stream_data_uni =
|
|
616
|
-
case 0x08: out.initial_max_streams_bidi =
|
|
617
|
-
case 0x09: out.initial_max_streams_uni =
|
|
618
|
-
case 0x0a: out.ack_delay_exponent =
|
|
619
|
-
case 0x0b: out.max_ack_delay =
|
|
715
|
+
case 0x03: out.max_udp_payload_size = vi(valueBytes); break;
|
|
716
|
+
case 0x04: out.initial_max_data = vi(valueBytes); break;
|
|
717
|
+
case 0x05: out.initial_max_stream_data_bidi_local = vi(valueBytes); break;
|
|
718
|
+
case 0x06: out.initial_max_stream_data_bidi_remote = vi(valueBytes); break;
|
|
719
|
+
case 0x07: out.initial_max_stream_data_uni = vi(valueBytes); break;
|
|
720
|
+
case 0x08: out.initial_max_streams_bidi = vi(valueBytes); break;
|
|
721
|
+
case 0x09: out.initial_max_streams_uni = vi(valueBytes); break;
|
|
722
|
+
case 0x0a: out.ack_delay_exponent = vi(valueBytes); break;
|
|
723
|
+
case 0x0b: out.max_ack_delay = vi(valueBytes); break;
|
|
620
724
|
case 0x0c: out.disable_active_migration = true; break;
|
|
621
|
-
case 0x0e: out.active_connection_id_limit =
|
|
725
|
+
case 0x0e: out.active_connection_id_limit = vi(valueBytes); break;
|
|
622
726
|
case 0x0f: out.initial_source_connection_id = valueBytes; break;
|
|
623
727
|
case 0x10: out.retry_source_connection_id = valueBytes; break;
|
|
624
|
-
case 0x20: out.max_datagram_frame_size =
|
|
728
|
+
case 0x20: out.max_datagram_frame_size = vi(valueBytes); break;
|
|
625
729
|
default:
|
|
626
730
|
if (!out.unknown) out.unknown = [];
|
|
627
731
|
out.unknown.push({ id: id, bytes: valueBytes });
|
|
@@ -641,6 +745,11 @@ export {
|
|
|
641
745
|
parse_quic_datagram,
|
|
642
746
|
parse_quic_packet,
|
|
643
747
|
|
|
748
|
+
// Version negotiation
|
|
749
|
+
SUPPORTED_VERSIONS,
|
|
750
|
+
is_supported_version,
|
|
751
|
+
build_version_negotiation,
|
|
752
|
+
|
|
644
753
|
// Frame encode/decode
|
|
645
754
|
encode_quic_frames,
|
|
646
755
|
parse_quic_frames,
|
package/src/utils.js
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Debug flag — set QUICO_DEBUG=1 to enable verbose logging.
|
|
18
|
+
// Read once at module load. Call sites guard with `if (DEBUG)`
|
|
19
|
+
// so the (expensive) log-string construction is skipped entirely
|
|
20
|
+
// when debugging is off.
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
var DEBUG = !!(typeof process !== 'undefined' && process.env && process.env.QUICO_DEBUG);
|
|
24
|
+
|
|
25
|
+
|
|
16
26
|
// ============================================================
|
|
17
27
|
// Emitter — lightweight event emitter (single shared copy)
|
|
18
28
|
// ============================================================
|
|
@@ -124,10 +134,12 @@ function readVarInt(array, offset) {
|
|
|
124
134
|
array[offset + 7]
|
|
125
135
|
) >>> 0;
|
|
126
136
|
var full = BigInt(hi) * 4294967296n + BigInt(lo);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
// Always return a Number. QUIC fields (lengths, offsets, IDs, limits) never
|
|
138
|
+
// legitimately exceed 2^53, and a malicious 8-byte value that does will be
|
|
139
|
+
// rejected by the parsers' bounds checks anyway. Returning a BigInt here
|
|
140
|
+
// would throw "Cannot mix BigInt and other types" the moment the value is
|
|
141
|
+
// combined with a Number offset during parsing.
|
|
142
|
+
return { value: Number(full), byteLength: 8 };
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
return null;
|
|
@@ -164,6 +176,46 @@ function uint8Equal(a, b) {
|
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
|
|
179
|
+
// ============================================================
|
|
180
|
+
// WebTransport framing helpers (draft-ietf-webtrans-http3 / RFC 9297)
|
|
181
|
+
//
|
|
182
|
+
// Both client (webtransport.js) and server (h3.js) build the same
|
|
183
|
+
// byte layouts, so they live here to avoid duplicating the prefix
|
|
184
|
+
// construction across the stream and datagram send paths.
|
|
185
|
+
// ============================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a WebTransport stream header: VarInt(streamType) + VarInt(sessionId) [+ data].
|
|
189
|
+
* streamType is 0x41 for a bidirectional WT stream, 0x54 for a unidirectional one.
|
|
190
|
+
* The header prefixes the FIRST chunk on a WT stream (or a lone FIN frame); pass
|
|
191
|
+
* `data` for "header + payload", omit it for a header-only (e.g. FIN) frame.
|
|
192
|
+
*/
|
|
193
|
+
function wt_stream_header(isBidi, sessionId, data) {
|
|
194
|
+
var typePrefix = writeVarInt(isBidi ? 0x41 : 0x54);
|
|
195
|
+
var sessionPrefix = writeVarInt(sessionId);
|
|
196
|
+
var dataLen = (data && data.byteLength) ? data.byteLength : 0;
|
|
197
|
+
var out = new Uint8Array(typePrefix.length + sessionPrefix.length + dataLen);
|
|
198
|
+
out.set(typePrefix, 0);
|
|
199
|
+
out.set(sessionPrefix, typePrefix.length);
|
|
200
|
+
if (dataLen) out.set(data, typePrefix.length + sessionPrefix.length);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build a WebTransport datagram payload: VarInt(quarter_stream_id) + data.
|
|
206
|
+
* The quarter-stream-id is the session's CONNECT stream id divided by 4
|
|
207
|
+
* (RFC 9297 §2.1). Pass the session stream id; the division is done here.
|
|
208
|
+
*/
|
|
209
|
+
function wt_datagram_payload(sessionStreamId, data) {
|
|
210
|
+
var prefix = writeVarInt(Math.floor(sessionStreamId / 4));
|
|
211
|
+
var dataLen = (data && data.byteLength) ? data.byteLength : 0;
|
|
212
|
+
var out = new Uint8Array(prefix.length + dataLen);
|
|
213
|
+
out.set(prefix, 0);
|
|
214
|
+
if (dataLen) out.set(data, prefix.length);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
167
219
|
// ============================================================
|
|
168
220
|
// ACK range helpers (flat [from,to, from,to, ...] format)
|
|
169
221
|
// ============================================================
|
|
@@ -280,11 +332,14 @@ function ranges_to_ack_frame(flatRanges, ecnStats, ackDelay) {
|
|
|
280
332
|
|
|
281
333
|
|
|
282
334
|
export {
|
|
335
|
+
DEBUG,
|
|
283
336
|
Emitter,
|
|
284
337
|
writeVarInt,
|
|
285
338
|
readVarInt,
|
|
286
339
|
concatUint8Arrays,
|
|
287
340
|
uint8Equal,
|
|
341
|
+
wt_stream_header,
|
|
342
|
+
wt_datagram_payload,
|
|
288
343
|
ack_frame_to_ranges,
|
|
289
344
|
ranges_to_ack_frame
|
|
290
345
|
};
|