quico 0.3.0 → 0.7.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 +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -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 +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/h2_frames.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
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
|
+
// HTTP/2 framing (RFC 9113 §4-§6).
|
|
18
|
+
//
|
|
19
|
+
// Pure codec: bytes in, frame objects out, no connection state. The state
|
|
20
|
+
// machine, flow control and HPACK live in h2.js, exactly as h1.js is a parser
|
|
21
|
+
// and h1_server is the thing that owns a socket.
|
|
22
|
+
//
|
|
23
|
+
// Every frame is a 9-byte header followed by a payload:
|
|
24
|
+
//
|
|
25
|
+
// +-----------------------------------------------+
|
|
26
|
+
// | Length (24) |
|
|
27
|
+
// +---------------+---------------+---------------+
|
|
28
|
+
// | Type (8) | Flags (8) |
|
|
29
|
+
// +-+-------------+---------------+-------------------------------+
|
|
30
|
+
// |R| Stream Identifier (31) |
|
|
31
|
+
// +=+=============================================================+
|
|
32
|
+
// | Frame Payload (0...) ...
|
|
33
|
+
//
|
|
34
|
+
// Note the R bit: the stream id is 31 bits, not 32. Reading it as a uint32
|
|
35
|
+
// makes every id with the high bit set enormous and negative-ish, and RFC
|
|
36
|
+
// 9113 §4.1 says a receiver MUST ignore that bit rather than reject the
|
|
37
|
+
// frame — so it is masked off here, once, for every frame type.
|
|
38
|
+
// ============================================================
|
|
39
|
+
|
|
40
|
+
var FRAME_HEADER_SIZE = 9;
|
|
41
|
+
|
|
42
|
+
var TYPE = {
|
|
43
|
+
DATA: 0x0,
|
|
44
|
+
HEADERS: 0x1,
|
|
45
|
+
PRIORITY: 0x2,
|
|
46
|
+
RST_STREAM: 0x3,
|
|
47
|
+
SETTINGS: 0x4,
|
|
48
|
+
PUSH_PROMISE: 0x5,
|
|
49
|
+
PING: 0x6,
|
|
50
|
+
GOAWAY: 0x7,
|
|
51
|
+
WINDOW_UPDATE: 0x8,
|
|
52
|
+
CONTINUATION: 0x9,
|
|
53
|
+
// RFC 9218 §7.1: a stream's priority after its request, client to server,
|
|
54
|
+
// on stream 0 — the scheme that replaced PRIORITY (RFC 9113 §5.3.2).
|
|
55
|
+
PRIORITY_UPDATE: 0x10
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
var TYPE_NAME = {};
|
|
59
|
+
for (var _t in TYPE) TYPE_NAME[TYPE[_t]] = _t;
|
|
60
|
+
|
|
61
|
+
var FLAG = {
|
|
62
|
+
END_STREAM: 0x01, // DATA, HEADERS
|
|
63
|
+
ACK: 0x01, // SETTINGS, PING — same bit, different meaning
|
|
64
|
+
END_HEADERS: 0x04, // HEADERS, PUSH_PROMISE, CONTINUATION
|
|
65
|
+
PADDED: 0x08, // DATA, HEADERS, PUSH_PROMISE
|
|
66
|
+
PRIORITY: 0x20 // HEADERS
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// RFC 9113 §7.
|
|
70
|
+
var ERROR = {
|
|
71
|
+
NO_ERROR: 0x0,
|
|
72
|
+
PROTOCOL_ERROR: 0x1,
|
|
73
|
+
INTERNAL_ERROR: 0x2,
|
|
74
|
+
FLOW_CONTROL_ERROR: 0x3,
|
|
75
|
+
SETTINGS_TIMEOUT: 0x4,
|
|
76
|
+
STREAM_CLOSED: 0x5,
|
|
77
|
+
FRAME_SIZE_ERROR: 0x6,
|
|
78
|
+
REFUSED_STREAM: 0x7,
|
|
79
|
+
CANCEL: 0x8,
|
|
80
|
+
COMPRESSION_ERROR: 0x9,
|
|
81
|
+
CONNECT_ERROR: 0xa,
|
|
82
|
+
ENHANCE_YOUR_CALM: 0xb,
|
|
83
|
+
INADEQUATE_SECURITY: 0xc,
|
|
84
|
+
HTTP_1_1_REQUIRED: 0xd
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
var ERROR_NAME = {};
|
|
88
|
+
for (var _e in ERROR) ERROR_NAME[ERROR[_e]] = _e;
|
|
89
|
+
|
|
90
|
+
// RFC 9113 §6.5.2.
|
|
91
|
+
var SETTINGS = {
|
|
92
|
+
HEADER_TABLE_SIZE: 0x1,
|
|
93
|
+
ENABLE_PUSH: 0x2,
|
|
94
|
+
MAX_CONCURRENT_STREAMS: 0x3,
|
|
95
|
+
INITIAL_WINDOW_SIZE: 0x4,
|
|
96
|
+
MAX_FRAME_SIZE: 0x5,
|
|
97
|
+
MAX_HEADER_LIST_SIZE: 0x6,
|
|
98
|
+
// RFC 8441 §3. A server that sends this with the value 1 is telling clients
|
|
99
|
+
// they may use extended CONNECT — a CONNECT request carrying a `:protocol`
|
|
100
|
+
// pseudo-header — which is how WebSocket runs over HTTP/2. Without it, a
|
|
101
|
+
// browser will not even try, and opens a separate HTTP/1.1 connection
|
|
102
|
+
// instead.
|
|
103
|
+
ENABLE_CONNECT_PROTOCOL: 0x8,
|
|
104
|
+
// RFC 9218 §2.1: 1 — this endpoint ignores RFC 7540's priority tree (as
|
|
105
|
+
// RFC 9113 lets it), and reads RFC 9218's signals instead.
|
|
106
|
+
NO_RFC7540_PRIORITIES: 0x9
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
var SETTINGS_NAME = {};
|
|
110
|
+
for (var _s in SETTINGS) SETTINGS_NAME[SETTINGS[_s]] = _s;
|
|
111
|
+
|
|
112
|
+
// The connection preface a client sends before anything else (§3.4). It is
|
|
113
|
+
// deliberately something an HTTP/1.1 server cannot mistake for a request, so
|
|
114
|
+
// that speaking h2 to a server that does not know it fails immediately rather
|
|
115
|
+
// than being half-understood.
|
|
116
|
+
var PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'latin1');
|
|
117
|
+
|
|
118
|
+
var MAX_WINDOW = 2147483647; // 2^31 - 1
|
|
119
|
+
var DEFAULT_MAX_FRAME_SIZE = 16384; // §6.5.2, also the minimum allowed
|
|
120
|
+
var MAX_ALLOWED_FRAME_SIZE = 16777215;
|
|
121
|
+
var DEFAULT_WINDOW_SIZE = 65535;
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A protocol failure.
|
|
126
|
+
*
|
|
127
|
+
* `stream` distinguishes the two kinds RFC 9113 §5.4 defines and never mixes:
|
|
128
|
+
* a CONNECTION error (stream === 0) ends everything with GOAWAY, a STREAM
|
|
129
|
+
* error resets one stream with RST_STREAM and leaves the rest running.
|
|
130
|
+
* Choosing wrongly in either direction is a real bug — tearing down a
|
|
131
|
+
* connection over one malformed request is a denial of service against the
|
|
132
|
+
* other streams on it, and continuing after a framing error means every
|
|
133
|
+
* subsequent frame is read at the wrong offset.
|
|
134
|
+
*/
|
|
135
|
+
function h2Error(message, code, streamId) {
|
|
136
|
+
var err = new Error(message);
|
|
137
|
+
err.code = 'H2_' + (ERROR_NAME[code] || 'PROTOCOL_ERROR');
|
|
138
|
+
err.h2Code = code === undefined ? ERROR.PROTOCOL_ERROR : code;
|
|
139
|
+
err.streamId = streamId || 0;
|
|
140
|
+
return err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
// ============================================================
|
|
145
|
+
// Decoding
|
|
146
|
+
// ============================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Read one frame from `buf` at `offset`.
|
|
150
|
+
*
|
|
151
|
+
* Returns null when the buffer does not yet hold a whole frame — a partial
|
|
152
|
+
* frame is the normal state of affairs on a TCP stream, not an error.
|
|
153
|
+
* Throws h2Error for anything malformed.
|
|
154
|
+
*/
|
|
155
|
+
function readFrame(buf, offset, maxFrameSize) {
|
|
156
|
+
offset = offset || 0;
|
|
157
|
+
maxFrameSize = maxFrameSize || DEFAULT_MAX_FRAME_SIZE;
|
|
158
|
+
|
|
159
|
+
if (buf.length - offset < FRAME_HEADER_SIZE) return null;
|
|
160
|
+
|
|
161
|
+
var length = (buf[offset] << 16) | (buf[offset + 1] << 8) | buf[offset + 2];
|
|
162
|
+
var type = buf[offset + 3];
|
|
163
|
+
var flags = buf[offset + 4];
|
|
164
|
+
// Mask the reserved bit (§4.1).
|
|
165
|
+
var streamId = buf.readUInt32BE(offset + 5) & 0x7fffffff;
|
|
166
|
+
|
|
167
|
+
if (length > maxFrameSize) {
|
|
168
|
+
// §4.2. Checked before waiting for the payload: otherwise a peer could
|
|
169
|
+
// announce a 16 MB frame and make us buffer it before we objected.
|
|
170
|
+
throw h2Error('frame of ' + length + ' bytes exceeds SETTINGS_MAX_FRAME_SIZE ' +
|
|
171
|
+
maxFrameSize, ERROR.FRAME_SIZE_ERROR);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (buf.length - offset < FRAME_HEADER_SIZE + length) return null;
|
|
175
|
+
|
|
176
|
+
var payload = buf.subarray(offset + FRAME_HEADER_SIZE, offset + FRAME_HEADER_SIZE + length);
|
|
177
|
+
var frame;
|
|
178
|
+
try {
|
|
179
|
+
frame = parsePayload(type, flags, streamId, payload);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// The frame is whole, so its extent is known: a caller that answers a
|
|
182
|
+
// STREAM error with RST_STREAM skips exactly this frame and reads on.
|
|
183
|
+
err.consumed = FRAME_HEADER_SIZE + length;
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
frame.length = length;
|
|
187
|
+
frame.consumed = FRAME_HEADER_SIZE + length;
|
|
188
|
+
return frame;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
function parsePayload(type, flags, streamId, payload) {
|
|
193
|
+
var frame = {
|
|
194
|
+
type: type,
|
|
195
|
+
typeName: TYPE_NAME[type] || ('UNKNOWN_0x' + type.toString(16)),
|
|
196
|
+
flags: flags,
|
|
197
|
+
streamId: streamId
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
switch (type) {
|
|
201
|
+
|
|
202
|
+
case TYPE.DATA: {
|
|
203
|
+
if (streamId === 0) {
|
|
204
|
+
throw h2Error('DATA on stream 0', ERROR.PROTOCOL_ERROR);
|
|
205
|
+
}
|
|
206
|
+
var d = stripPadding(payload, flags);
|
|
207
|
+
frame.data = d;
|
|
208
|
+
frame.endStream = (flags & FLAG.END_STREAM) !== 0;
|
|
209
|
+
return frame;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case TYPE.HEADERS: {
|
|
213
|
+
if (streamId === 0) {
|
|
214
|
+
throw h2Error('HEADERS on stream 0', ERROR.PROTOCOL_ERROR);
|
|
215
|
+
}
|
|
216
|
+
var p = stripPadding(payload, flags);
|
|
217
|
+
var pos = 0;
|
|
218
|
+
if (flags & FLAG.PRIORITY) {
|
|
219
|
+
if (p.length < 5) {
|
|
220
|
+
throw h2Error('HEADERS with PRIORITY is shorter than 5 bytes',
|
|
221
|
+
ERROR.FRAME_SIZE_ERROR);
|
|
222
|
+
}
|
|
223
|
+
var dep = p.readUInt32BE(0);
|
|
224
|
+
frame.exclusive = (dep & 0x80000000) !== 0;
|
|
225
|
+
frame.dependency = dep & 0x7fffffff;
|
|
226
|
+
frame.weight = p[4] + 1;
|
|
227
|
+
// §5.3.1: a stream cannot depend on itself. RFC 9113 deprecates
|
|
228
|
+
// priority signalling but keeps this as a stream error.
|
|
229
|
+
if (frame.dependency === streamId) {
|
|
230
|
+
throw h2Error('stream depends on itself', ERROR.PROTOCOL_ERROR, streamId);
|
|
231
|
+
}
|
|
232
|
+
pos = 5;
|
|
233
|
+
}
|
|
234
|
+
frame.headerBlock = p.subarray(pos);
|
|
235
|
+
frame.endStream = (flags & FLAG.END_STREAM) !== 0;
|
|
236
|
+
frame.endHeaders = (flags & FLAG.END_HEADERS) !== 0;
|
|
237
|
+
return frame;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case TYPE.PRIORITY: {
|
|
241
|
+
if (streamId === 0) throw h2Error('PRIORITY on stream 0', ERROR.PROTOCOL_ERROR);
|
|
242
|
+
// A stream error, not a connection error (§6.3) — the frame is
|
|
243
|
+
// self-contained, so the connection is still readable.
|
|
244
|
+
if (payload.length !== 5) {
|
|
245
|
+
throw h2Error('PRIORITY payload must be 5 bytes', ERROR.FRAME_SIZE_ERROR, streamId);
|
|
246
|
+
}
|
|
247
|
+
var dep2 = payload.readUInt32BE(0);
|
|
248
|
+
frame.exclusive = (dep2 & 0x80000000) !== 0;
|
|
249
|
+
frame.dependency = dep2 & 0x7fffffff;
|
|
250
|
+
frame.weight = payload[4] + 1;
|
|
251
|
+
if (frame.dependency === streamId) {
|
|
252
|
+
throw h2Error('stream depends on itself', ERROR.PROTOCOL_ERROR, streamId);
|
|
253
|
+
}
|
|
254
|
+
return frame;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case TYPE.RST_STREAM: {
|
|
258
|
+
if (streamId === 0) throw h2Error('RST_STREAM on stream 0', ERROR.PROTOCOL_ERROR);
|
|
259
|
+
if (payload.length !== 4) {
|
|
260
|
+
throw h2Error('RST_STREAM payload must be 4 bytes', ERROR.FRAME_SIZE_ERROR);
|
|
261
|
+
}
|
|
262
|
+
frame.errorCode = payload.readUInt32BE(0);
|
|
263
|
+
return frame;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case TYPE.SETTINGS: {
|
|
267
|
+
if (streamId !== 0) throw h2Error('SETTINGS on a non-zero stream', ERROR.PROTOCOL_ERROR);
|
|
268
|
+
frame.ack = (flags & FLAG.ACK) !== 0;
|
|
269
|
+
if (frame.ack) {
|
|
270
|
+
// §6.5: an ACK carries no settings. A payload here means the peer is
|
|
271
|
+
// confused about which direction this frame is going.
|
|
272
|
+
if (payload.length !== 0) {
|
|
273
|
+
throw h2Error('SETTINGS ACK must have an empty payload', ERROR.FRAME_SIZE_ERROR);
|
|
274
|
+
}
|
|
275
|
+
frame.settings = {};
|
|
276
|
+
return frame;
|
|
277
|
+
}
|
|
278
|
+
if (payload.length % 6 !== 0) {
|
|
279
|
+
throw h2Error('SETTINGS payload is not a multiple of 6', ERROR.FRAME_SIZE_ERROR);
|
|
280
|
+
}
|
|
281
|
+
var settings = {};
|
|
282
|
+
// The same parameters again as an ORDERED list, including the ones
|
|
283
|
+
// dropped above. Both forms are needed and neither substitutes for the
|
|
284
|
+
// other: the protocol wants a lookup by name, and fingerprinting wants
|
|
285
|
+
// the sequence exactly as sent — which parameters appear, in which
|
|
286
|
+
// order, and which are absent. Unknown identifiers are kept here for the
|
|
287
|
+
// same reason: an unrecognised setting is still a fact about the peer.
|
|
288
|
+
var settingsList = [];
|
|
289
|
+
for (var i = 0; i + 6 <= payload.length; i += 6) {
|
|
290
|
+
var id = payload.readUInt16BE(i);
|
|
291
|
+
var value = payload.readUInt32BE(i + 2);
|
|
292
|
+
validateSetting(id, value);
|
|
293
|
+
settingsList.push({ id: id, value: value });
|
|
294
|
+
// Unknown identifiers must be IGNORED (§6.5.2), not rejected: that is
|
|
295
|
+
// the extension point the whole protocol relies on for future settings.
|
|
296
|
+
if (SETTINGS_NAME[id]) settings[SETTINGS_NAME[id]] = value;
|
|
297
|
+
}
|
|
298
|
+
frame.settings = settings;
|
|
299
|
+
frame.settingsList = settingsList;
|
|
300
|
+
return frame;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case TYPE.PUSH_PROMISE: {
|
|
304
|
+
if (streamId === 0) throw h2Error('PUSH_PROMISE on stream 0', ERROR.PROTOCOL_ERROR);
|
|
305
|
+
var pp = stripPadding(payload, flags);
|
|
306
|
+
if (pp.length < 4) {
|
|
307
|
+
throw h2Error('PUSH_PROMISE is shorter than 4 bytes', ERROR.FRAME_SIZE_ERROR);
|
|
308
|
+
}
|
|
309
|
+
frame.promisedStreamId = pp.readUInt32BE(0) & 0x7fffffff;
|
|
310
|
+
frame.headerBlock = pp.subarray(4);
|
|
311
|
+
frame.endHeaders = (flags & FLAG.END_HEADERS) !== 0;
|
|
312
|
+
return frame;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case TYPE.PING: {
|
|
316
|
+
if (streamId !== 0) throw h2Error('PING on a non-zero stream', ERROR.PROTOCOL_ERROR);
|
|
317
|
+
if (payload.length !== 8) {
|
|
318
|
+
throw h2Error('PING payload must be 8 bytes', ERROR.FRAME_SIZE_ERROR);
|
|
319
|
+
}
|
|
320
|
+
frame.ack = (flags & FLAG.ACK) !== 0;
|
|
321
|
+
frame.payload = Buffer.from(payload);
|
|
322
|
+
return frame;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case TYPE.GOAWAY: {
|
|
326
|
+
if (streamId !== 0) throw h2Error('GOAWAY on a non-zero stream', ERROR.PROTOCOL_ERROR);
|
|
327
|
+
if (payload.length < 8) {
|
|
328
|
+
throw h2Error('GOAWAY is shorter than 8 bytes', ERROR.FRAME_SIZE_ERROR);
|
|
329
|
+
}
|
|
330
|
+
frame.lastStreamId = payload.readUInt32BE(0) & 0x7fffffff;
|
|
331
|
+
frame.errorCode = payload.readUInt32BE(4);
|
|
332
|
+
frame.debugData = Buffer.from(payload.subarray(8));
|
|
333
|
+
return frame;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
case TYPE.WINDOW_UPDATE: {
|
|
337
|
+
if (payload.length !== 4) {
|
|
338
|
+
throw h2Error('WINDOW_UPDATE payload must be 4 bytes', ERROR.FRAME_SIZE_ERROR);
|
|
339
|
+
}
|
|
340
|
+
frame.increment = payload.readUInt32BE(0) & 0x7fffffff;
|
|
341
|
+
if (frame.increment === 0) {
|
|
342
|
+
// §6.9. On a stream it is a stream error; on the connection it ends
|
|
343
|
+
// everything, because a zero increment can only be a sender that has
|
|
344
|
+
// lost track of its own window.
|
|
345
|
+
throw h2Error('WINDOW_UPDATE increment of 0', ERROR.PROTOCOL_ERROR, streamId);
|
|
346
|
+
}
|
|
347
|
+
return frame;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case TYPE.PRIORITY_UPDATE: {
|
|
351
|
+
// RFC 9218 §7.1: on stream 0 only, and naming a stream.
|
|
352
|
+
if (streamId !== 0) throw h2Error('PRIORITY_UPDATE on stream ' + streamId, ERROR.PROTOCOL_ERROR);
|
|
353
|
+
if (payload.length < 4) throw h2Error('PRIORITY_UPDATE shorter than 4 bytes', ERROR.FRAME_SIZE_ERROR);
|
|
354
|
+
frame.prioritizedStreamId = payload.readUInt32BE(0) & 0x7fffffff;
|
|
355
|
+
if (frame.prioritizedStreamId === 0) throw h2Error('PRIORITY_UPDATE for stream 0', ERROR.PROTOCOL_ERROR);
|
|
356
|
+
frame.priorityField = payload.subarray(4).toString('latin1');
|
|
357
|
+
return frame;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
case TYPE.CONTINUATION: {
|
|
361
|
+
if (streamId === 0) throw h2Error('CONTINUATION on stream 0', ERROR.PROTOCOL_ERROR);
|
|
362
|
+
frame.headerBlock = payload;
|
|
363
|
+
frame.endHeaders = (flags & FLAG.END_HEADERS) !== 0;
|
|
364
|
+
return frame;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
default:
|
|
368
|
+
// §4.1: a frame of unknown type MUST be ignored and discarded. Another
|
|
369
|
+
// extension point — rejecting it would break every future frame type.
|
|
370
|
+
frame.unknown = true;
|
|
371
|
+
frame.payload = Buffer.from(payload);
|
|
372
|
+
return frame;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Remove the padding a PADDED frame carries (§6.1).
|
|
379
|
+
*
|
|
380
|
+
* The length byte counts padding AFTER itself, so a value >= the remaining
|
|
381
|
+
* length means the padding would consume the real payload and then some. §6.1
|
|
382
|
+
* calls that a connection error, and it is the shape of an attempt to make two
|
|
383
|
+
* implementations disagree about where the payload ends.
|
|
384
|
+
*/
|
|
385
|
+
function stripPadding(payload, flags) {
|
|
386
|
+
if ((flags & FLAG.PADDED) === 0) return payload;
|
|
387
|
+
if (payload.length < 1) {
|
|
388
|
+
throw h2Error('PADDED frame with no padding length', ERROR.PROTOCOL_ERROR);
|
|
389
|
+
}
|
|
390
|
+
var padLength = payload[0];
|
|
391
|
+
if (padLength >= payload.length) {
|
|
392
|
+
throw h2Error('padding length ' + padLength + ' is not smaller than the payload',
|
|
393
|
+
ERROR.PROTOCOL_ERROR);
|
|
394
|
+
}
|
|
395
|
+
return payload.subarray(1, payload.length - padLength);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
function validateSetting(id, value) {
|
|
400
|
+
switch (id) {
|
|
401
|
+
case SETTINGS.ENABLE_PUSH:
|
|
402
|
+
if (value !== 0 && value !== 1) {
|
|
403
|
+
throw h2Error('SETTINGS_ENABLE_PUSH must be 0 or 1', ERROR.PROTOCOL_ERROR);
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case SETTINGS.ENABLE_CONNECT_PROTOCOL:
|
|
407
|
+
// RFC 8441 §3: 0 or 1, and once advertised as 1 it may never go back to
|
|
408
|
+
// 0 — a peer that has started using extended CONNECT cannot be told to
|
|
409
|
+
// stop mid-connection. The one-way rule is enforced in h2.js, which is
|
|
410
|
+
// where the previous value is known.
|
|
411
|
+
if (value !== 0 && value !== 1) {
|
|
412
|
+
throw h2Error('SETTINGS_ENABLE_CONNECT_PROTOCOL must be 0 or 1', ERROR.PROTOCOL_ERROR);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case SETTINGS.NO_RFC7540_PRIORITIES:
|
|
416
|
+
if (value !== 0 && value !== 1) {
|
|
417
|
+
throw h2Error('SETTINGS_NO_RFC7540_PRIORITIES must be 0 or 1', ERROR.PROTOCOL_ERROR);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case SETTINGS.INITIAL_WINDOW_SIZE:
|
|
421
|
+
if (value > MAX_WINDOW) {
|
|
422
|
+
throw h2Error('SETTINGS_INITIAL_WINDOW_SIZE above 2^31-1',
|
|
423
|
+
ERROR.FLOW_CONTROL_ERROR);
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
case SETTINGS.MAX_FRAME_SIZE:
|
|
427
|
+
if (value < DEFAULT_MAX_FRAME_SIZE || value > MAX_ALLOWED_FRAME_SIZE) {
|
|
428
|
+
throw h2Error('SETTINGS_MAX_FRAME_SIZE outside 16384..16777215',
|
|
429
|
+
ERROR.PROTOCOL_ERROR);
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
// ============================================================
|
|
437
|
+
// Encoding
|
|
438
|
+
// ============================================================
|
|
439
|
+
|
|
440
|
+
function frameHeader(length, type, flags, streamId) {
|
|
441
|
+
var h = Buffer.allocUnsafe(FRAME_HEADER_SIZE);
|
|
442
|
+
h[0] = (length >>> 16) & 0xff;
|
|
443
|
+
h[1] = (length >>> 8) & 0xff;
|
|
444
|
+
h[2] = length & 0xff;
|
|
445
|
+
h[3] = type;
|
|
446
|
+
h[4] = flags;
|
|
447
|
+
h.writeUInt32BE(streamId & 0x7fffffff, 5);
|
|
448
|
+
return h;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function buildFrame(type, flags, streamId, payload) {
|
|
452
|
+
payload = payload || Buffer.alloc(0);
|
|
453
|
+
return Buffer.concat([frameHeader(payload.length, type, flags, streamId), payload]);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildData(streamId, data, endStream) {
|
|
457
|
+
return buildFrame(TYPE.DATA, endStream ? FLAG.END_STREAM : 0, streamId, data);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildHeaders(streamId, headerBlock, options) {
|
|
461
|
+
options = options || {};
|
|
462
|
+
var flags = 0;
|
|
463
|
+
if (options.endStream) flags |= FLAG.END_STREAM;
|
|
464
|
+
if (options.endHeaders !== false) flags |= FLAG.END_HEADERS;
|
|
465
|
+
return buildFrame(TYPE.HEADERS, flags, streamId, headerBlock);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function buildContinuation(streamId, headerBlock, endHeaders) {
|
|
469
|
+
return buildFrame(TYPE.CONTINUATION, endHeaders ? FLAG.END_HEADERS : 0,
|
|
470
|
+
streamId, headerBlock);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function buildRstStream(streamId, errorCode) {
|
|
474
|
+
var p = Buffer.allocUnsafe(4);
|
|
475
|
+
p.writeUInt32BE(errorCode >>> 0, 0);
|
|
476
|
+
return buildFrame(TYPE.RST_STREAM, 0, streamId, p);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function buildSettings(settings, ack) {
|
|
480
|
+
if (ack) return buildFrame(TYPE.SETTINGS, FLAG.ACK, 0, Buffer.alloc(0));
|
|
481
|
+
var names = Object.keys(settings || {});
|
|
482
|
+
var p = Buffer.allocUnsafe(names.length * 6);
|
|
483
|
+
var off = 0;
|
|
484
|
+
for (var i = 0; i < names.length; i++) {
|
|
485
|
+
var id = SETTINGS[names[i]];
|
|
486
|
+
if (id === undefined) continue;
|
|
487
|
+
p.writeUInt16BE(id, off);
|
|
488
|
+
p.writeUInt32BE(settings[names[i]] >>> 0, off + 2);
|
|
489
|
+
off += 6;
|
|
490
|
+
}
|
|
491
|
+
return buildFrame(TYPE.SETTINGS, 0, 0, p.subarray(0, off));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function buildPing(payload, ack) {
|
|
495
|
+
var p = Buffer.alloc(8);
|
|
496
|
+
if (payload) Buffer.from(payload).copy(p, 0, 0, Math.min(8, payload.length));
|
|
497
|
+
return buildFrame(TYPE.PING, ack ? FLAG.ACK : 0, 0, p);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function buildGoaway(lastStreamId, errorCode, debugData) {
|
|
501
|
+
var d = debugData ? Buffer.from(debugData) : Buffer.alloc(0);
|
|
502
|
+
var p = Buffer.allocUnsafe(8 + d.length);
|
|
503
|
+
p.writeUInt32BE(lastStreamId & 0x7fffffff, 0);
|
|
504
|
+
p.writeUInt32BE(errorCode >>> 0, 4);
|
|
505
|
+
d.copy(p, 8);
|
|
506
|
+
return buildFrame(TYPE.GOAWAY, 0, 0, p);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// RFC 9218 §7.1: the stream, then its Priority Field Value.
|
|
510
|
+
function buildPriorityUpdate(streamId, field) {
|
|
511
|
+
var f = Buffer.from(field || '', 'latin1');
|
|
512
|
+
var p = Buffer.allocUnsafe(4 + f.length);
|
|
513
|
+
p.writeUInt32BE(streamId & 0x7fffffff, 0);
|
|
514
|
+
f.copy(p, 4);
|
|
515
|
+
return buildFrame(TYPE.PRIORITY_UPDATE, 0, 0, p);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildWindowUpdate(streamId, increment) {
|
|
519
|
+
var p = Buffer.allocUnsafe(4);
|
|
520
|
+
p.writeUInt32BE(increment & 0x7fffffff, 0);
|
|
521
|
+
return buildFrame(TYPE.WINDOW_UPDATE, 0, streamId, p);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Split a header block into HEADERS + as many CONTINUATION frames as needed.
|
|
526
|
+
*
|
|
527
|
+
* A header block larger than MAX_FRAME_SIZE is ordinary — a request with a big
|
|
528
|
+
* cookie reaches it easily — and the ONLY legal way to send it is to fragment.
|
|
529
|
+
* The fragments must be contiguous on the connection with no other frame
|
|
530
|
+
* between them (§6.10), which is why this returns them together rather than
|
|
531
|
+
* letting a caller interleave.
|
|
532
|
+
*/
|
|
533
|
+
function buildHeaderFrames(streamId, headerBlock, options, maxFrameSize) {
|
|
534
|
+
options = options || {};
|
|
535
|
+
maxFrameSize = maxFrameSize || DEFAULT_MAX_FRAME_SIZE;
|
|
536
|
+
|
|
537
|
+
if (headerBlock.length <= maxFrameSize) {
|
|
538
|
+
return [buildHeaders(streamId, headerBlock, { endStream: options.endStream, endHeaders: true })];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
var out = [];
|
|
542
|
+
out.push(buildHeaders(streamId, headerBlock.subarray(0, maxFrameSize), {
|
|
543
|
+
endStream: options.endStream,
|
|
544
|
+
endHeaders: false
|
|
545
|
+
}));
|
|
546
|
+
var pos = maxFrameSize;
|
|
547
|
+
while (pos < headerBlock.length) {
|
|
548
|
+
var end = Math.min(pos + maxFrameSize, headerBlock.length);
|
|
549
|
+
out.push(buildContinuation(streamId, headerBlock.subarray(pos, end),
|
|
550
|
+
end >= headerBlock.length));
|
|
551
|
+
pos = end;
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
export {
|
|
558
|
+
FRAME_HEADER_SIZE,
|
|
559
|
+
TYPE, TYPE_NAME,
|
|
560
|
+
FLAG,
|
|
561
|
+
ERROR, ERROR_NAME,
|
|
562
|
+
SETTINGS, SETTINGS_NAME,
|
|
563
|
+
PREFACE,
|
|
564
|
+
MAX_WINDOW,
|
|
565
|
+
DEFAULT_MAX_FRAME_SIZE,
|
|
566
|
+
MAX_ALLOWED_FRAME_SIZE,
|
|
567
|
+
DEFAULT_WINDOW_SIZE,
|
|
568
|
+
h2Error,
|
|
569
|
+
readFrame,
|
|
570
|
+
buildFrame,
|
|
571
|
+
frameHeader,
|
|
572
|
+
buildData,
|
|
573
|
+
buildHeaders,
|
|
574
|
+
buildContinuation,
|
|
575
|
+
buildRstStream,
|
|
576
|
+
buildSettings,
|
|
577
|
+
buildPing,
|
|
578
|
+
buildGoaway,
|
|
579
|
+
buildWindowUpdate,
|
|
580
|
+
buildPriorityUpdate,
|
|
581
|
+
buildHeaderFrames
|
|
582
|
+
};
|