diff --git a/configure b/configure
index a481e444330c06..0b9c3a16ed09ff 100755
--- a/configure
+++ b/configure
@@ -64,6 +64,8 @@ shared_optgroup = optparse.OptionGroup(parser, "Shared libraries",
intl_optgroup = optparse.OptionGroup(parser, "Internationalization",
"Flags that lets you enable i18n features in Node.js as well as which "
"library you want to build against.")
+http2_optgroup = optparse.OptionGroup(parser, "HTTP2",
+ "Flags that allows you to control HTTP2 features in Node.js")
# Options should be in alphabetical order but keep --prefix at the top,
# that's arguably the one people will be looking for most.
@@ -397,6 +399,16 @@ intl_optgroup.add_option('--download-path',
parser.add_option_group(intl_optgroup)
+http2_optgroup.add_option('--debug-http2',
+ action='store_true',
+ dest='debug_http2',
+ help='build with http2 debug statements on (default is false)')
+
+http2_optgroup.add_option('--debug-nghttp2',
+ action='store_true',
+ dest='debug_nghttp2',
+ help='build nghttp2 with DEBUGBUILD (default is false)')
+
parser.add_option('--with-perfctr',
action='store_true',
dest='with_perfctr',
@@ -898,6 +910,16 @@ def configure_node(o):
if options.enable_static:
o['variables']['node_target_type'] = 'static_library'
+ if options.debug_http2:
+ o['variables']['debug_http2'] = 1
+ else:
+ o['variables']['debug_http2'] = 'false'
+
+ if options.debug_nghttp2:
+ o['variables']['debug_nghttp2'] = 1
+ else:
+ o['variables']['debug_nghttp2'] = 'false'
+
o['variables']['node_no_browser_globals'] = b(options.no_browser_globals)
o['variables']['node_shared'] = b(options.shared)
node_module_version = getmoduleversion.get_version()
diff --git a/doc/api/_toc.md b/doc/api/_toc.md
index 1075bc6be39858..6791e63f0c601a 100644
--- a/doc/api/_toc.md
+++ b/doc/api/_toc.md
@@ -24,6 +24,7 @@
* [File System](fs.html)
* [Globals](globals.html)
* [HTTP](http.html)
+* [HTTP/2](http2.html)
* [HTTPS](https.html)
* [Inspector](inspector.html)
* [Internationalization](intl.html)
diff --git a/doc/api/cli.md b/doc/api/cli.md
index 773e742966f330..f3172fbdde0f1d 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -170,6 +170,13 @@ added: v6.0.0
Silence all process warnings (including deprecations).
+### `--expose-http2`
+
+
+Enable the experimental `'http2'` module.
+
### `--napi-modules`
+
+* Extends: {EventEmitter}
+
+Instances of the `http2.Http2Session` class represent an active communications
+session between an HTTP/2 client and server. Instances of this class are *not*
+intended to be constructed directly by user code.
+
+Each `Http2Session` instance will exhibit slightly different behaviors
+depending on whether it is operating as a server or a client. The
+`http2session.type` property can be used to determine the mode in which an
+`Http2Session` is operating. On the server side, user code should rarely
+have occasion to work with the `Http2Session` object directly, with most
+actions typically taken through interactions with either the `Http2Server` or
+`Http2Stream` objects.
+
+#### Http2Session and Sockets
+
+Every `Http2Session` instance is associated with exactly one [`net.Socket`][] or
+[`tls.TLSSocket`][] when it is created. When either the `Socket` or the
+`Http2Session` are destroyed, both will be destroyed.
+
+Because the of the specific serialization and processing requirements imposed
+by the HTTP/2 protocol, it is not recommended for user code to read data from
+or write data to a `Socket` instance bound to a `Http2Session`. Doing so can
+put the HTTP/2 session into an indeterminate state causing the session and
+the socket to become unusable.
+
+Once a `Socket` has been bound to an `Http2Session`, user code should rely
+solely on the API of the `Http2Session`.
+
+#### Event: 'close'
+
+
+The `'close'` event is emitted once the `Http2Session` has been terminated.
+
+#### Event: 'connect'
+
+
+The `'connect'` event is emitted once the `Http2Session` has been successfully
+connected to the remote peer and communication may begin.
+
+*Note*: User code will typically not listen for this event directly.
+
+#### Event: 'error'
+
+
+The `'error'` event is emitted when an error occurs during the processing of
+an `Http2Session`.
+
+#### Event: 'frameError'
+
+
+The `'frameError'` event is emitted when an error occurs while attempting to
+send a frame on the session. If the frame that could not be sent is associated
+with a specific `Http2Stream`, an attempt to emit `'frameError'` event on the
+`Http2Stream` is made.
+
+When invoked, the handler function will receive three arguments:
+
+* An integer identifying the frame type.
+* An integer identifying the error code.
+* An integer identifying the stream (or 0 if the frame is not associated with
+ a stream).
+
+If the `'frameError'` event is associated with a stream, the stream will be
+closed and destroyed immediately following the `'frameError'` event. If the
+event is not associated with a stream, the `Http2Session` will be shutdown
+immediately following the `'frameError'` event.
+
+#### Event: 'goaway'
+
+
+The `'goaway'` event is emitted when a GOAWAY frame is received. When invoked,
+the handler function will receive three arguments:
+
+* `errorCode` {number} The HTTP/2 error code specified in the GOAWAY frame.
+* `lastStreamID` {number} The ID of the last stream the remote peer successfully
+ processed (or `0` if no ID is specified).
+* `opaqueData` {Buffer} If additional opaque data was included in the GOAWAY
+ frame, a `Buffer` instance will be passed containing that data.
+
+*Note*: The `Http2Session` instance will be shutdown automatically when the
+`'goaway'` event is emitted.
+
+#### Event: 'localSettings'
+
+
+The `'localSettings'` event is emitted when an acknowledgement SETTINGS frame
+has been received. When invoked, the handler function will receive a copy of
+the local settings.
+
+*Note*: When using `http2session.settings()` to submit new settings, the
+modified settings do not take effect until the `'localSettings'` event is
+emitted.
+
+```js
+session.settings({ enablePush: false });
+
+session.on('localSettings', (settings) => {
+ /** use the new settings **/
+});
+```
+
+#### Event: 'remoteSettings'
+
+
+The `'remoteSettings'` event is emitted when a new SETTINGS frame is received
+from the connected peer. When invoked, the handle function will receive a copy
+of the remote settings.
+
+```js
+session.on('remoteSettings', (settings) => {
+ /** use the new settings **/
+});
+```
+
+#### Event: 'stream'
+
+
+The `'stream'` event is emitted when a new `Http2Stream` is created. When
+invoked, the handler function will receive a reference to the `Http2Stream`
+object, a [Headers Object][], and numeric flags associated with the creation
+of the stream.
+
+```js
+const http2 = require('http2');
+const {
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_CONTENT_TYPE
+} = http2.constants;
+session.on('stream', (stream, headers, flags) => {
+ const method = headers[HTTP2_HEADER_METHOD];
+ const path = headers[HTTP2_HEADER_PATH];
+ // ...
+ stream.respond({
+ [HTTP2_HEADER_STATUS]: 200,
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+ });
+ stream.write('hello ');
+ stream.end('world');
+});
+```
+
+On the server side, user code will typically not listen for this event directly,
+and would instead register a handler for the `'stream'` event emitted by the
+`net.Server` or `tls.Server` instances returned by `http2.createServer()` and
+`http2.createSecureServer()`, respectively, as in the example below:
+
+```js
+const http2 = require('http2');
+
+// Create a plain-text HTTP/2 server
+const server = http2.createServer();
+
+server.on('stream', (stream, headers) => {
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('
Hello World
');
+});
+
+server.listen(80);
+```
+
+#### Event: 'socketError'
+
+
+The `'socketError'` event is emitted when an `'error'` is emitted on the
+`Socket` instance bound to the `Http2Session`. If this event is not handled,
+the `'error'` event will be re-emitted on the `Socket`.
+
+Likewise, when an `'error'` event is emitted on the `Http2Session`, a
+`'sessionError'` event will be emitted on the `Socket`. If that event is
+not handled, the `'error'` event will be re-emitted on the `Http2Session`.
+
+#### Event: 'timeout'
+
+
+After the `http2session.setTimeout()` method is used to set the timeout period
+for this `Http2Session`, the `'timeout'` event is emitted if there is no
+activity on the `Http2Session` after the configured number of milliseconds.
+
+```js
+session.setTimeout(2000);
+session.on('timeout', () => { /** .. **/ });
+```
+
+#### http2session.destroy()
+
+
+* Returns: {undefined}
+
+Immediately terminates the `Http2Session` and the associated `net.Socket` or
+`tls.TLSSocket`.
+
+#### http2session.destroyed
+
+
+* Value: {boolean}
+
+Will be `true` if this `Http2Session` instance has been destroyed and must no
+longer be used, otherwise `false`.
+
+#### http2session.localSettings
+
+
+* Value: {[Settings Object][]}
+
+A prototype-less object describing the current local settings of this
+`Http2Session`. The local settings are local to *this* `Http2Session` instance.
+
+#### http2session.pendingSettingsAck
+
+
+* Value: {boolean}
+
+Indicates whether or not the `Http2Session` is currently waiting for an
+acknowledgement for a sent SETTINGS frame. Will be `true` after calling the
+`http2session.settings()` method. Will be `false` once all sent SETTINGS
+frames have been acknowledged.
+
+#### http2session.remoteSettings
+
+
+* Value: {[Settings Object][]}
+
+A prototype-less object describing the current remote settings of this
+`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
+
+#### http2session.request(headers[, options])
+
+
+* `headers` {[Headers Object][]}
+* `options` {Object}
+ * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should
+ be closed initially, such as when sending a `GET` request that should not
+ expect a payload body.
+ * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
+ the created stream is made the sole direct dependency of the parent, with
+ all other existing dependents made a dependent of the newly created stream.
+ Defaults to `false`.
+ * `parent` {number} Specifies the numeric identifier of a stream the newly
+ created stream is dependent on.
+ * `weight` {number} Specifies the relative dependency of a stream in relation
+ to other streams with the same `parent`. The value is a number between `1`
+ and `256` (inclusive).
+* Returns: {ClientHttp2Stream}
+
+For HTTP/2 Client `Http2Session` instances only, the `http2session.request()`
+creates and returns an `Http2Stream` instance that can be used to send an
+HTTP/2 request to the connected server.
+
+This method is only available if `http2session.type` is equal to
+`http2.constants.NGHTTP2_SESSION_CLIENT`.
+
+```js
+const http2 = require('http2');
+const clientSession = http2.connect('https://localhost:1234');
+const {
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_STATUS
+} = http2.constants;
+
+const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' });
+req.on('response', (headers) => {
+ console.log(HTTP2_HEADER_STATUS);
+ req.on('data', (chunk) => { /** .. **/ });
+ req.on('end', () => { /** .. **/ });
+});
+```
+
+#### http2session.rstStream(stream, code)
+
+
+* stream {Http2Stream}
+* code {number} Unsigned 32-bit integer identifying the error code. Defaults to
+ `http2.constant.NGHTTP2_NO_ERROR` (`0x00`)
+* Returns: {undefined}
+
+Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing the given
+`Http2Stream` to be closed on both sides using [error code][] `code`.
+
+#### http2session.setTimeout(msecs, callback)
+
+
+* `msecs` {number}
+* `callback` {Function}
+* Returns: {undefined}
+
+Used to set a callback function that is called when there is no activity on
+the `Http2Session` after `msecs` milliseconds. The given `callback` is
+registered as a listener on the `'timeout'` event.
+
+#### http2session.shutdown(options[, callback])
+
+
+* `options` {Object}
+ * `graceful` {boolean} `true` to attempt a polite shutdown of the
+ `Http2Session`.
+ * `errorCode` {number} The HTTP/2 [error code][] to return. Note that this is
+ *not* the same thing as an HTTP Response Status Code. Defaults to `0x00`
+ (No Error).
+ * `lastStreamID` {number} The Stream ID of the last successfully processed
+ `Http2Stream` on this `Http2Session`.
+ * `opaqueData` {Buffer} A `Buffer` instance containing arbitrary additional
+ data to send to the peer upon disconnection. This is used, typically, to
+ provide additional data for debugging failures, if necessary.
+* `callback` {Function} A callback that is invoked after the session shutdown
+ has been completed.
+* Returns: {undefined}
+
+Attempts to shutdown this `Http2Session` using HTTP/2 defined procedures.
+If specified, the given `callback` function will be invoked once the shutdown
+process has completed.
+
+Note that calling `http2session.shutdown()` does *not* destroy the session or
+tear down the `Socket` connection. It merely prompts both sessions to begin
+preparing to cease activity.
+
+During a "graceful" shutdown, the session will first send a `GOAWAY` frame to
+the connected peer identifying the last processed stream as 232-1.
+Then, on the next tick of the event loop, a second `GOAWAY` frame identifying
+the most recently processed stream identifier is sent. This process allows the
+remote peer to begin preparing for the connection to be terminated.
+
+```js
+session.shutdown({
+ graceful: true,
+ opaqueData: Buffer.from('add some debugging data here')
+}, () => session.destroy());
+```
+
+#### http2session.socket
+
+
+* Value: {net.Socket|tls.TLSSocket}
+
+A reference to the [`net.Socket`][] or [`tls.TLSSocket`][] to which this
+`Http2Session` instance is bound.
+
+*Note*: It is not recommended for user code to interact directly with a
+`Socket` bound to an `Http2Session`. See [Http2Session and Sockets][] for
+details.
+
+#### http2session.state
+
+
+* Value: {Object}
+ * `effectiveLocalWindowSize` {number}
+ * `effectiveRecvDataLength` {number}
+ * `nextStreamID` {number}
+ * `localWindowSize` {number}
+ * `lastProcStreamID` {number}
+ * `remoteWindowSize` {number}
+ * `outboundQueueSize` {number}
+ * `deflateDynamicTableSize` {number}
+ * `inflateDynamicTableSize` {number}
+
+An object describing the current status of this `Http2Session`.
+
+#### http2session.priority(stream, options)
+
+
+* `stream` {Http2Stream}
+* `options` {Object}
+ * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
+ the given stream is made the sole direct dependency of the parent, with
+ all other existing dependents made a dependent of the given stream. Defaults
+ to `false`.
+ * `parent` {number} Specifies the numeric identifier of a stream the given
+ stream is dependent on.
+ * `weight` {number} Specifies the relative dependency of a stream in relation
+ to other streams with the same `parent`. The value is a number between `1`
+ and `256` (inclusive).
+ * `silent` {boolean} When `true`, changes the priority locally without
+ sending a `PRIORITY` frame to the connected peer.
+* Returns: {undefined}
+
+Updates the priority for the given `Http2Stream` instance.
+
+#### http2session.settings(settings)
+
+
+* `settings` {[Settings Object][]}
+* Returns {undefined}
+
+Updates the current local settings for this `Http2Session` and sends a new
+`SETTINGS` frame to the connected HTTP/2 peer.
+
+Once called, the `http2session.pendingSettingsAck` property will be `true`
+while the session is waiting for the remote peer to acknowledge the new
+settings.
+
+*Note*: The new settings will not become effective until the SETTINGS
+acknowledgement is received and the `'localSettings'` event is emitted. It
+is possible to send multiple SETTINGS frames while acknowledgement is still
+pending.
+
+#### http2session.type
+
+
+* Value: {number}
+
+The `http2session.type` will be equal to
+`http2.constants.NGHTTP2_SESSION_SERVER` if this `Http2Session` instance is a
+server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a
+client.
+
+### Class: Http2Stream
+
+
+* Extends: {Duplex}
+
+Each instance of the `Http2Stream` class represents a bidirectional HTTP/2
+communications stream over an `Http2Session` instance. Any single `Http2Session`
+may have up to 231-1 `Http2Stream` instances over its lifetime.
+
+User code will not construct `Http2Stream` instances directly. Rather, these
+are created, managed, and provided to user code through the `Http2Session`
+instance. On the server, `Http2Stream` instances are created either in response
+to an incoming HTTP request (and handed off to user code via the `'stream'`
+event), or in response to a call to the `http2stream.pushStream()` method.
+On the client, `Http2Stream` instances are created and returned when either the
+`http2session.request()` method is called, or in response to an incoming
+`'push'` event.
+
+*Note*: The `Http2Stream` class is a base for the [`ServerHttp2Stream`][] and
+[`ClientHttp2Stream`][] classes, each of which are used specifically by either
+the Server or Client side, respectively.
+
+All `Http2Stream` instances are [`Duplex`][] streams. The `Writable` side of the
+`Duplex` is used to send data to the connected peer, while the `Readable` side
+is used to receive data sent by the connected peer.
+
+#### Http2Stream Lifecycle
+
+##### Creation
+
+On the server side, instances of [`ServerHttp2Stream`][] are created either
+when:
+
+* A new HTTP/2 `HEADERS` frame with a previously unused stream ID is received;
+* The `http2stream.pushStream()` method is called.
+
+On the client side, instances of [`ClientHttp2Stream`[] are created when the
+`http2session.request()` method is called.
+
+*Note*: On the client, the `Http2Stream` instance returned by
+`http2session.request()` may not be immediately ready for use if the parent
+`Http2Session` has not yet been fully established. In such cases, operations
+called on the `Http2Stream` will be buffered until the `'ready'` event is
+emitted. User code should rarely, if ever, have need to handle the `'ready'`
+event directly. The ready status of an `Http2Stream` can be determined by
+checking the value of `http2stream.id`. If the value is `undefined`, the stream
+is not yet ready for use.
+
+##### Destruction
+
+All [`Http2Stream`][] instances are destroyed either when:
+
+* An `RST_STREAM` frame for the stream is received by the connected peer.
+* The `http2stream.rstStream()` or `http2session.rstStream()` methods are
+ called.
+* The `http2stream.destroy()` or `http2session.destroy()` methods are called.
+
+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
+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
+with an `Error` passed as the first argument.
+
+After the `Http2Stream` has been destroyed, the `http2stream.destroyed`
+property will be `true` and the `http2stream.rstCode` property will specify the
+`RST_STREAM` error code. The `Http2Stream` instance is no longer usable once
+destroyed.
+
+#### Event: 'aborted'
+
+
+The `'aborted'` event is emitted whenever a `Http2Stream` instance is
+abnormally aborted in mid-communication.
+
+*Note*: The `'aborted'` event will only be emitted if the `Http2Stream`
+writable side has not been ended.
+
+#### Event: 'error'
+
+
+The `'error'` event is emitted when an error occurs during the processing of
+an `Http2Stream`.
+
+#### Event: 'fetchTrailers'
+
+
+The `'fetchTrailers'` event is emitted by the `Http2Stream` immediately after
+queuing the last chunk of payload data to be sent. The listener callback is
+passed a single object (with a `null` prototype) that the listener may used
+to specify the trailing header fields to send to the peer.
+
+```js
+stream.on('fetchTrailers', (trailers) => {
+ trailers['ABC'] = 'some value to send';
+});
+```
+
+*Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
+"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
+will be emitted if the `'fetchTrailers'` event handler attempts to set such
+header fields.
+
+#### Event: 'frameError'
+
+
+The `'frameError'` event is emitted when an error occurs while attempting to
+send a frame. When invoked, the handler function will receive an integer
+argument identifying the frame type, and an integer argument identifying the
+error code. The `Http2Stream` instance will be destroyed immediately after the
+`'frameError'` event is emitted.
+
+#### Event: 'streamClosed'
+
+
+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'
+
+
+The `'timeout'` event is emitted after no activity is received for this
+`'Http2Stream'` within the number of millseconds set using
+`http2stream.setTimeout()`.
+
+#### Event: 'trailers'
+
+
+The `'trailers'` event is emitted when a block of headers associated with
+trailing header fields is received. The listener callback is passed the
+[Headers Object][] and flags associated with the headers.
+
+```js
+stream.on('trailers', (headers, flags) => {
+ console.log(headers);
+});
+```
+
+#### http2stream.aborted
+
+
+* Value: {boolean}
+
+Set to `true` if the `Http2Stream` instance was aborted abnormally. When set,
+the `'aborted'` event will have been emitted.
+
+#### http2stream.destroyed
+
+
+* Value: {boolean}
+
+Set to `true` if the `Http2Stream` instance has been destroyed and is no longer
+usable.
+
+#### http2stream.priority(options)
+
+
+* `options` {Object}
+ * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
+ this stream is made the sole direct dependency of the parent, with
+ all other existing dependents made a dependent of this stream. Defaults
+ to `false`.
+ * `parent` {number} Specifies the numeric identifier of a stream this stream
+ is dependent on.
+ * `weight` {number} Specifies the relative dependency of a stream in relation
+ to other streams with the same `parent`. The value is a number between `1`
+ and `256` (inclusive).
+ * `silent` {boolean} When `true`, changes the priority locally without
+ sending a `PRIORITY` frame to the connected peer.
+* Returns: {undefined}
+
+Updates the priority for this `Http2Stream` instance.
+
+#### http2stream.rstCode
+
+
+* Value: {number}
+
+Set to the `RST_STREAM` [error code][] reported when the `Http2Stream` is
+destroyed after either receiving an `RST_STREAM` frame from the connected peer,
+calling `http2stream.rstStream()`, or `http2stream.destroy()`. Will be
+`undefined` if the `Http2Stream` has not been closed.
+
+#### http2stream.rstStream(code)
+
+
+* code {number} Unsigned 32-bit integer identifying the error code. Defaults to
+ `http2.constant.NGHTTP2_NO_ERROR` (`0x00`)
+* Returns: {undefined}
+
+Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing this
+`Http2Stream` to be closed on both sides using [error code][] `code`.
+
+#### http2stream.rstWithNoError()
+
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error).
+
+#### http2stream.rstWithProtocolError() {
+
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error).
+
+#### http2stream.rstWithCancel() {
+
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel).
+
+#### http2stream.rstWithRefuse() {
+
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream).
+
+#### http2stream.rstWithInternalError() {
+
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error).
+
+#### http2stream.session
+
+
+* Value: {Http2Sesssion}
+
+A reference to the `Http2Session` instance that owns this `Http2Stream`. The
+value will be `undefined` after the `Http2Stream` instance is destroyed.
+
+#### http2stream.setTimeout(msecs, callback)
+
+
+* `msecs` {number}
+* `callback` {Function}
+* Returns: {undefined}
+
+```js
+const http2 = require('http2');
+const client = http2.connect('http://example.org:8000');
+
+const req = client.request({ ':path': '/' });
+
+// Cancel the stream if there's no activity after 5 seconds
+req.setTimeout(5000, () => req.rstStreamWithCancel());
+```
+
+#### http2stream.state
+
+
+* Value: {Object}
+ * `localWindowSize` {number}
+ * `state` {number}
+ * `streamLocalClose` {number}
+ * `streamRemoteClose` {number}
+ * `sumDependencyWeight` {number}
+ * `weight` {number}
+
+A current state of this `Http2Stream`.
+
+### Class: ClientHttp2Stream
+
+
+* Extends {Http2Stream}
+
+The `ClientHttp2Stream` class is an extension of `Http2Stream` that is
+used exclusively on HTTP/2 Clients. `Http2Stream` instances on the client
+provide events such as `'response'` and `'push'` that are only relevant on
+the client.
+
+#### Event: 'headers'
+
+
+The `'headers'` event is emitted when an additional block of headers is received
+for a stream, such as when a block of `1xx` informational headers are received.
+The listener callback is passed the [Headers Object][] and flags associated with
+the headers.
+
+```js
+stream.on('headers', (headers, flags) => {
+ console.log(headers);
+});
+```
+
+#### Event: 'push'
+
+
+The `'push'` event is emitted when response headers for a Server Push stream
+are received. The listener callback is passed the [Headers Object][] and flags
+associated with the headers.
+
+```js
+stream.on('push', (headers, flags) => {
+ console.log(headers);
+});
+```
+
+#### Event: 'response'
+
+
+The `'response'` event is emitted when a response `HEADERS` frame has been
+received for this stream from the connected HTTP/2 server. The listener is
+invoked with two arguments: an Object containing the received
+[Headers Object][], and flags associated with the headers.
+
+For example:
+
+```js
+const http2 = require('http');
+const client = http2.connect('https://localhost');
+const req = client.request({ ':path': '/' });
+req.on('response', (headers, flags) => {
+ console.log(headers[':status']);
+});
+```
+
+### Class: ServerHttp2Stream
+
+
+* Extends: {Http2Stream}
+
+The `ServerHttp2Stream` class is an extension of [`Http2Stream`][] that is
+used exclusively on HTTP/2 Servers. `Http2Stream` instances on the server
+provide additional methods such as `http2stream.pushStream()` and
+`http2stream.respond()` that are only relevant on the server.
+
+#### http2stream.additionalHeaders(headers)
+
+
+* `headers` {[Headers Object][]}
+* Returns: {undefined}
+
+Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer.
+
+#### http2stream.headersSent
+
+
+* Value: {boolean}
+
+Boolean (read-only). True if headers were sent, false otherwise.
+
+#### http2stream.pushAllowed
+
+
+* Value: {boolean}
+
+Read-only property mapped to the `SETTINGS_ENABLE_PUSH` flag of the remote
+client's most recent `SETTINGS` frame. Will be `true` if the remote peer
+accepts push streams, `false` otherwise. Settings are the same for every
+`Http2Stream` in the same `Http2Session`.
+
+#### http2stream.pushStream(headers[, options], callback)
+
+
+* `headers` {[Headers Object][]}
+* `options` {Object}
+ * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
+ the created stream is made the sole direct dependency of the parent, with
+ all other existing dependents made a dependent of the newly created stream.
+ Defaults to `false`.
+ * `parent` {number} Specifies the numeric identifier of a stream the newly
+ created stream is dependent on.
+ * `weight` {number} Specifies the relative dependency of a stream in relation
+ to other streams with the same `parent`. The value is a number between `1`
+ and `256` (inclusive).
+* `callback` {Function} Callback that is called once the push stream has been
+ initiated.
+* Returns: {undefined}
+
+Initiates a push stream. The callback is invoked with the new `Htt2Stream`
+instance created for the push stream.
+
+```js
+const http2 = require('http2');
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respond({ ':status': 200 });
+ stream.pushStream({ ':path': '/' }, (pushStream) => {
+ pushStream.respond({ ':status': 200 });
+ pushStream.end('some pushed data');
+ });
+ stream.end('some data');
+});
+```
+
+#### http2stream.respond([headers[, options]])
+
+
+* `headers` {[Headers Object][]}
+* `options` {Object}
+ * `endStream` {boolean} Set to `true` to indicate that the response will not
+ include payload data.
+* Returns: {undefined}
+
+```js
+const http2 = require('http2');
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ stream.respond({ ':status': 200 });
+ stream.end('some data');
+});
+```
+
+#### http2stream.respondWithFD(fd[, headers])
+
+
+* `fd` {number} A readable file descriptor
+* `headers` {[Headers Object][]}
+
+Initiates a response whose data is read from the given file descriptor. No
+validation is performed on the given file descriptor. If an error occurs while
+attempting to read data using the file descriptor, the `Http2Stream` will be
+closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` code.
+
+When used, the `Http2Stream` object's Duplex interface will be closed
+automatically. HTTP trailer fields cannot be sent. The `'fetchTrailers'` event
+will *not* be emitted.
+
+```js
+const http2 = require('http2');
+const fs = require('fs');
+
+const fd = fs.openSync('/some/file', 'r');
+
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ const stat = fs.fstatSync(fd);
+ const headers = {
+ 'content-length': stat.size,
+ 'last-modified': stat.mtime.toUTCString(),
+ 'content-type': 'text/plain'
+ };
+ stream.respondWithFD(fd, headers);
+});
+server.on('close', () => fs.closeSync(fd));
+```
+
+#### http2stream.respondWithFile(path[, headers[, options]])
+
+
+* `path` {string|Buffer|URL}
+* `headers` {[Headers Object][]}
+* `options` {Object}
+ * `statCheck` {Function}
+
+Sends a regular file as the response. The `path` must specify a regular file
+or an `'error'` event will be emitted on the `Http2Stream` object.
+
+When used, the `Http2Stream` object's Duplex interface will be closed
+automatically. HTTP trailer fields cannot be sent. The `'fetchTrailers'` event
+will *not* be emitted.
+
+The optional `options.statCheck` function may be specified to give user code
+an opportunity to set additional content headers based on the `fs.Stat` details
+of the given file:
+
+If an error occurs while attempting to read the file data, the `Http2Stream`
+will be closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR`
+code.
+
+Example using a file path:
+
+```js
+const http2 = require('http2');
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ function statCheck(stat, headers) {
+ headers['last-modified'] = stat.mtime.toUTCString();
+ }
+ stream.respondWithFile('/some/file',
+ { 'content-type': 'text/plain' },
+ { statCheck });
+});
+```
+
+The `options.statCheck` function may also be used to cancel the send operation
+by returning `false`. For instance, a conditional request may check the stat
+results to determine if the file has been modified to return an appropriate
+`304` response:
+
+```js
+const http2 = require('http2');
+const server = http2.createServer();
+server.on('stream', (stream) => {
+ function statCheck(stat, headers) {
+ // Check the stat here...
+ stream.respond({ ':status': 304 });
+ return false; // Cancel the send operation
+ }
+ stream.respondWithFile('/some/file',
+ { 'content-type': 'text/plain' },
+ { statCheck });
+});
+```
+
+The `content-length` header field will be automatically set.
+
+### Class: Http2Server
+
+
+* Extends: {net.Server}
+
+#### Event: 'sessionError'
+
+
+The `'sessionError'` event is emitted when an `'error'` event is emitted by
+an `Http2Session` object. If no listener is registered for this event, an
+`'error'` event is emitted.
+
+#### Event: 'socketError'
+
+
+The `'socketError'` event is emitted when an `'error'` event is emitted by
+a `Socket` associated with the server. If no listener is registered for this
+event, an `'error'` event is emitted.
+
+#### Event: 'stream'
+
+
+The `'stream'` event is emitted when a `'stream'` event has been emitted by
+an `Http2Session` associated with the server.
+
+```js
+const http2 = require('http2');
+const {
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_CONTENT_TYPE
+} = http2.constants;
+
+const server = http.createServer();
+server.on('stream', (stream, headers, flags) => {
+ const method = headers[HTTP2_HEADER_METHOD];
+ const path = headers[HTTP2_HEADER_PATH];
+ // ...
+ stream.respond({
+ [HTTP2_HEADER_STATUS]: 200,
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+ });
+ stream.write('hello ');
+ stream.end('world');
+});
+```
+
+#### Event: 'timeout'
+
+
+The `'timeout'` event is emitted when there is no activity on the Server for
+a given number of milliseconds set using `http2server.setTimeout()`.
+
+### Class: Http2SecureServer
+
+
+* Extends: {tls.Server}
+
+#### Event: 'sessionError'
+
+
+The `'sessionError'` event is emitted when an `'error'` event is emitted by
+an `Http2Session` object. If no listener is registered for this event, an
+`'error'` event is emitted on the `Http2Session` instance instead.
+
+#### Event: 'socketError'
+
+
+The `'socketError'` event is emitted when an `'error'` event is emitted by
+a `Socket` associated with the server. If no listener is registered for this
+event, an `'error'` event is emitted on the `Socket` instance instead.
+
+#### Event: 'unknownProtocol'
+
+
+The `'unknownProtocol'` event is emitted when a connecting client fails to
+negotiate an allowed protocol (i.e. HTTP/2 or HTTP/1.1). The event handler
+receives the socket for handling. If no listener is registered for this event,
+the connection is terminated. See the
+
+#### Event: 'stream'
+
+
+The `'stream'` event is emitted when a `'stream'` event has been emitted by
+an `Http2Session` associated with the server.
+
+```js
+const http2 = require('http2');
+const {
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_CONTENT_TYPE
+} = http2.constants;
+
+const options = getOptionsSomehow();
+
+const server = http.createSecureServer(options);
+server.on('stream', (stream, headers, flags) => {
+ const method = headers[HTTP2_HEADER_METHOD];
+ const path = headers[HTTP2_HEADER_PATH];
+ // ...
+ stream.respond({
+ [HTTP2_HEADER_STATUS]: 200,
+ [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+ });
+ stream.write('hello ');
+ stream.end('world');
+});
+```
+
+#### Event: 'timeout'
+
+
+### http2.createServer(options[, onRequestHandler])
+
+
+* `options` {Object}
+ * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
+ for deflating header fields. Defaults to 4Kib.
+ * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a
+ serialized, compressed block of headers. Attempts to send headers that
+ exceed this limit will result in a `'frameError'` event being emitted
+ and the stream being closed and destroyed.
+ * `paddingStrategy` {number} Identifies the strategy used for determining the
+ amount of padding to use for HEADERS and DATA frames. Defaults to
+ `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of:
+ * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is
+ to be applied.
+ * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum
+ amount of padding, as determined by the internal implementation, is to
+ be applied.
+ * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
+ provided `options.selectPadding` callback is to be used to determine the
+ amount of padding.
+ * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
+ streams for the remote peer as if a SETTINGS frame had been received. Will
+ be overridden if the remote peer sets its own value for
+ `maxConcurrentStreams`. Defaults to 100.
+ * `selectPadding` {Function} When `options.paddingStrategy` is equal to
+ `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
+ used to determine the padding. See [Using options.selectPadding][].
+ * `settings` {[Settings Object][]} The initial settings to send to the
+ remote peer upon connection.
+* `onRequestHandler` {Function} See [Compatibility API][]
+* Returns: {Http2Server}
+
+Returns a `net.Server` instance that creates and manages `Http2Session`
+instances.
+
+```js
+const http2 = require('http2');
+
+// Create a plain-text HTTP/2 server
+const server = http2.createServer();
+
+server.on('stream', (stream, headers) => {
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('
Hello World
');
+});
+
+server.listen(80);
+```
+
+### http2.createSecureServer(options[, onRequestHandler])
+
+
+* `options` {Object}
+ * `allowHTTP1` {boolean} Incoming client connections that do not support
+ HTTP/2 will be downgraded to HTTP/1.x when set to `true`. The default value
+ is `false`. See the [`'unknownProtocol'`][] event.
+ * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
+ for deflating header fields. Defaults to 4Kib.
+ * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a
+ serialized, compressed block of headers. Attempts to send headers that
+ exceed this limit will result in a `'frameError'` event being emitted
+ and the stream being closed and destroyed.
+ * `paddingStrategy` {number} Identifies the strategy used for determining the
+ amount of padding to use for HEADERS and DATA frames. Defaults to
+ `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of:
+ * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is
+ to be applied.
+ * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum
+ amount of padding, as determined by the internal implementation, is to
+ be applied.
+ * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
+ provided `options.selectPadding` callback is to be used to determine the
+ amount of padding.
+ * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
+ streams for the remote peer as if a SETTINGS frame had been received. Will
+ be overridden if the remote peer sets its own value for
+ `maxConcurrentStreams`. Defaults to 100.
+ * `selectPadding` {Function} When `options.paddingStrategy` is equal to
+ `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
+ used to determine the padding. See [Using options.selectPadding][].
+ * `settings` {[Settings Object][]} The initial settings to send to the
+ remote peer upon connection.
+ * ...: Any [`tls.createServer()`][] options can be provided. For
+ servers, the identity options (`pfx` or `key`/`cert`) are usually required.
+* `onRequestHandler` {Function} See [Compatibility API][]
+* Returns {Http2SecureServer}
+
+Returns a `tls.Server` instance that creates and manages `Http2Session`
+instances.
+
+```js
+const http2 = require('http2');
+
+const options = {
+ key: fs.readFileSync('server-key.pem'),
+ cert: fs.readFileSync('server-cert.pem')
+};
+
+// Create a plain-text HTTP/2 server
+const server = http2.createSecureServer(options);
+
+server.on('stream', (stream, headers) => {
+ stream.respond({
+ 'content-type': 'text/html',
+ ':status': 200
+ });
+ stream.end('
Hello World
');
+});
+
+server.listen(80);
+```
+
+### http2.connect(authority[, options][, listener])
+
+
+* `authority` {string|URL}
+* `options` {Object}
+ * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
+ for deflating header fields. Defaults to 4Kib.
+ * `maxReservedRemoteStreams` {number} Sets the maximum number of reserved push
+ streams the client will accept at any given time. Once the current number of
+ currently reserved push streams exceeds reaches this limit, new push streams
+ sent by the server will be automatically rejected.
+ * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a
+ serialized, compressed block of headers. Attempts to send headers that
+ exceed this limit will result in a `'frameError'` event being emitted
+ and the stream being closed and destroyed.
+ * `paddingStrategy` {number} Identifies the strategy used for determining the
+ amount of padding to use for HEADERS and DATA frames. Defaults to
+ `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of:
+ * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is
+ to be applied.
+ * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum
+ amount of padding, as determined by the internal implementation, is to
+ be applied.
+ * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
+ provided `options.selectPadding` callback is to be used to determine the
+ amount of padding.
+ * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
+ streams for the remote peer as if a SETTINGS frame had been received. Will
+ be overridden if the remote peer sets its own value for
+ `maxConcurrentStreams`. Defaults to 100.
+ * `selectPadding` {Function} When `options.paddingStrategy` is equal to
+ `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
+ used to determine the padding. See [Using options.selectPadding][].
+ * `settings` {[Settings Object][]} The initial settings to send to the
+ remote peer upon connection.
+* `listener` {Function}
+* Returns {Http2Session}
+
+Returns a HTTP/2 client `Http2Session` instance.
+
+```js
+const http2 = require('http2');
+const client = http2.connect('https://localhost:1234');
+
+/** use the client **/
+
+client.destroy();
+```
+
+### http2.constants
+
+
+#### Error Codes for RST_STREAM and GOAWAY
+
+
+| Value | Name | Constant |
+|-------|---------------------|-----------------------------------------------|
+| 0x00 | No Error | `http2.constants.NGHTTP2_NO_ERROR` |
+| 0x01 | Protocol Error | `http2.constants.NGHTTP2_PROTOCOL_ERROR` |
+| 0x02 | Internal Error | `http2.constants.NGHTTP2_INTERNAL_ERROR` |
+| 0x03 | Flow Control Error | `http2.constants.NGHTTP2_FLOW_CONTROL_ERROR` |
+| 0x04 | Settings Timeout | `http2.constants.NGHTTP2_SETTINGS_TIMEOUT` |
+| 0x05 | Stream Closed | `http2.constants.NGHTTP2_STREAM_CLOSED` |
+| 0x06 | Frame Size Error | `http2.constants.NGHTTP2_FRAME_SIZE_ERROR` |
+| 0x07 | Refused Stream | `http2.constants.NGHTTP2_REFUSED_STREAM` |
+| 0x08 | Cancel | `http2.constants.NGHTTP2_CANCEL` |
+| 0x09 | Compression Error | `http2.constants.NGHTTP2_COMPRESSION_ERROR` |
+| 0x0a | Connect Error | `http2.constants.NGHTTP2_CONNECT_ERROR` |
+| 0x0b | Enhance Your Calm | `http2.constants.NGHTTP2_ENHANCE_YOUR_CALM` |
+| 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()
+
+
+* 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)
+
+
+* `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)
+
+
+* `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
+keys will be serialized to lower-case. Property values should be strings (if
+they are not they will be coerced to strings) or an Array of strings (in order
+to send more than one value per header field).
+
+For example:
+
+```js
+const headers = {
+ ':status': '200',
+ 'content-type': 'text-plain',
+ 'ABC': ['has', 'more', 'than', 'one', 'value']
+};
+
+stream.respond(headers);
+```
+
+*Note*: Header objects passed to callback functions will have a `null`
+prototype. This means that normal JavaScript object methods such as
+`Object.prototype.toString()` and `Object.prototype.hasOwnProperty()` will
+not work.
+
+```js
+const http2 = require('http2');
+const server = http2.createServer();
+server.on('stream', (stream, headers) => {
+ console.log(headers[':path']);
+ console.log(headers.ABC);
+});
+```
+
+### Settings Object
+
+The `http2.getDefaultSettings()`, `http2.getPackedSettings()`,
+`http2.createServer()`, `http2.createSecureServer()`,
+`http2session.settings()`, `http2session.localSettings`, and
+`http2session.remoteSettings` APIs either return or receive as input an
+object that defines configuration settings for an `Http2Session` object.
+These objects are ordinary JavaScript objects containing the following
+properties.
+
+* `headerTableSize` {number} Specifies the maximum number of bytes used for
+ header compression. The default value is 4,096 octets. The minimum allowed
+ value is 0. The maximum allowed value is 232-1.
+* `enablePush` {boolean} Specifies `true` if HTTP/2 Push Streams are to be
+ permitted on the `Http2Session` instances.
+* `initialWindowSize` {number} Specifies the *senders* initial window size
+ for stream-level flow control. The default value is 65,535 bytes. The minimum
+ allowed value is 0. The maximum allowed value is 232-1.
+* `maxFrameSize` {number} Specifies the size of the largest frame payload.
+ The default and the minimum allowed value is 16,384 bytes. The maximum
+ allowed value is 224-1.
+* `maxConcurrentStreams` {number} Specifies the maximum number of concurrent
+ streams permitted on an `Http2Session`. There is no default value which
+ implies, at least theoretically, 231-1 streams may be open
+ concurrently at any given time in an `Http2Session`. The minimum value is
+ 0. The maximum allowed value is 231-1.
+* `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets)
+ of header list that will be accepted. There is no default value. The minimum
+ allowed value is 0. The maximum allowed value is 232-1.
+
+All additional properties on the settings object are ignored.
+
+### Using `options.selectPadding`
+
+When `options.paddingStrategy` is equal to
+`http2.constants.PADDING_STRATEGY_CALLBACK`, the the HTTP/2 implementation will
+consult the `options.selectPadding` callback function, if provided, to determine
+the specific amount of padding to use per HEADERS and DATA frame.
+
+The `options.selectPadding` function receives two numeric arguments,
+`frameLen` and `maxFrameLen` and must return a number `N` such that
+`frameLen <= N <= maxFrameLen`.
+
+```js
+const http2 = require('http2');
+const server = http2.createServer({
+ paddingStrategy: http2.constants.PADDING_STRATEGY_CALLBACK,
+ selectPadding(frameLen, maxFrameLen) {
+ return maxFrameLen;
+ }
+});
+```
+
+*Note*: The `options.selectPadding` function is invoked once for *every*
+HEADERS and DATA frame. This has a definite noticeable impact on
+performance.
+
+### Error Handling
+
+There are several types of error conditions that may arise when using the
+`http2` module:
+
+Validation Errors occur when an incorrect argument, option or setting value is
+passed in. These will always be reported by a synchronous `throw`.
+
+State Errors occur when an action is attempted at an incorrect time (for
+instance, attempting to send data on a stream after it has closed). These will
+be repoorted using either a synchronous `throw` or via an `'error'` event on
+the `Http2Stream`, `Http2Session` or HTTP/2 Server objects, depending on where
+and when the error occurs.
+
+Internal Errors occur when an HTTP/2 session fails unexpectedly. These will be
+reported via an `'error'` event on the `Http2Session` or HTTP/2 Server objects.
+
+Protocol Errors occur when various HTTP/2 protocol constraints are violated.
+These will be reported using either a synchronous `throw` or via an `'error'`
+event on the `Http2Stream`, `Http2Session` or HTTP/2 Server objects, depending
+on where and when the error occurs.
+
+### Push streams on the client
+
+To receive pushed streams on the client, set a listener for the `'stream'`
+event on the `ClientHttp2Session`:
+
+```js
+const http2 = require('http2');
+
+const client = http2.connect('http://localhost');
+
+client.on('stream', (pushedStream, requestHeaders) => {
+ pushedStream.on('push', (responseHeaders) => {
+ // process response headers
+ });
+ pushedStream.on('data', (chunk) => { /* handle pushed data */ });
+});
+
+const req = client.request({ ':path': '/' });
+```
+
+### Supporting the CONNECT method
+
+The `CONNECT` method is used to allow an HTTP/2 server to be used as a proxy
+for TCP/IP connections.
+
+A simple TCP Server:
+```js
+const net = require('net');
+
+const server = net.createServer((socket) => {
+ let name = '';
+ socket.setEncoding('utf8');
+ socket.on('data', (chunk) => name += chunk);
+ socket.on('end', () => socket.end(`hello ${name}`));
+});
+
+server.listen(8000);
+```
+
+An HTTP/2 CONNECT proxy:
+
+```js
+const http2 = require('http2');
+const net = require('net');
+const { URL } = require('url');
+
+const proxy = http2.createServer();
+proxy.on('stream', (stream, headers) => {
+ if (headers[':method'] !== 'CONNECT') {
+ // Only accept CONNECT requests
+ stream.rstWithRefused();
+ return;
+ }
+ const auth = new URL(`tcp://${headers[':authority']}`);
+ // It's a very good idea to verify that hostname and port are
+ // things this proxy should be connecting to.
+ const socket = net.connect(auth.port, auth.hostname, () => {
+ stream.respond();
+ socket.pipe(stream);
+ stream.pipe(socket);
+ });
+ socket.on('error', (error) => {
+ stream.rstStream(http2.constants.NGHTTP2_CONNECT_ERROR);
+ });
+});
+
+proxy.listen(8001);
+```
+
+An HTTP/2 CONNECT client:
+
+```js
+const http2 = require('http2');
+
+const client = http2.connect('http://localhost:8001');
+
+// Must not specify the ':path' and ':scheme' headers
+// for CONNECT requests or an error will be thrown.
+const req = client.request({
+ ':method': 'CONNECT',
+ ':authority': `localhost:${port}`
+});
+
+req.on('response', common.mustCall());
+let data = '';
+req.setEncoding('utf8');
+req.on('data', (chunk) => data += chunk);
+req.on('end', () => {
+ console.log(`The server says: ${data}`);
+ client.destroy();
+});
+req.end('Jane');
+```
+
+## Compatibility API
+
+TBD
+
+
+[HTTP/2]: https://tools.ietf.org/html/rfc7540
+[HTTP/1]: http.html
+[`net.Socket`]: net.html
+[`tls.TLSSocket`]: tls.html
+[`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener
+[ClientHttp2Stream]: #http2_class_clienthttp2stream
+[Compatibility API: #http2_compatibility_api
+[`Duplex`]: stream.html#stream_class_stream_duplex
+[Headers Object]: #http2_headers_object
+[Http2Stream]: #http2_class_http2stream
+[Http2Session and Sockets]: #http2_http2sesion_and_sockets
+[ServerHttp2Stream]: #http2_class_serverhttp2stream
+[Settings Object]: #http2_settings_object
+[Using options.selectPadding]: #http2_using_options_selectpadding
+[error code]: #error_codes
+[`'unknownProtocol'`]: #http2_event_unknownprotocol
diff --git a/doc/guides/writing-and-running-benchmarks.md b/doc/guides/writing-and-running-benchmarks.md
index 3135f2115d78cb..7aeb9728aaedf0 100644
--- a/doc/guides/writing-and-running-benchmarks.md
+++ b/doc/guides/writing-and-running-benchmarks.md
@@ -41,6 +41,14 @@ benchmarker to be used should be specified by providing it as an argument:
`node benchmark/http/simple.js benchmarker=autocannon`
+#### HTTP/2 Benchmark Requirements
+
+To run the `http2` benchmarks, the `h2load` benchmarker must be used. The
+`h2load` tool is a component of the `nghttp2` project and may be installed
+from [nghttp.org][] or built from source.
+
+`node benchmark/http2/simple.js benchmarker=autocannon`
+
### Benchmark Analysis Requirements
To analyze the results, `R` should be installed. Use one of the available
@@ -423,3 +431,4 @@ Supported options keys are:
[wrk]: https://github.com/wg/wrk
[t-test]: https://en.wikipedia.org/wiki/Student%27s_t-test#Equal_or_unequal_sample_sizes.2C_unequal_variances
[git-for-windows]: http://git-scm.com/download/win
+[nghttp2.org]: http://nghttp2.org
diff --git a/doc/node.1 b/doc/node.1
index 753bf0f78d0b87..cf79ce33f929df 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -130,6 +130,10 @@ Emit pending deprecation warnings.
.BR \-\-no\-warnings
Silence all process warnings (including deprecations).
+.TP
+.BR \-\-expose\-http2
+Enable the experimental `'http2'` module.
+
.TP
.BR \-\-napi\-modules
Enable loading native modules compiled with the ABI-stable Node.js API (N-API)
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
old mode 100755
new mode 100644
diff --git a/lib/http2.js b/lib/http2.js
new file mode 100644
index 00000000000000..e964abf589d0eb
--- /dev/null
+++ b/lib/http2.js
@@ -0,0 +1,27 @@
+'use strict';
+
+process.emitWarning(
+ 'The http2 module is an experimental API.',
+ 'ExperimentalWarning', undefined,
+ 'See https://github.com/nodejs/http2'
+);
+
+const {
+ constants,
+ getDefaultSettings,
+ getPackedSettings,
+ getUnpackedSettings,
+ createServer,
+ createSecureServer,
+ connect
+} = require('internal/http2/core');
+
+module.exports = {
+ constants,
+ getDefaultSettings,
+ getPackedSettings,
+ getUnpackedSettings,
+ createServer,
+ createSecureServer,
+ connect
+};
diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js
index 9b56faa75b6158..01a16a9f0c0936 100644
--- a/lib/internal/bootstrap_node.js
+++ b/lib/internal/bootstrap_node.js
@@ -498,6 +498,11 @@
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
+ const config = process.binding('config');
+
+ if (!config.exposeHTTP2)
+ delete NativeModule._source.http2;
+
NativeModule.require = function(id) {
if (id === 'native_module') {
return NativeModule;
@@ -536,8 +541,6 @@
return NativeModule._source.hasOwnProperty(id);
};
- const config = process.binding('config');
-
if (config.exposeInternals) {
NativeModule.nonInternalExists = NativeModule.exists;
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 3b54dcea934f83..b7dd509070731d 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -119,6 +119,69 @@ E('ERR_HTTP_HEADERS_SENT',
E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s');
E('ERR_HTTP_TRAILER_INVALID',
'Trailers are invalid with this transfer encoding');
+E('ERR_HTTP_INVALID_CHAR', 'Invalid character in statusMessage.');
+E('ERR_HTTP_INVALID_STATUS_CODE',
+ (originalStatusCode) => `Invalid status code: ${originalStatusCode}`);
+E('ERR_HTTP2_CONNECT_AUTHORITY',
+ ':authority header is required for CONNECT requests');
+E('ERR_HTTP2_CONNECT_PATH',
+ 'The :path header is forbidden for CONNECT requests');
+E('ERR_HTTP2_CONNECT_SCHEME',
+ 'The :scheme header is forbidden for CONNECT requests');
+E('ERR_HTTP2_FRAME_ERROR',
+ (type, code, id) => {
+ let msg = `Error sending frame type ${type}`;
+ if (id !== undefined)
+ msg += ` for stream ${id}`;
+ msg += ` with code ${code}`;
+ return msg;
+ });
+E('ERR_HTTP2_HEADER_REQUIRED',
+ (name) => `The ${name} header is required`);
+E('ERR_HTTP2_HEADER_SINGLE_VALUE',
+ (name) => `Header field "${name}" must have only a single value`);
+E('ERR_HTTP2_HEADERS_OBJECT', 'Headers must be an object');
+E('ERR_HTTP2_HEADERS_SENT', 'Response has already been initiated.');
+E('ERR_HTTP2_HEADERS_AFTER_RESPOND',
+ 'Cannot specify additional headers after response initiated');
+E('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND',
+ 'Cannot send informational headers after the HTTP message has been sent');
+E('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED',
+ 'Informational status codes cannot be used');
+E('ERR_HTTP2_INVALID_CONNECTION_HEADERS',
+ 'HTTP/1 Connection specific headers are forbidden');
+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');
+E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed');
+E('ERR_HTTP2_INVALID_SETTING_VALUE',
+ (name, value) => `Invalid value for setting "${name}": ${value}`);
+E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
+ (max) => `Maximum number of pending settings acknowledgements (${max})`);
+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_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');
+E('ERR_HTTP2_SOCKET_BOUND',
+ 'The socket is already bound to an Http2Session');
+E('ERR_HTTP2_STATUS_INVALID',
+ (code) => `Invalid status code: ${code}`);
+E('ERR_HTTP2_STATUS_101',
+ 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2');
+E('ERR_HTTP2_STREAM_CLOSED', 'The stream is already closed');
+E('ERR_HTTP2_STREAM_ERROR',
+ (code) => `Stream closed with error code ${code}`);
+E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', 'A stream cannot depend on itself');
+E('ERR_HTTP2_UNSUPPORTED_PROTOCOL',
+ (protocol) => `protocol "${protocol}" is unsupported.`);
E('ERR_INDEX_OUT_OF_RANGE', 'Index out of range');
E('ERR_INVALID_ARG_TYPE', invalidArgType);
E('ERR_INVALID_CALLBACK', 'callback must be a function');
@@ -156,6 +219,7 @@ E('ERR_SOCKET_BAD_TYPE',
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data');
E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running');
+E('ERR_OUTOFMEMORY', 'Out of memory');
E('ERR_STDERR_CLOSE', 'process.stderr cannot be closed');
E('ERR_STDOUT_CLOSE', 'process.stdout cannot be closed');
E('ERR_UNKNOWN_BUILTIN_MODULE', (id) => `No such built-in module: ${id}`);
diff --git a/lib/internal/http.js b/lib/internal/http.js
old mode 100755
new mode 100644
diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js
new file mode 100644
index 00000000000000..cd9a1fa2b7f5ed
--- /dev/null
+++ b/lib/internal/http2/compat.js
@@ -0,0 +1,570 @@
+'use strict';
+
+const Stream = require('stream');
+const Readable = Stream.Readable;
+const binding = process.binding('http2');
+const constants = binding.constants;
+const errors = require('internal/errors');
+
+const kFinish = Symbol('finish');
+const kBeginSend = Symbol('begin-send');
+const kState = Symbol('state');
+const kStream = Symbol('stream');
+const kRequest = Symbol('request');
+const kResponse = Symbol('response');
+const kHeaders = Symbol('headers');
+const kTrailers = Symbol('trailers');
+
+let statusMessageWarned = false;
+
+// Defines and implements an API compatibility layer on top of the core
+// HTTP/2 implementation, intended to provide an interface that is as
+// close as possible to the current require('http') API
+
+function assertValidHeader(name, value) {
+ if (isPseudoHeader(name))
+ throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED');
+ if (value === undefined || value === null)
+ throw new errors.TypeError('ERR_HTTP2_INVALID_HEADER_VALUE');
+}
+
+function isPseudoHeader(name) {
+ switch (name) {
+ case ':status':
+ return true;
+ case ':method':
+ return true;
+ case ':path':
+ return true;
+ case ':authority':
+ return true;
+ case ':scheme':
+ return true;
+ default:
+ return false;
+ }
+}
+
+function onStreamData(chunk) {
+ const request = this[kRequest];
+ if (!request.push(chunk))
+ this.pause();
+}
+
+function onStreamEnd() {
+ // Cause the request stream to end as well.
+ const request = this[kRequest];
+ request.push(null);
+}
+
+function onStreamError(error) {
+ const request = this[kRequest];
+ request.emit('error', error);
+}
+
+function onRequestPause() {
+ const stream = this[kStream];
+ stream.pause();
+}
+
+function onRequestResume() {
+ const stream = this[kStream];
+ stream.resume();
+}
+
+function onRequestDrain() {
+ if (this.isPaused())
+ this.resume();
+}
+
+function onStreamResponseDrain() {
+ const response = this[kResponse];
+ response.emit('drain');
+}
+
+function onStreamResponseError(error) {
+ const response = this[kResponse];
+ response.emit('error', error);
+}
+
+function onStreamClosedRequest() {
+ const req = this[kRequest];
+ req.push(null);
+}
+
+function onStreamClosedResponse() {
+ const res = this[kResponse];
+ res.writable = false;
+ res.emit('finish');
+}
+
+function onAborted(hadError, code) {
+ if ((this.writable) ||
+ (this._readableState && !this._readableState.ended)) {
+ this.emit('aborted', hadError, code);
+ }
+}
+
+class Http2ServerRequest extends Readable {
+ constructor(stream, headers, options) {
+ super(options);
+ this[kState] = {
+ statusCode: null,
+ closed: false,
+ closedCode: constants.NGHTTP2_NO_ERROR
+ };
+ this[kHeaders] = headers;
+ this[kStream] = stream;
+ stream[kRequest] = this;
+
+ // Pause the stream..
+ stream.pause();
+ stream.on('data', onStreamData);
+ stream.on('end', onStreamEnd);
+ stream.on('error', onStreamError);
+ stream.on('close', onStreamClosedRequest);
+ stream.on('aborted', onAborted.bind(this));
+ const onfinish = this[kFinish].bind(this);
+ stream.on('streamClosed', onfinish);
+ stream.on('finish', onfinish);
+ this.on('pause', onRequestPause);
+ this.on('resume', onRequestResume);
+ this.on('drain', onRequestDrain);
+ }
+
+ get closed() {
+ const state = this[kState];
+ return Boolean(state.closed);
+ }
+
+ get code() {
+ const state = this[kState];
+ return Number(state.closedCode);
+ }
+
+ get stream() {
+ return this[kStream];
+ }
+
+ get statusCode() {
+ return this[kState].statusCode;
+ }
+
+ get headers() {
+ return this[kHeaders];
+ }
+
+ get rawHeaders() {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return [];
+ const tuples = Object.entries(headers);
+ const flattened = Array.prototype.concat.apply([], tuples);
+ return flattened.map(String);
+ }
+
+ get trailers() {
+ return this[kTrailers];
+ }
+
+ get httpVersionMajor() {
+ return 2;
+ }
+
+ get httpVersionMinor() {
+ return 0;
+ }
+
+ get httpVersion() {
+ return '2.0';
+ }
+
+ get socket() {
+ return this.stream.session.socket;
+ }
+
+ get connection() {
+ return this.socket;
+ }
+
+ _read(nread) {
+ const stream = this[kStream];
+ if (stream) {
+ stream.resume();
+ } else {
+ throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+ }
+ }
+
+ get method() {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return;
+ return headers[constants.HTTP2_HEADER_METHOD];
+ }
+
+ get authority() {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return;
+ return headers[constants.HTTP2_HEADER_AUTHORITY];
+ }
+
+ get scheme() {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return;
+ return headers[constants.HTTP2_HEADER_SCHEME];
+ }
+
+ get url() {
+ return this.path;
+ }
+
+ set url(url) {
+ this.path = url;
+ }
+
+ get path() {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return;
+ return headers[constants.HTTP2_HEADER_PATH];
+ }
+
+ set path(path) {
+ let headers = this[kHeaders];
+ if (headers === undefined)
+ headers = this[kHeaders] = Object.create(null);
+ headers[constants.HTTP2_HEADER_PATH] = path;
+ }
+
+ setTimeout(msecs, callback) {
+ const stream = this[kStream];
+ if (stream === undefined) return;
+ stream.setTimeout(msecs, callback);
+ }
+
+ [kFinish](code) {
+ const state = this[kState];
+ if (state.closed)
+ return;
+ state.closedCode = code;
+ state.closed = true;
+ this.push(null);
+ this[kStream] = undefined;
+ }
+}
+
+class Http2ServerResponse extends Stream {
+ constructor(stream, options) {
+ super(options);
+ this[kState] = {
+ sendDate: true,
+ statusCode: constants.HTTP_STATUS_OK,
+ headerCount: 0,
+ trailerCount: 0,
+ closed: false,
+ closedCode: constants.NGHTTP2_NO_ERROR
+ };
+ this[kStream] = stream;
+ stream[kResponse] = this;
+ this.writable = true;
+ stream.on('drain', onStreamResponseDrain);
+ stream.on('error', onStreamResponseError);
+ stream.on('close', onStreamClosedResponse);
+ stream.on('aborted', onAborted.bind(this));
+ const onfinish = this[kFinish].bind(this);
+ stream.on('streamClosed', onfinish);
+ stream.on('finish', onfinish);
+ }
+
+ get finished() {
+ const stream = this[kStream];
+ return stream === undefined || stream._writableState.ended;
+ }
+
+ get closed() {
+ const state = this[kState];
+ return Boolean(state.closed);
+ }
+
+ get code() {
+ const state = this[kState];
+ return Number(state.closedCode);
+ }
+
+ get stream() {
+ return this[kStream];
+ }
+
+ get headersSent() {
+ const stream = this[kStream];
+ return stream.headersSent;
+ }
+
+ get sendDate() {
+ return Boolean(this[kState].sendDate);
+ }
+
+ set sendDate(bool) {
+ this[kState].sendDate = Boolean(bool);
+ }
+
+ get statusCode() {
+ return this[kState].statusCode;
+ }
+
+ set statusCode(code) {
+ const state = this[kState];
+ code |= 0;
+ if (code >= 100 && code < 200)
+ throw new errors.RangeError('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED');
+ if (code < 200 || code > 599)
+ throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID', code);
+ state.statusCode = code;
+ }
+
+ addTrailers(headers) {
+ let trailers = this[kTrailers];
+ const keys = Object.keys(headers);
+ let key = '';
+ if (keys.length > 0)
+ return;
+ if (trailers === undefined)
+ trailers = this[kTrailers] = Object.create(null);
+ for (var i = 0; i < keys.length; i++) {
+ key = String(keys[i]).trim().toLowerCase();
+ const value = headers[key];
+ assertValidHeader(key, value);
+ trailers[key] = String(value);
+ }
+ }
+
+ getHeader(name) {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return;
+ name = String(name).trim().toLowerCase();
+ return headers[name];
+ }
+
+ getHeaderNames() {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return [];
+ return Object.keys(headers);
+ }
+
+ getHeaders() {
+ const headers = this[kHeaders];
+ return Object.assign({}, headers);
+ }
+
+ hasHeader(name) {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return false;
+ name = String(name).trim().toLowerCase();
+ return Object.prototype.hasOwnProperty.call(headers, name);
+ }
+
+ removeHeader(name) {
+ const headers = this[kHeaders];
+ if (headers === undefined)
+ return;
+ name = String(name).trim().toLowerCase();
+ delete headers[name];
+ }
+
+ setHeader(name, value) {
+ name = String(name).trim().toLowerCase();
+ assertValidHeader(name, value);
+ let headers = this[kHeaders];
+ if (headers === undefined)
+ headers = this[kHeaders] = Object.create(null);
+ headers[name] = String(value);
+ }
+
+ flushHeaders() {
+ if (this[kStream].headersSent === false)
+ this[kBeginSend]();
+ }
+
+ writeHead(statusCode, statusMessage, headers) {
+ if (typeof statusMessage === 'string' && statusMessageWarned === false) {
+ process.emitWarning(
+ 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)',
+ 'UnsupportedWarning'
+ );
+ statusMessageWarned = true;
+ }
+ if (headers === undefined && typeof statusMessage === 'object') {
+ headers = statusMessage;
+ }
+ if (headers) {
+ const keys = Object.keys(headers);
+ let key = '';
+ for (var i = 0; i < keys.length; i++) {
+ key = keys[i];
+ this.setHeader(key, headers[key]);
+ }
+ }
+ this.statusCode = statusCode;
+ }
+
+ write(chunk, encoding, cb) {
+ const stream = this[kStream];
+
+ if (typeof encoding === 'function') {
+ cb = encoding;
+ encoding = 'utf8';
+ }
+
+ if (stream === undefined) {
+ const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+ if (cb)
+ process.nextTick(cb, err);
+ else
+ throw err;
+ return;
+ }
+ this[kBeginSend]();
+ return stream.write(chunk, encoding, cb);
+ }
+
+ end(chunk, encoding, cb) {
+ const stream = this[kStream];
+
+ if (typeof chunk === 'function') {
+ cb = chunk;
+ chunk = null;
+ encoding = 'utf8';
+ } else if (typeof encoding === 'function') {
+ cb = encoding;
+ encoding = 'utf8';
+ }
+ if (chunk !== null && chunk !== undefined) {
+ this.write(chunk, encoding);
+ }
+
+ if (typeof cb === 'function' && stream !== undefined) {
+ stream.once('finish', cb);
+ }
+
+ this[kBeginSend]({endStream: true});
+
+ if (stream !== undefined) {
+ stream.end();
+ }
+ }
+
+ destroy(err) {
+ const stream = this[kStream];
+ if (stream === undefined) {
+ // nothing to do, already closed
+ return;
+ }
+ stream.destroy(err);
+ }
+
+ setTimeout(msecs, callback) {
+ const stream = this[kStream];
+ if (stream === undefined) return;
+ stream.setTimeout(msecs, callback);
+ }
+
+ sendContinue(headers) {
+ this.sendInfo(100, headers);
+ }
+
+ sendInfo(code, headers) {
+ const stream = this[kStream];
+ if (stream.headersSent === true) {
+ throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND');
+ }
+ if (headers && typeof headers !== 'object')
+ throw new errors.TypeError('ERR_HTTP2_HEADERS_OBJECT');
+ if (stream === undefined) return;
+ code |= 0;
+ if (code < 100 || code >= 200)
+ throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS', code);
+
+ headers[constants.HTTP2_HEADER_STATUS] = code;
+ stream.respond(headers);
+ }
+
+ createPushResponse(headers, callback) {
+ const stream = this[kStream];
+ if (stream === undefined) {
+ throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+ }
+ stream.pushStream(headers, {}, function(stream, headers, options) {
+ const response = new Http2ServerResponse(stream);
+ callback(null, response);
+ });
+ }
+
+ [kBeginSend](options) {
+ const stream = this[kStream];
+ if (stream !== undefined && stream.headersSent === false) {
+ const state = this[kState];
+ const headers = this[kHeaders] || Object.create(null);
+ headers[constants.HTTP2_HEADER_STATUS] = state.statusCode;
+ if (stream.finished === true)
+ options.endStream = true;
+ if (stream.destroyed === false) {
+ stream.respond(headers, options);
+ }
+ }
+ }
+
+ [kFinish](code) {
+ const state = this[kState];
+ if (state.closed)
+ return;
+ state.closedCode = code;
+ state.closed = true;
+ this.end();
+ this[kStream] = undefined;
+ this.emit('finish');
+ }
+}
+
+function onServerStream(stream, headers, flags) {
+ const server = this;
+ const request = new Http2ServerRequest(stream, headers);
+ const response = new Http2ServerResponse(stream);
+
+ // Check for the CONNECT method
+ const method = headers[constants.HTTP2_HEADER_METHOD];
+ if (method === 'CONNECT') {
+ if (!server.emit('connect', request, response)) {
+ response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED;
+ response.end();
+ }
+ return;
+ }
+
+ // Check for Expectations
+ if (headers.expect !== undefined) {
+ if (headers.expect === '100-continue') {
+ if (server.listenerCount('checkContinue')) {
+ server.emit('checkContinue', request, response);
+ } else {
+ response.sendContinue();
+ server.emit('request', request, response);
+ }
+ } else if (server.listenerCount('checkExpectation')) {
+ server.emit('checkExpectation', request, response);
+ } else {
+ response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED;
+ response.end();
+ }
+ return;
+ }
+
+ server.emit('request', request, response);
+}
+
+module.exports = { onServerStream };
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
new file mode 100644
index 00000000000000..1bdd57926c4e62
--- /dev/null
+++ b/lib/internal/http2/core.js
@@ -0,0 +1,2392 @@
+'use strict';
+
+/* eslint-disable no-use-before-define */
+
+const binding = process.binding('http2');
+const debug = require('util').debuglog('http2');
+const assert = require('assert');
+const Buffer = require('buffer').Buffer;
+const EventEmitter = require('events');
+const net = require('net');
+const tls = require('tls');
+const util = require('util');
+const fs = require('fs');
+const errors = require('internal/errors');
+const { Duplex } = require('stream');
+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,
+ assertValidPseudoHeaderResponse,
+ assertValidPseudoHeaderTrailer,
+ assertWithinRange,
+ getDefaultSettings,
+ getSessionState,
+ getSettings,
+ getStreamState,
+ isPayloadMeaningless,
+ mapToHeaders,
+ NghttpError,
+ toHeaderObject,
+ updateOptionsBuffer,
+ updateSettingsBuffer
+} = require('internal/http2/util');
+
+const {
+ _unrefActive,
+ enroll,
+ unenroll
+} = require('timers');
+
+const { WriteWrap } = process.binding('stream_wrap');
+const { constants } = binding;
+
+const NETServer = net.Server;
+const TLSServer = tls.Server;
+
+const kInspect = require('internal/util').customInspectSymbol;
+
+const kAuthority = Symbol('authority');
+const kDestroySocket = Symbol('destroy-socket');
+const kHandle = Symbol('handle');
+const kID = Symbol('id');
+const kInit = Symbol('init');
+const kLocalSettings = Symbol('local-settings');
+const kOptions = Symbol('options');
+const kOwner = Symbol('owner');
+const kProceed = Symbol('proceed');
+const kProtocol = Symbol('protocol');
+const kRemoteSettings = Symbol('remote-settings');
+const kServer = Symbol('server');
+const kSession = Symbol('session');
+const kSocket = Symbol('socket');
+const kState = Symbol('state');
+const kType = Symbol('type');
+
+const kDefaultSocketTimeout = 2 * 60 * 1000;
+const kRenegTest = /TLS session renegotiation disabled for this socket/;
+
+const paddingBuffer = new Uint32Array(binding.paddingArrayBuffer);
+
+const {
+ NGHTTP2_CANCEL,
+ NGHTTP2_DEFAULT_WEIGHT,
+ NGHTTP2_FLAG_END_STREAM,
+ NGHTTP2_HCAT_HEADERS,
+ NGHTTP2_HCAT_PUSH_RESPONSE,
+ NGHTTP2_HCAT_RESPONSE,
+ NGHTTP2_INTERNAL_ERROR,
+ NGHTTP2_NO_ERROR,
+ NGHTTP2_PROTOCOL_ERROR,
+ NGHTTP2_REFUSED_STREAM,
+ NGHTTP2_SESSION_CLIENT,
+ NGHTTP2_SESSION_SERVER,
+ NGHTTP2_ERR_NOMEM,
+ NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
+ NGHTTP2_ERR_INVALID_ARGUMENT,
+ NGHTTP2_ERR_STREAM_CLOSED,
+
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_DATE,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_SCHEME,
+ 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,
+
+ HTTP_STATUS_CONTENT_RESET,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_NOT_MODIFIED,
+ HTTP_STATUS_SWITCHING_PROTOCOLS
+} = constants;
+
+function sessionName(type) {
+ switch (type) {
+ case NGHTTP2_SESSION_CLIENT:
+ return 'client';
+ case NGHTTP2_SESSION_SERVER:
+ return 'server';
+ default:
+ return '';
+ }
+}
+
+// Top level to avoid creating a closure
+function emit() {
+ this.emit.apply(this, arguments);
+}
+
+// Called when a new block of headers has been received for a given
+// stream. The stream may or may not be new. If the stream is new,
+// create the associated Http2Stream instance and emit the 'stream'
+// event. If the stream is not new, emit the 'headers' event to pass
+// the block of headers on.
+function onSessionHeaders(id, cat, flags, headers) {
+ _unrefActive(this);
+ const owner = this[kOwner];
+ debug(`[${sessionName(owner[kType])}] headers were received on ` +
+ `stream ${id}: ${cat}`);
+ const streams = owner[kState].streams;
+
+ const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM);
+ let stream = streams.get(id);
+
+ // Convert the array of header name value pairs into an object
+ const obj = toHeaderObject(headers);
+
+ if (stream === undefined) {
+ switch (owner[kType]) {
+ case NGHTTP2_SESSION_SERVER:
+ stream = new ServerHttp2Stream(owner, id,
+ { readable: !endOfStream },
+ obj);
+ if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
+ // For head requests, there must not be a body...
+ // end the writable side immediately.
+ stream.end();
+ const state = stream[kState];
+ state.headRequest = true;
+ }
+ break;
+ case NGHTTP2_SESSION_CLIENT:
+ stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
+ break;
+ default:
+ assert.fail(null, null,
+ 'Internal HTTP/2 Error. Invalid session type. Please ' +
+ 'report this as a bug in Node.js');
+ }
+ streams.set(id, stream);
+ process.nextTick(emit.bind(owner, 'stream', stream, obj, flags));
+ } else {
+ let event;
+ let status;
+ switch (cat) {
+ case NGHTTP2_HCAT_RESPONSE:
+ status = obj[HTTP2_HEADER_STATUS];
+ if (!endOfStream &&
+ status !== undefined &&
+ status >= 100 &&
+ status < 200) {
+ event = 'headers';
+ } else {
+ event = 'response';
+ }
+ break;
+ case NGHTTP2_HCAT_PUSH_RESPONSE:
+ event = 'push';
+ break;
+ case NGHTTP2_HCAT_HEADERS:
+ status = obj[HTTP2_HEADER_STATUS];
+ if (!endOfStream && status !== undefined && status >= 200) {
+ event = 'response';
+ } else {
+ event = endOfStream ? 'trailers' : 'headers';
+ }
+ break;
+ default:
+ assert.fail(null, null,
+ 'Internal HTTP/2 Error. Invalid headers category. Please ' +
+ 'report this as a bug in Node.js');
+ }
+ debug(`[${sessionName(owner[kType])}] emitting stream '${event}' event`);
+ process.nextTick(emit.bind(stream, event, obj, flags));
+ }
+}
+
+// Called to determine if there are trailers to be sent at the end of a
+// Stream. The 'fetchTrailers' event is emitted and passed a holder object.
+// The trailers to return are set on that object by the handler. Once the
+// event handler returns, those are sent off for processing. Note that this
+// is a necessarily synchronous operation. We need to know immediately if
+// there are trailing headers to send.
+function onSessionTrailers(id) {
+ const owner = this[kOwner];
+ debug(`[${sessionName(owner[kType])}] checking for trailers`);
+ const streams = owner[kState].streams;
+ const stream = streams.get(id);
+ // It should not be possible for the stream not to exist at this point.
+ // If it does not exist, there is something very very wrong.
+ assert(stream !== undefined,
+ 'Internal HTTP/2 Failure. Stream does not exist. Please ' +
+ 'report this as a bug in Node.js');
+
+ const trailers = Object.create(null);
+ stream.emit('fetchTrailers', trailers);
+ const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
+ if (!Array.isArray(headersList)) {
+ process.nextTick(() => stream.emit('error', headersList));
+ return;
+ }
+ return headersList;
+}
+
+// Called when the stream is closed. The streamClosed event is emitted on the
+// Http2Stream instance. Note that this event is distinctly different than the
+// require('stream') interface 'close' event which deals with the state of the
+// Readable and Writable sides of the Duplex.
+function onSessionStreamClose(id, code) {
+ const owner = this[kOwner];
+ debug(`[${sessionName(owner[kType])}] session is closing the stream ` +
+ `${id}: ${code}`);
+ const stream = owner[kState].streams.get(id);
+ if (stream === undefined)
+ return;
+ _unrefActive(this);
+ // Set the rst state for the stream
+ abort(stream);
+ const state = stream[kState];
+ state.rst = true;
+ state.rstCode = code;
+
+ if (state.fd !== undefined) {
+ debug(`Closing fd ${state.fd} for stream ${id}`);
+ fs.close(state.fd, afterFDClose.bind(stream));
+ }
+
+ setImmediate(stream.destroy.bind(stream));
+}
+
+function afterFDClose(err) {
+ if (err)
+ process.nextTick(() => this.emit('error', err));
+}
+
+// Called when an error event needs to be triggered
+function onSessionError(error) {
+ _unrefActive(this);
+ process.nextTick(() => this[kOwner].emit('error', error));
+}
+
+// Receives a chunk of data for a given stream and forwards it on
+// to the Http2Stream Duplex for processing.
+function onSessionRead(nread, buf, handle) {
+ const streams = this[kOwner][kState].streams;
+ const id = handle.id;
+ const stream = streams.get(id);
+ // It should not be possible for the stream to not exist at this point.
+ // If it does not, something is very very wrong
+ assert(stream !== undefined,
+ 'Internal HTTP/2 Failure. Stream does not exist. Please ' +
+ 'report this as a bug in Node.js');
+ const state = stream[kState];
+ _unrefActive(this); // Reset the session timeout timer
+ _unrefActive(stream); // Reset the stream timeout timer
+
+ if (nread >= 0) {
+ if (!stream.push(buf)) {
+ assert(this.streamReadStop(id) === undefined,
+ `HTTP/2 Stream ${id} does not exist. Please report this as ' +
+ 'a bug in Node.js`);
+ state.reading = false;
+ }
+ } else {
+ // Last chunk was received. End the readable side.
+ stream.push(null);
+ }
+}
+
+// Called when the remote peer settings have been updated.
+// Resets the cached settings.
+function onSettings(ack) {
+ const owner = this[kOwner];
+ debug(`[${sessionName(owner[kType])}] new settings received`);
+ _unrefActive(this);
+ let event = 'remoteSettings';
+ if (ack) {
+ if (owner[kState].pendingAck > 0)
+ owner[kState].pendingAck--;
+ owner[kLocalSettings] = undefined;
+ event = 'localSettings';
+ } else {
+ owner[kRemoteSettings] = undefined;
+ }
+ // Only emit the event if there are listeners registered
+ if (owner.listenerCount(event) > 0) {
+ const settings = event === 'localSettings' ?
+ owner.localSettings : owner.remoteSettings;
+ process.nextTick(emit.bind(owner, event, settings));
+ }
+}
+
+// If the stream exists, an attempt will be made to emit an event
+// on the stream object itself. Otherwise, forward it on to the
+// session (which may, in turn, forward it on to the server)
+function onPriority(id, parent, weight, exclusive) {
+ const owner = this[kOwner];
+ debug(`[${sessionName(owner[kType])}] priority advisement for stream ` +
+ `${id}: \n parent: ${parent},\n weight: ${weight},\n` +
+ ` exclusive: ${exclusive}`);
+ _unrefActive(this);
+ const streams = owner[kState].streams;
+ const stream = streams.get(id);
+ const emitter = stream === undefined ? owner : stream;
+ process.nextTick(
+ emit.bind(emitter, 'priority', id, parent, weight, exclusive));
+}
+
+function emitFrameError(id, type, code) {
+ if (!this.emit('frameError', type, code, id)) {
+ const err = new errors.Error('ERR_HTTP2_FRAME_ERROR', type, code, id);
+ err.errno = code;
+ this.emit('error', err);
+ }
+}
+
+// Called by the native layer when an error has occurred sending a
+// frame. This should be exceedingly rare.
+function onFrameError(id, type, code) {
+ const owner = this[kOwner];
+ debug(`[${sessionName(owner[kType])}] error sending frame type ` +
+ `${type} on stream ${id}, code: ${code}`);
+ _unrefActive(this);
+ const streams = owner[kState].streams;
+ const stream = streams.get(id);
+ const emitter = stream !== undefined ? stream : owner;
+ process.nextTick(emitFrameError.bind(emitter, id, type, code));
+}
+
+function emitGoaway(state, code, lastStreamID, buf) {
+ this.emit('goaway', code, lastStreamID, buf);
+ // Tear down the session or destroy
+ if (!state.shuttingDown && !state.shutdown) {
+ this.shutdown({}, this.destroy.bind(this));
+ } else {
+ this.destroy();
+ }
+}
+
+// Called by the native layer when a goaway frame has been received
+function onGoawayData(code, lastStreamID, buf) {
+ const owner = this[kOwner];
+ const state = owner[kState];
+ debug(`[${sessionName(owner[kType])}] goaway data received`);
+ process.nextTick(emitGoaway.bind(owner, state, code, lastStreamID, buf));
+}
+
+// Returns the padding to use per frame. The selectPadding callback is set
+// on the options. It is invoked with two arguments, the frameLen, and the
+// maxPayloadLen. The method must return a numeric value within the range
+// frameLen <= n <= maxPayloadLen.
+function onSelectPadding(fn) {
+ assert(typeof fn === 'function',
+ 'options.selectPadding must be a function. Please report this as a ' +
+ 'bug in Node.js');
+ return function getPadding() {
+ debug('fetching padding for frame');
+ const frameLen = paddingBuffer[0];
+ const maxFramePayloadLen = paddingBuffer[1];
+ paddingBuffer[2] = Math.min(maxFramePayloadLen,
+ Math.max(frameLen,
+ fn(frameLen,
+ maxFramePayloadLen) | 0));
+ };
+}
+
+// When a ClientHttp2Session is first created, the socket may not yet be
+// connected. If request() is called during this time, the actual request
+// will be deferred until the socket is ready to go.
+function requestOnConnect(headers, options) {
+ const session = this[kSession];
+ debug(`[${sessionName(session[kType])}] connected.. initializing request`);
+ const streams = session[kState].streams;
+ // ret will be either the reserved stream ID (if positive)
+ // or an error code (if negative)
+ validatePriorityOptions(options);
+ const handle = session[kHandle];
+
+ const headersList = mapToHeaders(headers);
+ if (!Array.isArray(headersList)) {
+ process.nextTick(() => this.emit('error', headersList));
+ return;
+ }
+
+ const ret = handle.submitRequest(headersList,
+ !!options.endStream,
+ options.parent | 0,
+ options.weight | 0,
+ !!options.exclusive);
+
+ // In an error condition, one of three possible response codes will be
+ // possible:
+ // * NGHTTP2_ERR_NOMEM - Out of memory, this should be fatal to the process.
+ // * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE - Maximum stream ID is reached, this
+ // is fatal for the session
+ // * NGHTTP2_ERR_INVALID_ARGUMENT - Stream was made dependent on itself, this
+ // impacts on this stream.
+ // For the first two, emit the error on the session,
+ // For the third, emit the error on the stream, it will bubble up to the
+ // session if not handled.
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => session.emit('error', err));
+ break;
+ case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
+ err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ case NGHTTP2_ERR_INVALID_ARGUMENT:
+ err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ default:
+ // Some other, unexpected error was returned. Emit on the session.
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => session.emit('error', err));
+ break;
+ }
+ debug(`[${sessionName(session[kType])}] stream ${ret} initialized`);
+ this[kInit](ret);
+ streams.set(ret, this);
+ }
+}
+
+function validatePriorityOptions(options) {
+ if (options.weight === undefined)
+ options.weight = NGHTTP2_DEFAULT_WEIGHT;
+ else if (typeof options.weight !== 'number') {
+ const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
+ 'weight',
+ options.weight);
+ Error.captureStackTrace(err, validatePriorityOptions);
+ throw err;
+ }
+
+ if (options.parent === undefined)
+ options.parent = 0;
+ else if (typeof options.parent !== 'number' || options.parent < 0) {
+ const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
+ 'parent',
+ options.parent);
+ Error.captureStackTrace(err, validatePriorityOptions);
+ throw err;
+ }
+
+ if (options.exclusive === undefined)
+ options.exclusive = false;
+ else if (typeof options.exclusive !== 'boolean') {
+ const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
+ 'exclusive',
+ options.exclusive);
+ Error.captureStackTrace(err, validatePriorityOptions);
+ throw err;
+ }
+
+ if (options.silent === undefined)
+ options.silent = false;
+ else if (typeof options.silent !== 'boolean') {
+ const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
+ 'silent',
+ options.silent);
+ Error.captureStackTrace(err, validatePriorityOptions);
+ throw err;
+ }
+}
+
+// Creates the internal binding.Http2Session handle for an Http2Session
+// instance. This occurs only after the socket connection has been
+// established. Note: the binding.Http2Session will take over ownership
+// of the socket. No other code should read from or write to the socket.
+function setupHandle(session, socket, type, options) {
+ return function() {
+ debug(`[${sessionName(type)}] setting up session handle`);
+ session[kState].connecting = false;
+
+ updateOptionsBuffer(options);
+ const handle = new binding.Http2Session(type);
+ handle[kOwner] = session;
+ handle.onpriority = onPriority;
+ handle.onsettings = onSettings;
+ handle.onheaders = onSessionHeaders;
+ handle.ontrailers = onSessionTrailers;
+ handle.onstreamclose = onSessionStreamClose;
+ handle.onerror = onSessionError;
+ handle.onread = onSessionRead;
+ handle.onframeerror = onFrameError;
+ handle.ongoawaydata = onGoawayData;
+
+ if (typeof options.selectPadding === 'function')
+ handle.ongetpadding = onSelectPadding(options.selectPadding);
+
+ assert(socket._handle !== undefined,
+ 'Internal HTTP/2 Failure. The socket is not connected. Please ' +
+ 'report this as a bug in Node.js');
+ handle.consume(socket._handle._externalStream);
+
+ session[kHandle] = handle;
+
+ const settings = typeof options.settings === 'object' ?
+ options.settings : Object.create(null);
+
+ session.settings(settings);
+ process.nextTick(emit.bind(session, 'connect', session, socket));
+ };
+}
+
+// Submits a SETTINGS frame to be sent to the remote peer.
+function submitSettings(settings) {
+ debug(`[${sessionName(this[kType])}] submitting actual settings`);
+ _unrefActive(this);
+ this[kLocalSettings] = undefined;
+
+ updateSettingsBuffer(settings);
+ const handle = this[kHandle];
+ const ret = handle.submitSettings();
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ default:
+ // Some other unexpected error was reported.
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ }
+ }
+ debug(`[${sessionName(this[kType])}] settings complete`);
+}
+
+// Submits a PRIORITY frame to be sent to the remote peer
+// Note: If the silent option is true, the change will be made
+// locally with no PRIORITY frame sent.
+function submitPriority(stream, options) {
+ debug(`[${sessionName(this[kType])}] submitting actual priority`);
+ _unrefActive(this);
+
+ const handle = this[kHandle];
+ const ret =
+ handle.submitPriority(
+ stream[kID],
+ options.parent | 0,
+ options.weight | 0,
+ !!options.exclusive,
+ !!options.silent);
+
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ default:
+ // Some other unexpected error was reported.
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ }
+ }
+ debug(`[${sessionName(this[kType])}] priority complete`);
+}
+
+// Submit an RST-STREAM frame to be sent to the remote peer.
+// This will cause the Http2Stream to be closed.
+function submitRstStream(stream, code) {
+ debug(`[${sessionName(this[kType])}] submit actual rststream`);
+ _unrefActive(this);
+ const id = stream[kID];
+ const handle = this[kHandle];
+ const ret = handle.submitRstStream(id, code);
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ default:
+ // Some other unexpected error was reported.
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ break;
+ }
+ stream.destroy();
+ }
+ debug(`[${sessionName(this[kType])}] rststream complete`);
+}
+
+function doShutdown(options) {
+ const handle = this[kHandle];
+ const state = this[kState];
+ if (handle === undefined || state.shutdown)
+ return; // Nothing to do, possibly because the session shutdown already.
+ const ret = handle.submitGoaway(options.errorCode | 0,
+ options.lastStreamID | 0,
+ options.opaqueData);
+ state.shuttingDown = false;
+ state.shutdown = true;
+ if (ret < 0) {
+ debug(`[${sessionName(this[kType])}] shutdown failed! code: ${ret}`);
+ const err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ return;
+ }
+ process.nextTick(emit.bind(this, 'shutdown', options));
+ debug(`[${sessionName(this[kType])}] shutdown is complete`);
+}
+
+// Submit a graceful or immediate shutdown request for the Http2Session.
+function submitShutdown(options) {
+ debug(`[${sessionName(this[kType])}] submitting actual shutdown request`);
+ const handle = this[kHandle];
+ const type = this[kType];
+ if (type === NGHTTP2_SESSION_SERVER &&
+ options.graceful === true) {
+ // first send a shutdown notice
+ handle.submitShutdownNotice();
+ // then, on flip of the event loop, do the actual shutdown
+ setImmediate(doShutdown.bind(this, options));
+ } else {
+ doShutdown.call(this, options);
+ }
+}
+
+function finishSessionDestroy(socket) {
+ if (!socket.destroyed)
+ socket.destroy();
+
+ // Destroy the handle
+ const handle = this[kHandle];
+ if (handle !== undefined) {
+ handle.destroy();
+ debug(`[${sessionName(this[kType])}] nghttp2session handle destroyed`);
+ }
+
+ this.emit('close');
+ debug(`[${sessionName(this[kType])}] nghttp2session destroyed`);
+}
+
+// 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 {
+
+ // type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT
+ // options { Object }
+ // socket { net.Socket | tls.TLSSocket }
+ constructor(type, options, socket) {
+ super();
+
+ // No validation is performed on the input parameters because this
+ // constructor is not exported directly for users.
+
+ // If the session property already exists on the socket,
+ // then it has already been bound to an Http2Session instance
+ // and cannot be attached again.
+ if (socket[kSession] !== undefined)
+ throw new errors.Error('ERR_HTTP2_SOCKET_BOUND');
+
+ socket[kSession] = this;
+
+ this[kState] = {
+ streams: new Map(),
+ destroyed: false,
+ shutdown: false,
+ shuttingDown: false,
+ pendingAck: 0,
+ maxPendingAck: Math.max(1, (options.maxPendingAck | 0) || 10)
+ };
+
+ this[kType] = type;
+ this[kSocket] = socket;
+
+ // Do not use nagle's algorithm
+ socket.setNoDelay();
+
+ // Disable TLS renegotiation on the socket
+ if (typeof socket.disableRenegotiation === 'function')
+ socket.disableRenegotiation();
+
+ socket[kDestroySocket] = socket.destroy;
+ socket.destroy = socketDestroy;
+
+ const setupFn = setupHandle(this, socket, type, options);
+ if (socket.connecting) {
+ this[kState].connecting = true;
+ socket.once('connect', setupFn);
+ } else {
+ setupFn();
+ }
+
+ // Any individual session can have any number of active open
+ // streams, these may all need to be made aware of changes
+ // in state that occur -- such as when the associated socket
+ // is closed. To do so, we need to set the max listener count
+ // to something more reasonable because we may have any number
+ // of concurrent streams (2^31-1 is the upper limit on the number
+ // of streams)
+ this.setMaxListeners((2 ** 31) - 1);
+ debug(`[${sessionName(type)}] http2session created`);
+ }
+
+ [kInspect](depth, opts) {
+ const state = this[kState];
+ const obj = {
+ type: this[kType],
+ destroyed: state.destroyed,
+ destroying: state.destroying,
+ shutdown: state.shutdown,
+ shuttingDown: state.shuttingDown,
+ state: this.state,
+ localSettings: this.localSettings,
+ remoteSettings: this.remoteSettings
+ };
+ return `Http2Session ${util.format(obj)}`;
+ }
+
+ // The socket owned by this session
+ get socket() {
+ return this[kSocket];
+ }
+
+ // The session type
+ get type() {
+ return this[kType];
+ }
+
+ // true if the Http2Session is waiting for a settings acknowledgement
+ get pendingSettingsAck() {
+ return this[kState].pendingAck > 0;
+ }
+
+ // true if the Http2Session has been destroyed
+ get destroyed() {
+ return this[kState].destroyed;
+ }
+
+ // Retrieves state information for the Http2Session
+ get state() {
+ const handle = this[kHandle];
+ return handle !== undefined ?
+ getSessionState(handle) :
+ Object.create(null);
+ }
+
+ // The settings currently in effect for the local peer. These will
+ // be updated only when a settings acknowledgement has been received.
+ get localSettings() {
+ let settings = this[kLocalSettings];
+ if (settings !== undefined)
+ return settings;
+
+ const handle = this[kHandle];
+ if (handle === undefined)
+ return Object.create(null);
+
+ settings = getSettings(handle, false); // Local
+ this[kLocalSettings] = settings;
+ return settings;
+ }
+
+ // The settings currently in effect for the remote peer.
+ get remoteSettings() {
+ let settings = this[kRemoteSettings];
+ if (settings !== undefined)
+ return settings;
+
+ const handle = this[kHandle];
+ if (handle === undefined)
+ return Object.create(null);
+
+ settings = getSettings(handle, true); // Remote
+ this[kRemoteSettings] = settings;
+ return settings;
+ }
+
+ // Submits a SETTINGS frame to be sent to the remote peer.
+ settings(settings) {
+ if (this[kState].destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
+ // Validate the input first
+ assertIsObject(settings, 'settings');
+ settings = Object.assign(Object.create(null), settings);
+ 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;
+ }
+ if (this[kState].pendingAck === this[kState].maxPendingAck) {
+ throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
+ this[kState].pendingAck);
+ }
+ debug(`[${sessionName(this[kType])}] sending settings`);
+
+ this[kState].pendingAck++;
+ if (this[kState].connecting) {
+ debug(`[${sessionName(this[kType])}] session still connecting, ` +
+ 'queue settings');
+ this.once('connect', submitSettings.bind(this, settings));
+ return;
+ }
+ submitSettings.call(this, settings);
+ }
+
+ // Submits a PRIORITY frame to be sent to the remote peer.
+ priority(stream, options) {
+ if (this[kState].destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
+ if (!(stream instanceof Http2Stream)) {
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ 'stream',
+ 'Http2Stream');
+ }
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+ validatePriorityOptions(options);
+
+ debug(`[${sessionName(this[kType])}] sending priority for stream ` +
+ `${stream[kID]}`);
+
+ // A stream cannot be made to depend on itself
+ if (options.parent === stream[kID]) {
+ debug(`[${sessionName(this[kType])}] session still connecting. queue ` +
+ 'priority');
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'parent',
+ options.parent);
+ }
+
+ if (stream[kID] === undefined) {
+ stream.once('ready', submitPriority.bind(this, stream, options));
+ return;
+ }
+ submitPriority.call(this, stream, options);
+ }
+
+ // Submits an RST-STREAM frame to be sent to the remote peer. This will
+ // cause the stream to be closed.
+ rstStream(stream, code = NGHTTP2_NO_ERROR) {
+ if (this[kState].destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
+ if (!(stream instanceof Http2Stream)) {
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ 'stream',
+ 'Http2Stream');
+ }
+
+ if (typeof code !== 'number') {
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ 'code',
+ 'number');
+ }
+
+ if (this[kState].rst) {
+ // rst has already been called, do not call again,
+ // skip straight to destroy
+ stream.destroy();
+ return;
+ }
+ stream[kState].rst = true;
+ stream[kState].rstCode = code;
+
+ debug(`[${sessionName(this[kType])}] initiating rststream for stream ` +
+ `${stream[kID]}: ${code}`);
+
+ if (stream[kID] === undefined) {
+ debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
+ 'rststream');
+ stream.once('ready', submitRstStream.bind(this, stream, code));
+ return;
+ }
+ submitRstStream.call(this, stream, code);
+ }
+
+ // Destroy the Http2Session
+ destroy() {
+ const state = this[kState];
+ if (state.destroyed || state.destroying)
+ return;
+
+ debug(`[${sessionName(this[kType])}] destroying nghttp2session`);
+ state.destroying = true;
+
+ // Unenroll the timer
+ unenroll(this);
+
+ // Shut down any still open streams
+ const streams = state.streams;
+ streams.forEach((stream) => stream.destroy());
+
+ // Disassociate from the socket and server
+ const socket = this[kSocket];
+ // socket.pause();
+ delete this[kSocket];
+ delete this[kServer];
+
+ state.destroyed = true;
+ state.destroying = false;
+
+ setImmediate(finishSessionDestroy.bind(this, socket));
+ }
+
+ // Graceful or immediate shutdown of the Http2Session. Graceful shutdown
+ // is only supported on the server-side
+ shutdown(options, callback) {
+ if (this[kState].destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
+ if (this[kState].shutdown || this[kState].shuttingDown)
+ return;
+
+ debug(`[${sessionName(this[kType])}] initiating shutdown`);
+ this[kState].shuttingDown = true;
+
+ const type = this[kType];
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+
+ if (options.opaqueData !== undefined &&
+ !Buffer.isBuffer(options.opaqueData)) {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'opaqueData',
+ options.opaqueData);
+ }
+ if (type === NGHTTP2_SESSION_SERVER &&
+ options.graceful !== undefined &&
+ typeof options.graceful !== 'boolean') {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'graceful',
+ options.graceful);
+ }
+ if (options.errorCode !== undefined &&
+ typeof options.errorCode !== 'number') {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'errorCode',
+ options.errorCode);
+ }
+ if (options.lastStreamID !== undefined &&
+ (typeof options.lastStreamID !== 'number' ||
+ options.lastStreamID < 0)) {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'lastStreamID',
+ options.lastStreamID);
+ }
+
+ if (options.opaqueData !== undefined &&
+ !Buffer.isBuffer(options.opaqueData)) {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'opaqueData',
+ options.opaqueData);
+ }
+
+ if (callback) {
+ this.on('shutdown', callback);
+ }
+
+ if (this[kState].connecting) {
+ debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
+ 'shutdown');
+ this.once('connect', submitShutdown.bind(this, options));
+ return;
+ }
+
+ debug(`[${sessionName(this[kType])}] sending shutdown`);
+ submitShutdown.call(this, options);
+ }
+
+ _onTimeout() {
+ this.emit('timeout');
+ }
+}
+
+class ServerHttp2Session extends Http2Session {
+ constructor(options, socket, server) {
+ super(NGHTTP2_SESSION_SERVER, options, socket);
+ this[kServer] = server;
+ }
+
+ get server() {
+ return this[kServer];
+ }
+}
+
+class ClientHttp2Session extends Http2Session {
+ constructor(options, socket) {
+ super(NGHTTP2_SESSION_CLIENT, options, socket);
+ debug(`[${sessionName(this[kType])}] clienthttp2session created`);
+ }
+
+ // Submits a new HTTP2 request to the connected peer. Returns the
+ // associated Http2Stream instance.
+ request(headers, options) {
+ if (this[kState].destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+ debug(`[${sessionName(this[kType])}] initiating request`);
+ _unrefActive(this);
+ assertIsObject(headers, 'headers');
+ assertIsObject(options, 'options');
+
+ headers = Object.assign(Object.create(null), headers);
+ options = Object.assign(Object.create(null), options);
+
+ if (headers[HTTP2_HEADER_METHOD] === undefined)
+ headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
+
+ const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
+
+ if (!connect) {
+ if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
+ headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
+ if (headers[HTTP2_HEADER_SCHEME] === undefined)
+ headers[HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1);
+ if (headers[HTTP2_HEADER_PATH] === undefined)
+ headers[HTTP2_HEADER_PATH] = '/';
+ } else {
+ if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
+ throw new errors.Error('ERR_HTTP2_CONNECT_AUTHORITY');
+ if (headers[HTTP2_HEADER_SCHEME] !== undefined)
+ throw new errors.Error('ERR_HTTP2_CONNECT_SCHEME');
+ if (headers[HTTP2_HEADER_PATH] !== undefined)
+ throw new errors.Error('ERR_HTTP2_CONNECT_PATH');
+ }
+
+ validatePriorityOptions(options);
+
+ if (options.endStream === undefined) {
+ // For some methods, we know that a payload is meaningless, so end the
+ // stream by default if the user has not specifically indicated a
+ // preference.
+ options.endStream = isPayloadMeaningless(headers[HTTP2_HEADER_METHOD]);
+ } else if (typeof options.endStream !== 'boolean') {
+ throw new errors.RangeError('ERR_INVALID_OPT_VALUE',
+ 'endStream',
+ options.endStream);
+ }
+
+ const stream = new ClientHttp2Stream(this, undefined, {});
+ const onConnect = requestOnConnect.bind(stream, headers, options);
+
+ // Close the writable side of the stream if options.endStream is set.
+ if (options.endStream)
+ stream.end();
+
+ if (this[kState].connecting) {
+ debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
+ 'stream init');
+ stream.on('connect', onConnect);
+ } else {
+ debug(`[${sessionName(this[kType])}] session connected, immediate ` +
+ 'stream init');
+ onConnect();
+ }
+ return stream;
+ }
+}
+
+function createWriteReq(req, handle, data, encoding) {
+ switch (encoding) {
+ case 'latin1':
+ case 'binary':
+ return handle.writeLatin1String(req, data);
+ case 'buffer':
+ return handle.writeBuffer(req, data);
+ case 'utf8':
+ case 'utf-8':
+ return handle.writeUtf8String(req, data);
+ case 'ascii':
+ return handle.writeAsciiString(req, data);
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return handle.writeUcs2String(req, data);
+ default:
+ return handle.writeBuffer(req, Buffer.from(data, encoding));
+ }
+}
+
+function afterDoStreamWrite(status, handle, req) {
+ _unrefActive(handle[kOwner]);
+ if (typeof req.callback === 'function')
+ req.callback();
+ this.handle = undefined;
+}
+
+function onHandleFinish() {
+ const session = this[kSession];
+ if (session === undefined) return;
+ if (this[kID] === undefined) {
+ this.once('ready', onHandleFinish.bind(this));
+ } else {
+ const handle = session[kHandle];
+ if (handle !== undefined) {
+ // Shutdown on the next tick of the event loop just in case there is
+ // still data pending in the outbound queue.
+ assert(handle.shutdownStream(this[kID]) === undefined,
+ `The stream ${this[kID]} does not exist. Please report this as ` +
+ 'a bug in Node.js');
+ }
+ }
+}
+
+function onSessionClose(hadError, code) {
+ abort(this);
+ // Close the readable side
+ this.push(null);
+ // Close the writable side
+ this.end();
+}
+
+function onStreamClosed(code) {
+ abort(this);
+ // Close the readable side
+ this.push(null);
+ // Close the writable side
+ this.end();
+}
+
+function streamOnResume() {
+ if (this._paused)
+ return this.pause();
+ if (this[kID] === undefined) {
+ this.once('ready', streamOnResume.bind(this));
+ return;
+ }
+ const session = this[kSession];
+ const state = this[kState];
+ if (session && !state.reading) {
+ state.reading = true;
+ assert(session[kHandle].streamReadStart(this[kID]) === undefined,
+ 'HTTP/2 Stream #{this[kID]} does not exist. Please report this as ' +
+ 'a bug in Node.js');
+ }
+}
+
+function streamOnPause() {
+ const session = this[kSession];
+ const state = this[kState];
+ if (session && state.reading) {
+ state.reading = false;
+ assert(session[kHandle].streamReadStop(this[kID]) === undefined,
+ `HTTP/2 Stream ${this[kID]} does not exist. Please report this as ' +
+ 'a bug in Node.js`);
+ }
+}
+
+function streamOnDrain() {
+ const needPause = 0 > this._writableState.highWaterMark;
+ if (this._paused && !needPause) {
+ this._paused = false;
+ this.resume();
+ }
+}
+
+function streamOnSessionConnect() {
+ const session = this[kSession];
+ debug(`[${sessionName(session[kType])}] session connected. emiting stream ` +
+ 'connect');
+ this[kState].connecting = false;
+ process.nextTick(emit.bind(this, 'connect'));
+}
+
+function streamOnceReady() {
+ const session = this[kSession];
+ debug(`[${sessionName(session[kType])}] stream ${this[kID]} is ready`);
+ this.uncork();
+}
+
+function abort(stream) {
+ if (!stream[kState].aborted &&
+ stream._writableState &&
+ !(stream._writableState.ended || stream._writableState.ending)) {
+ stream.emit('aborted');
+ stream[kState].aborted = true;
+ }
+}
+
+// An Http2Stream is a Duplex stream. On the server-side, the Readable side
+// provides access to the received request data. On the client-side, the
+// Readable side provides access to the received response data. On the
+// server side, the writable side is used to transmit response data, while
+// on the client side it is used to transmit request data.
+class Http2Stream extends Duplex {
+ constructor(session, options) {
+ options.allowHalfOpen = true;
+ super(options);
+ this.cork();
+ this[kSession] = session;
+
+ const state = this[kState] = {
+ rst: false,
+ rstCode: NGHTTP2_NO_ERROR,
+ headersSent: false,
+ aborted: false,
+ closeHandler: onSessionClose.bind(this)
+ };
+
+ this.once('ready', streamOnceReady);
+ this.once('streamClosed', onStreamClosed);
+ this.once('finish', onHandleFinish);
+ this.on('resume', streamOnResume);
+ this.on('pause', streamOnPause);
+ this.on('drain', streamOnDrain);
+ session.once('close', state.closeHandler);
+
+ if (session[kState].connecting) {
+ debug(`[${sessionName(session[kType])}] session is still connecting, ` +
+ 'queuing stream init');
+ state.connecting = true;
+ session.once('connect', streamOnSessionConnect.bind(this));
+ }
+ debug(`[${sessionName(session[kType])}] http2stream created`);
+ }
+
+ [kInit](id) {
+ this[kID] = id;
+ this.emit('ready');
+ }
+
+ [kInspect](depth, opts) {
+ const obj = {
+ id: this[kID],
+ state: this.state,
+ readableState: this._readableState,
+ writeableSate: this._writableState
+ };
+ return `Http2Stream ${util.format(obj)}`;
+ }
+
+ // The id of the Http2Stream, will be undefined if the socket is not
+ // yet connected.
+ get id() {
+ return this[kID];
+ }
+
+ // The Http2Session that owns this Http2Stream.
+ get session() {
+ return this[kSession];
+ }
+
+ _onTimeout() {
+ this.emit('timeout');
+ }
+
+ // true if the Http2Stream was aborted abornomally.
+ get aborted() {
+ return this[kState].aborted;
+ }
+
+ // The error code reported when this Http2Stream was closed.
+ get rstCode() {
+ return this[kState].rst ? this[kState].rstCode : undefined;
+ }
+
+ // State information for the Http2Stream
+ get state() {
+ const id = this[kID];
+ if (this.destroyed || id === undefined)
+ return Object.create(null);
+ return getStreamState(this[kSession][kHandle], id);
+ }
+
+ [kProceed]() {
+ assert.fail(null, null,
+ 'Implementors MUST implement this. Please report this as a ' +
+ 'bug in Node.js');
+ }
+
+ _write(data, encoding, cb) {
+ if (this[kID] === undefined) {
+ this.once('ready', this._write.bind(this, data, encoding, cb));
+ return;
+ }
+ _unrefActive(this);
+ if (!this[kState].headersSent)
+ this[kProceed]();
+ const session = this[kSession];
+ const handle = session[kHandle];
+ const req = new WriteWrap();
+ req.stream = this[kID];
+ req.handle = handle;
+ req.callback = cb;
+ req.oncomplete = afterDoStreamWrite;
+ req.async = false;
+ const err = createWriteReq(req, handle, data, encoding);
+ if (err)
+ throw util._errnoException(err, 'write', req.error);
+ this._bytesDispatched += req.bytes;
+
+ }
+
+ _writev(data, cb) {
+ if (this[kID] === undefined) {
+ this.once('ready', this._writev.bind(this, data, cb));
+ return;
+ }
+ _unrefActive(this);
+ if (!this[kState].headersSent)
+ this[kProceed]();
+ const session = this[kSession];
+ const handle = session[kHandle];
+ const req = new WriteWrap();
+ req.stream = this[kID];
+ req.handle = handle;
+ req.callback = cb;
+ req.oncomplete = afterDoStreamWrite;
+ req.async = false;
+ const chunks = new Array(data.length << 1);
+ for (var i = 0; i < data.length; i++) {
+ const entry = data[i];
+ chunks[i * 2] = entry.chunk;
+ chunks[i * 2 + 1] = entry.encoding;
+ }
+ const err = handle.writev(req, chunks);
+ if (err)
+ throw util._errnoException(err, 'write', req.error);
+ }
+
+ _read(nread) {
+ if (this[kID] === undefined) {
+ this.once('ready', this._read.bind(this, nread));
+ return;
+ }
+ if (this.destroyed) {
+ this.push(null);
+ return;
+ }
+ _unrefActive(this);
+ const state = this[kState];
+ if (state.reading)
+ return;
+ state.reading = true;
+ assert(this[kSession][kHandle].streamReadStart(this[kID]) === undefined,
+ 'HTTP/2 Stream #{this[kID]} does not exist. Please report this as ' +
+ 'a bug in Node.js');
+ }
+
+ // Submits an RST-STREAM frame to shutdown this stream.
+ // If the stream ID has not yet been allocated, the action will
+ // defer until the ready event is emitted.
+ // After sending the rstStream, this.destroy() will be called making
+ // the stream object no longer usable.
+ rstStream(code = NGHTTP2_NO_ERROR) {
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+ const session = this[kSession];
+ if (this[kID] === undefined) {
+ debug(
+ `[${sessionName(session[kType])}] queuing rstStream for new stream`);
+ this.once('ready', this.rstStream.bind(this, code));
+ return;
+ }
+ debug(`[${sessionName(session[kType])}] sending rstStream for stream ` +
+ `${this[kID]}: ${code}`);
+ _unrefActive(this);
+ this[kSession].rstStream(this, code);
+ }
+
+ rstWithNoError() {
+ this.rstStream(NGHTTP2_NO_ERROR);
+ }
+
+ rstWithProtocolError() {
+ this.rstStream(NGHTTP2_PROTOCOL_ERROR);
+ }
+
+ rstWithCancel() {
+ this.rstStream(NGHTTP2_CANCEL);
+ }
+
+ rstWithRefuse() {
+ this.rstStream(NGHTTP2_REFUSED_STREAM);
+ }
+
+ rstWithInternalError() {
+ this.rstStream(NGHTTP2_INTERNAL_ERROR);
+ }
+
+ // Note that this (and other methods like additionalHeaders and rstStream)
+ // cause nghttp to queue frames up in its internal buffer that are not
+ // actually sent on the wire until the next tick of the event loop. The
+ // semantics of this method then are: queue a priority frame to be sent and
+ // not immediately send the priority frame. There is current no callback
+ // triggered when the data is actually sent.
+ priority(options) {
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+ const session = this[kSession];
+ if (this[kID] === undefined) {
+ debug(`[${sessionName(session[kType])}] queuing priority for new stream`);
+ this.once('ready', this.priority.bind(this, options));
+ return;
+ }
+ debug(`[${sessionName(session[kType])}] sending priority for stream ` +
+ `${this[kID]}`);
+ _unrefActive(this);
+ this[kSession].priority(this, options);
+ }
+
+ // Called by this.destroy().
+ // * If called before the stream is allocated, will defer until the
+ // ready event is emitted.
+ // * Will submit an RST stream to shutdown the stream if necessary.
+ // This will cause the internal resources to be released.
+ // * Then cleans up the resources on the js side
+ _destroy(err, callback) {
+ const session = this[kSession];
+ const handle = session[kHandle];
+ if (this[kID] === undefined) {
+ debug(`[${sessionName(session[kType])}] queuing destroy for new stream`);
+ this.once('ready', this._destroy.bind(this, err, callback));
+ return;
+ }
+ debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
+
+ // Submit RST-STREAM frame if one hasn't been sent already and the
+ // stream hasn't closed normally...
+ if (!this[kState].rst) {
+ const code =
+ err instanceof Error ?
+ NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
+ this[kSession].rstStream(this, code);
+ }
+
+
+ // Remove the close handler on the session
+ session.removeListener('close', this[kState].closeHandler);
+
+ // Unenroll the timer
+ unenroll(this);
+
+ setImmediate(finishStreamDestroy.bind(this, handle));
+ session[kState].streams.delete(this[kID]);
+ delete this[kSession];
+
+ // All done
+ const rst = this[kState].rst;
+ const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR;
+ if (code !== NGHTTP2_NO_ERROR) {
+ const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
+ process.nextTick(() => this.emit('error', err));
+ }
+ process.nextTick(emit.bind(this, 'streamClosed', code));
+ debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
+ callback(err);
+ }
+}
+
+function finishStreamDestroy(handle) {
+ if (handle !== undefined)
+ handle.destroyStream(this[kID]);
+}
+
+function processHeaders(headers) {
+ assertIsObject(headers, 'headers');
+ headers = Object.assign(Object.create(null), headers);
+ if (headers[HTTP2_HEADER_STATUS] == null)
+ headers[HTTP2_HEADER_STATUS] = HTTP_STATUS_OK;
+ headers[HTTP2_HEADER_DATE] = utcDate();
+
+ const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
+ // This is intentionally stricter than the HTTP/1 implementation, which
+ // allows values between 100 and 999 (inclusive) in order to allow for
+ // backwards compatibility with non-spec compliant code. With HTTP/2,
+ // we have the opportunity to start fresh with stricter spec copmliance.
+ // This will have an impact on the compatibility layer for anyone using
+ // non-standard, non-compliant status codes.
+ if (statusCode < 200 || statusCode > 599)
+ throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID',
+ headers[HTTP2_HEADER_STATUS]);
+
+ return headers;
+}
+
+function processRespondWithFD(fd, headers) {
+ const session = this[kSession];
+ const state = this[kState];
+ state.headersSent = true;
+
+ // Close the writable side of the stream
+ this.end();
+
+ const handle = session[kHandle];
+ const ret =
+ handle.submitFile(this[kID], fd, headers);
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => session.emit('error', err));
+ break;
+ default:
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ }
+ }
+}
+
+function doSendFD(session, options, fd, headers, err, stat) {
+ if (this.destroyed || session.destroyed) {
+ abort(this);
+ return;
+ }
+ if (err) {
+ process.nextTick(() => this.emit('error', err));
+ return;
+ }
+ if (!stat.isFile()) {
+ err = new errors.Error('ERR_HTTP2_SEND_FILE');
+ process.nextTick(() => this.emit('error', err));
+ return;
+ }
+
+ // Set the content-length by default
+ headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size;
+ if (typeof options.statCheck === 'function' &&
+ options.statCheck.call(this, stat, headers) === false) {
+ return;
+ }
+
+ const headersList = mapToHeaders(headers,
+ assertValidPseudoHeaderResponse);
+ if (!Array.isArray(headersList)) {
+ throw headersList;
+ }
+
+ processRespondWithFD.call(this, fd, headersList);
+}
+
+function afterOpen(session, options, headers, err, fd) {
+ const state = this[kState];
+ if (this.destroyed || session.destroyed) {
+ abort(this);
+ return;
+ }
+ if (err) {
+ process.nextTick(() => this.emit('error', err));
+ return;
+ }
+ state.fd = fd;
+
+ fs.fstat(fd, doSendFD.bind(this, session, options, fd, headers));
+}
+
+
+class ServerHttp2Stream extends Http2Stream {
+ constructor(session, id, options, headers) {
+ super(session, options);
+ this[kInit](id);
+ this[kProtocol] = headers[HTTP2_HEADER_SCHEME];
+ this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY];
+ debug(`[${sessionName(session[kType])}] created serverhttp2stream`);
+ }
+
+ // true if the HEADERS frame has been sent
+ get headersSent() {
+ return this[kState].headersSent;
+ }
+
+ // true if the remote peer accepts push streams
+ get pushAllowed() {
+ return this[kSession].remoteSettings.enablePush;
+ }
+
+ // create a push stream, call the given callback with the created
+ // Http2Stream for the push stream.
+ pushStream(headers, options, callback) {
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+
+ const session = this[kSession];
+ debug(`[${sessionName(session[kType])}] initiating push stream for stream` +
+ ` ${this[kID]}`);
+
+ _unrefActive(this);
+ const state = session[kState];
+ const streams = state.streams;
+ const handle = session[kHandle];
+
+ if (!this[kSession].remoteSettings.enablePush)
+ throw new errors.Error('ERR_HTTP2_PUSH_DISABLED');
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+
+ if (typeof callback !== 'function')
+ throw new errors.TypeError('ERR_INVALID_CALLBACK');
+
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+ options.endStream = !!options.endStream;
+
+ assertIsObject(headers, 'headers');
+ headers = Object.assign(Object.create(null), headers);
+
+ if (headers[HTTP2_HEADER_METHOD] === undefined)
+ headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
+ if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
+ headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
+ if (headers[HTTP2_HEADER_SCHEME] === undefined)
+ headers[HTTP2_HEADER_SCHEME] = this[kProtocol];
+ if (headers[HTTP2_HEADER_PATH] === undefined)
+ headers[HTTP2_HEADER_PATH] = '/';
+
+ let headRequest = false;
+ if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
+ headRequest = true;
+ options.endStream = true;
+ }
+
+ const headersList = mapToHeaders(headers);
+ if (!Array.isArray(headersList)) {
+ // An error occurred!
+ throw headersList;
+ }
+
+ const ret = handle.submitPushPromise(this[kID],
+ headersList,
+ options.endStream);
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => session.emit('error', err));
+ break;
+ case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
+ err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ case NGHTTP2_ERR_STREAM_CLOSED:
+ err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+ process.nextTick(() => this.emit('error', err));
+ break;
+ default:
+ if (ret <= 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ break;
+ }
+ debug(`[${sessionName(session[kType])}] push stream ${ret} created`);
+ options.readable = !options.endStream;
+
+ const stream = new ServerHttp2Stream(session, ret, options, headers);
+
+ // If the push stream is a head request, close the writable side of
+ // the stream immediately as there won't be any data sent.
+ if (headRequest) {
+ stream.end();
+ const state = stream[kState];
+ state.headRequest = true;
+ }
+
+ streams.set(ret, stream);
+ process.nextTick(callback, stream, headers, 0);
+ }
+ }
+
+ // Initiate a response on this Http2Stream
+ respond(headers, options) {
+ const session = this[kSession];
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+ debug(`[${sessionName(session[kType])}] initiating response for stream ` +
+ `${this[kID]}`);
+ _unrefActive(this);
+ const state = this[kState];
+
+ if (state.headersSent)
+ throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
+
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+ options.endStream = !!options.endStream;
+
+ headers = processHeaders(headers);
+ const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
+
+ // Payload/DATA frames are not permitted in these cases so set
+ // the options.endStream option to true so that the underlying
+ // bits do not attempt to send any.
+ if (statusCode === HTTP_STATUS_NO_CONTENT ||
+ statusCode === HTTP_STATUS_CONTENT_RESET ||
+ statusCode === HTTP_STATUS_NOT_MODIFIED ||
+ state.headRequest === true) {
+ options.endStream = true;
+ }
+
+ const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
+ if (!Array.isArray(headersList)) {
+ // An error occurred!
+ throw headersList;
+ }
+ state.headersSent = true;
+
+ // Close the writable side if the endStream option is set
+ if (options.endStream)
+ this.end();
+
+ const handle = session[kHandle];
+ const ret =
+ handle.submitResponse(this[kID], headersList, options.endStream);
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => session.emit('error', err));
+ break;
+ default:
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ }
+ }
+ }
+
+ // Initiate a response using an open FD. Note that there are fewer
+ // protections with this approach. For one, the fd is not validated.
+ // In respondWithFile, the file is checked to make sure it is a
+ // regular file, here the fd is passed directly. If the underlying
+ // mechanism is not able to read from the fd, then the stream will be
+ // reset with an error code.
+ respondWithFD(fd, headers) {
+ const session = this[kSession];
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+ debug(`[${sessionName(session[kType])}] initiating response for stream ` +
+ `${this[kID]}`);
+ _unrefActive(this);
+ const state = this[kState];
+
+ if (state.headersSent)
+ throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
+
+ if (typeof fd !== 'number')
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ 'fd', 'number');
+
+ headers = processHeaders(headers);
+ const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
+ // Payload/DATA frames are not permitted in these cases
+ if (statusCode === HTTP_STATUS_NO_CONTENT ||
+ statusCode === HTTP_STATUS_CONTENT_RESET ||
+ statusCode === HTTP_STATUS_NOT_MODIFIED) {
+ throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode);
+ }
+
+ const headersList = mapToHeaders(headers,
+ assertValidPseudoHeaderResponse);
+ if (!Array.isArray(headersList)) {
+ throw headersList;
+ }
+
+ processRespondWithFD.call(this, fd, headersList);
+ }
+
+ // Initiate a file response on this Http2Stream. The path is passed to
+ // fs.open() to acquire the fd with mode 'r', then the fd is passed to
+ // fs.fstat(). Assuming fstat is successful, a check is made to ensure
+ // that the file is a regular file, then options.statCheck is called,
+ // giving the user an opportunity to verify the details and set additional
+ // headers. If statCheck returns false, the operation is aborted and no
+ // file details are sent.
+ respondWithFile(path, headers, options) {
+ const session = this[kSession];
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+ debug(`[${sessionName(session[kType])}] initiating response for stream ` +
+ `${this[kID]}`);
+ _unrefActive(this);
+ const state = this[kState];
+
+ if (state.headersSent)
+ throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
+
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+
+ if (options.statCheck !== undefined &&
+ typeof options.statCheck !== 'function') {
+ throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
+ 'statCheck',
+ options.statCheck);
+ }
+
+ headers = processHeaders(headers);
+ const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
+ // Payload/DATA frames are not permitted in these cases
+ if (statusCode === HTTP_STATUS_NO_CONTENT ||
+ statusCode === HTTP_STATUS_CONTENT_RESET ||
+ statusCode === HTTP_STATUS_NOT_MODIFIED) {
+ throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode);
+ }
+
+ fs.open(path, 'r', afterOpen.bind(this, session, options, headers));
+ }
+
+ // Sends a block of informational headers. In theory, the HTTP/2 spec
+ // allows sending a HEADER block at any time during a streams lifecycle,
+ // but the HTTP request/response semantics defined in HTTP/2 places limits
+ // such that HEADERS may only be sent *before* or *after* DATA frames.
+ // If the block of headers being sent includes a status code, it MUST be
+ // a 1xx informational code and it MUST be sent before the request/response
+ // headers are sent, or an error will be thrown.
+ additionalHeaders(headers) {
+ if (this.destroyed)
+ throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+
+ if (this[kState].headersSent)
+ throw new errors.Error('ERR_HTTP2_HEADERS_AFTER_RESPOND');
+
+ const session = this[kSession];
+ debug(`[${sessionName(session[kType])}] sending additional headers`);
+
+ assertIsObject(headers, 'headers');
+ headers = Object.assign(Object.create(null), headers);
+ if (headers[HTTP2_HEADER_STATUS] != null) {
+ const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
+ if (statusCode === HTTP_STATUS_SWITCHING_PROTOCOLS)
+ throw new errors.Error('ERR_HTTP2_STATUS_101');
+ if (statusCode < 100 || statusCode >= 200) {
+ throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS',
+ headers[HTTP2_HEADER_STATUS]);
+ }
+ }
+
+ _unrefActive(this);
+ const handle = this[kSession][kHandle];
+
+ const headersList = mapToHeaders(headers,
+ assertValidPseudoHeaderResponse);
+ if (!Array.isArray(headersList)) {
+ throw headersList;
+ }
+
+ const ret =
+ handle.sendHeaders(this[kID], headersList);
+ let err;
+ switch (ret) {
+ case NGHTTP2_ERR_NOMEM:
+ err = new errors.Error('ERR_OUTOFMEMORY');
+ process.nextTick(() => this[kSession].emit('error', err));
+ break;
+ default:
+ if (ret < 0) {
+ err = new NghttpError(ret);
+ process.nextTick(() => this.emit('error', err));
+ }
+ }
+ }
+}
+
+ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond;
+
+class ClientHttp2Stream extends Http2Stream {
+ constructor(session, id, options) {
+ super(session, options);
+ this[kState].headersSent = true;
+ if (id !== undefined)
+ this[kInit](id);
+ debug(`[${sessionName(session[kType])}] clienthttp2stream created`);
+ }
+}
+
+const setTimeout = {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: function(msecs, callback) {
+ if (typeof msecs !== 'number') {
+ throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ 'msecs',
+ 'number');
+ }
+ if (msecs === 0) {
+ unenroll(this);
+ if (callback !== undefined) {
+ if (typeof callback !== 'function')
+ throw new errors.TypeError('ERR_INVALID_CALLBACK');
+ this.removeListener('timeout', callback);
+ }
+ } else {
+ enroll(this, msecs);
+ _unrefActive(this);
+ if (callback !== undefined) {
+ if (typeof callback !== 'function')
+ throw new errors.TypeError('ERR_INVALID_CALLBACK');
+ this.once('timeout', callback);
+ }
+ }
+ return this;
+ }
+};
+
+const onTimeout = {
+ configurable: false,
+ enumerable: false,
+ value: function() {
+ this.emit('timeout');
+ }
+};
+
+Object.defineProperties(Http2Stream.prototype, {
+ setTimeout,
+ onTimeout
+});
+Object.defineProperties(Http2Session.prototype, {
+ setTimeout,
+ onTimeout
+});
+
+// --------------------------------------------------------------------
+
+// Set as a replacement for socket.prototype.destroy upon the
+// establishment of a new connection.
+function socketDestroy(error) {
+ const type = this[kSession][kType];
+ debug(`[${sessionName(type)}] socket destroy called`);
+ delete this[kServer];
+ // destroy the session first so that it will stop trying to
+ // send data while we close the socket.
+ this[kSession].destroy();
+ this.destroy = this[kDestroySocket];
+ debug(`[${sessionName(type)}] destroying the socket`);
+ this.destroy(error);
+}
+
+function socketOnResume() {
+ if (this._paused)
+ return this.pause();
+ if (this._handle && !this._handle.reading) {
+ this._handle.reading = true;
+ this._handle.readStart();
+ }
+}
+
+function socketOnPause() {
+ if (this._handle && this._handle.reading) {
+ this._handle.reading = false;
+ this._handle.readStop();
+ }
+}
+
+function socketOnDrain() {
+ const needPause = 0 > this._writableState.highWaterMark;
+ if (this._paused && !needPause) {
+ this._paused = false;
+ this.resume();
+ }
+}
+
+// When an Http2Session emits an error, first try to forward it to the
+// server as a sessionError; failing that, forward it to the socket as
+// a sessionError; failing that, destroy, remove the error listener, and
+// re-emit the error event
+function sessionOnError(error) {
+ debug(`[${sessionName(this[kType])}] server session error: ${error.message}`);
+ if (this[kServer] !== undefined && this[kServer].emit('sessionError', error))
+ return;
+ if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
+ return;
+ this.destroy();
+ this.removeListener('error', sessionOnError);
+ this.emit('error', error);
+}
+
+// When a Socket emits an error, first try to forward it to the server
+// as a socketError; failing that, forward it to the session as a
+// socketError; failing that, remove the listener and call destroy
+function socketOnError(error) {
+ const type = this[kSession] && this[kSession][kType];
+ debug(`[${sessionName(type)}] server socket error: ${error.message}`);
+ if (kRenegTest.test(error.message))
+ return this.destroy();
+ if (this[kServer] !== undefined && this[kServer].emit('socketError', error))
+ return;
+ if (this[kSession] !== undefined && this[kSession].emit('socketError', error))
+ return;
+ this.removeListener('error', socketOnError);
+ this.destroy(error);
+}
+
+// When the socket times out, attempt a graceful shutdown
+// of the session
+function socketOnTimeout() {
+ debug('socket timeout');
+ const server = this[kServer];
+ // server can be null if the socket is a client
+ if (server === undefined || !server.emit('timeout', this)) {
+ this[kSession].shutdown(
+ {
+ graceful: true,
+ errorCode: NGHTTP2_NO_ERROR
+ },
+ this.destroy.bind(this));
+ }
+}
+
+// Handles the on('stream') event for a session and forwards
+// it on to the server object.
+function sessionOnStream(stream, headers, flags) {
+ debug(`[${sessionName(this[kType])}] emit server stream event`);
+ this[kServer].emit('stream', stream, headers, flags);
+}
+
+function sessionOnPriority(stream, parent, weight, exclusive) {
+ debug(`[${sessionName(this[kType])}] priority change received`);
+ this[kServer].emit('priority', stream, parent, weight, exclusive);
+}
+
+function connectionListener(socket) {
+ debug('[server] received a connection');
+ const options = this[kOptions] || {};
+
+ if (this.timeout) {
+ socket.setTimeout(this.timeout);
+ socket.on('timeout', socketOnTimeout);
+ }
+
+ if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
+ if (options.allowHTTP1 === true) {
+ // Fallback to HTTP/1.1
+ return httpConnectionListener.call(this, socket);
+ } else if (this.emit('unknownProtocol', socket)) {
+ // Let event handler deal with the socket
+ return;
+ } else {
+ // Reject non-HTTP/2 client
+ return socket.destroy();
+ }
+ }
+
+ socket.on('error', socketOnError);
+ socket.on('resume', socketOnResume);
+ socket.on('pause', socketOnPause);
+ socket.on('drain', socketOnDrain);
+
+ // Set up the Session
+ const session = new ServerHttp2Session(options, socket, this);
+
+ session.on('error', sessionOnError);
+ session.on('stream', sessionOnStream);
+ session.on('priority', sessionOnPriority);
+
+ socket[kServer] = this;
+
+ process.nextTick(emit.bind(this, 'session', session));
+}
+
+function initializeOptions(options) {
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+ options.allowHalfOpen = true;
+ assertIsObject(options.settings, 'options.settings');
+ options.settings = Object.assign(Object.create(null), options.settings);
+ return options;
+}
+
+function initializeTLSOptions(options, servername) {
+ options = initializeOptions(options);
+ options.ALPNProtocols = ['h2'];
+ if (options.allowHTTP1 === true)
+ options.ALPNProtocols.push('http/1.1');
+ if (servername !== undefined && options.servername === undefined)
+ options.servername = servername;
+ return options;
+}
+
+function onErrorSecureServerSession(err, conn) {
+ if (!this.emit('clientError', err, conn))
+ conn.destroy(err);
+}
+
+class Http2SecureServer extends TLSServer {
+ constructor(options, requestListener) {
+ options = initializeTLSOptions(options);
+ super(options, connectionListener);
+ this[kOptions] = options;
+ this.timeout = kDefaultSocketTimeout;
+ this.on('newListener', setupCompat);
+ if (typeof requestListener === 'function')
+ this.on('request', requestListener);
+ this.on('tlsClientError', onErrorSecureServerSession);
+ debug('http2secureserver created');
+ }
+
+ setTimeout(msecs, callback) {
+ this.timeout = msecs;
+ if (callback !== undefined) {
+ if (typeof callback !== 'function')
+ throw new errors.TypeError('ERR_INVALID_CALLBACK');
+ this.on('timeout', callback);
+ }
+ return this;
+ }
+}
+
+class Http2Server extends NETServer {
+ constructor(options, requestListener) {
+ super(connectionListener);
+ this[kOptions] = initializeOptions(options);
+ this.timeout = kDefaultSocketTimeout;
+ this.on('newListener', setupCompat);
+ if (typeof requestListener === 'function')
+ this.on('request', requestListener);
+ debug('http2server created');
+ }
+
+ setTimeout(msecs, callback) {
+ this.timeout = msecs;
+ if (callback !== undefined) {
+ if (typeof callback !== 'function')
+ throw new errors.TypeError('ERR_INVALID_CALLBACK');
+ this.on('timeout', callback);
+ }
+ return this;
+ }
+}
+
+function setupCompat(ev) {
+ if (ev === 'request') {
+ debug('setting up compatibility handler');
+ this.removeListener('newListener', setupCompat);
+ this.on('stream', onServerStream);
+ }
+}
+
+// If the socket emits an error, forward it to the session as a socketError;
+// failing that, remove the listener and destroy the socket
+function clientSocketOnError(error) {
+ const type = this[kSession] && this[kSession][kType];
+ debug(`[${sessionName(type)}] client socket error: ${error.message}`);
+ if (kRenegTest.test(error.message))
+ return this.destroy();
+ if (this[kSession] !== undefined && this[kSession].emit('socketError', error))
+ return;
+ this.removeListener('error', clientSocketOnError);
+ this.destroy(error);
+}
+
+// If the session emits an error, forward it to the socket as a sessionError;
+// failing that, destroy the session, remove the listener and re-emit the error
+function clientSessionOnError(error) {
+ debug(`client session error: ${error.message}`);
+ if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
+ return;
+ this.destroy();
+ this.removeListener('error', clientSocketOnError);
+ this.removeListener('error', clientSessionOnError);
+}
+
+function connect(authority, options, listener) {
+ if (typeof options === 'function') {
+ listener = options;
+ options = undefined;
+ }
+
+ assertIsObject(options, 'options');
+ options = Object.assign(Object.create(null), options);
+
+ if (typeof authority === 'string')
+ authority = new URL(authority);
+
+ assertIsObject(authority, 'authority', ['string', 'object', 'URL']);
+
+ debug(`connecting to ${authority}`);
+
+ const protocol = authority.protocol || options.protocol || 'https:';
+ const port = '' + (authority.port !== '' ? authority.port : 443);
+ const host = authority.hostname || authority.host || 'localhost';
+
+ let socket;
+ switch (protocol) {
+ case 'http:':
+ socket = net.connect(port, host);
+ break;
+ case 'https:':
+ socket = tls.connect(port, host, initializeTLSOptions(options, host));
+ break;
+ default:
+ throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol);
+ }
+
+ socket.on('error', clientSocketOnError);
+ socket.on('resume', socketOnResume);
+ socket.on('pause', socketOnPause);
+ socket.on('drain', socketOnDrain);
+
+ const session = new ClientHttp2Session(options, socket);
+
+ session.on('error', clientSessionOnError);
+
+ session[kAuthority] = `${options.servername || host}:${port}`;
+ session[kProtocol] = protocol;
+
+ if (typeof listener === 'function')
+ session.once('connect', listener);
+ return session;
+}
+
+function createSecureServer(options, handler) {
+ if (typeof options === 'function') {
+ handler = options;
+ options = Object.create(null);
+ }
+ debug('creating http2secureserver');
+ return new Http2SecureServer(options, handler);
+}
+
+function createServer(options, handler) {
+ if (typeof options === 'function') {
+ handler = options;
+ options = Object.create(null);
+ }
+ debug('creating htt2pserver');
+ return new Http2Server(options, handler);
+}
+
+// Returns a Base64 encoded settings frame payload from the given
+// object. The value is suitable for passing as the value of the
+// HTTP2-Settings header frame.
+function getPackedSettings(settings) {
+ assertIsObject(settings, 'settings');
+ settings = settings || Object.create(null);
+ 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;
+ }
+ updateSettingsBuffer(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
+};
+
+/* eslint-enable no-use-before-define */
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
new file mode 100644
index 00000000000000..ea36444fadfa36
--- /dev/null
+++ b/lib/internal/http2/util.js
@@ -0,0 +1,513 @@
+'use strict';
+
+const binding = process.binding('http2');
+const errors = require('internal/errors');
+
+const {
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_AGE,
+ HTTP2_HEADER_AUTHORIZATION,
+ HTTP2_HEADER_CONTENT_ENCODING,
+ HTTP2_HEADER_CONTENT_LANGUAGE,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_CONTENT_LOCATION,
+ HTTP2_HEADER_CONTENT_MD5,
+ HTTP2_HEADER_CONTENT_RANGE,
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_COOKIE,
+ HTTP2_HEADER_DATE,
+ HTTP2_HEADER_ETAG,
+ HTTP2_HEADER_EXPIRES,
+ HTTP2_HEADER_FROM,
+ HTTP2_HEADER_IF_MATCH,
+ HTTP2_HEADER_IF_NONE_MATCH,
+ HTTP2_HEADER_IF_MODIFIED_SINCE,
+ HTTP2_HEADER_IF_RANGE,
+ HTTP2_HEADER_IF_UNMODIFIED_SINCE,
+ HTTP2_HEADER_LAST_MODIFIED,
+ HTTP2_HEADER_LOCATION,
+ HTTP2_HEADER_MAX_FORWARDS,
+ HTTP2_HEADER_PROXY_AUTHORIZATION,
+ HTTP2_HEADER_RANGE,
+ HTTP2_HEADER_REFERER,
+ HTTP2_HEADER_RETRY_AFTER,
+ HTTP2_HEADER_USER_AGENT,
+
+ HTTP2_HEADER_CONNECTION,
+ HTTP2_HEADER_UPGRADE,
+ HTTP2_HEADER_HTTP2_SETTINGS,
+ HTTP2_HEADER_TE,
+ HTTP2_HEADER_TRANSFER_ENCODING,
+ HTTP2_HEADER_HOST,
+ HTTP2_HEADER_KEEP_ALIVE,
+ HTTP2_HEADER_PROXY_CONNECTION,
+
+ HTTP2_METHOD_DELETE,
+ HTTP2_METHOD_GET,
+ HTTP2_METHOD_HEAD
+} = binding.constants;
+
+// This set is defined strictly by the HTTP/2 specification. Only
+// :-prefixed headers defined by that specification may be added to
+// this set.
+const kValidPseudoHeaders = new Set([
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_PATH
+]);
+
+// This set contains headers that are permitted to have only a single
+// value. Multiple instances must not be specified.
+const kSingleValueHeaders = new Set([
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_AGE,
+ HTTP2_HEADER_AUTHORIZATION,
+ HTTP2_HEADER_CONTENT_ENCODING,
+ HTTP2_HEADER_CONTENT_LANGUAGE,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_CONTENT_LOCATION,
+ HTTP2_HEADER_CONTENT_MD5,
+ HTTP2_HEADER_CONTENT_RANGE,
+ HTTP2_HEADER_CONTENT_TYPE,
+ HTTP2_HEADER_DATE,
+ HTTP2_HEADER_ETAG,
+ HTTP2_HEADER_EXPIRES,
+ HTTP2_HEADER_FROM,
+ HTTP2_HEADER_IF_MATCH,
+ HTTP2_HEADER_IF_MODIFIED_SINCE,
+ HTTP2_HEADER_IF_NONE_MATCH,
+ HTTP2_HEADER_IF_RANGE,
+ HTTP2_HEADER_IF_UNMODIFIED_SINCE,
+ HTTP2_HEADER_LAST_MODIFIED,
+ HTTP2_HEADER_LOCATION,
+ HTTP2_HEADER_MAX_FORWARDS,
+ HTTP2_HEADER_PROXY_AUTHORIZATION,
+ HTTP2_HEADER_RANGE,
+ HTTP2_HEADER_REFERER,
+ HTTP2_HEADER_RETRY_AFTER,
+ HTTP2_HEADER_USER_AGENT
+]);
+
+// The HTTP methods in this set are specifically defined as assigning no
+// meaning to the request payload. By default, unless the user explicitly
+// overrides the endStream option on the request method, the endStream
+// option will be defaulted to true when these methods are used.
+const kNoPayloadMethods = new Set([
+ HTTP2_METHOD_DELETE,
+ HTTP2_METHOD_GET,
+ HTTP2_METHOD_HEAD
+]);
+
+// The following ArrayBuffer instances are used to share memory more efficiently
+// with the native binding side for a number of methods. These are not intended
+// to be used directly by users in any way. The ArrayBuffers are created on
+// the native side with values that are filled in on demand, the js code then
+// reads those values out. The set of IDX constants that follow identify the
+// relevant data positions within these buffers.
+const settingsBuffer = new Uint32Array(binding.settingsArrayBuffer);
+const optionsBuffer = new Uint32Array(binding.optionsArrayBuffer);
+
+// Note that Float64Array is used here because there is no Int64Array available
+// and these deal with numbers that can be beyond the range of Uint32 and Int32.
+// The values set on the native side will always be integers. This is not a
+// unique example of this, this pattern can be found in use in other parts of
+// Node.js core as a performance optimization.
+const sessionState = new Float64Array(binding.sessionStateArrayBuffer);
+const streamState = new Float64Array(binding.streamStateArrayBuffer);
+
+const IDX_SETTINGS_HEADER_TABLE_SIZE = 0;
+const IDX_SETTINGS_ENABLE_PUSH = 1;
+const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2;
+const IDX_SETTINGS_MAX_FRAME_SIZE = 3;
+const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4;
+const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5;
+const IDX_SETTINGS_FLAGS = 6;
+
+const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0;
+const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1;
+const IDX_SESSION_STATE_NEXT_STREAM_ID = 2;
+const IDX_SESSION_STATE_LOCAL_WINDOW_SIZE = 3;
+const IDX_SESSION_STATE_LAST_PROC_STREAM_ID = 4;
+const IDX_SESSION_STATE_REMOTE_WINDOW_SIZE = 5;
+const IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE = 6;
+const IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE = 7;
+const IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE = 8;
+const IDX_STREAM_STATE = 0;
+const IDX_STREAM_STATE_WEIGHT = 1;
+const IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT = 2;
+const IDX_STREAM_STATE_LOCAL_CLOSE = 3;
+const IDX_STREAM_STATE_REMOTE_CLOSE = 4;
+const IDX_STREAM_STATE_LOCAL_WINDOW_SIZE = 5;
+
+const IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 0;
+const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1;
+const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2;
+const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3;
+const IDX_OPTIONS_PADDING_STRATEGY = 4;
+const IDX_OPTIONS_FLAGS = 5;
+
+function updateOptionsBuffer(options) {
+ var flags = 0;
+ if (typeof options.maxDeflateDynamicTableSize === 'number') {
+ flags |= (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE);
+ optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE] =
+ options.maxDeflateDynamicTableSize;
+ }
+ if (typeof options.maxReservedRemoteStreams === 'number') {
+ flags |= (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS);
+ optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS] =
+ options.maxReservedRemoteStreams;
+ }
+ if (typeof options.maxSendHeaderBlockLength === 'number') {
+ flags |= (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH);
+ optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] =
+ options.maxSendHeaderBlockLength;
+ }
+ if (typeof options.peerMaxConcurrentStreams === 'number') {
+ flags |= (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS);
+ optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS] =
+ options.peerMaxConcurrentStreams;
+ }
+ if (typeof options.paddingStrategy === 'number') {
+ flags |= (1 << IDX_OPTIONS_PADDING_STRATEGY);
+ optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY] =
+ options.paddingStrategy;
+ }
+ optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
+}
+
+function getDefaultSettings() {
+ settingsBuffer[IDX_SETTINGS_FLAGS] = 0;
+ binding.refreshDefaultSettings();
+ const holder = Object.create(null);
+
+ const flags = settingsBuffer[IDX_SETTINGS_FLAGS];
+
+ if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) ===
+ (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) {
+ holder.headerTableSize =
+ settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE];
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) ===
+ (1 << IDX_SETTINGS_ENABLE_PUSH)) {
+ holder.enablePush =
+ settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] === 1;
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) ===
+ (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) {
+ holder.initialWindowSize =
+ settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE];
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) ===
+ (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) {
+ holder.maxFrameSize =
+ settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE];
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) ===
+ (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) {
+ console.log('setting it');
+ holder.maxConcurrentStreams =
+ settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS];
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) ===
+ (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) {
+ holder.maxHeaderListSize =
+ settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
+ }
+
+ return holder;
+}
+
+// remote is a boolean. true to fetch remote settings, false to fetch local.
+// this is only called internally
+function getSettings(session, remote) {
+ const holder = Object.create(null);
+ if (remote)
+ binding.refreshRemoteSettings(session);
+ else
+ binding.refreshLocalSettings(session);
+
+ holder.headerTableSize =
+ settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE];
+ holder.enablePush =
+ !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH];
+ holder.initialWindowSize =
+ settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE];
+ holder.maxFrameSize =
+ settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE];
+ holder.maxConcurrentStreams =
+ settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS];
+ holder.maxHeaderListSize =
+ settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
+ return holder;
+}
+
+function updateSettingsBuffer(settings) {
+ var flags = 0;
+ if (typeof settings.headerTableSize === 'number') {
+ flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE);
+ settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =
+ settings.headerTableSize;
+ }
+ if (typeof settings.maxConcurrentStreams === 'number') {
+ flags |= (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS);
+ settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] =
+ settings.maxConcurrentStreams;
+ }
+ if (typeof settings.initialWindowSize === 'number') {
+ flags |= (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE);
+ settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] =
+ settings.initialWindowSize;
+ }
+ if (typeof settings.maxFrameSize === 'number') {
+ flags |= (1 << IDX_SETTINGS_MAX_FRAME_SIZE);
+ settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] =
+ settings.maxFrameSize;
+ }
+ if (typeof settings.maxHeaderListSize === 'number') {
+ flags |= (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE);
+ settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] =
+ settings.maxHeaderListSize;
+ }
+ if (typeof settings.enablePush === 'boolean') {
+ flags |= (1 << IDX_SETTINGS_ENABLE_PUSH);
+ settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush);
+ }
+
+ settingsBuffer[IDX_SETTINGS_FLAGS] = flags;
+}
+
+function getSessionState(session) {
+ const holder = Object.create(null);
+ binding.refreshSessionState(session);
+ holder.effectiveLocalWindowSize =
+ sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE];
+ holder.effectiveRecvDataLength =
+ sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH];
+ holder.nextStreamID =
+ sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID];
+ holder.localWindowSize =
+ sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE];
+ holder.lastProcStreamID =
+ sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID];
+ holder.remoteWindowSize =
+ sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE];
+ holder.outboundQueueSize =
+ sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE];
+ holder.deflateDynamicTableSize =
+ sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE];
+ holder.inflateDynamicTableSize =
+ sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE];
+ return holder;
+}
+
+function getStreamState(session, stream) {
+ const holder = Object.create(null);
+ binding.refreshStreamState(session, stream);
+ holder.state =
+ streamState[IDX_STREAM_STATE];
+ holder.weight =
+ streamState[IDX_STREAM_STATE_WEIGHT];
+ holder.sumDependencyWeight =
+ streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT];
+ holder.localClose =
+ streamState[IDX_STREAM_STATE_LOCAL_CLOSE];
+ holder.remoteClose =
+ streamState[IDX_STREAM_STATE_REMOTE_CLOSE];
+ holder.localWindowSize =
+ streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE];
+ return holder;
+}
+
+function isIllegalConnectionSpecificHeader(name, value) {
+ switch (name) {
+ case HTTP2_HEADER_CONNECTION:
+ case HTTP2_HEADER_UPGRADE:
+ case HTTP2_HEADER_HOST:
+ case HTTP2_HEADER_HTTP2_SETTINGS:
+ case HTTP2_HEADER_KEEP_ALIVE:
+ case HTTP2_HEADER_PROXY_CONNECTION:
+ case HTTP2_HEADER_TRANSFER_ENCODING:
+ return true;
+ case HTTP2_HEADER_TE:
+ const val = Array.isArray(value) ? value.join(', ') : value;
+ return val !== 'trailers';
+ default:
+ return false;
+ }
+}
+
+function assertValidPseudoHeader(key) {
+ if (!kValidPseudoHeaders.has(key)) {
+ const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key);
+ Error.captureStackTrace(err, assertValidPseudoHeader);
+ return err;
+ }
+}
+
+function assertValidPseudoHeaderResponse(key) {
+ if (key !== ':status') {
+ const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key);
+ Error.captureStackTrace(err, assertValidPseudoHeaderResponse);
+ return err;
+ }
+}
+
+function assertValidPseudoHeaderTrailer(key) {
+ const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key);
+ Error.captureStackTrace(err, assertValidPseudoHeaderTrailer);
+ return err;
+}
+
+function mapToHeaders(map,
+ assertValuePseudoHeader = assertValidPseudoHeader) {
+ const ret = [];
+ 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];
+ let val;
+ if (typeof key === 'symbol' || value === undefined || !key)
+ continue;
+ key = String(key).toLowerCase();
+ const isArray = Array.isArray(value);
+ if (isArray) {
+ switch (value.length) {
+ case 0:
+ continue;
+ case 1:
+ value = String(value[0]);
+ break;
+ default:
+ if (kSingleValueHeaders.has(key))
+ return new errors.Error('ERR_HTTP2_HEADER_SINGLE_VALUE', key);
+ }
+ }
+ if (key[0] === ':') {
+ const err = assertValuePseudoHeader(key);
+ if (err !== undefined)
+ return err;
+ ret.unshift([key, String(value)]);
+ } else {
+ if (kSingleValueHeaders.has(key)) {
+ if (singles.has(key))
+ return new errors.Error('ERR_HTTP2_HEADER_SINGLE_VALUE', key);
+ singles.add(key);
+ }
+ if (isIllegalConnectionSpecificHeader(key, value)) {
+ return new errors.Error('ERR_HTTP2_INVALID_CONNECTION_HEADERS');
+ }
+ if (isArray) {
+ for (var k = 0; k < value.length; k++) {
+ val = String(value[k]);
+ ret.push([key, val]);
+ }
+ } else {
+ val = String(value);
+ ret.push([key, val]);
+ }
+ }
+ }
+
+ return ret;
+}
+
+class NghttpError extends Error {
+ constructor(ret) {
+ super(binding.nghttp2ErrorString(ret));
+ this.code = 'ERR_HTTP2_ERROR';
+ this.name = 'Error [ERR_HTTP2_ERROR]';
+ this.errno = ret;
+ }
+}
+
+function assertIsObject(value, name, types) {
+ if (value !== undefined &&
+ (value === null ||
+ typeof value !== 'object' ||
+ Array.isArray(value))) {
+ const err = new errors.TypeError('ERR_INVALID_ARG_TYPE',
+ name, types || 'object');
+ Error.captureStackTrace(err, assertIsObject);
+ throw err;
+ }
+}
+
+function assertWithinRange(name, value, min = 0, max = Infinity) {
+ if (value !== undefined &&
+ (typeof value !== 'number' || value < min || value > max)) {
+ const err = new errors.RangeError('ERR_HTTP2_INVALID_SETTING_VALUE',
+ name, value);
+ err.min = min;
+ err.max = max;
+ err.actual = value;
+ Error.captureStackTrace(err, assertWithinRange);
+ throw err;
+ }
+}
+
+function toHeaderObject(headers) {
+ const obj = Object.create(null);
+ for (var n = 0; n < headers.length; n = n + 2) {
+ var name = headers[n];
+ var value = headers[n + 1];
+ if (name === HTTP2_HEADER_STATUS)
+ value |= 0;
+ var existing = obj[name];
+ if (existing === undefined) {
+ obj[name] = value;
+ } else if (!kSingleValueHeaders.has(name)) {
+ if (name === HTTP2_HEADER_COOKIE) {
+ // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
+ // "...If there are multiple Cookie header fields after decompression,
+ // these MUST be concatenated into a single octet string using the
+ // two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") before
+ // being passed into a non-HTTP/2 context."
+ obj[name] = `${existing}; ${value}`;
+ } else {
+ if (Array.isArray(existing))
+ existing.push(value);
+ else
+ obj[name] = [existing, value];
+ }
+ }
+ }
+ return obj;
+}
+
+function isPayloadMeaningless(method) {
+ return kNoPayloadMethods.has(method);
+}
+
+module.exports = {
+ assertIsObject,
+ assertValidPseudoHeaderResponse,
+ assertValidPseudoHeaderTrailer,
+ assertWithinRange,
+ getDefaultSettings,
+ getSessionState,
+ getSettings,
+ getStreamState,
+ isPayloadMeaningless,
+ mapToHeaders,
+ NghttpError,
+ toHeaderObject,
+ updateOptionsBuffer,
+ updateSettingsBuffer
+};
diff --git a/lib/internal/module.js b/lib/internal/module.js
index 08d8f770c8d873..cf994b51c0675f 100644
--- a/lib/internal/module.js
+++ b/lib/internal/module.js
@@ -78,11 +78,15 @@ function stripShebang(content) {
const builtinLibs = [
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'crypto',
- 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', 'os',
- 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream',
+ 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net',
+ 'os', 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream',
'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib'
];
+const { exposeHTTP2 } = process.binding('config');
+if (exposeHTTP2)
+ builtinLibs.push('http2');
+
function addBuiltinLibsToObject(object) {
// Make built-in modules available directly (loaded lazily).
builtinLibs.forEach((name) => {
diff --git a/node.gyp b/node.gyp
index 1650f1598bf02a..81f549f8b63f73 100644
--- a/node.gyp
+++ b/node.gyp
@@ -37,6 +37,7 @@
'lib/events.js',
'lib/fs.js',
'lib/http.js',
+ 'lib/http2.js',
'lib/_http_agent.js',
'lib/_http_client.js',
'lib/_http_common.js',
@@ -103,6 +104,9 @@
'lib/internal/test/unicode.js',
'lib/internal/url.js',
'lib/internal/util.js',
+ 'lib/internal/http2/core.js',
+ 'lib/internal/http2/compat.js',
+ 'lib/internal/http2/util.js',
'lib/internal/v8_prof_polyfill.js',
'lib/internal/v8_prof_processor.js',
'lib/internal/streams/lazy_transform.js',
@@ -146,6 +150,7 @@
'dependencies': [
'node_js2c#host',
+ 'deps/nghttp2/nghttp2.gyp:nghttp2'
],
'includes': [
@@ -156,7 +161,8 @@
'src',
'tools/msvs/genfiles',
'deps/uv/src/ares',
- '<(SHARED_INTERMEDIATE_DIR)',
+ '<(SHARED_INTERMEDIATE_DIR)', # for node_natives.h
+ 'deps/nghttp2/lib/includes'
],
'sources': [
@@ -178,6 +184,8 @@
'src/node_contextify.cc',
'src/node_debug_options.cc',
'src/node_file.cc',
+ 'src/node_http2_core.cc',
+ 'src/node_http2.cc',
'src/node_http_parser.cc',
'src/node_main.cc',
'src/node_os.cc',
@@ -220,9 +228,12 @@
'src/handle_wrap.h',
'src/js_stream.h',
'src/node.h',
+ 'src/node_http2_core.h',
+ 'src/node_http2_core-inl.h',
'src/node_buffer.h',
'src/node_constants.h',
'src/node_debug_options.h',
+ 'src/node_http2.h',
'src/node_internals.h',
'src/node_javascript.h',
'src/node_mutex.h',
@@ -265,6 +276,8 @@
'NODE_WANT_INTERNALS=1',
# Warn when using deprecated V8 APIs.
'V8_DEPRECATION_WARNINGS=1',
+ # We're using the nghttp2 static lib
+ 'NGHTTP2_STATICLIB'
],
},
{
diff --git a/node.gypi b/node.gypi
index a926d9a8e7ff26..e9905ab4436dd8 100644
--- a/node.gypi
+++ b/node.gypi
@@ -52,6 +52,10 @@
'NODE_RELEASE_URLBASE="<(node_release_urlbase)"',
]
}],
+ [
+ 'debug_http2==1', {
+ 'defines': [ 'NODE_DEBUG_HTTP2=1' ]
+ }],
[ 'v8_enable_i18n_support==1', {
'defines': [ 'NODE_HAVE_I18N_SUPPORT=1' ],
'dependencies': [
diff --git a/src/async-wrap.h b/src/async-wrap.h
index a123ae06e8e936..ffdf8358747f12 100644
--- a/src/async-wrap.h
+++ b/src/async-wrap.h
@@ -41,6 +41,8 @@ namespace node {
V(FSREQWRAP) \
V(GETADDRINFOREQWRAP) \
V(GETNAMEINFOREQWRAP) \
+ V(HTTP2SESSION) \
+ V(HTTP2SESSIONSHUTDOWNWRAP) \
V(HTTPPARSER) \
V(JSSTREAM) \
V(PIPECONNECTWRAP) \
diff --git a/src/env-inl.h b/src/env-inl.h
index f7d9ff626f598a..cbbfceea3f85bc 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -303,6 +303,7 @@ inline Environment::Environment(IsolateData* isolate_data,
#endif
handle_cleanup_waiting_(0),
http_parser_buffer_(nullptr),
+ http2_socket_buffer_(nullptr),
fs_stats_field_array_(nullptr),
context_(context->GetIsolate(), context) {
// We'll be creating new objects so make sure we've entered the context.
@@ -329,6 +330,12 @@ inline Environment::~Environment() {
delete[] heap_statistics_buffer_;
delete[] heap_space_statistics_buffer_;
delete[] http_parser_buffer_;
+ delete[] http2_socket_buffer_;
+ delete[] http2_settings_buffer_;
+ delete[] http2_options_buffer_;
+ delete[] http2_session_state_buffer_;
+ delete[] http2_stream_state_buffer_;
+ delete[] http2_padding_buffer_;
}
inline v8::Isolate* Environment::isolate() const {
@@ -469,6 +476,55 @@ inline void Environment::set_heap_space_statistics_buffer(double* pointer) {
heap_space_statistics_buffer_ = pointer;
}
+inline uint32_t* Environment::http2_settings_buffer() const {
+ CHECK_NE(http2_settings_buffer_, nullptr);
+ return http2_settings_buffer_;
+}
+
+inline void Environment::set_http2_settings_buffer(uint32_t* pointer) {
+ CHECK_EQ(http2_settings_buffer_, nullptr); // Should be set only once
+ http2_settings_buffer_ = pointer;
+}
+
+inline uint32_t* Environment::http2_options_buffer() const {
+ CHECK_NE(http2_options_buffer_, nullptr);
+ return http2_options_buffer_;
+}
+
+inline void Environment::set_http2_options_buffer(uint32_t* pointer) {
+ CHECK_EQ(http2_options_buffer_, nullptr); // Should be set only once
+ http2_options_buffer_ = pointer;
+}
+
+inline double* Environment::http2_session_state_buffer() const {
+ CHECK_NE(http2_session_state_buffer_, nullptr);
+ return http2_session_state_buffer_;
+}
+
+inline void Environment::set_http2_session_state_buffer(double* pointer) {
+ CHECK_EQ(http2_session_state_buffer_, nullptr);
+ http2_session_state_buffer_ = pointer;
+}
+
+inline double* Environment::http2_stream_state_buffer() const {
+ CHECK_NE(http2_stream_state_buffer_, nullptr);
+ return http2_stream_state_buffer_;
+}
+
+inline void Environment::set_http2_stream_state_buffer(double* pointer) {
+ CHECK_EQ(http2_stream_state_buffer_, nullptr);
+ http2_stream_state_buffer_ = pointer;
+}
+
+inline uint32_t* Environment::http2_padding_buffer() const {
+ CHECK_NE(http2_padding_buffer_, nullptr);
+ return http2_padding_buffer_;
+}
+
+inline void Environment::set_http2_padding_buffer(uint32_t* pointer) {
+ CHECK_EQ(http2_padding_buffer_, nullptr);
+ http2_padding_buffer_ = pointer;
+}
inline char* Environment::http_parser_buffer() const {
return http_parser_buffer_;
@@ -488,6 +544,15 @@ inline void Environment::set_fs_stats_field_array(double* fields) {
fs_stats_field_array_ = fields;
}
+inline char* Environment::http2_socket_buffer() const {
+ return http2_socket_buffer_;
+}
+
+inline void Environment::set_http2_socket_buffer(char* buffer) {
+ CHECK_EQ(http2_socket_buffer_, nullptr); // Should be set only once.
+ http2_socket_buffer_ = buffer;
+}
+
inline IsolateData* Environment::isolate_data() const {
return isolate_data_;
}
diff --git a/src/env.h b/src/env.h
index ae8deb5e04f960..3e601b0118d338 100644
--- a/src/env.h
+++ b/src/env.h
@@ -104,6 +104,7 @@ namespace node {
V(configurable_string, "configurable") \
V(cwd_string, "cwd") \
V(dest_string, "dest") \
+ V(destroy_string, "destroy") \
V(detached_string, "detached") \
V(disposed_string, "_disposed") \
V(dns_a_string, "A") \
@@ -117,11 +118,13 @@ namespace node {
V(dns_srv_string, "SRV") \
V(dns_txt_string, "TXT") \
V(domain_string, "domain") \
+ V(emit_string, "emit") \
V(emitting_top_level_domain_error_string, "_emittingTopLevelDomainError") \
V(exchange_string, "exchange") \
V(enumerable_string, "enumerable") \
V(idle_string, "idle") \
V(irq_string, "irq") \
+ V(enablepush_string, "enablePush") \
V(encoding_string, "encoding") \
V(enter_string, "enter") \
V(entries_string, "entries") \
@@ -148,8 +151,11 @@ namespace node {
V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \
V(gid_string, "gid") \
V(handle_string, "handle") \
+ V(heap_total_string, "heapTotal") \
+ V(heap_used_string, "heapUsed") \
V(homedir_string, "homedir") \
V(hostmaster_string, "hostmaster") \
+ V(id_string, "id") \
V(ignore_string, "ignore") \
V(immediate_callback_string, "_immediateCallback") \
V(infoaccess_string, "infoAccess") \
@@ -174,6 +180,7 @@ namespace node {
V(netmask_string, "netmask") \
V(nice_string, "nice") \
V(nsname_string, "nsname") \
+ V(nexttick_string, "nextTick") \
V(ocsp_request_string, "OCSPRequest") \
V(onchange_string, "onchange") \
V(onclienthello_string, "onclienthello") \
@@ -182,19 +189,27 @@ namespace node {
V(ondone_string, "ondone") \
V(onerror_string, "onerror") \
V(onexit_string, "onexit") \
+ V(onframeerror_string, "onframeerror") \
+ V(ongetpadding_string, "ongetpadding") \
V(onhandshakedone_string, "onhandshakedone") \
V(onhandshakestart_string, "onhandshakestart") \
+ V(onheaders_string, "onheaders") \
V(onmessage_string, "onmessage") \
V(onnewsession_string, "onnewsession") \
V(onnewsessiondone_string, "onnewsessiondone") \
V(onocspresponse_string, "onocspresponse") \
+ V(ongoawaydata_string, "ongoawaydata") \
+ V(onpriority_string, "onpriority") \
V(onread_string, "onread") \
V(onreadstart_string, "onreadstart") \
V(onreadstop_string, "onreadstop") \
V(onselect_string, "onselect") \
+ V(onsettings_string, "onsettings") \
V(onshutdown_string, "onshutdown") \
V(onsignal_string, "onsignal") \
V(onstop_string, "onstop") \
+ V(onstreamclose_string, "onstreamclose") \
+ V(ontrailers_string, "ontrailers") \
V(onwrite_string, "onwrite") \
V(output_string, "output") \
V(order_string, "order") \
@@ -234,6 +249,7 @@ namespace node {
V(stack_string, "stack") \
V(status_string, "status") \
V(stdio_string, "stdio") \
+ V(stream_string, "stream") \
V(subject_string, "subject") \
V(subjectaltname_string, "subjectaltname") \
V(sys_string, "sys") \
@@ -262,7 +278,7 @@ namespace node {
V(write_host_object_string, "_writeHostObject") \
V(write_queue_size_string, "writeQueueSize") \
V(x_forwarded_string, "x-forwarded-for") \
- V(zero_return_string, "ZERO_RETURN") \
+ V(zero_return_string, "ZERO_RETURN")
#define ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) \
V(as_external, v8::External) \
@@ -580,8 +596,25 @@ class Environment {
inline double* heap_space_statistics_buffer() const;
inline void set_heap_space_statistics_buffer(double* pointer);
+ inline uint32_t* http2_settings_buffer() const;
+ inline void set_http2_settings_buffer(uint32_t* pointer);
+
+ inline uint32_t* http2_options_buffer() const;
+ inline void set_http2_options_buffer(uint32_t* pointer);
+
+ inline double* http2_session_state_buffer() const;
+ inline void set_http2_session_state_buffer(double* pointer);
+
+ inline double* http2_stream_state_buffer() const;
+ inline void set_http2_stream_state_buffer(double* pointer);
+
+ inline uint32_t* http2_padding_buffer() const;
+ inline void set_http2_padding_buffer(uint32_t* pointer);
+
inline char* http_parser_buffer() const;
inline void set_http_parser_buffer(char* buffer);
+ inline char* http2_socket_buffer() const;
+ inline void set_http2_socket_buffer(char* buffer);
inline double* fs_stats_field_array() const;
inline void set_fs_stats_field_array(double* fields);
@@ -687,8 +720,14 @@ class Environment {
double* heap_statistics_buffer_ = nullptr;
double* heap_space_statistics_buffer_ = nullptr;
+ uint32_t* http2_settings_buffer_ = nullptr;
+ uint32_t* http2_options_buffer_ = nullptr;
+ double* http2_session_state_buffer_ = nullptr;
+ double* http2_stream_state_buffer_ = nullptr;
+ uint32_t* http2_padding_buffer_ = nullptr;
char* http_parser_buffer_;
+ char* http2_socket_buffer_;
double* fs_stats_field_array_;
diff --git a/src/freelist.h b/src/freelist.h
new file mode 100644
index 00000000000000..7dff56a35d348a
--- /dev/null
+++ b/src/freelist.h
@@ -0,0 +1,92 @@
+#ifndef SRC_FREELIST_H_
+#define SRC_FREELIST_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "util.h"
+
+namespace node {
+
+struct DefaultFreelistTraits;
+
+template
+class Freelist {
+ public:
+ typedef struct list_item {
+ T* item = nullptr;
+ list_item* next = nullptr;
+ } list_item;
+
+ Freelist() {}
+ ~Freelist() {
+ while (head_ != nullptr) {
+ list_item* item = head_;
+ head_ = item->next;
+ FreelistTraits::Free(item->item);
+ free(item);
+ }
+ }
+
+ void push(T* item) {
+ if (size_ > kMaximumLength) {
+ FreelistTraits::Free(item);
+ } else {
+ size_++;
+ FreelistTraits::Reset(item);
+ list_item* li = Calloc(1);
+ li->item = item;
+ if (head_ == nullptr) {
+ head_ = li;
+ tail_ = li;
+ } else {
+ tail_->next = li;
+ tail_ = li;
+ }
+ }
+ }
+
+ T* pop() {
+ if (head_ != nullptr) {
+ size_--;
+ list_item* cur = head_;
+ T* item = cur->item;
+ head_ = cur->next;
+ free(cur);
+ return item;
+ } else {
+ return FreelistTraits::template Alloc();
+ }
+ }
+
+ private:
+ size_t size_ = 0;
+ list_item* head_ = nullptr;
+ list_item* tail_ = nullptr;
+};
+
+struct DefaultFreelistTraits {
+ template
+ static T* Alloc() {
+ return ::new (Malloc(1)) T();
+ }
+
+ template
+ static void Free(T* item) {
+ item->~T();
+ free(item);
+ }
+
+ template
+ static void Reset(T* item) {
+ item->~T();
+ ::new (item) T();
+ }
+};
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#endif // SRC_FREELIST_H_
diff --git a/src/node.cc b/src/node.cc
index 551f9fbf396949..775accc0412606 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -59,6 +59,7 @@
#include "env-inl.h"
#include "handle_wrap.h"
#include "http_parser.h"
+#include "nghttp2/nghttp2ver.h"
#include "req-wrap.h"
#include "req-wrap-inl.h"
#include "string_bytes.h"
@@ -232,6 +233,9 @@ std::string config_warning_file; // NOLINT(runtime/string)
// that is used by lib/internal/bootstrap_node.js
bool config_expose_internals = false;
+// Set in node.cc by ParseArgs when --expose-http2 is used.
+bool config_expose_http2 = false;
+
bool v8_initialized = false;
bool linux_at_secure = false;
@@ -3210,6 +3214,10 @@ void SetupProcessObject(Environment* env,
"modules",
FIXED_ONE_BYTE_STRING(env->isolate(), node_modules_version));
+ READONLY_PROPERTY(versions,
+ "nghttp2",
+ FIXED_ONE_BYTE_STRING(env->isolate(), NGHTTP2_VERSION));
+
// process._promiseRejectEvent
Local