Skip to content
This repository has been archived by the owner on Jul 6, 2018. It is now read-only.

Commit

Permalink
http2: add getUnpackedSettings and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Jul 13, 2017
1 parent 7ea8c07 commit 476c288
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 44 deletions.
81 changes: 46 additions & 35 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1233,41 +1233,6 @@ server.on('stream', (stream, headers, flags) => {
added: REPLACEME
-->

The `'timeout'` event is emitted when there is no activity on the Server for
a given number of milliseconds set using `http2server.setTimeout()`.

### http2.getDefaultSettings()
<!-- YAML
added: REPLACEME
-->

* Returns: {[Settings Object][]}

Returns an object containing the default settings for an `Http2Session`
instance. This method returns a new object instance every time it is called
so instances returned may be safely modified for use.

### http2.getPackedSettings(settings)
<!-- YAML
added: REPLACEME
-->

* `settings` {[Settings Object][]}
* Returns: {Buffer}

Returns a [Buffer][] instance containing serialized representation of the given
HTTP/2 settings as specified in the [HTTP/2][] specification. This is intended
for use with the `HTTP2-Settings` header field.

```js
const http2 = require('http2');

const packed = http2.getPackedSettings({ enablePush: false });

console.log(packed.toString('base64'));
// Prints: AAIAAAAA
```

### http2.createServer(options[, onRequestHandler])
<!-- YAML
added: REPLACEME
Expand Down Expand Up @@ -1464,6 +1429,52 @@ added: REPLACEME
| 0x0c | Inadequate Security | `http2.constants.NGHTTP2_INADEQUATE_SECURITY` |
| 0x0d | HTTP/1.1 Required | `http2.constants.NGHTTP2_HTTP_1_1_REQUIRED` |

The `'timeout'` event is emitted when there is no activity on the Server for
a given number of milliseconds set using `http2server.setTimeout()`.

### http2.getDefaultSettings()
<!-- YAML
added: REPLACEME
-->

* Returns: {[Settings Object][]}

Returns an object containing the default settings for an `Http2Session`
instance. This method returns a new object instance every time it is called
so instances returned may be safely modified for use.

### http2.getPackedSettings(settings)
<!-- YAML
added: REPLACEME
-->

* `settings` {[Settings Object][]}
* Returns: {Buffer}

Returns a [Buffer][] instance containing serialized representation of the given
HTTP/2 settings as specified in the [HTTP/2][] specification. This is intended
for use with the `HTTP2-Settings` header field.

```js
const http2 = require('http2');

const packed = http2.getPackedSettings({ enablePush: false });

console.log(packed.toString('base64'));
// Prints: AAIAAAAA
```

### http2.getUnpackedSettings(buf)
<!-- YAML
added: REPLACEME
-->

* `buf` {Buffer|Uint8Array} The packed settings
* Returns: {[Settings Object][]}

Returns a [Settings Object][] containing the deserialized settings from the
given `Buffer` as generated by `http2.getPackedSettings()`.

### Headers Object

Headers are represented as own-properties on JavaScript objects. The property
Expand Down
2 changes: 2 additions & 0 deletions lib/http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
constants,
getDefaultSettings,
getPackedSettings,
getUnpackedSettings,
createServer,
createSecureServer,
connect
Expand All @@ -19,6 +20,7 @@ module.exports = {
constants,
getDefaultSettings,
getPackedSettings,
getUnpackedSettings,
createServer,
createSecureServer,
connect
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ E('ERR_HTTP2_INVALID_CONNECTION_HEADERS',
E('ERR_HTTP2_INVALID_HEADER_VALUE', 'Value must not be undefined or null');
E('ERR_HTTP2_INVALID_INFO_STATUS',
(code) => `Invalid informational status code: ${code}`);
E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
'Packed settings length must be a multiple of six');
E('ERR_HTTP2_INVALID_PSEUDOHEADER',
(name) => `"${name}" is an invalid pseudoheader or is used incorrectly`);
E('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed');
Expand Down
73 changes: 73 additions & 0 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { URL } = require('url');
const { onServerStream } = require('internal/http2/compat');
const { utcDate } = require('internal/http');
const { _connectionListener: httpConnectionListener } = require('http');
const { isUint8Array } = process.binding('util');

const {
assertIsObject,
Expand Down Expand Up @@ -97,6 +98,13 @@ const {
HTTP2_HEADER_STATUS,
HTTP2_HEADER_CONTENT_LENGTH,

NGHTTP2_SETTINGS_HEADER_TABLE_SIZE,
NGHTTP2_SETTINGS_ENABLE_PUSH,
NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE,
NGHTTP2_SETTINGS_MAX_FRAME_SIZE,
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE,

HTTP2_METHOD_GET,
HTTP2_METHOD_HEAD,
HTTP2_METHOD_CONNECT,
Expand Down Expand Up @@ -2289,11 +2297,76 @@ function getPackedSettings(settings) {
return binding.packSettings();
}

function getUnpackedSettings(buf, options = {}) {
if (!isUint8Array(buf)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buf',
['Buffer', 'Uint8Array']);
}
if (buf.length % 6 !== 0)
throw new errors.RangeError('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH');
const settings = Object.create(null);
let offset = 0;
while (offset < buf.length) {
const id = buf.readUInt16BE(offset);
offset += 2;
const value = buf.readUInt32BE(offset);
switch (id) {
case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE:
settings.headerTableSize = value;
break;
case NGHTTP2_SETTINGS_ENABLE_PUSH:
settings.enablePush = Boolean(value);
break;
case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS:
settings.maxConcurrentStreams = value;
break;
case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE:
settings.initialWindowSize = value;
break;
case NGHTTP2_SETTINGS_MAX_FRAME_SIZE:
settings.maxFrameSize = value;
break;
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
settings.maxHeaderListSize = value;
break;
}
offset += 4;
}

if (options != null && options.validate) {
assertWithinRange('headerTableSize',
settings.headerTableSize,
0, 2 ** 32 - 1);
assertWithinRange('initialWindowSize',
settings.initialWindowSize,
0, 2 ** 32 - 1);
assertWithinRange('maxFrameSize',
settings.maxFrameSize,
16384, 2 ** 24 - 1);
assertWithinRange('maxConcurrentStreams',
settings.maxConcurrentStreams,
0, 2 ** 31 - 1);
assertWithinRange('maxHeaderListSize',
settings.maxHeaderListSize,
0, 2 ** 32 - 1);
if (settings.enablePush !== undefined &&
typeof settings.enablePush !== 'boolean') {
const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE',
'enablePush', settings.enablePush);
err.actual = settings.enablePush;
throw err;
}
}

return settings;
}

// Exports
module.exports = {
constants,
getDefaultSettings,
getPackedSettings,
getUnpackedSettings,
createServer,
createSecureServer,
connect
Expand Down
7 changes: 7 additions & 0 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,13 @@ void Initialize(Local<Object> target,
NODE_DEFINE_CONSTANT(constants, MAX_INITIAL_WINDOW_SIZE);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_DEFAULT_WEIGHT);

NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_ENABLE_PUSH);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);

NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE);
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_MAX);
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_CALLBACK);
Expand Down
9 changes: 0 additions & 9 deletions test/parallel/test-http2-binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

