quico 0.4.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 +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: a full HTTP stack in JavaScript
|
|
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://github.com/colocohen/quico
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import { sha12 } from './http_common.js';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Protocol-layer fingerprints: HTTP/2 and HTTP/3.
|
|
20
|
+
//
|
|
21
|
+
// The third layer, alongside the TLS fingerprints lemon-tls produces (JA4)
|
|
22
|
+
// and the header fingerprint in ja4h.js. Each answers a different question:
|
|
23
|
+
//
|
|
24
|
+
// JA4 which TLS library built the ClientHello
|
|
25
|
+
// JA4H which HTTP client wrote the headers
|
|
26
|
+
// here which HTTP/2 or HTTP/3 implementation set up the connection
|
|
27
|
+
//
|
|
28
|
+
// Their value is in DISAGREEING. A request whose TLS says BoringSSL, whose
|
|
29
|
+
// headers say Chrome, and whose SETTINGS frame says hyper-h2 is not Chrome,
|
|
30
|
+
// and no amount of User-Agent spoofing hides it. All three layers have to
|
|
31
|
+
// tell the same story, and each one a client fakes correctly is one more it
|
|
32
|
+
// has to keep in sync with the others.
|
|
33
|
+
//
|
|
34
|
+
// Naming: the HTTP/2 format below is the one published by Elad Shuster at
|
|
35
|
+
// Black Hat Europe 2017 from more than 10 million connections observed at
|
|
36
|
+
// Akamai's edge. It is a research format, not a licensed standard, and there
|
|
37
|
+
// is no reference implementation with terms attached — unlike JA4+, where
|
|
38
|
+
// everything except JA4 itself carries the FoxIO License. "Akamai" is their
|
|
39
|
+
// trademark, so the function is named for what it does.
|
|
40
|
+
// ============================================================
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// ============================================================
|
|
46
|
+
// HTTP/2
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* h2Fingerprint(observed) — the four-section HTTP/2 fingerprint.
|
|
51
|
+
*
|
|
52
|
+
* S[;] | WU | P[,] | PS[,]
|
|
53
|
+
*
|
|
54
|
+
* 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p <- Chrome
|
|
55
|
+
*
|
|
56
|
+
* S SETTINGS as ID:Value pairs, IN THE ORDER SENT. Which parameters are
|
|
57
|
+
* ABSENT matters as much as the values: Chrome sends only four of the
|
|
58
|
+
* six, omitting MAX_CONCURRENT_STREAMS and MAX_FRAME_SIZE entirely, so
|
|
59
|
+
* a client sending all six is not Chrome whatever it claims.
|
|
60
|
+
*
|
|
61
|
+
* WU The WINDOW_UPDATE increment on stream 0, or 0 if none was sent.
|
|
62
|
+
* Chrome sends 15663105 and Firefox 12517377 — browsers open large
|
|
63
|
+
* windows because they fetch dozens of resources at once, while
|
|
64
|
+
* libraries keep the 65535 default and send no WINDOW_UPDATE at all.
|
|
65
|
+
*
|
|
66
|
+
* P PRIORITY frames as StreamID:Exclusive:DependsOn:Weight, or 0.
|
|
67
|
+
* Mostly historical now: RFC 9218 deprecated the dependency tree, and
|
|
68
|
+
* modern Chrome sends no PRIORITY frames at all — it signals priority
|
|
69
|
+
* through the HEADERS frame and the `priority` header instead. The 0 IS
|
|
70
|
+
* the fingerprint. Firefox still sends them, so presence or absence
|
|
71
|
+
* remains a clean binary that has to match the claimed browser.
|
|
72
|
+
*
|
|
73
|
+
* PS Pseudo-header order. Chrome m,a,s,p — Firefox m,p,a,s —
|
|
74
|
+
* Safari m,s,p,a. The spec fixes that pseudo-headers come first but
|
|
75
|
+
* says nothing about their order among themselves, so each
|
|
76
|
+
* implementation picked one and kept it. This is the single strongest
|
|
77
|
+
* section: it is set deep in the network stack, and most HTTP libraries
|
|
78
|
+
* use an order matching no browser at all.
|
|
79
|
+
*/
|
|
80
|
+
function h2Fingerprint(observed) {
|
|
81
|
+
observed = observed || {};
|
|
82
|
+
|
|
83
|
+
// Pairs in the order received. An object would not do: insertion order is
|
|
84
|
+
// an implementation detail, and the order IS the signal here.
|
|
85
|
+
var settings = Array.isArray(observed.settings) ? observed.settings : [];
|
|
86
|
+
var s = settings.map(function (p) { return p.id + ':' + p.value; }).join(';');
|
|
87
|
+
|
|
88
|
+
var wu = observed.windowUpdate ? String(observed.windowUpdate) : '0';
|
|
89
|
+
|
|
90
|
+
var priorities = Array.isArray(observed.priorities) ? observed.priorities : [];
|
|
91
|
+
var p = priorities.length
|
|
92
|
+
? priorities.map(function (x) {
|
|
93
|
+
return x.streamId + ':' + (x.exclusive ? 1 : 0) + ':' +
|
|
94
|
+
x.dependsOn + ':' + x.weight;
|
|
95
|
+
}).join(',')
|
|
96
|
+
: '0';
|
|
97
|
+
|
|
98
|
+
var ps = Array.isArray(observed.pseudoHeaderOrder) && observed.pseudoHeaderOrder.length
|
|
99
|
+
? observed.pseudoHeaderOrder.join(',')
|
|
100
|
+
: '0';
|
|
101
|
+
|
|
102
|
+
var raw = s + '|' + wu + '|' + p + '|' + ps;
|
|
103
|
+
return {
|
|
104
|
+
hash: sha12(raw),
|
|
105
|
+
|
|
106
|
+
// The MD5 of the raw string is the form published alongside this format,
|
|
107
|
+
// and it is what an external database is keyed by. Verified against an
|
|
108
|
+
// independent capture of Chrome: the same header set yields
|
|
109
|
+
// 52d84b11737d980aef856699f885ca86 there and here.
|
|
110
|
+
//
|
|
111
|
+
// Both are returned because they answer different questions. `hash` keeps
|
|
112
|
+
// every fingerprint on a request looking alike; `md5` is for matching
|
|
113
|
+
// anything outside quico, and without it a cross-check fails for no
|
|
114
|
+
// visible reason.
|
|
115
|
+
md5: crypto.createHash('md5').update(raw, 'utf8').digest('hex'),
|
|
116
|
+
|
|
117
|
+
raw: raw, s: s, wu: wu, p: p, ps: ps
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** ':method' -> 'm'. The four are the only pseudo-headers a request may carry. */
|
|
122
|
+
var PSEUDO_LETTER = { ':method': 'm', ':authority': 'a', ':scheme': 's', ':path': 'p' };
|
|
123
|
+
|
|
124
|
+
function pseudoHeaderOrder(names) {
|
|
125
|
+
var out = [];
|
|
126
|
+
for (var i = 0; i < (names || []).length; i++) {
|
|
127
|
+
var l = PSEUDO_LETTER[names[i]];
|
|
128
|
+
if (l) out.push(l);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// ============================================================
|
|
135
|
+
// HTTP/3 over QUIC
|
|
136
|
+
// ============================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* h3Fingerprint(observed) — the same idea one layer down.
|
|
140
|
+
*
|
|
141
|
+
* QUIC transport parameters do for the transport what SETTINGS does for
|
|
142
|
+
* HTTP/2, and each implementation ships different defaults: Chrome asks for
|
|
143
|
+
* 15MB of connection data and 6291456 per stream — mirroring its HTTP/2
|
|
144
|
+
* INITIAL_WINDOW_SIZE — and a 30s idle timeout.
|
|
145
|
+
*
|
|
146
|
+
* NO PUBLISHED FORMAT EXISTS. Unlike the HTTP/2 string above, nobody has
|
|
147
|
+
* standardised how to serialise these, so the layout below is quico's own:
|
|
148
|
+
* the same four-section, pipe-delimited shape, with QUIC's equivalents in
|
|
149
|
+
* each slot.
|
|
150
|
+
*
|
|
151
|
+
* TP | H3_SETTINGS | ALPN | GREASE
|
|
152
|
+
*
|
|
153
|
+
* That means these values are comparable ACROSS TIME and between quico
|
|
154
|
+
* servers, but NOT against a third-party database — there is nothing to
|
|
155
|
+
* compare them to. Which is fine for the actual use: catching a client whose
|
|
156
|
+
* layers disagree with each other.
|
|
157
|
+
*
|
|
158
|
+
* A second caution: HTTP/3 fingerprinting is less settled than HTTP/2 in
|
|
159
|
+
* general. Anti-bot products have not caught up to HTTP/2-level detection
|
|
160
|
+
* here, and the parameters themselves are still moving.
|
|
161
|
+
*/
|
|
162
|
+
function h3Fingerprint(observed) {
|
|
163
|
+
observed = observed || {};
|
|
164
|
+
|
|
165
|
+
// Transport parameters SORTED by identifier, not left in the order sent.
|
|
166
|
+
//
|
|
167
|
+
// This is the opposite of the HTTP/2 section above, and measurement is why.
|
|
168
|
+
// Chrome shuffles its QUIC transport parameters between connections: two
|
|
169
|
+
// requests moments apart produced
|
|
170
|
+
//
|
|
171
|
+
// 5:6291456;9:103;15:L0;1:30000;17:0;3:1472;...
|
|
172
|
+
// 8:100;3:1472;17:0;6:6291456;4:15728640;...
|
|
173
|
+
//
|
|
174
|
+
// — same parameters, same values, different order. Hashing them as sent
|
|
175
|
+
// gives a fingerprint that never repeats, which is worse than no fingerprint
|
|
176
|
+
// at all because it looks like it works.
|
|
177
|
+
//
|
|
178
|
+
// The shuffling is deliberate, and the same defensive idea as GREASE: it
|
|
179
|
+
// stops anyone treating the order as fixed. So the order is noise here, and
|
|
180
|
+
// sorting removes it — exactly as JA4 sorts TLS extensions and JA3, to its
|
|
181
|
+
// cost, does not.
|
|
182
|
+
//
|
|
183
|
+
// Sorted NUMERICALLY, since these are numbers and a string sort would put
|
|
184
|
+
// 17 before 3.
|
|
185
|
+
var tp = Array.isArray(observed.transportParams) ? observed.transportParams : [];
|
|
186
|
+
var t = tp.slice()
|
|
187
|
+
.sort(function (a, b) { return Number(a.id) - Number(b.id); })
|
|
188
|
+
.map(function (x) { return x.id + ':' + x.value; })
|
|
189
|
+
.join(';');
|
|
190
|
+
|
|
191
|
+
// HTTP/3's own SETTINGS frame — a different thing from the QUIC transport
|
|
192
|
+
// parameters above: QPACK table size, blocked streams,
|
|
193
|
+
// ENABLE_CONNECT_PROTOCOL.
|
|
194
|
+
//
|
|
195
|
+
// Left in the order sent, because unlike the transport parameters it IS
|
|
196
|
+
// stable: the same Chrome produced 1:65536;6:262144;7:100;51:1 in that order
|
|
197
|
+
// across every connection observed. Sorting it would throw away signal that
|
|
198
|
+
// is really there.
|
|
199
|
+
var h3s = Array.isArray(observed.h3Settings) ? observed.h3Settings : [];
|
|
200
|
+
var h = h3s.map(function (x) { return x.id + ':' + x.value; }).join(';');
|
|
201
|
+
|
|
202
|
+
// The connection-level flow-control window the client opened, mirroring the
|
|
203
|
+
// WINDOW_UPDATE section of the HTTP/2 string. Chrome asks for 15MB over TCP
|
|
204
|
+
// and a comparable figure over QUIC; libraries keep the default and ask for
|
|
205
|
+
// nothing, which is its own signal.
|
|
206
|
+
var wu = observed.windowUpdate ? String(observed.windowUpdate) : '0';
|
|
207
|
+
|
|
208
|
+
// Pseudo-header order — the strongest section of the HTTP/2 fingerprint, and
|
|
209
|
+
// no weaker here. Chrome sends m,a,s,p, Firefox m,p,a,s, Safari m,s,p,a;
|
|
210
|
+
// the order is a constant in each browser's source and most HTTP libraries
|
|
211
|
+
// match none of them.
|
|
212
|
+
var ps = Array.isArray(observed.pseudoHeaderOrder) && observed.pseudoHeaderOrder.length
|
|
213
|
+
? observed.pseudoHeaderOrder.join(',')
|
|
214
|
+
: '0';
|
|
215
|
+
|
|
216
|
+
var raw = t + '|' + h + '|' + wu + '|' + ps;
|
|
217
|
+
return {
|
|
218
|
+
hash: sha12(raw),
|
|
219
|
+
md5: crypto.createHash('md5').update(raw, 'utf8').digest('hex'),
|
|
220
|
+
raw: raw,
|
|
221
|
+
tp: t, h3: h, wu: wu, ps: ps,
|
|
222
|
+
|
|
223
|
+
// GREASE stays OUTSIDE the hashed string, unlike the published HTTP/3
|
|
224
|
+
// captures that write a literal "GREASE" token at the position it was
|
|
225
|
+
// injected. Recording the position would be more informative, but only if
|
|
226
|
+
// the position is stable — and it is not: the same Chrome shuffles its
|
|
227
|
+
// transport parameters between connections, which is exactly why they are
|
|
228
|
+
// sorted above. A position that moves would put the noise straight back
|
|
229
|
+
// into the hash.
|
|
230
|
+
//
|
|
231
|
+
// So its PRESENCE is reported as a separate field, which is stable and is
|
|
232
|
+
// most of the signal: browsers send GREASE, most libraries do not.
|
|
233
|
+
grease: observed.grease ? '1' : '0'
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// ============================================================
|
|
239
|
+
// Collection
|
|
240
|
+
// ============================================================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Watch an H2Connection and accumulate what the fingerprint needs.
|
|
244
|
+
*
|
|
245
|
+
* Everything here arrives in the first flight, before any request, which is
|
|
246
|
+
* the point: the fingerprint exists before the server has processed a single
|
|
247
|
+
* byte of content and can be acted on before anything is served.
|
|
248
|
+
*
|
|
249
|
+
* Returns a getter; call it once a request has arrived, so the pseudo-header
|
|
250
|
+
* order is known.
|
|
251
|
+
*/
|
|
252
|
+
function observeH2(conn) {
|
|
253
|
+
var state = { settings: [], windowUpdate: 0, priorities: [], pseudoHeaderOrder: [] };
|
|
254
|
+
|
|
255
|
+
// Two arguments: the object keyed by name, and the ordered list. The list is
|
|
256
|
+
// the second one — the object cannot carry order, which is the whole point
|
|
257
|
+
// of this section.
|
|
258
|
+
conn.on('settings', function (settingsObj, settingsList) {
|
|
259
|
+
if (state.settings.length) return; // first SETTINGS only
|
|
260
|
+
var list = settingsList || [];
|
|
261
|
+
for (var i = 0; i < list.length; i++) {
|
|
262
|
+
state.settings.push({ id: list[i].id, value: list[i].value });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
conn.on('window_update', function (streamId, increment) {
|
|
267
|
+
// Stream 0 only — the connection-level window. Per-stream updates are
|
|
268
|
+
// ordinary flow control and say nothing about the implementation.
|
|
269
|
+
if (streamId === 0 && !state.windowUpdate) state.windowUpdate = increment;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
conn.on('priority', function (streamId, exclusive, dependsOn, weight) {
|
|
273
|
+
if (state.priorities.length >= 16) return; // bounded; a peer controls this
|
|
274
|
+
state.priorities.push({ streamId: streamId, exclusive: exclusive,
|
|
275
|
+
dependsOn: dependsOn, weight: weight });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
conn.on('pseudo_order', function (names) {
|
|
279
|
+
if (state.pseudoHeaderOrder.length) return; // first request only
|
|
280
|
+
state.pseudoHeaderOrder = pseudoHeaderOrder(names);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return function () { return h2Fingerprint(state); };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Watch an H3Connection and its QUIC connection, and accumulate what the
|
|
289
|
+
* HTTP/3 fingerprint needs.
|
|
290
|
+
*
|
|
291
|
+
* The mirror of observeH2, with the parts QUIC puts in different places: the
|
|
292
|
+
* transport parameters arrive inside the TLS ClientHello and are read from the
|
|
293
|
+
* QUIC connection, while the HTTP/3 SETTINGS arrive on the control stream.
|
|
294
|
+
*
|
|
295
|
+
* Returns a getter, callable once the handshake is complete.
|
|
296
|
+
*/
|
|
297
|
+
/**
|
|
298
|
+
* Is this a GREASE identifier?
|
|
299
|
+
*
|
|
300
|
+
* RFC 9287 reserves identifiers of the form 31 * N + 27 for QUIC, and RFC 9114
|
|
301
|
+
* uses the same construction for HTTP/3 settings. An endpoint sends one with a
|
|
302
|
+
* random identifier and a random value purely to check that the peer ignores
|
|
303
|
+
* what it does not recognise. Chrome does; most libraries do not.
|
|
304
|
+
*
|
|
305
|
+
* They MUST be stripped before hashing, for the same reason JA3 strips TLS
|
|
306
|
+
* GREASE: identifier and value are both random per connection, so keeping them
|
|
307
|
+
* yields a fingerprint that never repeats. What is kept is the single fact
|
|
308
|
+
* that GREASE was present, which is a stable property of the implementation —
|
|
309
|
+
* and, in practice, one of the cleaner browser-versus-library signals.
|
|
310
|
+
*
|
|
311
|
+
* The test is by MAGNITUDE, not by the 31N+27 formula, and that is deliberate.
|
|
312
|
+
* A QUIC varint holds up to 2^62-1, well past the 2^53 where a JavaScript
|
|
313
|
+
* number stops being exact — Chrome sent 4421552879925488000, which cannot be
|
|
314
|
+
* represented and arrives already rounded. Any modulo on it answers a question
|
|
315
|
+
* about the rounded value rather than the real one, and answers it wrongly.
|
|
316
|
+
*
|
|
317
|
+
* So: no real transport parameter or HTTP/3 setting is anywhere near that
|
|
318
|
+
* range. Every one currently defined fits in a couple of bytes, and the
|
|
319
|
+
* registry reserves the high space precisely so that GREASE can live there.
|
|
320
|
+
* Treating "absurdly large" as "reserved" is exact where the arithmetic
|
|
321
|
+
* cannot be.
|
|
322
|
+
*/
|
|
323
|
+
// Every identifier assigned by RFC 9000, RFC 9114 and their extensions is
|
|
324
|
+
// small: the transport parameters run to 0x20, the HTTP/3 settings to 0x33,
|
|
325
|
+
// and the registered extensions (datagrams, version negotiation) sit below
|
|
326
|
+
// 0x100. 4096 leaves an order of magnitude of headroom above anything real.
|
|
327
|
+
var GREASE_FLOOR = 0x1000; // 4096
|
|
328
|
+
|
|
329
|
+
function isGrease(id) {
|
|
330
|
+
var n = Number(id);
|
|
331
|
+
if (!isFinite(n)) return false;
|
|
332
|
+
|
|
333
|
+
// BY MAGNITUDE, not by the 31N+27 formula.
|
|
334
|
+
//
|
|
335
|
+
// The formula is what the RFCs describe for reserving the identifier space,
|
|
336
|
+
// and it is the wrong test to apply here. Measured against what Chrome
|
|
337
|
+
// actually sends — 12583, 12584, 41284154305, 4421552879925488000 — not one
|
|
338
|
+
// satisfies it; their residues mod 31 are 1, 2, 6 and 8. Whatever Chrome
|
|
339
|
+
// uses to pick a reserved identifier, it is not that formula, and a check
|
|
340
|
+
// built on it lets every one of them into the hash along with its random
|
|
341
|
+
// value.
|
|
342
|
+
//
|
|
343
|
+
// What IS true of all four, and of no assigned identifier, is that they are
|
|
344
|
+
// enormous. That is the property to test.
|
|
345
|
+
return n >= GREASE_FLOOR;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
function observeH3(h3, quic, existingState) {
|
|
350
|
+
// State may be handed in, because the two halves of an HTTP/3 connection
|
|
351
|
+
// become available at different times: the QUIC transport parameters arrive
|
|
352
|
+
// during the handshake, and the H3 control stream only afterwards. Passing
|
|
353
|
+
// the accumulated state forward lets a caller attach to QUIC early and to H3
|
|
354
|
+
// later without losing what the first pass collected.
|
|
355
|
+
var state = existingState ||
|
|
356
|
+
{ transportParams: [], h3Settings: [], alpn: 'h3', grease: false,
|
|
357
|
+
windowUpdate: 0, pseudoHeaderOrder: [] };
|
|
358
|
+
|
|
359
|
+
if (h3 && h3.on) {
|
|
360
|
+
h3.on('pseudo_order', function (names) {
|
|
361
|
+
if (state.pseudoHeaderOrder.length) return; // first request only
|
|
362
|
+
state.pseudoHeaderOrder = pseudoHeaderOrder(names);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
h3.on('peer_settings', function (ordered) {
|
|
366
|
+
if (state.h3Settings.length) return;
|
|
367
|
+
for (var i = 0; i < ordered.length; i++) {
|
|
368
|
+
if (isGrease(ordered[i].id)) { state.grease = true; continue; }
|
|
369
|
+
state.h3Settings.push({ id: ordered[i].id, value: ordered[i].value });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (quic && quic.on) {
|
|
375
|
+
quic.on('peer_max_data', function (bytes) {
|
|
376
|
+
// initial_max_data from the transport parameters: the connection-level
|
|
377
|
+
// window, and the QUIC equivalent of an HTTP/2 WINDOW_UPDATE on stream 0.
|
|
378
|
+
if (!state.windowUpdate) state.windowUpdate = bytes;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
quic.on('peer_transport_params', function (params) {
|
|
382
|
+
if (state.transportParams.length) return;
|
|
383
|
+
for (var k = 0; k < (params || []).length; k++) {
|
|
384
|
+
if (isGrease(params[k].id)) { state.grease = true; continue; }
|
|
385
|
+
// 0x04 is initial_max_data — the connection-level window. Captured
|
|
386
|
+
// into its own section as well as the parameter list, so the string
|
|
387
|
+
// has the same four-part shape as the HTTP/2 one.
|
|
388
|
+
if (Number(params[k].id) === 0x04 && !state.windowUpdate) {
|
|
389
|
+
state.windowUpdate = params[k].value;
|
|
390
|
+
}
|
|
391
|
+
state.transportParams.push({ id: params[k].id, value: params[k].value });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
var getter = function () { return h3Fingerprint(state); };
|
|
397
|
+
// Exposed so a second call can continue accumulating into the same object.
|
|
398
|
+
getter.state = state;
|
|
399
|
+
return getter;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
export { h2Fingerprint, h3Fingerprint, observeH2, observeH3, pseudoHeaderOrder, isGrease };
|