Skip to content

Commit

Permalink
http2: add http2.noStore headers
Browse files Browse the repository at this point in the history
Add support headers that use `NGHTTP2_NV_FLAG_NO_INDEX` to avoid
being indexed by the HTTP2 header compression.
  • Loading branch information
addaleax committed Aug 10, 2017
1 parent 95964a1 commit bb2fb6c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 10 deletions.
27 changes: 27 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,15 @@ added: REPLACEME
Returns a [Settings Object][] containing the deserialized settings from the
given `Buffer` as generated by `http2.getPackedSettings()`.

### http2.noStore
<!-- YAML
added: REPLACEME
-->

* {symbol}

See [Headers Object][].

### Headers Object

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

The HTTP/2 header compression mechanism allows the sender to decide, on a
header-by-header basis, whether or not the header should be stored in the
stateful header compression table. To leverage this feature, the `http2.noStore`
symbol key on the `headers` object can be used to provide more headers in the
same format:

```
const headers = {
':status': '200',
'content-type': 'text-plain',
[http2.noStore]: {
'ABC': ['has', 'more', 'than', 'one', 'value']
}
};
stream.respond(headers);
```

### Settings Object

The `http2.getDefaultSettings()`, `http2.getPackedSettings()`,
Expand Down
6 changes: 4 additions & 2 deletions lib/http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const {
getUnpackedSettings,
createServer,
createSecureServer,
connect
connect,
noStore
} = require('internal/http2/core');

module.exports = {
Expand All @@ -23,5 +24,6 @@ module.exports = {
getUnpackedSettings,
createServer,
createSecureServer,
connect
connect,
noStore
};
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ E('ERR_HTTP2_PAYLOAD_FORBIDDEN',
(code) => `Responses with ${code} status must not have a payload`);
E('ERR_HTTP2_OUT_OF_STREAMS',
'No stream ID is available because maximum stream ID has been reached');
E('ERR_HTTP2_PSEUDOHEADER_INVALID_FLAGS',
(name) => `HTTP/2 pseudo-header "${name}" cannot be used with http2.noStore`);
E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers');
E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams');
E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent');
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {
getStreamState,
isPayloadMeaningless,
mapToHeaders,
noStore,
NghttpError,
toHeaderObject,
updateOptionsBuffer,
Expand Down Expand Up @@ -2546,7 +2547,8 @@ module.exports = {
getUnpackedSettings,
createServer,
createSecureServer,
connect
connect,
noStore
};

/* eslint-enable no-use-before-define */
36 changes: 30 additions & 6 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ const {

HTTP2_METHOD_DELETE,
HTTP2_METHOD_GET,
HTTP2_METHOD_HEAD
HTTP2_METHOD_HEAD,

NGHTTP2_NV_FLAG_NONE,
NGHTTP2_NV_FLAG_NO_INDEX
} = binding.constants;

const HEADER_DEFAULT = String.fromCharCode(NGHTTP2_NV_FLAG_NONE);
const HEADER_NO_STORE = String.fromCharCode(NGHTTP2_NV_FLAG_NO_INDEX);
const noStore = Symbol('http2.noStore');

// This set is defined strictly by the HTTP/2 specification. Only
// :-prefixed headers defined by that specification may be added to
// this set.
Expand Down Expand Up @@ -374,11 +381,12 @@ function assertValidPseudoHeaderTrailer(key) {
}

function mapToHeaders(map,
assertValuePseudoHeader = assertValidPseudoHeader) {
assertValuePseudoHeader = assertValidPseudoHeader,
flag = HEADER_DEFAULT,
singles = new Set()) {
let ret = '';
let count = 0;
const keys = Object.keys(map);
const singles = new Set();
for (var i = 0; i < keys.length; i++) {
let key = keys[i];
let value = map[key];
Expand All @@ -403,7 +411,9 @@ function mapToHeaders(map,
const err = assertValuePseudoHeader(key);
if (err !== undefined)
return err;
ret = `${key}\0${String(value)}\0${ret}`;
// The first byte is always 0, passing NGHTTP2_NV_FLAG_NO_INDEX
// does not make sense for pseudo-headers.
ret = `\0${key}\0${String(value)}\0${ret}`;
count++;
} else {
if (kSingleValueHeaders.has(key)) {
Expand All @@ -417,17 +427,30 @@ function mapToHeaders(map,
if (isArray) {
for (var k = 0; k < value.length; k++) {
val = String(value[k]);
ret += `${key}\0${val}\0`;
ret += `${flag}${key}\0${val}\0`;
}
count += value.length;
} else {
val = String(value);
ret += `${key}\0${val}\0`;
ret += `${flag}${key}\0${val}\0`;
count++;
}
}
}

const noStoreMap = map[noStore];
if (noStoreMap !== undefined) {
const info = mapToHeaders(
noStoreMap,
(key) => new errors.Error('ERR_HTTP2_PSEUDOHEADER_INVALID_FLAGS', key),
HEADER_NO_STORE,
singles);
if (!Array.isArray(info))
return info;
ret += info[0];
count += info[1];
}

return [ret, count];
}

Expand Down Expand Up @@ -510,6 +533,7 @@ module.exports = {
isPayloadMeaningless,
mapToHeaders,
NghttpError,
noStore,
toHeaderObject,
updateOptionsBuffer,
updateSettingsBuffer
Expand Down
2 changes: 1 addition & 1 deletion src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ Headers::Headers(Isolate* isolate,
return;
}

nva[n].flags = NGHTTP2_NV_FLAG_NONE;
nva[n].flags = *(p++);
nva[n].name = reinterpret_cast<uint8_t*>(p);
nva[n].namelen = strlen(p);
p += nva[n].namelen + 1;
Expand Down
38 changes: 38 additions & 0 deletions test/parallel/test-http2-nostore-header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Flags: --expose-http2
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');

const server = http2.createServer();

const src = {
'regular-header': 'foo',
[http2.noStore]: {
'unindexed-header': 'A'.repeat(1000)
}

This comment has been minimized.

Copy link
@jasnell

jasnell Aug 11, 2017

Collaborator

wondering if we should test the following case also:

{
  'some-header': 'foo',
  [http2.noStore]: {
    'some-header': 'bar'
  }
}

On the other side, both header values should come through.

};

function checkHeaders(headers) {
assert.strictEqual(headers['regular-header'], 'foo');
assert.strictEqual(headers['unindexed-header'], 'A'.repeat(1000));
}

server.on('stream', common.mustCall((stream, headers) => {
checkHeaders(headers);
stream.respond(src);
stream.end();
}));

server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request(src);
req.on('response', common.mustCall(checkHeaders));
req.on('streamClosed', common.mustCall(() => {
server.close();
client.destroy();
}));
}));

0 comments on commit bb2fb6c

Please sign in to comment.