quico 0.1.3 → 0.3.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 +381 -216
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +49 -23
- package/package.json +83 -60
- package/src/agent.js +283 -0
- package/src/client.js +444 -0
- package/src/crypto.js +534 -0
- package/src/h3.js +920 -0
- package/src/h3_client.js +332 -0
- package/src/h3_server.js +392 -0
- package/src/quic_connection.js +2338 -0
- package/src/quic_server.js +314 -0
- package/src/quic_socket.js +76 -0
- package/src/server.js +354 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +273 -0
- package/src/transport.js +667 -0
- package/src/utils.js +345 -0
- package/src/webtransport.js +655 -0
- package/CHANGELOG.md +0 -31
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -23
- package/h3_server.js +0 -416
- package/h3_socket.js +0 -473
- package/libs/crypto.js +0 -1841
- package/libs/h3.js +0 -1461
- package/libs/utils.js +0 -404
- package/quic_socket.js +0 -1550
- package/quico.svg +0 -1
package/src/utils.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
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
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// Emitter — lightweight event emitter (single shared copy)
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
function Emitter() {
|
|
31
|
+
var listeners = {};
|
|
32
|
+
return {
|
|
33
|
+
on: function (name, fn) {
|
|
34
|
+
(listeners[name] = listeners[name] || []).push(fn);
|
|
35
|
+
},
|
|
36
|
+
off: function (name, fn) {
|
|
37
|
+
var arr = listeners[name];
|
|
38
|
+
if (arr) {
|
|
39
|
+
var idx = arr.indexOf(fn);
|
|
40
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
emit: function (name) {
|
|
44
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
45
|
+
var arr = listeners[name] || [];
|
|
46
|
+
for (var i = 0; i < arr.length; i++) {
|
|
47
|
+
try { arr[i].apply(null, args); } catch (e) { /* swallow */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// QUIC Variable-Length Integer (RFC 9000 §16)
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
function writeVarInt(value) {
|
|
59
|
+
if (value < 0x40) {
|
|
60
|
+
return new Uint8Array([value]);
|
|
61
|
+
}
|
|
62
|
+
if (value < 0x4000) {
|
|
63
|
+
return new Uint8Array([
|
|
64
|
+
0x40 | (value >> 8),
|
|
65
|
+
value & 0xff
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
if (value < 0x40000000) {
|
|
69
|
+
return new Uint8Array([
|
|
70
|
+
0x80 | (value >> 24),
|
|
71
|
+
(value >> 16) & 0xff,
|
|
72
|
+
(value >> 8) & 0xff,
|
|
73
|
+
value & 0xff
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
if (value <= Number.MAX_SAFE_INTEGER) {
|
|
77
|
+
var hi = Math.floor(value / 2 ** 32);
|
|
78
|
+
var lo = value >>> 0;
|
|
79
|
+
return new Uint8Array([
|
|
80
|
+
0xc0 | (hi >> 24),
|
|
81
|
+
(hi >> 16) & 0xff,
|
|
82
|
+
(hi >> 8) & 0xff,
|
|
83
|
+
hi & 0xff,
|
|
84
|
+
(lo >> 24) & 0xff,
|
|
85
|
+
(lo >> 16) & 0xff,
|
|
86
|
+
(lo >> 8) & 0xff,
|
|
87
|
+
lo & 0xff
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
throw new Error('Value too large for QUIC VarInt');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
function readVarInt(array, offset) {
|
|
95
|
+
if (offset >= array.length) return null;
|
|
96
|
+
|
|
97
|
+
var first = array[offset];
|
|
98
|
+
var prefix = first >> 6;
|
|
99
|
+
|
|
100
|
+
if (prefix === 0) {
|
|
101
|
+
return { value: first & 0x3f, byteLength: 1 };
|
|
102
|
+
}
|
|
103
|
+
if (prefix === 1) {
|
|
104
|
+
if (offset + 1 >= array.length) return null;
|
|
105
|
+
return {
|
|
106
|
+
value: ((first & 0x3f) << 8) | array[offset + 1],
|
|
107
|
+
byteLength: 2
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (prefix === 2) {
|
|
111
|
+
if (offset + 3 >= array.length) return null;
|
|
112
|
+
return {
|
|
113
|
+
value: (
|
|
114
|
+
((first & 0x3f) << 24) |
|
|
115
|
+
(array[offset + 1] << 16) |
|
|
116
|
+
(array[offset + 2] << 8) |
|
|
117
|
+
array[offset + 3]
|
|
118
|
+
) >>> 0,
|
|
119
|
+
byteLength: 4
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (prefix === 3) {
|
|
123
|
+
if (offset + 7 >= array.length) return null;
|
|
124
|
+
var hi = (
|
|
125
|
+
((first & 0x3f) << 24) |
|
|
126
|
+
(array[offset + 1] << 16) |
|
|
127
|
+
(array[offset + 2] << 8) |
|
|
128
|
+
array[offset + 3]
|
|
129
|
+
) >>> 0;
|
|
130
|
+
var lo = (
|
|
131
|
+
(array[offset + 4] << 24) |
|
|
132
|
+
(array[offset + 5] << 16) |
|
|
133
|
+
(array[offset + 6] << 8) |
|
|
134
|
+
array[offset + 7]
|
|
135
|
+
) >>> 0;
|
|
136
|
+
var full = BigInt(hi) * 4294967296n + BigInt(lo);
|
|
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 };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// ============================================================
|
|
150
|
+
// Binary helpers
|
|
151
|
+
// ============================================================
|
|
152
|
+
|
|
153
|
+
function concatUint8Arrays(arrays) {
|
|
154
|
+
var totalLength = 0;
|
|
155
|
+
for (var i = 0; i < arrays.length; i++) {
|
|
156
|
+
totalLength += arrays[i].length;
|
|
157
|
+
}
|
|
158
|
+
var result = new Uint8Array(totalLength);
|
|
159
|
+
var offset = 0;
|
|
160
|
+
for (var i = 0; i < arrays.length; i++) {
|
|
161
|
+
result.set(arrays[i], offset);
|
|
162
|
+
offset += arrays[i].length;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
function uint8Equal(a, b) {
|
|
169
|
+
if (a === b) return true;
|
|
170
|
+
if (!a || !b) return false;
|
|
171
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
172
|
+
for (var i = 0; i < a.byteLength; i++) {
|
|
173
|
+
if (a[i] !== b[i]) return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
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
|
+
|
|
219
|
+
// ============================================================
|
|
220
|
+
// ACK range helpers (flat [from,to, from,to, ...] format)
|
|
221
|
+
// ============================================================
|
|
222
|
+
|
|
223
|
+
function ack_frame_to_ranges(ackFrame) {
|
|
224
|
+
var flat = [];
|
|
225
|
+
if (!ackFrame || ackFrame.type !== 'ack') return flat;
|
|
226
|
+
|
|
227
|
+
var largest = Number(ackFrame.largest);
|
|
228
|
+
var firstRange = Number(ackFrame.firstRange);
|
|
229
|
+
if (!isFinite(largest) || !isFinite(firstRange)) return flat;
|
|
230
|
+
|
|
231
|
+
var rangesDesc = [];
|
|
232
|
+
|
|
233
|
+
// First range
|
|
234
|
+
var rangeEnd = largest;
|
|
235
|
+
var rangeStart = rangeEnd - firstRange;
|
|
236
|
+
if (rangeStart <= rangeEnd) {
|
|
237
|
+
rangesDesc.push([rangeStart, rangeEnd]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Additional ranges
|
|
241
|
+
var more = ackFrame.ranges || [];
|
|
242
|
+
var prevStart = rangeStart;
|
|
243
|
+
for (var i = 0; i < more.length; i++) {
|
|
244
|
+
var gap = Number(more[i].gap);
|
|
245
|
+
var length = Number(more[i].length);
|
|
246
|
+
if (!isFinite(gap) || !isFinite(length)) continue;
|
|
247
|
+
|
|
248
|
+
rangeEnd = prevStart - gap - 2;
|
|
249
|
+
rangeStart = rangeEnd - length;
|
|
250
|
+
if (rangeStart <= rangeEnd) {
|
|
251
|
+
rangesDesc.push([rangeStart, rangeEnd]);
|
|
252
|
+
prevStart = rangeStart;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Sort ascending, merge, and flatten
|
|
257
|
+
rangesDesc.sort(function (a, b) { return a[0] - b[0]; });
|
|
258
|
+
|
|
259
|
+
var merged = [];
|
|
260
|
+
for (var j = 0; j < rangesDesc.length; j++) {
|
|
261
|
+
var s = rangesDesc[j][0], e = rangesDesc[j][1];
|
|
262
|
+
if (merged.length === 0) {
|
|
263
|
+
merged.push([s, e]);
|
|
264
|
+
} else {
|
|
265
|
+
var last = merged[merged.length - 1];
|
|
266
|
+
if (s <= last[1] + 1) {
|
|
267
|
+
if (e > last[1]) last[1] = e;
|
|
268
|
+
} else {
|
|
269
|
+
merged.push([s, e]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (var k = 0; k < merged.length; k++) {
|
|
275
|
+
flat.push(merged[k][0], merged[k][1]);
|
|
276
|
+
}
|
|
277
|
+
return flat;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
function ranges_to_ack_frame(flatRanges, ecnStats, ackDelay) {
|
|
282
|
+
if (!flatRanges || flatRanges.length === 0) return null;
|
|
283
|
+
if (flatRanges.length % 2 !== 0) throw new Error('flatRanges must be [from,to,...] pairs');
|
|
284
|
+
|
|
285
|
+
// flat_ranges uses exclusive end: [from, to) → convert to inclusive [start, end]
|
|
286
|
+
var ranges = [];
|
|
287
|
+
for (var i = 0; i < flatRanges.length; i += 2) {
|
|
288
|
+
if (flatRanges[i + 1] > flatRanges[i]) {
|
|
289
|
+
ranges.push({ start: flatRanges[i], end: flatRanges[i + 1] - 1 });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (ranges.length === 0) return null;
|
|
294
|
+
|
|
295
|
+
ranges.sort(function (a, b) { return b.end - a.end; });
|
|
296
|
+
|
|
297
|
+
// Merge overlapping/adjacent
|
|
298
|
+
var merged = [ranges[0]];
|
|
299
|
+
for (var i = 1; i < ranges.length; i++) {
|
|
300
|
+
var last = merged[merged.length - 1];
|
|
301
|
+
var curr = ranges[i];
|
|
302
|
+
if (curr.end >= last.start - 1) {
|
|
303
|
+
last.start = Math.min(last.start, curr.start);
|
|
304
|
+
} else {
|
|
305
|
+
merged.push(curr);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
var largest = merged[0].end;
|
|
310
|
+
var firstRange = largest - merged[0].start;
|
|
311
|
+
var ackRanges = [];
|
|
312
|
+
|
|
313
|
+
for (var i = 1; i < merged.length; i++) {
|
|
314
|
+
var gap = merged[i - 1].start - merged[i].end - 2;
|
|
315
|
+
var length = merged[i].end - merged[i].start;
|
|
316
|
+
ackRanges.push({ gap: gap, length: length });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
type: 'ack',
|
|
321
|
+
largest: largest,
|
|
322
|
+
delay: ackDelay || 0,
|
|
323
|
+
firstRange: firstRange,
|
|
324
|
+
ranges: ackRanges,
|
|
325
|
+
ecn: ecnStats ? {
|
|
326
|
+
ect0: ecnStats.ect0 || 0,
|
|
327
|
+
ect1: ecnStats.ect1 || 0,
|
|
328
|
+
ce: ecnStats.ce || 0
|
|
329
|
+
} : null
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
export {
|
|
335
|
+
DEBUG,
|
|
336
|
+
Emitter,
|
|
337
|
+
writeVarInt,
|
|
338
|
+
readVarInt,
|
|
339
|
+
concatUint8Arrays,
|
|
340
|
+
uint8Equal,
|
|
341
|
+
wt_stream_header,
|
|
342
|
+
wt_datagram_payload,
|
|
343
|
+
ack_frame_to_ranges,
|
|
344
|
+
ranges_to_ack_frame
|
|
345
|
+
};
|