quico 0.3.0 → 0.7.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/README.md +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/h2.js
ADDED
|
@@ -0,0 +1,1325 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { HpackEncoder, HpackDecoder } from './hpack.js';
|
|
16
|
+
import * as F from './h2_frames.js';
|
|
17
|
+
import {
|
|
18
|
+
Emitter, parseContentLength, validateFieldSection, parsePriority, formatPriority
|
|
19
|
+
} from './http_common.js';
|
|
20
|
+
import { mergePriority } from './utils.js';
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// H2Connection — HTTP/2 over one byte stream (RFC 9113).
|
|
25
|
+
//
|
|
26
|
+
// The same position in the stack as h3.js: it is handed ordered bytes and
|
|
27
|
+
// turns them into request/response events, knowing nothing about sockets.
|
|
28
|
+
// What it must do that h3.js does not is everything QUIC provides for free —
|
|
29
|
+
// streams, their state machine, and flow control in both directions. In
|
|
30
|
+
// HTTP/3 those are the transport's; here they are the protocol's own, which
|
|
31
|
+
// is most of the reason this file is four times the size of h3.js's framing.
|
|
32
|
+
//
|
|
33
|
+
// Events:
|
|
34
|
+
// 'headers' (streamId, headers, endStream)
|
|
35
|
+
// 'data' (streamId, chunk)
|
|
36
|
+
// 'end' (streamId)
|
|
37
|
+
// 'rst' (streamId, errorCode)
|
|
38
|
+
// 'settings' (settings)
|
|
39
|
+
// 'goaway' (lastStreamId, errorCode, debug)
|
|
40
|
+
// 'ping' (payload)
|
|
41
|
+
// 'send' (buffer) — bytes to put on the wire
|
|
42
|
+
// 'error' (err) — connection error; the connection is over
|
|
43
|
+
// 'drain' (streamId) — a stream's send window reopened
|
|
44
|
+
// 'stream_close' (streamId) — a stream that was open is closed (both
|
|
45
|
+
// ends, or reset); `activeStreams` counts the rest
|
|
46
|
+
// 'window_blocked' (streamId) — a stream's DATA waits for the peer to
|
|
47
|
+
// open a window (WINDOW_UPDATE)
|
|
48
|
+
// ============================================================
|
|
49
|
+
|
|
50
|
+
var STATE = {
|
|
51
|
+
IDLE: 'idle',
|
|
52
|
+
RESERVED_LOCAL: 'reserved_local',
|
|
53
|
+
RESERVED_REMOTE: 'reserved_remote',
|
|
54
|
+
OPEN: 'open',
|
|
55
|
+
HALF_CLOSED_LOCAL: 'half_closed_local',
|
|
56
|
+
HALF_CLOSED_REMOTE: 'half_closed_remote',
|
|
57
|
+
CLOSED: 'closed'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Pseudo-header sets and the connection-specific field list live in
|
|
61
|
+
// http_common.js: RFC 9114 §4.2 names exactly the same fields for HTTP/3, so
|
|
62
|
+
// h3.js will want them too rather than a second copy that can drift.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
function H2Connection(options) {
|
|
66
|
+
if (!(this instanceof H2Connection)) return new H2Connection(options);
|
|
67
|
+
options = options || {};
|
|
68
|
+
|
|
69
|
+
var isServer = options.isServer !== false;
|
|
70
|
+
// The server's counters (server_stats.js), when its server keeps them.
|
|
71
|
+
var stats = options.stats || null;
|
|
72
|
+
|
|
73
|
+
var ev = Emitter();
|
|
74
|
+
var on = ev.on, emit = ev.emit, off = ev.off;
|
|
75
|
+
|
|
76
|
+
// ---- Settings ----
|
|
77
|
+
var localSettings = {
|
|
78
|
+
HEADER_TABLE_SIZE: options.headerTableSize != null ? options.headerTableSize : 4096,
|
|
79
|
+
ENABLE_PUSH: 0, // we never accept push; see below
|
|
80
|
+
// Node's own HTTP/2 server advertises 2^32-1 here, i.e. no limit, and
|
|
81
|
+
// browsers are tuned against that. Advertising 100 is defensible but it is
|
|
82
|
+
// a much lower ceiling than anything a browser expects to meet, and a
|
|
83
|
+
// client that is counting streams against it may decline to open one it
|
|
84
|
+
// would otherwise have opened. Matching Node removes that variable; a
|
|
85
|
+
// caller that wants a real limit can still set one.
|
|
86
|
+
// 100 is what browsers, nghttp2 and most public servers settle on, and it
|
|
87
|
+
// is what h2spec §5.1.2 exercises: a server advertising 2^32-1 is saying
|
|
88
|
+
// there is no limit, and the conformance suite duly tries to open that
|
|
89
|
+
// many streams. Node's own server does advertise 2^32-1, but Node is not
|
|
90
|
+
// trying to be a public-facing server by default.
|
|
91
|
+
//
|
|
92
|
+
// Worth being deliberate about rather than inherited: this is the only
|
|
93
|
+
// backstop against a peer opening unbounded streams, and each stream costs
|
|
94
|
+
// memory that the flow-control window does not bound. A caller that wants
|
|
95
|
+
// a different ceiling can now set one — that option used to be dropped
|
|
96
|
+
// before it reached this layer.
|
|
97
|
+
MAX_CONCURRENT_STREAMS: options.maxConcurrentStreams != null ? options.maxConcurrentStreams : 100,
|
|
98
|
+
INITIAL_WINDOW_SIZE: options.initialWindowSize != null ? options.initialWindowSize : 65535,
|
|
99
|
+
MAX_FRAME_SIZE: options.maxFrameSize != null ? options.maxFrameSize : 16384,
|
|
100
|
+
MAX_HEADER_LIST_SIZE: options.maxHeaderListSize != null ? options.maxHeaderListSize : 128 * 1024,
|
|
101
|
+
// RFC 8441. A server advertises it so clients may run WebSocket over this
|
|
102
|
+
// connection; a client never does (it has nothing to offer a server), so
|
|
103
|
+
// it is only included below when isServer.
|
|
104
|
+
ENABLE_CONNECT_PROTOCOL: (isServer && options.enableConnectProtocol !== false) ? 1 : 0
|
|
105
|
+
};
|
|
106
|
+
// RFC 9218 §2.1: the server reads RFC 9218's signals, not RFC 7540's tree.
|
|
107
|
+
// A server's only: a client's SETTINGS are left as they were (a client
|
|
108
|
+
// profile may be matching a browser's exactly).
|
|
109
|
+
if (isServer) localSettings.NO_RFC7540_PRIORITIES = 1;
|
|
110
|
+
|
|
111
|
+
var remoteSettings = {
|
|
112
|
+
HEADER_TABLE_SIZE: 4096,
|
|
113
|
+
ENABLE_PUSH: 1,
|
|
114
|
+
MAX_CONCURRENT_STREAMS: Infinity,
|
|
115
|
+
INITIAL_WINDOW_SIZE: F.DEFAULT_WINDOW_SIZE,
|
|
116
|
+
MAX_FRAME_SIZE: F.DEFAULT_MAX_FRAME_SIZE,
|
|
117
|
+
MAX_HEADER_LIST_SIZE: Infinity,
|
|
118
|
+
ENABLE_CONNECT_PROTOCOL: 0
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
var encoder = new HpackEncoder({ maxTableSize: remoteSettings.HEADER_TABLE_SIZE });
|
|
122
|
+
var decoder = new HpackDecoder({
|
|
123
|
+
maxTableSize: localSettings.HEADER_TABLE_SIZE,
|
|
124
|
+
maxHeaderListSize: localSettings.MAX_HEADER_LIST_SIZE
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---- Connection state ----
|
|
128
|
+
var buf = Buffer.alloc(0);
|
|
129
|
+
var prefaceReceived = !isServer; // a server waits for it, a client sends it
|
|
130
|
+
var streams = {};
|
|
131
|
+
var lastRemoteStreamId = 0;
|
|
132
|
+
var nextLocalStreamId = isServer ? 2 : 1;
|
|
133
|
+
var goawaySent = false;
|
|
134
|
+
var goawayLastStreamId = 0; // the last-stream-id our GOAWAY carried
|
|
135
|
+
// Streams counted against MAX_CONCURRENT_STREAMS, kept as a count: it was
|
|
136
|
+
// recomputed by walking every stream record on every new HEADERS.
|
|
137
|
+
var activeCount = 0;
|
|
138
|
+
// Whether the transport below can take more right now (the socket's buffer
|
|
139
|
+
// is not over its mark). DATA waits while it cannot; see flushStream.
|
|
140
|
+
var isWriteBlocked = typeof options.isWriteBlocked === 'function' ? options.isWriteBlocked : function () { return false; };
|
|
141
|
+
var goawayReceived = false;
|
|
142
|
+
var closed = false;
|
|
143
|
+
var settingsAcked = false;
|
|
144
|
+
|
|
145
|
+
// Connection-level flow control. Two independent windows per direction: one
|
|
146
|
+
// for the connection, one per stream, and a DATA frame consumes from BOTH.
|
|
147
|
+
//
|
|
148
|
+
// Both start at 65535 and NOT at INITIAL_WINDOW_SIZE. RFC 9113 §6.9.2 is
|
|
149
|
+
// explicit that SETTINGS_INITIAL_WINDOW_SIZE governs stream windows only and
|
|
150
|
+
// does not change the connection window, which moves solely by WINDOW_UPDATE
|
|
151
|
+
// on stream 0. Seeding it from the setting instead makes a server that
|
|
152
|
+
// advertises a small per-stream window unable to receive anything at all —
|
|
153
|
+
// the connection window runs out first and every request dies with a
|
|
154
|
+
// connection-level FLOW_CONTROL_ERROR that looks like the peer's fault.
|
|
155
|
+
var localWindow = F.DEFAULT_WINDOW_SIZE; // what we may receive
|
|
156
|
+
var remoteWindow = F.DEFAULT_WINDOW_SIZE; // what we may send
|
|
157
|
+
|
|
158
|
+
// What we widen the connection receive window to in our first flight; see
|
|
159
|
+
// sendPreface. Configurable because a proxy fanning out thousands of
|
|
160
|
+
// connections may not want 16MB of credit outstanding on each.
|
|
161
|
+
var CONNECTION_WINDOW = options.connectionWindowSize != null
|
|
162
|
+
? options.connectionWindowSize
|
|
163
|
+
: 16 * 1024 * 1024;
|
|
164
|
+
|
|
165
|
+
// A header block may be split across HEADERS + CONTINUATION, and §6.10
|
|
166
|
+
// forbids ANY other frame in between — including one for a different stream.
|
|
167
|
+
// Tracking it as connection state rather than per-stream is the point: an
|
|
168
|
+
// interleaved frame is a connection error precisely because the HPACK
|
|
169
|
+
// decoder is connection-wide and would be fed the fragments out of order.
|
|
170
|
+
var continuationStream = 0;
|
|
171
|
+
var continuationFragments = [];
|
|
172
|
+
var continuationFrame = null;
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
function connectionError(message, code) {
|
|
176
|
+
if (closed) return;
|
|
177
|
+
var err = message instanceof Error ? message
|
|
178
|
+
: F.h2Error(message, code === undefined ? F.ERROR.PROTOCOL_ERROR : code);
|
|
179
|
+
// A protocol error says what was wrong; anything else is ours (an
|
|
180
|
+
// exception from a listener, the application's request handler among
|
|
181
|
+
// them) and is INTERNAL_ERROR, with no detail. Its message went out as the
|
|
182
|
+
// GOAWAY's debug data — internal error text handed to the client — under
|
|
183
|
+
// PROTOCOL_ERROR, which blamed the peer.
|
|
184
|
+
if (err.h2Code) sendGoaway(err.h2Code, err.message);
|
|
185
|
+
else sendGoaway(F.ERROR.INTERNAL_ERROR, 'internal error');
|
|
186
|
+
closed = true;
|
|
187
|
+
emit('error', err);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A peer that keeps sending on a stream we have reset gets a reset back each
|
|
191
|
+
// time (RFC 9113 §5.1), but only so many: answering an endless stream of
|
|
192
|
+
// frames with an endless stream of RST_STREAMs is a reflection amplifier
|
|
193
|
+
// pointed at ourselves. Past the limit the connection goes.
|
|
194
|
+
var MAX_RESETS_PER_STREAM = 8;
|
|
195
|
+
|
|
196
|
+
function streamError(streamId, code, message) {
|
|
197
|
+
var s = streams[streamId];
|
|
198
|
+
|
|
199
|
+
// Do NOT bail out because the stream is already closed. That was the bug
|
|
200
|
+
// h2spec §5.1.8/9/11/12 found: a DATA or HEADERS frame arriving after we
|
|
201
|
+
// reset a stream MUST be answered with STREAM_CLOSED, and returning here
|
|
202
|
+
// meant answering with nothing at all. The peer sat waiting for a reply
|
|
203
|
+
// that never came — every one of those four tests failed as "Timeout"
|
|
204
|
+
// rather than as a wrong error code, which is what silence looks like from
|
|
205
|
+
// the other end.
|
|
206
|
+
if (s) {
|
|
207
|
+
s.resetCount = (s.resetCount || 0) + 1;
|
|
208
|
+
if (s.resetCount > MAX_RESETS_PER_STREAM) {
|
|
209
|
+
connectionError('too many resets on stream ' + streamId, F.ERROR.ENHANCE_YOUR_CALM);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// A stream the peer reset stays so: it is the peer's later frames that
|
|
213
|
+
// this answers.
|
|
214
|
+
if (s.closedBy !== 'rst_received') s.closedBy = 'rst_sent';
|
|
215
|
+
markClosed(s);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
write(F.buildRstStream(streamId, code));
|
|
219
|
+
if (stats) stats.code(stats.streamResets.h2.sent, code);
|
|
220
|
+
emit('rst', streamId, code);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Has this stream id never been used, as opposed to used and finished?
|
|
225
|
+
*
|
|
226
|
+
* RFC 9113 §5.1 treats the two completely differently: a frame for an IDLE
|
|
227
|
+
* stream is a connection error, while the same frame for a CLOSED one is
|
|
228
|
+
* ordinary — the peer had it in flight when we closed the stream. Telling
|
|
229
|
+
* them apart is possible because ids only ever increase: anything above the
|
|
230
|
+
* highest we have seen was never opened, anything at or below it was.
|
|
231
|
+
*/
|
|
232
|
+
function isIdleStreamId(id) {
|
|
233
|
+
if (streams[id]) return streams[id].state === STATE.IDLE;
|
|
234
|
+
var peerInitiated = isServer ? (id % 2 === 1) : (id % 2 === 0);
|
|
235
|
+
if (peerInitiated) return id > lastRemoteStreamId;
|
|
236
|
+
return id >= nextLocalStreamId;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function write(buffer) {
|
|
240
|
+
if (buffer && buffer.length) emit('send', buffer);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
// ============================================================
|
|
245
|
+
// Streams
|
|
246
|
+
// ============================================================
|
|
247
|
+
|
|
248
|
+
function getStream(id, create) {
|
|
249
|
+
var s = streams[id];
|
|
250
|
+
if (s) return s;
|
|
251
|
+
if (!create) return null;
|
|
252
|
+
s = {
|
|
253
|
+
id: id,
|
|
254
|
+
state: STATE.IDLE,
|
|
255
|
+
// Per-stream windows, seeded from each side's INITIAL_WINDOW_SIZE.
|
|
256
|
+
localWindow: localSettings.INITIAL_WINDOW_SIZE,
|
|
257
|
+
remoteWindow: remoteSettings.INITIAL_WINDOW_SIZE,
|
|
258
|
+
headersReceived: false,
|
|
259
|
+
trailersReceived: false,
|
|
260
|
+
endStreamReceived: false,
|
|
261
|
+
endStreamSent: false,
|
|
262
|
+
pendingData: [], // queued while the send window is closed
|
|
263
|
+
contentLength: null,
|
|
264
|
+
bytesReceived: 0
|
|
265
|
+
};
|
|
266
|
+
streams[id] = s;
|
|
267
|
+
return s;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* `how` records WHY the stream closed, because RFC 9113 §5.1 answers a later
|
|
272
|
+
* frame differently depending on it:
|
|
273
|
+
*
|
|
274
|
+
* 'rst_sent' — we reset it. What the peer sent before our RST_STREAM
|
|
275
|
+
* reached it is still arriving — a response's DATA up to
|
|
276
|
+
* a whole stream window — and §5.1 says to ignore it:
|
|
277
|
+
* "An endpoint MUST ignore frames that it receives on
|
|
278
|
+
* closed streams after it has sent a RST_STREAM frame"
|
|
279
|
+
* (counted in the connection's flow control all the
|
|
280
|
+
* same, §6.9). Each was answered with another RST_STREAM,
|
|
281
|
+
* and past MAX_RESETS_PER_STREAM the connection went
|
|
282
|
+
* (ENHANCE_YOUR_CALM): a download the client cancelled
|
|
283
|
+
* took the client's whole connection down with it once
|
|
284
|
+
* more than 8 frames were in flight.
|
|
285
|
+
* 'rst_received' — the peer reset it, so it knows the stream is gone: a
|
|
286
|
+
* frame after that is answered STREAM_CLOSED, a stream
|
|
287
|
+
* error (h2spec §5.1.9–11).
|
|
288
|
+
* 'end' — END_STREAM was seen. Both sides agreed the stream was over, so a
|
|
289
|
+
* further frame means the peer's state machine has diverged from
|
|
290
|
+
* ours, and §5.1 makes that a CONNECTION error of type
|
|
291
|
+
* STREAM_CLOSED.
|
|
292
|
+
*
|
|
293
|
+
* The two look identical from the outside — the stream is closed either way
|
|
294
|
+
* — which is why h2spec §5.1.12 was the last test standing: we answered a
|
|
295
|
+
* post-END_STREAM HEADERS with a polite RST_STREAM where the spec requires
|
|
296
|
+
* the connection to end.
|
|
297
|
+
*/
|
|
298
|
+
function closeStream(id, how) {
|
|
299
|
+
var s = streams[id];
|
|
300
|
+
if (!s) return;
|
|
301
|
+
if (how) s.closedBy = how;
|
|
302
|
+
markClosed(s);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function markOpen(s) {
|
|
306
|
+
s.state = STATE.OPEN;
|
|
307
|
+
if (!s.active) { s.active = true; activeCount++; }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Closed streams are kept briefly rather than deleted: frames for a
|
|
311
|
+
// just-closed stream are ordinary (the peer had them in flight when we
|
|
312
|
+
// closed it) and must be ignored, not treated as frames for an idle stream
|
|
313
|
+
// — which would be a connection error. Kept in the order they closed, and
|
|
314
|
+
// let go after 30 s or beyond the most recent MAX_RETAINED_CLOSED. The sweep
|
|
315
|
+
// walked every record on every close once there were 1000 of them, and kept
|
|
316
|
+
// everything closed in the last 30 s however many that was — a peer that
|
|
317
|
+
// opened and reset streams as fast as it could grew the map without bound
|
|
318
|
+
// and made each close slower than the last.
|
|
319
|
+
var MAX_RETAINED_CLOSED = 4096;
|
|
320
|
+
var closedOrder = [];
|
|
321
|
+
function markClosed(s) {
|
|
322
|
+
s.state = STATE.CLOSED;
|
|
323
|
+
s.pendingData = [];
|
|
324
|
+
waiting.delete(s);
|
|
325
|
+
if (s.active) { s.active = false; activeCount--; emit('stream_close', s.id); }
|
|
326
|
+
if (s.closedAt) return;
|
|
327
|
+
s.closedAt = Date.now();
|
|
328
|
+
closedOrder.push(s.id);
|
|
329
|
+
var cutoff = s.closedAt - 30000;
|
|
330
|
+
while (closedOrder.length > 0) {
|
|
331
|
+
var old = streams[closedOrder[0]];
|
|
332
|
+
if (old && old.state === STATE.CLOSED && closedOrder.length <= MAX_RETAINED_CLOSED && old.closedAt >= cutoff) break;
|
|
333
|
+
if (old && old.state === STATE.CLOSED) delete streams[closedOrder[0]];
|
|
334
|
+
closedOrder.shift();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
// ============================================================
|
|
340
|
+
// Receiving
|
|
341
|
+
// ============================================================
|
|
342
|
+
|
|
343
|
+
function execute(chunk) {
|
|
344
|
+
if (closed) return;
|
|
345
|
+
if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
|
|
346
|
+
buf = buf.length === 0 ? chunk : Buffer.concat([buf, chunk]);
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
if (!prefaceReceived) {
|
|
350
|
+
if (buf.length < F.PREFACE.length) return;
|
|
351
|
+
if (!buf.subarray(0, F.PREFACE.length).equals(F.PREFACE)) {
|
|
352
|
+
// Almost always an HTTP/1.1 client that reached an h2-only port. The
|
|
353
|
+
// preface exists so this fails here instead of being parsed as a
|
|
354
|
+
// request line.
|
|
355
|
+
throw F.h2Error('invalid connection preface', F.ERROR.PROTOCOL_ERROR);
|
|
356
|
+
}
|
|
357
|
+
buf = buf.subarray(F.PREFACE.length);
|
|
358
|
+
prefaceReceived = true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Every frame is handled once. A STREAM error — thrown by the parser
|
|
362
|
+
// for a frame it refused, or by a handler — resets that stream and the
|
|
363
|
+
// loop goes on from the next frame. It used to end the loop with the
|
|
364
|
+
// buffer not advanced: every frame of the chunk already handled was
|
|
365
|
+
// handled again on the next read (a HEADERS block decoded twice, the
|
|
366
|
+
// HPACK table desynchronised), and the buffer only grew.
|
|
367
|
+
var offset = 0;
|
|
368
|
+
for (;;) {
|
|
369
|
+
var frame;
|
|
370
|
+
try {
|
|
371
|
+
frame = F.readFrame(buf, offset, localSettings.MAX_FRAME_SIZE);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (!err.streamId || !err.consumed) throw err;
|
|
374
|
+
offset += err.consumed;
|
|
375
|
+
streamError(err.streamId, err.h2Code, err.message);
|
|
376
|
+
if (closed) return;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!frame) break;
|
|
380
|
+
offset += frame.consumed;
|
|
381
|
+
try {
|
|
382
|
+
handleFrame(frame);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (!err.streamId) throw err;
|
|
385
|
+
streamError(err.streamId, err.h2Code, err.message);
|
|
386
|
+
}
|
|
387
|
+
if (closed) return;
|
|
388
|
+
}
|
|
389
|
+
buf = offset > 0 ? buf.subarray(offset) : buf;
|
|
390
|
+
|
|
391
|
+
} catch (err) {
|
|
392
|
+
connectionError(err);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
function handleFrame(frame) {
|
|
398
|
+
// §6.10 again: while a header block is open, the ONLY frame that may
|
|
399
|
+
// arrive is a CONTINUATION for the same stream.
|
|
400
|
+
if (continuationStream !== 0) {
|
|
401
|
+
if (frame.type !== F.TYPE.CONTINUATION || frame.streamId !== continuationStream) {
|
|
402
|
+
throw F.h2Error('frame ' + frame.typeName + ' interleaved into a header block',
|
|
403
|
+
F.ERROR.PROTOCOL_ERROR);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
switch (frame.type) {
|
|
408
|
+
case F.TYPE.SETTINGS: return onSettings(frame);
|
|
409
|
+
case F.TYPE.HEADERS: return onHeaders(frame);
|
|
410
|
+
case F.TYPE.CONTINUATION: return onContinuation(frame);
|
|
411
|
+
case F.TYPE.DATA: return onData(frame);
|
|
412
|
+
case F.TYPE.RST_STREAM: return onRstStream(frame);
|
|
413
|
+
case F.TYPE.WINDOW_UPDATE: return onWindowUpdate(frame);
|
|
414
|
+
case F.TYPE.PING: return onPing(frame);
|
|
415
|
+
case F.TYPE.GOAWAY: return onGoaway(frame);
|
|
416
|
+
case F.TYPE.PRIORITY: return; // parsed and ignored; deprecated by RFC 9113
|
|
417
|
+
case F.TYPE.PRIORITY_UPDATE: return onPriorityUpdate(frame);
|
|
418
|
+
case F.TYPE.PUSH_PROMISE:
|
|
419
|
+
// We advertise ENABLE_PUSH 0, so receiving one is a protocol error
|
|
420
|
+
// (§6.6). A server receiving PUSH_PROMISE is always an error.
|
|
421
|
+
throw F.h2Error('PUSH_PROMISE received although push is disabled',
|
|
422
|
+
F.ERROR.PROTOCOL_ERROR);
|
|
423
|
+
default:
|
|
424
|
+
return; // unknown type: ignore (§4.1)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
function onSettings(frame) {
|
|
430
|
+
if (frame.ack) { settingsAcked = true; return; }
|
|
431
|
+
|
|
432
|
+
for (var name in frame.settings) {
|
|
433
|
+
var value = frame.settings[name];
|
|
434
|
+
|
|
435
|
+
if (name === 'INITIAL_WINDOW_SIZE') {
|
|
436
|
+
// §6.9.2: changing this retroactively adjusts the send window of every
|
|
437
|
+
// OPEN stream by the difference. Not applying the delta is a slow,
|
|
438
|
+
// baffling bug — streams stall or over-send long after the SETTINGS
|
|
439
|
+
// frame that caused it.
|
|
440
|
+
var delta = value - remoteSettings.INITIAL_WINDOW_SIZE;
|
|
441
|
+
for (var id in streams) {
|
|
442
|
+
var s = streams[id];
|
|
443
|
+
if (s.state === STATE.CLOSED) continue;
|
|
444
|
+
s.remoteWindow += delta;
|
|
445
|
+
if (s.remoteWindow > F.MAX_WINDOW) {
|
|
446
|
+
throw F.h2Error('INITIAL_WINDOW_SIZE change overflows a stream window',
|
|
447
|
+
F.ERROR.FLOW_CONTROL_ERROR);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (name === 'ENABLE_CONNECT_PROTOCOL' &&
|
|
453
|
+
remoteSettings.ENABLE_CONNECT_PROTOCOL === 1 && value === 0) {
|
|
454
|
+
// §3: "a sender MUST NOT send a value of 0 after previously sending a
|
|
455
|
+
// value of 1". A stream may already be running extended CONNECT, and
|
|
456
|
+
// withdrawing permission would leave it in a state neither side can
|
|
457
|
+
// describe.
|
|
458
|
+
throw F.h2Error('SETTINGS_ENABLE_CONNECT_PROTOCOL cannot be withdrawn',
|
|
459
|
+
F.ERROR.PROTOCOL_ERROR);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
remoteSettings[name] = value;
|
|
463
|
+
|
|
464
|
+
if (name === 'HEADER_TABLE_SIZE') encoder.setMaxTableSize(value);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// The ACK must go out before anything that relies on the new settings.
|
|
468
|
+
write(F.buildSettings(null, true));
|
|
469
|
+
// Both forms: the object for consumers that want a value by name, and the
|
|
470
|
+
// ordered list for fingerprinting, which needs the sequence as sent.
|
|
471
|
+
emit('settings', frame.settings, frame.settingsList || []);
|
|
472
|
+
|
|
473
|
+
// Flush anything that was waiting on a window this SETTINGS may have opened.
|
|
474
|
+
flushAll();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
function onHeaders(frame) {
|
|
479
|
+
var id = frame.streamId;
|
|
480
|
+
|
|
481
|
+
if (isServer) {
|
|
482
|
+
// §5.1.1: client-initiated streams are odd, and ids must strictly
|
|
483
|
+
// increase. A reused or lower id would let a peer resurrect a closed
|
|
484
|
+
// stream, which is how request state gets confused between them.
|
|
485
|
+
if (id % 2 === 0) {
|
|
486
|
+
throw F.h2Error('client-initiated stream with an even id', F.ERROR.PROTOCOL_ERROR);
|
|
487
|
+
}
|
|
488
|
+
if (id <= lastRemoteStreamId && !streams[id]) {
|
|
489
|
+
throw F.h2Error('stream id ' + id + ' is not greater than the previous ' +
|
|
490
|
+
lastRemoteStreamId, F.ERROR.PROTOCOL_ERROR);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// A closed stream that has already been reaped leaves no record. Without
|
|
495
|
+
// one we cannot know whether it ended by RST_STREAM or by END_STREAM, and
|
|
496
|
+
// the stricter reading is the safe one: a peer opening a stream id it has
|
|
497
|
+
// already used is desynchronised whichever way it closed.
|
|
498
|
+
if (!streams[id] && !isIdleStreamId(id)) {
|
|
499
|
+
throw F.h2Error('HEADERS on a closed stream', F.ERROR.STREAM_CLOSED);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var streamIsNew = !streams[id];
|
|
503
|
+
var s = getStream(id, true);
|
|
504
|
+
|
|
505
|
+
// Every header block is decoded, whatever becomes of its stream — refused,
|
|
506
|
+
// reset, or after our GOAWAY. The HPACK table is shared by the connection
|
|
507
|
+
// and the peer's encoder has already applied this block to its copy; a
|
|
508
|
+
// block skipped here leaves the two tables apart, and every later block on
|
|
509
|
+
// every stream decodes wrong (RFC 9113 §4.3). A refused stream had its
|
|
510
|
+
// block skipped, and, when it lacked END_HEADERS, its CONTINUATION then
|
|
511
|
+
// found no open block and took the connection down. startHeaderBlock on a
|
|
512
|
+
// stream already closed decodes and drops (finishHeaderBlock).
|
|
513
|
+
|
|
514
|
+
// A new stream, announced before its request is processed — the point at
|
|
515
|
+
// which a rate limiter can refuse it: refused here, it costs the block's
|
|
516
|
+
// decode and never becomes a request. Guarded: no listener, no cost.
|
|
517
|
+
if (streamIsNew && isServer && ev.listenerCount('stream_open')) {
|
|
518
|
+
emit('stream_open', id);
|
|
519
|
+
if (s.state === STATE.CLOSED) {
|
|
520
|
+
// Refused by the listener (sendRstStream), which has already sent the
|
|
521
|
+
// RST; the id is used all the same.
|
|
522
|
+
lastRemoteStreamId = Math.max(lastRemoteStreamId, id);
|
|
523
|
+
startHeaderBlock(frame, id);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (s.state === STATE.CLOSED) {
|
|
529
|
+
if (s.closedBy === 'rst_sent') {
|
|
530
|
+
// Reset by us: in flight before our RST_STREAM — ignored, its block
|
|
531
|
+
// decoded all the same (HPACK's table is the connection's).
|
|
532
|
+
startHeaderBlock(frame, id);
|
|
533
|
+
} else if (s.closedBy === 'rst_received') {
|
|
534
|
+
streamError(id, F.ERROR.STREAM_CLOSED, 'HEADERS after RST_STREAM');
|
|
535
|
+
startHeaderBlock(frame, id);
|
|
536
|
+
} else {
|
|
537
|
+
throw F.h2Error('HEADERS after END_STREAM', F.ERROR.STREAM_CLOSED);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (s.state === STATE.HALF_CLOSED_REMOTE) {
|
|
542
|
+
streamError(id, F.ERROR.STREAM_CLOSED, 'HEADERS after END_STREAM');
|
|
543
|
+
startHeaderBlock(frame, id);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (s.state === STATE.IDLE) {
|
|
548
|
+
// Refused streams use their id too: ids only increase (§5.1.1), and a
|
|
549
|
+
// refusal left lastRemoteStreamId behind, so a lower unused id was
|
|
550
|
+
// still accepted afterwards.
|
|
551
|
+
if (isServer) lastRemoteStreamId = id;
|
|
552
|
+
|
|
553
|
+
// After our GOAWAY, a stream above its last-stream-id is not processed
|
|
554
|
+
// (§6.8). It was: goaway() — which a rate limiter calls to stop a peer —
|
|
555
|
+
// did not stop new requests at all. REFUSED_STREAM tells the client it
|
|
556
|
+
// may retry the request on a new connection.
|
|
557
|
+
if (isServer && goawaySent && id > goawayLastStreamId) {
|
|
558
|
+
streamError(id, F.ERROR.REFUSED_STREAM, 'received after GOAWAY');
|
|
559
|
+
startHeaderBlock(frame, id);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (isServer && activeCount >= localSettings.MAX_CONCURRENT_STREAMS) {
|
|
563
|
+
// §5.1.2: refusing is a stream error, and REFUSED_STREAM specifically
|
|
564
|
+
// tells the peer the request was never processed, so it is safe to
|
|
565
|
+
// retry elsewhere.
|
|
566
|
+
streamError(id, F.ERROR.REFUSED_STREAM, 'too many concurrent streams');
|
|
567
|
+
startHeaderBlock(frame, id);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
markOpen(s);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
startHeaderBlock(frame, id);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// A header block (HEADERS + CONTINUATION) is bounded while it accumulates,
|
|
577
|
+
// before any of it can be decoded: in bytes, and in frames. Nothing bounded
|
|
578
|
+
// it — fragments were collected until END_HEADERS, however many and however
|
|
579
|
+
// large, including empty ones (the CONTINUATION flood of 2024). The byte
|
|
580
|
+
// bound leaves room for the decoded list's own limit (MAX_HEADER_LIST_SIZE)
|
|
581
|
+
// even through HPACK's worst-case expansion; the frame bound is nghttp2's,
|
|
582
|
+
// and so node's. Past either, the block cannot be decoded and the HPACK
|
|
583
|
+
// state is lost with it: a connection error.
|
|
584
|
+
var MAX_CONTINUATION_FRAMES = 8;
|
|
585
|
+
var continuationBytes = 0;
|
|
586
|
+
var continuationCount = 0;
|
|
587
|
+
function headerBlockByteLimit() {
|
|
588
|
+
return Math.max(64 * 1024, 4 * localSettings.MAX_HEADER_LIST_SIZE);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function onContinuation(frame) {
|
|
592
|
+
if (continuationStream === 0) {
|
|
593
|
+
throw F.h2Error('CONTINUATION with no open header block', F.ERROR.PROTOCOL_ERROR);
|
|
594
|
+
}
|
|
595
|
+
continuationBytes += frame.headerBlock.length;
|
|
596
|
+
if (++continuationCount > MAX_CONTINUATION_FRAMES) {
|
|
597
|
+
throw F.h2Error('header block split across too many CONTINUATION frames', F.ERROR.ENHANCE_YOUR_CALM);
|
|
598
|
+
}
|
|
599
|
+
if (continuationBytes > headerBlockByteLimit()) {
|
|
600
|
+
throw F.h2Error('header block too large', F.ERROR.ENHANCE_YOUR_CALM);
|
|
601
|
+
}
|
|
602
|
+
continuationFragments.push(frame.headerBlock);
|
|
603
|
+
if (frame.endHeaders) finishHeaderBlock();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// A header block left open — HEADERS without END_HEADERS — holds the whole
|
|
607
|
+
// connection: until its CONTINUATIONs end it, no other frame may arrive
|
|
608
|
+
// (§6.10). 'header_block_open' and 'header_block_closed' bracket that time,
|
|
609
|
+
// for whoever bounds it (h2_server's headersTimeout); refused and reset
|
|
610
|
+
// streams' blocks included, since they hold the connection just the same.
|
|
611
|
+
var headerBlockOpen = false;
|
|
612
|
+
|
|
613
|
+
function startHeaderBlock(frame, id) {
|
|
614
|
+
continuationStream = id;
|
|
615
|
+
continuationFragments = [frame.headerBlock];
|
|
616
|
+
continuationFrame = frame;
|
|
617
|
+
continuationBytes = frame.headerBlock.length;
|
|
618
|
+
continuationCount = 0;
|
|
619
|
+
if (frame.endHeaders) { finishHeaderBlock(); return; }
|
|
620
|
+
headerBlockOpen = true;
|
|
621
|
+
emit('header_block_open', id);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function finishHeaderBlock() {
|
|
625
|
+
var id = continuationStream;
|
|
626
|
+
var frame = continuationFrame;
|
|
627
|
+
var block = Buffer.concat(continuationFragments);
|
|
628
|
+
|
|
629
|
+
continuationStream = 0;
|
|
630
|
+
continuationFragments = [];
|
|
631
|
+
continuationFrame = null;
|
|
632
|
+
if (headerBlockOpen) { headerBlockOpen = false; emit('header_block_closed', id); }
|
|
633
|
+
|
|
634
|
+
var list;
|
|
635
|
+
try {
|
|
636
|
+
list = decoder.decode(block);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
// HPACK failures are ALWAYS connection errors: the table is shared, so a
|
|
639
|
+
// block we could not decode leaves our table and the peer's permanently
|
|
640
|
+
// out of step, and every later block on every stream would be wrong.
|
|
641
|
+
throw F.h2Error(err.message, F.ERROR.COMPRESSION_ERROR);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
var s = streams[id];
|
|
645
|
+
if (!s || s.state === STATE.CLOSED) return; // reset while the block arrived
|
|
646
|
+
|
|
647
|
+
var headers;
|
|
648
|
+
try {
|
|
649
|
+
headers = validateHeaderList(list, s);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
streamError(id, err.h2Code || F.ERROR.PROTOCOL_ERROR, err.message);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (s.headersReceived) {
|
|
656
|
+
// A second header block on the same stream is trailers, and trailers
|
|
657
|
+
// must end the stream (§8.1).
|
|
658
|
+
if (!frame.endStream) {
|
|
659
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, 'trailers without END_STREAM');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// A body that content-length promised and the trailers ended short
|
|
663
|
+
// (§8.1.1) — checked on DATA's END_STREAM, not here.
|
|
664
|
+
if (isServer && s.contentLength !== null && s.bytesReceived !== s.contentLength) {
|
|
665
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, 'body shorter than content-length');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
s.trailersReceived = true;
|
|
669
|
+
emit('trailers', id, headers);
|
|
670
|
+
} else if (!isServer && /^1\d\d$/.test(String(headers[':status']))) {
|
|
671
|
+
// A 1xx response (103 Early Hints) is not the response (§8.1): the
|
|
672
|
+
// final head follows on the same stream. Counted as the head, the
|
|
673
|
+
// real one after it was taken for trailers — with a :status in it,
|
|
674
|
+
// malformed — and the stream was reset: a response with Early Hints
|
|
675
|
+
// never arrived. It may not end the stream.
|
|
676
|
+
if (frame.endStream) {
|
|
677
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, 'informational response with END_STREAM');
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
emit('headers', id, headers, false, null);
|
|
681
|
+
return;
|
|
682
|
+
} else {
|
|
683
|
+
s.headersReceived = true;
|
|
684
|
+
try {
|
|
685
|
+
var n = parseContentLength(headers['content-length']);
|
|
686
|
+
if (n !== null) s.contentLength = n;
|
|
687
|
+
} catch (e) {
|
|
688
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, e.message);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// A request that promises a body and ends with its headers (§8.1.1).
|
|
692
|
+
// Requests only: a response to HEAD, or a 304, carries a length and no body.
|
|
693
|
+
if (isServer && frame.endStream && s.contentLength) {
|
|
694
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, 'content-length ' + s.contentLength + ' but no body');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (isServer) applyRequestPriority(s, headers);
|
|
698
|
+
// An extended CONNECT stream is a bidirectional byte channel, not a
|
|
699
|
+
// request/response pair: END_STREAM on the request would close the
|
|
700
|
+
// client's half before the protocol on top has said anything.
|
|
701
|
+
emit('headers', id, headers, !!frame.endStream, s.extendedConnect || null);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (frame.endStream) receiveEndStream(s);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* §8.3 and §8.2.2. Malformed here means a STREAM error, not a connection
|
|
710
|
+
* error: one bad request should not take down the other streams sharing the
|
|
711
|
+
* connection.
|
|
712
|
+
*/
|
|
713
|
+
function validateHeaderList(list, stream) {
|
|
714
|
+
// The rules are shared with HTTP/3 (validateFieldSection, http_common);
|
|
715
|
+
// what is HTTP/2's is the answer — a stream error, PROTOCOL_ERROR — and
|
|
716
|
+
// where the result goes.
|
|
717
|
+
var r;
|
|
718
|
+
try {
|
|
719
|
+
r = validateFieldSection(list, {
|
|
720
|
+
isServer: isServer,
|
|
721
|
+
trailers: stream.headersReceived,
|
|
722
|
+
connectProtocol: !!localSettings.ENABLE_CONNECT_PROTOCOL
|
|
723
|
+
});
|
|
724
|
+
} catch (err) {
|
|
725
|
+
throw F.h2Error(err.message, F.ERROR.PROTOCOL_ERROR);
|
|
726
|
+
}
|
|
727
|
+
if (r.protocol) stream.extendedConnect = r.protocol;
|
|
728
|
+
// Emitted only after validation passes: a malformed header list is not a
|
|
729
|
+
// fingerprint, and reporting one would let a peer feed noise into whatever
|
|
730
|
+
// is collecting these.
|
|
731
|
+
if (r.pseudoOrder.length) emit('pseudo_order', r.pseudoOrder);
|
|
732
|
+
return r.headers;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
function onData(frame) {
|
|
737
|
+
var id = frame.streamId;
|
|
738
|
+
var s = streams[id];
|
|
739
|
+
|
|
740
|
+
// Flow control is accounted for even on a stream we are about to reject:
|
|
741
|
+
// the peer has spent the window, and if we do not give it back the
|
|
742
|
+
// connection stalls (§6.9.1). This is the classic "works until it
|
|
743
|
+
// doesn't" bug — small bodies pass, then everything freezes.
|
|
744
|
+
var len = frame.length;
|
|
745
|
+
localWindow -= len;
|
|
746
|
+
if (localWindow < 0) {
|
|
747
|
+
throw F.h2Error('connection flow-control window exceeded', F.ERROR.FLOW_CONTROL_ERROR);
|
|
748
|
+
}
|
|
749
|
+
maybeSendConnectionWindowUpdate(len);
|
|
750
|
+
|
|
751
|
+
if (isIdleStreamId(id)) {
|
|
752
|
+
throw F.h2Error('DATA on an idle stream', F.ERROR.PROTOCOL_ERROR);
|
|
753
|
+
}
|
|
754
|
+
if (!s || s.state === STATE.CLOSED || s.state === STATE.HALF_CLOSED_REMOTE) {
|
|
755
|
+
// DATA after END_STREAM is a connection error for the same reason as
|
|
756
|
+
// HEADERS; after a reset it is a stream error. §5.1 again.
|
|
757
|
+
if (s && s.closedBy === 'rst_sent') {
|
|
758
|
+
// In flight before our RST_STREAM: ignored (counted above).
|
|
759
|
+
} else if (s && s.closedBy === 'rst_received') {
|
|
760
|
+
streamError(id, F.ERROR.STREAM_CLOSED, 'DATA after RST_STREAM');
|
|
761
|
+
} else {
|
|
762
|
+
throw F.h2Error('DATA after END_STREAM', F.ERROR.STREAM_CLOSED);
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
s.localWindow -= len;
|
|
768
|
+
if (s.localWindow < 0) {
|
|
769
|
+
streamError(id, F.ERROR.FLOW_CONTROL_ERROR, 'stream window exceeded');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
s.bytesReceived += frame.data.length;
|
|
774
|
+
if (s.contentLength !== null && s.bytesReceived > s.contentLength) {
|
|
775
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, 'body longer than content-length');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (frame.data.length) emit('data', id, frame.data);
|
|
780
|
+
maybeSendStreamWindowUpdate(s, len);
|
|
781
|
+
|
|
782
|
+
if (frame.endStream) {
|
|
783
|
+
if (s.contentLength !== null && s.bytesReceived !== s.contentLength) {
|
|
784
|
+
streamError(id, F.ERROR.PROTOCOL_ERROR, 'body shorter than content-length');
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
receiveEndStream(s);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function receiveEndStream(s) {
|
|
792
|
+
s.endStreamReceived = true;
|
|
793
|
+
if (s.state === STATE.OPEN) s.state = STATE.HALF_CLOSED_REMOTE;
|
|
794
|
+
else if (s.state === STATE.HALF_CLOSED_LOCAL) closeStream(s.id, 'end');
|
|
795
|
+
emit('end', s.id);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
function onRstStream(frame) {
|
|
800
|
+
if (isIdleStreamId(frame.streamId)) {
|
|
801
|
+
throw F.h2Error('RST_STREAM on an idle stream', F.ERROR.PROTOCOL_ERROR);
|
|
802
|
+
}
|
|
803
|
+
closeStream(frame.streamId, 'rst_received');
|
|
804
|
+
if (stats) stats.code(stats.streamResets.h2.received, frame.errorCode);
|
|
805
|
+
emit('rst', frame.streamId, frame.errorCode);
|
|
806
|
+
|
|
807
|
+
// Separate from 'rst', which also fires for resets WE send. Only the
|
|
808
|
+
// peer's resets are the Rapid Reset signal (CVE-2023-44487): open a
|
|
809
|
+
// stream, reset it immediately, repeat. MAX_CONCURRENT_STREAMS never
|
|
810
|
+
// catches it because no stream stays open — the rate of resets is the
|
|
811
|
+
// only thing that shows it.
|
|
812
|
+
if (ev.listenerCount('stream_reset')) {
|
|
813
|
+
emit('stream_reset', frame.streamId, frame.errorCode);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
function onWindowUpdate(frame) {
|
|
819
|
+
// Surfaced before any validation: the connection-level increment a peer
|
|
820
|
+
// opens with is one of the strongest implementation tells there is —
|
|
821
|
+
// Chrome sends 15663105, Firefox 12517377, and most libraries send none.
|
|
822
|
+
emit('window_update', frame.streamId, frame.increment);
|
|
823
|
+
|
|
824
|
+
if (frame.streamId === 0) {
|
|
825
|
+
remoteWindow += frame.increment;
|
|
826
|
+
if (remoteWindow > F.MAX_WINDOW) {
|
|
827
|
+
throw F.h2Error('connection send window overflow', F.ERROR.FLOW_CONTROL_ERROR);
|
|
828
|
+
}
|
|
829
|
+
flushAll();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (isIdleStreamId(frame.streamId)) {
|
|
834
|
+
// §5.1: nothing may be sent for a stream that was never opened. This is
|
|
835
|
+
// the distinction the old code missed — it lumped "idle" together with
|
|
836
|
+
// "closed" and ignored both, so h2spec §5.1.3 waited for a GOAWAY that
|
|
837
|
+
// never came.
|
|
838
|
+
throw F.h2Error('WINDOW_UPDATE on an idle stream', F.ERROR.PROTOCOL_ERROR);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
var s = streams[frame.streamId];
|
|
842
|
+
if (!s || s.state === STATE.CLOSED) {
|
|
843
|
+
// Genuinely closed: the peer had this in flight when we closed the
|
|
844
|
+
// stream, which is ordinary and must be ignored (§6.9).
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
s.remoteWindow += frame.increment;
|
|
848
|
+
if (s.remoteWindow > F.MAX_WINDOW) {
|
|
849
|
+
streamError(frame.streamId, F.ERROR.FLOW_CONTROL_ERROR, 'stream send window overflow');
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
flushStream(s);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
function onPing(frame) {
|
|
857
|
+
if (frame.ack) { emit('pong', frame.payload); return; }
|
|
858
|
+
// §6.7: a PING must be answered immediately with the same payload. It is
|
|
859
|
+
// also how a peer measures RTT, so delaying it is not neutral.
|
|
860
|
+
write(F.buildPing(frame.payload, true));
|
|
861
|
+
emit('ping', frame.payload);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
function onGoaway(frame) {
|
|
866
|
+
goawayReceived = true;
|
|
867
|
+
if (stats) stats.code(stats.closes.h2.received, frame.errorCode);
|
|
868
|
+
emit('goaway', frame.lastStreamId, frame.errorCode, frame.debugData);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
// ============================================================
|
|
873
|
+
// Flow control — receive side
|
|
874
|
+
//
|
|
875
|
+
// When credit is returned decides whether flow control does anything at all.
|
|
876
|
+
//
|
|
877
|
+
// autoWindowUpdate (the default) grants it as soon as a DATA frame is
|
|
878
|
+
// parsed. Simple, and correct for a consumer that keeps up — but it means
|
|
879
|
+
// the window never actually closes, so there is no back-pressure: a peer
|
|
880
|
+
// can push a body far faster than the application reads it and the excess
|
|
881
|
+
// piles up in our memory. That is fine for a proxy that forwards
|
|
882
|
+
// immediately and wrong for anything that buffers.
|
|
883
|
+
//
|
|
884
|
+
// With autoWindowUpdate false, credit is returned only when the consumer
|
|
885
|
+
// calls consume(streamId, bytes). The window then reflects what has really
|
|
886
|
+
// been dealt with, and a slow application slows the peer down instead of
|
|
887
|
+
// filling the heap. h2_server uses this so that a paused Readable
|
|
888
|
+
// back-pressures the network, the same way h1_server lets TCP do it.
|
|
889
|
+
//
|
|
890
|
+
// The threshold exists because a WINDOW_UPDATE per DATA frame doubles the
|
|
891
|
+
// frame count for no benefit; half the window is what every implementation
|
|
892
|
+
// uses.
|
|
893
|
+
// ============================================================
|
|
894
|
+
|
|
895
|
+
var autoWindowUpdate = options.autoWindowUpdate !== false;
|
|
896
|
+
var creditNow = autoWindowUpdate;
|
|
897
|
+
var pendingConnectionCredit = 0;
|
|
898
|
+
|
|
899
|
+
function maybeSendConnectionWindowUpdate(consumed) {
|
|
900
|
+
pendingConnectionCredit += consumed;
|
|
901
|
+
if (!creditNow) return;
|
|
902
|
+
// Keyed off the connection window's own size, for the same reason as
|
|
903
|
+
// above: with a small INITIAL_WINDOW_SIZE this would otherwise emit a
|
|
904
|
+
// WINDOW_UPDATE for stream 0 after every few bytes. Its own size is
|
|
905
|
+
// CONNECTION_WINDOW (sendPreface widens it to that), not the 65535 it
|
|
906
|
+
// starts at: keyed off that, it went every 32 KB of a 16 MB window — a
|
|
907
|
+
// frame, a TLS record and a system call per 32 KB, 3000 of them in a
|
|
908
|
+
// 96 MB download, 10% of the client's CPU.
|
|
909
|
+
var threshold = Math.max(1, Math.floor(Math.max(F.DEFAULT_WINDOW_SIZE, CONNECTION_WINDOW) / 2));
|
|
910
|
+
if (pendingConnectionCredit >= threshold) {
|
|
911
|
+
write(F.buildWindowUpdate(0, pendingConnectionCredit));
|
|
912
|
+
localWindow += pendingConnectionCredit;
|
|
913
|
+
pendingConnectionCredit = 0;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function maybeSendStreamWindowUpdate(s, consumed) {
|
|
918
|
+
s.pendingCredit = (s.pendingCredit || 0) + consumed;
|
|
919
|
+
if (!creditNow) return;
|
|
920
|
+
var threshold = Math.max(1, Math.floor(localSettings.INITIAL_WINDOW_SIZE / 2));
|
|
921
|
+
if (s.pendingCredit >= threshold && s.state !== STATE.CLOSED) {
|
|
922
|
+
write(F.buildWindowUpdate(s.id, s.pendingCredit));
|
|
923
|
+
s.localWindow += s.pendingCredit;
|
|
924
|
+
s.pendingCredit = 0;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
// ============================================================
|
|
930
|
+
// Sending
|
|
931
|
+
// ============================================================
|
|
932
|
+
|
|
933
|
+
function sendPreface() {
|
|
934
|
+
if (!isServer) write(F.PREFACE);
|
|
935
|
+
|
|
936
|
+
var toSend = {};
|
|
937
|
+
for (var k in localSettings) {
|
|
938
|
+
// A client has no use for ENABLE_CONNECT_PROTOCOL: it is a server
|
|
939
|
+
// telling clients what they may ask for, and RFC 8441 §3 only defines
|
|
940
|
+
// the server-to-client direction.
|
|
941
|
+
if (k === 'ENABLE_CONNECT_PROTOCOL' && !isServer) continue;
|
|
942
|
+
toSend[k] = localSettings[k];
|
|
943
|
+
}
|
|
944
|
+
write(F.buildSettings(toSend, false));
|
|
945
|
+
|
|
946
|
+
// Widen the CONNECTION receive window immediately.
|
|
947
|
+
//
|
|
948
|
+
// It starts at 65535 and, unlike a stream window, SETTINGS cannot change
|
|
949
|
+
// it — RFC 9113 §6.9.2 leaves WINDOW_UPDATE on stream 0 as the only way to
|
|
950
|
+
// move it. A server that never sends one is telling every peer it can
|
|
951
|
+
// accept 64KB across the whole connection before stopping to wait for
|
|
952
|
+
// credit, no matter how many streams are open or how large the per-stream
|
|
953
|
+
// windows are.
|
|
954
|
+
//
|
|
955
|
+
// Every real implementation widens it in its first flight, which is why
|
|
956
|
+
// Chrome's HTTP/2 session table shows send windows in the megabytes for
|
|
957
|
+
// other servers and exactly 65535 for one that does not. On a fast link
|
|
958
|
+
// the difference is a round trip per 64KB — the connection spends most of
|
|
959
|
+
// its time waiting for WINDOW_UPDATEs it could have been given up front.
|
|
960
|
+
//
|
|
961
|
+
// 16MB nearly matches what Chrome itself advertises (15728640) and is
|
|
962
|
+
// comfortably under the 2^31-1 ceiling.
|
|
963
|
+
var bump = CONNECTION_WINDOW - F.DEFAULT_WINDOW_SIZE;
|
|
964
|
+
if (bump > 0) {
|
|
965
|
+
localWindow += bump;
|
|
966
|
+
write(F.buildWindowUpdate(0, bump));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function sendHeaders(streamId, headers, endStream) {
|
|
971
|
+
var s = getStream(streamId, true);
|
|
972
|
+
if (s.state === STATE.IDLE) markOpen(s);
|
|
973
|
+
|
|
974
|
+
var block = encoder.encode(headers);
|
|
975
|
+
var frames = F.buildHeaderFrames(streamId, block, { endStream: !!endStream },
|
|
976
|
+
remoteSettings.MAX_FRAME_SIZE);
|
|
977
|
+
// Concatenated into ONE write: §6.10 forbids any other frame between
|
|
978
|
+
// HEADERS and its CONTINUATIONs, and emitting them separately invites a
|
|
979
|
+
// caller (or a queue) to interleave.
|
|
980
|
+
write(Buffer.concat(frames));
|
|
981
|
+
|
|
982
|
+
if (endStream) sendEndStream(s);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Queue body bytes. Returns false when the send window is closed, so a
|
|
987
|
+
* caller can stop producing — the same contract as a Node stream's write().
|
|
988
|
+
*/
|
|
989
|
+
function sendData(streamId, data, endStream) {
|
|
990
|
+
var s = streams[streamId];
|
|
991
|
+
if (!s || s.state === STATE.CLOSED) return false;
|
|
992
|
+
// The caller's bytes as they are, not a copy — as net.Socket takes them:
|
|
993
|
+
// what is handed to write() is not the caller's to change until it has
|
|
994
|
+
// gone. A copy here, and another to put a frame header in front of each
|
|
995
|
+
// piece, were most of what a large response cost.
|
|
996
|
+
if (data && data.length) {
|
|
997
|
+
s.pendingData.push(typeof data === 'string' ? Buffer.from(data)
|
|
998
|
+
: Buffer.isBuffer(data) ? data
|
|
999
|
+
: Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
|
1000
|
+
}
|
|
1001
|
+
if (endStream) s.pendingEnd = true;
|
|
1002
|
+
// Straight out while nothing else waits; behind the others, in their
|
|
1003
|
+
// order, when something does (see flushAll).
|
|
1004
|
+
if (waiting.size === 0 || (waiting.size === 1 && waiting.has(s))) flushStream(s);
|
|
1005
|
+
else { waiting.add(s); flushAll(); }
|
|
1006
|
+
// Done with this write once nothing of it is left queued; 'drain' follows
|
|
1007
|
+
// when what is queued has gone. It returned whether the window was still
|
|
1008
|
+
// open — so a write that used it up exactly, leaving nothing queued, was
|
|
1009
|
+
// told to wait for a 'drain' that nothing would ever emit.
|
|
1010
|
+
return s.pendingData.length === 0;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function sendableBytes(s) {
|
|
1014
|
+
return Math.max(0, Math.min(s.remoteWindow, remoteWindow));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* One DATA frame of a stream's queue — as much as both windows allow, up to
|
|
1019
|
+
* MAX_FRAME_SIZE — or its END_STREAM alone once the queue is empty. True if
|
|
1020
|
+
* a frame was written.
|
|
1021
|
+
*
|
|
1022
|
+
* Both windows: the connection's and the stream's. Checking only one is the
|
|
1023
|
+
* single most common HTTP/2 flow-control bug — it works in every test with a
|
|
1024
|
+
* single stream and deadlocks the moment there are several, because one
|
|
1025
|
+
* greedy stream drains the connection window and the others never learn why
|
|
1026
|
+
* they stalled.
|
|
1027
|
+
*/
|
|
1028
|
+
function writeFrame(s) {
|
|
1029
|
+
if (s.state === STATE.CLOSED) return false;
|
|
1030
|
+
if (s.pendingData.length === 0) {
|
|
1031
|
+
if (!s.pendingEnd) return false;
|
|
1032
|
+
s.pendingEnd = false;
|
|
1033
|
+
write(F.buildData(s.id, Buffer.alloc(0), true));
|
|
1034
|
+
sendEndStream(s);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
// The windows say what the PEER will accept; the transport's buffer says
|
|
1038
|
+
// what is actually leaving. A peer that grants a large window and then
|
|
1039
|
+
// stops reading had every byte its windows allowed — up to 2 GB a stream
|
|
1040
|
+
// — queued in the socket (the "internal data buffering" attack of 2019).
|
|
1041
|
+
// DATA now waits in this queue, and flushAll() resumes when the socket
|
|
1042
|
+
// drains.
|
|
1043
|
+
if (isWriteBlocked()) return false;
|
|
1044
|
+
var budget = Math.min(sendableBytes(s), remoteSettings.MAX_FRAME_SIZE);
|
|
1045
|
+
if (budget <= 0) { emit('window_blocked', s.id); return false; }
|
|
1046
|
+
|
|
1047
|
+
var chunk = s.pendingData[0];
|
|
1048
|
+
var take = Math.min(chunk.length, budget);
|
|
1049
|
+
var piece = chunk.subarray(0, take);
|
|
1050
|
+
|
|
1051
|
+
if (take === chunk.length) s.pendingData.shift();
|
|
1052
|
+
else s.pendingData[0] = chunk.subarray(take);
|
|
1053
|
+
|
|
1054
|
+
s.remoteWindow -= take;
|
|
1055
|
+
remoteWindow -= take;
|
|
1056
|
+
|
|
1057
|
+
var last = s.pendingData.length === 0 && s.pendingEnd;
|
|
1058
|
+
// Header and payload as two writes, the payload never copied; the
|
|
1059
|
+
// server corks the socket, so they still leave together.
|
|
1060
|
+
write(F.frameHeader(take, F.TYPE.DATA, last ? F.FLAG.END_STREAM : 0, s.id));
|
|
1061
|
+
write(piece);
|
|
1062
|
+
if (last) { s.pendingEnd = false; sendEndStream(s); }
|
|
1063
|
+
if (s.pendingData.length === 0) { waiting.delete(s); emit('drain', s.id); }
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// As much of one stream's queue as can go now; what cannot waits (waiting).
|
|
1068
|
+
function flushStream(s) {
|
|
1069
|
+
if (!s) return;
|
|
1070
|
+
while (writeFrame(s)) {}
|
|
1071
|
+
if (s.state !== STATE.CLOSED && (s.pendingData.length > 0 || s.pendingEnd)) waiting.add(s);
|
|
1072
|
+
else waiting.delete(s);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ---- Which stream's DATA goes next (RFC 9218) ────────────────────────
|
|
1076
|
+
//
|
|
1077
|
+
// The streams whose DATA waits — for a window, or for the socket to drain —
|
|
1078
|
+
// and the order they go in when there is room, by the rules HTTP/3's
|
|
1079
|
+
// sender uses (quic_connection's execute_quic_burst): urgency first, most
|
|
1080
|
+
// urgent first; within one, the non-incremental streams one at a time, by
|
|
1081
|
+
// stream number, then the incremental ones taking turns, a frame each,
|
|
1082
|
+
// the round starting after whoever went first last time. A stream with no
|
|
1083
|
+
// signal takes turns at urgency 3. They went by stream number, each until
|
|
1084
|
+
// blocked: after every drain the lowest-numbered stream took all the room.
|
|
1085
|
+
//
|
|
1086
|
+
// Only what waits is ordered: a write while nothing waits goes straight to
|
|
1087
|
+
// the socket, and what the socket holds (up to its mark, isWriteBlocked)
|
|
1088
|
+
// leaves in the order it was written.
|
|
1089
|
+
var waiting = new Set();
|
|
1090
|
+
var rrCursor = {}; // urgency → the stream that opened the last round
|
|
1091
|
+
|
|
1092
|
+
function flushAll() {
|
|
1093
|
+
if (waiting.size === 0) return;
|
|
1094
|
+
var tiers = new Map();
|
|
1095
|
+
waiting.forEach(function (s) {
|
|
1096
|
+
if (s.state === STATE.CLOSED) { waiting.delete(s); return; }
|
|
1097
|
+
var pr = s.priority, u = pr && pr.urgency != null ? pr.urgency : 3;
|
|
1098
|
+
var t = tiers.get(u);
|
|
1099
|
+
if (!t) { t = { seq: [], inc: [] }; tiers.set(u, t); }
|
|
1100
|
+
(pr && pr.incremental === false ? t.seq : t.inc).push(s);
|
|
1101
|
+
});
|
|
1102
|
+
var byId = function (a, b) { return a.id - b.id; };
|
|
1103
|
+
var urgencies = Array.from(tiers.keys()).sort(function (a, b) { return a - b; });
|
|
1104
|
+
for (var ui = 0; ui < urgencies.length; ui++) {
|
|
1105
|
+
var u = urgencies[ui], t = tiers.get(u);
|
|
1106
|
+
t.seq.sort(byId);
|
|
1107
|
+
for (var i = 0; i < t.seq.length; i++) {
|
|
1108
|
+
flushStream(t.seq[i]);
|
|
1109
|
+
if (isWriteBlocked()) return;
|
|
1110
|
+
}
|
|
1111
|
+
var inc = t.inc.sort(byId);
|
|
1112
|
+
if (inc.length === 0) continue;
|
|
1113
|
+
var first = 0;
|
|
1114
|
+
if (rrCursor[u] !== undefined) {
|
|
1115
|
+
while (first < inc.length && inc[first].id <= rrCursor[u]) first++;
|
|
1116
|
+
if (first === inc.length) first = 0;
|
|
1117
|
+
}
|
|
1118
|
+
var opened = false, progress = true;
|
|
1119
|
+
while (progress) {
|
|
1120
|
+
progress = false;
|
|
1121
|
+
for (var k = 0; k < inc.length; k++) {
|
|
1122
|
+
var st = inc[(first + k) % inc.length];
|
|
1123
|
+
if (!writeFrame(st)) continue;
|
|
1124
|
+
progress = true;
|
|
1125
|
+
if (!opened) { rrCursor[u] = st.id; opened = true; }
|
|
1126
|
+
}
|
|
1127
|
+
if (isWriteBlocked()) break;
|
|
1128
|
+
}
|
|
1129
|
+
for (var j = 0; j < inc.length; j++) flushStream(inc[j]); // waiting, as they now are
|
|
1130
|
+
if (isWriteBlocked()) return;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ---- RFC 9218's signals ----
|
|
1135
|
+
//
|
|
1136
|
+
// As in HTTP/3 (h3.js): a request's `priority` header and PRIORITY_UPDATE,
|
|
1137
|
+
// which replaces it, are the client's signals; what the application sets
|
|
1138
|
+
// (setStreamPriority, h2_server's res.setPriority) is the server's decision,
|
|
1139
|
+
// and a signal does not override it (mergePriority). An update for a
|
|
1140
|
+
// stream not yet opened is held until its request arrives — bounded,
|
|
1141
|
+
// oldest out.
|
|
1142
|
+
var PRIORITY_PENDING_MAX = 128;
|
|
1143
|
+
var prioPending = new Map(); // stream id → Priority Field Value
|
|
1144
|
+
|
|
1145
|
+
function setStreamPriority(id, p, fromPeer) {
|
|
1146
|
+
var s = streams[id];
|
|
1147
|
+
if (!s || s.state === STATE.CLOSED) return false;
|
|
1148
|
+
s.priority = mergePriority(s.priority || {}, p, fromPeer);
|
|
1149
|
+
if (waiting.has(s)) flushAll();
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function applyRequestPriority(s, headers) {
|
|
1154
|
+
if (headers.priority != null) setStreamPriority(s.id, parsePriority(headers.priority), true);
|
|
1155
|
+
if (prioPending.has(s.id)) {
|
|
1156
|
+
setStreamPriority(s.id, parsePriority(prioPending.get(s.id)), true);
|
|
1157
|
+
prioPending.delete(s.id);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function onPriorityUpdate(frame) {
|
|
1162
|
+
// §7.1: the server sends none; the stream must be one the client opens.
|
|
1163
|
+
if (!isServer) throw F.h2Error('PRIORITY_UPDATE from the server', F.ERROR.PROTOCOL_ERROR);
|
|
1164
|
+
var id = frame.prioritizedStreamId;
|
|
1165
|
+
if (id % 2 !== 1) throw F.h2Error('PRIORITY_UPDATE for stream ' + id + ', not a request stream', F.ERROR.PROTOCOL_ERROR);
|
|
1166
|
+
var s = streams[id];
|
|
1167
|
+
if (isIdleStreamId(id) || (s && !s.headersReceived)) {
|
|
1168
|
+
prioPending.delete(id);
|
|
1169
|
+
prioPending.set(id, frame.priorityField);
|
|
1170
|
+
if (prioPending.size > PRIORITY_PENDING_MAX) prioPending.delete(prioPending.keys().next().value);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
setStreamPriority(id, parsePriority(frame.priorityField), true); // a closed stream: nothing to do
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function sendEndStream(s) {
|
|
1177
|
+
s.endStreamSent = true;
|
|
1178
|
+
if (s.state === STATE.OPEN) {
|
|
1179
|
+
s.state = STATE.HALF_CLOSED_LOCAL;
|
|
1180
|
+
// Our side is done and the peer's is not: a server that answered
|
|
1181
|
+
// before the request body was in (RFC 9113 §8.1). The layer above
|
|
1182
|
+
// decides whether to tell the client to stop (RST_STREAM NO_ERROR).
|
|
1183
|
+
emit('local_end', s.id);
|
|
1184
|
+
}
|
|
1185
|
+
else if (s.state === STATE.HALF_CLOSED_REMOTE) closeStream(s.id, 'end');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function sendGoaway(errorCode, debug) {
|
|
1189
|
+
if (goawaySent) return;
|
|
1190
|
+
goawaySent = true;
|
|
1191
|
+
goawayLastStreamId = lastRemoteStreamId;
|
|
1192
|
+
write(F.buildGoaway(lastRemoteStreamId, errorCode || F.ERROR.NO_ERROR, debug));
|
|
1193
|
+
if (stats) stats.code(stats.closes.h2.sent, errorCode || F.ERROR.NO_ERROR);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
// ============================================================
|
|
1198
|
+
// API
|
|
1199
|
+
// ============================================================
|
|
1200
|
+
|
|
1201
|
+
this.on = on;
|
|
1202
|
+
// off() matters here in a way it does not for a parser: h2_server registers
|
|
1203
|
+
// a one-shot 'drain' listener per blocked write, and a long response on a
|
|
1204
|
+
// narrow window blocks thousands of times. Without a way to remove them the
|
|
1205
|
+
// listener array grows for the life of the connection.
|
|
1206
|
+
this.off = off;
|
|
1207
|
+
this.execute = execute;
|
|
1208
|
+
this.sendPreface = sendPreface;
|
|
1209
|
+
this.sendHeaders = sendHeaders;
|
|
1210
|
+
this.sendData = sendData;
|
|
1211
|
+
/** The transport can take more again (its 'drain'): send what DATA waited. */
|
|
1212
|
+
this.flushAll = flushAll;
|
|
1213
|
+
/**
|
|
1214
|
+
* A stream's priority: { urgency (0..7, 0 first; default 3), incremental }
|
|
1215
|
+
* (RFC 9218). Set by the application, it is the decision; fromPeer marks
|
|
1216
|
+
* the peer's signal, which does not override it (see mergePriority).
|
|
1217
|
+
*/
|
|
1218
|
+
this.setStreamPriority = setStreamPriority;
|
|
1219
|
+
/** The client's side: change a request's priority after it was sent (RFC 9218 §7.1). */
|
|
1220
|
+
this.sendPriorityUpdate = function (streamId, p) {
|
|
1221
|
+
if (isServer) return false;
|
|
1222
|
+
write(F.buildPriorityUpdate(Number(streamId), formatPriority(p)));
|
|
1223
|
+
return true;
|
|
1224
|
+
};
|
|
1225
|
+
this.sendGoaway = sendGoaway;
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Tell the peer to stop opening streams.
|
|
1229
|
+
*
|
|
1230
|
+
* NOT a disconnect: RFC 9113 §6.8 leaves streams below lastStreamId running
|
|
1231
|
+
* to completion, and only forbids new ones. That is what makes it right for
|
|
1232
|
+
* a rate limiter — in-flight requests still get answered — and wrong if the
|
|
1233
|
+
* intent is to cut the peer off, which needs destroying the socket as well.
|
|
1234
|
+
*/
|
|
1235
|
+
this.goaway = function (errorCode) {
|
|
1236
|
+
sendGoaway(errorCode === undefined ? F.ERROR.NO_ERROR : errorCode);
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
this.sendRstStream = function (streamId, errorCode) {
|
|
1240
|
+
streamError(streamId, errorCode === undefined ? F.ERROR.CANCEL : errorCode);
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
this.sendPing = function (payload) { write(F.buildPing(payload, false)); };
|
|
1244
|
+
|
|
1245
|
+
this.sendTrailers = function (streamId, headers) {
|
|
1246
|
+
var s = streams[streamId];
|
|
1247
|
+
if (!s || s.state === STATE.CLOSED) return;
|
|
1248
|
+
var block = encoder.encode(headers);
|
|
1249
|
+
write(Buffer.concat(F.buildHeaderFrames(streamId, block, { endStream: true },
|
|
1250
|
+
remoteSettings.MAX_FRAME_SIZE)));
|
|
1251
|
+
sendEndStream(s);
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
/** Allocate the next stream id for a locally initiated stream. */
|
|
1255
|
+
this.nextStreamId = function () {
|
|
1256
|
+
var id = nextLocalStreamId;
|
|
1257
|
+
nextLocalStreamId += 2;
|
|
1258
|
+
return id;
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Tell the connection that `bytes` of a stream's body have been dealt with,
|
|
1263
|
+
* returning that much flow-control credit to the peer.
|
|
1264
|
+
*
|
|
1265
|
+
* Only meaningful with autoWindowUpdate: false; with the default the credit
|
|
1266
|
+
* has already been returned and this is a no-op.
|
|
1267
|
+
*/
|
|
1268
|
+
this.consume = function (streamId, bytes) {
|
|
1269
|
+
if (autoWindowUpdate) return;
|
|
1270
|
+
var s = streams[streamId];
|
|
1271
|
+
creditNow = true;
|
|
1272
|
+
try {
|
|
1273
|
+
maybeSendConnectionWindowUpdate(0);
|
|
1274
|
+
if (s) {
|
|
1275
|
+
s.pendingCredit = (s.pendingCredit || 0);
|
|
1276
|
+
maybeSendStreamWindowUpdate(s, 0);
|
|
1277
|
+
}
|
|
1278
|
+
} finally { creditNow = false; }
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
/** The `:protocol` value if this stream is an extended CONNECT, else null. */
|
|
1282
|
+
this.getStreamProtocol = function (id) {
|
|
1283
|
+
var s = streams[id];
|
|
1284
|
+
return (s && s.extendedConnect) || null;
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
/** Whether the PEER told us it accepts extended CONNECT. */
|
|
1288
|
+
this.peerAllowsConnectProtocol = function () {
|
|
1289
|
+
return remoteSettings.ENABLE_CONNECT_PROTOCOL === 1;
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
this.getStreamState = function (id) {
|
|
1293
|
+
var s = streams[id];
|
|
1294
|
+
return s ? s.state : STATE.IDLE;
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
this.canSend = function (id) {
|
|
1298
|
+
var s = streams[id];
|
|
1299
|
+
return s ? sendableBytes(s) > 0 : false;
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
/** Whether some stream's DATA waits for the peer's WINDOW_UPDATE. */
|
|
1303
|
+
this.windowBlocked = function () {
|
|
1304
|
+
var blocked = false;
|
|
1305
|
+
waiting.forEach(function (s) {
|
|
1306
|
+
if (s.state !== STATE.CLOSED && s.pendingData.length > 0 && sendableBytes(s) <= 0) blocked = true;
|
|
1307
|
+
});
|
|
1308
|
+
return blocked;
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
Object.defineProperty(this, 'localSettings', { get: function () { return localSettings; } });
|
|
1312
|
+
Object.defineProperty(this, 'remoteSettings', { get: function () { return remoteSettings; } });
|
|
1313
|
+
Object.defineProperty(this, 'activeStreams', { get: function () { return activeCount; } });
|
|
1314
|
+
Object.defineProperty(this, 'goawayReceived', { get: function () { return goawayReceived; } });
|
|
1315
|
+
Object.defineProperty(this, 'closed', { get: function () { return closed; } });
|
|
1316
|
+
|
|
1317
|
+
return this;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
// DATA waits while a socket holds this much unsent (isWriteBlocked) — the
|
|
1322
|
+
// server's and the client's sessions alike.
|
|
1323
|
+
var SOCKET_HIGH_WATER = 256 * 1024;
|
|
1324
|
+
|
|
1325
|
+
export { H2Connection, STATE, SOCKET_HIGH_WATER };
|