require('../common');
const assert = require('assert');
const Buffer = require('buffer').Buffer;

assert.doesNotThrow(() => process.binding('http2'));

Expand All @@ -22,14 +21,6 @@ assert.strictEqual(settings.maxFrameSize, 16384);
assert.strictEqual(binding.nghttp2ErrorString(-517),
'GOAWAY has already been sent');

const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x05,
0x00, 0x00, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00,
0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);
const val = http2.getPackedSettings(http2.getDefaultSettings());
assert.deepStrictEqual(val, check);

assert.doesNotThrow(() => assert(Buffer.isBuffer(http2.getPackedSettings())));

// assert constants are present
assert(binding.constants);
assert.strictEqual(typeof binding.constants, 'object');
Expand Down
131 changes: 131 additions & 0 deletions test/parallel/test-http2-getpackedsettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Flags: --expose-http2
'use strict';

const common = require('../common');
const assert = require('assert');
const http2 = require('http2');

const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x05,
0x00, 0x00, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00,
0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);
const val = http2.getPackedSettings(http2.getDefaultSettings());
assert.deepStrictEqual(val, check);

[
['headerTableSize', 0],
['headerTableSize', 2 ** 32 - 1],
['initialWindowSize', 0],
['initialWindowSize', 2 ** 32 - 1],
['maxFrameSize', 16384],
['maxFrameSize', 2 ** 24 - 1],
['maxConcurrentStreams', 0],
['maxConcurrentStreams', 2 ** 31 - 1],
['maxHeaderListSize', 0],
['maxHeaderListSize', 2 ** 32 - 1]
].forEach((i) => {
assert.doesNotThrow(() => http2.getPackedSettings({ [i[0]]: i[1] }));
});

assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: true }));
assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false }));

[
['headerTableSize', -1],
['headerTableSize', 2 ** 32],
['initialWindowSize', -1],
['initialWindowSize', 2 ** 32],
['maxFrameSize', 16383],
['maxFrameSize', 2 ** 24],
['maxConcurrentStreams', -1],
['maxConcurrentStreams', 2 ** 31],
['maxHeaderListSize', -1],
['maxHeaderListSize', 2 ** 32]
].forEach((i) => {
assert.throws(() => {
http2.getPackedSettings({ [i[0]]: i[1] });
}, common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: RangeError,
message: `Invalid value for setting "${i[0]}": ${i[1]}`
}));
});

[
1, null, '', Infinity, new Date(), {}, NaN, [false]
].forEach((i) => {
assert.throws(() => {
http2.getPackedSettings({ enablePush: i });
}, common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: TypeError,
message: `Invalid value for setting "enablePush": ${i}`
}));
});

{
const check = Buffer.from([
0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00,
0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04,
0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64,
0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);

const packed = http2.getPackedSettings({
headerTableSize: 100,
initialWindowSize: 100,
maxFrameSize: 20000,
maxConcurrentStreams: 200,
maxHeaderListSize: 100,
enablePush: true,
foo: 'ignored'
});
assert.strictEqual(packed.length, 36);
assert.deepStrictEqual(packed, check);
}

{
const packed = Buffer.from([
0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00,
0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04,
0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64,
0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);

[1, true, '', [], {}, NaN].forEach((i) => {
assert.throws(() => {
http2.getUnpackedSettings(i);
}, common.expectsError({
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "buf" argument must be one of type Buffer or Uint8Array'
}));
});

assert.throws(() => {
http2.getUnpackedSettings(packed.slice(5));
}, common.expectsError({
code: 'ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH',
type: RangeError,
message: 'Packed settings length must be a multiple of six'
}));

const settings = http2.getUnpackedSettings(packed);

assert(settings);
assert.strictEqual(settings.headerTableSize, 100);
assert.strictEqual(settings.initialWindowSize, 100);
assert.strictEqual(settings.maxFrameSize, 20000);
assert.strictEqual(settings.maxConcurrentStreams, 200);
assert.strictEqual(settings.maxHeaderListSize, 100);
assert.strictEqual(settings.enablePush, true);
}

{
const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]);

assert.throws(() => {
http2.getUnpackedSettings(packed, {validate: true});
}, common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: RangeError,
message: 'Invalid value for setting "maxConcurrentStreams": 4294967295'
}));
}

0 comments on commit 476c288

Please sign in to comment.