h2lean 0.0.0-stage → 0.1.1

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/lib/session.js ADDED
@@ -0,0 +1,2698 @@
1
+ // HTTP/2 connection state machine (server side, h2c).
2
+ //
3
+ // Two performance decisions shape this file:
4
+ // * one output staging buffer per connection: every frame produced while draining
5
+ // one socket read is serialised into it and goes out in a single write(), and
6
+ // flow-control credits are coalesced over the same batch;
7
+ // * whole frames are parsed in place out of the chunk node handed us. A frame
8
+ // that straddles two reads is not copied into a buffer of the session's own
9
+ // either: its payload is kept as the views of the chunks it arrived in, one
10
+ // view per read. A DATA payload is handed to the application as those views,
11
+ // which is what node does with the same frame (tools/probe-chunking.js
12
+ // measures both stacks delivering a cut payload as the pieces it arrived in);
13
+ // a header block is decoded out of them (lib/hpack.js decodePieces); and the
14
+ // control frames -- small and fixed-size -- are put in a buffer of their own
15
+ // for the parse. A padded DATA payload is no copy either: its views are not
16
+ // the payload (RFC 9113 6.1 -- the first octet is the pad length, the last
17
+ // ones are the padding), so the padding is taken off the views once the frame
18
+ // is complete (lib/frames.js trimPadding).
19
+ 'use strict';
20
+
21
+ const { EventEmitter } = require('node:events');
22
+ const f = require('./frames');
23
+ const { Decoder, Encoder, HPackError } = require('./hpack');
24
+ const { Http2Stream, errorOf, emitNT } = require('./stream');
25
+ const { pieceCopyTo, pieceSlice, pieceConcat, pieceEquals } = require('./pieces');
26
+ const { nullProto, hasBadValue } = require('./headers');
27
+ // One shared 499 ms clock for the long idle timeouts, instead of a libuv timer
28
+ // handle per connection. Vendored verbatim; see lib/fast-timer.js.
29
+ const { setTimeout: idleSetTimeout, clearTimeout: idleClearTimeout } =
30
+ require('./fast-timer');
31
+
32
+ const {
33
+ DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY,
34
+ WINDOW_UPDATE, CONTINUATION,
35
+ F_END_STREAM, F_ACK, F_END_HEADERS, F_PADDED, F_PRIORITY,
36
+ NO_ERROR, PROTOCOL_ERROR, INTERNAL_ERROR, FLOW_CONTROL_ERROR, STREAM_CLOSED,
37
+ FRAME_SIZE_ERROR, REFUSED_STREAM, COMPRESSION_ERROR, ENHANCE_YOUR_CALM,
38
+ PREFACE_LEN, HEADER_LEN, readHeader, writeHeader, trimPadding
39
+ } = f;
40
+
41
+ // The values the session operates on when the application says nothing. Derived from node's own
42
+ // getDefaultSettings() so the numbers the guards compare against are the ones node's guards compare
43
+ // against. They are NOT advertised by default: what goes into a SETTINGS frame is decided by
44
+ // _advertised, because an advertised value is a promise to the peer rather than a local choice.
45
+ const OUR_DEFAULTS = { ...require('node:http2').getDefaultSettings(), enablePush: false };
46
+
47
+ // What node actually has in effect before the peer's SETTINGS arrives. Read off
48
+ // a live session: doc/api/http2.md quotes different numbers for
49
+ // initialWindowSize and maxHeaderListSize than the running binary reports, and
50
+ // the running values are what peers observe.
51
+ const PEER_DEFAULTS = {
52
+ headerTableSize: 4096,
53
+ enablePush: true,
54
+ maxConcurrentStreams: 4294967295,
55
+ initialWindowSize: 65535,
56
+ maxFrameSize: 16384,
57
+ maxHeaderListSize: 4294967295,
58
+ enableConnectProtocol: false
59
+ };
60
+
61
+ // SETTINGS identifiers, from http2.constants.NGHTTP2_SETTINGS_*.
62
+ // The numbering is NOT the order the names are usually listed in:
63
+ // 3 is maxConcurrentStreams and 4 is initialWindowSize.
64
+ const SETTINGS_BY_ID = {
65
+ 1: 'headerTableSize',
66
+ 2: 'enablePush',
67
+ 3: 'maxConcurrentStreams',
68
+ 4: 'initialWindowSize',
69
+ 5: 'maxFrameSize',
70
+ 6: 'maxHeaderListSize',
71
+ 8: 'enableConnectProtocol'
72
+ };
73
+
74
+ const ID_BY_SETTING = Object.fromEntries(
75
+ Object.entries(SETTINGS_BY_ID).map(([id, name]) => [name, Number(id)]));
76
+
77
+ const MAX_REFUSED_HISTORY = 32;
78
+ const MAX_DROPPED_HISTORY = 256;
79
+ const MAX_SHAPES = 128;
80
+
81
+ // RFC 7541 4.1: the size of an entry is the length of its name plus the length of its value
82
+ // plus 32 -- the computation RFC 9113 6.5.2 states SETTINGS_MAX_HEADER_LIST_SIZE in terms of.
83
+ // That is the number node's `maxHeaderListSize` is compared against, measured on
84
+ // v26.12.1 with tools/probe-inbound-limits.js: a 4-field block whose names and values total 46
85
+ // octets is REFUSED under a limit of 48 (46 + 4*32 = 174), and for a 5-field block the line
86
+ // between served and refused falls between 87 and 93 decoded octets (250 - 5*32 = 90). The
87
+ // plain sum of the field octets is what h2lean compared, and it is 128 entry octets away from
88
+ // node's verdict for a four-field request: three of the measured blocks were served that node
89
+ // refuses. test/inbound-limits.test.js pins both sides of the line.
90
+ const ENTRY_OVERHEAD = 32;
91
+
92
+ // How many bytes a session may have queued for the socket while a write is still
93
+ // outstanding. It has to be an absolute number: derived from the size of the staging
94
+ // region it becomes a feedback loop, because whatever fits raises the limit and lets
95
+ // the next body in. Measured without it on 64 connections x 32 streams with 256 KB
96
+ // responses: the staging reached 16 MB per session (1 GB of RSS for 64 connections,
97
+ // and a peer chooses the body size), one write() carried 8 MB, and the compaction
98
+ // behind the frozen region moved 251 KB per request.
99
+ //
100
+ // The ceiling itself is a syscall/memory trade measured in one session
101
+ // (OPTIMIZATION.md item 34): at 128 KiB a 256 KB response leaves in two writev calls
102
+ // with a park/resume round-trip between them; at 256 KiB it leaves in one and the
103
+ // download case is ~7% cheaper per request (arm ranges non-overlap), for a doubled
104
+ // worst case -- 256 KiB held per session -- against a peer that stops reading.
105
+ const MAX_OUT_BATCH = 256 * 1024;
106
+
107
+ // How much a connection may make this session hold for answers it is not allowed to send. A body
108
+ // that flow control will not pass is parked on its stream until the peer opens the window, which is
109
+ // right per request and unbounded per connection: a client that never sends a WINDOW_UPDATE can
110
+ // make the server keep a live stream plus its unsent body for every HEADERS it puts on the wire.
111
+ // Measured with tools/probe-stream-concurrency.js -- 200 000 streams on one connection, a 14-byte
112
+ // response each, no credit ever granted -- the session held 195 319 live streams and 170 MB after a
113
+ // forced collection, i.e. 892 bytes per parked stream whose queue entry was 23 bytes: what costs
114
+ // here is the stream record, not its payload. So the charge has both terms, and the number is then
115
+ // a statement about memory rather than about one dimension of it -- at the measured cost, about
116
+ // 8 000 parked streams per connection, the same order as the point where node's own session bound
117
+ // stopped the identical flood (13 454 streams, same probe, maxSessionMemory left at its default).
118
+ // The socket-queue half of the retention is not counted because it is already bounded absolutely by
119
+ // MAX_OUT_BATCH above.
120
+ const MAX_SESSION_HOLD = 8 * 1024 * 1024;
121
+ const STREAM_HOLD_CHARGE = 1024;
122
+
123
+ // Smallest payload that is worth queuing by reference instead of copying into the
124
+ // staging buffer. Below it the extra chunk in the writev batch costs more than the
125
+ // memcpy -- the break-even is measured at 4 KB (OPTIMIZATION.md item 15) -- and one
126
+ // frame at the default maxFrameSize of 16 384 is comfortably above it either way.
127
+ const BODY_MIN = 4096;
128
+
129
+ // First size of the staging buffer; doubled per frame that does not fit, so a batch of
130
+ // header blocks costs one allocation and one copy of each block.
131
+ const STAGE_MIN = 1024;
132
+
133
+ // The 'stream' event carries node's options object. Two constants instead of a
134
+ // literal per request: the payload never changes, and 1.9M requests is 1.9M
135
+ // objects the young generation would otherwise have to collect.
136
+ const OPT_END_STREAM = { endStream: true };
137
+ const OPT_NO_END_STREAM = { endStream: false };
138
+
139
+ // How long the teardown waits for the goodbye to reach the peer before the socket is
140
+ // taken away anyway. The FIN is queued behind whatever we wrote, so a peer that stopped
141
+ // reading would hold it forever -- node does not have that problem because its data sits
142
+ // inside nghttp2 until the window allows it, while h2lean hands it to the socket. The
143
+ // wait itself is what makes the difference between a closed connection and a reset
144
+ // (h2spec's http2/7), so it gets a bound of its own: one second is three orders of
145
+ // magnitude longer than the flush takes when the peer is reading.
146
+ const SOCKET_CLOSE_GRACE = 1000;
147
+
148
+ // ---- inbound request validation ---------------------------------------------
149
+ //
150
+ // RFC 9113 8.1, 8.2.2, 8.3 and 8.5 are not style advice. Whatever sits in front of us
151
+ // translates these fields into HTTP/1.1 one line at a time, and every rule below exists
152
+ // because a receiver on the far side of that translation can read a field differently
153
+ // than we do. That is request smuggling, and it is a named CVE family in this exact
154
+ // stack: CVE-2020-8287 (Node.js took the first of two copies of a header field and
155
+ // ignored the second). The frame-level pair of the same family -- a SETTINGS frame with
156
+ // an enormous number of entries (CVE-2020-11080) and a header block spread over an
157
+ // enormous number of CONTINUATION frames (CVE-2024-28182) -- is bounded by
158
+ // MAX_SETTINGS_ENTRIES and MAX_CONTINUATIONS just below.
159
+ //
160
+ // The reference for what is refused and how is nghttp2 -- the HTTP/2 stack node:http2
161
+ // embeds -- lib/nghttp2_http.c: check_pseudo_header(), http_request_on_header(),
162
+ // nghttp2_http_on_request_headers(). Every rule below was checked against a live
163
+ // node:http2 server, and the same matrix is asserted in test/request-validation.test.js,
164
+ // so this file mirrors measured behaviour rather than a reading of the RFC.
165
+ //
166
+ // Two different actions, and the distinction is deliberate:
167
+ // * a pseudo-header that is missing, repeated, empty, unknown, out of order, or a
168
+ // connection-specific / te / content-length violation -> RST_STREAM PROTOCOL_ERROR,
169
+ // the stream never reaches the application. nghttp2 is strict here: "we are strict
170
+ // for pseudo header field. One bad character should lead to fail".
171
+ // * a *regular* field whose name contains a character that is not allowed, but is not
172
+ // upper case -> the field is dropped and the stream lives. Measured on node: a name
173
+ // of 'bad name' arrives as nothing, the handler still runs. nghttp2 calls this
174
+ // NGHTTP2_ERR_IGN_HTTP_HEADER, with the comment that illegal regular headers float
175
+ // around the public internet in quantity and killing a stream over one would break
176
+ // sites. An upper-case name is NOT in that class: it is a hard error (also measured).
177
+ // * a *regular* field whose VALUE carries a character that may not appear in a field
178
+ // value -> the field is dropped and the stream lives, the same action as the name
179
+ // case above. Measured on node, character by character (bench/micro/probe-value-matrix.js):
180
+ // every control character but HTAB, and DEL, makes the field disappear while the
181
+ // request is still served. The class is the one lib/headers.js refuses on the way
182
+ // out, and the reason is the same: a value with a CR in it is a line two readers
183
+ // may disagree about (CVE-2020-8287's class).
184
+ const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; // RFC 9110 9: method is a token
185
+ const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+\-.]*$/; // RFC 3986 3.1
186
+ const NUM_RE = /^[0-9]+$/; // RFC 9110 8.6: Content-Length = 1*DIGIT
187
+ // RFC 9113 8.3.1 :authority is host/pport, i.e. URI authority syntax (RFC 3986 3.2).
188
+ const AUTHORITY_RE = /^[A-Za-z0-9._~%!$&'()*+,;=:@[\]-]+$/;
189
+ // Field names on an HTTP/2 connection must be lower case (RFC 9113 8.2.1). Split into
190
+ // two tests because the two halves have different consequences, see above.
191
+ const NAME_LOWER_RE = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/;
192
+ const HAS_UPPER = /[A-Z]/;
193
+ // RFC 9113 8.2.2: connection-specific fields may not be sent in HTTP/2 at all. The same
194
+ // five names lib/headers.js refuses on the way out; kept separate because the action
195
+ // here is different (refuse the stream, not throw at the application).
196
+ const INBOUND_FORBIDDEN = new Set(['connection', 'keep-alive', 'proxy-connection',
197
+ 'transfer-encoding', 'upgrade']);
198
+
199
+ // How many SETTINGS entries one frame may carry. CVE-2020-11080 is the shape: a frame
200
+ // with 2 400 entries (14 400 bytes, and SETTINGS is exempt from the max frame size limit
201
+ // -- see _allowFrame) repeated until the CPU is gone, because every entry is parsed even
202
+ // though the peer meant nothing by them. nghttp2 1.41 answered with a limit and node has
203
+ // it: measured on a live node:http2 server, 32 entries are accepted and 33 bring
204
+ // GOAWAY(ENHANCE_YOUR_CALM) with the debug string 'SETTINGS: too many setting entries'.
205
+ // Same number here, so a peer that is fine with node is fine with us.
206
+ const MAX_SETTINGS_ENTRIES = 32;
207
+
208
+ // How many CONTINUATION frames may finish one header block. CVE-2024-28182 is the shape:
209
+ // an unbounded number of CONTINUATION frames is unbounded work, and nghttp2 additionally
210
+ // kept decoding them after the stream was reset just to keep HPACK in sync. The limit in
211
+ // nghttp2 is NGHTTP2_DEFAULT_MAX_CONTINUATIONS = 8 (lib/nghttp2_session.h, read from the
212
+ // v1.70.0 source), and it is what node answers with: measured 8 frames served, 9 brought
213
+ // GOAWAY(INTERNAL_ERROR). Eight is generous for a real block -- at the default frame size
214
+ // it is 8 x 16 KB of headers before the limit bites.
215
+ const MAX_CONTINUATIONS = 8;
216
+
217
+ // The rapid-reset budget: one token per inbound RST_STREAM, and a session that runs out
218
+ // is closed. CVE-2023-44487 ("HTTP/2 Rapid Reset", exploited in the wild in August through
219
+ // October 2023) is the shape this bounds: a client opens a stream and resets it
220
+ // immediately, in a loop. Every stream costs the server an allocation, a 'stream' event
221
+ // and a teardown, while the client pays one 13-octet frame -- and the concurrency limit
222
+ // does not bind, because no stream is ever left open long enough to count against it.
223
+ // Measured here before the fix: 3 000 open-and-reset pairs were all served, the handler
224
+ // fired 3 000 times and the connection never objected.
225
+ //
226
+ // The numbers are nghttp2's, which is what node:http2 ships (v1.57.0 added the mitigation;
227
+ // lib/nghttp2_session.c v1.70.0 session_update_stream_reset_ratelim() with
228
+ // NGHTTP2_DEFAULT_STREAM_RESET_BURST = 1000 and _RATE = 33 from lib/nghttp2_session.h):
229
+ // the bucket starts full, gains 33 tokens per *elapsed second* -- integer seconds, so a
230
+ // burst inside one second does not refill it -- drains one per reset, and on an empty
231
+ // drain the server sends GOAWAY(INTERNAL_ERROR). Server side only, as in nghttp2: the
232
+ // client is not the endpoint being exhausted.
233
+ //
234
+ // They are copied rather than invented, because they are the observable budget a peer has
235
+ // against node: measured on a live node:http2 server, 1 000 resets pass and the 1 001st is
236
+ // answered with GOAWAY last=<highest stream received> code=2. A client that is allowed
237
+ // that against node must not be throttled harder here, and one that exhausts node must not
238
+ // get more work out of us.
239
+ const RST_BURST = 1000;
240
+ const RST_RATE = 33; // tokens per second
241
+
242
+ // FNV-1a over the octets of a header block. Math.imul keeps the accumulator in
243
+ // 32 bits, which is also what makes the hash cheap: no strings are allocated.
244
+ function hashBlock(buf) {
245
+ let h = 0x811c9dc5;
246
+ for (let i = 0; i < buf.length; i++) {
247
+ h = Math.imul(h ^ buf[i], 0x01000193);
248
+ }
249
+ return h >>> 0;
250
+ }
251
+
252
+ // The same hash over the views a header block arrived in. A block that a socket
253
+ // read split has no buffer of its own, and the shape cache's key is the hash of
254
+ // the block's octets.
255
+ function hashPieces(pieces, start, end) {
256
+ let h = 0x811c9dc5;
257
+ let off = 0;
258
+ for (let i = 0; i < pieces.length; i++) {
259
+ const p = pieces[i];
260
+ const pend = off + p.length;
261
+ if (pend > start && off < end) {
262
+ const a = start > off ? start - off : 0;
263
+ const b = end < pend ? end - off : p.length;
264
+ for (let k = a; k < b; k++) {
265
+ h = Math.imul(h ^ p[k], 0x01000193);
266
+ }
267
+ }
268
+ off = pend;
269
+ if (off >= end) {
270
+ break;
271
+ }
272
+ }
273
+ return h >>> 0;
274
+ }
275
+
276
+ // The idle-timeout callback. It takes the session as its argument because both
277
+ // the native setTimeout and FastTimer forward a timer argument to the callback,
278
+ // which is what lets setTimeout() allocate nothing per connection.
279
+ function onIdleTimeout(session) {
280
+ session._idleTimer = null;
281
+ session.emit('timeout');
282
+ const cb = session._idleCb;
283
+ if (cb !== null) {
284
+ cb.call(session);
285
+ }
286
+ }
287
+
288
+ class Http2Session extends EventEmitter {
289
+ constructor(socket, options) {
290
+ super();
291
+ const opts = options || {};
292
+ this.socket = socket;
293
+ this.server = null; // set by the server on attach
294
+ this.type = 0; // NGHTTP2_SESSION_SERVER
295
+ this.closed = false;
296
+ this.destroyed = false;
297
+ this.connecting = false;
298
+ // node reads both of these off the socket. Over h2c there is no encryption and
299
+ // no ALPN; over h2s a peer that negotiated nothing reports false rather than
300
+ // a protocol name, which is h2 with prior knowledge on a TLS connection.
301
+ this.encrypted = socket.encrypted === true;
302
+ this.alpnProtocol = socket.alpnProtocol === undefined ? null : socket.alpnProtocol;
303
+ // What the application sees. node keeps the received values in a private state object and
304
+ // exposes them through getters that answer NO_ERROR / 0 when no GOAWAY has arrived
305
+ // (`this[kState].goawayCode || NGHTTP2_NO_ERROR`), so `session.goawayCode` reads 0 on a live
306
+ // connection rather than null. The nulls are kept here as _goawayCode because destroy()
307
+ // needs to know whether the peer has said anything at all -- and node's `goawayCode` says
308
+ // nothing about a teardown this side started, which is why destroy() does not write it.
309
+ this._goawayCode = null;
310
+ this._goawayLastStreamID = null;
311
+ this.pendingSettingsAck = true;
312
+ this.originSet = [];
313
+ this.localSettings = { ...OUR_DEFAULTS, ...opts.settings };
314
+ // Which of those values the application asked for, in the order it asked for them, and only
315
+ // those go on the wire. Measured on a live node server: created with no options its first
316
+ // SETTINGS frame carries an EMPTY payload, and created with
317
+ // settings {maxConcurrentStreams: 100, maxFrameSize: 32768} it advertises exactly those two
318
+ // values in that order and no others -- the same rule is visible in node's internal
319
+ // getDefaultSettings(), which emits a setting only when its flag bit is set.
320
+ // The difference is not cosmetic. A number we advertise is a number a peer plans around, and
321
+ // SETTINGS_MAX_CONCURRENT_STREAMS decides how hard implementations push a connection before
322
+ // they count it as exceeded: h2spec sends the advertised limit plus one HEADERS frames, so
323
+ // advertising the unlimited default turns its concurrency check into four billion streams.
324
+ // Nothing is lost by keeping silent, because the local values still guard the inbound path, and
325
+ // the one promise the old comment worried about -- push -- is kept by the frame rule, not by
326
+ // the advertisement: a peer that sends PUSH_PROMISE gets GOAWAY(PROTOCOL_ERROR) in _onFrame.
327
+ this._advertised = new Set((opts.advertised !== undefined
328
+ ? opts.advertised : Object.keys(opts.settings || {}))
329
+ .filter((k) => ID_BY_SETTING[k] !== undefined));
330
+ this.remoteSettings = { ...PEER_DEFAULTS };
331
+ this.state = {
332
+ effectiveLocalWindowSize: this.localSettings.initialWindowSize,
333
+ effectiveRecvDataLength: 0,
334
+ nextStreamID: 2,
335
+ localWindowSize: this.localSettings.initialWindowSize,
336
+ lastProcStreamID: 0,
337
+ remoteWindowSize: 65535,
338
+ outboundQueueSize: 0,
339
+ deflateDynamicTableSize: 0,
340
+ inflateDynamicTableSize: 0
341
+ };
342
+
343
+ this._streams = new Map();
344
+ this._decoder = new Decoder(this.localSettings.headerTableSize);
345
+ // The two halves of the HPACK state, and neither is shared. Each one is bounded by
346
+ // the SETTINGS of the endpoint that OWNS it: the decoder holds the table our peer
347
+ // builds for us, so its ceiling is what WE advertised; the encoder builds the table
348
+ // the peer will decode, so its ceiling is what THE PEER advertised (RFC 7541 4.2).
349
+ // Blocks that depend on the encoder's indices are cached per session with them.
350
+ this._encoder = new Encoder(this.remoteSettings.headerTableSize,
351
+ this.localSettings.headerTableSize);
352
+ this._blocks = new WeakMap();
353
+ this._shapes = opts.shapeCache === false ? null : new Map();
354
+ this._shapeEpoch = 0;
355
+ this._shapeHits = 0;
356
+ this._shapeMisses = 0;
357
+ // shareHeaders: the application is handed a frozen headers object, and two
358
+ // requests whose HPACK block is byte-identical get the SAME object. That
359
+ // removes the per-request object construction for repeating requests; it
360
+ // also means any write through req.headers is a mutation of other requests,
361
+ // so everything the app receives is frozen and the setters throw
362
+ // (OPTIMIZATION.md item 22 for both sides of the trade-off).
363
+ this._share = opts.shareHeaders === true;
364
+ this._flat = [];
365
+ // The compat layer needs the decoded field list as it came off the wire.
366
+ // Copying it is a per-request allocation, so it only happens for servers
367
+ // that actually listen for 'request'.
368
+ this._wantRaw = opts.compat === true;
369
+
370
+ // Outbound frames go to the socket as one corked writev batch. A body is queued as
371
+ // the buffers that already hold it -- a frame header and a view of the
372
+ // application's buffer -- so assembling a response costs references rather than
373
+ // memcpys. Everything else is copied into a staging buffer, because a hundred tiny
374
+ // responses want one write each rather than two chunks apiece: node's write path
375
+ // charges per chunk, and the staging buffer makes a batch ONE chunk. The shape,
376
+ // the BODY_MIN threshold and the measurements behind it: OPTIMIZATION.md item 15.
377
+ this._outQ = [];
378
+ this._stage = null;
379
+ this._stageLen = 0; // bytes of _stage not yet queued
380
+ this._stageCap = STAGE_MIN; // size of the next staging buffer
381
+ this._outLen = 0; // queued bytes the socket has not been given
382
+ this._heldBytes = 0; // bytes of response parked on streams that lack credit
383
+ this._frozenLen = 0; // bytes handed over, callback still owed
384
+ this._writing = false;
385
+ this._connSendWin = PEER_DEFAULTS.initialWindowSize;
386
+ this._prevInitialWin = PEER_DEFAULTS.initialWindowSize;
387
+ this._connRecvUsed = 0; // consumed bytes owed back to the connection
388
+ this._blocked = null; // streams waiting on credit
389
+
390
+ // A frame that arrives in pieces keeps its payload as the views it arrived in
391
+ // (see _fragPieces), so the session holds no buffer per connection for it.
392
+ // The only octets the session copies are the nine of a frame header that a
393
+ // read split: they wait in this scratch until the header is complete.
394
+ this._fragHdr = Buffer.allocUnsafe(HEADER_LEN);
395
+ this._frag = 0; // bytes of a straddling frame
396
+ this._fragTarget = null;
397
+ // A straddling DATA payload is NOT assembled: it is kept as the views of the
398
+ // chunks the peer sent it in -- one per read -- and handed to the application
399
+ // as they are. Node does the same: its payload reaches the application in the
400
+ // pieces it arrived in, not in one piece per frame (measured with a frame whose
401
+ // payload was cut in two: node's handler gets two chunks, tools/probe-chunking.js),
402
+ // so this is parity rather than a shortcut.
403
+ //
404
+ // A PADDED payload is the one case where the views are not the payload: its
405
+ // first octet is the pad length and its last octets are the padding (RFC 9113
406
+ // 6.1). The padding is taken off the views when the frame is complete
407
+ // (frames.js trimPadding), so even that case costs no copy.
408
+ this._fragPieces = null;
409
+ // The header of the frame being assembled, kept HERE and not read back from
410
+ // frames.js's FH_TYPE/FH_FLAGS/FH_SID. Those are module-level slots shared by every
411
+ // session in the process, and a straddling frame is dispatched in a LATER _onData()
412
+ // call: another connection's readHeader() in between replaces them, and this session
413
+ // dispatches a frame its peer never sent. The same shape was measured on the client
414
+ // with 8 sessions x 64 streams of 256 kB (bench/micro/probe-client-framedrop.js): a DATA frame
415
+ // delivered as WINDOW_UPDATE/0 and one whole frame missing per affected stream. A
416
+ // server sees it whenever two connections' reads interleave with a straddling inbound
417
+ // frame -- a big request body on a busy listener is exactly that.
418
+ this._fragType = 0;
419
+ this._fragFlags = 0;
420
+ this._fragSid = 0;
421
+ this._prefaceSeen = 0;
422
+ // The peer's very first frame has to be its SETTINGS (see _allowFrame); the flag is
423
+ // cleared the moment one frame has been judged, so the rule costs one boolean test per
424
+ // frame header for the rest of the connection's life.
425
+ this._firstFramePending = true;
426
+
427
+ // The header block of a request, while it is being assembled. A block whose
428
+ // frames all arrived whole is kept in one buffer (_hdr); a block with a frame
429
+ // that straddled a read is kept as the views it arrived in (_hdrPieces).
430
+ this._hdr = null;
431
+ this._hdrPieces = null;
432
+ this._hdrLen = 0;
433
+ // The length of the header block fragment in the frame being read: the payload
434
+ // without the pad length, the priority block and the padding. A padded frame
435
+ // carries its padding at the END of the payload (RFC 9113 6.2), so the block
436
+ // does not run to the end of it -- see _headersPrefix.
437
+ this._hdrFrag = 0;
438
+ this._hdrSid = -1;
439
+ this._hdrFlags = 0;
440
+ this._hdrCont = 0; // CONTINUATION frames in this block
441
+ // The first octets of a HEADERS payload -- the pad length and the priority
442
+ // block -- are at most six, and a payload that arrived in pieces has no
443
+ // buffer to read them from: they are copied into this scratch.
444
+ this._hdrPrefix = Buffer.allocUnsafe(6);
445
+
446
+ // Rapid-reset token bucket, per connection. See RST_BURST/RST_RATE above; the state is
447
+ // session data precisely because the budget is one peer's behaviour on one connection.
448
+ // The second is fixed at construction so that a catch-up grant can only ever be the
449
+ // time this connection has actually existed.
450
+ this._rstTokens = RST_BURST;
451
+ this._rstSec = (Date.now() / 1000) | 0;
452
+
453
+ this._maxInboundSid = 0;
454
+ this._refused = [];
455
+ this._dropped = null; // ids a reset ended, newest last
456
+ // ids whose message ended in the read batch being processed, see _dropStream. Cleared by the
457
+ // next read: after this batch the stream has been answered and forgotten, and a frame for it
458
+ // is 5.1's discard rather than its connection error -- which is exactly what node's verdict
459
+ // for the same sequence does once the response has gone out (measured with GAP= in
460
+ // tools/probe-inbound-limits.js, cases U1, U3, U8, U9).
461
+ this._ended = [];
462
+ this._open = true;
463
+ this._closing = false;
464
+ this._closeEmitted = false;
465
+ this._liveStreams = 0;
466
+ this._goawaySent = false;
467
+ this._idleTimer = null;
468
+ this._idleCb = null;
469
+ this._inBatch = false;
470
+ this._flushScheduled = false;
471
+ this._flushSoon = () => {
472
+ this._flushScheduled = false;
473
+ this._flush();
474
+ };
475
+ // one closure per session, so that handing the staged bytes to the socket
476
+ // costs no allocation per write
477
+ this._onWrittenCb = () => this._onWritten();
478
+
479
+ socket.on('data', (d) => this._onData(d));
480
+ socket.on('error', (err) => this._onSocketError(err));
481
+ socket.on('end', () => this.destroy(NO_ERROR));
482
+ // 'finish' means everything we wrote reached the OS, so the GOAWAY of a close() is never
483
+ // cut off by a destroy -- but it is not the end of the session either. node keeps reading
484
+ // until the PEER closes, which is what lets it receive and report the peer's answer to that
485
+ // GOAWAY; the wait ends at the peer's FIN ('end' above) or at 'close'.
486
+ socket.on('finish', () => {
487
+ if (!this._closing) {
488
+ this.destroy(NO_ERROR);
489
+ }
490
+ });
491
+ socket.on('close', () => this._onSocketClose());
492
+
493
+ this._sendSettings();
494
+ }
495
+
496
+ // ---- outbound queue --------------------------------------------------------
497
+ // A payload reaches the socket either by reference or by copy, and the difference is
498
+ // who owns the memory. `owned` says it may be kept by reference: the buffer is either
499
+ // one this session allocated for the frame (SETTINGS, RST_STREAM, GOAWAY, PING) or a
500
+ // view of what the application handed to stream.end(), which it may not mutate before
501
+ // the bytes are on the wire -- the same guarantee node:http2 gives. Bodies above
502
+ // BODY_MIN take that path and are never copied. Everything else travels by copy,
503
+ // because it is a view into memory the next call rewrites -- the HPACK scratch block
504
+ // in lib/headers.js -- and those payloads are tens of bytes.
505
+ _frame(type, flags, sid, payload, owned) {
506
+ const len = payload === null || payload === undefined ? 0 : payload.length;
507
+ if (owned && len >= BODY_MIN) {
508
+ this._commit(); // what is staged was produced earlier, so it
509
+ // has to go out first
510
+ const head = Buffer.allocUnsafe(HEADER_LEN);
511
+ writeHeader(head, 0, len, type, flags, sid);
512
+ this._outQ.push(head, payload);
513
+ } else {
514
+ this._need(HEADER_LEN + len);
515
+ const off = this._stageLen;
516
+ writeHeader(this._stage, off, len, type, flags, sid);
517
+ if (len) {
518
+ payload.copy(this._stage, off + HEADER_LEN);
519
+ }
520
+ this._stageLen = off + HEADER_LEN + len;
521
+ }
522
+ this._outLen += HEADER_LEN + len;
523
+ if (!this._inBatch) {
524
+ this._schedule();
525
+ }
526
+ }
527
+
528
+ // Room in the staging buffer for n more bytes, committing what it holds to the queue
529
+ // if it does not fit. A buffer that goes into _outQ is never written into again --
530
+ // those bytes may already be on their way to the peer, which is exactly what the
531
+ // copyWithin compaction used to have to dance around -- so a full buffer is handed
532
+ // over and a fresh one started, one allocation per batch instead of one per frame.
533
+ _need(n) {
534
+ if (this._stage !== null && this._stageLen + n <= this._stage.length) {
535
+ return;
536
+ }
537
+ this._commit();
538
+ let cap = this._stageCap;
539
+ while (cap < n) {
540
+ cap *= 2;
541
+ }
542
+ this._stageCap = Math.min(cap, MAX_OUT_BATCH);
543
+ this._stage = Buffer.allocUnsafe(cap);
544
+ }
545
+
546
+ _commit() {
547
+ if (this._stageLen === 0) {
548
+ return;
549
+ }
550
+ this._outQ.push(this._stage.subarray(0, this._stageLen));
551
+ this._stage = null; // the queued bytes are not ours to write
552
+ this._stageLen = 0;
553
+ }
554
+
555
+ // Frames produced outside a read batch -- a handler that answers from the
556
+ // 'end' callback (which is a nextTick emission) or after an await -- would
557
+ // otherwise sit in the queue until the next request arrived. Node drains the
558
+ // tick queue before microtasks, so one microtask per batch still coalesces
559
+ // everything produced in the same turn of the loop into a single write().
560
+ _schedule() {
561
+ if (this._flushScheduled) {
562
+ return;
563
+ }
564
+ this._flushScheduled = true;
565
+ queueMicrotask(this._flushSoon);
566
+ }
567
+
568
+ _flush() {
569
+ // leaving a batch, whatever the reason: from here on frames are produced
570
+ // outside a socket read and have to schedule their own write
571
+ this._inBatch = false;
572
+ this._writePending();
573
+ }
574
+
575
+ // Hands the queued frames to the socket. The batch is written corked, so the whole
576
+ // of it goes out through one _writev -- both transports implement it (net.Socket's
577
+ // own cork() bumps state.corked, which is what makes clearBuffer() take the vector
578
+ // path), and that is what keeps a HEADERS + DATA + WINDOW_UPDATE batch in a single
579
+ // TCP segment even though the frames were never concatenated into one buffer.
580
+ //
581
+ // Still only one batch at a time: the buffers we hand over belong to the socket
582
+ // until the callback fires -- a TLS socket encrypts at flush time, which can be well
583
+ // after the next socket read -- and an application that keeps writing to a buffer it
584
+ // passed to stream.end() would otherwise change bytes already promised. The queue
585
+ // fills up behind the outstanding write and the cap in sendData() defers streams for
586
+ // lack of room, which _onWritten() restarts.
587
+ _writePending() {
588
+ if (this._outLen === 0) {
589
+ return;
590
+ }
591
+ if (!this._open) {
592
+ this._outQ = [];
593
+ this._stage = null;
594
+ this._stageLen = 0;
595
+ this._outLen = 0;
596
+ this._writing = false;
597
+ return;
598
+ }
599
+ if (this._writing) {
600
+ return;
601
+ }
602
+ this._writing = true;
603
+ this._commit();
604
+ const batch = this._outQ;
605
+ this._outQ = [];
606
+ this._frozenLen = this._outLen;
607
+ this._outLen = 0;
608
+ const socket = this.socket;
609
+ if (batch.length === 1) {
610
+ // the common case for a small response: everything the batch holds went into one
611
+ // staging buffer, so there is nothing to coalesce and corking would only add a
612
+ // round trip through the stream's own buffer (OPTIMIZATION.md item 16)
613
+ socket.write(batch[0], this._onWrittenCb);
614
+ } else {
615
+ // one callback for the batch, put on its last chunk: writes complete in order, so
616
+ // the last one is the point at which every buffer in it is the socket's no longer
617
+ socket.cork();
618
+ for (let i = 0; i < batch.length - 1; i++) {
619
+ socket.write(batch[i]);
620
+ }
621
+ socket.write(batch[batch.length - 1], this._onWrittenCb);
622
+ socket.uncork();
623
+ }
624
+ }
625
+
626
+ _onWritten() {
627
+ this._writing = false;
628
+ // Dropping the batch is the whole release: the frames were never assembled into
629
+ // anything shared, so there is no tail to move down and nothing to reset.
630
+ this._frozenLen = 0;
631
+ // Room in the queue is a reason to stall a stream, so its release is also a
632
+ // reason to restart one: without this, a stream deferred for lack of room has
633
+ // no event that ever looks at it again unless the peer also owes it credit.
634
+ this._releaseAll();
635
+ this._writePending();
636
+ // NOT here, and this is a measured trade-off rather than an oversight. Finishing the close
637
+ // as soon as our own write lands does two things at once: it lets a peer that says nothing
638
+ // after our GOAWAY go away (the session is destroyed and the FIN goes out, which is what node
639
+ // does -- see the silent-peer case in test/goaway.test.js), and it also destroys the session
640
+ // before the peer's answer to the GOAWAY can be read, which costs seventeen event-order
641
+ // traces that agree with node today. Deferring the same look-over with `setImmediate(this...)`,
642
+ // which is how node re-runs it (`setImmediate(() => session[kMaybeDestroy]())` in Http2Stream's
643
+ // teardown, "Delay with setImmediate so we don't do it on the nghttp2 stack"), still costs
644
+ // seven: the session 'close' then goes out ahead of the peer's, in the idle-timeout and goaway
645
+ // traces. The read-side finish (`_onGoaway` -> close) and the stream teardown are therefore the
646
+ // only re-checks, and a silent peer keeps the connection. Closing that gap means re-deriving
647
+ // the traces against node, not moving this call around -- and part of it is not ours to close:
648
+ // see tools/probe-stream-teardown.js for node closing a server stream whose request body was
649
+ // never read, the trigger its own re-check waits for.
650
+ }
651
+
652
+ _sendSettings() {
653
+ const s = this.localSettings;
654
+ // Only what the application set, in the order it set it -- see _advertised in the constructor.
655
+ // An empty list is a legal and, for node, the usual SETTINGS frame: a zero-length payload.
656
+ const names = [...this._advertised].filter((k) => ID_BY_SETTING[k] !== undefined
657
+ && s[k] !== undefined);
658
+ // Six octets per setting (RFC 9113 6.5), so the size is known before the loop and
659
+ // every octet of it is written here -- allocUnsafe rather than alloc, which would
660
+ // zero memory we are about to fill. Once per session, so this is the code saying
661
+ // what it means; there is no measurable claim attached to it.
662
+ const body = Buffer.allocUnsafe(names.length * 6);
663
+ for (let i = 0; i < names.length; i++) {
664
+ const name = names[i];
665
+ const value = typeof s[name] === 'boolean' ? (s[name] ? 1 : 0) : s[name];
666
+ const at = i * 6;
667
+ body.writeUInt16BE(ID_BY_SETTING[name], at);
668
+ body.writeUInt32BE(value >>> 0, at + 2);
669
+ }
670
+ this._frame(SETTINGS, 0, 0, body, true);
671
+ this.pendingSettingsAck = true;
672
+ this._flush();
673
+ }
674
+
675
+ sendHeaders(sid, block, end) {
676
+ this.state.deflateDynamicTableSize = this._encoder.size;
677
+ const max = this.remoteSettings.maxFrameSize;
678
+ if (block.length <= max) {
679
+ this._frame(HEADERS, F_END_HEADERS | (end ? F_END_STREAM : 0), sid, block);
680
+ return;
681
+ }
682
+ this._frame(HEADERS, 0, sid, block.subarray(0, max));
683
+ let off = max;
684
+ for (;;) {
685
+ const n = Math.min(block.length - off, max);
686
+ const last = off + n >= block.length;
687
+ this._frame(CONTINUATION, last ? F_END_HEADERS | (end ? F_END_STREAM : 0) : 0,
688
+ sid, block.subarray(off, off + n));
689
+ off += n;
690
+ if (last) {
691
+ return;
692
+ }
693
+ }
694
+ }
695
+
696
+ // Writes DATA within the stream and connection windows. Returns true when
697
+ // everything went out; false means the caller should wait for WINDOW_UPDATE.
698
+ sendData(stream, buf, end) {
699
+ if (stream._queued !== null) { // preserve order behind a stall
700
+ if (buf.length > 0) {
701
+ stream._queued.push(buf);
702
+ this._heldBytes += buf.length;
703
+ }
704
+ if (end) {
705
+ stream._endQueued = true;
706
+ }
707
+ return false;
708
+ }
709
+ const total = buf.length;
710
+ const max = this.remoteSettings.maxFrameSize;
711
+ let off = 0;
712
+ while (off < total) {
713
+ // Room in the queue is a third limit next to the two windows. Without it every
714
+ // stream produced its whole body while one write was still in flight, because
715
+ // _flush() cannot do anything while writing: 64 connections serving 256 KB
716
+ // responses staged 16 MB each, put 8 MB into a single write(), and moved
717
+ // 251 KB per request through the compaction that has just been removed.
718
+ const room = Math.min(this._connSendWin, stream._sendWin,
719
+ MAX_OUT_BATCH - this._outLen - HEADER_LEN);
720
+ const take = Math.min(room, max, total - off);
721
+ if (take <= 0) {
722
+ break;
723
+ }
724
+ const last = end && off + take === total;
725
+ // owned: a view of the application's buffer, which is not copied any more
726
+ this._frame(DATA, last ? F_END_STREAM : 0, stream.id, buf.subarray(off, off + take),
727
+ true);
728
+ this._connSendWin -= take;
729
+ stream._sendWin -= take;
730
+ off += take;
731
+ }
732
+ const done = off >= total;
733
+ // an empty body still owes the peer END_STREAM; go through _frame() so that
734
+ // a response finished outside a read batch also gets its write scheduled
735
+ if (done && end && total === 0) {
736
+ this._frame(DATA, F_END_STREAM, stream.id, null);
737
+ }
738
+ if (!done) {
739
+ stream._queued = [buf.subarray(off)];
740
+ this._heldBytes += total - off;
741
+ if (end) {
742
+ stream._endQueued = true;
743
+ }
744
+ if (this._blocked === null) {
745
+ this._blocked = new Set();
746
+ }
747
+ this._blocked.add(stream);
748
+ this.state.outboundQueueSize = this._blocked.size;
749
+ } else if (end) {
750
+ stream._endLocalSide();
751
+ }
752
+ this.state.remoteWindowSize = this._connSendWin;
753
+ if (this._outLen >= 32 * 1024) {
754
+ this._flush();
755
+ }
756
+ return done && this._connSendWin > 0 && stream._sendWin > 0;
757
+ }
758
+
759
+ sendRst(sid, code, reason) {
760
+ const tail = code !== 0 && reason ? Buffer.byteLength(reason) : 0;
761
+ const body = Buffer.alloc(4 + tail);
762
+ body.writeUInt32BE(code, 0);
763
+ if (tail) {
764
+ body.write(reason, 4);
765
+ }
766
+ this._frame(RST_STREAM, 0, sid, body, true);
767
+ }
768
+
769
+ sendGoaway(lastStreamID, code, reason) {
770
+ // Writing a GOAWAY is not an event: node emits 'goaway' only when one is RECEIVED
771
+ // (internal/http2/core.js:769, and the comment above Http2Session.prototype.goaway says
772
+ // "sending a goaway frame has no other effect"), so the application's 'goaway' listener is
773
+ // the peer's decision, never our own. Announcing our own here made a listener fire on a
774
+ // shutdown it had no part in.
775
+ const tail = reason ? Buffer.byteLength(reason) : 0;
776
+ const body = Buffer.alloc(8 + tail);
777
+ body.writeUInt32BE(lastStreamID >>> 0, 0);
778
+ body.writeUInt32BE(code, 4);
779
+ if (tail) {
780
+ body.write(reason, 8);
781
+ }
782
+ this._frame(GOAWAY, 0, 0, body, true);
783
+ this._goawaySent = true;
784
+ }
785
+
786
+ _windowUpdate(sid, inc) {
787
+ if (inc <= 0) {
788
+ return;
789
+ }
790
+ this._need(HEADER_LEN + 4);
791
+ const off = this._stageLen;
792
+ writeHeader(this._stage, off, 4, WINDOW_UPDATE, 0, sid);
793
+ this._stage.writeUInt32BE(inc, off + HEADER_LEN);
794
+ this._stageLen = off + HEADER_LEN + 4;
795
+ this._outLen += HEADER_LEN + 4;
796
+ // same rule as _frame(): a credit released between batches has to reach the
797
+ // socket by itself, or the sender waits for a window update we never wrote
798
+ if (!this._inBatch) {
799
+ this._schedule();
800
+ }
801
+ }
802
+
803
+ // ---- inbound ----------------------------------------------------------------
804
+ _onData(chunk) {
805
+ const n = chunk.length;
806
+ let i = 0;
807
+ this._inBatch = true;
808
+ this._ended.length = 0; // the last batch's finished streams are no longer here
809
+ this._touchIdleTimer();
810
+
811
+ if (this._prefaceSeen < PREFACE_LEN) {
812
+ const take = Math.min(PREFACE_LEN - this._prefaceSeen, n);
813
+ const expected = f.PREFACE.subarray(this._prefaceSeen, this._prefaceSeen + take);
814
+ if (!chunk.subarray(0, take).equals(expected)) {
815
+ return this._fatal(PROTOCOL_ERROR, 'connection preface malformed');
816
+ }
817
+ this._prefaceSeen += take;
818
+ // node's server-side session fires 'connect' once the peer's preface is in,
819
+ // before any SETTINGS have been decoded
820
+ if (this._prefaceSeen === PREFACE_LEN) {
821
+ this.emit('connect');
822
+ }
823
+ i = take;
824
+ // the chunk stopped inside the preface: nothing was produced, but the
825
+ // batch flag must not leak into whatever the peer sends next
826
+ if (i >= n) {
827
+ this._endBatch(); return;
828
+ }
829
+ }
830
+
831
+ while (i < n) {
832
+ if (this._frag > 0) {
833
+ // continuing a frame that straddles reads: take no more than it wants
834
+ const want = this._fragTarget === null
835
+ ? HEADER_LEN - this._frag
836
+ : this._fragTarget - this._frag;
837
+ const take = Math.min(want, n - i);
838
+ if (take <= 0) {
839
+ break;
840
+ }
841
+ if (this._fragPieces !== null) {
842
+ this._fragPieces.push(chunk.subarray(i, i + take));
843
+ } else {
844
+ // the frame's header is still incomplete: its octets wait in the
845
+ // scratch until the header is whole
846
+ chunk.copy(this._fragHdr, this._frag, i, i + take);
847
+ }
848
+ this._frag += take;
849
+ i += take;
850
+ if (this._fragTarget === null && this._frag === HEADER_LEN) {
851
+ readHeader(this._fragHdr, 0);
852
+ if (!this._allowFrame(f.FH_TYPE, f.FH_LEN, f.FH_FLAGS)) {
853
+ return;
854
+ }
855
+ this._fragTarget = HEADER_LEN + f.FH_LEN;
856
+ this._fragType = f.FH_TYPE;
857
+ this._fragFlags = f.FH_FLAGS;
858
+ this._fragSid = f.FH_SID;
859
+ // The payload of the frame is kept as the views it arrives in: a DATA
860
+ // payload is handed to the application that way, and a header block is
861
+ // decoded out of them (lib/hpack.js decodePieces).
862
+ this._fragPieces = [];
863
+ }
864
+ if (this._fragTarget !== null && this._frag === this._fragTarget) {
865
+ const pieces = this._fragPieces;
866
+ const len = this._fragTarget - HEADER_LEN;
867
+ const ok = this._dispatchPieces(this._fragType, this._fragFlags, this._fragSid, pieces, len);
868
+ this._frag = 0;
869
+ this._fragTarget = null;
870
+ this._fragPieces = null;
871
+ if (!ok) {
872
+ return;
873
+ }
874
+ }
875
+ continue;
876
+ }
877
+
878
+ if (n - i < HEADER_LEN) { // even the header is incomplete
879
+ this._fragTarget = null;
880
+ this._fragPieces = null;
881
+ this._frag = n - i;
882
+ chunk.copy(this._fragHdr, 0, i, n);
883
+ break;
884
+ }
885
+
886
+ readHeader(chunk, i);
887
+ if (!this._allowFrame(f.FH_TYPE, f.FH_LEN, f.FH_FLAGS)) {
888
+ return;
889
+ }
890
+ const total = HEADER_LEN + f.FH_LEN;
891
+ if (n - i < total) { // header known, payload straddles
892
+ this._fragTarget = total;
893
+ this._fragType = f.FH_TYPE;
894
+ this._fragFlags = f.FH_FLAGS;
895
+ this._fragSid = f.FH_SID;
896
+ this._frag = n - i;
897
+ // The payload is not contiguous: it starts in this chunk and ends in a
898
+ // later one. It is kept as the views it is and handed over as the pieces
899
+ // it arrived in, which is what node does with the same frame. A header
900
+ // block is decoded out of them, so it costs no copy either.
901
+ this._fragPieces = [chunk.subarray(i + HEADER_LEN, n)];
902
+ break;
903
+ }
904
+ if (!this._dispatch(chunk, i + HEADER_LEN, f.FH_LEN, f.FH_TYPE, f.FH_FLAGS, f.FH_SID)) {
905
+ return;
906
+ }
907
+ i += total;
908
+ if (!this._open) {
909
+ return;
910
+ }
911
+ }
912
+
913
+ this._endBatch();
914
+ }
915
+
916
+ _allowFrame(type, len, flags) {
917
+ // RFC 9113 3.4 requires the client's first frame to be a SETTINGS frame, and nghttp2
918
+ // -- the stack node:http2 runs -- enforces it literally: once the connection preface
919
+ // has been read, the next frame must be SETTINGS and must not carry the ACK flag,
920
+ // otherwise the session is terminated with GOAWAY(PROTOCOL_ERROR) and the debug
921
+ // string "SETTINGS expected" (lib/nghttp2_session.c v1.70.0, the
922
+ // NGHTTP2_IB_READ_FIRST_SETTINGS state). Measured against a live node server: HEADERS,
923
+ // PING and a SETTINGS-with-ACK each get exactly that, where this accepted all three.
924
+ // It is worth the check because the settings parameterise everything that follows --
925
+ // initial window, header table size, concurrent streams -- so a request answered
926
+ // before any of them is announced is work done against numbers that may still move.
927
+ if (this._firstFramePending) {
928
+ this._firstFramePending = false;
929
+ if (type !== SETTINGS || (flags & F_ACK) !== 0) {
930
+ this._fatal(PROTOCOL_ERROR, 'SETTINGS expected');
931
+ return false;
932
+ }
933
+ }
934
+ if (type !== PING && type !== SETTINGS && len > this.localSettings.maxFrameSize) {
935
+ this._fatal(FRAME_SIZE_ERROR, 'frame larger than our advertised max frame size');
936
+ return false;
937
+ }
938
+ return true;
939
+ }
940
+
941
+ _dispatch(buf, start, len, type, flags, sid) {
942
+ try {
943
+ this._handle(type, flags, sid, buf, start, len);
944
+ } catch (err) {
945
+ return this._onFrameError(err, sid);
946
+ }
947
+ return this._open;
948
+ }
949
+
950
+ // A frame whose payload straddled two reads is dispatched as the pieces it
951
+ // arrived in. A DATA payload is handed to the application as those pieces; a
952
+ // header block is decoded out of them; the control frames -- small and of a
953
+ // fixed size -- are put in a buffer of their own for the parse, which is the
954
+ // parse the whole-frame path uses.
955
+ _dispatchPieces(type, flags, sid, pieces, len) {
956
+ try {
957
+ // RFC 9113 6.10: the same rule the whole-frame path applies (see _handle).
958
+ // A header block that is unfinished may only be followed by CONTINUATION.
959
+ if (this._hdrSid !== -1 && type !== CONTINUATION) {
960
+ this._fatal(PROTOCOL_ERROR, 'frame interleaved with an unfinished header block');
961
+ return this._open;
962
+ }
963
+ if (type === DATA) {
964
+ // RFC 9113 6.1: a padded payload's views are not the payload -- the pad
965
+ // length is at the front and the padding at the back -- so the padding
966
+ // comes off the views and the flow control counts the data alone.
967
+ let data = len;
968
+ if (flags & F_PADDED) {
969
+ // A padded frame with no payload has no pad length to read, and node
970
+ // answers that with GOAWAY(PROTOCOL_ERROR) (measured, tools/probe-pad-bad.js).
971
+ if (len === 0) {
972
+ this._fatal(PROTOCOL_ERROR, 'padded DATA frame with an empty payload');
973
+ return this._open;
974
+ }
975
+ data = trimPadding(pieces);
976
+ if (data < 0) {
977
+ this._fatal(PROTOCOL_ERROR, 'pad larger than the DATA payload');
978
+ return this._open;
979
+ }
980
+ }
981
+ this._onDataPieces(sid, flags, pieces, data);
982
+ return this._open;
983
+ }
984
+ if (type === HEADERS) {
985
+ this._beginHeadersPieces(sid, flags, pieces, len);
986
+ return this._open;
987
+ }
988
+ if (type === CONTINUATION) {
989
+ // the same two rules the whole-frame path applies (see _handle)
990
+ if (this._hdrSid !== sid) {
991
+ this._fatal(PROTOCOL_ERROR, 'CONTINUATION without a matching HEADERS');
992
+ return this._open;
993
+ }
994
+ // CVE-2024-28182: one block may not be spread over an unbounded number of frames.
995
+ // Counted here rather than by size because the cost being bounded is the frame
996
+ // handling itself; the byte bound of _appendHdr stays as it is, for the other one.
997
+ if (++this._hdrCont > MAX_CONTINUATIONS) {
998
+ this._fatal(INTERNAL_ERROR, 'CONTINUATION: too many frames for one block');
999
+ return this._open;
1000
+ }
1001
+ this._appendHdrPieces(pieces, 0, len);
1002
+ if (flags & F_END_HEADERS) {
1003
+ this._finishHeaders();
1004
+ }
1005
+ return this._open;
1006
+ }
1007
+ // the control frames: the payload goes into a buffer of its own and the
1008
+ // parse is the one _handle does
1009
+ const buf = pieceConcat(pieces, 0, len);
1010
+ this._handle(type, flags, sid, buf, 0, len);
1011
+ } catch (err) {
1012
+ return this._onFrameError(err, sid);
1013
+ }
1014
+ return this._open;
1015
+ }
1016
+
1017
+ _handle(type, flags, sid, buf, start, len) {
1018
+ // RFC 9113 6.10: once a header block has begun, the only frames that may follow are
1019
+ // CONTINUATION frames for the same stream. An interruption is a connection error, not
1020
+ // a thing to skip over -- nghttp2 answers it with
1021
+ // GOAWAY(PROTOCOL_ERROR) 'unexpected non-CONTINUATION frame or stream_id is invalid',
1022
+ // measured on node for a SETTINGS, PING, WINDOW_UPDATE, RST_STREAM and DATA frame all
1023
+ // five. Note that this also covers HEADERS, which used to have its own check here.
1024
+ if (this._hdrSid !== -1 && type !== CONTINUATION) {
1025
+ this._fatal(PROTOCOL_ERROR, 'frame interleaved with an unfinished header block');
1026
+ return;
1027
+ }
1028
+ switch (type) {
1029
+ case SETTINGS:
1030
+ // RFC 9113 6.5: SETTINGS is a connection control frame -- it names no stream, and an
1031
+ // ACK carries no entries. Measured on node (h2spec http2/6.5), a SETTINGS on stream 1
1032
+ // gets GOAWAY(PROTOCOL_ERROR) and a SETTINGS|ACK with one octet of payload gets
1033
+ // GOAWAY(FRAME_SIZE_ERROR). This answered both with an ACK of its own and kept the
1034
+ // connection up, so a peer that had misframed its own settings never heard about it.
1035
+ if (sid !== 0) {
1036
+ this._fatal(PROTOCOL_ERROR, 'SETTINGS with a non-zero stream id');
1037
+ return;
1038
+ }
1039
+ if (flags & F_ACK) {
1040
+ if (len !== 0) {
1041
+ this._fatal(FRAME_SIZE_ERROR, 'SETTINGS ACK must have an empty payload');
1042
+ return;
1043
+ }
1044
+ this.pendingSettingsAck = false;
1045
+ return;
1046
+ }
1047
+ this._applySettings(buf, start, len);
1048
+ this.emit('remoteSettings', this.remoteSettings);
1049
+ this._frame(SETTINGS, F_ACK, 0, null);
1050
+ return;
1051
+ case PING: {
1052
+ // RFC 9113 6.7 puts the same two requirements on both halves of the exchange: stream
1053
+ // id 0 and a payload of exactly 8 octets. Measured on node (h2spec http2/6.7 and
1054
+ // 5.4.1): a PING on stream 3 gets GOAWAY(PROTOCOL_ERROR), and a PING|ACK with 4 octets
1055
+ // gets GOAWAY(FRAME_SIZE_ERROR). This ACKed the first -- echoing an identifier it is not
1056
+ // allowed to interpret -- and answered the second with nothing at all.
1057
+ if (sid !== 0) {
1058
+ this._fatal(PROTOCOL_ERROR, 'PING with a non-zero stream id'); return;
1059
+ }
1060
+ if (len !== 8) {
1061
+ this._fatal(FRAME_SIZE_ERROR, 'PING payload must be 8 octets'); return;
1062
+ }
1063
+ // The payload goes to the application, which may keep it. A PING that arrived
1064
+ // in pieces is put in a buffer of its own for the parse -- a fresh one, not a
1065
+ // reused one -- so the view does not change under the application
1066
+ // (AGENTS.md 5: issued buffers stay valid). Eight octets, once per PING: a
1067
+ // copy is the cheap way out, and the copy is made once and handed to both
1068
+ // halves of the exchange. Node's payload comes out of the C++ layer as a
1069
+ // fresh buffer, so it is stable there.
1070
+ // copy through allocUnsafe rather than Buffer.from(subarray): the payload goes
1071
+ // to the application and the ACK frame, so it must be a fresh buffer either
1072
+ // way, and the pooled allocation of a small buffer is the cheaper way to get
1073
+ // one -- see the block cache in lib/headers.js for the same measurement
1074
+ const payload = Buffer.allocUnsafe(8);
1075
+ buf.copy(payload, 0, start, start + 8);
1076
+ if (flags & F_ACK) {
1077
+ this.emit('ping', true, payload);
1078
+ return;
1079
+ }
1080
+ this._frame(PING, F_ACK, 0, payload);
1081
+ this.emit('ping', false, payload);
1082
+ return;
1083
+ }
1084
+ case HEADERS:
1085
+ this._beginHeaders(sid, flags, buf, start, len);
1086
+ return;
1087
+ case CONTINUATION:
1088
+ if (this._hdrSid !== sid) {
1089
+ this._fatal(PROTOCOL_ERROR, 'CONTINUATION without a matching HEADERS');
1090
+ return;
1091
+ }
1092
+ // CVE-2024-28182: one block may not be spread over an unbounded number of frames.
1093
+ // Counted here rather than by size because the cost being bounded is the frame
1094
+ // handling itself; the byte bound of _appendHdr stays as it is, for the other one.
1095
+ if (++this._hdrCont > MAX_CONTINUATIONS) {
1096
+ this._fatal(INTERNAL_ERROR, 'CONTINUATION: too many frames for one block');
1097
+ return;
1098
+ }
1099
+ this._appendHdr(buf, start, len);
1100
+ if (flags & F_END_HEADERS) {
1101
+ this._finishHeaders();
1102
+ }
1103
+ return;
1104
+ case DATA:
1105
+ this._onDataFrame(sid, flags, buf, start, len);
1106
+ return;
1107
+ case WINDOW_UPDATE:
1108
+ if (len !== 4) {
1109
+ this._fatal(FRAME_SIZE_ERROR, 'WINDOW_UPDATE payload'); return;
1110
+ }
1111
+ this._onWindowUpdate(sid, buf.readUInt32BE(start) & 0x7fffffff);
1112
+ return;
1113
+ case RST_STREAM:
1114
+ if (len !== 4) {
1115
+ this._fatal(FRAME_SIZE_ERROR, 'RST_STREAM payload'); return;
1116
+ }
1117
+ this._onRst(sid, buf.readUInt32BE(start));
1118
+ return;
1119
+ case GOAWAY:
1120
+ this._onGoaway(buf, start, len);
1121
+ return;
1122
+ case PRIORITY:
1123
+ // RFC 9113 6.3: the payload is fixed at 5 octets, and the frame always names a stream --
1124
+ // a PRIORITY received on stream 0 MUST be answered with a connection error of type
1125
+ // PROTOCOL_ERROR. The frame's CONTENTS are deprecated (RFC 9218) and ignored below, but
1126
+ // neither its length nor its stream id is ignorable: measured on node (h2spec http2/6.3),
1127
+ // a 4-octet PRIORITY gets GOAWAY(FRAME_SIZE_ERROR), while node silently drops a PRIORITY
1128
+ // on stream 0 -- the one place the parity rule and the RFC disagree, so we follow the RFC.
1129
+ // The contents also carry the self-dependency RFC 7540 5.3.1 made a stream error of type
1130
+ // PROTOCOL_ERROR, and that rule is gone: RFC 9113 5.3.2 deprecated the priority
1131
+ // signalling and 6.3 keeps only the two rules above. Both stacks drop a self-dependent
1132
+ // PRIORITY (probe case K4: ACK ANSWERED, the frame ignored) and h2spec's http2/5.3.1 arm
1133
+ // still asks for RFC 7540's PROTOCOL_ERROR -- do not bend this to the tool (AGENTS.md 10).
1134
+ if (len !== 5) {
1135
+ this._fatal(FRAME_SIZE_ERROR, 'PRIORITY payload must be 5 octets'); return;
1136
+ }
1137
+ if (sid === 0) {
1138
+ this._fatal(PROTOCOL_ERROR, 'PRIORITY on stream 0'); return;
1139
+ }
1140
+ return; // RFC 9218: weight-only, ignored
1141
+ case PUSH_PROMISE:
1142
+ this._fatal(PROTOCOL_ERROR, 'peer sent PUSH_PROMISE');
1143
+ break;
1144
+
1145
+ default:
1146
+ // RFC 9113 5.5: "Implementations MUST discard frames that have unknown or unsupported
1147
+ // types." Measured on a live node server with tools/probe-inbound-limits.js: type 10
1148
+ // (ALTSVC, RFC 7807), type 99 and type 0xff with a 4 KiB payload all leave the
1149
+ // connection up and the next request served, and h2lean answers the same. Answering a
1150
+ // fatal error here would break every extension frame a peer has not told us about.
1151
+ // What is NOT ignored is a frame that interrupts a field block -- 5.5 forbids those
1152
+ // outright, and that is the case above.
1153
+ // unknown frame types are ignored
1154
+ }
1155
+ }
1156
+
1157
+ _applySettings(buf, start, len) {
1158
+ if (len % 6 !== 0) {
1159
+ this._fatal(FRAME_SIZE_ERROR, 'SETTINGS payload size'); return;
1160
+ }
1161
+ // CVE-2020-11080: the frame size limit does not cover SETTINGS (_allowFrame exempts
1162
+ // it, as RFC 9113 6.2 requires), so without an entry bound a peer can hand us any
1163
+ // number of entries to parse. 32 is what node accepts; measured, see the constant.
1164
+ if (len / 6 > MAX_SETTINGS_ENTRIES) {
1165
+ this._fatal(ENHANCE_YOUR_CALM, 'SETTINGS: too many setting entries');
1166
+ return;
1167
+ }
1168
+ const seen = this.remoteSettings;
1169
+ for (let i = 0; i + 6 <= len; i += 6) {
1170
+ const id = buf.readUInt16BE(start + i);
1171
+ const value = buf.readUInt32BE(start + i + 2);
1172
+ const name = SETTINGS_BY_ID[id];
1173
+ if (name === undefined) {
1174
+ continue;
1175
+ } // unknown settings are ignored
1176
+ if (id === 0x5 && (value < 16384 || value > 16777215)) {
1177
+ this._fatal(PROTOCOL_ERROR, 'SETTINGS_MAX_FRAME_SIZE out of range');
1178
+ return;
1179
+ }
1180
+ if (id === 0x4 && value > 0x7fffffff) {
1181
+ this._fatal(FLOW_CONTROL_ERROR, 'SETTINGS_INITIAL_WINDOW_SIZE out of range');
1182
+ return;
1183
+ }
1184
+ if (id === 0x1 && value > 0x7fffffff) {
1185
+ this._fatal(COMPRESSION_ERROR, 'SETTINGS_HEADER_TABLE_SIZE out of range');
1186
+ return;
1187
+ }
1188
+ // RFC 9113 6.5.2 defines SETTINGS_ENABLE_PUSH as a boolean, and 6.5.1 makes a value
1189
+ // outside its definition a PROTOCOL_ERROR. Measured on node (h2spec http2/6.5.2):
1190
+ // ENABLE_PUSH=2 ends the connection; an unknown setting id is still ignored above.
1191
+ if (id === 0x2 && value > 1) {
1192
+ this._fatal(PROTOCOL_ERROR, 'SETTINGS_ENABLE_PUSH must be 0 or 1');
1193
+ return;
1194
+ }
1195
+ seen[name] = value;
1196
+ }
1197
+ // RFC 9113 6.9.2: a new INITIAL_WINDOW_SIZE shifts every open stream window
1198
+ // by the same delta, and may legitimately drive it negative
1199
+ const delta = seen.initialWindowSize - this._prevInitialWin;
1200
+ if (delta !== 0) {
1201
+ this._prevInitialWin = seen.initialWindowSize;
1202
+ for (const s of this._streams.values()) {
1203
+ s._sendWin += delta;
1204
+ }
1205
+ }
1206
+ // Only the encoder's table is bounded by the peer's setting. Our own decoder keeps
1207
+ // the size WE advertised, and the peer changes it by sending a size update, which
1208
+ // Decoder.setMaxSize checks against that -- resizing the decoder here instead made
1209
+ // us evict entries a peer with a small table had legitimately indexed for us.
1210
+ this._encoder.setPeerTableSize(seen.headerTableSize);
1211
+ this.state.remoteWindowSize = this._connSendWin;
1212
+ }
1213
+
1214
+ // ---- HEADERS assembly -------------------------------------------------------
1215
+ // The pad length and the priority block at the front of a HEADERS payload
1216
+ // (RFC 9113 6.2 and 6.3). `head` is the buffer the payload's first octets are
1217
+ // in, `base` the offset of the payload inside it and `headLen` how many of the
1218
+ // payload's octets are there: the whole-frame path has all of them, the split
1219
+ // path copies the first six into a scratch. Returns the offset of the header
1220
+ // block inside the payload, or -1 after refusing the frame.
1221
+ //
1222
+ // The length of the block itself is left in _hdrFrag, because it is not the
1223
+ // rest of the payload: a padded frame carries its padding at the END of the
1224
+ // payload (RFC 9113 6.2: "The remainder of the frame payload is the header
1225
+ // block fragment"), so the block ends where the padding starts. Feeding the
1226
+ // padding to the HPACK decoder is what a server must not do: the padding is
1227
+ // not part of the block, and a block that ends with eight zero octets decodes
1228
+ // as a literal header with no name and no value -- which is what h2lean
1229
+ // answered to a valid padded HEADERS with GOAWAY(COMPRESSION_ERROR), while node
1230
+ // served the request (measured: tools/probe-hdr-pad.js, "the RFC shape: pad
1231
+ // length, block, padding"; the same frame is what h2spec's generic/3.2 sends,
1232
+ // and h2spec is the check that failed).
1233
+ _headersPrefix(head, base, headLen, sid, flags, len) {
1234
+ let p = 0;
1235
+ let l = len;
1236
+ if (flags & F_PADDED) {
1237
+ // A padded frame with no payload has no pad length octet to read at all,
1238
+ // and node answers that with GOAWAY(PROTOCOL_ERROR) (measured,
1239
+ // tools/probe-hdr-pad.js: 'HEADERS: insufficient padding space').
1240
+ if (headLen < 1) {
1241
+ this._fatal(PROTOCOL_ERROR, 'padded HEADERS frame with an empty payload');
1242
+ return -1;
1243
+ }
1244
+ const pad = head[base];
1245
+ p += 1;
1246
+ l -= 1 + pad;
1247
+ if (l < 0) {
1248
+ this._fatal(PROTOCOL_ERROR, 'pad larger than the HEADERS payload'); return -1;
1249
+ }
1250
+ }
1251
+ if (flags & F_PRIORITY) {
1252
+ // The 5 octets were skipped without ever being read, which is how both of the rules
1253
+ // RFC 9113 5.3 puts on them went unenforced. Measured on node (h2spec http2/5.3.1 and
1254
+ // 6.3): a HEADERS frame whose priority block is truncated is GOAWAY(FRAME_SIZE_ERROR),
1255
+ // and a stream that depends on itself is GOAWAY(PROTOCOL_ERROR) -- a connection error,
1256
+ // not a stream one, because the dependency is a statement about the whole tree.
1257
+ if (l < 5) {
1258
+ this._fatal(FRAME_SIZE_ERROR, 'HEADERS priority block must be 5 octets');
1259
+ return -1;
1260
+ }
1261
+ if ((head.readUInt32BE(base + p) & 0x7fffffff) === sid) {
1262
+ this._fatal(PROTOCOL_ERROR, 'HEADERS depends on itself');
1263
+ return -1;
1264
+ }
1265
+ p += 5; l -= 5;
1266
+ }
1267
+ this._hdrFrag = l;
1268
+ return p;
1269
+ }
1270
+
1271
+ _beginHeaders(sid, flags, buf, start, len) {
1272
+ if ((sid & 1) === 0) {
1273
+ this._fatal(PROTOCOL_ERROR, 'even-numbered stream id from a client');
1274
+ return;
1275
+ }
1276
+ this._hdrCont = 0; // one block, however many frames it takes
1277
+ const p = this._headersPrefix(buf, start, len, sid, flags, len);
1278
+ if (p < 0) {
1279
+ return;
1280
+ }
1281
+ const l = this._hdrFrag;
1282
+ if (flags & F_END_HEADERS) {
1283
+ this._processHeaders(sid, (flags & F_END_STREAM) !== 0, buf.subarray(start + p, start + p + l));
1284
+ return;
1285
+ }
1286
+ this._hdrSid = sid;
1287
+ this._hdrEnd = (flags & F_END_STREAM) !== 0;
1288
+ this._hdrLen = 0;
1289
+ this._appendHdr(buf, start + p, l);
1290
+ }
1291
+
1292
+ // A HEADERS frame whose payload straddled two reads: the pad length and the
1293
+ // priority block are at its front and are at most six octets, so they are
1294
+ // copied into the scratch and read there. The header block itself stays in
1295
+ // the views and is decoded out of them.
1296
+ _beginHeadersPieces(sid, flags, pieces, len) {
1297
+ if ((sid & 1) === 0) {
1298
+ this._fatal(PROTOCOL_ERROR, 'even-numbered stream id from a client');
1299
+ return;
1300
+ }
1301
+ this._hdrCont = 0;
1302
+ const pre = len < 6 ? len : 6;
1303
+ pieceCopyTo(this._hdrPrefix, 0, pieces, 0, pre);
1304
+ const p = this._headersPrefix(this._hdrPrefix, 0, pre, sid, flags, len);
1305
+ if (p < 0) {
1306
+ return;
1307
+ }
1308
+ const l = this._hdrFrag;
1309
+ if (flags & F_END_HEADERS) {
1310
+ this._processBlockPieces(sid, (flags & F_END_STREAM) !== 0, pieces, p, p + l);
1311
+ return;
1312
+ }
1313
+ this._hdrSid = sid;
1314
+ this._hdrEnd = (flags & F_END_STREAM) !== 0;
1315
+ this._hdrLen = 0;
1316
+ this._appendHdrPieces(pieces, p, p + l);
1317
+ }
1318
+
1319
+ _appendHdr(buf, start, len) {
1320
+ if (this._hdrPieces !== null) {
1321
+ // The block is already kept as the views it arrived in, and this frame's
1322
+ // payload is a view of the chunk it came in -- so it joins them.
1323
+ this._hdrPieces.push(buf.subarray(start, start + len));
1324
+ this._hdrLen += len;
1325
+ return;
1326
+ }
1327
+ if (this._hdr === null) {
1328
+ this._hdr = Buffer.allocUnsafe(8192);
1329
+ }
1330
+ // No maxHeaderListSize test here, although this is where the octets are counted: the limit
1331
+ // is about the size of the header LIST, which is only known once the block is decoded, and
1332
+ // h2lean applies it there (_processHeaders). Measured against node, an over-limit block
1333
+ // arriving in one HEADERS frame gets RST_STREAM(ENHANCE_YOUR_CALM) and the connection
1334
+ // stays up; a check on the compressed length here made it GOAWAY(PROTOCOL_ERROR) instead,
1335
+ // which turns one bad request into an error for every other client on the session. The
1336
+ // buffer is bounded without it: MAX_CONTINUATIONS frames of at most
1337
+ // localSettings.maxFrameSize octets each, 8 x 16 KiB by default.
1338
+ if (this._hdrLen + len > this._hdr.length) {
1339
+ let cap = this._hdr.length * 2;
1340
+ while (cap < this._hdrLen + len) {
1341
+ cap *= 2;
1342
+ }
1343
+ const bigger = Buffer.allocUnsafe(cap);
1344
+ this._hdr.copy(bigger, 0, 0, this._hdrLen);
1345
+ this._hdr = bigger;
1346
+ }
1347
+ buf.copy(this._hdr, this._hdrLen, start, start + len);
1348
+ this._hdrLen += len;
1349
+ }
1350
+
1351
+ // The block is kept as the views it arrived in as soon as one frame of it
1352
+ // straddled a read. A block whose frames all arrived whole is kept in one
1353
+ // buffer, and the first split frame turns that buffer into the first view of
1354
+ // the list: the buffer is not written to again while the block is open, and
1355
+ // the block is decoded before the next one starts.
1356
+ _appendHdrPieces(pieces, from, to) {
1357
+ if (this._hdrPieces === null) {
1358
+ this._hdrPieces = this._hdrLen === 0 ? [] : [this._hdr.subarray(0, this._hdrLen)];
1359
+ }
1360
+ const parts = pieceSlice(pieces, from, to);
1361
+ for (let i = 0; i < parts.length; i++) {
1362
+ this._hdrPieces.push(parts[i]);
1363
+ }
1364
+ this._hdrLen += to - from;
1365
+ }
1366
+
1367
+ _finishHeaders() {
1368
+ const sid = this._hdrSid;
1369
+ const end = this._hdrEnd;
1370
+ if (this._hdrPieces !== null) {
1371
+ const pieces = this._hdrPieces;
1372
+ const len = this._hdrLen;
1373
+ this._hdrSid = -1;
1374
+ this._hdrLen = 0;
1375
+ this._hdrPieces = null;
1376
+ this._processBlockPieces(sid, end, pieces, 0, len);
1377
+ return;
1378
+ }
1379
+ const block = this._hdr.subarray(0, this._hdrLen);
1380
+ this._hdrSid = -1;
1381
+ this._hdrLen = 0;
1382
+ this._processHeaders(sid, end, block);
1383
+ }
1384
+
1385
+ // Bucket for the request shape cache. A 32-bit number rather than a string
1386
+ // because latin1Slice allocated one string per request and the CPU profile
1387
+ // showed that cost as GC time; a collision is caught by the byte comparison
1388
+ // at the lookup site, which then simply replaces the older entry.
1389
+ // Identical bytes do NOT always mean identical fields -- HPACK indexes are
1390
+ // only meaningful against the dynamic table, so the cache is dropped as a
1391
+ // whole whenever the peer changes that table (decoder.epoch).
1392
+ _shapeSlot(block) {
1393
+ if (this._shapeEpoch !== this._decoder.epoch) {
1394
+ this._shapes.clear();
1395
+ this._shapeEpoch = this._decoder.epoch;
1396
+ }
1397
+ return hashBlock(block);
1398
+ }
1399
+
1400
+ // The same bucket for a block that arrived in pieces: the hash is over the
1401
+ // same octets, so a block that arrives whole and one that arrives split are
1402
+ // the same cache entry.
1403
+ _shapeSlotPieces(pieces, from, to) {
1404
+ if (this._shapeEpoch !== this._decoder.epoch) {
1405
+ this._shapes.clear();
1406
+ this._shapeEpoch = this._decoder.epoch;
1407
+ }
1408
+ return hashPieces(pieces, from, to);
1409
+ }
1410
+
1411
+ // The frozen headers object for one shape. Object.freeze is shallow and
1412
+ // node's toHeaderObject puts set-cookie in an array, so that array has to be
1413
+ // frozen too -- otherwise push() would corrupt every other request sharing it.
1414
+ _sharedObject(fields) {
1415
+ const o = this._toObject(fields);
1416
+ for (let i = 0; i < fields.length; i += 2) {
1417
+ const v = o[fields[i]];
1418
+ if (Array.isArray(v)) {
1419
+ Object.freeze(v);
1420
+ }
1421
+ }
1422
+ return Object.freeze(o);
1423
+ }
1424
+
1425
+ // Validates a decoded request (or trailer) field list. Returns null when the fields
1426
+ // may go to the application exactly as they are, { problem } when the stream has to be
1427
+ // refused, or { fields } when only fields with illegal names were dropped. See the
1428
+ // block comment on the constants above for why each rule exists and where the
1429
+ // behaviour came from.
1430
+ _requestProblem(src, trailer) {
1431
+ let method = null, path = null, scheme = null, authority = null, host = null;
1432
+ let protocol = null, contentLength = null;
1433
+ // RFC 9113 8.1: pseudo-header fields, if any, come before all regular fields. Once
1434
+ // one has been seen, a later pseudo-header is a violation -- and in a trailer block
1435
+ // no pseudo-header is allowed at all, so the flag starts in that state.
1436
+ let pseudoAllowed = !trailer;
1437
+ let kept = null; // copied list, only if something is dropped
1438
+
1439
+ for (let i = 0; i < src.length; i += 2) {
1440
+ const name = src[i];
1441
+ const value = src[i + 1];
1442
+
1443
+ if (name.charCodeAt(0) === 58 /* : */) {
1444
+ if (!pseudoAllowed) {
1445
+ return { problem: `pseudo-header ${name} after a field` };
1446
+ }
1447
+ // check_pseudo_header() in nghttp2: a repeated pseudo-header, or one with an
1448
+ // empty value, is refused. An empty value is the smuggling case -- a receiver
1449
+ // that drops the field and one that keeps it disagree about the request.
1450
+ if (value.length === 0) {
1451
+ return { problem: `${name} with an empty value` };
1452
+ }
1453
+ switch (name) {
1454
+ case ':method':
1455
+ if (method !== null) {
1456
+ return { problem: ':method repeated' };
1457
+ }
1458
+ if (!TOKEN_RE.test(value)) {
1459
+ return { problem: `:method ${value} is not a token` };
1460
+ }
1461
+ method = value;
1462
+ break;
1463
+ case ':path':
1464
+ if (path !== null) {
1465
+ return { problem: ':path repeated' };
1466
+ }
1467
+ path = value;
1468
+ break;
1469
+ case ':scheme':
1470
+ if (scheme !== null) {
1471
+ return { problem: ':scheme repeated' };
1472
+ }
1473
+ if (!SCHEME_RE.test(value)) {
1474
+ return { problem: `:scheme ${value} is not a scheme` };
1475
+ }
1476
+ scheme = value;
1477
+ break;
1478
+ case ':authority':
1479
+ if (authority !== null) {
1480
+ return { problem: ':authority repeated' };
1481
+ }
1482
+ if (!AUTHORITY_RE.test(value)) {
1483
+ return { problem: `:authority ${value} has illegal characters` };
1484
+ }
1485
+ authority = value;
1486
+ break;
1487
+ case ':protocol': // RFC 8441/9220, extended CONNECT
1488
+ if (protocol !== null) {
1489
+ return { problem: ':protocol repeated' };
1490
+ }
1491
+ // Extended CONNECT has to be offered by us before a peer may use it, and we
1492
+ // advertise enableConnectProtocol off by default -- which is what makes node
1493
+ // refuse a :protocol it was never offered.
1494
+ if (!this.localSettings.enableConnectProtocol) {
1495
+ return { problem: ':protocol without enableConnectProtocol' };
1496
+ }
1497
+ protocol = value;
1498
+ break;
1499
+ default:
1500
+ // RFC 9113 8.1: an unknown pseudo-header is a PROTOCOL_ERROR, and :status
1501
+ // lands here too -- a server never receives it on a request.
1502
+ return { problem: `unknown pseudo-header ${name}` };
1503
+ }
1504
+ continue;
1505
+ }
1506
+
1507
+ pseudoAllowed = false;
1508
+ if (HAS_UPPER.test(name)) {
1509
+ return { problem: `field name is not lower-case: ${name}` };
1510
+ }
1511
+ if (!NAME_LOWER_RE.test(name)) {
1512
+ // Illegal character in a regular name: drop the field, keep the stream.
1513
+ if (kept === null) {
1514
+ kept = src.slice(0, i);
1515
+ }
1516
+ continue;
1517
+ }
1518
+ if (INBOUND_FORBIDDEN.has(name)) {
1519
+ return { problem: `${name} is a connection-specific field` };
1520
+ }
1521
+ if (name === 'te') {
1522
+ // RFC 9113 8.2.2: the TE field may appear only with the value "trailers". This is the
1523
+ // Transfer-Encoding hole of CVE-2020-8287 seen from the other side.
1524
+ if (value !== 'trailers') {
1525
+ return { problem: `te: ${value} is not "trailers"` };
1526
+ }
1527
+ } else if (name === 'content-length') {
1528
+ if (!NUM_RE.test(value)) {
1529
+ return { problem: `content-length ${value} is not numeric` };
1530
+ }
1531
+ // Two copies are the smuggling case itself; nghttp2 refuses a second one even
1532
+ // when both say the same thing, and node does too (measured).
1533
+ if (contentLength !== null) {
1534
+ return { problem: 'content-length repeated' };
1535
+ }
1536
+ contentLength = value;
1537
+ } else if (name === 'host') {
1538
+ if (!AUTHORITY_RE.test(value)) {
1539
+ if (kept === null) {
1540
+ kept = src.slice(0, i);
1541
+ } continue;
1542
+ }
1543
+ if (host !== null) {
1544
+ return { problem: 'host repeated' };
1545
+ }
1546
+ host = value;
1547
+ }
1548
+ // The last of the per-field rules, and the only one that looks at the value as a
1549
+ // whole: a character that may not appear in a field value makes the field
1550
+ // disappear, and the stream lives (NGHTTP2_ERR_IGN_HTTP_HEADER, measured on node:
1551
+ // bench/micro/probe-value-matrix.js). It sits after the rules above because those win: a
1552
+ // 'te' or a 'content-length' carrying a CR is a PROTOCOL_ERROR on node, not a
1553
+ // dropped field (bench/micro/probe-value-order.js).
1554
+ if (hasBadValue(value)) {
1555
+ if (kept === null) {
1556
+ kept = src.slice(0, i);
1557
+ }
1558
+ continue;
1559
+ }
1560
+ if (kept !== null) {
1561
+ kept.push(name, value);
1562
+ }
1563
+ }
1564
+
1565
+ if (trailer) {
1566
+ return kept === null ? null : { fields: kept };
1567
+ }
1568
+
1569
+ if (method === null) {
1570
+ return { problem: 'missing :method' };
1571
+ }
1572
+ if (method === 'CONNECT' && protocol === null) {
1573
+ // RFC 9113 8.5: for CONNECT the :path and :scheme fields are not applicable, and
1574
+ // the authority is everything the request is about.
1575
+ if (scheme !== null || path !== null) {
1576
+ return { problem: 'CONNECT must not carry :scheme or :path' };
1577
+ }
1578
+ if (authority === null) {
1579
+ return { problem: 'CONNECT without :authority' };
1580
+ }
1581
+ return kept === null ? null : { fields: kept };
1582
+ }
1583
+ if (protocol !== null && method !== 'CONNECT') {
1584
+ return { problem: ':protocol requires CONNECT' };
1585
+ }
1586
+ if (path === null) {
1587
+ return { problem: 'missing :path' };
1588
+ }
1589
+ if (scheme === null) {
1590
+ return { problem: 'missing :scheme' };
1591
+ }
1592
+ // RFC 9113 8.3.1: a client request carries the target in :authority or in host.
1593
+ if (authority === null && host === null) {
1594
+ return { problem: 'missing :authority and host' };
1595
+ }
1596
+ // check_path() in nghttp2: with an http or https scheme the path is an absolute
1597
+ // path, or "*" for a system-wide OPTIONS. Anything else is refused -- a relative
1598
+ // form is exactly what a downstream HTTP/1 translator would resolve on its own.
1599
+ if ((scheme === 'http' || scheme === 'https')
1600
+ && path.charCodeAt(0) !== 47 /* / */
1601
+ && !(method === 'OPTIONS' && path === '*')) {
1602
+ return { problem: `:path ${path} does not start with "/"` };
1603
+ }
1604
+ return kept === null ? null : { fields: kept };
1605
+ }
1606
+
1607
+ // A header block that arrived whole and one that arrived in pieces are the
1608
+ // same block: the octets are the same, so the hash, the cache entry and the
1609
+ // verdict are the same. `block` is null for the split one, and the range
1610
+ // [from, to) of `pieces` is where the block sits in the views.
1611
+ _processHeaders(sid, end, block) {
1612
+ this._processBlock(sid, end, block, null, 0, 0);
1613
+ }
1614
+
1615
+ _processBlockPieces(sid, end, pieces, from, to) {
1616
+ this._processBlock(sid, end, null, pieces, from, to);
1617
+ }
1618
+
1619
+ _processBlock(sid, end, block, pieces, from, to) {
1620
+ const existing = this._streams.get(sid);
1621
+ const flat = this._flat;
1622
+ const shapes = existing === undefined ? this._shapes : null;
1623
+ const slot = shapes === null ? 0
1624
+ : (block !== null ? this._shapeSlot(block) : this._shapeSlotPieces(pieces, from, to));
1625
+ const hit = shapes === null ? undefined : shapes.get(slot);
1626
+ // where this request's field list and header object come from
1627
+ let fields = flat;
1628
+ let shared = null;
1629
+ let bytes = 0;
1630
+ // verdict of the inbound validator: null means the fields may go to the application
1631
+ let verdict;
1632
+
1633
+ try {
1634
+ if (hit !== undefined && (block !== null
1635
+ ? hit.block.equals(block)
1636
+ : pieceEquals(hit.block, pieces, from, to))) {
1637
+ // the whole header block was seen before on this connection under the same table
1638
+ // state: neither decode nor rebuild -- and no re-validation either, because the
1639
+ // fields are the same ones that were checked when the entry was made
1640
+ this._shapeHits++;
1641
+ bytes = hit.bytes;
1642
+ fields = hit.flat;
1643
+ shared = hit.headers;
1644
+ verdict = hit.verdict;
1645
+ } else {
1646
+ if (block !== null) {
1647
+ this._decoder.decode(block, 0, block.length, flat);
1648
+ } else {
1649
+ this._decoder.decodePieces(pieces, from, to, flat);
1650
+ }
1651
+ for (let i = 0; i < flat.length; i++) {
1652
+ bytes += flat[i].length;
1653
+ }
1654
+ // flat is name, value, name, value: one entry overhead per field line
1655
+ bytes += (flat.length >> 1) * ENTRY_OVERHEAD;
1656
+ verdict = this._requestProblem(flat, existing !== undefined);
1657
+ if (verdict !== null && verdict.fields !== undefined) {
1658
+ fields = verdict.fields;
1659
+ }
1660
+ if (shapes !== null) {
1661
+ // A client that varies one header per request (x-request-id) makes
1662
+ // every lookup a miss and every miss two allocations, which is worse
1663
+ // than never caching. Give up once the ratio proves that case. With
1664
+ // shareHeaders on, freezing stays uniform either way.
1665
+ if (++this._shapeMisses > 64 && this._shapeHits * 4 < this._shapeMisses) {
1666
+ this._shapes = null;
1667
+ } else {
1668
+ if (shapes.size >= MAX_SHAPES) {
1669
+ shapes.clear();
1670
+ } // bounded: clients vary
1671
+ const entry = {
1672
+ block: block !== null ? Buffer.from(block) : pieceConcat(pieces, from, to),
1673
+ flat: fields.slice(0), bytes, headers: null,
1674
+ verdict
1675
+ };
1676
+ if (this._share) {
1677
+ entry.headers = this._sharedObject(fields);
1678
+ shared = entry.headers;
1679
+ }
1680
+ shapes.set(slot, entry);
1681
+ }
1682
+ }
1683
+ }
1684
+ } catch (err) {
1685
+ if (err instanceof HPackError) {
1686
+ this._fatal(COMPRESSION_ERROR, err.message);
1687
+ } else {
1688
+ throw err;
1689
+ }
1690
+ return;
1691
+ }
1692
+ this.state.inflateDynamicTableSize = this._decoder.size;
1693
+
1694
+ // RFC 9113 5.1 puts the state of the stream a frame names ahead of anything the frame says, so
1695
+ // this is decided before the request is even looked at: a HEADERS that arrives after the peer
1696
+ // closed the message with its own END_STREAM is a connection error of type STREAM_CLOSED
1697
+ // whichever fields the block holds -- measured on node (probe cases U3, U9), where the second
1698
+ // block is not a valid request at all and the verdict is still GOAWAY(STREAM_CLOSED). 5.1 reads a
1699
+ // frame on a closed stream as "MAY ... treat ... as a connection error", and h2lean takes that
1700
+ // option for BOTH the still-present (half-closed(remote)) and the just-closed (_ended) shape: a
1701
+ // stream error would be the other reading of half-closed(remote), but node does not make that
1702
+ // distinction and h2spec's half-closed(remote) HEADERS check waits for the error, not the silence
1703
+ // a discard would give. Sitting above the trailer rule below is also what keeps a LEGAL trailer,
1704
+ // which arrives with the message still open, on that path.
1705
+ //
1706
+ // The arms that fail on BOTH stacks are not this one: they are the frames that arrive after the
1707
+ // answer closed the stream, which 5.1 lets the endpoint discard, and the tool's expectation for
1708
+ // those is RFC 7540's ("MUST treat that as a connection error of type STREAM_CLOSED") where
1709
+ // RFC 9113 says "MAY". Answering them with an error is not a fix -- it would end the connection
1710
+ // on a frame the RFC lets the server keep quiet about (AGENTS.md 10).
1711
+ if (existing !== undefined ? existing.state.remoteClose === 1
1712
+ : (!this._isGone(sid) && this._ended.includes(sid))) {
1713
+ this._fatal(STREAM_CLOSED, 'HEADERS after the peer ended the stream');
1714
+ return;
1715
+ }
1716
+ if (existing === undefined) {
1717
+ if (sid <= this._maxInboundSid) {
1718
+ // An id at or below the highest one opened cannot open a NEW stream: 5.1.1 says a stream the
1719
+ // peer skipped past has already left "idle", so it cannot be established again here. Two
1720
+ // shapes reach this line. If this side has a record of the id -- refused, or ended by a
1721
+ // reset -- the frames that follow are that stream's, and the discard of 5.1 is right (probe
1722
+ // U15, U16, U19); node agrees. With no such record the peer is opening a stream on an id it
1723
+ // is not allowed to use: 5.1.1 names that an unexpected identifier and mandates a connection
1724
+ // error PROTOCOL_ERROR. node stays silent for it -- the deviation h2spec's 5.1.1 check flags
1725
+ // -- so this is the second place (after PRIORITY on stream 0) where we follow the RFC over
1726
+ // node. The id does not move either way, keeping the frames that follow in the same category.
1727
+ //
1728
+ // The discard is also what h2spec's two 'closed: ... after sending RST_STREAM frame' arms
1729
+ // fail on: RFC 9113 5.1 says of a frame that follows a reset "An endpoint MUST minimally
1730
+ // process and then discard any frames it receives in this state", where RFC 7540 made it a
1731
+ // stream error of type STREAM_CLOSED. node discards too (probe case U6: ACK), and the
1732
+ // conformance tool waits for the error -- on both stacks. Do not answer with an error to
1733
+ // satisfy it (AGENTS.md 10).
1734
+ if (this._isGone(sid)) {
1735
+ return; // refused or reset earlier: the discard of 5.1
1736
+ }
1737
+ this._fatal(PROTOCOL_ERROR, 'HEADERS on an id below the highest one opened');
1738
+ return;
1739
+ }
1740
+ // The id is taken before anything refuses the request, because the peer has opened it: the
1741
+ // frames it had already put on the wire for that id are then the in-flight frames of a stream
1742
+ // this side refused -- measured on node, probe case U17, where the DATA that follows the
1743
+ // refused HEADERS is ignored and the next request is served -- and not the 'idle' error of 5.1.
1744
+ this._maxInboundSid = sid;
1745
+ this.state.lastProcStreamID = sid;
1746
+ }
1747
+
1748
+ // RFC 9113 8.1.1: a request that fails the message parsing rules is refused with
1749
+ // RST_STREAM(PROTOCOL_ERROR), before the application ever sees it. Checking here --
1750
+ // after decode, before any stream object or event -- is what makes the difference
1751
+ // between "the handler declined the request" and "the handler was never reached",
1752
+ // and only the second one denies a smuggling chain its second reader.
1753
+ if (verdict !== null && verdict.problem !== undefined) {
1754
+ this.sendRst(sid, PROTOCOL_ERROR);
1755
+ return;
1756
+ }
1757
+
1758
+ if (existing !== undefined) {
1759
+ if (!end) {
1760
+ // RFC 9113 8.1: a HEADERS block after the request headers is the trailer block, and a
1761
+ // trailer closes the message -- it has to arrive with END_STREAM. Without it the body
1762
+ // would still be open with its tail already sent. Measured on node (h2spec http2/8.1):
1763
+ // RST_STREAM(PROTOCOL_ERROR), the connection untouched.
1764
+ this._streamError(existing, PROTOCOL_ERROR, 'trailing HEADERS without END_STREAM');
1765
+ return;
1766
+ }
1767
+ const trailers = this._share ? this._sharedObject(fields) : this._toObject(fields);
1768
+ existing.emit('trailers', trailers);
1769
+ if (end) {
1770
+ existing._remoteEnded();
1771
+ }
1772
+ return;
1773
+ }
1774
+
1775
+ if (this._liveStreams >= this.localSettings.maxConcurrentStreams) {
1776
+ this.sendRst(sid, REFUSED_STREAM);
1777
+ this._refused.push(sid);
1778
+ if (this._refused.length > MAX_REFUSED_HISTORY) {
1779
+ this._refused.shift();
1780
+ }
1781
+ return;
1782
+ }
1783
+
1784
+ // The same refusal, for a limit the concurrency guard cannot express. Advertising no
1785
+ // SETTINGS_MAX_CONCURRENT_STREAMS (what node does, and now what this does) leaves the guard
1786
+ // above comparing against the unlimited default, so a peer that opens streams and then refuses
1787
+ // to grant credit is limited by nothing: every HEADERS it sends costs a live stream and an
1788
+ // unpromotable response, and that total is what this checks. Refusing at the accept decision is
1789
+ // the only place the total can still be kept small -- a stream that exists already has a handler
1790
+ // holding its object, and its parked body has to stay parked for the peer that may still open the
1791
+ // window. REFUSED_STREAM is the code for "this request was not processed", so the peer may retry
1792
+ // it elsewhere, and the connection and every stream already served on it are untouched. The
1793
+ // refusal clears itself: credit returns, _release drains, the charge drops below the bound.
1794
+ // What is bounded is the cost of accepting, not the total ever: an application that ignores
1795
+ // backpressure can still write into a stalled stream without limit.
1796
+ if (this._heldCost() >= MAX_SESSION_HOLD) {
1797
+ this.sendRst(sid, REFUSED_STREAM);
1798
+ this._refused.push(sid);
1799
+ if (this._refused.length > MAX_REFUSED_HISTORY) {
1800
+ this._refused.shift();
1801
+ }
1802
+ return;
1803
+ }
1804
+
1805
+ // the size limit is re-checked every request, not only when the shape was
1806
+ // first seen: the application may have lowered maxHeaderListSize since then.
1807
+ // RFC 9113 6.5.2 makes maxHeaderListSize advisory ("a lower limit than what is advertised
1808
+ // MAY be enforced") and 7 defines ENHANCE_YOUR_CALM as the code for a peer generating
1809
+ // excessive load -- that is what node answers, measured, together with the two things that
1810
+ // follow from it:
1811
+ // the refusal is per stream (a legal request on the same connection is still answered, and
1812
+ // an identical oversized block is refused again rather than served from the shape cache),
1813
+ // and the id joins the refused history so the peer's already-sent DATA for it is ignored
1814
+ // instead of becoming an error of ours.
1815
+ if (bytes > this.localSettings.maxHeaderListSize) {
1816
+ this.sendRst(sid, ENHANCE_YOUR_CALM);
1817
+ this._refused.push(sid);
1818
+ if (this._refused.length > MAX_REFUSED_HISTORY) {
1819
+ this._refused.shift();
1820
+ }
1821
+ return;
1822
+ }
1823
+
1824
+ const headers = shared !== null ? shared
1825
+ : (this._share ? Object.freeze(this._toObject(fields)) : this._toObject(fields));
1826
+ // The one shape in which the content-length promise is already broken before a single body
1827
+ // octet could have arrived: the field promises a body and the HEADERS frame ends the message.
1828
+ // Refusing at the accept decision -- the same place the parse failures and the size limits are
1829
+ // refused -- is what makes the verdict match node's: measured (probe case P7, and T4 of
1830
+ // tools/probe-inbound-limits.js for the reading handler), node answers RST_STREAM
1831
+ // (PROTOCOL_ERROR) and the application never sees the stream, so nothing is answered on it.
1832
+ // Letting it through to Http2Stream._remoteEnded() instead resets the stream and then hands
1833
+ // the handler a destroyed one, and a handler that answers it puts a response on the wire for a
1834
+ // request this side has already refused.
1835
+ if (end && headers['content-length'] !== undefined && +headers['content-length'] > 0) {
1836
+ this.sendRst(sid, PROTOCOL_ERROR);
1837
+ // The frame that carried the refusal also carried the peer's END_STREAM, so the message is
1838
+ // over: the DATA that follows it (or a second HEADERS) is the error of 5.1 and not the
1839
+ // in-flight frame of a refused stream. Measured on node, probe case U2: GOAWAY
1840
+ // (STREAM_CLOSED). The refusals above never get here without having ended the message, and
1841
+ // the one that has not ended it (a malformed request without END_STREAM, probe case U17) is
1842
+ // the discard, which is what the absence of this record gives.
1843
+ if (this._ended.length < MAX_DROPPED_HISTORY) {
1844
+ this._ended.push(sid);
1845
+ }
1846
+ return;
1847
+ }
1848
+ const stream = new Http2Stream(this, sid);
1849
+ this._streams.set(sid, stream);
1850
+ this._liveStreams++;
1851
+ // content-length is a promise about the body, and the two checks that hold the peer to it
1852
+ // (see _onDataFrame and Http2Stream._remoteEnded) need it as a number. The inbound
1853
+ // validator has already proved the value is all digits, so this is a property read and a
1854
+ // coercion -- and -1, the "nothing was promised" marker, costs one comparison per DATA
1855
+ // frame for the requests that never sent the field at all.
1856
+ const promised = headers['content-length'];
1857
+ if (promised !== undefined) {
1858
+ stream._expLen = +promised;
1859
+ }
1860
+ // node reports END_STREAM while it parses the frame. That is safe here
1861
+ // because the readable events are deferred until a reader exists: the
1862
+ // handler attached inside the emit below still sees 'data' before 'end'.
1863
+ if (end) {
1864
+ stream._remoteEnded();
1865
+ }
1866
+ // node's argument order; the field list is the session's shared decode array
1867
+ // or a cached one, so the raw list handed out is always a copy
1868
+ this.emit('stream', stream, headers, end ? OPT_END_STREAM : OPT_NO_END_STREAM,
1869
+ this._wantRaw ? fields.slice(0) : null);
1870
+ }
1871
+
1872
+ // node's toHeaderObject semantics: null prototype, :status coerced to a
1873
+ // number, set-cookie always an array, repeats joined.
1874
+ //
1875
+ // The map is a fast-properties object with a null prototype rather than a
1876
+ // { __proto__: null } literal: the literal is in V8's dictionary mode, and this
1877
+ // map is built once per request and read once per request by the application --
1878
+ // in the stream path and in the compat path alike (OPTIMIZATION.md item 4).
1879
+ _toObject(flat) {
1880
+ const o = nullProto();
1881
+ for (let i = 0; i < flat.length; i += 2) {
1882
+ const name = flat[i];
1883
+ let value = flat[i + 1];
1884
+ if (name === ':status') {
1885
+ value = value | 0;
1886
+ }
1887
+ const have = o[name];
1888
+ if (have === undefined) {
1889
+ o[name] = name === 'set-cookie' ? [value] : value;
1890
+ } else if (name === 'cookie') {
1891
+ o[name] = `${have}; ${value}`;
1892
+ } else if (name === 'set-cookie') {
1893
+ have.push(value);
1894
+ } else {
1895
+ o[name] = `${have}, ${value}`;
1896
+ }
1897
+ }
1898
+ return o;
1899
+ }
1900
+
1901
+ // The checks and the accounting a DATA frame gets before its payload is handed
1902
+ // over. Split out of _onDataFrame because a payload that straddles two reads is
1903
+ // delivered in as many pieces as it has reads, and it is still ONE frame: the
1904
+ // window, the content-length promise and the body's length are decided once, for
1905
+ // the whole payload, before the first piece reaches the application.
1906
+ // Answers the stream to deliver to, or undefined when the payload is not
1907
+ // delivered at all -- a discarded frame, or a fatal error.
1908
+ _accountData(sid, l) {
1909
+ const stream = this._streams.get(sid);
1910
+ if (stream === undefined) {
1911
+ // A stream we refused or that a reset ended can still have data in flight from
1912
+ // the peer -- it sent before our RST arrived. Ignore those bytes, but give
1913
+ // the connection window credit for them, or enough refused uploads would
1914
+ // slowly deadlock the session.
1915
+ if (this._isGone(sid)) {
1916
+ this._connRecvUsed += l;
1917
+ return undefined;
1918
+ }
1919
+ // RFC 9113 5.1 and 8.2: the peer already closed the message with its END_STREAM, so a DATA
1920
+ // that follows it names a stream that has nothing left to receive. h2lean ends the connection
1921
+ // with STREAM_CLOSED -- the discretion 5.1 grants a closed stream ("MAY ... treat ... as a
1922
+ // connection error") and what h2spec's closed checks wait for; a silent discard is legal by
1923
+ // the letter but the conformance tool times out on it, and this is the just-closed id (it left
1924
+ // the table in this read), not the never-opened id of the skip branch below. node agrees here
1925
+ // (measured: GOAWAY(STREAM_CLOSED), "DATA: stream in half-closed(remote)").
1926
+ //
1927
+ // Note what the tool wants here and what the RFC says: h2spec 2.6.0 quotes RFC 7540's
1928
+ // "MUST treat that as a connection error of type STREAM_CLOSED", RFC 9113 5.1 turned that
1929
+ // into a "MAY ... as a connection error", and both stacks answer this shape the same way.
1930
+ // The arm that fails on both stacks is the other one -- the frame that arrives AFTER the
1931
+ // answer closed the stream, which is discarded below -- and it is not to be "fixed" by
1932
+ // ending the connection here (AGENTS.md 10).
1933
+ if (this._ended.includes(sid)) {
1934
+ this._fatal(STREAM_CLOSED, 'DATA after the peer ended the stream');
1935
+ return undefined;
1936
+ }
1937
+ // Neither refused, reset nor finished: an id this side has never been told about, which is
1938
+ // the 'idle' state of 5.1, and DATA is allowed in 'open' and 'half-closed (local)' only.
1939
+ // Measured on node (probe cases U5, U13, U14): GOAWAY(PROTOCOL_ERROR) -- including for the
1940
+ // connection-level sid 0 and for an even id, neither of which a client may ever open.
1941
+ if (sid === 0 || (sid & 1) === 0 || sid > this._maxInboundSid) {
1942
+ this._fatal(PROTOCOL_ERROR, 'DATA on a stream in the idle state');
1943
+ return undefined;
1944
+ }
1945
+ // An id at or below the highest one opened that this side never saw: a peer may skip ids,
1946
+ // and 5.1's discard is what node answers for it (probe cases U18, U20). It is also the
1947
+ // answer for a frame that arrives after this side's own answer closed the stream: RFC 9113
1948
+ // 5.1 allows a closed stream to be discarded ("An endpoint MUST minimally process and then
1949
+ // discard any frames it receives in this state"), node stays silent there (probe case U1
1950
+ // with the two frames in separate reads: ACK ANSWERED), and h2spec's http2/5.1 'closed'
1951
+ // arms fail on both stacks because of it. Silence is the RFC's answer, not a bug -- do not
1952
+ // end the connection here to satisfy the tool (AGENTS.md 10).
1953
+ this._connRecvUsed += l;
1954
+ return undefined;
1955
+ }
1956
+ if (stream.state.remoteClose === 1) {
1957
+ // The same rule while the stream is still in the table, which is how it reaches a server that
1958
+ // has not answered yet: measured on node (probe cases U10, U11) in both batching shapes, the
1959
+ // connection ends. RFC 9113 6.1 reads a DATA on a stream outside "open"/"half-closed(local)"
1960
+ // as a stream error of type STREAM_CLOSED, and h2spec's half-closed(remote) DATA and 6.1 checks
1961
+ // accept the reset, the GOAWAY, or a closed connection -- but node makes no present/closed
1962
+ // distinction and answers the connection error below on BOTH stacks, so h2lean keeps node's
1963
+ // verdict here rather than the stream error (which regressed the tool's DATA echo). Leaving it
1964
+ // is the honest call: it is node's deviation, the parity rule keeps it, and 5.1's closed-state
1965
+ // read is the discretionary one ("MAY ... treat ... as a connection error"). The arms that
1966
+ // fail on both stacks are the ones the tool writes for a frame that arrives after the answer
1967
+ // closed the stream -- the discard above -- not this one.
1968
+ this._fatal(STREAM_CLOSED, 'DATA after the peer ended the stream');
1969
+ return undefined;
1970
+ }
1971
+ if (l > this.state.localWindowSize) {
1972
+ this._fatal(FLOW_CONTROL_ERROR, 'connection flow control window exceeded');
1973
+ return undefined;
1974
+ }
1975
+ // RFC 9113 8.2 makes content-length a promise, and the body may not run past it. Measured
1976
+ // on node (h2spec http2/8.1.2.6): the frame that crosses the line ends the CONNECTION --
1977
+ // GOAWAY(INTERNAL_ERROR) -- rather than the stream, because nghttp2 reads an overflow as a
1978
+ // failure of the messaging layer, not of one request. The other direction, too few octets,
1979
+ // cannot be seen before the message ends: that one is caught in Http2Stream._remoteEnded()
1980
+ // and is a stream error. Both compare against the octets of this stream's body, padding
1981
+ // excluded, which is what `l` is here.
1982
+ if (stream._expLen >= 0 && stream._bodyLen + l > stream._expLen) {
1983
+ this._fatal(INTERNAL_ERROR, `body of ${stream._bodyLen + l} octets against ` +
1984
+ `content-length ${stream._expLen}`);
1985
+ return undefined;
1986
+ }
1987
+ stream._bodyLen += l;
1988
+ this.state.localWindowSize -= l;
1989
+ this.state.effectiveRecvDataLength += l;
1990
+ stream.state.localWindowSize -= l;
1991
+ return stream;
1992
+ }
1993
+
1994
+ _onDataFrame(sid, flags, buf, start, len) {
1995
+ let p = start;
1996
+ let l = len;
1997
+ if (flags & F_PADDED) {
1998
+ // RFC 9113 6.1: the payload is the pad length, the data, and the padding.
1999
+ // A padded frame with no payload has no pad length to read, and node
2000
+ // answers that with GOAWAY(PROTOCOL_ERROR) (measured, tools/probe-pad-bad.js).
2001
+ if (len === 0) {
2002
+ this._fatal(PROTOCOL_ERROR, 'padded DATA frame with an empty payload'); return;
2003
+ }
2004
+ const pad = buf[p];
2005
+ p += 1;
2006
+ l -= 1 + pad;
2007
+ if (l < 0) {
2008
+ this._fatal(PROTOCOL_ERROR, 'pad larger than the DATA payload'); return;
2009
+ }
2010
+ }
2011
+ const stream = this._accountData(sid, l);
2012
+ if (stream === undefined) {
2013
+ return;
2014
+ }
2015
+ // The stream decides when the credit is released: at once if the application
2016
+ // is reading, later if it is not. Either way the buffer it takes is one it may
2017
+ // keep: a view of the chunk node read off the socket, which node never writes
2018
+ // into again (tools/probe-read-allocator.js). A padded frame's payload is a
2019
+ // view too -- the pad length and the padding are taken off the views before
2020
+ // this point (frames.js trimPadding), so the copy is gone from this path as
2021
+ // well. Nothing here is recycled underneath the application, so the receive
2022
+ // path needs no copy at all.
2023
+ stream._recvData(buf.subarray(p, p + l));
2024
+ // 'end' must follow the last 'data'
2025
+ if (flags & F_END_STREAM) {
2026
+ stream._remoteEnded();
2027
+ }
2028
+ }
2029
+
2030
+ // A payload that straddled two reads: one piece per read, each a view of the
2031
+ // chunk node read off the socket. No copy, and the pieces are the frame's bytes
2032
+ // in the order the peer sent them.
2033
+ _onDataPieces(sid, flags, pieces, len) {
2034
+ const stream = this._accountData(sid, len);
2035
+ if (stream === undefined) {
2036
+ return;
2037
+ }
2038
+ // The credit is released as the application takes each piece, exactly as it is
2039
+ // for a payload that arrived whole.
2040
+ for (let i = 0; i < pieces.length; i++) {
2041
+ // A piece the padding took all of is empty, and an empty chunk is not a
2042
+ // chunk: node emits no 'data' for one.
2043
+ if (pieces[i] !== null) {
2044
+ stream._recvData(pieces[i]);
2045
+ }
2046
+ }
2047
+ // 'end' must follow the last 'data'
2048
+ if (flags & F_END_STREAM) {
2049
+ stream._remoteEnded();
2050
+ }
2051
+ }
2052
+
2053
+ _onWindowUpdate(sid, inc) {
2054
+ if (inc === 0) {
2055
+ this._fatal(PROTOCOL_ERROR, 'WINDOW_UPDATE with zero increment'); return;
2056
+ }
2057
+ if (sid === 0) {
2058
+ const win = this._connSendWin + inc;
2059
+ if (win > 0x7fffffff) {
2060
+ this._fatal(FLOW_CONTROL_ERROR, 'connection window overflow'); return;
2061
+ }
2062
+ this._connSendWin = win;
2063
+ this.state.remoteWindowSize = win;
2064
+ this._releaseAll();
2065
+ return;
2066
+ }
2067
+ const stream = this._streams.get(sid);
2068
+ if (stream === undefined) {
2069
+ // RFC 9113 5.1: an id above the highest one this side has ever opened names a stream in
2070
+ // the 'idle' state, and 6.9 lets WINDOW_UPDATE be sent only in 'open' and 'half-closed
2071
+ // (local)'. Measured on node (h2spec http2/5.1 and 6.9), WINDOW_UPDATE for an idle stream
2072
+ // is GOAWAY(PROTOCOL_ERROR). For an id at or below the highest one opened the stream has
2073
+ // been answered, refused or reset -- a credit note that arrived late -- and node drops it,
2074
+ // so this does too rather than turning the peer's own in-flight frame into an error.
2075
+ if (sid > this._maxInboundSid) {
2076
+ this._fatal(PROTOCOL_ERROR, 'WINDOW_UPDATE on an idle stream');
2077
+ }
2078
+ return;
2079
+ }
2080
+ const next = stream._sendWin + inc;
2081
+ if (next > 0x7fffffff) {
2082
+ this._streamError(stream, FLOW_CONTROL_ERROR, 'stream window overflow');
2083
+ return;
2084
+ }
2085
+ stream._sendWin = next;
2086
+ if (stream._queued !== null) {
2087
+ this._release(stream);
2088
+ }
2089
+ if (this._outLen >= 32 * 1024) {
2090
+ this._flush();
2091
+ }
2092
+ }
2093
+
2094
+ // What this connection currently costs in answers it cannot send. Two terms because the flood
2095
+ // that needs bounding is many small stalled responses, and charging only their payloads would let
2096
+ // 8 MiB of bound stand for 350 000 live stream records -- see MAX_SESSION_HOLD for the measurement.
2097
+ _heldCost() {
2098
+ return this._heldBytes
2099
+ + (this._blocked === null ? 0 : this._blocked.size * STREAM_HOLD_CHARGE);
2100
+ }
2101
+
2102
+ // Only streams that actually stalled are worth revisiting. Scanning every
2103
+ // stream on each connection-level WINDOW_UPDATE looks harmless at low load
2104
+ // and is quadratic under a large response body (OPTIMIZATION.md item 19).
2105
+ _releaseAll() {
2106
+ const blocked = this._blocked;
2107
+ if (blocked === null || blocked.size === 0) {
2108
+ return;
2109
+ }
2110
+ for (const s of blocked) {
2111
+ if (s._queued !== null) {
2112
+ this._release(s);
2113
+ }
2114
+ }
2115
+ }
2116
+
2117
+ _release(stream) {
2118
+ const q = stream._queued;
2119
+ const max = this.remoteSettings.maxFrameSize;
2120
+ while (q.length > 0) {
2121
+ const buf = q[0];
2122
+ let sent = 0;
2123
+ while (sent < buf.length) {
2124
+ const room = Math.min(this._connSendWin, stream._sendWin, max,
2125
+ MAX_OUT_BATCH - this._outLen - HEADER_LEN);
2126
+ if (room <= 0) {
2127
+ // keep the unsent remainder where the next release can find it: that is a
2128
+ // WINDOW_UPDATE for a credit stall and a write callback for a full queue
2129
+ if (sent > 0) {
2130
+ q[0] = buf.subarray(sent);
2131
+ }
2132
+ return;
2133
+ }
2134
+ const take = Math.min(room, buf.length - sent);
2135
+ const last = q.length === 1 && sent + take === buf.length && stream._endQueued;
2136
+ this._frame(DATA, last ? F_END_STREAM : 0, stream.id, buf.subarray(sent, sent + take),
2137
+ true);
2138
+ this._connSendWin -= take;
2139
+ stream._sendWin -= take;
2140
+ // the byte is no longer held once the frame is on its way to the socket
2141
+ this._heldBytes -= take;
2142
+ sent += take;
2143
+ }
2144
+ q.shift();
2145
+ }
2146
+ stream._queued = null;
2147
+ this._blocked.delete(stream);
2148
+ this.state.outboundQueueSize = this._blocked.size;
2149
+ // A trailer block stashed by sendTrailers() belongs after the body it follows, and it
2150
+ // carries the END_STREAM the body was not allowed to send (RFC 9113 6.1: a trailing
2151
+ // HEADERS precedes the close -- measured against node: HEADERS/EH DATA HEADERS/ES/EH).
2152
+ if (stream._pendingTrailers !== null) {
2153
+ const trailers = stream._pendingTrailers;
2154
+ stream._pendingTrailers = null;
2155
+ this._frame(HEADERS, F_END_HEADERS | F_END_STREAM, stream.id, trailers, true);
2156
+ stream._endLocalSide();
2157
+ }
2158
+ // the writable side accepted bytes again: res.on('drain') is how an
2159
+ // application is allowed to resume writing after write() returned false
2160
+ process.nextTick(emitNT, stream, 'drain');
2161
+ if (stream._endQueued) {
2162
+ stream._endLocalSide();
2163
+ }
2164
+ if (this._outLen >= 32 * 1024) {
2165
+ this._flush();
2166
+ }
2167
+ }
2168
+
2169
+ _onRst(sid, code) {
2170
+ // The budget is charged per frame received, before anything else and whether or not
2171
+ // the stream is still known: what is being limited is the rate at which a peer can
2172
+ // make us do this work, not the work of one particular stream.
2173
+ //
2174
+ // The clock is sampled only when the bucket has run empty. Deferring it is exactly
2175
+ // equivalent to refilling every second: between two samples the balance only
2176
+ // decreases, and min(burst, v + g1 + g2) == min(burst, min(burst, v + g1) + g2) for
2177
+ // non-negative gains, so one catch-up grant gives the same balance as stepwise ones
2178
+ // (OPTIMIZATION.md item 20 for the measured share of Date.now()).
2179
+ if (this._rstTokens === 0) {
2180
+ const d = ((Date.now() / 1000) | 0) - this._rstSec;
2181
+ if (d > 0) {
2182
+ this._rstSec += d; // never behind the sample we just took
2183
+ const gained = RST_RATE * d;
2184
+ this._rstTokens = gained > RST_BURST ? RST_BURST : gained;
2185
+ }
2186
+ if (this._rstTokens === 0) {
2187
+ this._fatal(INTERNAL_ERROR, 'RST_STREAM: too many resets in a short period');
2188
+ return;
2189
+ }
2190
+ }
2191
+ this._rstTokens--;
2192
+
2193
+ if (sid === 0) {
2194
+ // RFC 9113 6.4: RST_STREAM always applies to a stream, so 0 is not a value it may carry.
2195
+ // Measured on node (h2spec http2/6.4): GOAWAY(PROTOCOL_ERROR), not a reset of nothing.
2196
+ this._fatal(PROTOCOL_ERROR, 'RST_STREAM with stream id 0');
2197
+ return;
2198
+ }
2199
+ const stream = this._streams.get(sid);
2200
+ if (stream === undefined) {
2201
+ // As for WINDOW_UPDATE: an id this side never opened names an 'idle' stream, and 6.4
2202
+ // allows RST_STREAM in 'open' and 'half-closed (local)' only. Measured on node (h2spec
2203
+ // http2/5.1 and 6.4): GOAWAY(PROTOCOL_ERROR). A reset for a stream that has already been
2204
+ // answered or refused is the peer's own late frame and is dropped.
2205
+ if (sid > this._maxInboundSid) {
2206
+ this._fatal(PROTOCOL_ERROR, 'RST_STREAM on an idle stream');
2207
+ return;
2208
+ }
2209
+ // Below the line the reset lands, even if there is nothing left to reset: it is the peer
2210
+ // closing the stream, and whatever it had already put on the wire for that id is then the
2211
+ // discard of 5.1 rather than the error of a frame after the peer's END_STREAM (U12, U19).
2212
+ this._rememberKilled(sid);
2213
+ return;
2214
+ }
2215
+ // The peer's RST_STREAM closes both halves at once: same teardown as our own
2216
+ // reset. Whether the application hears 'aborted' is decided there, by
2217
+ // whether the response was still open (lib/stream.js, _destroy).
2218
+ stream._destroy(code, undefined, false);
2219
+ }
2220
+
2221
+ get goawayCode() {
2222
+ return this._goawayCode === null ? NO_ERROR : this._goawayCode;
2223
+ }
2224
+
2225
+ get goawayLastStreamID() {
2226
+ return this._goawayLastStreamID || 0;
2227
+ }
2228
+
2229
+ _onGoaway(buf, start, len) {
2230
+ // node begins this handler with `if (session.destroyed) return;`, because its nghttp2 handle
2231
+ // can report a frame after the session was destroyed. h2lean cannot: _dispatch() answers with
2232
+ // `this._open` and the read batch stops at the frame that destroyed the session, so a
2233
+ // destroyed session has no frames left to reach. That stop is what is tested -- in
2234
+ // test/goaway.test.js, 'the read batch stops at the frame that destroyed the session' -- and
2235
+ // a second check down here would be a line no test can ever make fail.
2236
+ if (len < 8) {
2237
+ this._fatal(FRAME_SIZE_ERROR, 'GOAWAY payload too short'); return;
2238
+ }
2239
+ this._goawayLastStreamID = buf.readUInt32BE(start);
2240
+ this._goawayCode = buf.readUInt32BE(start + 4);
2241
+ // the debug data goes to the application, which may keep it: the same reason as
2242
+ // the PING payload above. allocUnsafe + copy instead of Buffer.from(subarray) for
2243
+ // the pooled allocation (lib/headers.js, block cache). Node's copy comes out of
2244
+ // the C++ layer as a fresh buffer, so it does not move under the application there.
2245
+ let debug;
2246
+ if (len > 8) {
2247
+ debug = Buffer.allocUnsafe(len - 8);
2248
+ buf.copy(debug, 0, start + 8, start + len);
2249
+ }
2250
+ this.emit('goaway', this._goawayCode, this._goawayLastStreamID, debug);
2251
+ // node's rule on receipt (onGoawayData in internal/http2/core): a GOAWAY with NO_ERROR
2252
+ // starts a graceful shutdown of our own -- no new streams, existing ones finish naturally,
2253
+ // and we answer with our GOAWAY -- while any other code destroys the session at once. The
2254
+ // destroy is asked for with NO_ERROR even then, because the code that arrived describes the
2255
+ // PEER's reason, not a failure of this side; `goawayCode` keeps what was received.
2256
+ //
2257
+ // Streams above the peer's lastStreamID are deliberately NOT torn down here. That is what a
2258
+ // GOAWAY means -- the sender will not process anything newer -- and the streams it names are
2259
+ // the ones the SENDER opened; a server told to stop at stream 0 still owes an answer for the
2260
+ // request already in front of it. We destroyed them with PROTOCOL_ERROR, which closed a live
2261
+ // request the moment the peer said goodbye: measured against node on that shape (a request
2262
+ // that is not answered, then GOAWAY(NO_ERROR, lastStreamID 0)), node reports
2263
+ // closed && !destroyed with the stream still open, us a finished teardown.
2264
+ if (this._goawayCode === NO_ERROR) {
2265
+ this.close(); return;
2266
+ }
2267
+ // Any other code: node destroys the session at once, telling the application with
2268
+ // ERR_HTTP2_SESSION_ERROR -- carrying the code that arrived, in node's own words -- and
2269
+ // putting GOAWAY(NO_ERROR) on the wire, because there was no failure on this side.
2270
+ this.destroy(errorOf('ERR_HTTP2_SESSION_ERROR',
2271
+ `Session closed with error code ${this._goawayCode}`), NO_ERROR);
2272
+ }
2273
+
2274
+ // ---- flow control credit, released on consumption -------------------------
2275
+ // RFC 9113 5.2.2 and 6.9: flow control exists so that a receiver unable to process
2276
+ // one stream's data can still serve the rest of the connection, and WINDOW_UPDATE is
2277
+ // the only way to give that credit back. So credit returns when the application has
2278
+ // actually taken the bytes, not when they land. That is what backpressure means in
2279
+ // HTTP/2: a body nobody reads costs the sender, not our heap. The connection
2280
+ // window is accounted separately because it must be restored even for a stream
2281
+ // that is destroyed before anyone reads it -- see _abandon().
2282
+ _consume(stream, n) {
2283
+ stream._recvUsed -= n; // connection share, released below
2284
+ this._connRecvUsed += n;
2285
+ // The peer's stream window only grows when we say so, and saying so once per
2286
+ // quarter window keeps the frame count down instead of one update per chunk.
2287
+ stream._unannounced += n;
2288
+ if (stream._unannounced << 2 >= this.localSettings.initialWindowSize) {
2289
+ const inc = stream._unannounced;
2290
+ stream._unannounced = 0;
2291
+ stream.state.localWindowSize += inc;
2292
+ this._windowUpdate(stream.id, inc);
2293
+ }
2294
+ // A reader that shows up between batches (a timer, an await) has to be able
2295
+ // to unblock the sender on its own: _endBatch() is not coming this turn.
2296
+ if (!this._inBatch) {
2297
+ this._creditConnection();
2298
+ }
2299
+ }
2300
+
2301
+ // The connection-level half of the credit, coalesced into one frame.
2302
+ _creditConnection() {
2303
+ // Not every consumed octet is worth a frame. WINDOW_UPDATE is a signal about space, not an
2304
+ // accounting entry, and node's nghttp2 likewise lets the connection window run down before it
2305
+ // releases anything: measured on h2spec's http2/6.1 sequence (four octets discarded on a stream
2306
+ // that had already closed), our flush-per-read-batch put a WINDOW_UPDATE(sid 0) on the wire in
2307
+ // front of the frame the tool was waiting for, where node sent nothing at all. Measured on a
2308
+ // 640 KiB upload (tools/probe-conn-credit.js): node releases the connection window in steps of
2309
+ // 32768 octets -- half the 65535 it advertises -- and the same threshold here takes h2lean's
2310
+ // cadence from one frame per read batch to the same half-window steps, at the same throughput.
2311
+ // A peer that keeps sending is therefore credited before its window can run out, so the
2312
+ // anti-deadlock rule that this counter exists for still holds; below the threshold the bytes
2313
+ // stay owed, and the next batch pays them.
2314
+ if (this._connRecvUsed < this.localSettings.initialWindowSize >> 1) {
2315
+ return;
2316
+ }
2317
+ const inc = this._connRecvUsed;
2318
+ this._connRecvUsed = 0;
2319
+ this.state.localWindowSize += inc;
2320
+ this._windowUpdate(0, inc);
2321
+ }
2322
+
2323
+ // Bytes that will never be read: the stream's own window no longer matters,
2324
+ // but the connection's share has to come back or the session deadlocks.
2325
+ _abandon(stream) {
2326
+ if (stream._recvUsed > 0) {
2327
+ this._connRecvUsed += stream._recvUsed;
2328
+ stream._recvUsed = 0;
2329
+ if (!this._inBatch) {
2330
+ this._creditConnection();
2331
+ }
2332
+ }
2333
+ }
2334
+
2335
+ _endBatch() {
2336
+ this._creditConnection();
2337
+ this._flush();
2338
+ if (this._closing && this._liveStreams === 0) {
2339
+ this.socket.end();
2340
+ }
2341
+ }
2342
+
2343
+ // ---- errors and lifecycle ---------------------------------------------------
2344
+ _onFrameError(err, sid) {
2345
+ if (err instanceof HPackError) {
2346
+ this._fatal(COMPRESSION_ERROR, err.message);
2347
+ return false;
2348
+ }
2349
+ const code = typeof err.code === 'string' ? err.code : '';
2350
+ if (code.startsWith('ERR_HTTP2') || code === 'ERR_INVALID_HTTP_TOKEN') {
2351
+ // the peer handed us something we refuse to interpret
2352
+ if (sid !== 0 && this._streams.has(sid)) {
2353
+ this._streamError(this._streams.get(sid), PROTOCOL_ERROR, err.message);
2354
+ } else {
2355
+ this._fatal(PROTOCOL_ERROR, err.message);
2356
+ }
2357
+ return false;
2358
+ }
2359
+ this._fatal(INTERNAL_ERROR, err.message);
2360
+ return false;
2361
+ }
2362
+
2363
+ _streamError(stream, code, msg) {
2364
+ this.sendRst(stream.id, code);
2365
+ this._flush();
2366
+ stream._destroy(code, msg, false);
2367
+ if (this.listenerCount('frameError') > 0) {
2368
+ this.emit('frameError', DATA, stream.id, code);
2369
+ }
2370
+ process.nextTick(emitNT, stream, 'error', errorOf('ERR_HTTP2_STREAM_ERROR', msg));
2371
+ }
2372
+
2373
+ _fatal(code, msg) {
2374
+ if (!this._open) {
2375
+ return;
2376
+ }
2377
+ this.sendGoaway(this._maxInboundSid, code, msg);
2378
+ this._flush();
2379
+ const err = errorOf('ERR_HTTP2_SESSION_ERROR', msg);
2380
+ err.http2Code = code;
2381
+ this.destroy(code);
2382
+ if (this.listenerCount('error') > 0) {
2383
+ this.emit('error', err);
2384
+ }
2385
+ }
2386
+
2387
+ // Takes the stream object, not the id: the caller always holds it, and
2388
+ // Map.delete already reports whether the entry was there. `killed` says which of the two ways a
2389
+ // stream leaves the table it is: a reset (ours or the peer's) or the end of the message.
2390
+ _dropStream(stream, killed) {
2391
+ if (!this._streams.delete(stream.id)) {
2392
+ return;
2393
+ }
2394
+ this._liveStreams--;
2395
+ // inbound bytes the application never got still owe the connection window
2396
+ this._abandon(stream);
2397
+ if (killed) {
2398
+ // remember the id: DATA the peer already put on the wire will turn up next
2399
+ this._rememberKilled(stream.id);
2400
+ } else {
2401
+ // The other memory, and the shorter one by design: reaching here means both halves closed on
2402
+ // their own terms, so the remote half was closed by the peer's END_STREAM (it is the only
2403
+ // thing that sets remoteClose besides a reset, and a reset takes the path above). A frame for
2404
+ // that id is then the error of RFC 9113 5.1 -- until this read batch is over.
2405
+ if (this._ended.length < MAX_DROPPED_HISTORY) {
2406
+ this._ended.push(stream.id);
2407
+ }
2408
+ }
2409
+ // a stalled stream that gets reset must not be kept alive by the blocked set
2410
+ if (this._blocked !== null) {
2411
+ this._blocked.delete(stream);
2412
+ }
2413
+ // and the bytes it was holding leave the charge with it, or the session would go on paying for
2414
+ // a queue that no longer exists and refuse requests it could now serve
2415
+ if (stream._queued !== null) {
2416
+ for (const b of stream._queued) {
2417
+ this._heldBytes -= b.length;
2418
+ }
2419
+ stream._queued = null;
2420
+ }
2421
+ // a closing session whose last stream has gone has nothing left to carry
2422
+ if (this._closing) {
2423
+ this._maybeFinishClose();
2424
+ }
2425
+ }
2426
+
2427
+ // Recently finished stream ids: a peer that is mid-upload keeps sending DATA
2428
+ // for them, and those frames are ours to ignore, not to treat as an error.
2429
+ _isGone(sid) {
2430
+ return this._refused.includes(sid) ||
2431
+ (this._dropped !== null && this._dropped.includes(sid));
2432
+ }
2433
+
2434
+ // An id that a reset ended. Reached from _dropStream for a stream that was still in the table,
2435
+ // and from _onRst for one that was not: a reset that arrives for a stream this side has already
2436
+ // finished is the peer telling it to stop, and the frames it had put on the wire before that are
2437
+ // then the discard of RFC 9113 5.4 rather than the "after the peer's own END_STREAM" error.
2438
+ // Measured: node answers nothing at all for HEADERS|END_STREAM, RST_STREAM, DATA (probe U12).
2439
+ _rememberKilled(sid) {
2440
+ if (this._dropped === null) {
2441
+ this._dropped = [];
2442
+ }
2443
+ this._dropped.push(sid);
2444
+ if (this._dropped.length > MAX_DROPPED_HISTORY) {
2445
+ this._dropped.shift();
2446
+ }
2447
+ }
2448
+
2449
+ close() {
2450
+ // node bails on both flags: a destroyed session cannot be closed, and close() is not a way
2451
+ // to undo a destroy.
2452
+ if (this.closed || this.destroyed) {
2453
+ return this;
2454
+ }
2455
+ this.closed = true;
2456
+ this._closing = true;
2457
+ this.sendGoaway(this._maxInboundSid, NO_ERROR);
2458
+ this._flush();
2459
+ this._maybeFinishClose();
2460
+ return this;
2461
+ }
2462
+
2463
+ // node's [kMaybeDestroy](), plus the graceful close that nghttp2 completes for it: once the
2464
+ // session has nothing left to carry the connection is let go, and a session that still has
2465
+ // streams or bytes on their way is not. It is looked at when close() is asked for and when the
2466
+ // last stream goes, because close() typically hands its GOAWAY over and finds `_frozenLen`
2467
+ // covering it -- and a session that is closed with a live stream has to be finished by the
2468
+ // stream's own teardown rather than left sitting in server.sessions with the listening server
2469
+ // waiting on it.
2470
+ _maybeFinishClose() {
2471
+ if (!this.closed || this.destroyed) {
2472
+ return;
2473
+ }
2474
+ if (this._liveStreams > 0 || this._outLen > 0 || this._frozenLen > 0) {
2475
+ return;
2476
+ }
2477
+ this.socket.end();
2478
+ this.destroy(NO_ERROR);
2479
+ }
2480
+
2481
+ // node's signature is destroy(error = NGHTTP2_NO_ERROR, code): the first argument may be the
2482
+ // Error the application is handed, in which case the second is the code that goes on the wire.
2483
+ // That is exactly what a peer's GOAWAY does -- destroy(new ERR_HTTP2_SESSION_ERROR(code),
2484
+ // NO_ERROR) -- and the measurement below is what made the distinction visible: the application
2485
+ // is told WHY (the code that arrived) while the peer is told goodbye with NO_ERROR, because
2486
+ // this side did not fail. A number as the first argument is h2lean's own convenience, used by
2487
+ // the socket handlers and _fatal(): destroy with this GOAWAY code and nothing to report.
2488
+ destroy(error = NO_ERROR, code) {
2489
+ if (this.destroyed) {
2490
+ return this;
2491
+ }
2492
+ let appError;
2493
+ if (typeof error === 'number') {
2494
+ code = error;
2495
+ } else {
2496
+ appError = error;
2497
+ if (code === undefined) {
2498
+ code = INTERNAL_ERROR;
2499
+ }
2500
+ }
2501
+ this.destroyed = true;
2502
+ // node hands the code to nghttp2 when the session is destroyed, and the peer receives a
2503
+ // GOAWAY carrying it -- measured against node 26: destroying an open session leaves the
2504
+ // peer with GOAWAY(NO_ERROR) on the wire and no local 'goaway' event on the session. The
2505
+ // frame has to be handed over before _open goes false, because _writePending() drops
2506
+ // whatever is queued once the session is not open. It is written even when a GOAWAY has
2507
+ // already arrived from the peer -- node answers one goodbye with one of its own -- and only
2508
+ // skipped when this side has already sent one (_fatal, close).
2509
+ if (this._open && !this._goawaySent) {
2510
+ this.sendGoaway(this._maxInboundSid, code);
2511
+ this._flush();
2512
+ }
2513
+ this._open = false;
2514
+ // `closed` is NOT touched here: node's closeSession() only sets the destroyed flag, so
2515
+ // `session.closed` keeps saying whether close() was asked for. Measured on a session
2516
+ // destroyed after a peer GOAWAY with a code: node reports closed:false destroyed:true.
2517
+ for (const s of [...this._streams.values()]) {
2518
+ s._destroy(code, undefined, false, appError);
2519
+ }
2520
+ this._streams.clear();
2521
+ this._liveStreams = 0;
2522
+ if (this._idleTimer !== null) {
2523
+ idleClearTimeout(this._idleTimer);
2524
+ this._idleTimer = null;
2525
+ }
2526
+ this._flush();
2527
+ // node's teardown shape (internal/http2/core.js, finishSessionClose): the socket is
2528
+ // ended first and the destroy waits for the write to be flushed, so the GOAWAY queued
2529
+ // above reaches the peer and the connection ends with a FIN instead of a reset.
2530
+ // h2lean used to destroy the socket outright, which threw that GOAWAY away and closed
2531
+ // the connection with a reset whenever the write had not reached the OS yet -- under
2532
+ // load that is what h2spec's http2/7 reads as a failure ("connection reset by peer"
2533
+ // where node's "connection closed" passes), measured in bench/micro/probe-goaway-ping.js.
2534
+ const socket = this.socket;
2535
+ if (socket.destroyed) {
2536
+ socket.destroy();
2537
+ } else {
2538
+ socket.end(() => {
2539
+ setImmediate(() => socket.destroy());
2540
+ });
2541
+ // The bound: a peer that stopped reading holds the flush (and the FIN behind it)
2542
+ // forever, and the socket would stay on the heap with it. The timer is unref'd --
2543
+ // it must not keep the process alive -- and cleared as soon as the socket is gone.
2544
+ const grace = setTimeout(() => socket.destroy(), SOCKET_CLOSE_GRACE);
2545
+ if (typeof grace.unref === 'function') {
2546
+ grace.unref();
2547
+ }
2548
+ socket.once('close', () => clearTimeout(grace));
2549
+ }
2550
+ // node's closeSession() hands the streams their error first and the session gets it in
2551
+ // finishSessionClose() afterwards, which is the order the application sees: stream 'error',
2552
+ // stream 'close', session 'error', session 'close'.
2553
+ if (appError !== undefined) {
2554
+ process.nextTick(emitNT, this, 'error', appError);
2555
+ }
2556
+ return this;
2557
+ }
2558
+
2559
+ // The session's 'close', exactly once. node emits it when the SOCKET closes
2560
+ // (internal/http2/core.js, finishSessionClose: socket.on('close', () =>
2561
+ // emitClose(session, error))), and the server's own 'close' is emitted before it because
2562
+ // net.Server's handler was registered on the socket before this one.
2563
+ //
2564
+ // Moving it here is not optional. The socket half of node's teardown shape on its own (the
2565
+ // end-then-destroy in destroy()) left the session's 'close' coming out of destroy() on a
2566
+ // nextTick, where it OVERTOOK the server's own 'close' -- nine event-order traces went red
2567
+ // (simple, post, reset, clientReset, goaway, trailers, eof, eof-wait, eof-body) and all
2568
+ // twelve agree with node again only once the 'close' moved with it.
2569
+ _emitSessionClose() {
2570
+ if (this._closeEmitted) {
2571
+ return;
2572
+ }
2573
+ this._closeEmitted = true;
2574
+ process.nextTick(emitNT, this, 'close');
2575
+ }
2576
+
2577
+ _onSocketError(err) {
2578
+ // node ignores an ECONNRESET that arrives after a GOAWAY was received: "there's nothing we can
2579
+ // do and the other side is fully within its rights to do so" (socketOnError in
2580
+ // internal/http2/core). Telling the application about it turned a peer's ordinary hang-up
2581
+ // into a session error that node reports nothing for -- measured on the 'destroy' scenario,
2582
+ // where the peer destroys itself and the reset lands right behind its GOAWAY.
2583
+ if (err !== undefined && err.code === 'ECONNRESET' && this._goawayCode !== null) {
2584
+ this.destroy(NO_ERROR);
2585
+ return;
2586
+ }
2587
+ if (this.listenerCount('socketError') > 0) {
2588
+ this.emit('socketError', err);
2589
+ } else if (this.listenerCount('error') > 0) {
2590
+ this.emit('error', err);
2591
+ }
2592
+ this.destroy(NO_ERROR);
2593
+ }
2594
+
2595
+ _onSocketClose() {
2596
+ this._open = false;
2597
+ if (!this.destroyed) {
2598
+ this.destroy(NO_ERROR);
2599
+ }
2600
+ this._emitSessionClose();
2601
+ }
2602
+
2603
+ // Idle timeout, node's setTimeout semantics: it counts silence, so every read
2604
+ // batch refreshes the handle instead of allocating a new one, and calling
2605
+ // setTimeout twice replaces the previous timer rather than leaving it armed.
2606
+ //
2607
+ // Delays above 1 s are armed on the shared clock in lib/fast-timer.js: one 499 ms
2608
+ // libuv handle for the whole process instead of one per connection, and a
2609
+ // refresh() that is an array write rather than a uv_timer_reset syscall. The
2610
+ // cost is accuracy -- such a timeout fires within +/- 500 ms of the deadline.
2611
+ // Shorter delays keep a native handle, so a 600 ms timeout stays precise.
2612
+ setTimeout(ms, cb) {
2613
+ if (this._idleTimer !== null) {
2614
+ idleClearTimeout(this._idleTimer);
2615
+ this._idleTimer = null;
2616
+ }
2617
+ this._idleCb = cb === undefined ? null : cb;
2618
+ if (ms === 0) {
2619
+ return this;
2620
+ }
2621
+ // the session is passed as the timer argument, which both the native and the
2622
+ // fast path forward to the callback: nothing is allocated per connection here
2623
+ const timer = idleSetTimeout(onIdleTimeout, ms, this);
2624
+ if (timer.unref) {
2625
+ timer.unref();
2626
+ } // the fast clock unrefs itself
2627
+ this._idleTimer = timer;
2628
+ return this;
2629
+ }
2630
+
2631
+ _touchIdleTimer() {
2632
+ if (this._idleTimer !== null) {
2633
+ this._idleTimer.refresh();
2634
+ }
2635
+ }
2636
+
2637
+ setLocalWindowSize(size) {
2638
+ this.state.localWindowSize = size;
2639
+ this.state.effectiveLocalWindowSize = size;
2640
+ return this;
2641
+ }
2642
+
2643
+ setNextStreamID(id) {
2644
+ this.state.nextStreamID = id; return this;
2645
+ }
2646
+
2647
+ // Advertise new settings to the peer. node's session.settings() returns nothing -- measured
2648
+ // on a live session of node 26, `session.settings()` answers undefined, so this does not hand
2649
+ // out the session for chaining either; a caller that wants the current values has
2650
+ // session.localSettings, which both stacks expose.
2651
+ settings(settings, cb) {
2652
+ if (settings) {
2653
+ Object.assign(this.localSettings, settings);
2654
+ // a value the application sets part-way through the connection is a promise it made, so it
2655
+ // joins the advertisement; the values it never touched stay private to this session
2656
+ for (const k of Object.keys(settings)) {
2657
+ if (ID_BY_SETTING[k] !== undefined) {
2658
+ this._advertised.add(k);
2659
+ }
2660
+ }
2661
+ }
2662
+ this._sendSettings();
2663
+ if (cb) {
2664
+ process.nextTick(cb);
2665
+ }
2666
+ }
2667
+
2668
+ ping(opaque, cb) {
2669
+ const body = Buffer.alloc(8);
2670
+ if (opaque) {
2671
+ Buffer.from(opaque).copy(body);
2672
+ }
2673
+ this._frame(PING, 0, 0, body, true);
2674
+ this._flush();
2675
+ if (cb) {
2676
+ this.once('ping', () => cb());
2677
+ }
2678
+ return this;
2679
+ }
2680
+
2681
+ goaway(code = NO_ERROR, lastStreamID = this._maxInboundSid, reason) {
2682
+ this.sendGoaway(lastStreamID, code, reason);
2683
+ this._flush();
2684
+ return this;
2685
+ }
2686
+
2687
+ ref() {
2688
+ this.socket.ref(); return this;
2689
+ }
2690
+
2691
+ unref() {
2692
+ this.socket.unref(); return this;
2693
+ }
2694
+ }
2695
+
2696
+ module.exports = { Http2Session, OUR_DEFAULTS, PEER_DEFAULTS, SETTINGS_BY_ID,
2697
+ ID_BY_SETTING, MAX_OUT_BATCH, MAX_SESSION_HOLD, STREAM_HOLD_CHARGE, BODY_MIN,
2698
+ STAGE_MIN, MAX_SHAPES, hashBlock, hashPieces };