h2lean 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +265 -2
- package/index.js +72 -0
- package/lib/client-session.js +2068 -0
- package/lib/client-stream.js +166 -0
- package/lib/client.js +85 -0
- package/lib/compat.js +849 -0
- package/lib/fast-timer.js +436 -0
- package/lib/frames.js +127 -0
- package/lib/headers.js +463 -0
- package/lib/hpack-data.js +333 -0
- package/lib/hpack.js +1288 -0
- package/lib/pieces.js +134 -0
- package/lib/server.js +272 -0
- package/lib/session.js +2698 -0
- package/lib/stream.js +519 -0
- package/package.json +49 -4
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 h2lite 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 h2lite 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 h2lite 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 h2lite
|
|
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
|
+
// h2lite 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 h2lite 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. h2lite 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 h2lite 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. h2lite 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 h2lite'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 h2lite'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
|
+
// h2lite 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 };
|