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.
- package/LICENSE +28 -0
- package/README.md +617 -0
- package/README.zh-CN.md +470 -0
- package/package.json +74 -0
- package/src/client/cookies.js +429 -0
- package/src/client/decode.js +346 -0
- package/src/client/redirect.js +249 -0
- package/src/client.js +704 -0
- package/src/errors.js +181 -0
- package/src/http1/chunked.js +289 -0
- package/src/http1/index.js +10 -0
- package/src/http1/request.js +143 -0
- package/src/http1/response.js +493 -0
- package/src/http2/connection.js +1170 -0
- package/src/http2/constants.js +129 -0
- package/src/http2/frames.js +291 -0
- package/src/http2/hpack.js +420 -0
- package/src/http2/huffman.js +203 -0
- package/src/http2/index.js +21 -0
- package/src/index.js +46 -0
- package/src/pool.js +256 -0
- package/src/proxy/direct.js +62 -0
- package/src/proxy/http-connect.js +206 -0
- package/src/proxy/index.js +197 -0
- package/src/proxy/socks5.js +344 -0
- package/src/tls/aead.js +263 -0
- package/src/tls/connect.js +407 -0
- package/src/tls/constants.js +334 -0
- package/src/tls/extensions.js +376 -0
- package/src/tls/handshake-messages.js +901 -0
- package/src/tls/handshake.js +568 -0
- package/src/tls/handshake12.js +507 -0
- package/src/tls/index.js +44 -0
- package/src/tls/keyschedule.js +473 -0
- package/src/tls/record.js +872 -0
- package/src/tls/tickets.js +145 -0
- package/src/tls/transcript.js +101 -0
- package/src/tls/wire.js +224 -0
- package/src/transport.js +296 -0
- package/src/trust/der.js +551 -0
- package/src/trust/index.js +375 -0
- package/src/trust/name.js +235 -0
- package/src/trust/ocsp.js +759 -0
- package/src/trust/path.js +595 -0
- package/src/trust/roots.js +454 -0
- package/src/trust/x509.js +902 -0
- package/src/util/bytes.js +470 -0
- package/src/util/deadline.js +266 -0
- package/src/warmup-fixture.js +85 -0
- package/src/warmup.js +243 -0
- package/types/client/cookies.d.ts +159 -0
- package/types/client/decode.d.ts +54 -0
- package/types/client/redirect.d.ts +96 -0
- package/types/client.d.ts +323 -0
- package/types/errors.d.ts +141 -0
- package/types/http1/chunked.d.ts +48 -0
- package/types/http1/index.d.ts +3 -0
- package/types/http1/request.d.ts +44 -0
- package/types/http1/response.d.ts +183 -0
- package/types/http2/connection.d.ts +282 -0
- package/types/http2/constants.d.ts +95 -0
- package/types/http2/frames.d.ts +116 -0
- package/types/http2/hpack.d.ts +99 -0
- package/types/http2/huffman.d.ts +21 -0
- package/types/http2/index.d.ts +5 -0
- package/types/index.d.ts +17 -0
- package/types/pool.d.ts +135 -0
- package/types/proxy/direct.d.ts +26 -0
- package/types/proxy/http-connect.d.ts +37 -0
- package/types/proxy/index.d.ts +62 -0
- package/types/proxy/socks5.d.ts +47 -0
- package/types/tls/aead.d.ts +67 -0
- package/types/tls/connect.d.ts +280 -0
- package/types/tls/constants.d.ts +275 -0
- package/types/tls/extensions.d.ts +195 -0
- package/types/tls/handshake-messages.d.ts +430 -0
- package/types/tls/handshake.d.ts +90 -0
- package/types/tls/handshake12.d.ts +35 -0
- package/types/tls/index.d.ts +9 -0
- package/types/tls/keyschedule.d.ts +272 -0
- package/types/tls/record.d.ts +361 -0
- package/types/tls/tickets.d.ts +66 -0
- package/types/tls/transcript.d.ts +52 -0
- package/types/tls/wire.d.ts +106 -0
- package/types/transport.d.ts +222 -0
- package/types/trust/der.d.ts +239 -0
- package/types/trust/index.d.ts +194 -0
- package/types/trust/name.d.ts +33 -0
- package/types/trust/ocsp.d.ts +138 -0
- package/types/trust/path.d.ts +139 -0
- package/types/trust/roots.d.ts +36 -0
- package/types/trust/x509.d.ts +401 -0
- package/types/util/bytes.d.ts +183 -0
- package/types/util/deadline.d.ts +133 -0
- package/types/warmup-fixture.d.ts +11 -0
- 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
|
+
}
|