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
|
@@ -0,0 +1,2068 @@
|
|
|
1
|
+
// HTTP/2 client connection state machine (h2c and h2s). The mirror of lib/session.js.
|
|
2
|
+
//
|
|
3
|
+
// The server file is hard-locked to its role -- it reads the client preface
|
|
4
|
+
// (session.js:771), refuses even stream ids (session.js:968) and treats the
|
|
5
|
+
// rapid-reset budget as "server side only". A client does the exact inverse on
|
|
6
|
+
// every one of those: it WRITES the preface, opens ODD ids, parses `:status`
|
|
7
|
+
// instead of `:method`, obeys the peer's SETTINGS rather than advertising its
|
|
8
|
+
// own limits, and returns flow-control credit for bytes it consumed instead of
|
|
9
|
+
// granting it from its window. So this is a second state machine sharing the
|
|
10
|
+
// role-neutral primitives, not a `role` flag on the first.
|
|
11
|
+
//
|
|
12
|
+
// Reference for the JS-visible client surface is node's own bundled
|
|
13
|
+
// internal/http2/core.js: connect() (arg normalisation), ClientHttp2Session
|
|
14
|
+
// .request() (defaults + endStream = isPayloadMeaningless(method)) and
|
|
15
|
+
// ClientHttp2Stream (a Duplex). Measured off node v26.12.1 in the same session.
|
|
16
|
+
//
|
|
17
|
+
// The two performance decisions are the server's, ported rather than re-invented
|
|
18
|
+
// (AGENTS.md 1), because a client and a server move the same bytes in opposite
|
|
19
|
+
// directions and the shape of the work is identical:
|
|
20
|
+
// * a whole frame is parsed in place out of the chunk node handed us. A frame
|
|
21
|
+
// that straddles two reads is not assembled at all: its payload is kept as
|
|
22
|
+
// the views of the chunks it arrived in, one view per read. A DATA payload
|
|
23
|
+
// reaches the application that way, which is what node does with the same
|
|
24
|
+
// frame (tools/probe-chunking.js measures both stacks delivering a cut
|
|
25
|
+
// payload as the pieces it arrived in), and a header block is decoded out of
|
|
26
|
+
// the views (lib/hpack.js decodePieces). A padded DATA payload is no copy
|
|
27
|
+
// either: its views are not the payload (RFC 9113 6.1 -- the first octet is
|
|
28
|
+
// the pad length, the last ones are the padding), so the padding is taken
|
|
29
|
+
// off the views once the frame is complete (lib/frames.js trimPadding);
|
|
30
|
+
// * one output staging buffer per batch: every frame produced while handling one
|
|
31
|
+
// read or one tick is serialised into it and leaves through a single write (or a
|
|
32
|
+
// single corked writev for a body that travels by reference above BODY_MIN), so a
|
|
33
|
+
// HEADERS + WINDOW_UPDATE batch costs no per-frame allocation and no per-frame
|
|
34
|
+
// syscall. The credit release has its own coalescing buffer (_windowUpdate), because
|
|
35
|
+
// it is due at the end of a read batch, when the previous one is usually still in
|
|
36
|
+
// flight and the queue would hold it a whole poll cycle.
|
|
37
|
+
// What still differs from the server, deliberately: receive credit is returned for
|
|
38
|
+
// bytes as they ARRIVE, not when the application reads them (so a slow reader does
|
|
39
|
+
// not throttle the peer -- read-side backpressure is a behaviour change that needs
|
|
40
|
+
// its own differential tests, not a performance tweak), and a body is never deferred
|
|
41
|
+
// for lack of queue room because the staging bound above already caps what one
|
|
42
|
+
// connection can make us hold.
|
|
43
|
+
// A fatal inbound frame ends the session the way node's does: GOAWAY on the wire
|
|
44
|
+
// -- flushed out of the staging batch before the socket is closed (_fatal; the
|
|
45
|
+
// server side does it the same way, lib/session.js _fatal -> sendGoaway -> _flush)
|
|
46
|
+
// -- and ERR_HTTP2_ERROR reported on the stream and on the session. Measured
|
|
47
|
+
// against node v26.12.1 on a malformed padded DATA frame
|
|
48
|
+
// (tools/probe-client-pad-bad.js) and on padded HEADERS frames
|
|
49
|
+
// (tools/probe-client-hdr-pad.js).
|
|
50
|
+
// The padded-frame rules are the server's, ported (lib/session.js _headersPrefix
|
|
51
|
+
// and _onDataFrame): a padded HEADERS whose padding does not fit is a connection
|
|
52
|
+
// error of type PROTOCOL_ERROR (RFC 9113 6.2: "If the length of the padding is
|
|
53
|
+
// the length of the frame payload or greater"), and so is a padded frame with no
|
|
54
|
+
// payload at all -- no pad-length octet to read (_beginHeaders refuses both). The
|
|
55
|
+
// DATA rule is RFC 9113 6.1, measured by tools/probe-client-pad-bad.js
|
|
56
|
+
// (_onDataFrame). The response pseudo-header rules are checked in
|
|
57
|
+
// _refuseHeaders before the application sees the object: a response without
|
|
58
|
+
// exactly one :status, a request-only pseudo-header on a response, and a
|
|
59
|
+
// pseudo-header in a trailer block (RFC 9113 8.1) are all per-stream refusals
|
|
60
|
+
// -- ERR_HTTP2_STREAM_ERROR, RST_STREAM(PROTOCOL_ERROR), the session stays up
|
|
61
|
+
// (measured, tools/probe-client-status.js; this closes the empty-block
|
|
62
|
+
// divergence).
|
|
63
|
+
'use strict';
|
|
64
|
+
|
|
65
|
+
const { EventEmitter } = require('node:events');
|
|
66
|
+
const tls = require('node:tls');
|
|
67
|
+
const f = require('./frames');
|
|
68
|
+
const { Decoder, Encoder, HPackError } = require('./hpack');
|
|
69
|
+
const { nullProto, isFieldName, lowerName, CONNECTION_SPECIFIC } = require('./headers');
|
|
70
|
+
const { ClientHttp2Stream } = require('./client-stream');
|
|
71
|
+
const { pieceByteAt, pieceSlice, pieceConcat, pieceEquals } = require('./pieces');
|
|
72
|
+
// The settings tables, defaults and buffer-path constants are the server's, reused
|
|
73
|
+
// verbatim so a client and a server in this one library never disagree about what a
|
|
74
|
+
// SETTINGS id means, what node advertises, or where the copy/reference break-even sits.
|
|
75
|
+
const {
|
|
76
|
+
OUR_DEFAULTS, PEER_DEFAULTS, SETTINGS_BY_ID, ID_BY_SETTING, BODY_MIN, MAX_OUT_BATCH,
|
|
77
|
+
STAGE_MIN, MAX_SHAPES, hashBlock, hashPieces
|
|
78
|
+
} = require('./session');
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY,
|
|
82
|
+
WINDOW_UPDATE, CONTINUATION,
|
|
83
|
+
F_END_STREAM, F_ACK, F_END_HEADERS, F_PADDED, F_PRIORITY,
|
|
84
|
+
NO_ERROR, PROTOCOL_ERROR, FRAME_SIZE_ERROR, COMPRESSION_ERROR, CANCEL,
|
|
85
|
+
HEADER_LEN, PREFACE, PREFACE_LEN, readHeader, writeHeader, trimPadding
|
|
86
|
+
} = f;
|
|
87
|
+
|
|
88
|
+
const MAX_SETTINGS_ENTRIES = 32; // CVE-2020-11080; same bound as the server
|
|
89
|
+
const MAX_CONTINUATIONS = 8; // CVE-2024-28182; same bound as the server
|
|
90
|
+
// RFC 7540 6.9.2 / 6.5.2: what an endpoint that has not been told otherwise runs a
|
|
91
|
+
// stream's flow control with. The reason it matters here is ordering: a server writes
|
|
92
|
+
// its SETTINGS -- advertising its own window -- before it has read ours, so any stream it
|
|
93
|
+
// answers on is sized by OUR advertised value if that has landed by then and by this
|
|
94
|
+
// default if it has not. The client cannot tell which happened, so credit release is
|
|
95
|
+
// planned against the smaller of the two -- see _creditRecv().
|
|
96
|
+
const DEFAULT_WINDOW = 65535;
|
|
97
|
+
|
|
98
|
+
// The field-name checks are the server's, imported rather than restated: `isFieldName`
|
|
99
|
+
// carries the cache of names already known to be legal (lib/headers.js, and
|
|
100
|
+
// OPTIMIZATION.md item 7 for what it measures on the names a request really
|
|
101
|
+
// carries). Re-running an uncached regex here would be the same work the response
|
|
102
|
+
// path has already learned to skip.
|
|
103
|
+
|
|
104
|
+
// Methods RFC 9113 asks a client not to send a body with; node ends the stream by
|
|
105
|
+
// default for these (`isPayloadMeaningless(method)` in core.js).
|
|
106
|
+
function payloadMeaningless(method) {
|
|
107
|
+
return method === 'GET' || method === 'HEAD' || method === 'DELETE' || method === 'CONNECT'
|
|
108
|
+
|| method === 'OPTIONS' || method === 'TRACE';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class ClientHttp2Session extends EventEmitter {
|
|
112
|
+
constructor(socket, options, scheme, authority) {
|
|
113
|
+
super();
|
|
114
|
+
const opts = options || {};
|
|
115
|
+
this.socket = socket;
|
|
116
|
+
this.type = 1; // NGHTTP2_SESSION_CLIENT
|
|
117
|
+
this.closed = false;
|
|
118
|
+
this.destroyed = false;
|
|
119
|
+
this.connecting = true;
|
|
120
|
+
// encrypted/alpnProtocol are derived from the socket once it is connected
|
|
121
|
+
// (_start), the way node's setupHandle fills them after 'secureConnect':
|
|
122
|
+
// before that node's getters read undefined.
|
|
123
|
+
this.pendingSettingsAck = true;
|
|
124
|
+
this.originSet = [];
|
|
125
|
+
this.localSettings = { ...OUR_DEFAULTS, ...opts.settings };
|
|
126
|
+
this.remoteSettings = { ...PEER_DEFAULTS };
|
|
127
|
+
this._scheme = scheme || 'http';
|
|
128
|
+
this._authority = authority || 'localhost';
|
|
129
|
+
|
|
130
|
+
// Only what the application asked for is advertised (see session.js _advertised:
|
|
131
|
+
// a number we advertise is a number a peer plans around). Empty by default, which
|
|
132
|
+
// is node's usual client SETTINGS frame too.
|
|
133
|
+
this._advertised = new Set((opts.advertised !== undefined
|
|
134
|
+
? opts.advertised : Object.keys(opts.settings || {}))
|
|
135
|
+
.filter((k) => ID_BY_SETTING[k] !== undefined));
|
|
136
|
+
|
|
137
|
+
this._nextSid = 1; // client owns odd ids
|
|
138
|
+
this._streams = new Map();
|
|
139
|
+
this._outboundQueue = []; // requests waiting for a concurrency slot
|
|
140
|
+
this._decoder = new Decoder(this.localSettings.headerTableSize);
|
|
141
|
+
this._encoder = new Encoder(this.remoteSettings.headerTableSize,
|
|
142
|
+
this.localSettings.headerTableSize);
|
|
143
|
+
this._blocks = new WeakMap();
|
|
144
|
+
this._flat = [];
|
|
145
|
+
|
|
146
|
+
// Send-side windows: our budget to write, governed by the peer's window. Both
|
|
147
|
+
// start at 65535 and move with the peer's SETTINGS delta and its WINDOW_UPDATEs.
|
|
148
|
+
this._connSendWin = PEER_DEFAULTS.initialWindowSize;
|
|
149
|
+
this._prevInitialWin = PEER_DEFAULTS.initialWindowSize;
|
|
150
|
+
// The receive side's mirror: octets consumed from the connection window whose
|
|
151
|
+
// credit is not yet announced. Released in half-window steps -- see
|
|
152
|
+
// _creditRecv() and the server's _creditConnection() for the measurement.
|
|
153
|
+
this._connRecvUsed = 0;
|
|
154
|
+
// The connection receive window we actually maintain: spent by every DATA octet,
|
|
155
|
+
// restored by every WINDOW_UPDATE(0) we send. It starts at the smaller of what we
|
|
156
|
+
// advertise and the 65535 a peer runs with until it has read our SETTINGS, which is
|
|
157
|
+
// the only value we can promise it credit against -- see _creditRecv(). At the
|
|
158
|
+
// default this is the same number the old `>= advertised >> 1` test produced.
|
|
159
|
+
this._connRecvWin = Math.min(this.localSettings.initialWindowSize, DEFAULT_WINDOW);
|
|
160
|
+
// Streams that took DATA since the last credit pass. _creditRecv() visits only
|
|
161
|
+
// these instead of scanning every open stream: with the default 100 concurrent
|
|
162
|
+
// streams a download that touches one of them used to pay ~100 property reads per
|
|
163
|
+
// socket read (OPTIMIZATION.md item 19).
|
|
164
|
+
this._debt = [];
|
|
165
|
+
// The credit pass writes every WINDOW_UPDATE due from one batch into this one
|
|
166
|
+
// session-owned buffer -- reused, never handed to the application -- so the batch
|
|
167
|
+
// costs one small write instead of one allocation and one write call per update.
|
|
168
|
+
this._wuBuf = null;
|
|
169
|
+
this._wuLen = 0;
|
|
170
|
+
|
|
171
|
+
// Inbound parsing, the server's design: frames are read straight out of the chunk
|
|
172
|
+
// node gave us, and a frame that straddles two reads is kept as the views it
|
|
173
|
+
// arrived in. The only octets the session copies are the nine of a frame header
|
|
174
|
+
// that a read split: they wait in this scratch until the header is complete.
|
|
175
|
+
this._fragHdr = Buffer.allocUnsafe(HEADER_LEN);
|
|
176
|
+
this._frag = 0; // bytes of a straddling frame
|
|
177
|
+
this._fragTarget = null;
|
|
178
|
+
// A straddling DATA payload is NOT assembled: it is kept as the views of the
|
|
179
|
+
// chunks the peer sent it in -- one per read -- and handed to the application
|
|
180
|
+
// as they are. Node does the same: its payload reaches the application in the
|
|
181
|
+
// pieces it arrived in, not in one piece per frame (measured with a frame whose
|
|
182
|
+
// payload was cut in two: node's client gets two chunks, tools/probe-chunking.js),
|
|
183
|
+
// so this is parity rather than a shortcut.
|
|
184
|
+
this._fragPieces = null;
|
|
185
|
+
//
|
|
186
|
+
// A PADDED payload is the one case where the views are not the payload: its
|
|
187
|
+
// first octet is the pad length and its last octets are padding (RFC 9113
|
|
188
|
+
// 6.1). The padding is taken off the views when the frame is complete
|
|
189
|
+
// (frames.js trimPadding), so even that case costs no copy.
|
|
190
|
+
// The header of the frame being assembled, kept HERE and not read back from
|
|
191
|
+
// frames.js's FH_TYPE/FH_FLAGS/FH_SID. Those are module-level slots shared by every
|
|
192
|
+
// session in the process, and a straddling frame is dispatched in a LATER _onData()
|
|
193
|
+
// call: another connection's readHeader() in between replaces them, and this session
|
|
194
|
+
// delivers a frame its peer never sent. Measured with 8 sessions x 64 streams of
|
|
195
|
+
// 256 kB (bench/micro/probe-client-framedrop.js): a DATA frame dispatched as WINDOW_UPDATE/0,
|
|
196
|
+
// one whole 16 384-octet frame missing per affected stream, then HPACK
|
|
197
|
+
// COMPRESSION_ERROR and the connection destroyed -- which is what the paired
|
|
198
|
+
// benchmark had been reading as "the client's download is 2.5x slower".
|
|
199
|
+
this._fragType = 0;
|
|
200
|
+
this._fragFlags = 0;
|
|
201
|
+
this._fragSid = 0;
|
|
202
|
+
// CONTINUATION assembly for one inbound header block. A block whose frames all
|
|
203
|
+
// arrived whole is kept in one buffer (_hdr); a block with a frame that straddled
|
|
204
|
+
// a read is kept as the views it arrived in (_hdrPieces).
|
|
205
|
+
this._hdr = null;
|
|
206
|
+
this._hdrPieces = null;
|
|
207
|
+
this._hdrLen = 0;
|
|
208
|
+
this._hdrSid = -1;
|
|
209
|
+
this._hdrEnd = false;
|
|
210
|
+
this._hdrCont = 0;
|
|
211
|
+
// The first octets of a HEADERS payload -- the pad length and the priority
|
|
212
|
+
// block -- are at most six, and a payload that arrived in pieces has no
|
|
213
|
+
// buffer to read them from: they are copied into this scratch.
|
|
214
|
+
this._hdrPrefix = Buffer.allocUnsafe(6);
|
|
215
|
+
|
|
216
|
+
// Outbound: frames are serialised into one staging buffer per batch and queued by
|
|
217
|
+
// reference only when the payload is big enough for the extra writev chunk to beat
|
|
218
|
+
// the memcpy (BODY_MIN). See _frame/_need/_commit/_writePending, which mirror the
|
|
219
|
+
// server's queue exactly.
|
|
220
|
+
this._outQ = [];
|
|
221
|
+
this._stage = null;
|
|
222
|
+
this._stageLen = 0; // bytes of _stage not yet queued
|
|
223
|
+
this._stageCap = STAGE_MIN; // size of the next staging buffer
|
|
224
|
+
this._outLen = 0; // bytes the socket has not been given
|
|
225
|
+
this._writing = false; // one batch in flight per connection
|
|
226
|
+
this._flushScheduled = false;
|
|
227
|
+
this._open = true;
|
|
228
|
+
this._started = false;
|
|
229
|
+
this._queuedReq = null; // requests made before 'connect'
|
|
230
|
+
this._closeRequested = false; // close()/goaway asked to end once drained
|
|
231
|
+
this._goawayCode = null;
|
|
232
|
+
this._goawayLastStreamID = null;
|
|
233
|
+
this._pings = new Map(); // 8-octet payload -> callback
|
|
234
|
+
this._pingSeq = 0;
|
|
235
|
+
// Response header shapes seen before: the cached object for a block whose bytes
|
|
236
|
+
// we have already decoded under this dynamic-table state (the server's cache).
|
|
237
|
+
this._shapes = opts.shapeCache === false ? null : new Map();
|
|
238
|
+
this._shapeEpoch = 0;
|
|
239
|
+
this._shapeHits = 0;
|
|
240
|
+
this._shapeMisses = 0;
|
|
241
|
+
|
|
242
|
+
this._flushSoon = () => {
|
|
243
|
+
this._flushScheduled = false; this._flush();
|
|
244
|
+
};
|
|
245
|
+
this._onWrittenCb = () => this._onWritten();
|
|
246
|
+
|
|
247
|
+
socket.on('data', (d) => this._onData(d));
|
|
248
|
+
socket.on('error', (err) => this._onSocketError(err));
|
|
249
|
+
socket.on('close', () => this._onSocketClose());
|
|
250
|
+
socket.on('end', () => { /* peer FIN; 'close' does the teardown */ });
|
|
251
|
+
// The socket setup node's Http2Session constructor does when a handle is
|
|
252
|
+
// available (core.js): no Nagle, and no TLS renegotiation under TLS.
|
|
253
|
+
if (typeof socket.setNoDelay === 'function') {
|
|
254
|
+
socket.setNoDelay();
|
|
255
|
+
}
|
|
256
|
+
if (typeof socket.disableRenegotiation === 'function') {
|
|
257
|
+
socket.disableRenegotiation();
|
|
258
|
+
}
|
|
259
|
+
// The preface and our SETTINGS may only go out once the socket is connected;
|
|
260
|
+
// under TLS that is 'secureConnect' -- the handshake has to complete and pick
|
|
261
|
+
// ALPN before the first octet is written (core.js defers setupHandle the same
|
|
262
|
+
// way: "socket.connecting || socket.secureConnecting"). A createConnection
|
|
263
|
+
// socket can already be established, so start immediately then.
|
|
264
|
+
if (socket.connecting || socket.secureConnecting) {
|
|
265
|
+
const ev = socket instanceof tls.TLSSocket ? 'secureConnect' : 'connect';
|
|
266
|
+
socket.once(ev, () => this._start());
|
|
267
|
+
} else {
|
|
268
|
+
this._start();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---- start / connect --------------------------------------------------------
|
|
273
|
+
_start() {
|
|
274
|
+
if (this._started) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this._started = true;
|
|
278
|
+
this.connecting = false;
|
|
279
|
+
// What node's setupHandle records once the socket is connected: whether the
|
|
280
|
+
// transport is a TLSSocket, and what ALPN negotiated on it -- 'h2' normally,
|
|
281
|
+
// false when the peer ignored ALPN (prior knowledge over TLS), 'h2c' on a
|
|
282
|
+
// plain socket. Reported as measured off the socket; node does not check the
|
|
283
|
+
// client side against a required value, and neither do we.
|
|
284
|
+
if (this.socket.encrypted) {
|
|
285
|
+
this.encrypted = true;
|
|
286
|
+
this.alpnProtocol = this.socket.alpnProtocol;
|
|
287
|
+
} else {
|
|
288
|
+
this.encrypted = false;
|
|
289
|
+
this.alpnProtocol = 'h2c';
|
|
290
|
+
}
|
|
291
|
+
// Client magic: 24-octet preface, then SETTINGS. This is the frame ordering the
|
|
292
|
+
// server validates on the way in (session.js:771) and the one we owe here.
|
|
293
|
+
// Anything the application queued before the socket connected (a control frame,
|
|
294
|
+
// say) must follow the handshake, never precede it -- so those bytes are set aside,
|
|
295
|
+
// the handshake is staged in front of them, and the set-aside bytes go back into the
|
|
296
|
+
// queue afterwards, which puts them behind SETTINGS on the wire.
|
|
297
|
+
this._commit();
|
|
298
|
+
const preQueued = this._outQ;
|
|
299
|
+
this._outQ = [];
|
|
300
|
+
this._need(PREFACE_LEN);
|
|
301
|
+
PREFACE.copy(this._stage, this._stageLen);
|
|
302
|
+
this._stageLen += PREFACE_LEN;
|
|
303
|
+
this._outLen += PREFACE_LEN;
|
|
304
|
+
this._sendSettings();
|
|
305
|
+
this._flush();
|
|
306
|
+
if (preQueued.length !== 0) {
|
|
307
|
+
this._outQ = preQueued;
|
|
308
|
+
for (let i = 0; i < preQueued.length; i++) {
|
|
309
|
+
this._outLen += preQueued[i].length;
|
|
310
|
+
}
|
|
311
|
+
this._schedule();
|
|
312
|
+
}
|
|
313
|
+
// Node's ClientHttp2Session defers 'connect' unconditionally — even for an
|
|
314
|
+
// already-established socket passed via createConnection — so that the caller
|
|
315
|
+
// has a chance to attach a listener after connect() returns and before the
|
|
316
|
+
// event fires. Without the deferral, this pattern misses the event:
|
|
317
|
+
// const sess = lite.connect(url, { createConnection: () => alreadyOpenSocket });
|
|
318
|
+
// sess.on('connect', handler); // attached after emit already ran synchronously
|
|
319
|
+
process.nextTick(() => {
|
|
320
|
+
if (this.destroyed || this._closeEmitted) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.emit('connect');
|
|
324
|
+
if (this._queuedReq !== null) {
|
|
325
|
+
const q = this._queuedReq;
|
|
326
|
+
this._queuedReq = null;
|
|
327
|
+
for (const r of q) {
|
|
328
|
+
this._enqueue(r);
|
|
329
|
+
} // respects the peer's concurrency limit
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---- outbound ---------------------------------------------------------------
|
|
335
|
+
// A payload reaches the socket either by reference or by copy, and the difference is
|
|
336
|
+
// who owns the memory. `owned` says it may be kept by reference: the buffer is either
|
|
337
|
+
// one this session allocated for the frame (SETTINGS, RST_STREAM, GOAWAY, PING) or a
|
|
338
|
+
// view of what the application handed to stream.write()/end(), which it may not mutate
|
|
339
|
+
// before the bytes are on the wire -- the same guarantee node:http2 gives. Bodies above
|
|
340
|
+
// BODY_MIN take that path and are never copied. Everything else travels by copy into the
|
|
341
|
+
// staging buffer, because it is a view into memory the next call rewrites -- the HPACK
|
|
342
|
+
// scratch block -- and those payloads are tens of bytes.
|
|
343
|
+
_frame(type, flags, sid, payload, owned) {
|
|
344
|
+
const len = payload === null || payload === undefined ? 0 : payload.length;
|
|
345
|
+
if (owned && len >= BODY_MIN) {
|
|
346
|
+
this._commit(); // what is staged was produced earlier, so it
|
|
347
|
+
// has to go out first
|
|
348
|
+
const head = Buffer.allocUnsafe(HEADER_LEN);
|
|
349
|
+
writeHeader(head, 0, len, type, flags, sid);
|
|
350
|
+
this._outQ.push(head, payload);
|
|
351
|
+
} else {
|
|
352
|
+
this._need(HEADER_LEN + len);
|
|
353
|
+
const off = this._stageLen;
|
|
354
|
+
writeHeader(this._stage, off, len, type, flags, sid);
|
|
355
|
+
if (len) {
|
|
356
|
+
payload.copy(this._stage, off + HEADER_LEN);
|
|
357
|
+
}
|
|
358
|
+
this._stageLen = off + HEADER_LEN + len;
|
|
359
|
+
}
|
|
360
|
+
this._outLen += HEADER_LEN + len;
|
|
361
|
+
if (!this._inBatch) {
|
|
362
|
+
this._schedule();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Room in the staging buffer for n more bytes, committing what it holds to the queue
|
|
367
|
+
// if it does not fit. A buffer that goes into _outQ is never written into again --
|
|
368
|
+
// those bytes may already be on their way to the peer -- so a full buffer is handed
|
|
369
|
+
// over and a fresh one started, one allocation per batch instead of one per frame.
|
|
370
|
+
_need(n) {
|
|
371
|
+
if (this._stage !== null && this._stageLen + n <= this._stage.length) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this._commit();
|
|
375
|
+
let cap = this._stageCap;
|
|
376
|
+
while (cap < n) {
|
|
377
|
+
cap *= 2;
|
|
378
|
+
}
|
|
379
|
+
this._stageCap = Math.min(cap, MAX_OUT_BATCH);
|
|
380
|
+
this._stage = Buffer.allocUnsafe(cap);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_commit() {
|
|
384
|
+
if (this._stageLen === 0) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this._outQ.push(this._stage.subarray(0, this._stageLen));
|
|
388
|
+
this._stage = null; // the queued bytes are not ours to write
|
|
389
|
+
this._stageLen = 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Frames produced outside a read batch -- a request made from an 'open' handler or
|
|
393
|
+
// after an await -- would otherwise sit in the queue until the next frame arrived.
|
|
394
|
+
// Node drains the tick queue before microtasks, so one microtask per batch still
|
|
395
|
+
// coalesces everything produced in the same turn of the loop into a single write.
|
|
396
|
+
_schedule() {
|
|
397
|
+
if (this._flushScheduled) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this._flushScheduled = true;
|
|
401
|
+
queueMicrotask(this._flushSoon);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_flush() {
|
|
405
|
+
// leaving a batch, whatever the reason: from here on frames are produced outside a
|
|
406
|
+
// socket read and have to schedule their own write
|
|
407
|
+
this._inBatch = false;
|
|
408
|
+
this._writePending();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Hands the queued frames to the socket. The batch is written corked, so the whole of
|
|
412
|
+
// it goes out through one _writev -- both transports implement it -- which is what
|
|
413
|
+
// keeps a HEADERS + DATA + WINDOW_UPDATE batch in a single TCP segment even though the
|
|
414
|
+
// frames were never concatenated into one buffer. Only one batch is ever in flight:
|
|
415
|
+
// the buffers handed over belong to the socket until the callback fires (a TLS socket
|
|
416
|
+
// encrypts at flush time, well after the next read), and an application that keeps
|
|
417
|
+
// writing into a buffer it passed to stream.end() would otherwise change bytes already
|
|
418
|
+
// promised. The queue fills up behind the outstanding write and leaves on _onWritten.
|
|
419
|
+
_writePending() {
|
|
420
|
+
if (this._outLen === 0) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// Nothing leaves before the handshake is laid down: a frame flushed to the socket
|
|
424
|
+
// ahead of PREFACE + SETTINGS makes the peer read it as a pre-preface violation
|
|
425
|
+
// (nghttp2 answers GOAWAY(INTERNAL_ERROR)). _start sets _started and drives the
|
|
426
|
+
// first write itself; until then queued frames simply wait in the staging buffer.
|
|
427
|
+
if (!this._started) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!this._open) {
|
|
431
|
+
this._discardOut(); return;
|
|
432
|
+
}
|
|
433
|
+
if (this._writing || !this.socket.writable) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this._writing = true;
|
|
437
|
+
this._commit();
|
|
438
|
+
const batch = this._outQ;
|
|
439
|
+
this._outQ = [];
|
|
440
|
+
this._outLen = 0;
|
|
441
|
+
const s = this.socket;
|
|
442
|
+
if (batch.length === 1) {
|
|
443
|
+
// the common case: everything the batch holds went into one staging buffer, so
|
|
444
|
+
// there is nothing to coalesce and corking would only add a round trip through the
|
|
445
|
+
// stream's own buffer (OPTIMIZATION.md item 16).
|
|
446
|
+
s.write(batch[0], this._onWrittenCb);
|
|
447
|
+
} else {
|
|
448
|
+
// one callback for the batch, put on its last chunk: writes complete in order, so
|
|
449
|
+
// the last one is the point at which every buffer in it is the socket's no longer
|
|
450
|
+
s.cork();
|
|
451
|
+
for (let i = 0; i < batch.length - 1; i++) {
|
|
452
|
+
s.write(batch[i]);
|
|
453
|
+
}
|
|
454
|
+
s.write(batch[batch.length - 1], this._onWrittenCb);
|
|
455
|
+
s.uncork();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_onWritten() {
|
|
460
|
+
this._writing = false;
|
|
461
|
+
// Dropping the batch is the whole release: the frames were never assembled into
|
|
462
|
+
// anything shared, so there is no tail to move down and nothing to reset.
|
|
463
|
+
this._writePending();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
_discardOut() {
|
|
467
|
+
this._outQ = [];
|
|
468
|
+
this._stage = null;
|
|
469
|
+
this._stageLen = 0;
|
|
470
|
+
this._outLen = 0;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_sendSettings() {
|
|
474
|
+
const s = this.localSettings;
|
|
475
|
+
const names = [...this._advertised].filter((k) => ID_BY_SETTING[k] !== undefined
|
|
476
|
+
&& s[k] !== undefined);
|
|
477
|
+
const body = Buffer.allocUnsafe(names.length * 6);
|
|
478
|
+
for (let i = 0; i < names.length; i++) {
|
|
479
|
+
const name = names[i];
|
|
480
|
+
const value = typeof s[name] === 'boolean' ? (s[name] ? 1 : 0) : s[name];
|
|
481
|
+
const at = i * 6;
|
|
482
|
+
body.writeUInt16BE(ID_BY_SETTING[name], at);
|
|
483
|
+
body.writeUInt32BE(value >>> 0, at + 2);
|
|
484
|
+
}
|
|
485
|
+
this._frame(SETTINGS, 0, 0, body, true);
|
|
486
|
+
this.pendingSettingsAck = true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// request(): the node-shaped surface. Accepts an object, an array of pairs, or
|
|
490
|
+
// nothing; fills the request pseudo-headers from the session's scheme/authority
|
|
491
|
+
// unless the application supplied them; returns a ClientHttp2Stream.
|
|
492
|
+
request(headersParam, options) {
|
|
493
|
+
if (this.destroyed) {
|
|
494
|
+
throw httpErr('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed');
|
|
495
|
+
}
|
|
496
|
+
if (this.closed) {
|
|
497
|
+
throw httpErr('ERR_HTTP2_GOAWAY_SESSION', 'New streams cannot be created after receiving a GOAWAY');
|
|
498
|
+
}
|
|
499
|
+
const options2 = options ? { ...options } : {};
|
|
500
|
+
const list = [];
|
|
501
|
+
let method = 'GET';
|
|
502
|
+
let sawMethod = false;
|
|
503
|
+
|
|
504
|
+
const pushPseudo = (name, value) => {
|
|
505
|
+
list.push(name, value);
|
|
506
|
+
};
|
|
507
|
+
const src = headersParam === undefined ? {} : headersParam;
|
|
508
|
+
if (Array.isArray(src)) {
|
|
509
|
+
for (let i = 0; i + 1 < src.length; i += 2) {
|
|
510
|
+
this._addField(list, src[i], src[i + 1]);
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
const keys = Object.keys(src);
|
|
514
|
+
for (let i = 0; i < keys.length; i++) {
|
|
515
|
+
this._addField(list, keys[i], src[keys[i]]);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// list may already contain app pseudo-headers; extract method for the endStream
|
|
519
|
+
// default and the pseudo-header completion node performs.
|
|
520
|
+
for (let i = 0; i < list.length; i += 2) {
|
|
521
|
+
if (list[i] === ':method') {
|
|
522
|
+
method = list[i + 1]; sawMethod = true; break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!sawMethod) {
|
|
526
|
+
pushPseudo(':method', method);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let endStream = options2.endStream;
|
|
530
|
+
if (endStream === undefined) {
|
|
531
|
+
endStream = payloadMeaningless(method);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const stream = new ClientHttp2Stream(this, this._nextSid, {});
|
|
535
|
+
this._nextSid += 2; // the id is reserved now, HEADERS may wait
|
|
536
|
+
stream.endAfterHeaders = endStream;
|
|
537
|
+
if (endStream) {
|
|
538
|
+
stream.end();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// hp: the original headersParam object. When the application reuses the same
|
|
542
|
+
// object across requests, the HPACK encoder produces identical bytes every time
|
|
543
|
+
// after the dynamic table stabilises, and _sendRequest can use _blocks to replay
|
|
544
|
+
// the encoded block without re-encoding. (The server's shareHeaders equivalent.)
|
|
545
|
+
const req = { stream, list, end: endStream, hp: headersParam };
|
|
546
|
+
if (this._started) {
|
|
547
|
+
this._enqueue(req);
|
|
548
|
+
} else {
|
|
549
|
+
(this._queuedReq || (this._queuedReq = [])).push(req);
|
|
550
|
+
}
|
|
551
|
+
return stream;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Open a request if a concurrency slot is free, otherwise park it. The peer's
|
|
555
|
+
// SETTINGS_MAX_CONCURRENT_STREAMS bounds how many streams may be open at once; a
|
|
556
|
+
// request beyond that limit keeps its reserved id but its HEADERS wait in the queue
|
|
557
|
+
// until an open stream closes (RFC 9113 5.1.2, 6.5.2).
|
|
558
|
+
//
|
|
559
|
+
// The request is NOT held back until the peer's SETTINGS have been applied. Node
|
|
560
|
+
// does not hold it back either: its client emits 'connect' as soon as the socket
|
|
561
|
+
// is up (internal/http2/core.js setupHandle -> nextTick(emit, 'connect')) and
|
|
562
|
+
// submits the request then, so the request goes out in the first flight. Measured
|
|
563
|
+
// against a raw server that delays its SETTINGS by 200 ms: node's request HEADERS
|
|
564
|
+
// arrive at 5 ms, while h2lite's used to arrive at 225 ms -- after the SETTINGS it
|
|
565
|
+
// was waiting for -- which is a round trip on every session's first request
|
|
566
|
+
// (tools/probe-client-first.js).
|
|
567
|
+
//
|
|
568
|
+
// What the peer's SETTINGS would have told us is applied when it is known: the
|
|
569
|
+
// window a stream runs on is taken when the stream is opened (_openStream), and a
|
|
570
|
+
// peer that will not take the stream answers REFUSED_STREAM -- which is what
|
|
571
|
+
// node's client gets for a burst fired before the peer's SETTINGS land too.
|
|
572
|
+
_enqueue(req) {
|
|
573
|
+
const max = this.remoteSettings.maxConcurrentStreams;
|
|
574
|
+
if (this._outboundQueue.length === 0
|
|
575
|
+
&& (max === undefined || this._streams.size < max)) {
|
|
576
|
+
this._openStream(req);
|
|
577
|
+
} else {
|
|
578
|
+
this._outboundQueue.push(req);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
_openStream(req) {
|
|
583
|
+
// RFC 9113 6.9.2: a stream runs on the peer's SETTINGS_INITIAL_WINDOW_SIZE. The
|
|
584
|
+
// stream object was built when the application asked for the request, which may
|
|
585
|
+
// have been before the peer's SETTINGS arrived -- and the delta a later SETTINGS
|
|
586
|
+
// applies reaches only the streams that are open by then (see _applySettings).
|
|
587
|
+
// Taking the window here, where the stream is actually opened, is what makes it
|
|
588
|
+
// the peer's number and not the 65535 default the peer may never have chosen.
|
|
589
|
+
req.stream._sendWin = this.remoteSettings.initialWindowSize;
|
|
590
|
+
this._streams.set(req.stream.id, req.stream);
|
|
591
|
+
this._sendRequest(req.stream, req.list, req.end, req.hp);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// A slot just freed (a stream closed): start queued requests while the peer's
|
|
595
|
+
// concurrency limit and the live connection allow.
|
|
596
|
+
_drainQueue() {
|
|
597
|
+
if (this.destroyed || this._closeEmitted) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const max = this.remoteSettings.maxConcurrentStreams;
|
|
601
|
+
while (this._outboundQueue.length > 0 && (max === undefined || this._streams.size < max)) {
|
|
602
|
+
const req = this._outboundQueue.shift();
|
|
603
|
+
if (this.closed && req.stream.id > (this._goawayLastStreamID || 0)) {
|
|
604
|
+
// past the peer's GOAWAY mark: this request will not be carried; surface it.
|
|
605
|
+
req.stream.destroy(httpErr('ERR_HTTP2_GOAWAY_SESSION',
|
|
606
|
+
'New streams cannot be created after receiving a GOAWAY'));
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
this._openStream(req);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Normalises one application-supplied field into the flat [name, value, ...] list:
|
|
614
|
+
// lower-cases names, coerces pseudo/regular, rejects connection-specific fields
|
|
615
|
+
// (RFC 9113 8.2.2) and a non-token name (RFC 9113 8.2.1) exactly as node's
|
|
616
|
+
// prepareRequestHeaders* does for a client request.
|
|
617
|
+
_addField(list, rawKey, rawValue) {
|
|
618
|
+
if (rawValue === undefined) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const key = lowerName(rawKey);
|
|
622
|
+
// The colon test is spelled as a code-point read, where lib/headers.js uses
|
|
623
|
+
// `key[0] === ':'`. Not a style difference: the one-character read boxes a new
|
|
624
|
+
// one-char string, and charCodeAt is the cheaper spelling on every input --
|
|
625
|
+
// the fall-through case and the match case alike (OPTIMIZATION.md item 33).
|
|
626
|
+
if (key.charCodeAt(0) === 58 /* : */) {
|
|
627
|
+
if (key !== ':method' && key !== ':path' && key !== ':scheme' && key !== ':authority'
|
|
628
|
+
&& key !== ':protocol') {
|
|
629
|
+
throw httpErr('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', `cannot send ${key}`);
|
|
630
|
+
}
|
|
631
|
+
const single = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
632
|
+
list.push(key, String(single));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (!isFieldName(key)) {
|
|
636
|
+
throw httpErr('ERR_INVALID_HTTP_TOKEN', `${key} is an invalid header token`);
|
|
637
|
+
}
|
|
638
|
+
if (CONNECTION_SPECIFIC.has(key)) {
|
|
639
|
+
throw httpErr('ERR_HTTP2_INVALID_CONNECTION_HEADERS',
|
|
640
|
+
`HTTP/1 Connection specific header is forbidden: ${key}`);
|
|
641
|
+
}
|
|
642
|
+
if (Array.isArray(rawValue)) {
|
|
643
|
+
for (let j = 0; j < rawValue.length; j++) {
|
|
644
|
+
list.push(key, String(rawValue[j]));
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
list.push(key, String(rawValue));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
_sendRequest(stream, list, end, hp) {
|
|
652
|
+
// Fill the pseudo-headers the application omitted, in the canonical order, then
|
|
653
|
+
// hand the whole list to HPACK. node completes :path to '/', :scheme and
|
|
654
|
+
// :authority from the session; we mirror that so `request({})` is a GET /.
|
|
655
|
+
//
|
|
656
|
+
// The outbound block cache: when the same headersParam object is passed to
|
|
657
|
+
// request() again and the encoder's dynamic-table epoch has not moved since
|
|
658
|
+
// we last encoded it, the HPACK block is byte-identical to the cached form.
|
|
659
|
+
// Replaying the cached bytes skips _addField, orderPseudo, and _encodeRequest
|
|
660
|
+
// in their entirety -- the server's shareHeaders mechanism inverted: on the
|
|
661
|
+
// server the same idea caches outbound response blocks; here it caches outbound
|
|
662
|
+
// request blocks. The WeakMap entry is dropped with the object itself (no
|
|
663
|
+
// explicit invalidation), and epoch mismatch handles dynamic-table changes.
|
|
664
|
+
// The subarray the cache holds is stable: _encodeRequest's buf is freshly
|
|
665
|
+
// allocated each call, so the view stays alive as long as the entry is reachable.
|
|
666
|
+
const cached = hp !== undefined && hp !== null && typeof hp === 'object'
|
|
667
|
+
? this._blocks.get(hp) : undefined;
|
|
668
|
+
let block;
|
|
669
|
+
if (cached !== undefined && cached.epoch === this._encoder.epoch) {
|
|
670
|
+
block = cached.block;
|
|
671
|
+
} else {
|
|
672
|
+
if (!list.includes(':path')) {
|
|
673
|
+
list.push(':path', '/');
|
|
674
|
+
}
|
|
675
|
+
if (!list.includes(':scheme')) {
|
|
676
|
+
list.push(':scheme', this._scheme);
|
|
677
|
+
}
|
|
678
|
+
if (!list.includes(':authority')) {
|
|
679
|
+
list.push(':authority', this._authority);
|
|
680
|
+
}
|
|
681
|
+
const ordered = orderPseudo(list);
|
|
682
|
+
block = this._encodeRequest(ordered);
|
|
683
|
+
if (hp !== undefined && hp !== null && typeof hp === 'object') {
|
|
684
|
+
this._blocks.set(hp, { epoch: this._encoder.epoch, block });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
stream._sid = stream.id;
|
|
688
|
+
this._sendHeaders(stream.id, block, end);
|
|
689
|
+
// HEADERS is now queued; release any body the application wrote before the
|
|
690
|
+
// socket connected (or before this queued request was framed). The DATA frames
|
|
691
|
+
// land in _out behind the HEADERS, so the wire order stays HEADERS -> DATA.
|
|
692
|
+
stream._headersSent = true;
|
|
693
|
+
stream.headersSent = true;
|
|
694
|
+
if (stream._pending !== null || stream._finalCb !== null) {
|
|
695
|
+
this._pump(stream);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
_encodeRequest(list) {
|
|
700
|
+
const enc = this._encoder;
|
|
701
|
+
// The dynamic-table-size announcement has to precede the block it applies to, and its
|
|
702
|
+
// length is not known before the block is written: the field lines are what change the
|
|
703
|
+
// table, and the announcement reports where the table ended up. So the take happens
|
|
704
|
+
// first and its octets are copied into the headroom of the same buffer the block is
|
|
705
|
+
// encoded into -- never into a second one.
|
|
706
|
+
const update = enc.takeSizeUpdate();
|
|
707
|
+
let cap = 0;
|
|
708
|
+
for (let i = 0; i < list.length; i += 2) {
|
|
709
|
+
cap += Encoder.maxBytesOf(list[i], list[i + 1]);
|
|
710
|
+
}
|
|
711
|
+
// Without a size update the block IS the scratch: _sendHeaders runs before any later
|
|
712
|
+
// encode can rewrite it, and _frame(owned=false) puts a stable copy in the batch.
|
|
713
|
+
// With one, the old form was Buffer.concat([update, block]) -- a second allocation,
|
|
714
|
+
// a second copy of the whole block and a temporary array per request. Reserving the
|
|
715
|
+
// update's length up front costs one memcpy of the update itself
|
|
716
|
+
// (OPTIMIZATION.md item 14).
|
|
717
|
+
// The headroom exists on this branch only, and the returned view then starts past
|
|
718
|
+
// the front of its buffer -- which every consumer here reads as
|
|
719
|
+
// .length/.copy/write(), all of which honour byteOffset.
|
|
720
|
+
const ul = update === null ? 0 : update.length;
|
|
721
|
+
const buf = Buffer.allocUnsafe(ul + cap);
|
|
722
|
+
enc.unsettled = false;
|
|
723
|
+
let off = ul;
|
|
724
|
+
for (let i = 0; i < list.length; i += 2) {
|
|
725
|
+
off = enc.write(buf, off, list[i], list[i + 1], false);
|
|
726
|
+
}
|
|
727
|
+
if (ul === 0) {
|
|
728
|
+
return buf.subarray(0, off);
|
|
729
|
+
}
|
|
730
|
+
update.copy(buf, 0);
|
|
731
|
+
return buf.subarray(0, off);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
_sendHeaders(sid, block, end) {
|
|
735
|
+
const max = this.remoteSettings.maxFrameSize;
|
|
736
|
+
if (block.length <= max) {
|
|
737
|
+
// owned=false: the batch keeps a copy, because `block` may be the encoder
|
|
738
|
+
// scratch; one memcpy, and the double allocation the old by-reference path cost
|
|
739
|
+
// is gone.
|
|
740
|
+
this._frame(HEADERS, F_END_HEADERS | (end ? F_END_STREAM : 0), sid, block, false);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this._frame(HEADERS, 0, sid, block.subarray(0, max), false);
|
|
744
|
+
let off = max;
|
|
745
|
+
for (;;) {
|
|
746
|
+
const n = Math.min(block.length - off, max);
|
|
747
|
+
const last = off + n >= block.length;
|
|
748
|
+
this._frame(CONTINUATION, last ? F_END_HEADERS | (end ? F_END_STREAM : 0) : 0,
|
|
749
|
+
sid, block.subarray(off, off + n), false);
|
|
750
|
+
off += n;
|
|
751
|
+
if (last) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Flow-controlled send for the writable half of a stream. Called from the Duplex's
|
|
758
|
+
// _write/_final and again from WINDOW_UPDATE / SETTINGS to resume a stalled body.
|
|
759
|
+
_pump(stream) {
|
|
760
|
+
// No bytes may leave before the stream's HEADERS: DATA on an unopened stream id
|
|
761
|
+
// is a connection error, and nghttp2 answers it with GOAWAY(INTERNAL_ERROR). A
|
|
762
|
+
// write() that arrives before 'connect' parks its payload in _pending and waits
|
|
763
|
+
// for _sendRequest to flip _headersSent and call us again.
|
|
764
|
+
if (!stream._headersSent) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const max = this.remoteSettings.maxFrameSize;
|
|
768
|
+
while (stream._pending !== null) {
|
|
769
|
+
const room = Math.min(this._connSendWin, stream._sendWin, max);
|
|
770
|
+
if (room <= 0) {
|
|
771
|
+
return;
|
|
772
|
+
} // stalled; WINDOW_UPDATE resumes
|
|
773
|
+
const avail = stream._pending.length - stream._pendingOff;
|
|
774
|
+
const take = Math.min(room, avail);
|
|
775
|
+
const finishedByte = (stream._pendingOff + take) === stream._pending.length;
|
|
776
|
+
const es = finishedByte && stream._writeCb === null && stream._finalCb !== null;
|
|
777
|
+
const slice = (stream._pendingOff === 0 && take === stream._pending.length)
|
|
778
|
+
? stream._pending : stream._pending.subarray(stream._pendingOff, stream._pendingOff + take);
|
|
779
|
+
this._frame(DATA, es ? F_END_STREAM : 0, stream.id, slice, true);
|
|
780
|
+
this._connSendWin -= take;
|
|
781
|
+
stream._sendWin -= take;
|
|
782
|
+
stream._pendingOff += take;
|
|
783
|
+
if (stream._pendingOff === stream._pending.length) {
|
|
784
|
+
const cb = stream._writeCb;
|
|
785
|
+
stream._pending = null;
|
|
786
|
+
stream._pendingOff = 0;
|
|
787
|
+
if (cb !== null) {
|
|
788
|
+
stream._writeCb = null; cb();
|
|
789
|
+
}
|
|
790
|
+
if (es) {
|
|
791
|
+
stream._writableDone(); return;
|
|
792
|
+
}
|
|
793
|
+
break; // a further write may follow
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// Everything is framed and we are finalizing: close the message with END_STREAM,
|
|
797
|
+
// unless the GET put it on HEADERS already.
|
|
798
|
+
if (stream._pending === null && stream._finalCb !== null && stream._writeCb === null
|
|
799
|
+
&& !stream._endSent) {
|
|
800
|
+
if (stream.endAfterHeaders) {
|
|
801
|
+
stream._writableDone(); return;
|
|
802
|
+
}
|
|
803
|
+
this._frame(DATA, F_END_STREAM, stream.id, null, false);
|
|
804
|
+
stream._writableDone();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ---- inbound ----------------------------------------------------------------
|
|
809
|
+
// The server's parse loop, mirrored: a frame that fits inside the chunk is handled
|
|
810
|
+
// in place with no copy at all, and a straddling frame is kept as the views it
|
|
811
|
+
// arrived in -- a DATA payload is handed to the application as those views, a
|
|
812
|
+
// header block is decoded out of them, and the control frames are put in a
|
|
813
|
+
// buffer of their own for the parse. Node's socket chunks are never rewritten
|
|
814
|
+
// after being handed out (measured, AGENTS.md 4), which is what makes the view
|
|
815
|
+
// safe to keep.
|
|
816
|
+
_onData(chunk) {
|
|
817
|
+
const n = chunk.length;
|
|
818
|
+
let i = 0;
|
|
819
|
+
this._inBatch = true;
|
|
820
|
+
|
|
821
|
+
while (i < n) {
|
|
822
|
+
if (this._frag > 0) {
|
|
823
|
+
// continuing a frame that straddles reads: take no more than it wants
|
|
824
|
+
const want = this._fragTarget === null
|
|
825
|
+
? HEADER_LEN - this._frag
|
|
826
|
+
: this._fragTarget - this._frag;
|
|
827
|
+
const take = Math.min(want, n - i);
|
|
828
|
+
if (take <= 0) {
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
if (this._fragPieces !== null) {
|
|
832
|
+
this._fragPieces.push(chunk.subarray(i, i + take));
|
|
833
|
+
} else {
|
|
834
|
+
// the frame's header is still incomplete: its octets wait in the
|
|
835
|
+
// scratch until the header is whole
|
|
836
|
+
chunk.copy(this._fragHdr, this._frag, i, i + take);
|
|
837
|
+
}
|
|
838
|
+
this._frag += take;
|
|
839
|
+
i += take;
|
|
840
|
+
if (this._fragTarget === null && this._frag === HEADER_LEN) {
|
|
841
|
+
readHeader(this._fragHdr, 0);
|
|
842
|
+
if (!this._allowFrame(f.FH_TYPE, f.FH_LEN, f.FH_FLAGS)) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
this._fragTarget = HEADER_LEN + f.FH_LEN;
|
|
846
|
+
this._fragType = f.FH_TYPE;
|
|
847
|
+
this._fragFlags = f.FH_FLAGS;
|
|
848
|
+
this._fragSid = f.FH_SID;
|
|
849
|
+
// The payload of the frame is kept as the views it arrives in: a DATA
|
|
850
|
+
// payload is handed to the application that way, and a header block is
|
|
851
|
+
// decoded out of them (lib/hpack.js decodePieces).
|
|
852
|
+
this._fragPieces = [];
|
|
853
|
+
}
|
|
854
|
+
if (this._fragTarget !== null && this._frag === this._fragTarget) {
|
|
855
|
+
const pieces = this._fragPieces;
|
|
856
|
+
const len = this._fragTarget - HEADER_LEN;
|
|
857
|
+
const ok = this._handlePieces(this._fragType, this._fragFlags, this._fragSid, pieces, len);
|
|
858
|
+
this._frag = 0;
|
|
859
|
+
this._fragTarget = null;
|
|
860
|
+
this._fragPieces = null;
|
|
861
|
+
if (!ok) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (n - i < HEADER_LEN) { // even the header is incomplete
|
|
869
|
+
this._fragTarget = null;
|
|
870
|
+
this._fragPieces = null;
|
|
871
|
+
this._frag = n - i;
|
|
872
|
+
chunk.copy(this._fragHdr, 0, i, n);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
readHeader(chunk, i);
|
|
877
|
+
if (!this._allowFrame(f.FH_TYPE, f.FH_LEN, f.FH_FLAGS)) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const total = HEADER_LEN + f.FH_LEN;
|
|
881
|
+
if (n - i < total) { // header known, payload straddles
|
|
882
|
+
this._fragTarget = total;
|
|
883
|
+
this._fragType = f.FH_TYPE;
|
|
884
|
+
this._fragFlags = f.FH_FLAGS;
|
|
885
|
+
this._fragSid = f.FH_SID;
|
|
886
|
+
this._frag = n - i;
|
|
887
|
+
// The payload is not contiguous: it starts in this chunk and ends in a
|
|
888
|
+
// later one. It is kept as the views it is and handed over as the pieces
|
|
889
|
+
// it arrived in, which is what node does with the same frame. A header
|
|
890
|
+
// block is decoded out of them, so it costs no copy either.
|
|
891
|
+
this._fragPieces = [chunk.subarray(i + HEADER_LEN, n)];
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
if (!this._handle(f.FH_TYPE, f.FH_FLAGS, f.FH_SID, chunk, i + HEADER_LEN, f.FH_LEN)) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
i += total;
|
|
898
|
+
if (!this._open) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
this._endBatch();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// What may arrive at all. RFC 9113 6.5.2 lets the peer send up to OUR advertised
|
|
907
|
+
// SETTINGS_MAX_FRAME_SIZE and nothing more; a longer payload has no bounded home
|
|
908
|
+
// in the session, so it is a connection error of type FRAME_SIZE_ERROR -- the same
|
|
909
|
+
// rule the server applies to us. SETTINGS and PING are exempt because their
|
|
910
|
+
// payloads are not frame-size governed (and SETTINGS is bounded by entry count in
|
|
911
|
+
// _applySettings).
|
|
912
|
+
// Unlike the server, no first-frame-is-SETTINGS rule is enforced here: that is a
|
|
913
|
+
// protocol-strictness change, not a fast path, and it needs its own node comparison.
|
|
914
|
+
_allowFrame(type, len, _flags) {
|
|
915
|
+
if (type !== PING && type !== SETTINGS && len > this.localSettings.maxFrameSize) {
|
|
916
|
+
this._fatal(FRAME_SIZE_ERROR, 'frame larger than our advertised max frame size');
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
_endBatch() {
|
|
923
|
+
this._inBatch = false;
|
|
924
|
+
// One read batch is one accounting period: the frames that crossed their
|
|
925
|
+
// thresholds during it leave together, the rest of the bytes stay owed until
|
|
926
|
+
// the next batch pays them (the server's _endBatch does the same). The release
|
|
927
|
+
// visits only the streams in _debt -- the ones that actually took DATA.
|
|
928
|
+
this._creditRecv();
|
|
929
|
+
this._flush();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Return the flow-control credit (RFC 9113 6.9) for every stream that has
|
|
933
|
+
// consumed past half its window, then for the connection's share, each as one
|
|
934
|
+
// coalesced WINDOW_UPDATE. Not every consumed octet is worth a frame: node's
|
|
935
|
+
// nghttp2 releases credit in half-window steps (measured on the server side,
|
|
936
|
+
// session.js _creditConnection, tools/probe-conn-credit.js), and the eager
|
|
937
|
+
// one-frame-per-DATA alternative measured far slower (OPTIMIZATION.md item 32).
|
|
938
|
+
//
|
|
939
|
+
// Each threshold is half of the window the level could ACTUALLY be run with, and the
|
|
940
|
+
// honest answer to that question at both levels is the smaller of what we advertise
|
|
941
|
+
// and the 65535 an endpoint uses until it has read our SETTINGS.
|
|
942
|
+
//
|
|
943
|
+
// The ordering is what forces the conservative number rather than our advertised one.
|
|
944
|
+
// A server writes its SETTINGS before it has read ours, and it sizes a new stream's
|
|
945
|
+
// send window -- and the connection window it starts answering on -- by whatever it
|
|
946
|
+
// knows at that moment: the default if our SETTINGS have not landed yet, our value if
|
|
947
|
+
// they have. The client cannot tell which happened, and bench/micro/probe-win.js measured both
|
|
948
|
+
// shapes on nghttpd: with 262144 advertised the server sent 65535 octets on its
|
|
949
|
+
// connection window and then stopped, because the release for sid 0 was planned
|
|
950
|
+
// against half of 262144 and never came due. A release planned against half of the
|
|
951
|
+
// lower bound is early enough for either case and, being one frame per 32767 octets,
|
|
952
|
+
// is exactly node's measured cadence when both sides run the default -- which is every
|
|
953
|
+
// benchmark and differential test here.
|
|
954
|
+
//
|
|
955
|
+
// The connection window is additionally tracked (`_connRecvWin`) so that a peer which
|
|
956
|
+
// really does run it at the advertised size still gets it restored before it runs
|
|
957
|
+
// out: the release is due when EITHER the half-window step says so or the window the
|
|
958
|
+
// peer may fill without hearing from us drops under the step.
|
|
959
|
+
_creditRecv() {
|
|
960
|
+
const step = Math.min(this.localSettings.initialWindowSize, DEFAULT_WINDOW) >> 1;
|
|
961
|
+
const debt = this._debt;
|
|
962
|
+
// Compacted in place: a stream whose bytes have not yet crossed the half-window
|
|
963
|
+
// step STAYS in the list (and stays marked), because the credit it owes is only
|
|
964
|
+
// paid by a later pass. Clearing the mark without releasing would drop the debt on
|
|
965
|
+
// the floor and leave the peer waiting for a WINDOW_UPDATE that never comes.
|
|
966
|
+
let k = 0;
|
|
967
|
+
for (let i = 0; i < debt.length; i++) {
|
|
968
|
+
const stream = debt[i];
|
|
969
|
+
if (stream._recvUsed >= step) {
|
|
970
|
+
const inc = stream._recvUsed;
|
|
971
|
+
stream._recvUsed = 0;
|
|
972
|
+
stream._debtMarked = false;
|
|
973
|
+
this._windowUpdate(stream.id, inc);
|
|
974
|
+
} else {
|
|
975
|
+
debt[k++] = stream;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
debt.length = k;
|
|
979
|
+
if (this._connRecvUsed >= step
|
|
980
|
+
|| this._connRecvWin - this._connRecvUsed < step) {
|
|
981
|
+
const inc = this._connRecvUsed;
|
|
982
|
+
this._connRecvUsed = 0;
|
|
983
|
+
this._connRecvWin += inc;
|
|
984
|
+
this._windowUpdate(0, inc);
|
|
985
|
+
}
|
|
986
|
+
// Everything the pass produced leaves as one unit. Two exits, chosen by whether
|
|
987
|
+
// anything of ours is still unflushed: with the queue and the staging buffer empty
|
|
988
|
+
// and a batch in flight, the credit goes straight to the socket -- through _outQ it
|
|
989
|
+
// would wait for _onWritten, a whole poll cycle, and the peer sits out that delay
|
|
990
|
+
// (bench/client measured the 256 kB download spending it on every half-window step).
|
|
991
|
+
// Otherwise the bytes are copied into the staging buffer, behind whatever is already
|
|
992
|
+
// in it, so that a HEADERS produced in this same batch cannot be overtaken by the
|
|
993
|
+
// WINDOW_UPDATE for its own stream.
|
|
994
|
+
//
|
|
995
|
+
// The direct write hands over a COPY, not a view of _wuBuf: that buffer is rewritten
|
|
996
|
+
// by the next pass, and a chunk the socket still owns (a partial write, or a TLS
|
|
997
|
+
// socket that encrypts at flush time) must not read bytes we changed -- the issued-
|
|
998
|
+
// buffer rule of AGENTS.md 5. One allocation per credit batch replaces the old one
|
|
999
|
+
// per update.
|
|
1000
|
+
if (this._wuLen !== 0) {
|
|
1001
|
+
if (this._writing && this._open && this._started && this._outLen === 0
|
|
1002
|
+
&& this.socket.writable) {
|
|
1003
|
+
// a copy, not a view -- the reason is the comment above -- taken through
|
|
1004
|
+
// allocUnsafe rather than Buffer.from(subarray): the pooled allocation is the
|
|
1005
|
+
// cheaper way to get a fresh buffer (OPTIMIZATION.md item 18)
|
|
1006
|
+
const out = Buffer.allocUnsafe(this._wuLen);
|
|
1007
|
+
this._wuBuf.copy(out, 0, 0, this._wuLen);
|
|
1008
|
+
this.socket.write(out);
|
|
1009
|
+
} else {
|
|
1010
|
+
this._need(this._wuLen);
|
|
1011
|
+
this._wuBuf.copy(this._stage, this._stageLen, 0, this._wuLen);
|
|
1012
|
+
this._stageLen += this._wuLen;
|
|
1013
|
+
this._outLen += this._wuLen;
|
|
1014
|
+
this._schedule();
|
|
1015
|
+
}
|
|
1016
|
+
this._wuLen = 0;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Returns false when the session has been torn down mid-read (so the parse loop
|
|
1021
|
+
// stops), true to keep going. _fatal and stream teardown set _open false.
|
|
1022
|
+
_handle(type, flags, sid, buf, start, len) {
|
|
1023
|
+
// RFC 9113 6.10: only CONTINUATION may follow a HEADERS that lacks END_HEADERS.
|
|
1024
|
+
if (this._hdrSid !== -1 && type !== CONTINUATION) {
|
|
1025
|
+
return this._fatal(PROTOCOL_ERROR, 'frame interleaved with an unfinished header block');
|
|
1026
|
+
}
|
|
1027
|
+
switch (type) {
|
|
1028
|
+
case SETTINGS:
|
|
1029
|
+
if (sid !== 0) {
|
|
1030
|
+
return this._fatal(PROTOCOL_ERROR, 'SETTINGS with a non-zero stream id');
|
|
1031
|
+
}
|
|
1032
|
+
if (flags & F_ACK) {
|
|
1033
|
+
if (len !== 0) {
|
|
1034
|
+
return this._fatal(FRAME_SIZE_ERROR, 'SETTINGS ACK must be empty');
|
|
1035
|
+
}
|
|
1036
|
+
this.pendingSettingsAck = false;
|
|
1037
|
+
return this._open;
|
|
1038
|
+
}
|
|
1039
|
+
this._applySettings(buf, start, len);
|
|
1040
|
+
this.emit('remoteSettings', this.remoteSettings);
|
|
1041
|
+
this._frame(SETTINGS, F_ACK, 0, null, false);
|
|
1042
|
+
return this._open;
|
|
1043
|
+
case PING:
|
|
1044
|
+
if (sid !== 0) {
|
|
1045
|
+
return this._fatal(PROTOCOL_ERROR, 'PING with a non-zero stream id');
|
|
1046
|
+
}
|
|
1047
|
+
if (len !== 8) {
|
|
1048
|
+
return this._fatal(FRAME_SIZE_ERROR, 'PING payload must be 8 octets');
|
|
1049
|
+
}
|
|
1050
|
+
if (flags & F_ACK) {
|
|
1051
|
+
const key = buf.toString('latin1', start, start + 8);
|
|
1052
|
+
const entry = this._pings.get(key);
|
|
1053
|
+
if (entry !== undefined) {
|
|
1054
|
+
this._pings.delete(key);
|
|
1055
|
+
// node's measured callback shape is (err, duration, pong): duration is a
|
|
1056
|
+
// millisecond number and pong is the echoed 8-octet buffer.
|
|
1057
|
+
const duration = Number(process.hrtime.bigint() - entry.at) / 1e6;
|
|
1058
|
+
// The pong goes to the application, which may keep it: a PING ACK that
|
|
1059
|
+
// arrived in pieces was put in a buffer of its own for the parse -- a
|
|
1060
|
+
// fresh one, not a reused one -- so the view does not move under the
|
|
1061
|
+
// application (AGENTS.md 5: issued buffers stay valid). Node's copy
|
|
1062
|
+
// comes out of the C++ layer as a fresh buffer, so it does not move
|
|
1063
|
+
// under the application there.
|
|
1064
|
+
// a fresh 8-octet buffer for the callback; allocUnsafe + copy rather
|
|
1065
|
+
// than Buffer.from(subarray) for the pooled allocation
|
|
1066
|
+
// (OPTIMIZATION.md item 18)
|
|
1067
|
+
const pong = Buffer.allocUnsafe(8);
|
|
1068
|
+
buf.copy(pong, 0, start, start + 8);
|
|
1069
|
+
entry.cb(null, duration, pong);
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
this._frame(PING, F_ACK, 0, buf.subarray(start, start + 8), true);
|
|
1073
|
+
}
|
|
1074
|
+
return this._open;
|
|
1075
|
+
case HEADERS:
|
|
1076
|
+
this._beginHeaders(sid, flags, buf, start, len);
|
|
1077
|
+
return this._open;
|
|
1078
|
+
case CONTINUATION:
|
|
1079
|
+
if (this._hdrSid !== sid) {
|
|
1080
|
+
return this._fatal(PROTOCOL_ERROR, 'CONTINUATION without HEADERS');
|
|
1081
|
+
}
|
|
1082
|
+
if (++this._hdrCont > MAX_CONTINUATIONS) {
|
|
1083
|
+
return this._fatal(PROTOCOL_ERROR, 'CONTINUATION: too many frames for one block');
|
|
1084
|
+
}
|
|
1085
|
+
this._appendHdr(buf, start, len);
|
|
1086
|
+
if (flags & F_END_HEADERS) {
|
|
1087
|
+
this._finishHeaders();
|
|
1088
|
+
}
|
|
1089
|
+
return this._open;
|
|
1090
|
+
case DATA:
|
|
1091
|
+
this._onDataFrame(sid, flags, buf, start, len);
|
|
1092
|
+
return this._open;
|
|
1093
|
+
case WINDOW_UPDATE:
|
|
1094
|
+
if (len !== 4) {
|
|
1095
|
+
return this._fatal(FRAME_SIZE_ERROR, 'WINDOW_UPDATE payload');
|
|
1096
|
+
}
|
|
1097
|
+
this._onWindowUpdate(sid, buf.readUInt32BE(start) & 0x7fffffff);
|
|
1098
|
+
return this._open;
|
|
1099
|
+
case RST_STREAM:
|
|
1100
|
+
if (len !== 4) {
|
|
1101
|
+
return this._fatal(FRAME_SIZE_ERROR, 'RST_STREAM payload');
|
|
1102
|
+
}
|
|
1103
|
+
this._onRst(sid, buf.readUInt32BE(start));
|
|
1104
|
+
return this._open;
|
|
1105
|
+
case GOAWAY:
|
|
1106
|
+
return this._onGoaway(buf, start, len);
|
|
1107
|
+
case PRIORITY:
|
|
1108
|
+
if (len !== 5) {
|
|
1109
|
+
return this._fatal(FRAME_SIZE_ERROR, 'PRIORITY payload must be 5 octets');
|
|
1110
|
+
}
|
|
1111
|
+
if (sid === 0) {
|
|
1112
|
+
return this._fatal(PROTOCOL_ERROR, 'PRIORITY on stream 0');
|
|
1113
|
+
}
|
|
1114
|
+
return this._open; // contents deprecated (RFC 9218)
|
|
1115
|
+
case PUSH_PROMISE:
|
|
1116
|
+
// A server pushing to us is legal HTTP/2, but push is out of v1 scope (as it
|
|
1117
|
+
// is for the server side); RFC 9113 6.6 makes an unexpected PUSH_PROMISE a
|
|
1118
|
+
// connection error of type PROTOCOL_ERROR, which is what node answers too.
|
|
1119
|
+
return this._fatal(PROTOCOL_ERROR, 'peer sent PUSH_PROMISE');
|
|
1120
|
+
default:
|
|
1121
|
+
return this._open; // RFC 9113 5.5: discard unknown
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// A frame whose payload straddled two reads is dispatched as the pieces it
|
|
1126
|
+
// arrived in. A DATA payload is handed to the application as those pieces; a
|
|
1127
|
+
// header block is decoded out of them; the control frames -- small and of a
|
|
1128
|
+
// fixed size -- are put in a buffer of their own for the parse, which is the
|
|
1129
|
+
// parse the whole-frame path uses.
|
|
1130
|
+
_handlePieces(type, flags, sid, pieces, len) {
|
|
1131
|
+
// RFC 9113 6.10: the same rule the whole-frame path applies (see _handle).
|
|
1132
|
+
// A header block that is unfinished may only be followed by CONTINUATION.
|
|
1133
|
+
if (this._hdrSid !== -1 && type !== CONTINUATION) {
|
|
1134
|
+
return this._fatal(PROTOCOL_ERROR, 'frame interleaved with an unfinished header block');
|
|
1135
|
+
}
|
|
1136
|
+
if (type === DATA) {
|
|
1137
|
+
// RFC 9113 6.1: a padded payload's views are not the payload -- the pad
|
|
1138
|
+
// length is at the front and the padding at the back -- so the padding
|
|
1139
|
+
// comes off the views and the flow control counts the data alone.
|
|
1140
|
+
let data = len;
|
|
1141
|
+
if (flags & F_PADDED) {
|
|
1142
|
+
if (len === 0) {
|
|
1143
|
+
return this._fatal(PROTOCOL_ERROR, 'padded DATA frame with an empty payload');
|
|
1144
|
+
}
|
|
1145
|
+
data = trimPadding(pieces);
|
|
1146
|
+
if (data < 0) {
|
|
1147
|
+
return this._fatal(PROTOCOL_ERROR, 'pad larger than the DATA payload');
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
this._onDataPieces(sid, flags, pieces, data);
|
|
1151
|
+
return this._open;
|
|
1152
|
+
}
|
|
1153
|
+
if (type === HEADERS) {
|
|
1154
|
+
this._beginHeadersPieces(sid, flags, pieces, len);
|
|
1155
|
+
return this._open;
|
|
1156
|
+
}
|
|
1157
|
+
if (type === CONTINUATION) {
|
|
1158
|
+
// the same two rules the whole-frame path applies (see _handle)
|
|
1159
|
+
if (this._hdrSid !== sid) {
|
|
1160
|
+
return this._fatal(PROTOCOL_ERROR, 'CONTINUATION without HEADERS');
|
|
1161
|
+
}
|
|
1162
|
+
if (++this._hdrCont > MAX_CONTINUATIONS) {
|
|
1163
|
+
return this._fatal(PROTOCOL_ERROR, 'CONTINUATION: too many frames for one block');
|
|
1164
|
+
}
|
|
1165
|
+
this._appendHdrPieces(pieces, 0, len);
|
|
1166
|
+
if (flags & F_END_HEADERS) {
|
|
1167
|
+
this._finishHeaders();
|
|
1168
|
+
}
|
|
1169
|
+
return this._open;
|
|
1170
|
+
}
|
|
1171
|
+
// the control frames: the payload goes into a buffer of its own and the
|
|
1172
|
+
// parse is the one _handle does
|
|
1173
|
+
const buf = pieceConcat(pieces, 0, len);
|
|
1174
|
+
return this._handle(type, flags, sid, buf, 0, len);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
_applySettings(buf, start, len) {
|
|
1178
|
+
if (len % 6 !== 0) {
|
|
1179
|
+
this._fatal(FRAME_SIZE_ERROR, 'SETTINGS payload size'); return;
|
|
1180
|
+
}
|
|
1181
|
+
if (len / 6 > MAX_SETTINGS_ENTRIES) {
|
|
1182
|
+
this._fatal(PROTOCOL_ERROR, 'SETTINGS: too many entries'); return;
|
|
1183
|
+
}
|
|
1184
|
+
const seen = this.remoteSettings;
|
|
1185
|
+
for (let i = 0; i + 6 <= len; i += 6) {
|
|
1186
|
+
const id = buf.readUInt16BE(start + i);
|
|
1187
|
+
const value = buf.readUInt32BE(start + i + 2);
|
|
1188
|
+
const name = SETTINGS_BY_ID[id];
|
|
1189
|
+
if (name === undefined) {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
seen[name] = value;
|
|
1193
|
+
}
|
|
1194
|
+
// A new INITIAL_WINDOW_SIZE shifts every open stream window by the delta (RFC 9113
|
|
1195
|
+
// 6.9.2), and may drive it negative -- the same rule the server applies inbound.
|
|
1196
|
+
const delta = seen.initialWindowSize - this._prevInitialWin;
|
|
1197
|
+
if (delta !== 0) {
|
|
1198
|
+
this._prevInitialWin = seen.initialWindowSize;
|
|
1199
|
+
for (const s of this._streams.values()) {
|
|
1200
|
+
s._sendWin += delta; this._pump(s);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
// The peer's table size bounds the encoder that builds blocks for ITS decoder.
|
|
1204
|
+
this._encoder.setPeerTableSize(seen.headerTableSize);
|
|
1205
|
+
// A SETTINGS_MAX_CONCURRENT_STREAMS just learned may open a slot for a parked
|
|
1206
|
+
// request (or a raised limit frees several at once).
|
|
1207
|
+
this._drainQueue();
|
|
1208
|
+
// SETTINGS can end our pending-ack wait and unblock queued sends.
|
|
1209
|
+
for (const s of this._streams.values()) {
|
|
1210
|
+
this._pump(s);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
_beginHeaders(sid, flags, buf, start, len) {
|
|
1215
|
+
let p = start;
|
|
1216
|
+
let l = len;
|
|
1217
|
+
// A payload with no pad length octet has no block at all: the octet the
|
|
1218
|
+
// pad length would be read from is not this frame's, so it is not read.
|
|
1219
|
+
// The split path reads the same (no octet, no pad), so the two agree.
|
|
1220
|
+
if (flags & F_PADDED) {
|
|
1221
|
+
// RFC 9113 6.2: "If the length of the padding is the length of the frame
|
|
1222
|
+
// payload or greater, the recipient MUST treat this as a connection error
|
|
1223
|
+
// of type PROTOCOL_ERROR." A padded payload with no octet at all has no
|
|
1224
|
+
// pad length to read, which is the same connection error -- node ends the
|
|
1225
|
+
// session with ERR_HTTP2_ERROR for both (measured,
|
|
1226
|
+
// tools/probe-client-hdr-pad.js); the server refuses them identically
|
|
1227
|
+
// (lib/session.js _headersPrefix).
|
|
1228
|
+
if (len < 1) {
|
|
1229
|
+
return this._fatal(PROTOCOL_ERROR, 'padded HEADERS frame with an empty payload');
|
|
1230
|
+
}
|
|
1231
|
+
const pad = buf[p];
|
|
1232
|
+
p += 1;
|
|
1233
|
+
l -= 1 + pad;
|
|
1234
|
+
if (l < 0) {
|
|
1235
|
+
return this._fatal(PROTOCOL_ERROR, 'pad larger than the HEADERS payload');
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (flags & F_PRIORITY) {
|
|
1239
|
+
p += 5; l -= 5;
|
|
1240
|
+
}
|
|
1241
|
+
this._hdrCont = 0;
|
|
1242
|
+
if (flags & F_END_HEADERS) {
|
|
1243
|
+
this._processHeaders(sid, (flags & F_END_STREAM) !== 0, buf.subarray(p, p + l));
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
this._hdrSid = sid;
|
|
1247
|
+
this._hdrEnd = (flags & F_END_STREAM) !== 0;
|
|
1248
|
+
this._hdrLen = 0;
|
|
1249
|
+
this._appendHdr(buf, p, l);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// A HEADERS frame whose payload straddled two reads. The pad length is the
|
|
1253
|
+
// payload's first octet and the priority block is the five after it; both are
|
|
1254
|
+
// read out of the views, and the header block stays in them.
|
|
1255
|
+
_beginHeadersPieces(sid, flags, pieces, len) {
|
|
1256
|
+
let p = 0;
|
|
1257
|
+
let l = len;
|
|
1258
|
+
if (flags & F_PADDED) {
|
|
1259
|
+
// The split path enforces the same rule as the whole-buffer one above:
|
|
1260
|
+
// no pad-length octet, or padding that claims more than the payload
|
|
1261
|
+
// holds, is the connection error of RFC 9113 6.2.
|
|
1262
|
+
if (len < 1) {
|
|
1263
|
+
return this._fatal(PROTOCOL_ERROR, 'padded HEADERS frame with an empty payload');
|
|
1264
|
+
}
|
|
1265
|
+
const pad = pieceByteAt(pieces, 0);
|
|
1266
|
+
p += 1;
|
|
1267
|
+
l -= 1 + pad;
|
|
1268
|
+
if (l < 0) {
|
|
1269
|
+
return this._fatal(PROTOCOL_ERROR, 'pad larger than the HEADERS payload');
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (flags & F_PRIORITY) {
|
|
1273
|
+
p += 5; l -= 5;
|
|
1274
|
+
}
|
|
1275
|
+
this._hdrCont = 0;
|
|
1276
|
+
if (l < 0) {
|
|
1277
|
+
l = 0;
|
|
1278
|
+
}
|
|
1279
|
+
if (flags & F_END_HEADERS) {
|
|
1280
|
+
this._processBlockPieces(sid, (flags & F_END_STREAM) !== 0, pieces, p, p + l);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this._hdrSid = sid;
|
|
1284
|
+
this._hdrEnd = (flags & F_END_STREAM) !== 0;
|
|
1285
|
+
this._hdrLen = 0;
|
|
1286
|
+
this._appendHdrPieces(pieces, p, p + l);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
_appendHdr(buf, start, len) {
|
|
1290
|
+
if (this._hdrPieces !== null) {
|
|
1291
|
+
// The block is already kept as the views it arrived in, and this frame's
|
|
1292
|
+
// payload is a view of the chunk it came in -- so it joins them.
|
|
1293
|
+
this._hdrPieces.push(buf.subarray(start, start + len));
|
|
1294
|
+
this._hdrLen += len;
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (this._hdr === null) {
|
|
1298
|
+
this._hdr = Buffer.allocUnsafe(8192);
|
|
1299
|
+
}
|
|
1300
|
+
if (this._hdrLen + len > this._hdr.length) {
|
|
1301
|
+
let cap = this._hdr.length * 2;
|
|
1302
|
+
while (cap < this._hdrLen + len) {
|
|
1303
|
+
cap *= 2;
|
|
1304
|
+
}
|
|
1305
|
+
const bigger = Buffer.allocUnsafe(cap);
|
|
1306
|
+
this._hdr.copy(bigger, 0, 0, this._hdrLen);
|
|
1307
|
+
this._hdr = bigger;
|
|
1308
|
+
}
|
|
1309
|
+
buf.copy(this._hdr, this._hdrLen, start, start + len);
|
|
1310
|
+
this._hdrLen += len;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// The block is kept as the views it arrived in as soon as one frame of it
|
|
1314
|
+
// straddled a read. A block whose frames all arrived whole is kept in one
|
|
1315
|
+
// buffer, and the first split frame turns that buffer into the first view of
|
|
1316
|
+
// the list: the buffer is not written to again while the block is open, and
|
|
1317
|
+
// the block is decoded before the next one starts.
|
|
1318
|
+
_appendHdrPieces(pieces, from, to) {
|
|
1319
|
+
if (this._hdrPieces === null) {
|
|
1320
|
+
this._hdrPieces = this._hdrLen === 0 ? [] : [this._hdr.subarray(0, this._hdrLen)];
|
|
1321
|
+
}
|
|
1322
|
+
const parts = pieceSlice(pieces, from, to);
|
|
1323
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1324
|
+
this._hdrPieces.push(parts[i]);
|
|
1325
|
+
}
|
|
1326
|
+
this._hdrLen += to - from;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
_finishHeaders() {
|
|
1330
|
+
const sid = this._hdrSid, end = this._hdrEnd;
|
|
1331
|
+
if (this._hdrPieces !== null) {
|
|
1332
|
+
const pieces = this._hdrPieces;
|
|
1333
|
+
const len = this._hdrLen;
|
|
1334
|
+
this._hdrSid = -1;
|
|
1335
|
+
this._hdrLen = 0;
|
|
1336
|
+
this._hdrPieces = null;
|
|
1337
|
+
this._processBlockPieces(sid, end, pieces, 0, len);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const block = this._hdr.subarray(0, this._hdrLen);
|
|
1341
|
+
this._hdrSid = -1;
|
|
1342
|
+
this._hdrLen = 0;
|
|
1343
|
+
this._processHeaders(sid, end, block);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// The shape cache: the server's design, ported. A block whose bytes have been
|
|
1347
|
+
// seen before under the same dynamic-table state is decoded from a cached field
|
|
1348
|
+
// list rather than through the HPACK decoder again. The cache is dropped if the
|
|
1349
|
+
// decoder's epoch moves (a table-size change from the peer invalidates every
|
|
1350
|
+
// stored index), and gives up when the miss ratio proves the workload does not
|
|
1351
|
+
// repeat (same heuristic as session.js _processHeaders).
|
|
1352
|
+
_shapeSlot(block) {
|
|
1353
|
+
if (this._shapeEpoch !== this._decoder.epoch) {
|
|
1354
|
+
this._shapes.clear();
|
|
1355
|
+
this._shapeEpoch = this._decoder.epoch;
|
|
1356
|
+
}
|
|
1357
|
+
return hashBlock(block);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// The same bucket for a block that arrived in pieces: the hash is over the
|
|
1361
|
+
// same octets, so a block that arrives whole and one that arrives split are
|
|
1362
|
+
// the same cache entry.
|
|
1363
|
+
_shapeSlotPieces(pieces, from, to) {
|
|
1364
|
+
if (this._shapeEpoch !== this._decoder.epoch) {
|
|
1365
|
+
this._shapes.clear();
|
|
1366
|
+
this._shapeEpoch = this._decoder.epoch;
|
|
1367
|
+
}
|
|
1368
|
+
return hashPieces(pieces, from, to);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// A header block that arrived whole and one that arrived in pieces are the
|
|
1372
|
+
// same block: the octets are the same, so the hash, the cache entry and the
|
|
1373
|
+
// fields are the same. `block` is null for the split one, and the range
|
|
1374
|
+
// [from, to) of `pieces` is where the block sits in the views.
|
|
1375
|
+
_processHeaders(sid, end, block) {
|
|
1376
|
+
this._processBlock(sid, end, block, null, 0, 0);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
_processBlockPieces(sid, end, pieces, from, to) {
|
|
1380
|
+
this._processBlock(sid, end, null, pieces, from, to);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
_processBlock(sid, end, block, pieces, from, to) {
|
|
1384
|
+
const stream = this._streams.get(sid);
|
|
1385
|
+
const flat = this._flat;
|
|
1386
|
+
// The cache is disabled (null) when stream === undefined (we must decode for
|
|
1387
|
+
// HPACK state but there is nothing to cache it for) and when the application
|
|
1388
|
+
// opted out via shapeCache: false.
|
|
1389
|
+
const shapes = stream === undefined ? null : this._shapes;
|
|
1390
|
+
const slot = shapes === null ? 0
|
|
1391
|
+
: (block !== null ? this._shapeSlot(block) : this._shapeSlotPieces(pieces, from, to));
|
|
1392
|
+
const hit = shapes === null ? undefined : shapes.get(slot);
|
|
1393
|
+
let fields = flat;
|
|
1394
|
+
try {
|
|
1395
|
+
if (hit !== undefined && (block !== null
|
|
1396
|
+
? hit.block.equals(block)
|
|
1397
|
+
: pieceEquals(hit.block, pieces, from, to))) {
|
|
1398
|
+
this._shapeHits++;
|
|
1399
|
+
fields = hit.flat;
|
|
1400
|
+
} else {
|
|
1401
|
+
if (block !== null) {
|
|
1402
|
+
this._decoder.decode(block, 0, block.length, flat);
|
|
1403
|
+
} else {
|
|
1404
|
+
this._decoder.decodePieces(pieces, from, to, flat);
|
|
1405
|
+
}
|
|
1406
|
+
if (shapes !== null) {
|
|
1407
|
+
if (++this._shapeMisses > 64 && this._shapeHits * 4 < this._shapeMisses) {
|
|
1408
|
+
this._shapes = null;
|
|
1409
|
+
} else {
|
|
1410
|
+
if (shapes.size >= MAX_SHAPES) {
|
|
1411
|
+
shapes.clear();
|
|
1412
|
+
}
|
|
1413
|
+
shapes.set(slot, {
|
|
1414
|
+
block: block !== null ? Buffer.from(block) : pieceConcat(pieces, from, to),
|
|
1415
|
+
flat: flat.slice(0)
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
if (err instanceof HPackError) {
|
|
1422
|
+
this._fatal(COMPRESSION_ERROR, err.message);
|
|
1423
|
+
} else {
|
|
1424
|
+
throw err;
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (stream === undefined) {
|
|
1429
|
+
// HPACK is in sync (the block decoded above). nghttp2 owns the verdict for
|
|
1430
|
+
// a block naming no live stream (session_process_headers_frame falls
|
|
1431
|
+
// through to nghttp2_session_on_request_headers_received,
|
|
1432
|
+
// nghttp2_session.c): for a client an idle stream -- any id the client has
|
|
1433
|
+
// not sent, odd or even -- is a connection error (NGHTTP2_ERR_PROTO, hence
|
|
1434
|
+
// GOAWAY(PROTOCOL_ERROR) on the wire and ERR_HTTP2_ERROR on the stream and
|
|
1435
|
+
// the session), while a closed stream at or below last_sent_stream_id is
|
|
1436
|
+
// silently ignored (NGHTTP2_ERR_IGN_HEADER_BLOCK). Both measured on node
|
|
1437
|
+
// v26.12.1 (tools/probe-client-unopened-headers.js). The client allocates
|
|
1438
|
+
// odd ids sequentially, so the odd ids below _nextSid are exactly the
|
|
1439
|
+
// opened-and-closed ones.
|
|
1440
|
+
if ((sid & 1) === 1 && sid < this._nextSid) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
return this._fatal(PROTOCOL_ERROR, 'response HEADERS on an unopened stream id');
|
|
1444
|
+
}
|
|
1445
|
+
if (this._refuseHeaders(sid, stream, fields)) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
const headers = this._toObject(fields);
|
|
1449
|
+
stream._onHeaders(headers, end);
|
|
1450
|
+
if (end) {
|
|
1451
|
+
this._dropStream(sid);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// The response pseudo-header rules node enforces in nghttp2's C layer
|
|
1456
|
+
// (nghttp2_http.c nghttp2_http_on_response_headers); node's JS carries no
|
|
1457
|
+
// such check, so the verdicts are measured (tools/probe-client-status.js):
|
|
1458
|
+
// a response block without exactly one :status, or carrying a request-only
|
|
1459
|
+
// pseudo-header, and a trailer block with any pseudo-header at all (RFC 9113
|
|
1460
|
+
// 8.1) are all the same per-stream verdict -- ERR_HTTP2_STREAM_ERROR on the
|
|
1461
|
+
// stream, RST_STREAM(PROTOCOL_ERROR) on the wire, the session stays up.
|
|
1462
|
+
// Both shapes used to reach the application unvalidated. Returns true when
|
|
1463
|
+
// the block was refused, so the caller does not build or hand over the
|
|
1464
|
+
// header object.
|
|
1465
|
+
_refuseHeaders(sid, stream, fields) {
|
|
1466
|
+
let status = 0;
|
|
1467
|
+
let bad = null;
|
|
1468
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
1469
|
+
const name = fields[i];
|
|
1470
|
+
if (name.charCodeAt(0) !== 58) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
// The first block is the response and must carry exactly one :status;
|
|
1474
|
+
// every block after it is trailers, where any pseudo-header at all is
|
|
1475
|
+
// malformed (RFC 9113 8.1).
|
|
1476
|
+
if (!stream.responseEmitted && name === ':status') {
|
|
1477
|
+
status++;
|
|
1478
|
+
} else {
|
|
1479
|
+
bad = name;
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (stream.responseEmitted) {
|
|
1484
|
+
// Trailers: legal whenever no pseudo-header appears at all -- in
|
|
1485
|
+
// particular they carry no :status (the node:http2 server's own
|
|
1486
|
+
// addTrailers blocks are the differential case,
|
|
1487
|
+
// test/client/client-stream.test.js).
|
|
1488
|
+
if (bad === null) {
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
} else if (bad === null && status === 1) {
|
|
1492
|
+
// The response: exactly one :status and no other pseudo-header.
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
// The per-stream verdict: RST_STREAM on the wire, ERR_HTTP2_STREAM_ERROR
|
|
1496
|
+
// on the stream, no 'response'/'trailers' event, the connection lives on.
|
|
1497
|
+
this.rstStream(sid, PROTOCOL_ERROR);
|
|
1498
|
+
stream._onRst(PROTOCOL_ERROR);
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
_toObject(flat) {
|
|
1503
|
+
const o = nullProto();
|
|
1504
|
+
for (let i = 0; i < flat.length; i += 2) {
|
|
1505
|
+
const name = flat[i];
|
|
1506
|
+
let value = flat[i + 1];
|
|
1507
|
+
if (name === ':status') {
|
|
1508
|
+
value = value | 0;
|
|
1509
|
+
}
|
|
1510
|
+
const have = o[name];
|
|
1511
|
+
if (have === undefined) {
|
|
1512
|
+
o[name] = name === 'set-cookie' ? [value] : value;
|
|
1513
|
+
} else if (name === 'cookie') {
|
|
1514
|
+
o[name] = `${have}; ${value}`;
|
|
1515
|
+
} else if (name === 'set-cookie') {
|
|
1516
|
+
have.push(value);
|
|
1517
|
+
} else {
|
|
1518
|
+
o[name] = `${have}, ${value}`;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return o;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
_onDataFrame(sid, flags, buf, start, len) {
|
|
1525
|
+
let p = start;
|
|
1526
|
+
let l = len;
|
|
1527
|
+
if (flags & F_PADDED) {
|
|
1528
|
+
// RFC 9113 6.1: the payload is the pad length, the data, and the padding.
|
|
1529
|
+
// A padded frame with no payload has no pad length to read, and node
|
|
1530
|
+
// answers that with GOAWAY(PROTOCOL_ERROR) (measured, tools/probe-pad-bad.js).
|
|
1531
|
+
if (len === 0) {
|
|
1532
|
+
this._fatal(PROTOCOL_ERROR, 'padded DATA frame with an empty payload'); return;
|
|
1533
|
+
}
|
|
1534
|
+
const pad = buf[p];
|
|
1535
|
+
p += 1;
|
|
1536
|
+
l -= 1 + pad;
|
|
1537
|
+
if (l < 0) {
|
|
1538
|
+
this._fatal(PROTOCOL_ERROR, 'pad larger than the DATA payload'); return;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const stream = this._streams.get(sid);
|
|
1542
|
+
if (stream === undefined) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
// Handed over as a view, the way the server hands DATA to its application: it is
|
|
1546
|
+
// a slice of the chunk node gave us, which node never rewrites after delivery.
|
|
1547
|
+
// A padded frame's payload is a view too -- the pad length and the padding are
|
|
1548
|
+
// taken off the views before this point (frames.js trimPadding). The old
|
|
1549
|
+
// copy-out existed only because the client used to accumulate every read into a
|
|
1550
|
+
// shared buffer it reused -- that buffer is gone.
|
|
1551
|
+
stream._onData(buf.subarray(p, p + l));
|
|
1552
|
+
// Book the consumed bytes as credit owed to BOTH windows; the frames themselves
|
|
1553
|
+
// wait for the half-window threshold at the end of this read batch. Marking the
|
|
1554
|
+
// stream in _debt is what tells that pass which streams to visit.
|
|
1555
|
+
if (l > 0) {
|
|
1556
|
+
this._connRecvUsed += l;
|
|
1557
|
+
stream._recvUsed += l;
|
|
1558
|
+
if (!stream._debtMarked) {
|
|
1559
|
+
stream._debtMarked = true; this._debt.push(stream);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (flags & F_END_STREAM) {
|
|
1563
|
+
stream._endRead(); this._dropStream(sid);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// A payload that straddled two reads: one piece per read, each a view of the
|
|
1568
|
+
// chunk node read off the socket. No copy, and the pieces are the frame's bytes
|
|
1569
|
+
// in the order the peer sent them.
|
|
1570
|
+
_onDataPieces(sid, flags, pieces, total) {
|
|
1571
|
+
const stream = this._streams.get(sid);
|
|
1572
|
+
if (stream === undefined) {
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
1576
|
+
// A piece the padding took all of is empty, and an empty chunk is not a
|
|
1577
|
+
// chunk: node emits no 'data' for one.
|
|
1578
|
+
if (pieces[i] !== null) {
|
|
1579
|
+
stream._onData(pieces[i]);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// Book the consumed bytes as credit owed to BOTH windows; the frames themselves
|
|
1583
|
+
// wait for the half-window threshold at the end of this read batch. Marking the
|
|
1584
|
+
// stream in _debt is what tells that pass which streams to visit.
|
|
1585
|
+
if (total > 0) {
|
|
1586
|
+
this._connRecvUsed += total;
|
|
1587
|
+
stream._recvUsed += total;
|
|
1588
|
+
if (!stream._debtMarked) {
|
|
1589
|
+
stream._debtMarked = true; this._debt.push(stream);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (flags & F_END_STREAM) {
|
|
1593
|
+
stream._endRead(); this._dropStream(sid);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
_onWindowUpdate(sid, inc) {
|
|
1598
|
+
if (inc === 0) {
|
|
1599
|
+
this._fatal(PROTOCOL_ERROR, 'WINDOW_UPDATE increment of 0'); return;
|
|
1600
|
+
}
|
|
1601
|
+
if (sid === 0) {
|
|
1602
|
+
this._connSendWin += inc;
|
|
1603
|
+
for (const s of this._streams.values()) {
|
|
1604
|
+
this._pump(s);
|
|
1605
|
+
}
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const stream = this._streams.get(sid);
|
|
1609
|
+
if (stream === undefined) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
stream._sendWin += inc;
|
|
1613
|
+
this._pump(stream);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
_onRst(sid, code) {
|
|
1617
|
+
const stream = this._streams.get(sid);
|
|
1618
|
+
if (stream === undefined) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
this._dropStream(sid);
|
|
1622
|
+
stream._onRst(code);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Outbound reset: a stream the application abandons mid-flight (destroy/close)
|
|
1626
|
+
// owes the peer an RST_STREAM so its resources are freed. No-ops once the stream
|
|
1627
|
+
// is gone -- including the inbound-RST path, where _onRst has already dropped it,
|
|
1628
|
+
// so we never answer a reset with a reset of our own.
|
|
1629
|
+
rstStream(sid, code) {
|
|
1630
|
+
if (this.destroyed || !this._open || !this._streams.has(sid)) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
const body = Buffer.allocUnsafe(4);
|
|
1634
|
+
body.writeUInt32BE((code === undefined ? CANCEL : code) >>> 0, 0);
|
|
1635
|
+
this._frame(RST_STREAM, 0, sid, body, true);
|
|
1636
|
+
this._dropStream(sid);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
_onGoaway(buf, start, len) {
|
|
1640
|
+
if (len < 8) {
|
|
1641
|
+
this._fatal(FRAME_SIZE_ERROR, 'GOAWAY payload too short'); return this._open;
|
|
1642
|
+
}
|
|
1643
|
+
const lastStreamID = buf.readUInt32BE(start) & 0x7fffffff;
|
|
1644
|
+
const code = buf.readUInt32BE(start + 4);
|
|
1645
|
+
let debug = null;
|
|
1646
|
+
if (len > 8) {
|
|
1647
|
+
// allocUnsafe + copy rather than Buffer.from(subarray) for the pooled
|
|
1648
|
+
// allocation (OPTIMIZATION.md item 18)
|
|
1649
|
+
debug = Buffer.allocUnsafe(len - 8);
|
|
1650
|
+
buf.copy(debug, 0, start + 8, start + len);
|
|
1651
|
+
}
|
|
1652
|
+
this._goawayCode = code;
|
|
1653
|
+
this._goawayLastStreamID = lastStreamID;
|
|
1654
|
+
// Streams the peer refused to process (id above lastStreamID) may be retried;
|
|
1655
|
+
// for M1 we report them and stop opening new ones past the mark.
|
|
1656
|
+
if (lastStreamID < this._nextSid - 2) {
|
|
1657
|
+
this.closed = true;
|
|
1658
|
+
}
|
|
1659
|
+
// node hands the third goaway argument as undefined when the peer sent no debug
|
|
1660
|
+
// data (not an empty buffer), so the measured event shape matches only if we do.
|
|
1661
|
+
this.emit('goaway', code, lastStreamID, debug === null || debug.length === 0
|
|
1662
|
+
? undefined : debug);
|
|
1663
|
+
// A GOAWAY with no live streams left ends the connection for a client too.
|
|
1664
|
+
if (this._streams.size === 0) {
|
|
1665
|
+
this.close();
|
|
1666
|
+
}
|
|
1667
|
+
return this._open;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Credit release. Every update due from one read batch is serialised into the
|
|
1671
|
+
// session's own `_wuBuf` and leaves there as ONE write at the end of the pass (see
|
|
1672
|
+
// _creditRecv); this method only appends. Two properties make it safe.
|
|
1673
|
+
//
|
|
1674
|
+
// The buffer is owned by this session and never handed to the application, so it may
|
|
1675
|
+
// be kept and rewritten the way the per-frame allocation used to be: one 13-octet
|
|
1676
|
+
// memcpy replaces one Buffer.allocUnsafe plus one copy per update, and an eight-step
|
|
1677
|
+
// download closes with one small write instead of eight.
|
|
1678
|
+
//
|
|
1679
|
+
// Callers must be inside a credit pass: the bytes only reach the socket when
|
|
1680
|
+
// _creditRecv() empties _wuBuf at the end of the sweep.
|
|
1681
|
+
_windowUpdate(sid, inc) {
|
|
1682
|
+
if (inc <= 0) {
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
let buf = this._wuBuf;
|
|
1686
|
+
if (buf === null) {
|
|
1687
|
+
buf = this._wuBuf = Buffer.allocUnsafe(13 * 8);
|
|
1688
|
+
}
|
|
1689
|
+
const len = this._wuLen;
|
|
1690
|
+
if (len + HEADER_LEN + 4 > buf.length) {
|
|
1691
|
+
const bigger = Buffer.allocUnsafe((len + HEADER_LEN + 4) * 2);
|
|
1692
|
+
buf.copy(bigger, 0, 0, len);
|
|
1693
|
+
buf = this._wuBuf = bigger;
|
|
1694
|
+
}
|
|
1695
|
+
writeHeader(buf, len, 4, WINDOW_UPDATE, 0, sid);
|
|
1696
|
+
buf.writeUInt32BE(inc >>> 0, len + HEADER_LEN);
|
|
1697
|
+
this._wuLen = len + HEADER_LEN + 4;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// ---- session API ------------------------------------------------------------
|
|
1701
|
+
ping(payload, cb) {
|
|
1702
|
+
// node lets a caller pass the callback as the only argument: ping(cb).
|
|
1703
|
+
if (typeof payload === 'function') {
|
|
1704
|
+
cb = payload; payload = undefined;
|
|
1705
|
+
}
|
|
1706
|
+
const body = Buffer.allocUnsafe(8);
|
|
1707
|
+
if (payload === undefined) {
|
|
1708
|
+
const seq = this._pingSeq++;
|
|
1709
|
+
body.writeUInt32BE(seq >>> 0, 0);
|
|
1710
|
+
body.writeUInt32BE(0, 4);
|
|
1711
|
+
} else {
|
|
1712
|
+
// Buffer.from(payload) would clone the caller's whole buffer just to take the
|
|
1713
|
+
// first eight octets out of it; when the caller already passed a Buffer, copy
|
|
1714
|
+
// straight out of it. The fallback keeps whatever else a caller may pass
|
|
1715
|
+
// (a Uint8Array, a string) working the way Buffer.from decoded it.
|
|
1716
|
+
if (Buffer.isBuffer(payload)) {
|
|
1717
|
+
payload.copy(body, 0, 0, 8);
|
|
1718
|
+
} else {
|
|
1719
|
+
Buffer.from(payload).copy(body, 0, 0, 8);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (typeof cb === 'function') {
|
|
1723
|
+
this._pings.set(body.toString('latin1'), { cb, at: process.hrtime.bigint() });
|
|
1724
|
+
}
|
|
1725
|
+
this._frame(PING, 0, 0, body, true);
|
|
1726
|
+
return body;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
settings(payload, cb) {
|
|
1730
|
+
if (payload !== null && payload !== undefined) {
|
|
1731
|
+
const body = Buffer.from(payload);
|
|
1732
|
+
this._frame(SETTINGS, 0, 0, body, true);
|
|
1733
|
+
if (typeof cb === 'function') {
|
|
1734
|
+
process.nextTick(cb);
|
|
1735
|
+
}
|
|
1736
|
+
} else if (typeof payload === 'function') {
|
|
1737
|
+
process.nextTick(payload);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
goaway(code, lastStreamID, reason) {
|
|
1742
|
+
if (code === undefined) {
|
|
1743
|
+
code = NO_ERROR;
|
|
1744
|
+
}
|
|
1745
|
+
if (lastStreamID === undefined || lastStreamID === null) {
|
|
1746
|
+
lastStreamID = this._nextSid % 2 === 0 ? this._nextSid - 1 : this._nextSid - 2;
|
|
1747
|
+
if (lastStreamID < 0) {
|
|
1748
|
+
lastStreamID = 0;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const tail = reason ? Buffer.byteLength(reason) : 0;
|
|
1752
|
+
const body = Buffer.alloc(8 + tail);
|
|
1753
|
+
body.writeUInt32BE(lastStreamID >>> 0, 0);
|
|
1754
|
+
body.writeUInt32BE(code >>> 0, 4);
|
|
1755
|
+
if (tail) {
|
|
1756
|
+
body.write(reason, 8);
|
|
1757
|
+
}
|
|
1758
|
+
this._frame(GOAWAY, 0, 0, body, true);
|
|
1759
|
+
this.closed = true;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
close(code, cb) {
|
|
1763
|
+
if (typeof code === 'function') {
|
|
1764
|
+
cb = code; code = undefined;
|
|
1765
|
+
}
|
|
1766
|
+
this._closeRequested = true;
|
|
1767
|
+
if (!this.destroyed) {
|
|
1768
|
+
this.goaway(code === undefined ? NO_ERROR : code);
|
|
1769
|
+
}
|
|
1770
|
+
this._maybeClose();
|
|
1771
|
+
if (typeof cb === 'function') {
|
|
1772
|
+
if (this.closed && this._streams.size === 0) {
|
|
1773
|
+
process.nextTick(cb);
|
|
1774
|
+
} else {
|
|
1775
|
+
this.once('close', cb);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
destroy(err) {
|
|
1781
|
+
if (this.destroyed) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
this.destroyed = true;
|
|
1785
|
+
this.closed = true;
|
|
1786
|
+
this._open = false;
|
|
1787
|
+
for (const s of this._streams.values()) {
|
|
1788
|
+
if (!s.destroyed) {
|
|
1789
|
+
s.destroy(err);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
this._streams.clear();
|
|
1793
|
+
// Requests whose HEADERS never went on the wire are cancelled, not handed the
|
|
1794
|
+
// session's error: node distinguishes the two (internal/http2/core.js:1254-1260,
|
|
1795
|
+
// state.pendingStreams.forEach((stream) => stream.destroy(cancel))) so an
|
|
1796
|
+
// application can tell "my request was never sent" from "the connection broke
|
|
1797
|
+
// under it". Measured on a TLS handshake failure: node answers a queued request
|
|
1798
|
+
// with stream error ERR_HTTP2_STREAM_CANCEL before the session error, h2lite used
|
|
1799
|
+
// to leave it silent.
|
|
1800
|
+
if (this._queuedReq !== null && this._queuedReq.length > 0) {
|
|
1801
|
+
for (const r of this._queuedReq) {
|
|
1802
|
+
if (!r.stream.destroyed) {
|
|
1803
|
+
r.stream.destroy(cancelErr(err));
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
this._queuedReq = null;
|
|
1807
|
+
}
|
|
1808
|
+
for (const r of this._outboundQueue) {
|
|
1809
|
+
if (!r.stream.destroyed) {
|
|
1810
|
+
r.stream.destroy(cancelErr(err));
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
this._outboundQueue.length = 0;
|
|
1814
|
+
const socket = this.socket;
|
|
1815
|
+
this._discardOut();
|
|
1816
|
+
if (!socket.destroyed) {
|
|
1817
|
+
// Bytes already handed to the socket -- the GOAWAY a fatal inbound frame
|
|
1818
|
+
// leaves with (_fatal) -- are flushed by end() before the FIN; an outright
|
|
1819
|
+
// destroy() drops them whenever a write is still in flight. With nothing
|
|
1820
|
+
// queued the destroy is the immediate kill it always was.
|
|
1821
|
+
if (socket.writable && !socket.writableEnded && socket.writableLength > 0) {
|
|
1822
|
+
socket.end();
|
|
1823
|
+
} else {
|
|
1824
|
+
socket.destroy();
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
process.nextTick(() => {
|
|
1828
|
+
if (!this._closeEmitted) {
|
|
1829
|
+
this._emitClose(err);
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
ref() {
|
|
1835
|
+
this.socket.ref(); return this;
|
|
1836
|
+
}
|
|
1837
|
+
unref() {
|
|
1838
|
+
this.socket.unref(); return this;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
setTimeout(msecs, callback) {
|
|
1842
|
+
// node's session.setTimeout sets an idle timer; with no activity it emits
|
|
1843
|
+
// 'timeout' and, absent a listener, destroys the session. Kept faithful here.
|
|
1844
|
+
if (this._idleTimer) {
|
|
1845
|
+
clearTimeout(this._idleTimer);
|
|
1846
|
+
}
|
|
1847
|
+
this._idleCb = callback || null;
|
|
1848
|
+
if (msecs > 0 && !this.destroyed) {
|
|
1849
|
+
this._idleTimer = setTimeout(() => {
|
|
1850
|
+
this._idleTimer = null;
|
|
1851
|
+
this.emit('timeout');
|
|
1852
|
+
if (this._idleCb === null && !this.destroyed) {
|
|
1853
|
+
this.destroy();
|
|
1854
|
+
}
|
|
1855
|
+
}, msecs);
|
|
1856
|
+
this._idleTimer.unref();
|
|
1857
|
+
}
|
|
1858
|
+
return this;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
setNextStreamID(id) {
|
|
1862
|
+
this._nextSid = id; return this;
|
|
1863
|
+
}
|
|
1864
|
+
setLocalWindowSize(size) {
|
|
1865
|
+
// RFC 9113 6.9.2: the change rescales the peer's send windows retroactively, so the
|
|
1866
|
+
// window we maintain moves by the same delta rather than only applying to streams
|
|
1867
|
+
// opened from here on -- and _creditRecv()'s threshold is derived from it per call.
|
|
1868
|
+
this._connRecvWin += size - this.localSettings.initialWindowSize;
|
|
1869
|
+
this.localSettings.initialWindowSize = size;
|
|
1870
|
+
return this;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// ---- teardown ---------------------------------------------------------------
|
|
1874
|
+
// A client connection is keep-alive: finishing the last stream must NOT tear the
|
|
1875
|
+
// session down, or the next request() on the same session would find it destroyed.
|
|
1876
|
+
// Only an explicit close()/goaway (which sets _closeRequested), a peer GOAWAY that
|
|
1877
|
+
// left nothing live, or destroy() ends it -- then it ends once the outbound queue and
|
|
1878
|
+
// the open streams have both drained.
|
|
1879
|
+
_maybeClose() {
|
|
1880
|
+
if (this.destroyed || this._closeEmitted) {
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (this._closeRequested && this._streams.size === 0 && this._outboundQueue.length === 0
|
|
1884
|
+
&& this._outLen === 0) {
|
|
1885
|
+
this.destroy();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// _debt is drained at every point that ends a stream's life -- _dropStream (inbound
|
|
1890
|
+
// END_STREAM, RST, outbound reset, and the destroy path, which goes through
|
|
1891
|
+
// rstStream) -- so the list never grows across a long-lived connection and never
|
|
1892
|
+
// visits a closed stream. The mark is the guard: a stream that took no unsent credit
|
|
1893
|
+
// costs one property read here, and the scan only runs for the few that did.
|
|
1894
|
+
//
|
|
1895
|
+
// Dropping a stream that still owes stream-level credit is deliberate, not a leak: the
|
|
1896
|
+
// connection's share of those octets was booked into _connRecvUsed when they arrived
|
|
1897
|
+
// (as on the server, where _abandon() moves them), and the stream itself is closed, so
|
|
1898
|
+
// nobody can send another DATA on it that the missing WINDOW_UPDATE could unblock.
|
|
1899
|
+
_removeDebt(stream) {
|
|
1900
|
+
const debt = this._debt;
|
|
1901
|
+
for (let i = 0; i < debt.length; i++) {
|
|
1902
|
+
if (debt[i] === stream) {
|
|
1903
|
+
debt[i] = debt[debt.length - 1];
|
|
1904
|
+
debt.pop();
|
|
1905
|
+
stream._debtMarked = false;
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
_dropStream(sid) {
|
|
1912
|
+
const stream = this._streams.get(sid);
|
|
1913
|
+
if (stream !== undefined && stream._debtMarked) {
|
|
1914
|
+
this._removeDebt(stream);
|
|
1915
|
+
}
|
|
1916
|
+
this._streams.delete(sid);
|
|
1917
|
+
this._drainQueue();
|
|
1918
|
+
this._maybeClose();
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
_fatal(code, reason) {
|
|
1922
|
+
if (!this._open) {
|
|
1923
|
+
return false;
|
|
1924
|
+
}
|
|
1925
|
+
this._open = false;
|
|
1926
|
+
// node's client answers a fatal inbound frame with GOAWAY on the wire and
|
|
1927
|
+
// ERR_HTTP2_ERROR on the stream and on the session (measured,
|
|
1928
|
+
// tools/probe-client-pad-bad.js and tools/probe-client-hdr-pad.js).
|
|
1929
|
+
// destroy() drops the staged batch and resets the socket, so the batch --
|
|
1930
|
+
// the GOAWAY and anything staged behind it -- is handed to the socket here,
|
|
1931
|
+
// and destroy() leaves a queued batch alive (it ends the socket instead of
|
|
1932
|
+
// resetting it while bytes are still queued, so a write already in flight
|
|
1933
|
+
// drains first).
|
|
1934
|
+
this.goaway(code, undefined, reason);
|
|
1935
|
+
this._commit();
|
|
1936
|
+
const batch = this._outQ;
|
|
1937
|
+
this._outQ = [];
|
|
1938
|
+
this._outLen = 0;
|
|
1939
|
+
const socket = this.socket;
|
|
1940
|
+
if (batch.length !== 0 && socket.writable && !socket.destroyed) {
|
|
1941
|
+
if (batch.length === 1) {
|
|
1942
|
+
socket.write(batch[0]);
|
|
1943
|
+
} else {
|
|
1944
|
+
socket.cork();
|
|
1945
|
+
for (let i = 0; i < batch.length - 1; i++) {
|
|
1946
|
+
socket.write(batch[i]);
|
|
1947
|
+
}
|
|
1948
|
+
socket.write(batch[batch.length - 1]);
|
|
1949
|
+
socket.uncork();
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const err = httpErr('ERR_HTTP2_ERROR', reason);
|
|
1953
|
+
err.http2Code = code;
|
|
1954
|
+
this.destroy(err);
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
_onSocketError(err) {
|
|
1959
|
+
if (err && err.code === 'ECONNRESET' && this._goawayCode !== null) {
|
|
1960
|
+
return this.destroy();
|
|
1961
|
+
}
|
|
1962
|
+
this.destroy(err);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
_onSocketClose() {
|
|
1966
|
+
if (this._closeEmitted) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
this.destroyed = true;
|
|
1970
|
+
this._emitClose();
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
_emitClose(err) {
|
|
1974
|
+
if (this._closeEmitted) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
this._closeEmitted = true;
|
|
1978
|
+
if (err) {
|
|
1979
|
+
this.emit('error', err);
|
|
1980
|
+
}
|
|
1981
|
+
this.emit('close');
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// ---- read-only surface (node-compatible) ------------------------------------
|
|
1985
|
+
// The last GOAWAY this side received. Before any GOAWAY node reports the code 0
|
|
1986
|
+
// (NO_ERROR) and lastStreamID 0; we mirror that default rather than leaking null.
|
|
1987
|
+
get goawayCode() {
|
|
1988
|
+
return this._goawayCode === null ? 0 : this._goawayCode;
|
|
1989
|
+
}
|
|
1990
|
+
get goawayLastStreamID() {
|
|
1991
|
+
return this._goawayLastStreamID === null ? 0 : this._goawayLastStreamID;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// node's `state` is an nghttp2-internal snapshot. The fields an application can
|
|
1995
|
+
// legitimately read are the window sizes and the stream id bookkeeping, so those
|
|
1996
|
+
// are provided; the dynamic-table figures are reported live off the codecs.
|
|
1997
|
+
get state() {
|
|
1998
|
+
return {
|
|
1999
|
+
effectiveLocalWindowSize: this.localSettings.initialWindowSize,
|
|
2000
|
+
effectiveRecvDataLength: 0,
|
|
2001
|
+
nextStreamID: this._nextSid,
|
|
2002
|
+
localWindowSize: this.localSettings.initialWindowSize,
|
|
2003
|
+
lastProcStreamID: this._goawayLastStreamID === null ? 0 : this._goawayLastStreamID,
|
|
2004
|
+
remoteWindowSize: this._connSendWin,
|
|
2005
|
+
outboundQueueSize: this._outboundQueue.length,
|
|
2006
|
+
deflateDynamicTableSize: this._encoder.size,
|
|
2007
|
+
inflateDynamicTableSize: this._decoder.size
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Helpers ---------------------------------------------------------------------
|
|
2013
|
+
|
|
2014
|
+
// Pseudo-headers must precede regular fields (RFC 9113 8.1); the application may
|
|
2015
|
+
// have interleaved them, so re-order :method, :path, :scheme, :authority, :protocol
|
|
2016
|
+
// to the front and leave the rest in place.
|
|
2017
|
+
function orderPseudo(list) {
|
|
2018
|
+
const pseudo = [];
|
|
2019
|
+
const fields = [];
|
|
2020
|
+
for (let i = 0; i < list.length; i += 2) {
|
|
2021
|
+
(list[i].charCodeAt(0) === 58 ? pseudo : fields).push(list[i], list[i + 1]);
|
|
2022
|
+
}
|
|
2023
|
+
const byName = (n) => {
|
|
2024
|
+
for (let i = 0; i < pseudo.length; i += 2) {
|
|
2025
|
+
if (pseudo[i] === n) {
|
|
2026
|
+
return pseudo[i + 1];
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return undefined;
|
|
2030
|
+
};
|
|
2031
|
+
const out = [];
|
|
2032
|
+
for (const n of [':method', ':path', ':scheme', ':authority', ':protocol']) {
|
|
2033
|
+
const v = byName(n);
|
|
2034
|
+
if (v !== undefined) {
|
|
2035
|
+
out.push(n, v);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
for (let i = 0; i < pseudo.length; i += 2) {
|
|
2039
|
+
if (out.indexOf(pseudo[i]) === -1) {
|
|
2040
|
+
out.push(pseudo[i], pseudo[i + 1]);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
2044
|
+
out.push(fields[i], fields[i + 1]);
|
|
2045
|
+
}
|
|
2046
|
+
return out;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function httpErr(code, msg) {
|
|
2050
|
+
const err = new Error(msg);
|
|
2051
|
+
err.code = code;
|
|
2052
|
+
return err;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// The shape of node's ERR_HTTP2_STREAM_CANCEL (internal/errors.js:1335): the cause
|
|
2056
|
+
// rides along as .cause, and the message names it the way util did when node built
|
|
2057
|
+
// the string there. With no cause (a plain destroy()) the bare message is what node
|
|
2058
|
+
// emits too.
|
|
2059
|
+
function cancelErr(cause) {
|
|
2060
|
+
const err = httpErr('ERR_HTTP2_STREAM_CANCEL', 'The pending stream has been canceled'
|
|
2061
|
+
+ (cause ? ` (caused by: ${cause.message})` : ''));
|
|
2062
|
+
if (cause) {
|
|
2063
|
+
err.cause = cause;
|
|
2064
|
+
}
|
|
2065
|
+
return err;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
module.exports = { ClientHttp2Session };
|