tunnelfetch 1.0.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.
Files changed (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,129 @@
1
+ // HTTP/2 wire constants (RFC 9113), and the deliberately narrow client posture on top of them.
2
+ //
3
+ // This is a client that opens streams and reads responses. It never receives a request, never
4
+ // serves a push, and never prioritises — so the constants for those exist only to NAME what we
5
+ // refuse when a peer sends them, not to act on them.
6
+ //
7
+ // The SETTINGS and window values below are not defaults chosen for taste. They are the exact
8
+ // values curl 8.7.1 / nghttp2 1.69.0 puts on the wire, captured over a real ALPN-negotiated h2
9
+ // handshake. That matters empirically, not aesthetically: the whole reason to implement h2 here
10
+ // is that some sites challenge HTTP/1.1 as a bot signal while letting curl's h2 through, and a
11
+ // naive h2 fingerprint can fail exactly where curl's succeeds. Matching the SETTINGS frame, the
12
+ // initial window sizes, the connection WINDOW_UPDATE, and the pseudo-header order removes the
13
+ // cheapest tells. See the capture note beside `preferredSettingsOrder`.
14
+
15
+ /** The 24-byte client connection preface (RFC 9113 s3.4). Sent before any frame. */
16
+ export const CONNECTION_PREFACE = Uint8Array.from(
17
+ 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n',
18
+ (c) => c.charCodeAt(0),
19
+ );
20
+
21
+ /** Frame types (RFC 9113 s6). PRIORITY and PUSH_PROMISE are named to reject, not to honour. */
22
+ export const FRAME = {
23
+ DATA: 0x0,
24
+ HEADERS: 0x1,
25
+ PRIORITY: 0x2,
26
+ RST_STREAM: 0x3,
27
+ SETTINGS: 0x4,
28
+ PUSH_PROMISE: 0x5,
29
+ PING: 0x6,
30
+ GOAWAY: 0x7,
31
+ WINDOW_UPDATE: 0x8,
32
+ CONTINUATION: 0x9,
33
+ };
34
+
35
+ /** Reverse map for diagnostics: an unknown type still gets a number. */
36
+ export const FRAME_NAME = Object.fromEntries(Object.entries(FRAME).map(([k, v]) => [v, k]));
37
+
38
+ /** Frame flags (RFC 9113 s6). The same bit means different things per frame type. */
39
+ export const FLAG = {
40
+ END_STREAM: 0x1, // DATA, HEADERS
41
+ ACK: 0x1, // SETTINGS, PING (same bit, different frames)
42
+ END_HEADERS: 0x4, // HEADERS, CONTINUATION, PUSH_PROMISE
43
+ PADDED: 0x8, // DATA, HEADERS, PUSH_PROMISE
44
+ PRIORITY: 0x20, // HEADERS
45
+ };
46
+
47
+ /** SETTINGS parameter identifiers (RFC 9113 s6.5.2). */
48
+ export const SETTINGS = {
49
+ HEADER_TABLE_SIZE: 0x1,
50
+ ENABLE_PUSH: 0x2,
51
+ MAX_CONCURRENT_STREAMS: 0x3,
52
+ INITIAL_WINDOW_SIZE: 0x4,
53
+ MAX_FRAME_SIZE: 0x5,
54
+ MAX_HEADER_LIST_SIZE: 0x6,
55
+ };
56
+
57
+ export const SETTINGS_NAME = Object.fromEntries(
58
+ Object.entries(SETTINGS).map(([k, v]) => [v, k]),
59
+ );
60
+
61
+ /** Error codes (RFC 9113 s7). Sent in RST_STREAM and GOAWAY; also received in them. */
62
+ export const H2_ERROR = {
63
+ NO_ERROR: 0x0,
64
+ PROTOCOL_ERROR: 0x1,
65
+ INTERNAL_ERROR: 0x2,
66
+ FLOW_CONTROL_ERROR: 0x3,
67
+ SETTINGS_TIMEOUT: 0x4,
68
+ STREAM_CLOSED: 0x5,
69
+ FRAME_SIZE_ERROR: 0x6,
70
+ REFUSED_STREAM: 0x7,
71
+ CANCEL: 0x8,
72
+ COMPRESSION_ERROR: 0x9,
73
+ CONNECT_ERROR: 0xa,
74
+ ENHANCE_YOUR_CALM: 0xb,
75
+ INADEQUATE_SECURITY: 0xc,
76
+ HTTP_1_1_REQUIRED: 0xd,
77
+ };
78
+
79
+ export const H2_ERROR_NAME = Object.fromEntries(
80
+ Object.entries(H2_ERROR).map(([k, v]) => [v, k]),
81
+ );
82
+
83
+ // --- protocol-fixed limits ---------------------------------------------------------------------
84
+
85
+ /** The default SETTINGS_MAX_FRAME_SIZE and its floor (RFC 9113 s6.5.2): 2^14. A peer may raise
86
+ * its own limit, but until it says so in SETTINGS we must not send a frame larger than this. */
87
+ export const DEFAULT_MAX_FRAME_SIZE = 16384;
88
+ /** The ceiling a peer may set MAX_FRAME_SIZE to: 2^24 - 1. Beyond it, SETTINGS is a PROTOCOL_ERROR. */
89
+ export const MAX_ALLOWED_FRAME_SIZE = 16777215;
90
+ /** The default per-stream/-connection flow-control window before any SETTINGS/WINDOW_UPDATE. */
91
+ export const DEFAULT_INITIAL_WINDOW = 65535;
92
+ /** The largest a flow-control window may reach; exceeding it is a FLOW_CONTROL_ERROR (s6.9.1). */
93
+ export const MAX_WINDOW = 2147483647; // 2^31 - 1
94
+
95
+ // --- our client posture (the curl fingerprint) -------------------------------------------------
96
+
97
+ /**
98
+ * The SETTINGS we advertise, and the ORDER we advertise them in. Both are matched to curl
99
+ * 8.7.1 / nghttp2 1.69.0 as captured on the wire:
100
+ *
101
+ * SETTINGS: MAX_CONCURRENT_STREAMS=100, INITIAL_WINDOW_SIZE=10485760, ENABLE_PUSH=0
102
+ * emitted in the id order 3, 4, 2 — and nothing else (no HEADER_TABLE_SIZE, no MAX_FRAME_SIZE,
103
+ * no MAX_HEADER_LIST_SIZE), so those stay at their protocol defaults exactly as curl leaves them.
104
+ *
105
+ * These are the values the SERVER sees and fingerprints on, so they are held fixed here rather
106
+ * than exposed as knobs. The receive-side buffering they imply is bounded elsewhere (see the
107
+ * flow-control note in connection.js): a large advertised window is a fingerprint choice, not a
108
+ * promise to buffer that much before applying backpressure.
109
+ */
110
+ export const CLIENT_INITIAL_WINDOW_SIZE = 10485760; // 10 MiB, curl's SETTINGS_INITIAL_WINDOW_SIZE
111
+ export const CLIENT_MAX_CONCURRENT_STREAMS = 100; // curl's SETTINGS_MAX_CONCURRENT_STREAMS
112
+
113
+ /** curl raises the CONNECTION receive window to exactly 1000 MiB with one WINDOW_UPDATE on
114
+ * stream 0 right after SETTINGS. The increment below is 1000 MiB - 65535, i.e. what takes the
115
+ * default 65535 window up to 1048576000. Sent in the same preface flight, same order as curl. */
116
+ export const CLIENT_CONNECTION_WINDOW = 1048576000; // 1000 MiB
117
+ export const CLIENT_CONNECTION_WINDOW_INCREMENT = CLIENT_CONNECTION_WINDOW - DEFAULT_INITIAL_WINDOW; // 1048510465
118
+
119
+ /**
120
+ * The order pseudo-headers are emitted in a request HEADERS block. curl/nghttp2 sends
121
+ * :method, :scheme, :authority, :path — captured, not guessed. h2 fingerprinters read this order
122
+ * (the "m,s,a,p" tail of an Akamai-style h2 fingerprint), so it is fixed to match.
123
+ */
124
+ export const PSEUDO_HEADER_ORDER = [':method', ':scheme', ':authority', ':path'];
125
+
126
+ /** ALPN identifiers. Offering both and following the server's pick is the whole ALPN contract;
127
+ * there is no reconnect-and-retry path if the server picks the other one. */
128
+ export const ALPN_H2 = 'h2';
129
+ export const ALPN_HTTP11 = 'http/1.1';
@@ -0,0 +1,291 @@
1
+ // HTTP/2 frame layer (RFC 9113 s4, s6): the 9-octet header, and pure serialisers / parsers for
2
+ // the frame types a client sends and receives.
3
+ //
4
+ // This file knows nothing about streams, flow control, or state machines — it turns bytes into
5
+ // `{type, flags, streamId, payload}` and back, and validates only what framing itself dictates
6
+ // (a length the type forbids, padding longer than the frame). Everything semantic lives in
7
+ // connection.js. Splitting it this way is what lets the frame reader be run under all chunkings:
8
+ // it must produce identical frames whether the transport hands over one byte or the whole record.
9
+
10
+ import { Http2Error, codes } from '../errors.js';
11
+ import { UnexpectedEofError, readU16, readU32 } from '../util/bytes.js';
12
+ import { FRAME, FLAG, DEFAULT_MAX_FRAME_SIZE } from './constants.js';
13
+
14
+ /** The fixed frame header size (RFC 9113 s4.1): 24-bit length, type, flags, 31-bit stream id. */
15
+ export const FRAME_HEADER_SIZE = 9;
16
+
17
+ /**
18
+ * One frame off the wire. `payload` aliases the reader's buffer and must not be retained.
19
+ * @typedef {object} Frame
20
+ * @property {number} type
21
+ * @property {number} flags
22
+ * @property {number} streamId
23
+ * @property {Uint8Array} payload
24
+ */
25
+
26
+ /**
27
+ * Read exactly one frame. Enforces our advertised SETTINGS_MAX_FRAME_SIZE on receipt: a peer
28
+ * that sends a larger frame commits a FRAME_SIZE_ERROR (RFC 9113 s4.2), and refusing at the
29
+ * header — before reading the body — is what bounds how much a hostile peer can make us buffer.
30
+ * Returns null only on a clean EOF exactly at a frame boundary.
31
+ *
32
+ * @param {import('../util/bytes.js').ByteReader} reader
33
+ * @param {number} maxFrameSize the largest payload we will accept
34
+ * @returns {Promise<Frame | null>}
35
+ */
36
+ export async function readFrame(reader, maxFrameSize = DEFAULT_MAX_FRAME_SIZE) {
37
+ let header;
38
+ try {
39
+ header = await reader.readExactly(FRAME_HEADER_SIZE, 'HTTP/2 frame header');
40
+ } catch (e) {
41
+ if (e instanceof UnexpectedEofError && e.detail?.got === 0) return null; // clean end
42
+ throw e;
43
+ }
44
+ const length = (header[0] << 16) | (header[1] << 8) | header[2];
45
+ const type = header[3];
46
+ const flags = header[4];
47
+ // The reserved high bit of the stream id is explicitly ignored (RFC 9113 s4.1), never checked.
48
+ const streamId = readU32(header, 5) & 0x7fffffff;
49
+ if (length > maxFrameSize) {
50
+ throw new Http2Error(
51
+ codes.HTTP2_FRAME_SIZE,
52
+ `frame length ${length} exceeds SETTINGS_MAX_FRAME_SIZE ${maxFrameSize}`,
53
+ { length, limit: maxFrameSize, type, streamId },
54
+ );
55
+ }
56
+ const payload = length === 0 ? EMPTY : await reader.readExactly(length, 'HTTP/2 frame payload');
57
+ return { type, flags, streamId, payload };
58
+ }
59
+
60
+ const EMPTY = new Uint8Array(0);
61
+
62
+ // ---------------------------------------------------------------------- serialisers
63
+
64
+ /** Write a frame header into the first 9 bytes of `out`, at offset `o`. */
65
+ function putHeader(out, o, length, type, flags, streamId) {
66
+ out[o] = (length >>> 16) & 0xff;
67
+ out[o + 1] = (length >>> 8) & 0xff;
68
+ out[o + 2] = length & 0xff;
69
+ out[o + 3] = type;
70
+ out[o + 4] = flags;
71
+ out[o + 5] = (streamId >>> 24) & 0x7f; // clear the reserved bit
72
+ out[o + 6] = (streamId >>> 16) & 0xff;
73
+ out[o + 7] = (streamId >>> 8) & 0xff;
74
+ out[o + 8] = streamId & 0xff;
75
+ }
76
+
77
+ /**
78
+ * Serialise one frame (header + payload) into a single buffer.
79
+ * @param {number} type
80
+ * @param {number} flags
81
+ * @param {number} streamId
82
+ * @param {Uint8Array} payload
83
+ * @returns {Uint8Array}
84
+ */
85
+ export function serializeFrame(type, flags, streamId, payload = EMPTY) {
86
+ const out = new Uint8Array(FRAME_HEADER_SIZE + payload.length);
87
+ putHeader(out, 0, payload.length, type, flags, streamId);
88
+ out.set(payload, FRAME_HEADER_SIZE);
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * A SETTINGS frame. `entries` is an array of [id, value]; order is preserved because it is part
94
+ * of the client fingerprint. An ACK carries no payload and sets the ACK flag.
95
+ * @param {Array<[number, number]>} entries
96
+ * @param {boolean} [ack]
97
+ * @returns {Uint8Array}
98
+ */
99
+ export function settingsFrame(entries, ack = false) {
100
+ if (ack) return serializeFrame(FRAME.SETTINGS, FLAG.ACK, 0, EMPTY);
101
+ const payload = new Uint8Array(entries.length * 6);
102
+ let o = 0;
103
+ for (const [id, value] of entries) {
104
+ payload[o] = (id >>> 8) & 0xff;
105
+ payload[o + 1] = id & 0xff;
106
+ payload[o + 2] = (value >>> 24) & 0xff;
107
+ payload[o + 3] = (value >>> 16) & 0xff;
108
+ payload[o + 4] = (value >>> 8) & 0xff;
109
+ payload[o + 5] = value & 0xff;
110
+ o += 6;
111
+ }
112
+ return serializeFrame(FRAME.SETTINGS, 0, 0, payload);
113
+ }
114
+
115
+ /** A WINDOW_UPDATE frame (RFC 9113 s6.9). streamId 0 is connection-level. */
116
+ export function windowUpdateFrame(streamId, increment) {
117
+ const payload = new Uint8Array(4);
118
+ payload[0] = (increment >>> 24) & 0x7f;
119
+ payload[1] = (increment >>> 16) & 0xff;
120
+ payload[2] = (increment >>> 8) & 0xff;
121
+ payload[3] = increment & 0xff;
122
+ return serializeFrame(FRAME.WINDOW_UPDATE, 0, streamId, payload);
123
+ }
124
+
125
+ /** A RST_STREAM frame (RFC 9113 s6.4). */
126
+ export function rstStreamFrame(streamId, errorCode) {
127
+ const payload = new Uint8Array(4);
128
+ payload[0] = (errorCode >>> 24) & 0xff;
129
+ payload[1] = (errorCode >>> 16) & 0xff;
130
+ payload[2] = (errorCode >>> 8) & 0xff;
131
+ payload[3] = errorCode & 0xff;
132
+ return serializeFrame(FRAME.RST_STREAM, 0, streamId, payload);
133
+ }
134
+
135
+ /** A PING frame (RFC 9113 s6.7). The 8-byte opaque data is echoed on ACK. */
136
+ export function pingFrame(opaque, ack = false) {
137
+ return serializeFrame(FRAME.PING, ack ? FLAG.ACK : 0, 0, opaque);
138
+ }
139
+
140
+ /** A GOAWAY frame (RFC 9113 s6.8). */
141
+ export function goawayFrame(lastStreamId, errorCode, debug = EMPTY) {
142
+ const payload = new Uint8Array(8 + debug.length);
143
+ payload[0] = (lastStreamId >>> 24) & 0x7f;
144
+ payload[1] = (lastStreamId >>> 16) & 0xff;
145
+ payload[2] = (lastStreamId >>> 8) & 0xff;
146
+ payload[3] = lastStreamId & 0xff;
147
+ payload[4] = (errorCode >>> 24) & 0xff;
148
+ payload[5] = (errorCode >>> 16) & 0xff;
149
+ payload[6] = (errorCode >>> 8) & 0xff;
150
+ payload[7] = errorCode & 0xff;
151
+ payload.set(debug, 8);
152
+ return serializeFrame(FRAME.GOAWAY, 0, 0, payload);
153
+ }
154
+
155
+ /** A HEADERS frame carrying a full (already-fragmented-if-needed) block. No PADDED, no PRIORITY —
156
+ * a client that emits neither is exactly what curl does. */
157
+ export function headersFrame(streamId, block, { endStream = false, endHeaders = true } = {}) {
158
+ let flags = 0;
159
+ if (endStream) flags |= FLAG.END_STREAM;
160
+ if (endHeaders) flags |= FLAG.END_HEADERS;
161
+ return serializeFrame(FRAME.HEADERS, flags, streamId, block);
162
+ }
163
+
164
+ /** A CONTINUATION frame (RFC 9113 s6.10), for a header block that overflows one frame. */
165
+ export function continuationFrame(streamId, block, endHeaders) {
166
+ return serializeFrame(FRAME.CONTINUATION, endHeaders ? FLAG.END_HEADERS : 0, streamId, block);
167
+ }
168
+
169
+ /** A DATA frame (RFC 9113 s6.1). No padding is ever emitted. */
170
+ export function dataFrame(streamId, data, endStream) {
171
+ return serializeFrame(FRAME.DATA, endStream ? FLAG.END_STREAM : 0, streamId, data);
172
+ }
173
+
174
+ // ---------------------------------------------------------------------- payload parsers
175
+
176
+ /**
177
+ * Strip a DATA/HEADERS PADDED frame's pad length and padding, returning the meaningful slice.
178
+ * The pad length byte and padding both count against flow control (the caller handles that);
179
+ * this only removes them from the bytes handed onward. A pad length >= the remaining payload is
180
+ * a PROTOCOL_ERROR (RFC 9113 s6.1).
181
+ * @param {Uint8Array} payload
182
+ * @returns {{ data: Uint8Array, padLength: number }}
183
+ */
184
+ export function stripPadding(payload) {
185
+ if (payload.length === 0) {
186
+ throw new Http2Error(codes.HTTP2_PROTOCOL, 'PADDED frame has no pad length octet');
187
+ }
188
+ const padLength = payload[0];
189
+ if (padLength >= payload.length) {
190
+ throw new Http2Error(
191
+ codes.HTTP2_PROTOCOL,
192
+ `pad length ${padLength} is not smaller than the ${payload.length - 1} bytes that follow it`,
193
+ { padLength, available: payload.length - 1 },
194
+ );
195
+ }
196
+ return { data: payload.subarray(1, payload.length - padLength), padLength };
197
+ }
198
+
199
+ /**
200
+ * The header block fragment inside a HEADERS payload, after removing padding (PADDED) and the
201
+ * priority fields (PRIORITY). Priority is deprecated (RFC 9113 s5.3.2) and its fields are
202
+ * discarded, not acted on.
203
+ * @param {Uint8Array} payload
204
+ * @param {number} flags
205
+ * @returns {Uint8Array}
206
+ */
207
+ export function headersBlockFragment(payload, flags) {
208
+ let p = payload;
209
+ if (flags & FLAG.PADDED) p = stripPadding(p).data;
210
+ if (flags & FLAG.PRIORITY) {
211
+ if (p.length < 5) {
212
+ throw new Http2Error(codes.HTTP2_FRAME_SIZE, 'HEADERS PRIORITY block is shorter than 5 bytes');
213
+ }
214
+ p = p.subarray(5); // 4-byte stream dependency + 1-byte weight, both ignored
215
+ }
216
+ return p;
217
+ }
218
+
219
+ /**
220
+ * Parse a SETTINGS payload into [id, value] pairs. Length must be a multiple of 6
221
+ * (RFC 9113 s6.5); anything else is a FRAME_SIZE_ERROR.
222
+ * @param {Uint8Array} payload
223
+ * @returns {Array<[number, number]>}
224
+ */
225
+ export function parseSettings(payload) {
226
+ if (payload.length % 6 !== 0) {
227
+ throw new Http2Error(
228
+ codes.HTTP2_FRAME_SIZE,
229
+ `SETTINGS payload of ${payload.length} bytes is not a multiple of 6`,
230
+ { length: payload.length },
231
+ );
232
+ }
233
+ const out = [];
234
+ for (let o = 0; o < payload.length; o += 6) {
235
+ out.push([readU16(payload, o), readU32(payload, o + 2)]);
236
+ }
237
+ return out;
238
+ }
239
+
240
+ /**
241
+ * Parse a WINDOW_UPDATE payload. Must be exactly 4 bytes; a zero increment is a protocol error
242
+ * (RFC 9113 s6.9), surfaced by the caller which knows the stream context.
243
+ * @param {Uint8Array} payload
244
+ * @returns {number} the 31-bit increment
245
+ */
246
+ export function parseWindowUpdate(payload) {
247
+ if (payload.length !== 4) {
248
+ throw new Http2Error(
249
+ codes.HTTP2_FRAME_SIZE,
250
+ `WINDOW_UPDATE payload is ${payload.length} bytes, must be 4`,
251
+ { length: payload.length },
252
+ );
253
+ }
254
+ return readU32(payload, 0) & 0x7fffffff;
255
+ }
256
+
257
+ /**
258
+ * Parse a RST_STREAM payload (exactly 4 bytes: an error code).
259
+ * @param {Uint8Array} payload
260
+ * @returns {number}
261
+ */
262
+ export function parseRstStream(payload) {
263
+ if (payload.length !== 4) {
264
+ throw new Http2Error(
265
+ codes.HTTP2_FRAME_SIZE,
266
+ `RST_STREAM payload is ${payload.length} bytes, must be 4`,
267
+ { length: payload.length },
268
+ );
269
+ }
270
+ return readU32(payload, 0);
271
+ }
272
+
273
+ /**
274
+ * Parse a GOAWAY payload (RFC 9113 s6.8): last-stream-id, error code, optional debug data.
275
+ * @param {Uint8Array} payload
276
+ * @returns {{ lastStreamId: number, errorCode: number, debug: Uint8Array }}
277
+ */
278
+ export function parseGoaway(payload) {
279
+ if (payload.length < 8) {
280
+ throw new Http2Error(
281
+ codes.HTTP2_FRAME_SIZE,
282
+ `GOAWAY payload is ${payload.length} bytes, must be at least 8`,
283
+ { length: payload.length },
284
+ );
285
+ }
286
+ return {
287
+ lastStreamId: readU32(payload, 0) & 0x7fffffff,
288
+ errorCode: readU32(payload, 4),
289
+ debug: payload.subarray(8),
290
+ };
291
+ }