summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xconfigure22
-rw-r--r--doc/api/_toc.md1
-rw-r--r--doc/api/cli.md7
-rw-r--r--doc/api/errors.md184
-rwxr-xr-xdoc/api/http2.md1704
-rw-r--r--doc/guides/writing-and-running-benchmarks.md9
-rw-r--r--doc/node.14
-rw-r--r--[-rwxr-xr-x]lib/_http_outgoing.js0
-rw-r--r--lib/http2.js27
-rw-r--r--lib/internal/bootstrap_node.js7
-rw-r--r--lib/internal/errors.js64
-rw-r--r--[-rwxr-xr-x]lib/internal/http.js0
-rw-r--r--lib/internal/http2/compat.js570
-rw-r--r--lib/internal/http2/core.js2392
-rw-r--r--lib/internal/http2/util.js513
-rw-r--r--lib/internal/module.js8
-rw-r--r--node.gyp15
-rw-r--r--node.gypi4
-rw-r--r--src/async-wrap.h2
-rw-r--r--src/env-inl.h65
-rw-r--r--src/env.h41
-rw-r--r--src/freelist.h92
-rw-r--r--src/node.cc13
-rw-r--r--src/node.h19
-rw-r--r--src/node_config.cc3
-rw-r--r--src/node_crypto_bio.cc1
-rw-r--r--src/node_http2.cc1326
-rw-r--r--src/node_http2.h572
-rw-r--r--src/node_http2_core-inl.h590
-rw-r--r--src/node_http2_core.cc326
-rw-r--r--src/node_http2_core.h465
-rw-r--r--src/node_internals.h3
-rw-r--r--src/stream_base.cc1
-rw-r--r--src/stream_base.h11
-rw-r--r--vcbuild.bat7
35 files changed, 9061 insertions, 7 deletions
diff --git a/configure b/configure
index 9f327d1377..44ec5d2c47 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 1075bc6be3..6791e63f0c 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 d4703c1cf0..6665941480 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`
+<!-- YAML
+added: REPLACEME
+-->
+
+Enable the experimental `'http2'` module.
+
### `--napi-modules`
<!-- YAML
added: v8.0.0
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 5de3fa4067..cb55ed05fd 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -624,6 +624,190 @@ Used for status codes outside the regular status code ranges (100-999).
Used when the `Trailer` header is set even though the transfer encoding does not
support that.
+<a id="ERR_HTTP2_CONNECT_AUTHORITY"></a>
+### ERR_HTTP2_CONNECT_AUTHORITY
+
+For HTTP/2 requests using the `CONNECT` method, the `:authority` pseudo-header
+is required.
+
+<a id="ERR_HTTP2_CONNECT_PATH"></a>
+### ERR_HTTP2_CONNECT_PATH
+
+For HTTP/2 requests using the `CONNECT` method, the `:path` pseudo-header is
+forbidden.
+
+<a id="ERR_HTTP2_CONNECT_SCHEME"></a>
+### ERR_HTTP2_CONNECT_SCHEME
+
+The HTTP/2 requests using the `CONNECT` method, the `:scheme` pseudo-header is
+forbidden.
+
+<a id="ERR_HTTP2_ERROR"></a>
+### ERR_HTTP2_ERROR
+
+A non-specific HTTP/2 error has occurred.
+
+<a id="ERR_HTTP2_FRAME_ERROR"></a>
+### ERR_HTTP2_FRAME_ERROR
+
+Used when a failure occurs sending an individual frame on the HTTP/2
+session.
+
+<a id="ERR_HTTP2_HEADERS_OBJECT"></a>
+### ERR_HTTP2_HEADERS_OBJECT
+
+Used when an HTTP/2 Headers Object is expected.
+
+<a id="ERR_HTTP2_HEADERS_SENT"></a>
+### ERR_HTTP2_HEADERS_SENT
+
+Used when an attempt is made to send multiple response headers.
+
+<a id="ERR_HTTP2_HEADER_SINGLE_VALUE"></a>
+### ERR_HTTP2_HEADER_SINGLE_VALUE
+
+Used when multiple values have been provided for an HTTP header field that
+required to have only a single value.
+
+<a id="ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND"></a>
+### ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND
+
+HTTP/2 Informational headers must only be sent *prior* to calling the
+`Http2Stream.prototype.respond()` method.
+
+<a id="ERR_HTTP2_INFO_STATUS_NOT_ALLOWED"></a>
+### ERR_HTTP2_INFO_STATUS_NOT_ALLOWED
+
+Informational HTTP status codes (`1xx`) may not be set as the response status
+code on HTTP/2 responses.
+
+<a id="ERR_HTTP2_INVALID_CONNECTION_HEADERS"></a>
+### ERR_HTTP2_INVALID_CONNECTION_HEADERS
+
+HTTP/1 connection specific headers are forbidden to be used in HTTP/2
+requests and responses.
+
+<a id="ERR_HTTP2_INVALID_HEADER_VALUE"></a>
+### ERR_HTTP2_INVALID_HEADER_VALUE
+
+Used to indicate that an invalid HTTP/2 header value has been specified.
+
+<a id="ERR_HTTP2_INVALID_INFO_STATUS"></a>
+### ERR_HTTP2_INVALID_INFO_STATUS
+
+An invalid HTTP informational status code has been specified. Informational
+status codes must be an integer between `100` and `199` (inclusive).
+
+<a id="ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH"></a>
+
+Input `Buffer` and `Uint8Array` instances passed to the
+`http2.getUnpackedSettings()` API must have a length that is a multiple of
+six.
+
+<a id="ERR_HTTP2_INVALID_PSEUDOHEADER"></a>
+### ERR_HTTP2_INVALID_PSEUDOHEADER
+
+Only valid HTTP/2 pseudoheaders (`:status`, `:path`, `:authority`, `:scheme`,
+and `:method`) may be used.
+
+<a id="ERR_HTTP2_INVALID_SESSION"></a>
+### ERR_HTTP2_INVALID_SESSION
+
+Used when any action is performed on an `Http2Session` object that has already
+been destroyed.
+
+<a id="ERR_HTTP2_INVALID_SETTING_VALUE"></a>
+### ERR_HTTP2_INVALID_SETTING_VALUE
+
+An invalid value has been specified for an HTTP/2 setting.
+
+<a id="ERR_HTTP2_INVALID_STREAM"></a>
+### ERR_HTTP2_INVALID_STREAM
+
+Used when an operation has been performed on a stream that has already been
+destroyed.
+
+<a id="ERR_HTTP2_MAX_PENDING_SETTINGS_ACK"></a>
+### ERR_HTTP2_MAX_PENDING_SETTINGS_ACK
+
+Whenever an HTTP/2 `SETTINGS` frame is sent to a connected peer, the peer is
+required to send an acknowledgement that it has received and applied the new
+SETTINGS. By default, a maximum number of un-acknowledged `SETTINGS` frame may
+be sent at any given time. This error code is used when that limit has been
+reached.
+
+<a id="ERR_HTTP2_OUT_OF_STREAMS"></a>
+### ERR_HTTP2_OUT_OF_STREAMS
+
+Used when the maximum number of streams on a single HTTP/2 session have been
+created.
+
+<a id="ERR_HTTP2_PAYLOAD_FORBIDDEN"></a>
+### ERR_HTTP2_PAYLOAD_FORBIDDEN
+
+Used when a message payload is specified for an HTTP response code for which
+a payload is forbidden.
+
+<a id="ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED"></a>
+### ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED
+
+Used to indicate that an HTTP/2 pseudo-header has been used inappropriately.
+Pseudo-headers are header key names that begin with the `:` prefix.
+
+<a id="ERR_HTTP2_PUSH_DISABLED"></a>
+### ERR_HTTP2_PUSH_DISABLED
+
+Used when push streams have been disabled by the client but an attempt to
+create a push stream is made.
+
+<a id="ERR_HTTP2_SEND_FILE"></a>
+### ERR_HTTP2_SEND_FILE
+
+Used when an attempt is made to use the
+`Http2Stream.prototype.responseWithFile()` API to send a non-regular file.
+
+<a id="ERR_HTTP2_SOCKET_BOUND"></a>
+### ERR_HTTP2_SOCKET_BOUND
+
+Used when an attempt is made to connect a `Http2Session` object to a
+`net.Socket` or `tls.TLSSocket` that has already been bound to another
+`Http2Session` object.
+
+<a id="ERR_HTTP2_STATUS_101"></a>
+### ERR_HTTP2_STATUS_101
+
+Use of the `101` Informational status code is forbidden in HTTP/2.
+
+<a id="ERR_HTTP2_STATUS_INVALID"></a>
+### ERR_HTTP2_STATUS_INVALID
+
+An invalid HTTP status code has been specified. Status codes must be an integer
+between `100` and `599` (inclusive).
+
+<a id="ERR_HTTP2_STREAM_CLOSED"></a>
+### ERR_HTTP2_STREAM_CLOSED
+
+Used when an action has been performed on an HTTP/2 Stream that has already
+been closed.
+
+<a id="ERR_HTTP2_STREAM_ERROR"></a>
+### ERR_HTTP2_STREAM_ERROR
+
+Used when a non-zero error code has been specified in an RST_STREAM frame.
+
+<a id="ERR_HTTP2_STREAM_SELF_DEPENDENCY"></a>
+### ERR_HTTP2_STREAM_SELF_DEPENDENCY
+
+When setting the priority for an HTTP/2 stream, the stream may be marked as
+a dependency for a parent stream. This error code is used when an attempt is
+made to mark a stream and dependent of itself.
+
+<a id="ERR_HTTP2_UNSUPPORTED_PROTOCOL"></a>
+### ERR_HTTP2_UNSUPPORTED_PROTOCOL
+
+Used when `http2.connect()` is passed a URL that uses any protocol other than
+`http:` or `https:`.
+
<a id="ERR_INDEX_OUT_OF_RANGE"></a>
### ERR_INDEX_OUT_OF_RANGE
diff --git a/doc/api/http2.md b/doc/api/http2.md
new file mode 100755
index 0000000000..ca696de81e
--- /dev/null
+++ b/doc/api/http2.md
@@ -0,0 +1,1704 @@
+# HTTP2
+
+> Stability: 1 - Experimental
+
+The `http2` module provides an implementation of the [HTTP/2][] protocol. It
+can be accessed using:
+
+```js
+const http2 = require('http2');
+```
+
+*Note*: Node.js must be launched with the `--expose-http2` command line flag
+in order to use the `'http2'` module.
+
+## Core API
+
+The Core API provides a low-level interface designed specifically around
+support for HTTP/2 protocol features. It is specifically *not* designed for
+compatibility with the existing [HTTP/1][] module API.
+
+The following illustrates a simple, plain-text HTTP/2 server:
+
+```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('<h1>Hello World</h1>');
+});
+
+server.listen(80);
+```
+
+The following illustrates an HTTP/2 client:
+
+```js
+const http2 = require('http2');
+
+const client = http2.connect('http://localhost:80');
+
+const req = client.request({ ':path': '/' });
+
+req.on('response', (headers) => {
+ console.log(headers[':status']);
+ console.log(headers['date']);
+});
+
+let data = '';
+req.setEncoding('utf8');
+req.on('data', (d) => data += d);
+req.on('end', () => client.destroy());
+req.end();
+```
+
+### Class: Http2Session
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'close'` event is emitted once the `Http2Session` has been terminated.
+
+#### Event: 'connect'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'error'` event is emitted when an error occurs during the processing of
+an `Http2Session`.
+
+#### Event: 'frameError'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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('<h1>Hello World</h1>');
+});
+
+server.listen(80);
+```
+
+#### Event: 'socketError'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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()
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {undefined}
+
+Immediately terminates the `Http2Session` and the associated `net.Socket` or
+`tls.TLSSocket`.
+
+#### http2session.destroyed
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Will be `true` if this `Http2Session` instance has been destroyed and must no
+longer be used, otherwise `false`.
+
+#### http2session.localSettings
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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 2<sup>32</sup>-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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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 2<sup>31</sup>-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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'error'` event is emitted when an error occurs during the processing of
+an `Http2Stream`.
+
+#### Event: 'fetchTrailers'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'timeout'` event is emitted after no activity is received for this
+`'Http2Stream'` within the number of millseconds set using
+`http2stream.setTimeout()`.
+
+#### Event: 'trailers'
+<!-- YAML
+added: REPLACEME
+-->
+
+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
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Set to `true` if the `Http2Stream` instance was aborted abnormally. When set,
+the `'aborted'` event will have been emitted.
+
+#### http2stream.destroyed
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Set to `true` if the `Http2Stream` instance has been destroyed and is no longer
+usable.
+
+#### http2stream.priority(options)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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()
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error).
+
+#### http2stream.rstWithProtocolError() {
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error).
+
+#### http2stream.rstWithCancel() {
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel).
+
+#### http2stream.rstWithRefuse() {
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream).
+
+#### http2stream.rstWithInternalError() {
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {undefined}
+
+Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error).
+
+#### http2stream.session
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {Object}
+ * `localWindowSize` {number}
+ * `state` {number}
+ * `streamLocalClose` {number}
+ * `streamRemoteClose` {number}
+ * `sumDependencyWeight` {number}
+ * `weight` {number}
+
+A current state of this `Http2Stream`.
+
+### Class: ClientHttp2Stream
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `headers` {[Headers Object][]}
+* Returns: {undefined}
+
+Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer.
+
+#### http2stream.headersSent
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Boolean (read-only). True if headers were sent, false otherwise.
+
+#### http2stream.pushAllowed
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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]])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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]])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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
+<!-- YAML
+added: REPLACEME
+-->
+
+* Extends: {net.Server}
+
+#### Event: 'sessionError'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'timeout'` event is emitted when there is no activity on the Server for
+a given number of milliseconds set using `http2server.setTimeout()`.
+
+### Class: Http2SecureServer
+<!-- YAML
+added: REPLACEME
+-->
+
+* Extends: {tls.Server}
+
+#### Event: 'sessionError'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+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'
+<!-- YAML
+added: REPLACEME
+-->
+
+### http2.createServer(options[, onRequestHandler])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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('<h1>Hello World</h1>');
+});
+
+server.listen(80);
+```
+
+### http2.createSecureServer(options[, onRequestHandler])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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('<h1>Hello World</h1>');
+});
+
+server.listen(80);
+```
+
+### http2.connect(authority[, options][, listener])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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
+<!-- YAML
+added: REPLACEME
+-->
+
+#### Error Codes for RST_STREAM and GOAWAY
+<a id="error_codes"></a>
+
+| 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()
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {[Settings Object][]}
+
+Returns an object containing the default settings for an `Http2Session`
+instance. This method returns a new object instance every time it is called
+so instances returned may be safely modified for use.
+
+### http2.getPackedSettings(settings)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `settings` {[Settings Object][]}
+* Returns: {Buffer}
+
+Returns a [Buffer][] instance containing serialized representation of the given
+HTTP/2 settings as specified in the [HTTP/2][] specification. This is intended
+for use with the `HTTP2-Settings` header field.
+
+```js
+const http2 = require('http2');
+
+const packed = http2.getPackedSettings({ enablePush: false });
+
+console.log(packed.toString('base64'));
+// Prints: AAIAAAAA
+```
+
+### http2.getUnpackedSettings(buf)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `buf` {Buffer|Uint8Array} The packed settings
+* Returns: {[Settings Object][]}
+
+Returns a [Settings Object][] containing the deserialized settings from the
+given `Buffer` as generated by `http2.getPackedSettings()`.
+
+### Headers Object
+
+Headers are represented as own-properties on JavaScript objects. The property
+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 2<sup>32</sup>-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 2<sup>32</sup>-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 2<sup>24</sup>-1.
+* `maxConcurrentStreams` {number} Specifies the maximum number of concurrent
+ streams permitted on an `Http2Session`. There is no default value which
+ implies, at least theoretically, 2<sup>31</sup>-1 streams may be open
+ concurrently at any given time in an `Http2Session`. The minimum value is
+ 0. The maximum allowed value is 2<sup>31</sup>-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 2<sup>32</sup>-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 3135f2115d..7aeb9728aa 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 ca142a2cab..c97a9d812d 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -131,6 +131,10 @@ Emit pending deprecation 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)
(experimental).
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
index f8ae30d4de..f8ae30d4de 100755..100644
--- a/lib/_http_outgoing.js
+++ b/lib/_http_outgoing.js
diff --git a/lib/http2.js b/lib/http2.js
new file mode 100644
index 0000000000..e964abf589
--- /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 feb6b3e366..3ba685b942 100644
--- a/lib/internal/bootstrap_node.js
+++ b/lib/internal/bootstrap_node.js
@@ -478,6 +478,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;
@@ -516,8 +521,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 9989528f76..be057259d7 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -118,6 +118,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_ARRAY_LENGTH',
@@ -173,6 +236,7 @@ E('ERR_SOCKET_BAD_TYPE',
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data');
E('ERR_SOCKET_CLOSED', 'Socket is closed');
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_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode');
diff --git a/lib/internal/http.js b/lib/internal/http.js
index 71e32498f3..71e32498f3 100755..100644
--- a/lib/internal/http.js
+++ b/lib/internal/http.js
diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js
new file mode 100644
index 0000000000..cd9a1fa2b7
--- /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 0000000000..1bdd57926c
--- /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 '<invalid>';
+ }
+}
+
+// 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 0000000000..ea36444fad
--- /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 08d8f770c8..cf994b51c0 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 1650f1598b..81f549f8b6 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 a926d9a8e7..e9905ab443 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 79332d9a42..a02356e845 100644
--- a/src/async-wrap.h
+++ b/src/async-wrap.h
@@ -40,6 +40,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 6753979ef5..021d50841d 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -302,6 +302,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.
@@ -328,6 +329,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 {
@@ -468,6 +475,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_;
@@ -487,6 +543,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 e1375080d1..59fe10448a 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) \
@@ -579,8 +595,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);
@@ -686,8 +719,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 0000000000..7dff56a35d
--- /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 <typename T,
+ size_t kMaximumLength,
+ typename FreelistTraits = DefaultFreelistTraits>
+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<list_item>(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<T>();
+ }
+ }
+
+ private:
+ size_t size_ = 0;
+ list_item* head_ = nullptr;
+ list_item* tail_ = nullptr;
+};
+
+struct DefaultFreelistTraits {
+ template <typename T>
+ static T* Alloc() {
+ return ::new (Malloc<T>(1)) T();
+ }
+
+ template <typename T>
+ static void Free(T* item) {
+ item->~T();
+ free(item);
+ }
+
+ template <typename T>
+ 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 741b5dea82..5b1b9cadd9 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<Object> promiseRejectEvent = Object::New(env->isolate());
READONLY_DONT_ENUM_PROPERTY(process,
@@ -3648,6 +3656,7 @@ static void PrintHelp() {
" --abort-on-uncaught-exception\n"
" aborting instead of exiting causes a\n"
" core file to be generated for analysis\n"
+ " --expose-http2 enable experimental HTTP2 support\n"
" --trace-warnings show stack traces on process warnings\n"
" --redirect-warnings=file\n"
" write warnings to file instead of\n"
@@ -3768,6 +3777,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env,
"--throw-deprecation",
"--no-warnings",
"--napi-modules",
+ "--expose-http2",
"--trace-warnings",
"--redirect-warnings",
"--trace-sync-io",
@@ -3965,6 +3975,9 @@ static void ParseArgs(int* argc,
} else if (strcmp(arg, "--expose-internals") == 0 ||
strcmp(arg, "--expose_internals") == 0) {
config_expose_internals = true;
+ } else if (strcmp(arg, "--expose-http2") == 0 ||
+ strcmp(arg, "--expose_http2") == 0) {
+ config_expose_http2 = true;
} else if (strcmp(arg, "-") == 0) {
break;
} else if (strcmp(arg, "--") == 0) {
diff --git a/src/node.h b/src/node.h
index b1d179bd16..59728685f7 100644
--- a/src/node.h
+++ b/src/node.h
@@ -253,6 +253,25 @@ NODE_EXTERN void RunAtExit(Environment* env);
} \
while (0)
+#define NODE_DEFINE_HIDDEN_CONSTANT(target, constant) \
+ do { \
+ v8::Isolate* isolate = target->GetIsolate(); \
+ v8::Local<v8::Context> context = isolate->GetCurrentContext(); \
+ v8::Local<v8::String> constant_name = \
+ v8::String::NewFromUtf8(isolate, #constant); \
+ v8::Local<v8::Number> constant_value = \
+ v8::Number::New(isolate, static_cast<double>(constant)); \
+ v8::PropertyAttribute constant_attributes = \
+ static_cast<v8::PropertyAttribute>(v8::ReadOnly | \
+ v8::DontDelete | \
+ v8::DontEnum); \
+ (target)->DefineOwnProperty(context, \
+ constant_name, \
+ constant_value, \
+ constant_attributes).FromJust(); \
+ } \
+ while (0)
+
// Used to be a macro, hence the uppercase name.
inline void NODE_SET_METHOD(v8::Local<v8::Template> recv,
const char* name,
diff --git a/src/node_config.cc b/src/node_config.cc
index b309171282..041e18f6b7 100644
--- a/src/node_config.cc
+++ b/src/node_config.cc
@@ -88,6 +88,9 @@ static void InitConfig(Local<Object> target,
if (config_expose_internals)
READONLY_BOOLEAN_PROPERTY("exposeInternals");
+
+ if (config_expose_http2)
+ READONLY_BOOLEAN_PROPERTY("exposeHTTP2");
} // InitConfig
} // namespace node
diff --git a/src/node_crypto_bio.cc b/src/node_crypto_bio.cc
index 00fd0b420c..4c84973f75 100644
--- a/src/node_crypto_bio.cc
+++ b/src/node_crypto_bio.cc
@@ -357,7 +357,6 @@ size_t NodeBIO::IndexOf(char delim, size_t limit) {
return max;
}
-
void NodeBIO::Write(const char* data, size_t size) {
size_t offset = 0;
size_t left = size;
diff --git a/src/node_http2.cc b/src/node_http2.cc
new file mode 100644
index 0000000000..5ad1352cc1
--- /dev/null
+++ b/src/node_http2.cc
@@ -0,0 +1,1326 @@
+#include "node.h"
+#include "node_buffer.h"
+#include "node_http2.h"
+
+namespace node {
+
+using v8::ArrayBuffer;
+using v8::Boolean;
+using v8::Context;
+using v8::Function;
+using v8::Integer;
+using v8::Undefined;
+
+namespace http2 {
+
+enum Http2SettingsIndex {
+ IDX_SETTINGS_HEADER_TABLE_SIZE,
+ IDX_SETTINGS_ENABLE_PUSH,
+ IDX_SETTINGS_INITIAL_WINDOW_SIZE,
+ IDX_SETTINGS_MAX_FRAME_SIZE,
+ IDX_SETTINGS_MAX_CONCURRENT_STREAMS,
+ IDX_SETTINGS_MAX_HEADER_LIST_SIZE,
+ IDX_SETTINGS_COUNT
+};
+
+enum Http2SessionStateIndex {
+ IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE,
+ IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH,
+ IDX_SESSION_STATE_NEXT_STREAM_ID,
+ IDX_SESSION_STATE_LOCAL_WINDOW_SIZE,
+ IDX_SESSION_STATE_LAST_PROC_STREAM_ID,
+ IDX_SESSION_STATE_REMOTE_WINDOW_SIZE,
+ IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE,
+ IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE,
+ IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE,
+ IDX_SESSION_STATE_COUNT
+};
+
+enum Http2StreamStateIndex {
+ IDX_STREAM_STATE,
+ IDX_STREAM_STATE_WEIGHT,
+ IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT,
+ IDX_STREAM_STATE_LOCAL_CLOSE,
+ IDX_STREAM_STATE_REMOTE_CLOSE,
+ IDX_STREAM_STATE_LOCAL_WINDOW_SIZE,
+ IDX_STREAM_STATE_COUNT
+};
+
+enum Http2OptionsIndex {
+ IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE,
+ IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS,
+ IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH,
+ IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS,
+ IDX_OPTIONS_PADDING_STRATEGY,
+ IDX_OPTIONS_FLAGS
+};
+
+Http2Options::Http2Options(Environment* env) {
+ nghttp2_option_new(&options_);
+
+ uint32_t* buffer = env->http2_options_buffer();
+ uint32_t flags = buffer[IDX_OPTIONS_FLAGS];
+
+ if ((flags & (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)) ==
+ (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)) {
+ SetMaxDeflateDynamicTableSize(
+ buffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE]);
+ }
+
+ if ((flags & (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)) ==
+ (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)) {
+ SetMaxReservedRemoteStreams(
+ buffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS]);
+ }
+
+ if ((flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)) ==
+ (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)) {
+ SetMaxSendHeaderBlockLength(
+ buffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH]);
+ }
+
+ SetPeerMaxConcurrentStreams(100); // Recommended default
+ if ((flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)) ==
+ (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)) {
+ SetPeerMaxConcurrentStreams(
+ buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
+ }
+
+ if ((flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)) ==
+ (1 << IDX_OPTIONS_PADDING_STRATEGY)) {
+ SetPaddingStrategy(buffer[IDX_OPTIONS_PADDING_STRATEGY]);
+ }
+}
+
+inline void CopyHeaders(Isolate* isolate,
+ Local<Context> context,
+ MaybeStackBuffer<nghttp2_nv>* list,
+ Local<Array> headers) {
+ Local<Value> item;
+ Local<Array> header;
+
+ for (size_t n = 0; n < headers->Length(); n++) {
+ item = headers->Get(context, n).ToLocalChecked();
+ header = item.As<Array>();
+ Local<Value> key = header->Get(context, 0).ToLocalChecked();
+ Local<Value> value = header->Get(context, 1).ToLocalChecked();
+ CHECK(key->IsString());
+ CHECK(value->IsString());
+ size_t keylen = StringBytes::StorageSize(isolate, key, ASCII);
+ size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII);
+ nghttp2_nv& nv = (*list)[n];
+ nv.flags = NGHTTP2_NV_FLAG_NONE;
+ Local<Value> flag = header->Get(context, 2).ToLocalChecked();
+ if (flag->BooleanValue(context).ToChecked())
+ nv.flags |= NGHTTP2_NV_FLAG_NO_INDEX;
+ nv.name = Malloc<uint8_t>(keylen);
+ nv.value = Malloc<uint8_t>(valuelen);
+ nv.namelen =
+ StringBytes::Write(isolate,
+ reinterpret_cast<char*>(nv.name),
+ keylen, key, ASCII);
+ nv.valuelen =
+ StringBytes::Write(isolate,
+ reinterpret_cast<char*>(nv.value),
+ valuelen, value, ASCII);
+ }
+}
+
+inline void FreeHeaders(MaybeStackBuffer<nghttp2_nv>* list) {
+ for (size_t n = 0; n < list->length(); n++) {
+ free((*list)[n].name);
+ free((*list)[n].value);
+ }
+}
+
+void Http2Session::OnFreeSession() {
+ ::delete this;
+}
+
+ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
+ size_t maxPayloadLen) {
+ DEBUG_HTTP2("Http2Session: using max frame size padding\n");
+ return maxPayloadLen;
+}
+
+ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
+ size_t maxPayloadLen) {
+ DEBUG_HTTP2("Http2Session: using callback padding\n");
+ Isolate* isolate = env()->isolate();
+ Local<Context> context = env()->context();
+
+ HandleScope handle_scope(isolate);
+ Context::Scope context_scope(context);
+
+ if (object()->Has(context, env()->ongetpadding_string()).FromJust()) {
+ uint32_t* buffer = env()->http2_padding_buffer();
+ buffer[0] = frameLen;
+ buffer[1] = maxPayloadLen;
+ MakeCallback(env()->ongetpadding_string(), 0, nullptr);
+ uint32_t retval = buffer[2];
+ retval = retval <= maxPayloadLen ? retval : maxPayloadLen;
+ retval = retval >= frameLen ? retval : frameLen;
+ CHECK_GE(retval, frameLen);
+ CHECK_LE(retval, maxPayloadLen);
+ return retval;
+ }
+ return frameLen;
+}
+
+void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ nghttp2_session* s = session->session();
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ DEBUG_HTTP2("Http2Session: setting next stream id to %d\n", id);
+ nghttp2_session_set_next_stream_id(s, id);
+}
+
+void HttpErrorString(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ uint32_t val = args[0]->Uint32Value(env->context()).ToChecked();
+ args.GetReturnValue().Set(
+ OneByteString(env->isolate(), nghttp2_strerror(val)));
+}
+
+// Serializes the settings object into a Buffer instance that
+// would be suitable, for instance, for creating the Base64
+// output for an HTTP2-Settings header field.
+void PackSettings(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ HandleScope scope(env->isolate());
+
+ std::vector<nghttp2_settings_entry> entries;
+ entries.reserve(6);
+
+ uint32_t* const buffer = env->http2_settings_buffer();
+ uint32_t flags = buffer[IDX_SETTINGS_COUNT];
+
+ if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) ==
+ (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) {
+ DEBUG_HTTP2("Setting header table size: %d\n",
+ buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE,
+ buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) ==
+ (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) {
+ DEBUG_HTTP2("Setting max concurrent streams: %d\n",
+ buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]);
+ entries.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
+ buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) ==
+ (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) {
+ DEBUG_HTTP2("Setting max frame size: %d\n",
+ buffer[IDX_SETTINGS_MAX_FRAME_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_MAX_FRAME_SIZE,
+ buffer[IDX_SETTINGS_MAX_FRAME_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) ==
+ (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) {
+ DEBUG_HTTP2("Setting initial window size: %d\n",
+ buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE,
+ buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) ==
+ (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) {
+ DEBUG_HTTP2("Setting max header list size: %d\n",
+ buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE,
+ buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) ==
+ (1 << IDX_SETTINGS_ENABLE_PUSH)) {
+ DEBUG_HTTP2("Setting enable push: %d\n",
+ buffer[IDX_SETTINGS_ENABLE_PUSH]);
+ entries.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH,
+ buffer[IDX_SETTINGS_ENABLE_PUSH]});
+ }
+
+ const size_t len = entries.size() * 6;
+ MaybeStackBuffer<char> buf(len);
+ ssize_t ret =
+ nghttp2_pack_settings_payload(
+ reinterpret_cast<uint8_t*>(*buf), len, &entries[0], entries.size());
+ if (ret >= 0) {
+ args.GetReturnValue().Set(
+ Buffer::Copy(env, *buf, len).ToLocalChecked());
+ }
+}
+
+// Used to fill in the spec defined initial values for each setting.
+void RefreshDefaultSettings(const FunctionCallbackInfo<Value>& args) {
+ DEBUG_HTTP2("Http2Session: refreshing default settings\n");
+ Environment* env = Environment::GetCurrent(args);
+ uint32_t* const buffer = env->http2_settings_buffer();
+ buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =
+ DEFAULT_SETTINGS_HEADER_TABLE_SIZE;
+ buffer[IDX_SETTINGS_ENABLE_PUSH] =
+ DEFAULT_SETTINGS_ENABLE_PUSH;
+ buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] =
+ DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE;
+ buffer[IDX_SETTINGS_MAX_FRAME_SIZE] =
+ DEFAULT_SETTINGS_MAX_FRAME_SIZE;
+ buffer[IDX_SETTINGS_COUNT] =
+ (1 << IDX_SETTINGS_HEADER_TABLE_SIZE) |
+ (1 << IDX_SETTINGS_ENABLE_PUSH) |
+ (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE) |
+ (1 << IDX_SETTINGS_MAX_FRAME_SIZE);
+}
+
+template <get_setting fn>
+void RefreshSettings(const FunctionCallbackInfo<Value>& args) {
+ DEBUG_HTTP2("Http2Session: refreshing settings for session\n");
+ CHECK_EQ(args.Length(), 1);
+ CHECK(args[0]->IsObject());
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
+ Environment* env = session->env();
+ nghttp2_session* s = session->session();
+
+ uint32_t* const buffer = env->http2_settings_buffer();
+ buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =
+ fn(s, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE);
+ buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] =
+ fn(s, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
+ buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] =
+ fn(s, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
+ buffer[IDX_SETTINGS_MAX_FRAME_SIZE] =
+ fn(s, NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
+ buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] =
+ fn(s, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
+ buffer[IDX_SETTINGS_ENABLE_PUSH] =
+ fn(s, NGHTTP2_SETTINGS_ENABLE_PUSH);
+}
+
+// Used to fill in the spec defined initial values for each setting.
+void RefreshSessionState(const FunctionCallbackInfo<Value>& args) {
+ DEBUG_HTTP2("Http2Session: refreshing session state\n");
+ CHECK_EQ(args.Length(), 1);
+ CHECK(args[0]->IsObject());
+ Environment* env = Environment::GetCurrent(args);
+ double* const buffer = env->http2_session_state_buffer();
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
+ nghttp2_session* s = session->session();
+
+ buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] =
+ nghttp2_session_get_effective_local_window_size(s);
+ buffer[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH] =
+ nghttp2_session_get_effective_recv_data_length(s);
+ buffer[IDX_SESSION_STATE_NEXT_STREAM_ID] =
+ nghttp2_session_get_next_stream_id(s);
+ buffer[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE] =
+ nghttp2_session_get_local_window_size(s);
+ buffer[IDX_SESSION_STATE_LAST_PROC_STREAM_ID] =
+ nghttp2_session_get_last_proc_stream_id(s);
+ buffer[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE] =
+ nghttp2_session_get_remote_window_size(s);
+ buffer[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE] =
+ nghttp2_session_get_outbound_queue_size(s);
+ buffer[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE] =
+ nghttp2_session_get_hd_deflate_dynamic_table_size(s);
+ buffer[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE] =
+ nghttp2_session_get_hd_inflate_dynamic_table_size(s);
+}
+
+void RefreshStreamState(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK_EQ(args.Length(), 2);
+ CHECK(args[0]->IsObject());
+ CHECK(args[1]->IsNumber());
+ int32_t id = args[1]->Int32Value(env->context()).ToChecked();
+ DEBUG_HTTP2("Http2Session: refreshing stream %d state\n", id);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>());
+ nghttp2_session* s = session->session();
+ Nghttp2Stream* stream;
+
+ double* const buffer = env->http2_stream_state_buffer();
+
+ if ((stream = session->FindStream(id)) == nullptr) {
+ buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE;
+ buffer[IDX_STREAM_STATE_WEIGHT] =
+ buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
+ buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
+ buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
+ buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0;
+ return;
+ }
+ nghttp2_stream* str =
+ nghttp2_session_find_stream(s, stream->id());
+
+ if (str == nullptr) {
+ buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE;
+ buffer[IDX_STREAM_STATE_WEIGHT] =
+ buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
+ buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
+ buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
+ buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0;
+ } else {
+ buffer[IDX_STREAM_STATE] =
+ nghttp2_stream_get_state(str);
+ buffer[IDX_STREAM_STATE_WEIGHT] =
+ nghttp2_stream_get_weight(str);
+ buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] =
+ nghttp2_stream_get_sum_dependency_weight(str);
+ buffer[IDX_STREAM_STATE_LOCAL_CLOSE] =
+ nghttp2_session_get_stream_local_close(s, id);
+ buffer[IDX_STREAM_STATE_REMOTE_CLOSE] =
+ nghttp2_session_get_stream_remote_close(s, id);
+ buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] =
+ nghttp2_session_get_stream_local_window_size(s, id);
+ }
+}
+
+void Http2Session::New(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args.IsConstructCall());
+
+ int val = args[0]->IntegerValue(env->context()).ToChecked();
+ nghttp2_session_type type = static_cast<nghttp2_session_type>(val);
+ DEBUG_HTTP2("Http2Session: creating a session of type: %d\n", type);
+ new Http2Session(env, args.This(), type);
+}
+
+
+// Capture the stream that this session will use to send and receive data
+void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) {
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ CHECK(args[0]->IsExternal());
+ session->Consume(args[0].As<External>());
+}
+
+void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
+ DEBUG_HTTP2("Http2Session: destroying session\n");
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ session->Unconsume();
+ session->Free();
+}
+
+void Http2Session::SubmitPriority(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Local<Context> context = env->context();
+
+ nghttp2_priority_spec spec;
+ int32_t id = args[0]->Int32Value(context).ToChecked();
+ int32_t parent = args[1]->Int32Value(context).ToChecked();
+ int32_t weight = args[2]->Int32Value(context).ToChecked();
+ bool exclusive = args[3]->BooleanValue(context).ToChecked();
+ bool silent = args[4]->BooleanValue(context).ToChecked();
+ DEBUG_HTTP2("Http2Session: submitting priority for stream %d: "
+ "parent: %d, weight: %d, exclusive: %d, silent: %d\n",
+ id, parent, weight, exclusive, silent);
+ CHECK_GT(id, 0);
+ CHECK_GE(parent, 0);
+ CHECK_GE(weight, 0);
+
+ Nghttp2Stream* stream;
+ if (!(stream = session->FindStream(id))) {
+ // invalid stream
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+ nghttp2_priority_spec_init(&spec, parent, weight, exclusive ? 1 : 0);
+
+ args.GetReturnValue().Set(stream->SubmitPriority(&spec, silent));
+}
+
+void Http2Session::SubmitSettings(const FunctionCallbackInfo<Value>& args) {
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+ Environment* env = session->env();
+
+ uint32_t* const buffer = env->http2_settings_buffer();
+ uint32_t flags = buffer[IDX_SETTINGS_COUNT];
+
+ std::vector<nghttp2_settings_entry> entries;
+ entries.reserve(6);
+
+ if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) ==
+ (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) {
+ DEBUG_HTTP2("Setting header table size: %d\n",
+ buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE,
+ buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) ==
+ (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) {
+ DEBUG_HTTP2("Setting max concurrent streams: %d\n",
+ buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]);
+ entries.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
+ buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) ==
+ (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) {
+ DEBUG_HTTP2("Setting max frame size: %d\n",
+ buffer[IDX_SETTINGS_MAX_FRAME_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_MAX_FRAME_SIZE,
+ buffer[IDX_SETTINGS_MAX_FRAME_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) ==
+ (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) {
+ DEBUG_HTTP2("Setting initial window size: %d\n",
+ buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE,
+ buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) ==
+ (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) {
+ DEBUG_HTTP2("Setting max header list size: %d\n",
+ buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]);
+ entries.push_back({NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE,
+ buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]});
+ }
+
+ if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) ==
+ (1 << IDX_SETTINGS_ENABLE_PUSH)) {
+ DEBUG_HTTP2("Setting enable push: %d\n",
+ buffer[IDX_SETTINGS_ENABLE_PUSH]);
+ entries.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH,
+ buffer[IDX_SETTINGS_ENABLE_PUSH]});
+ }
+
+ if (entries.size() > 0) {
+ args.GetReturnValue().Set(
+ session->Nghttp2Session::SubmitSettings(&entries[0], entries.size()));
+ } else {
+ args.GetReturnValue().Set(
+ session->Nghttp2Session::SubmitSettings(nullptr, 0));
+ }
+}
+
+void Http2Session::SubmitRstStream(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ CHECK(args[0]->IsNumber());
+ CHECK(args[1]->IsNumber());
+
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+ int32_t id = args[0]->Int32Value(context).ToChecked();
+ uint32_t code = args[1]->Uint32Value(context).ToChecked();
+
+ Nghttp2Stream* stream;
+ if (!(stream = session->FindStream(id))) {
+ // invalid stream
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+ DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n",
+ id, code);
+ args.GetReturnValue().Set(stream->SubmitRstStream(code));
+}
+
+void Http2Session::SubmitRequest(const FunctionCallbackInfo<Value>& args) {
+ // args[0] Array of headers
+ // args[1] endStream boolean
+ // args[2] parentStream ID (for priority spec)
+ // args[3] weight (for priority spec)
+ // args[4] exclusive boolean (for priority spec)
+ CHECK(args[0]->IsArray());
+
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Environment* env = session->env();
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+
+ Local<Array> headers = args[0].As<Array>();
+ bool endStream = args[1]->BooleanValue(context).ToChecked();
+ int32_t parent = args[2]->Int32Value(context).ToChecked();
+ int32_t weight = args[3]->Int32Value(context).ToChecked();
+ bool exclusive = args[4]->BooleanValue(context).ToChecked();
+
+ DEBUG_HTTP2("Http2Session: submitting request: headers: %d, end-stream: %d, "
+ "parent: %d, weight: %d, exclusive: %d\n", headers->Length(),
+ endStream, parent, weight, exclusive);
+
+ nghttp2_priority_spec prispec;
+ nghttp2_priority_spec_init(&prispec, parent, weight, exclusive ? 1 : 0);
+
+ Headers list(isolate, context, headers);
+
+ int32_t ret = session->Nghttp2Session::SubmitRequest(&prispec,
+ *list, list.length(),
+ nullptr, endStream);
+ DEBUG_HTTP2("Http2Session: request submitted, response: %d\n", ret);
+ args.GetReturnValue().Set(ret);
+}
+
+void Http2Session::SubmitResponse(const FunctionCallbackInfo<Value>& args) {
+ CHECK(args[0]->IsNumber());
+ CHECK(args[1]->IsArray());
+
+ Http2Session* session;
+ Nghttp2Stream* stream;
+
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Environment* env = session->env();
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+
+ int32_t id = args[0]->Int32Value(context).ToChecked();
+ Local<Array> headers = args[1].As<Array>();
+ bool endStream = args[2]->BooleanValue(context).ToChecked();
+
+ DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, "
+ "end-stream: %d\n", id, headers->Length(), endStream);
+
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+
+ Headers list(isolate, context, headers);
+
+ args.GetReturnValue().Set(
+ stream->SubmitResponse(*list, list.length(), endStream));
+}
+
+void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) {
+ CHECK(args[0]->IsNumber()); // Stream ID
+ CHECK(args[1]->IsNumber()); // File Descriptor
+ CHECK(args[2]->IsArray()); // Headers
+
+ Http2Session* session;
+ Nghttp2Stream* stream;
+
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Environment* env = session->env();
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+
+ int32_t id = args[0]->Int32Value(context).ToChecked();
+ int fd = args[1]->Int32Value(context).ToChecked();
+ Local<Array> headers = args[2].As<Array>();
+
+ DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, "
+ "end-stream: %d\n", fd, id, headers->Length());
+
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+
+ Headers list(isolate, context, headers);
+
+ args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length()));
+}
+
+void Http2Session::SendHeaders(const FunctionCallbackInfo<Value>& args) {
+ CHECK(args[0]->IsNumber());
+ CHECK(args[1]->IsArray());
+
+ Http2Session* session;
+ Nghttp2Stream* stream;
+
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Environment* env = session->env();
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ Local<Array> headers = args[1].As<Array>();
+
+ DEBUG_HTTP2("Http2Session: sending informational headers for stream %d, "
+ "count: %d\n", id, headers->Length());
+
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+
+ Headers list(isolate, context, headers);
+
+ args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length()));
+}
+
+void Http2Session::ShutdownStream(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsNumber());
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Nghttp2Stream* stream;
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ DEBUG_HTTP2("Http2Session: shutting down stream %d\n", id);
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+ stream->Shutdown();
+}
+
+
+void Http2Session::StreamReadStart(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsNumber());
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Nghttp2Stream* stream;
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+ stream->ReadStart();
+}
+
+
+void Http2Session::StreamReadStop(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsNumber());
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ Nghttp2Stream* stream;
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+ stream->ReadStop();
+}
+
+void Http2Session::SendShutdownNotice(
+ const FunctionCallbackInfo<Value>& args) {
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+ session->SubmitShutdownNotice();
+}
+
+void Http2Session::SubmitGoaway(const FunctionCallbackInfo<Value>& args) {
+ Http2Session* session;
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+ uint32_t errorCode = args[0]->Uint32Value(context).ToChecked();
+ int32_t lastStreamID = args[1]->Int32Value(context).ToChecked();
+ Local<Value> opaqueData = args[2];
+
+ uint8_t* data = NULL;
+ size_t length = 0;
+
+ if (opaqueData->BooleanValue(context).ToChecked()) {
+ THROW_AND_RETURN_UNLESS_BUFFER(env, opaqueData);
+ SPREAD_BUFFER_ARG(opaqueData, buf);
+ data = reinterpret_cast<uint8_t*>(buf_data);
+ length = buf_length;
+ }
+
+ DEBUG_HTTP2("Http2Session: initiating immediate shutdown. "
+ "last-stream-id: %d, code: %d, opaque-data: %d\n",
+ lastStreamID, errorCode, length);
+ int status = nghttp2_submit_goaway(session->session(),
+ NGHTTP2_FLAG_NONE,
+ lastStreamID,
+ errorCode,
+ data, length);
+ args.GetReturnValue().Set(status);
+}
+
+void Http2Session::DestroyStream(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Http2Session* session;
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+ CHECK_EQ(args.Length(), 1);
+ CHECK(args[0]->IsNumber());
+ int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+ DEBUG_HTTP2("Http2Session: destroy stream %d\n", id);
+ Nghttp2Stream* stream;
+ if (!(stream = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+ stream->Destroy();
+}
+
+void Http2Session::SubmitPushPromise(const FunctionCallbackInfo<Value>& args) {
+ Http2Session* session;
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ Isolate* isolate = env->isolate();
+ ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+ CHECK(args[0]->IsNumber()); // parent stream ID
+ CHECK(args[1]->IsArray()); // headers array
+
+ Nghttp2Stream* parent;
+ int32_t id = args[0]->Int32Value(context).ToChecked();
+ Local<Array> headers = args[1].As<Array>();
+ bool endStream = args[2]->BooleanValue(context).ToChecked();
+
+ DEBUG_HTTP2("Http2Session: submitting push promise for stream %d: "
+ "end-stream: %d, headers: %d\n", id, endStream,
+ headers->Length());
+
+ if (!(parent = session->FindStream(id))) {
+ return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID);
+ }
+
+ Headers list(isolate, context, headers);
+
+ int32_t ret = parent->SubmitPushPromise(*list, list.length(),
+ nullptr, endStream);
+ DEBUG_HTTP2("Http2Session: push promise submitted, ret: %d\n", ret);
+ args.GetReturnValue().Set(ret);
+}
+
+int Http2Session::DoWrite(WriteWrap* req_wrap,
+ uv_buf_t* bufs,
+ size_t count,
+ uv_stream_t* send_handle) {
+ Environment* env = req_wrap->env();
+ Local<Object> req_wrap_obj = req_wrap->object();
+ Local<Context> context = env->context();
+
+ Nghttp2Stream* stream;
+ {
+ Local<Value> val =
+ req_wrap_obj->Get(context, env->stream_string()).ToLocalChecked();
+ int32_t id = val->Int32Value(context).ToChecked();
+ if (!val->IsNumber() || !(stream = FindStream(id))) {
+ // invalid stream
+ req_wrap->Dispatched();
+ req_wrap->Done(0);
+ return NGHTTP2_ERR_INVALID_STREAM_ID;
+ }
+ }
+
+ nghttp2_stream_write_t* req = new nghttp2_stream_write_t;
+ req->data = req_wrap;
+
+ auto AfterWrite = [](nghttp2_stream_write_t* req, int status) {
+ WriteWrap* wrap = static_cast<WriteWrap*>(req->data);
+ wrap->Done(status);
+ delete req;
+ };
+ req_wrap->Dispatched();
+ stream->Write(req, bufs, count, AfterWrite);
+ return 0;
+}
+
+void Http2Session::AllocateSend(size_t recommended, uv_buf_t* buf) {
+ buf->base = stream_alloc();
+ buf->len = kAllocBufferSize;
+}
+
+void Http2Session::Send(uv_buf_t* buf, size_t length) {
+ if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) {
+ return;
+ }
+ HandleScope scope(env()->isolate());
+
+ auto AfterWrite = [](WriteWrap* req_wrap, int status) {
+ req_wrap->Dispose();
+ };
+ Local<Object> req_wrap_obj =
+ env()->write_wrap_constructor_function()
+ ->NewInstance(env()->context()).ToLocalChecked();
+ WriteWrap* write_req = WriteWrap::New(env(),
+ req_wrap_obj,
+ this,
+ AfterWrite);
+
+ uv_buf_t actual = uv_buf_init(buf->base, length);
+ if (stream_->DoWrite(write_req, &actual, 1, nullptr)) {
+ write_req->Dispose();
+ }
+}
+
+void Http2Session::OnTrailers(Nghttp2Stream* stream,
+ MaybeStackBuffer<nghttp2_nv>* trailers) {
+ DEBUG_HTTP2("Http2Session: prompting for trailers on stream %d\n",
+ stream->id());
+ Local<Context> context = env()->context();
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Context::Scope context_scope(context);
+
+ if (object()->Has(context, env()->ontrailers_string()).FromJust()) {
+ Local<Value> argv[1] = {
+ Integer::New(isolate, stream->id())
+ };
+
+ Local<Value> ret = MakeCallback(env()->ontrailers_string(),
+ arraysize(argv), argv);
+ if (!ret.IsEmpty()) {
+ if (ret->IsArray()) {
+ Local<Array> headers = ret.As<Array>();
+ if (headers->Length() > 0) {
+ trailers->AllocateSufficientStorage(headers->Length());
+ CopyHeaders(isolate, context, trailers, headers);
+ }
+ }
+ }
+ }
+}
+
+void Http2Session::OnHeaders(Nghttp2Stream* stream,
+ nghttp2_header_list* headers,
+ nghttp2_headers_category cat,
+ uint8_t flags) {
+ Local<Context> context = env()->context();
+ Isolate* isolate = env()->isolate();
+ Context::Scope context_scope(context);
+ HandleScope scope(isolate);
+ Local<String> name_str;
+ Local<String> value_str;
+
+ Local<Array> holder = Array::New(isolate);
+ Local<Function> fn = env()->push_values_to_array_function();
+ Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2];
+
+ // The headers are passed in above as a linked list of nghttp2_header_list
+ // structs. The following converts that into a JS array with the structure:
+ // [name1, value1, name2, value2, name3, value3, name3, value4] and so on.
+ // That array is passed up to the JS layer and converted into an Object form
+ // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it
+ // this way for performance reasons (it's faster to generate and pass an
+ // array than it is to generate and pass the object).
+ do {
+ size_t j = 0;
+ while (headers != nullptr && j < arraysize(argv) / 2) {
+ nghttp2_header_list* item = headers;
+ // The header name and value are passed as external one-byte strings
+ name_str = ExternalHeader::New(isolate, item->name);
+ value_str = ExternalHeader::New(isolate, item->value);
+ argv[j * 2] = name_str;
+ argv[j * 2 + 1] = value_str;
+ headers = item->next;
+ j++;
+ }
+ // For performance, we pass name and value pairs to array.protototype.push
+ // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no
+ // more items to push.
+ if (j > 0) {
+ fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked();
+ }
+ } while (headers != nullptr);
+
+ if (object()->Has(context, env()->onheaders_string()).FromJust()) {
+ Local<Value> argv[4] = {
+ Integer::New(isolate, stream->id()),
+ Integer::New(isolate, cat),
+ Integer::New(isolate, flags),
+ holder
+ };
+ MakeCallback(env()->onheaders_string(), arraysize(argv), argv);
+ }
+}
+
+
+void Http2Session::OnStreamClose(int32_t id, uint32_t code) {
+ Isolate* isolate = env()->isolate();
+ Local<Context> context = env()->context();
+ HandleScope scope(isolate);
+ Context::Scope context_scope(context);
+ if (object()->Has(context, env()->onstreamclose_string()).FromJust()) {
+ Local<Value> argv[2] = {
+ Integer::New(isolate, id),
+ Integer::NewFromUnsigned(isolate, code)
+ };
+ MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv);
+ }
+}
+
+void FreeDataChunk(char* data, void* hint) {
+ nghttp2_data_chunk_t* item = reinterpret_cast<nghttp2_data_chunk_t*>(hint);
+ delete[] data;
+ data_chunk_free_list.push(item);
+}
+
+void Http2Session::OnDataChunk(
+ Nghttp2Stream* stream,
+ nghttp2_data_chunk_t* chunk) {
+ Isolate* isolate = env()->isolate();
+ Local<Context> context = env()->context();
+ HandleScope scope(isolate);
+ Local<Object> obj = Object::New(isolate);
+ obj->Set(context,
+ env()->id_string(),
+ Integer::New(isolate, stream->id())).FromJust();
+ ssize_t len = -1;
+ Local<Object> buf;
+ if (chunk != nullptr) {
+ len = chunk->buf.len;
+ buf = Buffer::New(isolate,
+ chunk->buf.base, len,
+ FreeDataChunk,
+ chunk).ToLocalChecked();
+ }
+ EmitData(len, buf, obj);
+}
+
+void Http2Session::OnSettings(bool ack) {
+ Local<Context> context = env()->context();
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Context::Scope context_scope(context);
+ if (object()->Has(context, env()->onsettings_string()).FromJust()) {
+ Local<Value> argv[1] = { Boolean::New(isolate, ack) };
+ MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
+ }
+}
+
+void Http2Session::OnFrameError(int32_t id, uint8_t type, int error_code) {
+ Local<Context> context = env()->context();
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Context::Scope context_scope(context);
+ if (object()->Has(context, env()->onframeerror_string()).FromJust()) {
+ Local<Value> argv[3] = {
+ Integer::New(isolate, id),
+ Integer::New(isolate, type),
+ Integer::New(isolate, error_code)
+ };
+ MakeCallback(env()->onframeerror_string(), arraysize(argv), argv);
+ }
+}
+
+void Http2Session::OnPriority(int32_t stream,
+ int32_t parent,
+ int32_t weight,
+ int8_t exclusive) {
+ Local<Context> context = env()->context();
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Context::Scope context_scope(context);
+ if (object()->Has(context, env()->onpriority_string()).FromJust()) {
+ Local<Value> argv[4] = {
+ Integer::New(isolate, stream),
+ Integer::New(isolate, parent),
+ Integer::New(isolate, weight),
+ Boolean::New(isolate, exclusive)
+ };
+ MakeCallback(env()->onpriority_string(), arraysize(argv), argv);
+ }
+}
+
+void Http2Session::OnGoAway(int32_t lastStreamID,
+ uint32_t errorCode,
+ uint8_t* data,
+ size_t length) {
+ Local<Context> context = env()->context();
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Context::Scope context_scope(context);
+ if (object()->Has(context, env()->ongoawaydata_string()).FromJust()) {
+ Local<Value> argv[3] = {
+ Integer::NewFromUnsigned(isolate, errorCode),
+ Integer::New(isolate, lastStreamID),
+ Undefined(isolate)
+ };
+
+ if (length > 0) {
+ argv[2] = Buffer::Copy(isolate,
+ reinterpret_cast<char*>(data),
+ length).ToLocalChecked();
+ }
+
+ MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
+ }
+}
+
+void Http2Session::OnStreamAllocImpl(size_t suggested_size,
+ uv_buf_t* buf,
+ void* ctx) {
+ Http2Session* session = static_cast<Http2Session*>(ctx);
+ buf->base = session->stream_alloc();
+ buf->len = kAllocBufferSize;
+}
+
+
+void Http2Session::OnStreamReadImpl(ssize_t nread,
+ const uv_buf_t* bufs,
+ uv_handle_type pending,
+ void* ctx) {
+ Http2Session* session = static_cast<Http2Session*>(ctx);
+ if (nread < 0) {
+ uv_buf_t tmp_buf;
+ tmp_buf.base = nullptr;
+ tmp_buf.len = 0;
+ session->prev_read_cb_.fn(nread,
+ &tmp_buf,
+ pending,
+ session->prev_read_cb_.ctx);
+ return;
+ }
+ if (nread > 0) {
+ // Only pass data on if nread > 0
+ uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) };
+ ssize_t ret = session->Write(buf, 1);
+ if (ret < 0) {
+ DEBUG_HTTP2("Http2Session: fatal error receiving data: %d\n", ret);
+ nghttp2_session_terminate_session(session->session(),
+ NGHTTP2_PROTOCOL_ERROR);
+ }
+ }
+}
+
+
+void Http2Session::Consume(Local<External> external) {
+ DEBUG_HTTP2("Http2Session: consuming socket\n");
+ CHECK(prev_alloc_cb_.is_empty());
+ StreamBase* stream = static_cast<StreamBase*>(external->Value());
+ CHECK_NE(stream, nullptr);
+ stream->Consume();
+ stream_ = stream;
+ prev_alloc_cb_ = stream->alloc_cb();
+ prev_read_cb_ = stream->read_cb();
+ stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this });
+ stream->set_read_cb({ Http2Session::OnStreamReadImpl, this });
+}
+
+
+void Http2Session::Unconsume() {
+ DEBUG_HTTP2("Http2Session: unconsuming socket\n");
+ if (prev_alloc_cb_.is_empty())
+ return;
+ stream_->set_alloc_cb(prev_alloc_cb_);
+ stream_->set_read_cb(prev_read_cb_);
+ prev_alloc_cb_.clear();
+ prev_read_cb_.clear();
+ stream_ = nullptr;
+}
+
+
+void Initialize(Local<Object> target,
+ Local<Value> unused,
+ Local<Context> context,
+ void* priv) {
+ Environment* env = Environment::GetCurrent(context);
+ Isolate* isolate = env->isolate();
+ HandleScope scope(isolate);
+
+ // Initialize the buffer used for padding callbacks
+ env->set_http2_padding_buffer(new uint32_t[3]);
+ const size_t http2_padding_buffer_byte_length =
+ sizeof(*env->http2_padding_buffer()) * 3;
+
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(env->isolate(), "paddingArrayBuffer"),
+ ArrayBuffer::New(env->isolate(),
+ env->http2_padding_buffer(),
+ http2_padding_buffer_byte_length))
+ .FromJust();
+
+ // Initialize the buffer used to store the session state
+ env->set_http2_session_state_buffer(
+ new double[IDX_SESSION_STATE_COUNT]);
+
+ const size_t http2_session_state_buffer_byte_length =
+ sizeof(*env->http2_session_state_buffer()) *
+ IDX_SESSION_STATE_COUNT;
+
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(env->isolate(), "sessionStateArrayBuffer"),
+ ArrayBuffer::New(env->isolate(),
+ env->http2_session_state_buffer(),
+ http2_session_state_buffer_byte_length))
+ .FromJust();
+
+ // Initialize the buffer used to store the stream state
+ env->set_http2_stream_state_buffer(
+ new double[IDX_STREAM_STATE_COUNT]);
+
+ const size_t http2_stream_state_buffer_byte_length =
+ sizeof(*env->http2_stream_state_buffer()) *
+ IDX_STREAM_STATE_COUNT;
+
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(env->isolate(), "streamStateArrayBuffer"),
+ ArrayBuffer::New(env->isolate(),
+ env->http2_stream_state_buffer(),
+ http2_stream_state_buffer_byte_length))
+ .FromJust();
+
+ // Initialize the buffer used to store the current settings
+ env->set_http2_settings_buffer(
+ new uint32_t[IDX_SETTINGS_COUNT + 1]);
+
+ const size_t http2_settings_buffer_byte_length =
+ sizeof(*env->http2_settings_buffer()) *
+ (IDX_SETTINGS_COUNT + 1);
+
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(env->isolate(), "settingsArrayBuffer"),
+ ArrayBuffer::New(env->isolate(),
+ env->http2_settings_buffer(),
+ http2_settings_buffer_byte_length))
+ .FromJust();
+
+ // Initialize the buffer used to store the options
+ env->set_http2_options_buffer(
+ new uint32_t[IDX_OPTIONS_FLAGS + 1]);
+
+ const size_t http2_options_buffer_byte_length =
+ sizeof(*env->http2_options_buffer()) *
+ (IDX_OPTIONS_FLAGS + 1);
+
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(env->isolate(), "optionsArrayBuffer"),
+ ArrayBuffer::New(env->isolate(),
+ env->http2_options_buffer(),
+ http2_options_buffer_byte_length))
+ .FromJust();
+
+ // Method to fetch the nghttp2 string description of an nghttp2 error code
+ env->SetMethod(target, "nghttp2ErrorString", HttpErrorString);
+
+ Local<String> http2SessionClassName =
+ String::NewFromUtf8(isolate, "Http2Session",
+ v8::NewStringType::kInternalized).ToLocalChecked();
+
+ Local<FunctionTemplate> session =
+ env->NewFunctionTemplate(Http2Session::New);
+ session->SetClassName(http2SessionClassName);
+ session->InstanceTemplate()->SetInternalFieldCount(1);
+ env->SetProtoMethod(session, "getAsyncId", AsyncWrap::GetAsyncId);
+ env->SetProtoMethod(session, "consume",
+ Http2Session::Consume);
+ env->SetProtoMethod(session, "destroy",
+ Http2Session::Destroy);
+ env->SetProtoMethod(session, "sendHeaders",
+ Http2Session::SendHeaders);
+ env->SetProtoMethod(session, "submitShutdownNotice",
+ Http2Session::SendShutdownNotice);
+ env->SetProtoMethod(session, "submitGoaway",
+ Http2Session::SubmitGoaway);
+ env->SetProtoMethod(session, "submitSettings",
+ Http2Session::SubmitSettings);
+ env->SetProtoMethod(session, "submitPushPromise",
+ Http2Session::SubmitPushPromise);
+ env->SetProtoMethod(session, "submitRstStream",
+ Http2Session::SubmitRstStream);
+ env->SetProtoMethod(session, "submitResponse",
+ Http2Session::SubmitResponse);
+ env->SetProtoMethod(session, "submitFile",
+ Http2Session::SubmitFile);
+ env->SetProtoMethod(session, "submitRequest",
+ Http2Session::SubmitRequest);
+ env->SetProtoMethod(session, "submitPriority",
+ Http2Session::SubmitPriority);
+ env->SetProtoMethod(session, "shutdownStream",
+ Http2Session::ShutdownStream);
+ env->SetProtoMethod(session, "streamReadStart",
+ Http2Session::StreamReadStart);
+ env->SetProtoMethod(session, "streamReadStop",
+ Http2Session::StreamReadStop);
+ env->SetProtoMethod(session, "setNextStreamID",
+ Http2Session::SetNextStreamID);
+ env->SetProtoMethod(session, "destroyStream",
+ Http2Session::DestroyStream);
+ StreamBase::AddMethods<Http2Session>(env, session,
+ StreamBase::kFlagHasWritev |
+ StreamBase::kFlagNoShutdown);
+ target->Set(context,
+ http2SessionClassName,
+ session->GetFunction()).FromJust();
+
+ Local<Object> constants = Object::New(isolate);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SESSION_SERVER);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SESSION_CLIENT);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_IDLE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_OPEN);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_RESERVED_LOCAL);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_RESERVED_REMOTE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_CLOSED);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_NO_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_PROTOCOL_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_INTERNAL_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLOW_CONTROL_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_TIMEOUT);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_CLOSED);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FRAME_SIZE_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_REFUSED_STREAM);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_CANCEL);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_COMPRESSION_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_CONNECT_ERROR);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_ENHANCE_YOUR_CALM);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_INADEQUATE_SECURITY);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_HTTP_1_1_REQUIRED);
+
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_REQUEST);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_RESPONSE);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_PUSH_RESPONSE);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_HEADERS);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NONE);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NO_INDEX);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_DEFERRED);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_NOMEM);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_INVALID_ARGUMENT);
+ NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_CLOSED);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_ERR_FRAME_SIZE_ERROR);
+
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_NONE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_END_STREAM);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_END_HEADERS);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_ACK);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_PADDED);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_PRIORITY);
+
+ NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_HEADER_TABLE_SIZE);
+ NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_ENABLE_PUSH);
+ NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE);
+ NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_MAX_FRAME_SIZE);
+ NODE_DEFINE_CONSTANT(constants, MAX_MAX_FRAME_SIZE);
+ NODE_DEFINE_CONSTANT(constants, MIN_MAX_FRAME_SIZE);
+ NODE_DEFINE_CONSTANT(constants, MAX_INITIAL_WINDOW_SIZE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_DEFAULT_WEIGHT);
+
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_ENABLE_PUSH);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
+ NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
+
+ NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE);
+ NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_MAX);
+ NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_CALLBACK);
+
+#define STRING_CONSTANT(NAME, VALUE) \
+ NODE_DEFINE_STRING_CONSTANT(constants, "HTTP2_HEADER_" # NAME, VALUE);
+HTTP_KNOWN_HEADERS(STRING_CONSTANT)
+#undef STRING_CONSTANT
+
+#define STRING_CONSTANT(NAME, VALUE) \
+ NODE_DEFINE_STRING_CONSTANT(constants, "HTTP2_METHOD_" # NAME, VALUE);
+HTTP_KNOWN_METHODS(STRING_CONSTANT)
+#undef STRING_CONSTANT
+
+#define V(name, _) NODE_DEFINE_CONSTANT(constants, HTTP_STATUS_##name);
+HTTP_STATUS_CODES(V)
+#undef V
+
+ env->SetMethod(target, "refreshLocalSettings",
+ RefreshSettings<nghttp2_session_get_local_settings>);
+ env->SetMethod(target, "refreshRemoteSettings",
+ RefreshSettings<nghttp2_session_get_remote_settings>);
+ env->SetMethod(target, "refreshDefaultSettings", RefreshDefaultSettings);
+ env->SetMethod(target, "refreshSessionState", RefreshSessionState);
+ env->SetMethod(target, "refreshStreamState", RefreshStreamState);
+ env->SetMethod(target, "packSettings", PackSettings);
+
+ target->Set(context,
+ FIXED_ONE_BYTE_STRING(isolate, "constants"),
+ constants).FromJust();
+}
+} // namespace http2
+} // namespace node
+
+NODE_MODULE_CONTEXT_AWARE_BUILTIN(http2, node::http2::Initialize)
diff --git a/src/node_http2.h b/src/node_http2.h
new file mode 100644
index 0000000000..f6ccad2984
--- /dev/null
+++ b/src/node_http2.h
@@ -0,0 +1,572 @@
+#ifndef SRC_NODE_HTTP2_H_
+#define SRC_NODE_HTTP2_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "node_http2_core-inl.h"
+#include "stream_base-inl.h"
+#include "string_bytes.h"
+
+namespace node {
+namespace http2 {
+
+using v8::Array;
+using v8::Context;
+using v8::EscapableHandleScope;
+using v8::Isolate;
+using v8::MaybeLocal;
+
+#define HTTP_KNOWN_METHODS(V) \
+ V(ACL, "ACL") \
+ V(BASELINE_CONTROL, "BASELINE-CONTROL") \
+ V(BIND, "BIND") \
+ V(CHECKIN, "CHECKIN") \
+ V(CHECKOUT, "CHECKOUT") \
+ V(CONNECT, "CONNECT") \
+ V(COPY, "COPY") \
+ V(DELETE, "DELETE") \
+ V(GET, "GET") \
+ V(HEAD, "HEAD") \
+ V(LABEL, "LABEL") \
+ V(LINK, "LINK") \
+ V(LOCK, "LOCK") \
+ V(MERGE, "MERGE") \
+ V(MKACTIVITY, "MKACTIVITY") \
+ V(MKCALENDAR, "MKCALENDAR") \
+ V(MKCOL, "MKCOL") \
+ V(MKREDIRECTREF, "MKREDIRECTREF") \
+ V(MKWORKSPACE, "MKWORKSPACE") \
+ V(MOVE, "MOVE") \
+ V(OPTIONS, "OPTIONS") \
+ V(ORDERPATCH, "ORDERPATCH") \
+ V(PATCH, "PATCH") \
+ V(POST, "POST") \
+ V(PRI, "PRI") \
+ V(PROPFIND, "PROPFIND") \
+ V(PROPPATCH, "PROPPATCH") \
+ V(PUT, "PUT") \
+ V(REBIND, "REBIND") \
+ V(REPORT, "REPORT") \
+ V(SEARCH, "SEARCH") \
+ V(TRACE, "TRACE") \
+ V(UNBIND, "UNBIND") \
+ V(UNCHECKOUT, "UNCHECKOUT") \
+ V(UNLINK, "UNLINK") \
+ V(UNLOCK, "UNLOCK") \
+ V(UPDATE, "UPDATE") \
+ V(UPDATEREDIRECTREF, "UPDATEREDIRECTREF") \
+ V(VERSION_CONTROL, "VERSION-CONTROL")
+
+#define HTTP_KNOWN_HEADERS(V) \
+ V(STATUS, ":status") \
+ V(METHOD, ":method") \
+ V(AUTHORITY, ":authority") \
+ V(SCHEME, ":scheme") \
+ V(PATH, ":path") \
+ V(ACCEPT_CHARSET, "accept-charset") \
+ V(ACCEPT_ENCODING, "accept-encoding") \
+ V(ACCEPT_LANGUAGE, "accept-language") \
+ V(ACCEPT_RANGES, "accept-ranges") \
+ V(ACCEPT, "accept") \
+ V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \
+ V(AGE, "age") \
+ V(ALLOW, "allow") \
+ V(AUTHORIZATION, "authorization") \
+ V(CACHE_CONTROL, "cache-control") \
+ V(CONNECTION, "connection") \
+ V(CONTENT_DISPOSITION, "content-disposition") \
+ V(CONTENT_ENCODING, "content-encoding") \
+ V(CONTENT_LANGUAGE, "content-language") \
+ V(CONTENT_LENGTH, "content-length") \
+ V(CONTENT_LOCATION, "content-location") \
+ V(CONTENT_MD5, "content-md5") \
+ V(CONTENT_RANGE, "content-range") \
+ V(CONTENT_TYPE, "content-type") \
+ V(COOKIE, "cookie") \
+ V(DATE, "date") \
+ V(ETAG, "etag") \
+ V(EXPECT, "expect") \
+ V(EXPIRES, "expires") \
+ V(FROM, "from") \
+ V(HOST, "host") \
+ V(IF_MATCH, "if-match") \
+ V(IF_MODIFIED_SINCE, "if-modified-since") \
+ V(IF_NONE_MATCH, "if-none-match") \
+ V(IF_RANGE, "if-range") \
+ V(IF_UNMODIFIED_SINCE, "if-unmodified-since") \
+ V(LAST_MODIFIED, "last-modified") \
+ V(LINK, "link") \
+ V(LOCATION, "location") \
+ V(MAX_FORWARDS, "max-forwards") \
+ V(PREFER, "prefer") \
+ V(PROXY_AUTHENTICATE, "proxy-authenticate") \
+ V(PROXY_AUTHORIZATION, "proxy-authorization") \
+ V(RANGE, "range") \
+ V(REFERER, "referer") \
+ V(REFRESH, "refresh") \
+ V(RETRY_AFTER, "retry-after") \
+ V(SERVER, "server") \
+ V(SET_COOKIE, "set-cookie") \
+ V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \
+ V(TRANSFER_ENCODING, "transfer-encoding") \
+ V(TE, "te") \
+ V(UPGRADE, "upgrade") \
+ V(USER_AGENT, "user-agent") \
+ V(VARY, "vary") \
+ V(VIA, "via") \
+ V(WWW_AUTHENTICATE, "www-authenticate") \
+ V(HTTP2_SETTINGS, "http2-settings") \
+ V(KEEP_ALIVE, "keep-alive") \
+ V(PROXY_CONNECTION, "proxy-connection")
+
+enum http_known_headers {
+HTTP_KNOWN_HEADER_MIN,
+#define V(name, value) HTTP_HEADER_##name,
+HTTP_KNOWN_HEADERS(V)
+#undef V
+HTTP_KNOWN_HEADER_MAX
+};
+
+#define HTTP_STATUS_CODES(V) \
+ V(CONTINUE, 100) \
+ V(SWITCHING_PROTOCOLS, 101) \
+ V(PROCESSING, 102) \
+ V(OK, 200) \
+ V(CREATED, 201) \
+ V(ACCEPTED, 202) \
+ V(NON_AUTHORITATIVE_INFORMATION, 203) \
+ V(NO_CONTENT, 204) \
+ V(RESET_CONTENT, 205) \
+ V(PARTIAL_CONTENT, 206) \
+ V(MULTI_STATUS, 207) \
+ V(ALREADY_REPORTED, 208) \
+ V(IM_USED, 226) \
+ V(MULTIPLE_CHOICES, 300) \
+ V(MOVED_PERMANENTLY, 301) \
+ V(FOUND, 302) \
+ V(SEE_OTHER, 303) \
+ V(NOT_MODIFIED, 304) \
+ V(USE_PROXY, 305) \
+ V(TEMPORARY_REDIRECT, 307) \
+ V(PERMANENT_REDIRECT, 308) \
+ V(BAD_REQUEST, 400) \
+ V(UNAUTHORIZED, 401) \
+ V(PAYMENT_REQUIRED, 402) \
+ V(FORBIDDEN, 403) \
+ V(NOT_FOUND, 404) \
+ V(METHOD_NOT_ALLOWED, 405) \
+ V(NOT_ACCEPTABLE, 406) \
+ V(PROXY_AUTHENTICATION_REQUIRED, 407) \
+ V(REQUEST_TIMEOUT, 408) \
+ V(CONFLICT, 409) \
+ V(GONE, 410) \
+ V(LENGTH_REQUIRED, 411) \
+ V(PRECONDITION_FAILED, 412) \
+ V(PAYLOAD_TOO_LARGE, 413) \
+ V(URI_TOO_LONG, 414) \
+ V(UNSUPPORTED_MEDIA_TYPE, 415) \
+ V(RANGE_NOT_SATISFIABLE, 416) \
+ V(EXPECTATION_FAILED, 417) \
+ V(TEAPOT, 418) \
+ V(MISDIRECTED_REQUEST, 421) \
+ V(UNPROCESSABLE_ENTITY, 422) \
+ V(LOCKED, 423) \
+ V(FAILED_DEPENDENCY, 424) \
+ V(UNORDERED_COLLECTION, 425) \
+ V(UPGRADE_REQUIRED, 426) \
+ V(PRECONDITION_REQUIRED, 428) \
+ V(TOO_MANY_REQUESTS, 429) \
+ V(REQUEST_HEADER_FIELDS_TOO_LARGE, 431) \
+ V(UNAVAILABLE_FOR_LEGAL_REASONS, 451) \
+ V(INTERNAL_SERVER_ERROR, 500) \
+ V(NOT_IMPLEMENTED, 501) \
+ V(BAD_GATEWAY, 502) \
+ V(SERVICE_UNAVAILABLE, 503) \
+ V(GATEWAY_TIMEOUT, 504) \
+ V(HTTP_VERSION_NOT_SUPPORTED, 505) \
+ V(VARIANT_ALSO_NEGOTIATES, 506) \
+ V(INSUFFICIENT_STORAGE, 507) \
+ V(LOOP_DETECTED, 508) \
+ V(BANDWIDTH_LIMIT_EXCEEDED, 509) \
+ V(NOT_EXTENDED, 510) \
+ V(NETWORK_AUTHENTICATION_REQUIRED, 511)
+
+enum http_status_codes {
+#define V(name, code) HTTP_STATUS_##name = code,
+HTTP_STATUS_CODES(V)
+#undef V
+};
+
+enum padding_strategy_type {
+ // No padding strategy
+ PADDING_STRATEGY_NONE,
+ // Padding will ensure all data frames are maxFrameSize
+ PADDING_STRATEGY_MAX,
+ // Padding will be determined via JS callback
+ PADDING_STRATEGY_CALLBACK
+};
+
+#define NGHTTP2_ERROR_CODES(V) \
+ V(NGHTTP2_ERR_INVALID_ARGUMENT) \
+ V(NGHTTP2_ERR_BUFFER_ERROR) \
+ V(NGHTTP2_ERR_UNSUPPORTED_VERSION) \
+ V(NGHTTP2_ERR_WOULDBLOCK) \
+ V(NGHTTP2_ERR_PROTO) \
+ V(NGHTTP2_ERR_INVALID_FRAME) \
+ V(NGHTTP2_ERR_EOF) \
+ V(NGHTTP2_ERR_DEFERRED) \
+ V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) \
+ V(NGHTTP2_ERR_STREAM_CLOSED) \
+ V(NGHTTP2_ERR_STREAM_CLOSING) \
+ V(NGHTTP2_ERR_STREAM_SHUT_WR) \
+ V(NGHTTP2_ERR_INVALID_STREAM_ID) \
+ V(NGHTTP2_ERR_INVALID_STREAM_STATE) \
+ V(NGHTTP2_ERR_DEFERRED_DATA_EXIST) \
+ V(NGHTTP2_ERR_START_STREAM_NOT_ALLOWED) \
+ V(NGHTTP2_ERR_GOAWAY_ALREADY_SENT) \
+ V(NGHTTP2_ERR_INVALID_HEADER_BLOCK) \
+ V(NGHTTP2_ERR_INVALID_STATE) \
+ V(NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) \
+ V(NGHTTP2_ERR_FRAME_SIZE_ERROR) \
+ V(NGHTTP2_ERR_HEADER_COMP) \
+ V(NGHTTP2_ERR_FLOW_CONTROL) \
+ V(NGHTTP2_ERR_INSUFF_BUFSIZE) \
+ V(NGHTTP2_ERR_PAUSE) \
+ V(NGHTTP2_ERR_TOO_MANY_INFLIGHT_SETTINGS) \
+ V(NGHTTP2_ERR_PUSH_DISABLED) \
+ V(NGHTTP2_ERR_DATA_EXIST) \
+ V(NGHTTP2_ERR_SESSION_CLOSING) \
+ V(NGHTTP2_ERR_HTTP_HEADER) \
+ V(NGHTTP2_ERR_HTTP_MESSAGING) \
+ V(NGHTTP2_ERR_REFUSED_STREAM) \
+ V(NGHTTP2_ERR_INTERNAL) \
+ V(NGHTTP2_ERR_CANCEL) \
+ V(NGHTTP2_ERR_FATAL) \
+ V(NGHTTP2_ERR_NOMEM) \
+ V(NGHTTP2_ERR_CALLBACK_FAILURE) \
+ V(NGHTTP2_ERR_BAD_CLIENT_MAGIC) \
+ V(NGHTTP2_ERR_FLOODED)
+
+const char* nghttp2_errname(int rv) {
+ switch (rv) {
+#define V(code) case code: return #code;
+ NGHTTP2_ERROR_CODES(V)
+#undef V
+ default:
+ return "NGHTTP2_UNKNOWN_ERROR";
+ }
+}
+
+#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
+#define DEFAULT_SETTINGS_ENABLE_PUSH 1
+#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535
+#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384
+#define MAX_MAX_FRAME_SIZE 16777215
+#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE
+#define MAX_INITIAL_WINDOW_SIZE 2147483647
+
+class Http2Options {
+ public:
+ explicit Http2Options(Environment* env);
+
+ ~Http2Options() {
+ nghttp2_option_del(options_);
+ }
+
+ nghttp2_option* operator*() {
+ return options_;
+ }
+
+ void SetPaddingStrategy(uint32_t val) {
+ CHECK_LE(val, PADDING_STRATEGY_CALLBACK);
+ padding_strategy_ = static_cast<padding_strategy_type>(val);
+ }
+
+ void SetMaxDeflateDynamicTableSize(size_t val) {
+ nghttp2_option_set_max_deflate_dynamic_table_size(options_, val);
+ }
+
+ void SetMaxReservedRemoteStreams(uint32_t val) {
+ nghttp2_option_set_max_reserved_remote_streams(options_, val);
+ }
+
+ void SetMaxSendHeaderBlockLength(size_t val) {
+ nghttp2_option_set_max_send_header_block_length(options_, val);
+ }
+
+ void SetPeerMaxConcurrentStreams(uint32_t val) {
+ nghttp2_option_set_peer_max_concurrent_streams(options_, val);
+ }
+
+ padding_strategy_type GetPaddingStrategy() {
+ return padding_strategy_;
+ }
+
+ private:
+ nghttp2_option* options_;
+ padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
+};
+
+static const size_t kAllocBufferSize = 64 * 1024;
+
+////
+typedef uint32_t(*get_setting)(nghttp2_session* session,
+ nghttp2_settings_id id);
+
+class Http2Session : public AsyncWrap,
+ public StreamBase,
+ public Nghttp2Session {
+ public:
+ Http2Session(Environment* env,
+ Local<Object> wrap,
+ nghttp2_session_type type) :
+ AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION),
+ StreamBase(env) {
+ Wrap(object(), this);
+
+ Http2Options opts(env);
+
+ padding_strategy_ = opts.GetPaddingStrategy();
+
+ Init(env->event_loop(), type, *opts);
+ stream_buf_.AllocateSufficientStorage(kAllocBufferSize);
+ }
+
+ ~Http2Session() override {
+ CHECK_EQ(false, persistent().IsEmpty());
+ ClearWrap(object());
+ persistent().Reset();
+ CHECK_EQ(true, persistent().IsEmpty());
+ }
+
+ static void OnStreamAllocImpl(size_t suggested_size,
+ uv_buf_t* buf,
+ void* ctx);
+ static void OnStreamReadImpl(ssize_t nread,
+ const uv_buf_t* bufs,
+ uv_handle_type pending,
+ void* ctx);
+ protected:
+ void OnFreeSession() override;
+
+ ssize_t OnMaxFrameSizePadding(size_t frameLength,
+ size_t maxPayloadLen);
+
+ ssize_t OnCallbackPadding(size_t frame,
+ size_t maxPayloadLen);
+
+ bool HasGetPaddingCallback() override {
+ return padding_strategy_ == PADDING_STRATEGY_MAX ||
+ padding_strategy_ == PADDING_STRATEGY_CALLBACK;
+ }
+
+ ssize_t GetPadding(size_t frameLength, size_t maxPayloadLen) override {
+ if (padding_strategy_ == PADDING_STRATEGY_MAX) {
+ return OnMaxFrameSizePadding(frameLength, maxPayloadLen);
+ }
+
+ CHECK_EQ(padding_strategy_, PADDING_STRATEGY_CALLBACK);
+
+ return OnCallbackPadding(frameLength, maxPayloadLen);
+ }
+
+ void OnHeaders(Nghttp2Stream* stream,
+ nghttp2_header_list* headers,
+ nghttp2_headers_category cat,
+ uint8_t flags) override;
+ void OnStreamClose(int32_t id, uint32_t code) override;
+ void Send(uv_buf_t* bufs, size_t total) override;
+ void OnDataChunk(Nghttp2Stream* stream, nghttp2_data_chunk_t* chunk) override;
+ void OnSettings(bool ack) override;
+ void OnPriority(int32_t stream,
+ int32_t parent,
+ int32_t weight,
+ int8_t exclusive) override;
+ void OnGoAway(int32_t lastStreamID,
+ uint32_t errorCode,
+ uint8_t* data,
+ size_t length) override;
+ void OnFrameError(int32_t id, uint8_t type, int error_code) override;
+ void OnTrailers(Nghttp2Stream* stream,
+ MaybeStackBuffer<nghttp2_nv>* trailers) override;
+ void AllocateSend(size_t recommended, uv_buf_t* buf) override;
+
+ int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count,
+ uv_stream_t* send_handle) override;
+
+ AsyncWrap* GetAsyncWrap() override {
+ return static_cast<AsyncWrap*>(this);
+ }
+
+ void* Cast() override {
+ return reinterpret_cast<void*>(this);
+ }
+
+ // Required for StreamBase
+ bool IsAlive() override {
+ return true;
+ }
+
+ // Required for StreamBase
+ bool IsClosing() override {
+ return false;
+ }
+
+ // Required for StreamBase
+ int ReadStart() override { return 0; }
+
+ // Required for StreamBase
+ int ReadStop() override { return 0; }
+
+ // Required for StreamBase
+ int DoShutdown(ShutdownWrap* req_wrap) override {
+ return 0;
+ }
+
+ public:
+ void Consume(Local<External> external);
+ void Unconsume();
+
+ static void New(const FunctionCallbackInfo<Value>& args);
+ static void Consume(const FunctionCallbackInfo<Value>& args);
+ static void Unconsume(const FunctionCallbackInfo<Value>& args);
+ static void Destroy(const FunctionCallbackInfo<Value>& args);
+ static void SubmitSettings(const FunctionCallbackInfo<Value>& args);
+ static void SubmitRstStream(const FunctionCallbackInfo<Value>& args);
+ static void SubmitResponse(const FunctionCallbackInfo<Value>& args);
+ static void SubmitFile(const FunctionCallbackInfo<Value>& args);
+ static void SubmitRequest(const FunctionCallbackInfo<Value>& args);
+ static void SubmitPushPromise(const FunctionCallbackInfo<Value>& args);
+ static void SubmitPriority(const FunctionCallbackInfo<Value>& args);
+ static void SendHeaders(const FunctionCallbackInfo<Value>& args);
+ static void ShutdownStream(const FunctionCallbackInfo<Value>& args);
+ static void StreamWrite(const FunctionCallbackInfo<Value>& args);
+ static void StreamReadStart(const FunctionCallbackInfo<Value>& args);
+ static void StreamReadStop(const FunctionCallbackInfo<Value>& args);
+ static void SetNextStreamID(const FunctionCallbackInfo<Value>& args);
+ static void SendShutdownNotice(const FunctionCallbackInfo<Value>& args);
+ static void SubmitGoaway(const FunctionCallbackInfo<Value>& args);
+ static void DestroyStream(const FunctionCallbackInfo<Value>& args);
+
+ template <get_setting fn>
+ static void GetSettings(const FunctionCallbackInfo<Value>& args);
+
+ size_t self_size() const override {
+ return sizeof(*this);
+ }
+
+ char* stream_alloc() {
+ return *stream_buf_;
+ }
+
+ private:
+ StreamBase* stream_;
+ StreamResource::Callback<StreamResource::AllocCb> prev_alloc_cb_;
+ StreamResource::Callback<StreamResource::ReadCb> prev_read_cb_;
+ padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
+ MaybeStackBuffer<char, kAllocBufferSize> stream_buf_;
+};
+
+class ExternalHeader :
+ public String::ExternalOneByteStringResource {
+ public:
+ explicit ExternalHeader(nghttp2_rcbuf* buf)
+ : buf_(buf), vec_(nghttp2_rcbuf_get_buf(buf)) {
+ }
+
+ ~ExternalHeader() override {
+ nghttp2_rcbuf_decref(buf_);
+ buf_ = nullptr;
+ }
+
+ const char* data() const override {
+ return const_cast<const char*>(reinterpret_cast<char*>(vec_.base));
+ }
+
+ size_t length() const override {
+ return vec_.len;
+ }
+
+ static Local<String> New(Isolate* isolate, nghttp2_rcbuf* buf) {
+ EscapableHandleScope scope(isolate);
+ nghttp2_vec vec = nghttp2_rcbuf_get_buf(buf);
+ if (vec.len == 0) {
+ nghttp2_rcbuf_decref(buf);
+ return scope.Escape(String::Empty(isolate));
+ }
+
+ ExternalHeader* h_str = new ExternalHeader(buf);
+ MaybeLocal<String> str = String::NewExternalOneByte(isolate, h_str);
+ isolate->AdjustAmountOfExternalAllocatedMemory(vec.len);
+
+ if (str.IsEmpty()) {
+ delete h_str;
+ return scope.Escape(String::Empty(isolate));
+ }
+
+ return scope.Escape(str.ToLocalChecked());
+ }
+
+ private:
+ nghttp2_rcbuf* buf_;
+ nghttp2_vec vec_;
+};
+
+class Headers {
+ public:
+ Headers(Isolate* isolate, Local<Context> context, Local<Array> headers) {
+ headers_.AllocateSufficientStorage(headers->Length());
+ Local<Value> item;
+ Local<Array> header;
+
+ for (size_t n = 0; n < headers->Length(); n++) {
+ item = headers->Get(context, n).ToLocalChecked();
+ CHECK(item->IsArray());
+ header = item.As<Array>();
+ Local<Value> key = header->Get(context, 0).ToLocalChecked();
+ Local<Value> value = header->Get(context, 1).ToLocalChecked();
+ CHECK(key->IsString());
+ CHECK(value->IsString());
+ size_t keylen = StringBytes::StorageSize(isolate, key, ASCII);
+ size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII);
+ headers_[n].flags = NGHTTP2_NV_FLAG_NONE;
+ Local<Value> flag = header->Get(context, 2).ToLocalChecked();
+ if (flag->BooleanValue(context).ToChecked())
+ headers_[n].flags |= NGHTTP2_NV_FLAG_NO_INDEX;
+ uint8_t* buf = Malloc<uint8_t>(keylen + valuelen);
+ headers_[n].name = buf;
+ headers_[n].value = buf + keylen;
+ headers_[n].namelen =
+ StringBytes::Write(isolate,
+ reinterpret_cast<char*>(headers_[n].name),
+ keylen, key, ASCII);
+ headers_[n].valuelen =
+ StringBytes::Write(isolate,
+ reinterpret_cast<char*>(headers_[n].value),
+ valuelen, value, ASCII);
+ }
+ }
+
+ ~Headers() {
+ for (size_t n = 0; n < headers_.length(); n++)
+ free(headers_[n].name);
+ }
+
+ nghttp2_nv* operator*() {
+ return *headers_;
+ }
+
+ size_t length() const {
+ return headers_.length();
+ }
+
+ private:
+ MaybeStackBuffer<nghttp2_nv> headers_;
+};
+
+} // namespace http2
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#endif // SRC_NODE_HTTP2_H_
diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h
new file mode 100644
index 0000000000..49ec63b59b
--- /dev/null
+++ b/src/node_http2_core-inl.h
@@ -0,0 +1,590 @@
+#ifndef SRC_NODE_HTTP2_CORE_INL_H_
+#define SRC_NODE_HTTP2_CORE_INL_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "node_http2_core.h"
+#include "node_internals.h" // arraysize
+#include "freelist.h"
+
+namespace node {
+namespace http2 {
+
+#define FREELIST_MAX 1024
+
+#define LINKED_LIST_ADD(list, item) \
+ do { \
+ if (list ## _tail_ == nullptr) { \
+ list ## _head_ = item; \
+ list ## _tail_ = item; \
+ } else { \
+ list ## _tail_->next = item; \
+ list ## _tail_ = item; \
+ } \
+ } while (0);
+
+extern Freelist<nghttp2_data_chunk_t, FREELIST_MAX>
+ data_chunk_free_list;
+
+extern Freelist<Nghttp2Stream, FREELIST_MAX> stream_free_list;
+
+extern Freelist<nghttp2_header_list, FREELIST_MAX> header_free_list;
+
+extern Freelist<nghttp2_data_chunks_t, FREELIST_MAX>
+ data_chunks_free_list;
+
+// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html
+inline void Nghttp2Session::SubmitShutdownNotice() {
+ DEBUG_HTTP2("Nghttp2Session %d: submitting shutdown notice\n", session_type_);
+ nghttp2_submit_shutdown_notice(session_);
+}
+
+// Sends a SETTINGS frame on the current session
+// Note that this *should* send a SETTINGS frame even if niv == 0 and there
+// are no settings entries to send.
+inline int Nghttp2Session::SubmitSettings(const nghttp2_settings_entry iv[],
+ size_t niv) {
+ DEBUG_HTTP2("Nghttp2Session %d: submitting settings, count: %d\n",
+ session_type_, niv);
+ return nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv);
+}
+
+// Returns the Nghttp2Stream associated with the given id, or nullptr if none
+inline Nghttp2Stream* Nghttp2Session::FindStream(int32_t id) {
+ auto s = streams_.find(id);
+ if (s != streams_.end()) {
+ DEBUG_HTTP2("Nghttp2Session %d: stream %d found\n", session_type_, id);
+ return s->second;
+ } else {
+ DEBUG_HTTP2("Nghttp2Session %d: stream %d not found\n", session_type_, id);
+ return nullptr;
+ }
+}
+
+// Flushes any received queued chunks of data out to the JS layer
+inline void Nghttp2Stream::FlushDataChunks(bool done) {
+ while (data_chunks_head_ != nullptr) {
+ DEBUG_HTTP2("Nghttp2Stream %d: emitting data chunk\n", id_);
+ nghttp2_data_chunk_t* item = data_chunks_head_;
+ data_chunks_head_ = item->next;
+ // item will be passed to the Buffer instance and freed on gc
+ session_->OnDataChunk(this, item);
+ }
+ data_chunks_tail_ = nullptr;
+ if (done)
+ session_->OnDataChunk(this, nullptr);
+}
+
+// Passes all of the the chunks for a data frame out to the JS layer
+// The chunks are collected as the frame is being processed and sent out
+// to the JS side only when the frame is fully processed.
+inline void Nghttp2Session::HandleDataFrame(const nghttp2_frame* frame) {
+ int32_t id = frame->hd.stream_id;
+ DEBUG_HTTP2("Nghttp2Session %d: handling data frame for stream %d\n",
+ session_type_, id);
+ Nghttp2Stream* stream = this->FindStream(id);
+ // If the stream does not exist, something really bad happened
+ CHECK_NE(stream, nullptr);
+ bool done = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) ==
+ NGHTTP2_FLAG_END_STREAM;
+ stream->FlushDataChunks(done);
+}
+
+// Passes all of the collected headers for a HEADERS frame out to the JS layer.
+// The headers are collected as the frame is being processed and sent out
+// to the JS side only when the frame is fully processed.
+inline void Nghttp2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
+ int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ?
+ frame->push_promise.promised_stream_id : frame->hd.stream_id;
+ DEBUG_HTTP2("Nghttp2Session %d: handling headers frame for stream %d\n",
+ session_type_, id);
+ Nghttp2Stream* stream = FindStream(id);
+ // If the stream does not exist, something really bad happened
+ CHECK_NE(stream, nullptr);
+ OnHeaders(stream,
+ stream->headers(),
+ stream->headers_category(),
+ frame->hd.flags);
+ stream->FreeHeaders();
+}
+
+// Notifies the JS layer that a PRIORITY frame has been received
+inline void Nghttp2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
+ nghttp2_priority priority_frame = frame->priority;
+ int32_t id = frame->hd.stream_id;
+ DEBUG_HTTP2("Nghttp2Session %d: handling priority frame for stream %d\n",
+ session_type_, id);
+ // Ignore the priority frame if stream ID is <= 0
+ // This actually should never happen because nghttp2 should treat this as
+ // an error condition that terminates the session.
+ if (id > 0) {
+ nghttp2_priority_spec spec = priority_frame.pri_spec;
+ OnPriority(id, spec.stream_id, spec.weight, spec.exclusive);
+ }
+}
+
+// Notifies the JS layer that a GOAWAY frame has been received
+inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
+ nghttp2_goaway goaway_frame = frame->goaway;
+ DEBUG_HTTP2("Nghttp2Session %d: handling goaway frame\n", session_type_);
+
+ OnGoAway(goaway_frame.last_stream_id,
+ goaway_frame.error_code,
+ goaway_frame.opaque_data,
+ goaway_frame.opaque_data_len);
+}
+
+// Prompts nghttp2 to flush the queue of pending data frames
+inline void Nghttp2Session::SendPendingData() {
+ const uint8_t* data;
+ ssize_t len = 0;
+ size_t ncopy = 0;
+ uv_buf_t buf;
+ AllocateSend(SEND_BUFFER_RECOMMENDED_SIZE, &buf);
+ while (nghttp2_session_want_write(session_)) {
+ len = nghttp2_session_mem_send(session_, &data);
+ CHECK_GE(len, 0); // If this is less than zero, we're out of memory
+ // While len is greater than 0, send a chunk
+ while (len > 0) {
+ ncopy = len;
+ if (ncopy > buf.len)
+ ncopy = buf.len;
+ memcpy(buf.base, data, ncopy);
+ Send(&buf, ncopy);
+ len -= ncopy;
+ CHECK_GE(len, 0); // This should never be less than zero
+ }
+ }
+}
+
+// Initialize the Nghttp2Session handle by creating and
+// assigning the Nghttp2Session instance and associated
+// uv_loop_t.
+inline int Nghttp2Session::Init(uv_loop_t* loop,
+ const nghttp2_session_type type,
+ nghttp2_option* options,
+ nghttp2_mem* mem) {
+ DEBUG_HTTP2("Nghttp2Session %d: initializing session\n", type);
+ loop_ = loop;
+ session_type_ = type;
+ int ret = 0;
+
+ nghttp2_session_callbacks* callbacks
+ = callback_struct_saved[HasGetPaddingCallback() ? 1 : 0].callbacks;
+
+ nghttp2_option* opts;
+ if (options != nullptr) {
+ opts = options;
+ } else {
+ nghttp2_option_new(&opts);
+ }
+
+ switch (type) {
+ case NGHTTP2_SESSION_SERVER:
+ ret = nghttp2_session_server_new3(&session_,
+ callbacks,
+ this,
+ opts,
+ mem);
+ break;
+ case NGHTTP2_SESSION_CLIENT:
+ ret = nghttp2_session_client_new3(&session_,
+ callbacks,
+ this,
+ opts,
+ mem);
+ break;
+ }
+ if (opts != options) {
+ nghttp2_option_del(opts);
+ }
+
+ // For every node::Http2Session instance, there is a uv_prep_t handle
+ // whose callback is triggered on every tick of the event loop. When
+ // run, nghttp2 is prompted to send any queued data it may have stored.
+ uv_prepare_init(loop_, &prep_);
+ uv_prepare_start(&prep_, [](uv_prepare_t* t) {
+ Nghttp2Session* session = ContainerOf(&Nghttp2Session::prep_, t);
+ session->SendPendingData();
+ });
+// uv_unref(reinterpret_cast<uv_handle_t*>(&prep_));
+ return ret;
+}
+
+
+inline int Nghttp2Session::Free() {
+ assert(session_ != nullptr);
+ DEBUG_HTTP2("Nghttp2Session %d: freeing session\n", session_type_);
+ // Stop the loop
+ uv_prepare_stop(&prep_);
+ auto PrepClose = [](uv_handle_t* handle) {
+ Nghttp2Session* session =
+ ContainerOf(&Nghttp2Session::prep_,
+ reinterpret_cast<uv_prepare_t*>(handle));
+
+ session->OnFreeSession();
+ DEBUG_HTTP2("Nghttp2Session %d: session is free\n",
+ session->session_type_);
+ };
+ uv_close(reinterpret_cast<uv_handle_t*>(&prep_), PrepClose);
+
+ nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR);
+ nghttp2_session_del(session_);
+ session_ = nullptr;
+ loop_ = nullptr;
+ return 1;
+}
+
+// Write data received from the socket to the underlying nghttp2_session.
+inline ssize_t Nghttp2Session::Write(const uv_buf_t* bufs, unsigned int nbufs) {
+ size_t total = 0;
+ for (unsigned int n = 0; n < nbufs; n++) {
+ ssize_t ret =
+ nghttp2_session_mem_recv(session_,
+ reinterpret_cast<uint8_t*>(bufs[n].base),
+ bufs[n].len);
+ if (ret < 0) {
+ return ret;
+ } else {
+ total += ret;
+ }
+ }
+ SendPendingData();
+ return total;
+}
+
+inline void Nghttp2Session::AddStream(Nghttp2Stream* stream) {
+ streams_[stream->id()] = stream;
+}
+
+// Removes a stream instance from this session
+inline void Nghttp2Session::RemoveStream(int32_t id) {
+ streams_.erase(id);
+}
+
+// Implementation for Nghttp2Stream functions
+
+inline Nghttp2Stream* Nghttp2Stream::Init(
+ int32_t id,
+ Nghttp2Session* session,
+ nghttp2_headers_category category) {
+ DEBUG_HTTP2("Nghttp2Stream %d: initializing stream\n", id);
+ Nghttp2Stream* stream = stream_free_list.pop();
+ stream->ResetState(id, session, category);
+ session->AddStream(stream);
+ return stream;
+}
+
+
+// Resets the state of the stream instance to defaults
+inline void Nghttp2Stream::ResetState(
+ int32_t id,
+ Nghttp2Session* session,
+ nghttp2_headers_category category) {
+ DEBUG_HTTP2("Nghttp2Stream %d: resetting stream state\n", id);
+ session_ = session;
+ queue_head_ = nullptr;
+ queue_tail_ = nullptr;
+ data_chunks_head_ = nullptr;
+ data_chunks_tail_ = nullptr;
+ current_headers_head_ = nullptr;
+ current_headers_tail_ = nullptr;
+ current_headers_category_ = category;
+ flags_ = NGHTTP2_STREAM_FLAG_NONE;
+ id_ = id;
+ code_ = NGHTTP2_NO_ERROR;
+ prev_local_window_size_ = 65535;
+ queue_head_index_ = 0;
+ queue_head_offset_ = 0;
+}
+
+
+inline void Nghttp2Stream::Destroy() {
+ DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_);
+ // Do nothing if this stream instance is already destroyed
+ if (IsDestroyed() || IsDestroying())
+ return;
+ flags_ |= NGHTTP2_STREAM_DESTROYING;
+ Nghttp2Session* session = this->session_;
+
+ if (session != nullptr) {
+ // Remove this stream from the associated session
+ session_->RemoveStream(this->id());
+ session_ = nullptr;
+ }
+
+ // Free any remaining incoming data chunks.
+ while (data_chunks_head_ != nullptr) {
+ nghttp2_data_chunk_t* chunk = data_chunks_head_;
+ data_chunks_head_ = chunk->next;
+ delete[] chunk->buf.base;
+ data_chunk_free_list.push(chunk);
+ }
+ data_chunks_tail_ = nullptr;
+
+ // Free any remaining outgoing data chunks.
+ while (queue_head_ != nullptr) {
+ nghttp2_stream_write_queue* head = queue_head_;
+ queue_head_ = head->next;
+ head->cb(head->req, UV_ECANCELED);
+ delete head;
+ }
+ queue_tail_ = nullptr;
+
+ // Free any remaining headers
+ FreeHeaders();
+
+ // Return this stream instance to the freelist
+ stream_free_list.push(this);
+}
+
+inline void Nghttp2Stream::FreeHeaders() {
+ DEBUG_HTTP2("Nghttp2Stream %d: freeing headers\n", id_);
+ while (current_headers_head_ != nullptr) {
+ DEBUG_HTTP2("Nghttp2Stream %d: freeing header item\n", id_);
+ nghttp2_header_list* item = current_headers_head_;
+ current_headers_head_ = item->next;
+ header_free_list.push(item);
+ }
+ current_headers_tail_ = nullptr;
+}
+
+// Submit informational headers for a stream.
+inline int Nghttp2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
+ DEBUG_HTTP2("Nghttp2Stream %d: sending informational headers, count: %d\n",
+ id_, len);
+ CHECK_GT(len, 0);
+ return nghttp2_submit_headers(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_, nullptr,
+ nva, len, nullptr);
+}
+
+inline int Nghttp2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
+ bool silent) {
+ DEBUG_HTTP2("Nghttp2Stream %d: sending priority spec\n", id_);
+ return silent ?
+ nghttp2_session_change_stream_priority(session_->session(),
+ id_, prispec) :
+ nghttp2_submit_priority(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_, prispec);
+}
+
+// Submit an RST_STREAM frame
+inline int Nghttp2Stream::SubmitRstStream(const uint32_t code) {
+ DEBUG_HTTP2("Nghttp2Stream %d: sending rst-stream, code: %d\n", id_, code);
+ session_->SendPendingData();
+ return nghttp2_submit_rst_stream(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_,
+ code);
+}
+
+// Submit a push promise.
+inline int32_t Nghttp2Stream::SubmitPushPromise(
+ nghttp2_nv* nva,
+ size_t len,
+ Nghttp2Stream** assigned,
+ bool emptyPayload) {
+ CHECK_GT(len, 0);
+ DEBUG_HTTP2("Nghttp2Stream %d: sending push promise\n", id_);
+ int32_t ret = nghttp2_submit_push_promise(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_, nva, len,
+ nullptr);
+ if (ret > 0) {
+ auto stream = Nghttp2Stream::Init(ret, session_);
+ if (emptyPayload) stream->Shutdown();
+ if (assigned != nullptr) *assigned = stream;
+ }
+ return ret;
+}
+
+// Initiate a response. If the nghttp2_stream is still writable by
+// the time this is called, then an nghttp2_data_provider will be
+// initialized, causing at least one (possibly empty) data frame to
+// be sent.
+inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva,
+ size_t len,
+ bool emptyPayload) {
+ CHECK_GT(len, 0);
+ DEBUG_HTTP2("Nghttp2Stream %d: submitting response\n", id_);
+ nghttp2_data_provider* provider = nullptr;
+ nghttp2_data_provider prov;
+ prov.source.ptr = this;
+ prov.read_callback = Nghttp2Session::OnStreamRead;
+ if (!emptyPayload && IsWritable())
+ provider = &prov;
+
+ return nghttp2_submit_response(session_->session(), id_,
+ nva, len, provider);
+}
+
+// Initiate a response that contains data read from a file descriptor.
+inline int Nghttp2Stream::SubmitFile(int fd, nghttp2_nv* nva, size_t len) {
+ CHECK_GT(len, 0);
+ CHECK_GT(fd, 0);
+ DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_);
+ nghttp2_data_provider prov;
+ prov.source.ptr = this;
+ prov.source.fd = fd;
+ prov.read_callback = Nghttp2Session::OnStreamReadFD;
+
+ return nghttp2_submit_response(session_->session(), id_,
+ nva, len, &prov);
+}
+
+// Initiate a request. If writable is true (the default), then
+// an nghttp2_data_provider will be initialized, causing at
+// least one (possibly empty) data frame to to be sent.
+inline int32_t Nghttp2Session::SubmitRequest(
+ nghttp2_priority_spec* prispec,
+ nghttp2_nv* nva,
+ size_t len,
+ Nghttp2Stream** assigned,
+ bool emptyPayload) {
+ CHECK_GT(len, 0);
+ DEBUG_HTTP2("Nghttp2Session: submitting request\n");
+ nghttp2_data_provider* provider = nullptr;
+ nghttp2_data_provider prov;
+ prov.source.ptr = this;
+ prov.read_callback = OnStreamRead;
+ if (!emptyPayload)
+ provider = &prov;
+ int32_t ret = nghttp2_submit_request(session_,
+ prispec, nva, len,
+ provider, nullptr);
+ // Assign the Nghttp2Stream handle
+ if (ret > 0) {
+ Nghttp2Stream* stream = Nghttp2Stream::Init(ret, this);
+ if (emptyPayload) stream->Shutdown();
+ if (assigned != nullptr) *assigned = stream;
+ }
+ return ret;
+}
+
+// Queue the given set of uv_but_t handles for writing to an
+// nghttp2_stream. The callback will be invoked once the chunks
+// of data have been flushed to the underlying nghttp2_session.
+// Note that this does *not* mean that the data has been flushed
+// to the socket yet.
+inline int Nghttp2Stream::Write(nghttp2_stream_write_t* req,
+ const uv_buf_t bufs[],
+ unsigned int nbufs,
+ nghttp2_stream_write_cb cb) {
+ if (!IsWritable()) {
+ if (cb != nullptr)
+ cb(req, UV_EOF);
+ return 0;
+ }
+ DEBUG_HTTP2("Nghttp2Stream %d: queuing buffers to send, count: %d\n",
+ id_, nbufs);
+ nghttp2_stream_write_queue* item = new nghttp2_stream_write_queue;
+ item->cb = cb;
+ item->req = req;
+ item->nbufs = nbufs;
+ item->bufs.AllocateSufficientStorage(nbufs);
+ req->handle = this;
+ req->item = item;
+ memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs));
+
+ if (queue_head_ == nullptr) {
+ queue_head_ = item;
+ queue_tail_ = item;
+ } else {
+ queue_tail_->next = item;
+ queue_tail_ = item;
+ }
+ nghttp2_session_resume_data(session_->session(), id_);
+ return 0;
+}
+
+inline void Nghttp2Stream::ReadStart() {
+ // Has no effect if IsReading() is true.
+ if (IsReading())
+ return;
+ DEBUG_HTTP2("Nghttp2Stream %d: start reading\n", id_);
+ if (IsPaused()) {
+ // If handle->reading is less than zero, read_start had never previously
+ // been called. If handle->reading is zero, reading had started and read
+ // stop had been previously called, meaning that the flow control window
+ // has been explicitly set to zero. Reset the flow control window now to
+ // restart the flow of data.
+ nghttp2_session_set_local_window_size(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_,
+ prev_local_window_size_);
+ }
+ flags_ |= NGHTTP2_STREAM_READ_START;
+ flags_ &= ~NGHTTP2_STREAM_READ_PAUSED;
+
+ // Flush any queued data chunks immediately out to the JS layer
+ FlushDataChunks();
+}
+
+inline void Nghttp2Stream::ReadStop() {
+ DEBUG_HTTP2("Nghttp2Stream %d: stop reading\n", id_);
+ // Has no effect if IsReading() is false, which will happen if we either
+ // have not started reading yet at all (NGHTTP2_STREAM_READ_START is not
+ // set) or if we're already paused (NGHTTP2_STREAM_READ_PAUSED is set.
+ if (!IsReading())
+ return;
+ flags_ |= NGHTTP2_STREAM_READ_PAUSED;
+
+ // When not reading, explicitly set the local window size to 0 so that
+ // the peer does not keep sending data that has to be buffered
+ int32_t ret =
+ nghttp2_session_get_stream_local_window_size(session_->session(), id_);
+ if (ret >= 0)
+ prev_local_window_size_ = ret;
+ nghttp2_session_set_local_window_size(session_->session(),
+ NGHTTP2_FLAG_NONE,
+ id_, 0);
+}
+
+nghttp2_data_chunks_t::~nghttp2_data_chunks_t() {
+ for (unsigned int n = 0; n < nbufs; n++) {
+ free(buf[n].base);
+ }
+}
+
+Nghttp2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
+ nghttp2_session_callbacks_new(&callbacks);
+ nghttp2_session_callbacks_set_on_begin_headers_callback(
+ callbacks, OnBeginHeadersCallback);
+ nghttp2_session_callbacks_set_on_header_callback2(
+ callbacks, OnHeaderCallback);
+ nghttp2_session_callbacks_set_on_frame_recv_callback(
+ callbacks, OnFrameReceive);
+ nghttp2_session_callbacks_set_on_stream_close_callback(
+ callbacks, OnStreamClose);
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
+ callbacks, OnDataChunkReceived);
+ nghttp2_session_callbacks_set_on_frame_not_send_callback(
+ callbacks, OnFrameNotSent);
+
+ // nghttp2_session_callbacks_set_on_invalid_frame_recv(
+ // callbacks, OnInvalidFrameReceived);
+
+#ifdef NODE_DEBUG_HTTP2
+ nghttp2_session_callbacks_set_error_callback(
+ callbacks, OnNghttpError);
+#endif
+
+ if (kHasGetPaddingCallback) {
+ nghttp2_session_callbacks_set_select_padding_callback(
+ callbacks, OnSelectPadding);
+ }
+}
+
+Nghttp2Session::Callbacks::~Callbacks() {
+ nghttp2_session_callbacks_del(callbacks);
+}
+
+} // namespace http2
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#endif // SRC_NODE_HTTP2_CORE_INL_H_
diff --git a/src/node_http2_core.cc b/src/node_http2_core.cc
new file mode 100644
index 0000000000..4d9ab4a4df
--- /dev/null
+++ b/src/node_http2_core.cc
@@ -0,0 +1,326 @@
+#include "node_http2_core-inl.h"
+
+namespace node {
+namespace http2 {
+
+#ifdef NODE_DEBUG_HTTP2
+int Nghttp2Session::OnNghttpError(nghttp2_session* session,
+ const char* message,
+ size_t len,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: Error '%.*s'\n",
+ handle->session_type_, len, message);
+ return 0;
+}
+#endif
+
+// nghttp2 calls this at the beginning a new HEADERS or PUSH_PROMISE frame.
+// We use it to ensure that an Nghttp2Stream instance is allocated to store
+// the state.
+int Nghttp2Session::OnBeginHeadersCallback(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ?
+ frame->push_promise.promised_stream_id :
+ frame->hd.stream_id;
+ DEBUG_HTTP2("Nghttp2Session %d: beginning headers for stream %d\n",
+ handle->session_type_, id);
+
+ Nghttp2Stream* stream = handle->FindStream(id);
+ if (stream == nullptr) {
+ Nghttp2Stream::Init(id, handle, frame->headers.cat);
+ } else {
+ stream->StartHeaders(frame->headers.cat);
+ }
+ return 0;
+}
+
+// nghttp2 calls this once for every header name-value pair in a HEADERS
+// or PUSH_PROMISE block. CONTINUATION frames are handled automatically
+// and transparently so we do not need to worry about those at all.
+int Nghttp2Session::OnHeaderCallback(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ nghttp2_rcbuf *name,
+ nghttp2_rcbuf *value,
+ uint8_t flags,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ?
+ frame->push_promise.promised_stream_id :
+ frame->hd.stream_id;
+ Nghttp2Stream* stream = handle->FindStream(id);
+ nghttp2_header_list* header = header_free_list.pop();
+ header->name = name;
+ header->value = value;
+ nghttp2_rcbuf_incref(name);
+ nghttp2_rcbuf_incref(value);
+ LINKED_LIST_ADD(stream->current_headers, header);
+ return 0;
+}
+
+// When nghttp2 has completely processed a frame, it calls OnFrameReceive.
+// It is our responsibility to delegate out from there. We can ignore most
+// control frames since nghttp2 will handle those for us.
+int Nghttp2Session::OnFrameReceive(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: complete frame received: type: %d\n",
+ handle->session_type_, frame->hd.type);
+ bool ack;
+ switch (frame->hd.type) {
+ case NGHTTP2_DATA:
+ handle->HandleDataFrame(frame);
+ break;
+ case NGHTTP2_PUSH_PROMISE:
+ case NGHTTP2_HEADERS:
+ handle->HandleHeadersFrame(frame);
+ break;
+ case NGHTTP2_SETTINGS:
+ ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) == NGHTTP2_FLAG_ACK;
+ handle->OnSettings(ack);
+ break;
+ case NGHTTP2_PRIORITY:
+ handle->HandlePriorityFrame(frame);
+ break;
+ case NGHTTP2_GOAWAY:
+ handle->HandleGoawayFrame(frame);
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+int Nghttp2Session::OnFrameNotSent(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ int error_code,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: frame type %d was not sent, code: %d\n",
+ handle->session_type_, frame->hd.type, error_code);
+ // Do not report if the frame was not sent due to the session closing
+ if (error_code != NGHTTP2_ERR_SESSION_CLOSING &&
+ error_code != NGHTTP2_ERR_STREAM_CLOSED &&
+ error_code != NGHTTP2_ERR_STREAM_CLOSING)
+ handle->OnFrameError(frame->hd.stream_id, frame->hd.type, error_code);
+ return 0;
+}
+
+// Called when nghttp2 closes a stream, either in response to an RST_STREAM
+// frame or the stream closing naturally on it's own
+int Nghttp2Session::OnStreamClose(nghttp2_session *session,
+ int32_t id,
+ uint32_t code,
+ void *user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: stream %d closed, code: %d\n",
+ handle->session_type_, id, code);
+ Nghttp2Stream* stream = handle->FindStream(id);
+ // Intentionally ignore the callback if the stream does not exist
+ if (stream != nullptr)
+ stream->Close(code);
+ return 0;
+}
+
+// Called by nghttp2 multiple times while processing a DATA frame
+int Nghttp2Session::OnDataChunkReceived(nghttp2_session *session,
+ uint8_t flags,
+ int32_t id,
+ const uint8_t *data,
+ size_t len,
+ void *user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: buffering data chunk for stream %d, size: "
+ "%d, flags: %d\n", handle->session_type_, id, len, flags);
+ Nghttp2Stream* stream = handle->FindStream(id);
+ nghttp2_data_chunk_t* chunk = data_chunk_free_list.pop();
+ chunk->buf = uv_buf_init(new char[len], len);
+ memcpy(chunk->buf.base, data, len);
+ if (stream->data_chunks_tail_ == nullptr) {
+ stream->data_chunks_head_ =
+ stream->data_chunks_tail_ = chunk;
+ } else {
+ stream->data_chunks_tail_->next = chunk;
+ stream->data_chunks_tail_ = chunk;
+ }
+ return 0;
+}
+
+// Called by nghttp2 when it needs to determine how much padding to apply
+// to a DATA or HEADERS frame
+ssize_t Nghttp2Session::OnSelectPadding(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ size_t maxPayloadLen,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ assert(handle->HasGetPaddingCallback());
+ ssize_t padding = handle->GetPadding(frame->hd.length, maxPayloadLen);
+ DEBUG_HTTP2("Nghttp2Session %d: using padding, size: %d\n",
+ handle->session_type_, padding);
+ return padding;
+}
+
+// Called by nghttp2 to collect the data while a file response is sent.
+// The buf is the DATA frame buffer that needs to be filled with at most
+// length bytes. flags is used to control what nghttp2 does next.
+ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: reading outbound file data for stream %d\n",
+ handle->session_type_, id);
+ Nghttp2Stream* stream = handle->FindStream(id);
+
+ int fd = source->fd;
+ int64_t offset = stream->fd_offset_;
+ ssize_t numchars;
+
+ uv_buf_t data;
+ data.base = reinterpret_cast<char*>(buf);
+ data.len = length;
+
+ uv_fs_t read_req;
+ numchars = uv_fs_read(handle->loop_,
+ &read_req,
+ fd, &data, 1,
+ offset, nullptr);
+ uv_fs_req_cleanup(&read_req);
+
+ // Close the stream with an error if reading fails
+ if (numchars < 0)
+ return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+
+ // Update the read offset for the next read
+ stream->fd_offset_ += numchars;
+
+ // if numchars < length, assume that we are done.
+ if (static_cast<size_t>(numchars) < length) {
+ DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n",
+ handle->session_type_, id);
+ *flags |= NGHTTP2_DATA_FLAG_EOF;
+ // Sending trailers is not permitted with this provider.
+ }
+
+ return numchars;
+}
+
+// Called by nghttp2 to collect the data to pack within a DATA frame.
+// The buf is the DATA frame buffer that needs to be filled with at most
+// length bytes. flags is used to control what nghttp2 does next.
+ssize_t Nghttp2Session::OnStreamRead(nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data) {
+ Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data);
+ DEBUG_HTTP2("Nghttp2Session %d: reading outbound data for stream %d\n",
+ handle->session_type_, id);
+ Nghttp2Stream* stream = handle->FindStream(id);
+ size_t remaining = length;
+ size_t offset = 0;
+
+ // While there is data in the queue, copy data into buf until it is full.
+ // There may be data left over, which will be sent the next time nghttp
+ // calls this callback.
+ while (stream->queue_head_ != nullptr) {
+ DEBUG_HTTP2("Nghttp2Session %d: processing outbound data chunk\n",
+ handle->session_type_);
+ nghttp2_stream_write_queue* head = stream->queue_head_;
+ while (stream->queue_head_index_ < head->nbufs) {
+ if (remaining == 0) {
+ goto end;
+ }
+
+ unsigned int n = stream->queue_head_index_;
+ // len is the number of bytes in head->bufs[n] that are yet to be written
+ size_t len = head->bufs[n].len - stream->queue_head_offset_;
+ size_t bytes_to_write = len < remaining ? len : remaining;
+ memcpy(buf + offset,
+ head->bufs[n].base + stream->queue_head_offset_,
+ bytes_to_write);
+ offset += bytes_to_write;
+ remaining -= bytes_to_write;
+ if (bytes_to_write < len) {
+ stream->queue_head_offset_ += bytes_to_write;
+ } else {
+ stream->queue_head_index_++;
+ stream->queue_head_offset_ = 0;
+ }
+ }
+ stream->queue_head_offset_ = 0;
+ stream->queue_head_index_ = 0;
+ stream->queue_head_ = head->next;
+ head->cb(head->req, 0);
+ delete head;
+ }
+ stream->queue_tail_ = nullptr;
+
+ end:
+ // If we are no longer writable and there is no more data in the queue,
+ // then we need to set the NGHTTP2_DATA_FLAG_EOF flag.
+ // If we are still writable but there is not yet any data to send, set the
+ // NGHTTP2_ERR_DEFERRED flag. This will put the stream into a pending state
+ // that will wait for data to become available.
+ // If neither of these flags are set, then nghttp2 will call this callback
+ // again to get the data for the next DATA frame.
+ int writable = stream->queue_head_ != nullptr || stream->IsWritable();
+ if (offset == 0 && writable && stream->queue_head_ == nullptr) {
+ DEBUG_HTTP2("Nghttp2Session %d: deferring stream %d\n",
+ handle->session_type_, id);
+ return NGHTTP2_ERR_DEFERRED;
+ }
+ if (!writable) {
+ DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n",
+ handle->session_type_, id);
+ *flags |= NGHTTP2_DATA_FLAG_EOF;
+
+ // Only when we are done sending the last chunk of data do we check for
+ // any trailing headers that are to be sent. This is the only opportunity
+ // we have to make this check. If there are trailers, then the
+ // NGHTTP2_DATA_FLAG_NO_END_STREAM flag must be set.
+ MaybeStackBuffer<nghttp2_nv> trailers;
+ handle->OnTrailers(stream, &trailers);
+ if (trailers.length() > 0) {
+ DEBUG_HTTP2("Nghttp2Session %d: sending trailers for stream %d, "
+ "count: %d\n", handle->session_type_, id, trailers.length());
+ *flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
+ nghttp2_submit_trailer(session,
+ stream->id(),
+ *trailers,
+ trailers.length());
+ }
+ for (size_t n = 0; n < trailers.length(); n++) {
+ free(trailers[n].name);
+ free(trailers[n].value);
+ }
+ }
+ assert(offset <= length);
+ return offset;
+}
+
+Freelist<nghttp2_data_chunk_t, FREELIST_MAX>
+ data_chunk_free_list;
+
+Freelist<Nghttp2Stream, FREELIST_MAX> stream_free_list;
+
+Freelist<nghttp2_header_list, FREELIST_MAX> header_free_list;
+
+Freelist<nghttp2_data_chunks_t, FREELIST_MAX>
+ data_chunks_free_list;
+
+Nghttp2Session::Callbacks Nghttp2Session::callback_struct_saved[2] = {
+ Callbacks(false),
+ Callbacks(true)
+};
+
+} // namespace http2
+} // namespace node
diff --git a/src/node_http2_core.h b/src/node_http2_core.h
new file mode 100644
index 0000000000..10acd7736b
--- /dev/null
+++ b/src/node_http2_core.h
@@ -0,0 +1,465 @@
+#ifndef SRC_NODE_HTTP2_CORE_H_
+#define SRC_NODE_HTTP2_CORE_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "util.h"
+#include "util-inl.h"
+#include "uv.h"
+#include "nghttp2/nghttp2.h"
+
+#include <stdio.h>
+#include <unordered_map>
+
+namespace node {
+namespace http2 {
+
+#ifdef NODE_DEBUG_HTTP2
+
+// Adapted from nghttp2 own debug printer
+static inline void _debug_vfprintf(const char *fmt, va_list args) {
+ vfprintf(stderr, fmt, args);
+}
+
+void inline debug_vfprintf(const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ _debug_vfprintf(format, args);
+ va_end(args);
+}
+
+#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__);
+#else
+#define DEBUG_HTTP2(...) \
+ do { \
+ } while (0)
+#endif
+
+class Nghttp2Session;
+class Nghttp2Stream;
+
+struct nghttp2_stream_write_t;
+struct nghttp2_data_chunk_t;
+struct nghttp2_data_chunks_t;
+
+#define MAX_BUFFER_COUNT 10
+#define SEND_BUFFER_RECOMMENDED_SIZE 4096
+
+enum nghttp2_session_type {
+ NGHTTP2_SESSION_SERVER,
+ NGHTTP2_SESSION_CLIENT
+};
+
+enum nghttp2_shutdown_flags {
+ NGHTTP2_SHUTDOWN_FLAG_GRACEFUL
+};
+
+enum nghttp2_stream_flags {
+ NGHTTP2_STREAM_FLAG_NONE = 0x0,
+ // Writable side has ended
+ NGHTTP2_STREAM_FLAG_SHUT = 0x1,
+ // Reading has started
+ NGHTTP2_STREAM_READ_START = 0x2,
+ // Reading is paused
+ NGHTTP2_STREAM_READ_PAUSED = 0x4,
+ // Stream is closed
+ NGHTTP2_STREAM_CLOSED = 0x8,
+ // Stream is destroyed
+ NGHTTP2_STREAM_DESTROYED = 0x10,
+ // Stream is being destroyed
+ NGHTTP2_STREAM_DESTROYING = 0x20
+};
+
+
+// Callbacks
+typedef void (*nghttp2_stream_write_cb)(
+ nghttp2_stream_write_t* req,
+ int status);
+
+struct nghttp2_stream_write_queue {
+ unsigned int nbufs = 0;
+ nghttp2_stream_write_t* req = nullptr;
+ nghttp2_stream_write_cb cb = nullptr;
+ nghttp2_stream_write_queue* next = nullptr;
+ MaybeStackBuffer<uv_buf_t, MAX_BUFFER_COUNT> bufs;
+};
+
+struct nghttp2_header_list {
+ nghttp2_rcbuf* name = nullptr;
+ nghttp2_rcbuf* value = nullptr;
+ nghttp2_header_list* next = nullptr;
+};
+
+// Handle Types
+class Nghttp2Session {
+ public:
+ // Initializes the session instance
+ inline int Init(
+ uv_loop_t*,
+ const nghttp2_session_type type = NGHTTP2_SESSION_SERVER,
+ nghttp2_option* options = nullptr,
+ nghttp2_mem* mem = nullptr);
+
+ // Frees this session instance
+ inline int Free();
+
+ // Returns the pointer to the identified stream, or nullptr if
+ // the stream does not exist
+ inline Nghttp2Stream* FindStream(int32_t id);
+
+ // Submits a new request. If the request is a success, assigned
+ // will be a pointer to the Nghttp2Stream instance assigned.
+ // This only works if the session is a client session.
+ inline int32_t SubmitRequest(
+ nghttp2_priority_spec* prispec,
+ nghttp2_nv* nva,
+ size_t len,
+ Nghttp2Stream** assigned = nullptr,
+ bool emptyPayload = true);
+
+ // Submits a notice to the connected peer that the session is in the
+ // process of shutting down.
+ inline void SubmitShutdownNotice();
+
+ // Submits a SETTINGS frame to the connected peer.
+ inline int SubmitSettings(const nghttp2_settings_entry iv[], size_t niv);
+
+ // Write data to the session
+ inline ssize_t Write(const uv_buf_t* bufs, unsigned int nbufs);
+
+ // Returns the nghttp2 library session
+ inline nghttp2_session* session() { return session_; }
+
+ protected:
+ // Adds a stream instance to this session
+ inline void AddStream(Nghttp2Stream* stream);
+
+ // Removes a stream instance from this session
+ inline void RemoveStream(int32_t id);
+
+ virtual void Send(uv_buf_t* buf,
+ size_t length) {}
+ virtual void OnHeaders(Nghttp2Stream* stream,
+ nghttp2_header_list* headers,
+ nghttp2_headers_category cat,
+ uint8_t flags) {}
+ virtual void OnStreamClose(int32_t id, uint32_t code) {}
+ virtual void OnDataChunk(Nghttp2Stream* stream,
+ nghttp2_data_chunk_t* chunk) {}
+ virtual void OnSettings(bool ack) {}
+ virtual void OnPriority(int32_t id,
+ int32_t parent,
+ int32_t weight,
+ int8_t exclusive) {}
+ virtual void OnGoAway(int32_t lastStreamID,
+ uint32_t errorCode,
+ uint8_t* data,
+ size_t length) {}
+ virtual void OnFrameError(int32_t id,
+ uint8_t type,
+ int error_code) {}
+ virtual ssize_t GetPadding(size_t frameLength,
+ size_t maxFrameLength) { return 0; }
+ virtual void OnTrailers(Nghttp2Stream* stream,
+ MaybeStackBuffer<nghttp2_nv>* nva) {}
+ virtual void OnFreeSession() {}
+ virtual void AllocateSend(size_t suggested_size, uv_buf_t* buf) = 0;
+
+ virtual bool HasGetPaddingCallback() { return false; }
+
+ private:
+ inline void SendPendingData();
+ inline void HandleHeadersFrame(const nghttp2_frame* frame);
+ inline void HandlePriorityFrame(const nghttp2_frame* frame);
+ inline void HandleDataFrame(const nghttp2_frame* frame);
+ inline void HandleGoawayFrame(const nghttp2_frame* frame);
+
+ /* callbacks for nghttp2 */
+#ifdef NODE_DEBUG_HTTP2
+ static int OnNghttpError(nghttp2_session* session,
+ const char* message,
+ size_t len,
+ void* user_data);
+#endif
+
+ static int OnBeginHeadersCallback(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ void* user_data);
+ static int OnHeaderCallback(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ nghttp2_rcbuf* name,
+ nghttp2_rcbuf* value,
+ uint8_t flags,
+ void* user_data);
+ static int OnFrameReceive(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ void* user_data);
+ static int OnFrameNotSent(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ int error_code,
+ void* user_data);
+ static int OnStreamClose(nghttp2_session* session,
+ int32_t id,
+ uint32_t code,
+ void* user_data);
+ static int OnDataChunkReceived(nghttp2_session* session,
+ uint8_t flags,
+ int32_t id,
+ const uint8_t *data,
+ size_t len,
+ void* user_data);
+ static ssize_t OnStreamReadFD(nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data);
+ static ssize_t OnStreamRead(nghttp2_session* session,
+ int32_t id,
+ uint8_t* buf,
+ size_t length,
+ uint32_t* flags,
+ nghttp2_data_source* source,
+ void* user_data);
+ static ssize_t OnSelectPadding(nghttp2_session* session,
+ const nghttp2_frame* frame,
+ size_t maxPayloadLen,
+ void* user_data);
+
+ struct Callbacks {
+ inline explicit Callbacks(bool kHasGetPaddingCallback);
+ inline ~Callbacks();
+
+ nghttp2_session_callbacks* callbacks;
+ };
+
+ /* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */
+ static Callbacks callback_struct_saved[2];
+
+ nghttp2_session* session_;
+ uv_loop_t* loop_;
+ uv_prepare_t prep_;
+ nghttp2_session_type session_type_;
+ std::unordered_map<int32_t, Nghttp2Stream*> streams_;
+
+ friend class Nghttp2Stream;
+};
+
+
+
+class Nghttp2Stream {
+ public:
+ static inline Nghttp2Stream* Init(
+ int32_t id,
+ Nghttp2Session* session,
+ nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS);
+
+ inline ~Nghttp2Stream() {
+ CHECK_EQ(session_, nullptr);
+ CHECK_EQ(queue_head_, nullptr);
+ CHECK_EQ(queue_tail_, nullptr);
+ CHECK_EQ(data_chunks_head_, nullptr);
+ CHECK_EQ(data_chunks_tail_, nullptr);
+ CHECK_EQ(current_headers_head_, nullptr);
+ CHECK_EQ(current_headers_tail_, nullptr);
+ DEBUG_HTTP2("Nghttp2Stream %d: freed\n", id_);
+ }
+
+ inline void FlushDataChunks(bool done = false);
+
+ // Resets the state of the stream instance to defaults
+ inline void ResetState(
+ int32_t id,
+ Nghttp2Session* session,
+ nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS);
+
+ // Destroy this stream instance and free all held memory.
+ // Note that this will free queued outbound and inbound
+ // data chunks and inbound headers, so it's important not
+ // to call this until those are fully consumed.
+ //
+ // Also note: this does not actually destroy the instance.
+ // instead, it frees the held memory, removes the stream
+ // from the parent session, and returns the instance to
+ // the FreeList so that it can be reused.
+ inline void Destroy();
+
+ // Returns true if this stream has been destroyed
+ inline bool IsDestroyed() const {
+ return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED;
+ }
+
+ inline bool IsDestroying() const {
+ return (flags_ & NGHTTP2_STREAM_DESTROYING) == NGHTTP2_STREAM_DESTROYING;
+ }
+
+ // Queue outbound chunks of data to be sent on this stream
+ inline int Write(
+ nghttp2_stream_write_t* req,
+ const uv_buf_t bufs[],
+ unsigned int nbufs,
+ nghttp2_stream_write_cb cb);
+
+ // Initiate a response on this stream.
+ inline int SubmitResponse(nghttp2_nv* nva,
+ size_t len,
+ bool emptyPayload = false);
+
+ // Send data read from a file descriptor as the response on this stream.
+ inline int SubmitFile(int fd, nghttp2_nv* nva, size_t len);
+
+ // Submit informational headers for this stream
+ inline int SubmitInfo(nghttp2_nv* nva, size_t len);
+
+ // Submit a PRIORITY frame for this stream
+ inline int SubmitPriority(nghttp2_priority_spec* prispec,
+ bool silent = false);
+
+ // Submits an RST_STREAM frame using the given code
+ inline int SubmitRstStream(const uint32_t code);
+
+ // Submits a PUSH_PROMISE frame with this stream as the parent.
+ inline int SubmitPushPromise(
+ nghttp2_nv* nva,
+ size_t len,
+ Nghttp2Stream** assigned = nullptr,
+ bool writable = true);
+
+ // Marks the Writable side of the stream as being shutdown
+ inline void Shutdown() {
+ flags_ |= NGHTTP2_STREAM_FLAG_SHUT;
+ nghttp2_session_resume_data(session_->session(), id_);
+ }
+
+ // Returns true if this stream is writable.
+ inline bool IsWritable() const {
+ return (flags_ & NGHTTP2_STREAM_FLAG_SHUT) == 0;
+ }
+
+ // Start Reading. If there are queued data chunks, they are pushed into
+ // the session to be emitted at the JS side
+ inline void ReadStart();
+
+ // Stop/Pause Reading.
+ inline void ReadStop();
+
+ // Returns true if reading is paused
+ inline bool IsPaused() const {
+ return (flags_ & NGHTTP2_STREAM_READ_PAUSED) == NGHTTP2_STREAM_READ_PAUSED;
+ }
+
+ // Returns true if this stream is in the reading state, which occurs when
+ // the NGHTTP2_STREAM_READ_START flag has been set and the
+ // NGHTTP2_STREAM_READ_PAUSED flag is *not* set.
+ inline bool IsReading() const {
+ return ((flags_ & NGHTTP2_STREAM_READ_START) == NGHTTP2_STREAM_READ_START)
+ && ((flags_ & NGHTTP2_STREAM_READ_PAUSED) == 0);
+ }
+
+ inline void Close(int32_t code) {
+ DEBUG_HTTP2("Nghttp2Stream %d: closing with code %d\n", id_, code);
+ flags_ |= NGHTTP2_STREAM_CLOSED;
+ code_ = code;
+ session_->OnStreamClose(id_, code);
+ DEBUG_HTTP2("Nghttp2Stream %d: closed\n", id_);
+ }
+
+ // Returns true if this stream has been closed either by receiving or
+ // sending an RST_STREAM frame.
+ inline bool IsClosed() const {
+ return (flags_ & NGHTTP2_STREAM_CLOSED) == NGHTTP2_STREAM_CLOSED;
+ }
+
+ // Returns the RST_STREAM code used to close this stream
+ inline int32_t code() const {
+ return code_;
+ }
+
+ // Returns the stream identifier for this stream
+ inline int32_t id() const {
+ return id_;
+ }
+
+ inline nghttp2_header_list* headers() const {
+ return current_headers_head_;
+ }
+
+ inline nghttp2_headers_category headers_category() const {
+ return current_headers_category_;
+ }
+
+ inline void FreeHeaders();
+
+ void StartHeaders(nghttp2_headers_category category) {
+ DEBUG_HTTP2("Nghttp2Stream %d: starting headers, category: %d\n",
+ id_, category);
+ // We shouldn't be in the middle of a headers block already.
+ // Something bad happened if this fails
+ CHECK_EQ(current_headers_head_, nullptr);
+ CHECK_EQ(current_headers_tail_, nullptr);
+ current_headers_category_ = category;
+ }
+
+ private:
+ // The Parent HTTP/2 Session
+ Nghttp2Session* session_ = nullptr;
+
+ // The Stream Identifier
+ int32_t id_ = 0;
+
+ // Internal state flags
+ int flags_ = 0;
+
+ // Outbound Data... This is the data written by the JS layer that is
+ // waiting to be written out to the socket.
+ nghttp2_stream_write_queue* queue_head_ = nullptr;
+ nghttp2_stream_write_queue* queue_tail_ = nullptr;
+ unsigned int queue_head_index_ = 0;
+ size_t queue_head_offset_ = 0;
+ size_t fd_offset_ = 0;
+
+ // The Current Headers block... As headers are received for this stream,
+ // they are temporarily stored here until the OnFrameReceived is called
+ // signalling the end of the HEADERS frame
+ nghttp2_header_list* current_headers_head_ = nullptr;
+ nghttp2_header_list* current_headers_tail_ = nullptr;
+ nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
+
+ // Inbound Data... This is the data received via DATA frames for this stream.
+ nghttp2_data_chunk_t* data_chunks_head_ = nullptr;
+ nghttp2_data_chunk_t* data_chunks_tail_ = nullptr;
+
+ // The RST_STREAM code used to close this stream
+ int32_t code_ = NGHTTP2_NO_ERROR;
+
+ int32_t prev_local_window_size_ = 65535;
+
+ friend class Nghttp2Session;
+};
+
+struct nghttp2_stream_write_t {
+ void* data;
+ int status;
+ Nghttp2Stream* handle;
+ nghttp2_stream_write_queue* item;
+};
+
+struct nghttp2_data_chunk_t {
+ uv_buf_t buf;
+ nghttp2_data_chunk_t* next = nullptr;
+};
+
+struct nghttp2_data_chunks_t {
+ unsigned int nbufs = 0;
+ uv_buf_t buf[MAX_BUFFER_COUNT];
+
+ inline ~nghttp2_data_chunks_t();
+};
+
+} // namespace http2
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#endif // SRC_NODE_HTTP2_CORE_H_
diff --git a/src/node_internals.h b/src/node_internals.h
index d6bdf9b5ba..5d437fa302 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -83,6 +83,9 @@ extern std::string openssl_config;
// that is used by lib/module.js
extern bool config_preserve_symlinks;
+// Set in node.cc by ParseArgs when --expose-http2 is used.
+extern bool config_expose_http2;
+
// Set in node.cc by ParseArgs when --expose-internals or --expose_internals is
// used.
// Used in node_config.cc to set a constant on process.binding('config')
diff --git a/src/stream_base.cc b/src/stream_base.cc
index 51bad94a4f..3e94054546 100644
--- a/src/stream_base.cc
+++ b/src/stream_base.cc
@@ -408,6 +408,7 @@ void StreamBase::AfterWrite(WriteWrap* req_wrap, int status) {
// Unref handle property
Local<Object> req_wrap_obj = req_wrap->object();
req_wrap_obj->Delete(env->context(), env->handle_string()).FromJust();
+
wrap->OnAfterWrite(req_wrap);
Local<Value> argv[] = {
diff --git a/src/stream_base.h b/src/stream_base.h
index 68c82d243f..1b486e61db 100644
--- a/src/stream_base.h
+++ b/src/stream_base.h
@@ -89,6 +89,17 @@ class WriteWrap: public ReqWrap<uv_write_t>,
static const size_t kAlignSize = 16;
+ WriteWrap(Environment* env,
+ v8::Local<v8::Object> obj,
+ StreamBase* wrap,
+ DoneCb cb)
+ : ReqWrap(env, obj, AsyncWrap::PROVIDER_WRITEWRAP),
+ StreamReq<WriteWrap>(cb),
+ wrap_(wrap),
+ storage_size_(0) {
+ Wrap(obj, this);
+ }
+
protected:
WriteWrap(Environment* env,
v8::Local<v8::Object> obj,
diff --git a/vcbuild.bat b/vcbuild.bat
index 3e41d45e8b..30b557c26b 100644
--- a/vcbuild.bat
+++ b/vcbuild.bat
@@ -48,6 +48,8 @@ set js_test_suites=async-hooks inspector known_issues message parallel sequentia
set v8_test_options=
set v8_build_options=
set "common_test_suites=%js_test_suites% doctool addons addons-napi&set build_addons=1&set build_addons_napi=1"
+set http2_debug=
+set nghttp2_debug=
:next-arg
if "%1"=="" goto args-done
@@ -107,6 +109,8 @@ if /i "%1"=="enable-vtune" set enable_vtune_arg=1&goto arg-ok
if /i "%1"=="dll" set dll=1&goto arg-ok
if /i "%1"=="static" set enable_static=1&goto arg-ok
if /i "%1"=="no-NODE-OPTIONS" set no_NODE_OPTIONS=1&goto arg-ok
+if /i "%1"=="debug-http2" set debug_http2=1&goto arg-ok
+if /i "%1"=="debug-nghttp2" set debug_nghttp2=1&goto arg-ok
echo Error: invalid command line option `%1`.
exit /b 1
@@ -144,6 +148,9 @@ if defined dll set configure_flags=%configure_flags% --shared
if defined enable_static set configure_flags=%configure_flags% --enable-static
if defined no_NODE_OPTIONS set configure_flags=%configure_flags% --without-node-options
+REM if defined debug_http2 set configure_flags=%configure_flags% --debug-http2
+REM if defined debug_nghttp2 set configure_flags=%configure_flags% --debug-nghttp2
+
if "%i18n_arg%"=="full-icu" set configure_flags=%configure_flags% --with-intl=full-icu
if "%i18n_arg%"=="small-icu" set configure_flags=%configure_flags% --with-intl=small-icu
if "%i18n_arg%"=="intl-none" set configure_flags=%configure_flags% --with-intl=none