h2lean 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +265 -2
- package/index.js +72 -0
- package/lib/client-session.js +2068 -0
- package/lib/client-stream.js +166 -0
- package/lib/client.js +85 -0
- package/lib/compat.js +849 -0
- package/lib/fast-timer.js +436 -0
- package/lib/frames.js +127 -0
- package/lib/headers.js +463 -0
- package/lib/hpack-data.js +333 -0
- package/lib/hpack.js +1288 -0
- package/lib/pieces.js +134 -0
- package/lib/server.js +272 -0
- package/lib/session.js +2698 -0
- package/lib/stream.js +519 -0
- package/package.json +49 -4
package/lib/headers.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// Response/trailer header validation and HPACK block construction.
|
|
2
|
+
//
|
|
3
|
+
// The optimisation that matters here is granularity: encoding one field line at a
|
|
4
|
+
// time costs a key string plus a Map lookup each, while encoding the *whole block*
|
|
5
|
+
// once and memcpy-ing it is nearly free (OPTIMIZATION.md item 6). So the cache
|
|
6
|
+
// is keyed by the identity of the caller's headers object and holds the finished
|
|
7
|
+
// HPACK bytes.
|
|
8
|
+
//
|
|
9
|
+
// That is only sound when the object cannot change underneath us, so caching is
|
|
10
|
+
// restricted to frozen objects (Object.isSealed is not enough: sealed objects still
|
|
11
|
+
// allow a value to be reassigned). Non-frozen objects are encoded fresh on every call,
|
|
12
|
+
// exactly like node does. And because the encoder keeps an HPACK dynamic table, the
|
|
13
|
+
// bytes also depend on the connection they go out on: the cache lives on the session.
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const { Encoder } = require('./hpack');
|
|
17
|
+
const { sensitiveHeaders: KSensitiveHeaders } = require('node:http2');
|
|
18
|
+
|
|
19
|
+
// A header map with no prototype.
|
|
20
|
+
//
|
|
21
|
+
// The prototype is taken away after the object is built rather than never set,
|
|
22
|
+
// because V8 keeps such an object in fast-properties mode, where the stores and
|
|
23
|
+
// the keyed reads cost less than on a dictionary-mode one (OPTIMIZATION.md item 4;
|
|
24
|
+
// the measured numbers live there, not here).
|
|
25
|
+
//
|
|
26
|
+
// The prototype still has to be null: a field named "__proto__" must become an
|
|
27
|
+
// ordinary field and not a write to the map's prototype, and a map that keeps
|
|
28
|
+
// Object.prototype would answer "constructor" or "toString" for a field that
|
|
29
|
+
// was never set -- two readers of one map disagreeing about what it carries.
|
|
30
|
+
// node's own header objects are { __proto__: null } literals
|
|
31
|
+
// (internal/http2/util.js, toHeaderObject), and the two shapes are
|
|
32
|
+
// indistinguishable from the outside: same prototype, same enumeration order,
|
|
33
|
+
// same JSON, same freeze behaviour (bench/micro/nullproto-semantics.js).
|
|
34
|
+
//
|
|
35
|
+
// A fast map hangs on the process-wide transition tree of its root map, and the
|
|
36
|
+
// field names come from the peer -- so the question is whether that tree can
|
|
37
|
+
// grow without bound. It cannot: V8 rehomes the root map once its transition
|
|
38
|
+
// array fills (observed: "Transitions #1536"), and the retained heap is flat
|
|
39
|
+
// across 1.8 million distinct six-field name sets (bench/micro/nullproto-root.js,
|
|
40
|
+
// bench/micro/nullproto-tree2.js, bench/micro/nullproto-tree3.js).
|
|
41
|
+
function nullProto() {
|
|
42
|
+
return Object.setPrototypeOf({}, null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Encoded bytes for one response, reused between calls. The encoder that fills it
|
|
46
|
+
// belongs to the session (lib/session), because a dynamic table is per-connection
|
|
47
|
+
// state, so nothing on this path is shared between sessions.
|
|
48
|
+
// A response block lives for microseconds: _frame() copies it into the staging buffer
|
|
49
|
+
// of the outbound queue, so the queue never holds a view of this scratch. See block()
|
|
50
|
+
// before holding on to the returned view across another call.
|
|
51
|
+
let scratch = Buffer.allocUnsafe(4096);
|
|
52
|
+
|
|
53
|
+
const isToken = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
54
|
+
// Whether a field name is legal depends only on its characters, so the names already
|
|
55
|
+
// accepted are remembered and the regex is not run again for them (OPTIMIZATION.md
|
|
56
|
+
// item 7), which a compat
|
|
57
|
+
// response pays for every header it sends because its header object is not frozen
|
|
58
|
+
// and cannot be cached.
|
|
59
|
+
// Bounded: an application that reflects untrusted input into header names would
|
|
60
|
+
// otherwise grow the cache for the life of the process, and a name that is new to a
|
|
61
|
+
// full cache costs the regex and nothing else -- the same as before this existed.
|
|
62
|
+
// A Set rather than an object: 'constructor' and '__proto__' must not be special, and
|
|
63
|
+
// the object form is the slower one (OPTIMIZATION.md item 7).
|
|
64
|
+
const VALID_NAMES = new Set();
|
|
65
|
+
const VALID_NAMES_MAX = 1024;
|
|
66
|
+
|
|
67
|
+
// RFC 9113 8.2.1: a field name is a token, and it is lower case. The token check is not
|
|
68
|
+
// cosmetic -- a name carrying CR, LF or NUL is how one header becomes two *lines* again
|
|
69
|
+
// once a proxy translates this connection to HTTP/1.1, which is the class of CVE-2020-8287
|
|
70
|
+
// (Node.js read one of two copies of a field and ignored the other, and the same split
|
|
71
|
+
// happens when a separator is smuggled inside a field). Refusing the name here, before a
|
|
72
|
+
// single octet is produced, is the whole mitigation: the application cannot build a header
|
|
73
|
+
// that a downstream reader would parse differently. RFC 9113 10.3 calls out exactly this
|
|
74
|
+
// HTTP/2-to-HTTP/1.1 translation risk as the reason the rules are strict.
|
|
75
|
+
function isFieldName(key) {
|
|
76
|
+
if (VALID_NAMES.has(key)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (!isToken.test(key)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (VALID_NAMES.size < VALID_NAMES_MAX) {
|
|
83
|
+
VALID_NAMES.add(key);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A field name in the form the wire wants it.
|
|
89
|
+
//
|
|
90
|
+
// Two shapes, and they are not the same one: the compat layer's name is
|
|
91
|
+
// trimmed and lower case (node's compat layer does `name.trim().toLowerCase()`,
|
|
92
|
+
// internal/http2/compat.js), the encoder's is lower case alone (node's
|
|
93
|
+
// buildNgHeaderString does `key.toLowerCase()`, internal/http2/util.js).
|
|
94
|
+
//
|
|
95
|
+
// Both are cached. A name that is already in the wanted shape costs nothing to
|
|
96
|
+
// produce -- trim() and toLowerCase() return the very same string object --
|
|
97
|
+
// so what is left is the call itself -- a Map lookup beats the two calls it
|
|
98
|
+
// replaces (OPTIMIZATION.md item 8).
|
|
99
|
+
//
|
|
100
|
+
// Bounded, and it never evicts, like VALID_NAMES above: the names come from
|
|
101
|
+
// application code, and an application that reflects untrusted input into
|
|
102
|
+
// header names would otherwise grow the cache for the life of the process.
|
|
103
|
+
// The bound is the answer to that, and a better one than a periodic reset: a
|
|
104
|
+
// reset needs a timer (a live handle) and throws away the hot entries, while
|
|
105
|
+
// the bound caps the damage and keeps what is hot. A name that is new to a
|
|
106
|
+
// full cache costs the lookup and the normalisation, which is what a hit
|
|
107
|
+
// costs anyway.
|
|
108
|
+
//
|
|
109
|
+
// The miss is the expensive direction, and the insert into a growing Map is what
|
|
110
|
+
// makes it so (OPTIMIZATION.md item 8). So the cache pays for the names an application
|
|
111
|
+
// writes as literals, whose hash V8 computes once and keeps in the string, and it
|
|
112
|
+
// loses for the names it builds fresh every time.
|
|
113
|
+
const NAME_CACHE = new Map();
|
|
114
|
+
const LOWER_CACHE = new Map();
|
|
115
|
+
const NAME_CACHE_MAX = 1024;
|
|
116
|
+
|
|
117
|
+
// The compat layer's shape. A name that is not a string throws here, the way
|
|
118
|
+
// node's private kSetHeader does (name.trim() is not a function), and the
|
|
119
|
+
// public accessors in lib/compat.js refuse it before they get here.
|
|
120
|
+
function trimName(name) {
|
|
121
|
+
let out = NAME_CACHE.get(name);
|
|
122
|
+
if (out === undefined) {
|
|
123
|
+
out = name.trim().toLowerCase();
|
|
124
|
+
if (NAME_CACHE.size < NAME_CACHE_MAX) {
|
|
125
|
+
NAME_CACHE.set(name, out);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The encoder's shape: lower case, not trimmed. Trimming here would change
|
|
132
|
+
// what the encoder accepts -- a name with a space around it is a token error
|
|
133
|
+
// today, and it has to stay one (RFC 9113 8.2.1).
|
|
134
|
+
function lowerName(name) {
|
|
135
|
+
let out = LOWER_CACHE.get(name);
|
|
136
|
+
if (out === undefined) {
|
|
137
|
+
out = name.toLowerCase();
|
|
138
|
+
if (LOWER_CACHE.size < NAME_CACHE_MAX) {
|
|
139
|
+
LOWER_CACHE.set(name, out);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The size of the two caches, for the test that pins the bound. The caches are
|
|
146
|
+
// module state, and a bound that nothing can see is a bound nothing checks.
|
|
147
|
+
function nameCacheSize() {
|
|
148
|
+
return { names: NAME_CACHE.size, lowers: LOWER_CACHE.size };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// RFC 9113 8.2: a field value may not contain NUL, LF or CR (HTAB is allowed), and no
|
|
152
|
+
// other control character -- the same injection class as the name check above, seen from
|
|
153
|
+
// the value side. It is also what makes a value safe to use as a map key: the encoder's
|
|
154
|
+
// byte cache is keyed by the name and then by the value (lib/hpack.js), so the two are
|
|
155
|
+
// never concatenated and no separator can be spoofed -- but a key shape that does
|
|
156
|
+
// concatenate them would need this check to stay.
|
|
157
|
+
//
|
|
158
|
+
// The class is the one nghttp2 applies to an INBOUND value, measured against a live
|
|
159
|
+
// node server (bench/micro/probe-value-matrix.js): every control character but HTAB, and DEL.
|
|
160
|
+
// A field whose value carries one is ignored (NGHTTP2_ERR_IGN_HTTP_HEADER) and the
|
|
161
|
+
// stream lives -- the request is served, without the field.
|
|
162
|
+
//
|
|
163
|
+
// The OUTBOUND check is h2lean's own, and stricter than node's: node does not look at
|
|
164
|
+
// an outbound value at all and puts a CR on the wire (bench/micro/probe-out-value-matrix.js --
|
|
165
|
+
// every one of the fifteen characters tried is accepted, and the CR arrives in the
|
|
166
|
+
// HEADERS frame). Refusing here is the mitigation the CVE-2020-8287 class needs: the
|
|
167
|
+
// value never becomes an octet, so a translator behind this server cannot read it
|
|
168
|
+
// differently than we do. It costs an application that sets such a value a refusal
|
|
169
|
+
// node does not make.
|
|
170
|
+
const badValue = /[\u0000-\u0008\u000a-\u001f\u007f]/;
|
|
171
|
+
|
|
172
|
+
// Kept as a Set: at five names the shape of the operation decides it, not the
|
|
173
|
+
// size (OPTIMIZATION.md item 10).
|
|
174
|
+
// RFC 9113 8.2.2 forbids these on an HTTP/2 connection: they are hop-by-hop in HTTP/1.1,
|
|
175
|
+
// so sending one would either be ignored by the peer (a lie to the application) or
|
|
176
|
+
// forwarded by a downstream translator (CVE-2020-8287's class: two readers, one field).
|
|
177
|
+
const CONNECTION_SPECIFIC = new Set(['connection', 'keep-alive', 'proxy-connection',
|
|
178
|
+
'transfer-encoding', 'upgrade']);
|
|
179
|
+
|
|
180
|
+
// Whether a field value carries a character that may not appear in one. The server's
|
|
181
|
+
// inbound path asks the same question (lib/session.js) and answers it the way nghttp2
|
|
182
|
+
// does: the field is ignored, the stream lives.
|
|
183
|
+
function hasBadValue(value) {
|
|
184
|
+
return badValue.test(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function httpError(code, msg) {
|
|
188
|
+
const err = new Error(msg);
|
|
189
|
+
err.code = code;
|
|
190
|
+
return err;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// node adds a `date` header to every response unless the application set one or
|
|
194
|
+
// turned sendDate off. The value only changes once a second, so it is cached -- and
|
|
195
|
+
// the cache is invalidated by a one-shot unref'd timer rather than compared against
|
|
196
|
+
// the clock on every call, which is what node's own HTTP/1.1 date cache does
|
|
197
|
+
// (internal/http; OPTIMIZATION.md item 9 for the measurements). An idle server holds
|
|
198
|
+
// no timer at all; a refresh happens on the next response after the boundary, which
|
|
199
|
+
// can be a little late if the loop was busy -- exactly as in node.
|
|
200
|
+
let dateString;
|
|
201
|
+
let dateTimer = null;
|
|
202
|
+
let dateSecond = 0; // counts the boundaries crossed, see touchDate()
|
|
203
|
+
function httpDate() {
|
|
204
|
+
if (dateString === undefined) {
|
|
205
|
+
refreshDate();
|
|
206
|
+
}
|
|
207
|
+
return dateString;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Refreshes the string if the second has moved and reports which second that is. A block
|
|
211
|
+
// cache entry records it, because the generated date is the one field whose value is not
|
|
212
|
+
// fixed by the header object the entry is keyed on. Callers read the stamp *before* the
|
|
213
|
+
// value: a boundary can then only make the value newer than its stamp, which costs one
|
|
214
|
+
// re-encode, never one stale response.
|
|
215
|
+
function touchDate() {
|
|
216
|
+
if (dateString === undefined) {
|
|
217
|
+
refreshDate();
|
|
218
|
+
}
|
|
219
|
+
return dateSecond;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function refreshDate() {
|
|
223
|
+
const now = new Date();
|
|
224
|
+
dateString = now.toUTCString();
|
|
225
|
+
dateSecond++;
|
|
226
|
+
const ms = now.getMilliseconds();
|
|
227
|
+
dateTimer = setTimeout(() => {
|
|
228
|
+
dateString = undefined;
|
|
229
|
+
dateTimer = null;
|
|
230
|
+
}, ms === 0 ? 1000 : 1000 - ms);
|
|
231
|
+
dateTimer.unref();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// -> { status, pseudo: [name, value, ...], fields: [name, value, never, ...] }
|
|
235
|
+
// The two lists are flat: a field line is three slots in one array, not an array of
|
|
236
|
+
// its own. The compat API parses the headers of every response, and a response with
|
|
237
|
+
// five headers used to allocate five arrays plus the two lists that hold them; the
|
|
238
|
+
// flat form allocates the two lists and nothing else, and the loops that read them
|
|
239
|
+
// need no iterator.
|
|
240
|
+
// `info` marks the informational (1xx) block that additionalHeaders() sends: node applies
|
|
241
|
+
// a different status rule and different error codes there, and it generates no date for it.
|
|
242
|
+
function parse(headers, sensitive, { trailer = false, sendDate = false, info = false } = {}) {
|
|
243
|
+
if (headers === null || typeof headers !== 'object') {
|
|
244
|
+
throw httpError('ERR_HTTP2_INVALID_HEADER_VALUE', 'headers must be an object');
|
|
245
|
+
}
|
|
246
|
+
const neverSet = sensitive || headers[KSensitiveHeaders];
|
|
247
|
+
const neverList = Array.isArray(neverSet) && neverSet.length > 0
|
|
248
|
+
? new Set(neverSet.map((s) => lowerName(s)))
|
|
249
|
+
: null;
|
|
250
|
+
|
|
251
|
+
const pseudo = [];
|
|
252
|
+
const fields = [];
|
|
253
|
+
let status;
|
|
254
|
+
let hasDate = false;
|
|
255
|
+
let sawStatus = false;
|
|
256
|
+
const keys = Object.keys(headers);
|
|
257
|
+
for (let i = 0; i < keys.length; i++) {
|
|
258
|
+
const key = lowerName(keys[i]);
|
|
259
|
+
const raw = headers[keys[i]];
|
|
260
|
+
// Only an absent value is skipped. An empty name is not: node rejects it with
|
|
261
|
+
// ERR_INVALID_HTTP_TOKEN, and the token check below does the same, because the
|
|
262
|
+
// pattern requires at least one character.
|
|
263
|
+
if (raw === undefined) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (key[0] === ':') {
|
|
268
|
+
if (trailer) {
|
|
269
|
+
throw httpError('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'cannot send trailers with pseudo-headers');
|
|
270
|
+
}
|
|
271
|
+
if (key !== ':status') {
|
|
272
|
+
throw httpError('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', `cannot send ${key} in a response`);
|
|
273
|
+
}
|
|
274
|
+
const single = Array.isArray(raw) ? raw[0] : raw;
|
|
275
|
+
// node coerces with `statusCode | 0` -- not Number(), and not a range check on the
|
|
276
|
+
// string: '200.5' is 200, 1.9 is 1, 'abc' is 0. Both paths then add their own rule,
|
|
277
|
+
// measured against a live node server:
|
|
278
|
+
// respond(): 0 becomes 200, anything outside 200..599 throws
|
|
279
|
+
// ERR_HTTP2_STATUS_INVALID (core.js:2700 and 2747), so 103
|
|
280
|
+
// and 600 are refused here although this used to accept both;
|
|
281
|
+
// additionalHeaders(): 101 is ERR_HTTP2_STATUS_101 and every other value outside
|
|
282
|
+
// 100..199 is ERR_HTTP2_INVALID_INFO_STATUS (core.js:3268-3274).
|
|
283
|
+
// The value that goes on the wire is the coerced one, which is why :status is
|
|
284
|
+
// emitted below, after the default and the range have been decided.
|
|
285
|
+
const code = Number(single) | 0;
|
|
286
|
+
if (info) {
|
|
287
|
+
// 101 is Switching Protocols, which HTTP/2 forbids outright (RFC 9113 8.5): an
|
|
288
|
+
// upgrade can only be expressed as a connection-specific field, and RFC 9113 8.2.2
|
|
289
|
+
// removes those from this protocol.
|
|
290
|
+
if (code === 101) {
|
|
291
|
+
throw httpError('ERR_HTTP2_STATUS_101', 'HTTP status code 101 is disallowed');
|
|
292
|
+
}
|
|
293
|
+
if (code < 100 || code >= 200) {
|
|
294
|
+
throw httpError('ERR_HTTP2_INVALID_INFO_STATUS',
|
|
295
|
+
`invalid informational status code: ${single}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
status = code;
|
|
299
|
+
sawStatus = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!isFieldName(key)) {
|
|
304
|
+
throw httpError('ERR_INVALID_HTTP_TOKEN', `${key} is an invalid header token`);
|
|
305
|
+
}
|
|
306
|
+
if (CONNECTION_SPECIFIC.has(key)) {
|
|
307
|
+
throw httpError('ERR_HTTP2_INVALID_CONNECTION_HEADERS',
|
|
308
|
+
`HTTP/1 Connection specific header is forbidden: ${key}`);
|
|
309
|
+
}
|
|
310
|
+
const never = neverList !== null && neverList.has(key);
|
|
311
|
+
if (Array.isArray(raw)) {
|
|
312
|
+
for (let j = 0; j < raw.length; j++) {
|
|
313
|
+
fields.push(key, checked(key, raw[j]), never);
|
|
314
|
+
}
|
|
315
|
+
if (key === 'date') {
|
|
316
|
+
hasDate = true;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
fields.push(key, checked(key, raw), never);
|
|
320
|
+
if (key === 'date') {
|
|
321
|
+
hasDate = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// A response without :status is not an error in node: the same function that adds the
|
|
327
|
+
// date does `statusCode | 0 || HTTP_STATUS_OK`, so a missing (or NaN-ish) status
|
|
328
|
+
// answers 200. Measured: respond({'x-a': 1}) arrives as :status 200 with a date, where
|
|
329
|
+
// this used to throw. An informational block has no such default -- node validates a
|
|
330
|
+
// :status only if the application put one there -- and trailers have no status at all.
|
|
331
|
+
if (!trailer) {
|
|
332
|
+
if (info) {
|
|
333
|
+
if (sawStatus) {
|
|
334
|
+
pseudo.push(':status', String(status));
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
status = status || 200;
|
|
338
|
+
if (status < 200 || status > 599) {
|
|
339
|
+
throw httpError('ERR_HTTP2_STATUS_INVALID', `invalid status code: ${status}`);
|
|
340
|
+
}
|
|
341
|
+
pseudo.push(':status', String(status));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// node's rule (`internal/http2/core.js:2702`) is: no date when the application already
|
|
345
|
+
// set one, none when options.sendDate says so, and no status condition at all -- because
|
|
346
|
+
// respond() cannot carry a 1xx in the first place (node rejects :status 103 with
|
|
347
|
+
// ERR_HTTP2_STATUS_INVALID), and an informational block goes through additionalHeaders(),
|
|
348
|
+
// which asks for no date. Appended last, because that is where node's respond() puts it
|
|
349
|
+
// and header order is visible on the wire.
|
|
350
|
+
if (!trailer && !info && sendDate && status >= 200 && !hasDate) {
|
|
351
|
+
fields.push('date', httpDate());
|
|
352
|
+
}
|
|
353
|
+
if (status === 204 || status === 304 || (status >= 100 && status < 200)) {
|
|
354
|
+
for (let i = 0; i < fields.length; i += 3) {
|
|
355
|
+
if (fields[i] === 'content-length' && fields[i + 1] !== '0') {
|
|
356
|
+
throw httpError('ERR_HTTP2_PAYLOAD_FORBIDDEN',
|
|
357
|
+
`response with ${status} status must not declare a payload`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { status, pseudo, fields };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function checked(key, value) {
|
|
365
|
+
// String() on a string returns the very same string, so the call is only worth
|
|
366
|
+
// its price for the other types: the check below is the expensive half.
|
|
367
|
+
const v = typeof value === 'string' ? value : String(value);
|
|
368
|
+
if (badValue.test(v)) {
|
|
369
|
+
throw httpError('ERR_HTTP2_INVALID_HEADER_VALUE', `invalid value "${v}" for header "${key}"`);
|
|
370
|
+
}
|
|
371
|
+
return v;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The HPACK bytes for one HEADERS payload. The encoder carries a dynamic table, which
|
|
375
|
+
// is per-connection state, so the block cache lives on the session and is stamped with
|
|
376
|
+
// the table's epoch: cached bytes may name an index that a later insertion moved.
|
|
377
|
+
// The returned Buffer is a view into a shared scratch block: valid until the
|
|
378
|
+
// next call to block(), which is why every caller copies it immediately.
|
|
379
|
+
function block(headers, sensitive, opts, session) {
|
|
380
|
+
const sendDate = !!(opts && opts.sendDate);
|
|
381
|
+
const enc = session._encoder;
|
|
382
|
+
// The second the generated date belongs to, or -1 when the block carries none. A
|
|
383
|
+
// `date` block is cached for its own second and no longer: the value cannot change
|
|
384
|
+
// inside it, and across a boundary the entry has to be rebuilt rather than repeated.
|
|
385
|
+
const stamp = sendDate ? touchDate() : -1;
|
|
386
|
+
// A block carrying a pending size update is never cached: the announcement has to reach
|
|
387
|
+
// the peer with the next block rather than be swallowed by a cache hit (RFC 7541 6.3).
|
|
388
|
+
const cacheable = Object.isFrozen(headers) && enc.pendingSize === null;
|
|
389
|
+
if (cacheable) {
|
|
390
|
+
const hit = session._blocks.get(headers);
|
|
391
|
+
if (hit !== undefined && hit.epoch === enc.epoch && hit.stamp === stamp) {
|
|
392
|
+
return hit.bytes;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const update = enc.takeSizeUpdate();
|
|
397
|
+
const parsed = parse(headers, sensitive,
|
|
398
|
+
{ trailer: !!(opts && opts.trailer), sendDate, info: !!(opts && opts.info) });
|
|
399
|
+
|
|
400
|
+
// A shape reaches its final form over two or three responses, the way any HPACK
|
|
401
|
+
// encoder warms up: the first sends a line as a literal and remembers it, the second
|
|
402
|
+
// inserts it, the third references it. Only the last form is stable enough to cache,
|
|
403
|
+
// so a build that moved the table (`epoch`) or left a line pending promotion
|
|
404
|
+
// (`unsettled`) is re-encoded next response instead. Building twice in one call is
|
|
405
|
+
// not an option: the discarded bytes were the ones that performed the insertion, and
|
|
406
|
+
// dropping them would leave our table ahead of the peer's -- a desynchronisation the
|
|
407
|
+
// peer can only report as COMPRESSION_ERROR.
|
|
408
|
+
const epoch = enc.epoch;
|
|
409
|
+
// The announcement goes in front of the block, in the same buffer: the old form was
|
|
410
|
+
// Buffer.concat([update, out]) -- a second allocation, a copy of the whole block and a
|
|
411
|
+
// temporary array, on the one path where the peer changed the table size. The client
|
|
412
|
+
// does the same thing in its own _encodeRequest (lib/client-session.js).
|
|
413
|
+
const at = update === null ? 0 : update.length;
|
|
414
|
+
const end = encodeAt(parsed, enc, at);
|
|
415
|
+
if (at !== 0) {
|
|
416
|
+
update.copy(scratch, 0);
|
|
417
|
+
}
|
|
418
|
+
if (cacheable && enc.epoch === epoch && !enc.unsettled) {
|
|
419
|
+
// a cached block has to outlive the scratch, so only the cached path copies --
|
|
420
|
+
// allocUnsafe, not Buffer.from(subarray): at the sizes a response block has the
|
|
421
|
+
// pool copy is the cheaper allocation (OPTIMIZATION.md item 18)
|
|
422
|
+
const bytes = Buffer.allocUnsafe(end - at);
|
|
423
|
+
scratch.copy(bytes, 0, at, end);
|
|
424
|
+
session._blocks.set(headers, { bytes, epoch: enc.epoch, stamp });
|
|
425
|
+
}
|
|
426
|
+
return scratch.subarray(0, end);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// One pass over the parsed fields. `enc.unsettled` is reported back to block(): some
|
|
430
|
+
// line was seen for the first time and would be encoded differently next time.
|
|
431
|
+
function encode(parsed, enc) {
|
|
432
|
+
return scratch.subarray(0, encodeAt(parsed, enc, 0));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// The same pass, written at `at` instead of at the front of the scratch -- the place a
|
|
436
|
+
// size update's octets took, so that the announcement and the block are one buffer and
|
|
437
|
+
// one write. Returns the offset the block ends at.
|
|
438
|
+
function encodeAt(parsed, enc, at) {
|
|
439
|
+
enc.unsettled = false;
|
|
440
|
+
const pseudo = parsed.pseudo;
|
|
441
|
+
const fields = parsed.fields;
|
|
442
|
+
let cap = 16 + at;
|
|
443
|
+
for (let i = 0; i < pseudo.length; i += 2) {
|
|
444
|
+
cap += Encoder.maxBytesOf(pseudo[i], pseudo[i + 1]);
|
|
445
|
+
}
|
|
446
|
+
for (let i = 0; i < fields.length; i += 3) {
|
|
447
|
+
cap += Encoder.maxBytesOf(fields[i], fields[i + 1]);
|
|
448
|
+
}
|
|
449
|
+
if (scratch.length < cap) {
|
|
450
|
+
scratch = Buffer.allocUnsafe(cap);
|
|
451
|
+
}
|
|
452
|
+
let off = at;
|
|
453
|
+
for (let i = 0; i < pseudo.length; i += 2) {
|
|
454
|
+
off = enc.write(scratch, off, pseudo[i], pseudo[i + 1], false);
|
|
455
|
+
}
|
|
456
|
+
for (let i = 0; i < fields.length; i += 3) {
|
|
457
|
+
off = enc.write(scratch, off, fields[i], fields[i + 1], fields[i + 2]);
|
|
458
|
+
}
|
|
459
|
+
return off;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
module.exports = { block, encode, parse, nullProto, isFieldName, trimName, lowerName,
|
|
463
|
+
nameCacheSize, hasBadValue, CONNECTION_SPECIFIC, KSensitiveHeaders };
|