From 516881e22803142690bf2f6c5eff5aebdda669f1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 24 Nov 2017 12:43:05 -0800 Subject: [PATCH 1/7] http2: use correct connect event for TLS Socket --- lib/internal/http2/core.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 39f67c482382b2..d00ed6044183a5 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -707,7 +707,9 @@ class Http2Session extends EventEmitter { const setupFn = setupHandle(this, socket, type, options); if (socket.connecting) { this[kState].connecting = true; - socket.once('connect', setupFn); + const connectEvent = + socket instanceof tls.TLSSocket ? 'secureConnect' : 'connect'; + socket.once(connectEvent, setupFn); } else { setupFn(); } From a21f2c9009914777a4b11dd2f88141a5dca91d91 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 25 Nov 2017 08:26:46 -0800 Subject: [PATCH 2/7] http2: general cleanups --- lib/internal/http2/core.js | 49 +++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index d00ed6044183a5..5a28ee7492a0df 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -28,6 +28,8 @@ const { _connectionListener: httpConnectionListener } = require('http'); const { createPromise, promiseResolve } = process.binding('util'); const debug = util.debuglog('http2'); +const kMaxFrameSize = (2 ** 24) - 1; +const kMaxInt = (2 ** 32) - 1; const kMaxStreams = (2 ** 31) - 1; const { @@ -332,9 +334,9 @@ function emitGoaway(self, code, lastStreamID, buf) { return; if (!state.shuttingDown && !state.shutdown) { self.shutdown({}, self.destroy.bind(self)); - } else { - self.destroy(); + return; } + self.destroy(); } // Called by the native layer when a goaway frame has been received @@ -582,14 +584,15 @@ function doShutdown(options) { function submitShutdown(options) { const type = this[kType]; debug(`Http2Session ${sessionName(type)}: submitting shutdown request`); + const fn = doShutdown.bind(this, options); if (type === NGHTTP2_SESSION_SERVER && options.graceful === true) { // first send a shutdown notice this[kHandle].shutdownNotice(); // then, on flip of the event loop, do the actual shutdown - setImmediate(doShutdown.bind(this), options); - } else { - doShutdown.call(this, options); + setImmediate(fn); + return; } + fn(); } function finishSessionDestroy(socket) { @@ -844,19 +847,19 @@ class Http2Session extends EventEmitter { settings = Object.assign(Object.create(null), settings); assertWithinRange('headerTableSize', settings.headerTableSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); assertWithinRange('initialWindowSize', settings.initialWindowSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); assertWithinRange('maxFrameSize', settings.maxFrameSize, - 16384, 2 ** 24 - 1); + 16384, kMaxFrameSize); assertWithinRange('maxConcurrentStreams', settings.maxConcurrentStreams, 0, kMaxStreams); assertWithinRange('maxHeaderListSize', settings.maxHeaderListSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); if (settings.enablePush !== undefined && typeof settings.enablePush !== 'boolean') { const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE', @@ -871,11 +874,12 @@ class Http2Session extends EventEmitter { debug(`Http2Session ${sessionName(this[kType])}: sending settings`); state.pendingAck++; + const fn = submitSettings.bind(this, settings); if (state.connecting) { - this.once('connect', submitSettings.bind(this, settings)); + this.once('connect', fn); return; } - submitSettings.call(this, settings); + fn(); } // Destroy the Http2Session @@ -961,13 +965,14 @@ class Http2Session extends EventEmitter { this.on('shutdown', callback); } + const fn = submitShutdown.bind(this, options); if (state.connecting) { - this.once('connect', submitShutdown.bind(this, options)); + this.once('connect', fn); return; } debug(`Http2Session ${sessionName(type)}: sending shutdown`); - submitShutdown.call(this, options); + fn(); } _onTimeout() { @@ -1368,7 +1373,7 @@ class Http2Stream extends Duplex { rstStream(code = NGHTTP2_NO_ERROR) { if (typeof code !== 'number') throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number'); - if (code < 0 || code > 2 ** 32 - 1) + if (code < 0 || code > kMaxInt) throw new errors.RangeError('ERR_OUT_OF_RANGE', 'code'); const fn = submitRstStream.bind(this, code); @@ -2362,19 +2367,19 @@ function getPackedSettings(settings) { settings = settings || Object.create(null); assertWithinRange('headerTableSize', settings.headerTableSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); assertWithinRange('initialWindowSize', settings.initialWindowSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); assertWithinRange('maxFrameSize', settings.maxFrameSize, - 16384, 2 ** 24 - 1); + 16384, kMaxFrameSize); assertWithinRange('maxConcurrentStreams', settings.maxConcurrentStreams, 0, kMaxStreams); assertWithinRange('maxHeaderListSize', settings.maxHeaderListSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); if (settings.enablePush !== undefined && typeof settings.enablePush !== 'boolean') { const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE', @@ -2425,22 +2430,22 @@ function getUnpackedSettings(buf, options = {}) { if (options != null && options.validate) { assertWithinRange('headerTableSize', settings.headerTableSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); assertWithinRange('enablePush', settings.enablePush, 0, 1); assertWithinRange('initialWindowSize', settings.initialWindowSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); assertWithinRange('maxFrameSize', settings.maxFrameSize, - 16384, 2 ** 24 - 1); + 16384, kMaxFrameSize); assertWithinRange('maxConcurrentStreams', settings.maxConcurrentStreams, 0, kMaxStreams); assertWithinRange('maxHeaderListSize', settings.maxHeaderListSize, - 0, 2 ** 32 - 1); + 0, kMaxInt); } if (settings.enablePush !== undefined) { From 48514ce1c4a8dcf53b354e6ec171f6c680ae3548 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 25 Nov 2017 12:44:07 -0800 Subject: [PATCH 3/7] http2: reduce code duplication in settings --- lib/internal/http2/core.js | 106 ++++++------------ test/parallel/test-http2-getpackedsettings.js | 10 +- 2 files changed, 35 insertions(+), 81 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 5a28ee7492a0df..4225c9c2439c84 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -661,6 +661,33 @@ function pingCallback(cb) { }; } +function validateSettings(settings) { + settings = Object.assign({}, settings); + assertWithinRange('headerTableSize', + settings.headerTableSize, + 0, kMaxInt); + assertWithinRange('initialWindowSize', + settings.initialWindowSize, + 0, kMaxInt); + assertWithinRange('maxFrameSize', + settings.maxFrameSize, + 16384, kMaxFrameSize); + assertWithinRange('maxConcurrentStreams', + settings.maxConcurrentStreams, + 0, kMaxStreams); + assertWithinRange('maxHeaderListSize', + settings.maxHeaderListSize, + 0, kMaxInt); + 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; +} + // Upon creation, the Http2Session takes ownership of the socket. The session // may not be ready to use immediately if the socket is not yet fully connected. class Http2Session extends EventEmitter { @@ -844,29 +871,7 @@ class Http2Session extends EventEmitter { // Validate the input first assertIsObject(settings, 'settings'); - settings = Object.assign(Object.create(null), settings); - assertWithinRange('headerTableSize', - settings.headerTableSize, - 0, kMaxInt); - assertWithinRange('initialWindowSize', - settings.initialWindowSize, - 0, kMaxInt); - assertWithinRange('maxFrameSize', - settings.maxFrameSize, - 16384, kMaxFrameSize); - assertWithinRange('maxConcurrentStreams', - settings.maxConcurrentStreams, - 0, kMaxStreams); - assertWithinRange('maxHeaderListSize', - settings.maxHeaderListSize, - 0, kMaxInt); - 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; - } + settings = validateSettings(settings); if (state.pendingAck === state.maxPendingAck) { throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', this[kState].pendingAck); @@ -2364,30 +2369,7 @@ function createServer(options, handler) { // HTTP2-Settings header frame. function getPackedSettings(settings) { assertIsObject(settings, 'settings'); - settings = settings || Object.create(null); - assertWithinRange('headerTableSize', - settings.headerTableSize, - 0, kMaxInt); - assertWithinRange('initialWindowSize', - settings.initialWindowSize, - 0, kMaxInt); - assertWithinRange('maxFrameSize', - settings.maxFrameSize, - 16384, kMaxFrameSize); - assertWithinRange('maxConcurrentStreams', - settings.maxConcurrentStreams, - 0, kMaxStreams); - assertWithinRange('maxHeaderListSize', - settings.maxHeaderListSize, - 0, kMaxInt); - 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; - } - updateSettingsBuffer(settings); + updateSettingsBuffer(validateSettings(settings)); return binding.packSettings(); } @@ -2398,7 +2380,7 @@ function getUnpackedSettings(buf, options = {}) { } if (buf.length % 6 !== 0) throw new errors.RangeError('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH'); - const settings = Object.create(null); + const settings = {}; let offset = 0; while (offset < buf.length) { const id = buf.readUInt16BE(offset); @@ -2409,7 +2391,7 @@ function getUnpackedSettings(buf, options = {}) { settings.headerTableSize = value; break; case NGHTTP2_SETTINGS_ENABLE_PUSH: - settings.enablePush = value; + settings.enablePush = value !== 0; break; case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: settings.maxConcurrentStreams = value; @@ -2427,30 +2409,8 @@ function getUnpackedSettings(buf, options = {}) { offset += 4; } - if (options != null && options.validate) { - assertWithinRange('headerTableSize', - settings.headerTableSize, - 0, kMaxInt); - assertWithinRange('enablePush', - settings.enablePush, - 0, 1); - assertWithinRange('initialWindowSize', - settings.initialWindowSize, - 0, kMaxInt); - assertWithinRange('maxFrameSize', - settings.maxFrameSize, - 16384, kMaxFrameSize); - assertWithinRange('maxConcurrentStreams', - settings.maxConcurrentStreams, - 0, kMaxStreams); - assertWithinRange('maxHeaderListSize', - settings.maxHeaderListSize, - 0, kMaxInt); - } - - if (settings.enablePush !== undefined) { - settings.enablePush = !!settings.enablePush; - } + if (options != null && options.validate) + validateSettings(settings); return settings; } diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js index 7461176c5fcde7..16c84913893d0c 100644 --- a/test/parallel/test-http2-getpackedsettings.js +++ b/test/parallel/test-http2-getpackedsettings.js @@ -128,7 +128,6 @@ assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false })); assert.strictEqual(settings.enablePush, true); } -//should throw if enablePush is not 0 or 1 { const packed = Buffer.from([ 0x00, 0x02, 0x00, 0x00, 0x00, 0x00]); @@ -140,13 +139,8 @@ assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false })); const packed = Buffer.from([ 0x00, 0x02, 0x00, 0x00, 0x00, 0x64]); - assert.throws(() => { - http2.getUnpackedSettings(packed, { validate: true }); - }, common.expectsError({ - code: 'ERR_HTTP2_INVALID_SETTING_VALUE', - type: RangeError, - message: 'Invalid value for setting "enablePush": 100' - })); + const settings = http2.getUnpackedSettings(packed, { validate: true }); + assert.strictEqual(settings.enablePush, true); } //check for what happens if passing {validate: true} and no errors happen From e7ad83de34c67da9a18aa95df123facc71b9263d Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 25 Nov 2017 13:02:16 -0800 Subject: [PATCH 4/7] http2: use 'close' event instead of 'streamClosed' --- doc/api/http2.md | 26 +++++++++---------- lib/internal/http2/compat.js | 4 +-- lib/internal/http2/core.js | 8 +++--- .../test-http2-client-http1-server.js | 2 +- ...t-http2-client-rststream-before-connect.js | 2 +- ...p2-client-stream-destroy-before-connect.js | 2 +- .../test-http2-client-unescaped-path.js | 2 +- .../test-http2-compat-serverresponse-end.js | 4 +-- test/parallel/test-http2-compat-socket.js | 4 +-- test/parallel/test-http2-multiheaders-raw.js | 2 +- test/parallel/test-http2-multiheaders.js | 2 +- ...test-http2-options-max-reserved-streams.js | 6 ++--- .../test-http2-respond-file-errors.js | 2 +- .../test-http2-respond-file-fd-errors.js | 2 +- .../test-http2-respond-file-fd-range.js | 4 +-- test/parallel/test-http2-respond-no-data.js | 2 +- .../parallel/test-http2-response-splitting.js | 2 +- .../test-http2-server-rst-before-respond.js | 2 +- test/parallel/test-http2-server-rst-stream.js | 2 +- .../parallel/test-http2-server-socketerror.js | 2 +- test/parallel/test-http2-stream-client.js | 2 +- .../test-http2-stream-destroy-event-order.js | 2 +- test/parallel/test-http2-too-large-headers.js | 2 +- test/parallel/test-http2-too-many-headers.js | 2 +- test/parallel/test-http2-write-callbacks.js | 2 +- 25 files changed, 45 insertions(+), 47 deletions(-) diff --git a/doc/api/http2.md b/doc/api/http2.md index 3500c563a2782a..51d2bec2a913f0 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -633,7 +633,7 @@ All [`Http2Stream`][] instances are destroyed either when: When an `Http2Stream` instance is destroyed, an attempt will be made to send an `RST_STREAM` frame will be sent to the connected peer. -Once the `Http2Stream` instance is destroyed, the `'streamClosed'` event will +When the `Http2Stream` instance is destroyed, the `'close'` event will be emitted. Because `Http2Stream` is an instance of `stream.Duplex`, the `'end'` event will also be emitted if the stream data is currently flowing. The `'error'` event may also be emitted if `http2stream.destroy()` was called @@ -655,6 +655,18 @@ abnormally aborted in mid-communication. *Note*: The `'aborted'` event will only be emitted if the `Http2Stream` writable side has not been ended. +#### Event: 'close' + + +The `'close'` event is emitted when the `Http2Stream` is destroyed. Once +this event is emitted, the `Http2Stream` instance is no longer usable. + +The listener callback is passed a single argument specifying the HTTP/2 error +code specified when closing the stream. If the code is any value other than +`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will also be emitted. + #### Event: 'error' - -The `'streamClosed'` event is emitted when the `Http2Stream` is destroyed. Once -this event is emitted, the `Http2Stream` instance is no longer usable. - -The listener callback is passed a single argument specifying the HTTP/2 error -code specified when closing the stream. If the code is any value other than -`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will also be emitted. - #### Event: 'timeout'