Skip to content

Commit bb2fb6c

Browse files
committed
http2: add http2.noStore headers
Add support headers that use `NGHTTP2_NV_FLAG_NO_INDEX` to avoid being indexed by the HTTP2 header compression.
1 parent 95964a1 commit bb2fb6c

File tree

7 files changed

+105
-10
lines changed

7 files changed

+105
-10
lines changed

doc/api/http2.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,15 @@ added: REPLACEME
15951595
Returns a [Settings Object][] containing the deserialized settings from the
15961596
given `Buffer` as generated by `http2.getPackedSettings()`.
15971597

1598+
### http2.noStore
1599+
<!-- YAML
1600+
added: REPLACEME
1601+
-->
1602+
1603+
* {symbol}
1604+
1605+
See [Headers Object][].
1606+
15981607
### Headers Object
15991608

16001609
Headers are represented as own-properties on JavaScript objects. The property
@@ -1628,6 +1637,24 @@ server.on('stream', (stream, headers) => {
16281637
});
16291638
```
16301639

1640+
The HTTP/2 header compression mechanism allows the sender to decide, on a
1641+
header-by-header basis, whether or not the header should be stored in the
1642+
stateful header compression table. To leverage this feature, the `http2.noStore`
1643+
symbol key on the `headers` object can be used to provide more headers in the
1644+
same format:
1645+
1646+
```
1647+
const headers = {
1648+
':status': '200',
1649+
'content-type': 'text-plain',
1650+
[http2.noStore]: {
1651+
'ABC': ['has', 'more', 'than', 'one', 'value']
1652+
}
1653+
};
1654+
1655+
stream.respond(headers);
1656+
```
1657+
16311658
### Settings Object
16321659

16331660
The `http2.getDefaultSettings()`, `http2.getPackedSettings()`,

lib/http2.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const {
1313
getUnpackedSettings,
1414
createServer,
1515
createSecureServer,
16-
connect
16+
connect,
17+
noStore
1718
} = require('internal/http2/core');
1819

1920
module.exports = {
@@ -23,5 +24,6 @@ module.exports = {
2324
getUnpackedSettings,
2425
createServer,
2526
createSecureServer,
26-
connect
27+
connect,
28+
noStore
2729
};

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ E('ERR_HTTP2_PAYLOAD_FORBIDDEN',
166166
(code) => `Responses with ${code} status must not have a payload`);
167167
E('ERR_HTTP2_OUT_OF_STREAMS',
168168
'No stream ID is available because maximum stream ID has been reached');
169+
E('ERR_HTTP2_PSEUDOHEADER_INVALID_FLAGS',
170+
(name) => `HTTP/2 pseudo-header "${name}" cannot be used with http2.noStore`);
169171
E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers');
170172
E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams');
171173
E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent');

lib/internal/http2/core.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
getStreamState,
3434
isPayloadMeaningless,
3535
mapToHeaders,
36+
noStore,
3637
NghttpError,
3738
toHeaderObject,
3839
updateOptionsBuffer,
@@ -2546,7 +2547,8 @@ module.exports = {
25462547
getUnpackedSettings,
25472548
createServer,
25482549
createSecureServer,
2549-
connect
2550+
connect,
2551+
noStore
25502552
};
25512553

25522554
/* eslint-enable no-use-before-define */

lib/internal/http2/util.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,16 @@ const {
4848

4949
HTTP2_METHOD_DELETE,
5050
HTTP2_METHOD_GET,
51-
HTTP2_METHOD_HEAD
51+
HTTP2_METHOD_HEAD,
52+
53+
NGHTTP2_NV_FLAG_NONE,
54+
NGHTTP2_NV_FLAG_NO_INDEX
5255
} = binding.constants;
5356

57+
const HEADER_DEFAULT = String.fromCharCode(NGHTTP2_NV_FLAG_NONE);
58+
const HEADER_NO_STORE = String.fromCharCode(NGHTTP2_NV_FLAG_NO_INDEX);
59+
const noStore = Symbol('http2.noStore');
60+
5461
// This set is defined strictly by the HTTP/2 specification. Only
5562
// :-prefixed headers defined by that specification may be added to
5663
// this set.
@@ -374,11 +381,12 @@ function assertValidPseudoHeaderTrailer(key) {
374381
}
375382

376383
function mapToHeaders(map,
377-
assertValuePseudoHeader = assertValidPseudoHeader) {
384+
assertValuePseudoHeader = assertValidPseudoHeader,
385+
flag = HEADER_DEFAULT,
386+
singles = new Set()) {
378387
let ret = '';
379388
let count = 0;
380389
const keys = Object.keys(map);
381-
const singles = new Set();
382390
for (var i = 0; i < keys.length; i++) {
383391
let key = keys[i];
384392
let value = map[key];
@@ -403,7 +411,9 @@ function mapToHeaders(map,
403411
const err = assertValuePseudoHeader(key);
404412
if (err !== undefined)
405413
return err;
406-
ret = `${key}\0${String(value)}\0${ret}`;
414+
// The first byte is always 0, passing NGHTTP2_NV_FLAG_NO_INDEX
415+
// does not make sense for pseudo-headers.
416+
ret = `\0${key}\0${String(value)}\0${ret}`;
407417
count++;
408418
} else {
409419
if (kSingleValueHeaders.has(key)) {
@@ -417,17 +427,30 @@ function mapToHeaders(map,
417427
if (isArray) {
418428
for (var k = 0; k < value.length; k++) {
419429
val = String(value[k]);
420-
ret += `${key}\0${val}\0`;
430+
ret += `${flag}${key}\0${val}\0`;
421431
}
422432
count += value.length;
423433
} else {
424434
val = String(value);
425-
ret += `${key}\0${val}\0`;
435+
ret += `${flag}${key}\0${val}\0`;
426436
count++;
427437
}
428438
}
429439
}
430440

441+
const noStoreMap = map[noStore];
442+
if (noStoreMap !== undefined) {
443+
const info = mapToHeaders(
444+
noStoreMap,
445+
(key) => new errors.Error('ERR_HTTP2_PSEUDOHEADER_INVALID_FLAGS', key),
446+
HEADER_NO_STORE,
447+
singles);
448+
if (!Array.isArray(info))
449+
return info;
450+
ret += info[0];
451+
count += info[1];
452+
}
453+
431454
return [ret, count];
432455
}
433456

@@ -510,6 +533,7 @@ module.exports = {
510533
isPayloadMeaningless,
511534
mapToHeaders,
512535
NghttpError,
536+
noStore,
513537
toHeaderObject,
514538
updateOptionsBuffer,
515539
updateSettingsBuffer

src/node_http2.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,7 @@ Headers::Headers(Isolate* isolate,
11261126
return;
11271127
}
11281128

1129-
nva[n].flags = NGHTTP2_NV_FLAG_NONE;
1129+
nva[n].flags = *(p++);
11301130
nva[n].name = reinterpret_cast<uint8_t*>(p);
11311131
nva[n].namelen = strlen(p);
11321132
p += nva[n].namelen + 1;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Flags: --expose-http2
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
const assert = require('assert');
8+
const http2 = require('http2');
9+
10+
const server = http2.createServer();
11+
12+
const src = {
13+
'regular-header': 'foo',
14+
[http2.noStore]: {
15+
'unindexed-header': 'A'.repeat(1000)
16+
}
17+
};
18+
19+
function checkHeaders(headers) {
20+
assert.strictEqual(headers['regular-header'], 'foo');
21+
assert.strictEqual(headers['unindexed-header'], 'A'.repeat(1000));
22+
}
23+
24+
server.on('stream', common.mustCall((stream, headers) => {
25+
checkHeaders(headers);
26+
stream.respond(src);
27+
stream.end();
28+
}));
29+
30+
server.listen(0, common.mustCall(() => {
31+
const client = http2.connect(`http://localhost:${server.address().port}`);
32+
const req = client.request(src);
33+
req.on('response', common.mustCall(checkHeaders));
34+
req.on('streamClosed', common.mustCall(() => {
35+
server.close();
36+
client.destroy();
37+
}));
38+
}));

0 commit comments

Comments
 (0)