diff --git a/configure b/configure index a481e444330c06..0b9c3a16ed09ff 100755 --- a/configure +++ b/configure @@ -64,6 +64,8 @@ shared_optgroup = optparse.OptionGroup(parser, "Shared libraries", intl_optgroup = optparse.OptionGroup(parser, "Internationalization", "Flags that lets you enable i18n features in Node.js as well as which " "library you want to build against.") +http2_optgroup = optparse.OptionGroup(parser, "HTTP2", + "Flags that allows you to control HTTP2 features in Node.js") # Options should be in alphabetical order but keep --prefix at the top, # that's arguably the one people will be looking for most. @@ -397,6 +399,16 @@ intl_optgroup.add_option('--download-path', parser.add_option_group(intl_optgroup) +http2_optgroup.add_option('--debug-http2', + action='store_true', + dest='debug_http2', + help='build with http2 debug statements on (default is false)') + +http2_optgroup.add_option('--debug-nghttp2', + action='store_true', + dest='debug_nghttp2', + help='build nghttp2 with DEBUGBUILD (default is false)') + parser.add_option('--with-perfctr', action='store_true', dest='with_perfctr', @@ -898,6 +910,16 @@ def configure_node(o): if options.enable_static: o['variables']['node_target_type'] = 'static_library' + if options.debug_http2: + o['variables']['debug_http2'] = 1 + else: + o['variables']['debug_http2'] = 'false' + + if options.debug_nghttp2: + o['variables']['debug_nghttp2'] = 1 + else: + o['variables']['debug_nghttp2'] = 'false' + o['variables']['node_no_browser_globals'] = b(options.no_browser_globals) o['variables']['node_shared'] = b(options.shared) node_module_version = getmoduleversion.get_version() diff --git a/doc/api/_toc.md b/doc/api/_toc.md index 1075bc6be39858..6791e63f0c601a 100644 --- a/doc/api/_toc.md +++ b/doc/api/_toc.md @@ -24,6 +24,7 @@ * [File System](fs.html) * [Globals](globals.html) * [HTTP](http.html) +* [HTTP/2](http2.html) * [HTTPS](https.html) * [Inspector](inspector.html) * [Internationalization](intl.html) diff --git a/doc/api/cli.md b/doc/api/cli.md index 773e742966f330..f3172fbdde0f1d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -170,6 +170,13 @@ added: v6.0.0 Silence all process warnings (including deprecations). +### `--expose-http2` + + +Enable the experimental `'http2'` module. + ### `--napi-modules` + +* Extends: {EventEmitter} + +Instances of the `http2.Http2Session` class represent an active communications +session between an HTTP/2 client and server. Instances of this class are *not* +intended to be constructed directly by user code. + +Each `Http2Session` instance will exhibit slightly different behaviors +depending on whether it is operating as a server or a client. The +`http2session.type` property can be used to determine the mode in which an +`Http2Session` is operating. On the server side, user code should rarely +have occasion to work with the `Http2Session` object directly, with most +actions typically taken through interactions with either the `Http2Server` or +`Http2Stream` objects. + +#### Http2Session and Sockets + +Every `Http2Session` instance is associated with exactly one [`net.Socket`][] or +[`tls.TLSSocket`][] when it is created. When either the `Socket` or the +`Http2Session` are destroyed, both will be destroyed. + +Because the of the specific serialization and processing requirements imposed +by the HTTP/2 protocol, it is not recommended for user code to read data from +or write data to a `Socket` instance bound to a `Http2Session`. Doing so can +put the HTTP/2 session into an indeterminate state causing the session and +the socket to become unusable. + +Once a `Socket` has been bound to an `Http2Session`, user code should rely +solely on the API of the `Http2Session`. + +#### Event: 'close' + + +The `'close'` event is emitted once the `Http2Session` has been terminated. + +#### Event: 'connect' + + +The `'connect'` event is emitted once the `Http2Session` has been successfully +connected to the remote peer and communication may begin. + +*Note*: User code will typically not listen for this event directly. + +#### Event: 'error' + + +The `'error'` event is emitted when an error occurs during the processing of +an `Http2Session`. + +#### Event: 'frameError' + + +The `'frameError'` event is emitted when an error occurs while attempting to +send a frame on the session. If the frame that could not be sent is associated +with a specific `Http2Stream`, an attempt to emit `'frameError'` event on the +`Http2Stream` is made. + +When invoked, the handler function will receive three arguments: + +* An integer identifying the frame type. +* An integer identifying the error code. +* An integer identifying the stream (or 0 if the frame is not associated with + a stream). + +If the `'frameError'` event is associated with a stream, the stream will be +closed and destroyed immediately following the `'frameError'` event. If the +event is not associated with a stream, the `Http2Session` will be shutdown +immediately following the `'frameError'` event. + +#### Event: 'goaway' + + +The `'goaway'` event is emitted when a GOAWAY frame is received. When invoked, +the handler function will receive three arguments: + +* `errorCode` {number} The HTTP/2 error code specified in the GOAWAY frame. +* `lastStreamID` {number} The ID of the last stream the remote peer successfully + processed (or `0` if no ID is specified). +* `opaqueData` {Buffer} If additional opaque data was included in the GOAWAY + frame, a `Buffer` instance will be passed containing that data. + +*Note*: The `Http2Session` instance will be shutdown automatically when the +`'goaway'` event is emitted. + +#### Event: 'localSettings' + + +The `'localSettings'` event is emitted when an acknowledgement SETTINGS frame +has been received. When invoked, the handler function will receive a copy of +the local settings. + +*Note*: When using `http2session.settings()` to submit new settings, the +modified settings do not take effect until the `'localSettings'` event is +emitted. + +```js +session.settings({ enablePush: false }); + +session.on('localSettings', (settings) => { + /** use the new settings **/ +}); +``` + +#### Event: 'remoteSettings' + + +The `'remoteSettings'` event is emitted when a new SETTINGS frame is received +from the connected peer. When invoked, the handle function will receive a copy +of the remote settings. + +```js +session.on('remoteSettings', (settings) => { + /** use the new settings **/ +}); +``` + +#### Event: 'stream' + + +The `'stream'` event is emitted when a new `Http2Stream` is created. When +invoked, the handler function will receive a reference to the `Http2Stream` +object, a [Headers Object][], and numeric flags associated with the creation +of the stream. + +```js +const http2 = require('http2'); +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; +session.on('stream', (stream, headers, flags) => { + const method = headers[HTTP2_HEADER_METHOD]; + const path = headers[HTTP2_HEADER_PATH]; + // ... + stream.respond({ + [HTTP2_HEADER_STATUS]: 200, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + stream.write('hello '); + stream.end('world'); +}); +``` + +On the server side, user code will typically not listen for this event directly, +and would instead register a handler for the `'stream'` event emitted by the +`net.Server` or `tls.Server` instances returned by `http2.createServer()` and +`http2.createSecureServer()`, respectively, as in the example below: + +```js +const http2 = require('http2'); + +// Create a plain-text HTTP/2 server +const server = http2.createServer(); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('

Hello World

'); +}); + +server.listen(80); +``` + +#### Event: 'socketError' + + +The `'socketError'` event is emitted when an `'error'` is emitted on the +`Socket` instance bound to the `Http2Session`. If this event is not handled, +the `'error'` event will be re-emitted on the `Socket`. + +Likewise, when an `'error'` event is emitted on the `Http2Session`, a +`'sessionError'` event will be emitted on the `Socket`. If that event is +not handled, the `'error'` event will be re-emitted on the `Http2Session`. + +#### Event: 'timeout' + + +After the `http2session.setTimeout()` method is used to set the timeout period +for this `Http2Session`, the `'timeout'` event is emitted if there is no +activity on the `Http2Session` after the configured number of milliseconds. + +```js +session.setTimeout(2000); +session.on('timeout', () => { /** .. **/ }); +``` + +#### http2session.destroy() + + +* Returns: {undefined} + +Immediately terminates the `Http2Session` and the associated `net.Socket` or +`tls.TLSSocket`. + +#### http2session.destroyed + + +* Value: {boolean} + +Will be `true` if this `Http2Session` instance has been destroyed and must no +longer be used, otherwise `false`. + +#### http2session.localSettings + + +* Value: {[Settings Object][]} + +A prototype-less object describing the current local settings of this +`Http2Session`. The local settings are local to *this* `Http2Session` instance. + +#### http2session.pendingSettingsAck + + +* Value: {boolean} + +Indicates whether or not the `Http2Session` is currently waiting for an +acknowledgement for a sent SETTINGS frame. Will be `true` after calling the +`http2session.settings()` method. Will be `false` once all sent SETTINGS +frames have been acknowledged. + +#### http2session.remoteSettings + + +* Value: {[Settings Object][]} + +A prototype-less object describing the current remote settings of this +`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer. + +#### http2session.request(headers[, options]) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should + be closed initially, such as when sending a `GET` request that should not + expect a payload body. + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the created stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the newly created stream. + Defaults to `false`. + * `parent` {number} Specifies the numeric identifier of a stream the newly + created stream is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). +* Returns: {ClientHttp2Stream} + +For HTTP/2 Client `Http2Session` instances only, the `http2session.request()` +creates and returns an `Http2Stream` instance that can be used to send an +HTTP/2 request to the connected server. + +This method is only available if `http2session.type` is equal to +`http2.constants.NGHTTP2_SESSION_CLIENT`. + +```js +const http2 = require('http2'); +const clientSession = http2.connect('https://localhost:1234'); +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS +} = http2.constants; + +const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' }); +req.on('response', (headers) => { + console.log(HTTP2_HEADER_STATUS); + req.on('data', (chunk) => { /** .. **/ }); + req.on('end', () => { /** .. **/ }); +}); +``` + +#### http2session.rstStream(stream, code) + + +* stream {Http2Stream} +* code {number} Unsigned 32-bit integer identifying the error code. Defaults to + `http2.constant.NGHTTP2_NO_ERROR` (`0x00`) +* Returns: {undefined} + +Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing the given +`Http2Stream` to be closed on both sides using [error code][] `code`. + +#### http2session.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} +* Returns: {undefined} + +Used to set a callback function that is called when there is no activity on +the `Http2Session` after `msecs` milliseconds. The given `callback` is +registered as a listener on the `'timeout'` event. + +#### http2session.shutdown(options[, callback]) + + +* `options` {Object} + * `graceful` {boolean} `true` to attempt a polite shutdown of the + `Http2Session`. + * `errorCode` {number} The HTTP/2 [error code][] to return. Note that this is + *not* the same thing as an HTTP Response Status Code. Defaults to `0x00` + (No Error). + * `lastStreamID` {number} The Stream ID of the last successfully processed + `Http2Stream` on this `Http2Session`. + * `opaqueData` {Buffer} A `Buffer` instance containing arbitrary additional + data to send to the peer upon disconnection. This is used, typically, to + provide additional data for debugging failures, if necessary. +* `callback` {Function} A callback that is invoked after the session shutdown + has been completed. +* Returns: {undefined} + +Attempts to shutdown this `Http2Session` using HTTP/2 defined procedures. +If specified, the given `callback` function will be invoked once the shutdown +process has completed. + +Note that calling `http2session.shutdown()` does *not* destroy the session or +tear down the `Socket` connection. It merely prompts both sessions to begin +preparing to cease activity. + +During a "graceful" shutdown, the session will first send a `GOAWAY` frame to +the connected peer identifying the last processed stream as 232-1. +Then, on the next tick of the event loop, a second `GOAWAY` frame identifying +the most recently processed stream identifier is sent. This process allows the +remote peer to begin preparing for the connection to be terminated. + +```js +session.shutdown({ + graceful: true, + opaqueData: Buffer.from('add some debugging data here') +}, () => session.destroy()); +``` + +#### http2session.socket + + +* Value: {net.Socket|tls.TLSSocket} + +A reference to the [`net.Socket`][] or [`tls.TLSSocket`][] to which this +`Http2Session` instance is bound. + +*Note*: It is not recommended for user code to interact directly with a +`Socket` bound to an `Http2Session`. See [Http2Session and Sockets][] for +details. + +#### http2session.state + + +* Value: {Object} + * `effectiveLocalWindowSize` {number} + * `effectiveRecvDataLength` {number} + * `nextStreamID` {number} + * `localWindowSize` {number} + * `lastProcStreamID` {number} + * `remoteWindowSize` {number} + * `outboundQueueSize` {number} + * `deflateDynamicTableSize` {number} + * `inflateDynamicTableSize` {number} + +An object describing the current status of this `Http2Session`. + +#### http2session.priority(stream, options) + + +* `stream` {Http2Stream} +* `options` {Object} + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the given stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the given stream. Defaults + to `false`. + * `parent` {number} Specifies the numeric identifier of a stream the given + stream is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). + * `silent` {boolean} When `true`, changes the priority locally without + sending a `PRIORITY` frame to the connected peer. +* Returns: {undefined} + +Updates the priority for the given `Http2Stream` instance. + +#### http2session.settings(settings) + + +* `settings` {[Settings Object][]} +* Returns {undefined} + +Updates the current local settings for this `Http2Session` and sends a new +`SETTINGS` frame to the connected HTTP/2 peer. + +Once called, the `http2session.pendingSettingsAck` property will be `true` +while the session is waiting for the remote peer to acknowledge the new +settings. + +*Note*: The new settings will not become effective until the SETTINGS +acknowledgement is received and the `'localSettings'` event is emitted. It +is possible to send multiple SETTINGS frames while acknowledgement is still +pending. + +#### http2session.type + + +* Value: {number} + +The `http2session.type` will be equal to +`http2.constants.NGHTTP2_SESSION_SERVER` if this `Http2Session` instance is a +server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a +client. + +### Class: Http2Stream + + +* Extends: {Duplex} + +Each instance of the `Http2Stream` class represents a bidirectional HTTP/2 +communications stream over an `Http2Session` instance. Any single `Http2Session` +may have up to 231-1 `Http2Stream` instances over its lifetime. + +User code will not construct `Http2Stream` instances directly. Rather, these +are created, managed, and provided to user code through the `Http2Session` +instance. On the server, `Http2Stream` instances are created either in response +to an incoming HTTP request (and handed off to user code via the `'stream'` +event), or in response to a call to the `http2stream.pushStream()` method. +On the client, `Http2Stream` instances are created and returned when either the +`http2session.request()` method is called, or in response to an incoming +`'push'` event. + +*Note*: The `Http2Stream` class is a base for the [`ServerHttp2Stream`][] and +[`ClientHttp2Stream`][] classes, each of which are used specifically by either +the Server or Client side, respectively. + +All `Http2Stream` instances are [`Duplex`][] streams. The `Writable` side of the +`Duplex` is used to send data to the connected peer, while the `Readable` side +is used to receive data sent by the connected peer. + +#### Http2Stream Lifecycle + +##### Creation + +On the server side, instances of [`ServerHttp2Stream`][] are created either +when: + +* A new HTTP/2 `HEADERS` frame with a previously unused stream ID is received; +* The `http2stream.pushStream()` method is called. + +On the client side, instances of [`ClientHttp2Stream`[] are created when the +`http2session.request()` method is called. + +*Note*: On the client, the `Http2Stream` instance returned by +`http2session.request()` may not be immediately ready for use if the parent +`Http2Session` has not yet been fully established. In such cases, operations +called on the `Http2Stream` will be buffered until the `'ready'` event is +emitted. User code should rarely, if ever, have need to handle the `'ready'` +event directly. The ready status of an `Http2Stream` can be determined by +checking the value of `http2stream.id`. If the value is `undefined`, the stream +is not yet ready for use. + +##### Destruction + +All [`Http2Stream`][] instances are destroyed either when: + +* An `RST_STREAM` frame for the stream is received by the connected peer. +* The `http2stream.rstStream()` or `http2session.rstStream()` methods are + called. +* The `http2stream.destroy()` or `http2session.destroy()` methods are called. + +When an `Http2Stream` instance is destroyed, an attempt will be made to send an +`RST_STREAM` frame will be sent to the connected peer. + +Once the `Http2Stream` instance is destroyed, the `'streamClosed'` event will +be emitted. Because `Http2Stream` is an instance of `stream.Duplex`, the +`'end'` event will also be emitted if the stream data is currently flowing. +The `'error'` event may also be emitted if `http2stream.destroy()` was called +with an `Error` passed as the first argument. + +After the `Http2Stream` has been destroyed, the `http2stream.destroyed` +property will be `true` and the `http2stream.rstCode` property will specify the +`RST_STREAM` error code. The `Http2Stream` instance is no longer usable once +destroyed. + +#### Event: 'aborted' + + +The `'aborted'` event is emitted whenever a `Http2Stream` instance is +abnormally aborted in mid-communication. + +*Note*: The `'aborted'` event will only be emitted if the `Http2Stream` +writable side has not been ended. + +#### Event: 'error' + + +The `'error'` event is emitted when an error occurs during the processing of +an `Http2Stream`. + +#### Event: 'fetchTrailers' + + +The `'fetchTrailers'` event is emitted by the `Http2Stream` immediately after +queuing the last chunk of payload data to be sent. The listener callback is +passed a single object (with a `null` prototype) that the listener may used +to specify the trailing header fields to send to the peer. + +```js +stream.on('fetchTrailers', (trailers) => { + trailers['ABC'] = 'some value to send'; +}); +``` + +*Note*: The HTTP/1 specification forbids trailers from containing HTTP/2 +"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event +will be emitted if the `'fetchTrailers'` event handler attempts to set such +header fields. + +#### Event: 'frameError' + + +The `'frameError'` event is emitted when an error occurs while attempting to +send a frame. When invoked, the handler function will receive an integer +argument identifying the frame type, and an integer argument identifying the +error code. The `Http2Stream` instance will be destroyed immediately after the +`'frameError'` event is emitted. + +#### Event: 'streamClosed' + + +The `'streamClosed'` event is emitted when the `Http2Stream` is destroyed. Once +this event is emitted, the `Http2Stream` instance is no longer usable. + +The listener callback is passed a single argument specifying the HTTP/2 error +code specified when closing the stream. If the code is any value other than +`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will also be emitted. + +#### Event: 'timeout' + + +The `'timeout'` event is emitted after no activity is received for this +`'Http2Stream'` within the number of millseconds set using +`http2stream.setTimeout()`. + +#### Event: 'trailers' + + +The `'trailers'` event is emitted when a block of headers associated with +trailing header fields is received. The listener callback is passed the +[Headers Object][] and flags associated with the headers. + +```js +stream.on('trailers', (headers, flags) => { + console.log(headers); +}); +``` + +#### http2stream.aborted + + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance was aborted abnormally. When set, +the `'aborted'` event will have been emitted. + +#### http2stream.destroyed + + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance has been destroyed and is no longer +usable. + +#### http2stream.priority(options) + + +* `options` {Object} + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + this stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of this stream. Defaults + to `false`. + * `parent` {number} Specifies the numeric identifier of a stream this stream + is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). + * `silent` {boolean} When `true`, changes the priority locally without + sending a `PRIORITY` frame to the connected peer. +* Returns: {undefined} + +Updates the priority for this `Http2Stream` instance. + +#### http2stream.rstCode + + +* Value: {number} + +Set to the `RST_STREAM` [error code][] reported when the `Http2Stream` is +destroyed after either receiving an `RST_STREAM` frame from the connected peer, +calling `http2stream.rstStream()`, or `http2stream.destroy()`. Will be +`undefined` if the `Http2Stream` has not been closed. + +#### http2stream.rstStream(code) + + +* code {number} Unsigned 32-bit integer identifying the error code. Defaults to + `http2.constant.NGHTTP2_NO_ERROR` (`0x00`) +* Returns: {undefined} + +Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing this +`Http2Stream` to be closed on both sides using [error code][] `code`. + +#### http2stream.rstWithNoError() + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error). + +#### http2stream.rstWithProtocolError() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error). + +#### http2stream.rstWithCancel() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel). + +#### http2stream.rstWithRefuse() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream). + +#### http2stream.rstWithInternalError() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error). + +#### http2stream.session + + +* Value: {Http2Sesssion} + +A reference to the `Http2Session` instance that owns this `Http2Stream`. The +value will be `undefined` after the `Http2Stream` instance is destroyed. + +#### http2stream.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} +* Returns: {undefined} + +```js +const http2 = require('http2'); +const client = http2.connect('http://example.org:8000'); + +const req = client.request({ ':path': '/' }); + +// Cancel the stream if there's no activity after 5 seconds +req.setTimeout(5000, () => req.rstStreamWithCancel()); +``` + +#### http2stream.state + + +* Value: {Object} + * `localWindowSize` {number} + * `state` {number} + * `streamLocalClose` {number} + * `streamRemoteClose` {number} + * `sumDependencyWeight` {number} + * `weight` {number} + +A current state of this `Http2Stream`. + +### Class: ClientHttp2Stream + + +* Extends {Http2Stream} + +The `ClientHttp2Stream` class is an extension of `Http2Stream` that is +used exclusively on HTTP/2 Clients. `Http2Stream` instances on the client +provide events such as `'response'` and `'push'` that are only relevant on +the client. + +#### Event: 'headers' + + +The `'headers'` event is emitted when an additional block of headers is received +for a stream, such as when a block of `1xx` informational headers are received. +The listener callback is passed the [Headers Object][] and flags associated with +the headers. + +```js +stream.on('headers', (headers, flags) => { + console.log(headers); +}); +``` + +#### Event: 'push' + + +The `'push'` event is emitted when response headers for a Server Push stream +are received. The listener callback is passed the [Headers Object][] and flags +associated with the headers. + +```js +stream.on('push', (headers, flags) => { + console.log(headers); +}); +``` + +#### Event: 'response' + + +The `'response'` event is emitted when a response `HEADERS` frame has been +received for this stream from the connected HTTP/2 server. The listener is +invoked with two arguments: an Object containing the received +[Headers Object][], and flags associated with the headers. + +For example: + +```js +const http2 = require('http'); +const client = http2.connect('https://localhost'); +const req = client.request({ ':path': '/' }); +req.on('response', (headers, flags) => { + console.log(headers[':status']); +}); +``` + +### Class: ServerHttp2Stream + + +* Extends: {Http2Stream} + +The `ServerHttp2Stream` class is an extension of [`Http2Stream`][] that is +used exclusively on HTTP/2 Servers. `Http2Stream` instances on the server +provide additional methods such as `http2stream.pushStream()` and +`http2stream.respond()` that are only relevant on the server. + +#### http2stream.additionalHeaders(headers) + + +* `headers` {[Headers Object][]} +* Returns: {undefined} + +Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer. + +#### http2stream.headersSent + + +* Value: {boolean} + +Boolean (read-only). True if headers were sent, false otherwise. + +#### http2stream.pushAllowed + + +* Value: {boolean} + +Read-only property mapped to the `SETTINGS_ENABLE_PUSH` flag of the remote +client's most recent `SETTINGS` frame. Will be `true` if the remote peer +accepts push streams, `false` otherwise. Settings are the same for every +`Http2Stream` in the same `Http2Session`. + +#### http2stream.pushStream(headers[, options], callback) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the created stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the newly created stream. + Defaults to `false`. + * `parent` {number} Specifies the numeric identifier of a stream the newly + created stream is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). +* `callback` {Function} Callback that is called once the push stream has been + initiated. +* Returns: {undefined} + +Initiates a push stream. The callback is invoked with the new `Htt2Stream` +instance created for the push stream. + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond({ ':status': 200 }); + stream.pushStream({ ':path': '/' }, (pushStream) => { + pushStream.respond({ ':status': 200 }); + pushStream.end('some pushed data'); + }); + stream.end('some data'); +}); +``` + +#### http2stream.respond([headers[, options]]) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `endStream` {boolean} Set to `true` to indicate that the response will not + include payload data. +* Returns: {undefined} + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond({ ':status': 200 }); + stream.end('some data'); +}); +``` + +#### http2stream.respondWithFD(fd[, headers]) + + +* `fd` {number} A readable file descriptor +* `headers` {[Headers Object][]} + +Initiates a response whose data is read from the given file descriptor. No +validation is performed on the given file descriptor. If an error occurs while +attempting to read data using the file descriptor, the `Http2Stream` will be +closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` code. + +When used, the `Http2Stream` object's Duplex interface will be closed +automatically. HTTP trailer fields cannot be sent. The `'fetchTrailers'` event +will *not* be emitted. + +```js +const http2 = require('http2'); +const fs = require('fs'); + +const fd = fs.openSync('/some/file', 'r'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + const stat = fs.fstatSync(fd); + const headers = { + 'content-length': stat.size, + 'last-modified': stat.mtime.toUTCString(), + 'content-type': 'text/plain' + }; + stream.respondWithFD(fd, headers); +}); +server.on('close', () => fs.closeSync(fd)); +``` + +#### http2stream.respondWithFile(path[, headers[, options]]) + + +* `path` {string|Buffer|URL} +* `headers` {[Headers Object][]} +* `options` {Object} + * `statCheck` {Function} + +Sends a regular file as the response. The `path` must specify a regular file +or an `'error'` event will be emitted on the `Http2Stream` object. + +When used, the `Http2Stream` object's Duplex interface will be closed +automatically. HTTP trailer fields cannot be sent. The `'fetchTrailers'` event +will *not* be emitted. + +The optional `options.statCheck` function may be specified to give user code +an opportunity to set additional content headers based on the `fs.Stat` details +of the given file: + +If an error occurs while attempting to read the file data, the `Http2Stream` +will be closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` +code. + +Example using a file path: + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + function statCheck(stat, headers) { + headers['last-modified'] = stat.mtime.toUTCString(); + } + stream.respondWithFile('/some/file', + { 'content-type': 'text/plain' }, + { statCheck }); +}); +``` + +The `options.statCheck` function may also be used to cancel the send operation +by returning `false`. For instance, a conditional request may check the stat +results to determine if the file has been modified to return an appropriate +`304` response: + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + function statCheck(stat, headers) { + // Check the stat here... + stream.respond({ ':status': 304 }); + return false; // Cancel the send operation + } + stream.respondWithFile('/some/file', + { 'content-type': 'text/plain' }, + { statCheck }); +}); +``` + +The `content-length` header field will be automatically set. + +### Class: Http2Server + + +* Extends: {net.Server} + +#### Event: 'sessionError' + + +The `'sessionError'` event is emitted when an `'error'` event is emitted by +an `Http2Session` object. If no listener is registered for this event, an +`'error'` event is emitted. + +#### Event: 'socketError' + + +The `'socketError'` event is emitted when an `'error'` event is emitted by +a `Socket` associated with the server. If no listener is registered for this +event, an `'error'` event is emitted. + +#### Event: 'stream' + + +The `'stream'` event is emitted when a `'stream'` event has been emitted by +an `Http2Session` associated with the server. + +```js +const http2 = require('http2'); +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; + +const server = http.createServer(); +server.on('stream', (stream, headers, flags) => { + const method = headers[HTTP2_HEADER_METHOD]; + const path = headers[HTTP2_HEADER_PATH]; + // ... + stream.respond({ + [HTTP2_HEADER_STATUS]: 200, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + stream.write('hello '); + stream.end('world'); +}); +``` + +#### Event: 'timeout' + + +The `'timeout'` event is emitted when there is no activity on the Server for +a given number of milliseconds set using `http2server.setTimeout()`. + +### Class: Http2SecureServer + + +* Extends: {tls.Server} + +#### Event: 'sessionError' + + +The `'sessionError'` event is emitted when an `'error'` event is emitted by +an `Http2Session` object. If no listener is registered for this event, an +`'error'` event is emitted on the `Http2Session` instance instead. + +#### Event: 'socketError' + + +The `'socketError'` event is emitted when an `'error'` event is emitted by +a `Socket` associated with the server. If no listener is registered for this +event, an `'error'` event is emitted on the `Socket` instance instead. + +#### Event: 'unknownProtocol' + + +The `'unknownProtocol'` event is emitted when a connecting client fails to +negotiate an allowed protocol (i.e. HTTP/2 or HTTP/1.1). The event handler +receives the socket for handling. If no listener is registered for this event, +the connection is terminated. See the + +#### Event: 'stream' + + +The `'stream'` event is emitted when a `'stream'` event has been emitted by +an `Http2Session` associated with the server. + +```js +const http2 = require('http2'); +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; + +const options = getOptionsSomehow(); + +const server = http.createSecureServer(options); +server.on('stream', (stream, headers, flags) => { + const method = headers[HTTP2_HEADER_METHOD]; + const path = headers[HTTP2_HEADER_PATH]; + // ... + stream.respond({ + [HTTP2_HEADER_STATUS]: 200, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + stream.write('hello '); + stream.end('world'); +}); +``` + +#### Event: 'timeout' + + +### http2.createServer(options[, onRequestHandler]) + + +* `options` {Object} + * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size + for deflating header fields. Defaults to 4Kib. + * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a + serialized, compressed block of headers. Attempts to send headers that + exceed this limit will result in a `'frameError'` event being emitted + and the stream being closed and destroyed. + * `paddingStrategy` {number} Identifies the strategy used for determining the + amount of padding to use for HEADERS and DATA frames. Defaults to + `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of: + * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is + to be applied. + * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum + amount of padding, as determined by the internal implementation, is to + be applied. + * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user + provided `options.selectPadding` callback is to be used to determine the + amount of padding. + * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent + streams for the remote peer as if a SETTINGS frame had been received. Will + be overridden if the remote peer sets its own value for + `maxConcurrentStreams`. Defaults to 100. + * `selectPadding` {Function} When `options.paddingStrategy` is equal to + `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function + used to determine the padding. See [Using options.selectPadding][]. + * `settings` {[Settings Object][]} The initial settings to send to the + remote peer upon connection. +* `onRequestHandler` {Function} See [Compatibility API][] +* Returns: {Http2Server} + +Returns a `net.Server` instance that creates and manages `Http2Session` +instances. + +```js +const http2 = require('http2'); + +// Create a plain-text HTTP/2 server +const server = http2.createServer(); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('

Hello World

'); +}); + +server.listen(80); +``` + +### http2.createSecureServer(options[, onRequestHandler]) + + +* `options` {Object} + * `allowHTTP1` {boolean} Incoming client connections that do not support + HTTP/2 will be downgraded to HTTP/1.x when set to `true`. The default value + is `false`. See the [`'unknownProtocol'`][] event. + * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size + for deflating header fields. Defaults to 4Kib. + * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a + serialized, compressed block of headers. Attempts to send headers that + exceed this limit will result in a `'frameError'` event being emitted + and the stream being closed and destroyed. + * `paddingStrategy` {number} Identifies the strategy used for determining the + amount of padding to use for HEADERS and DATA frames. Defaults to + `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of: + * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is + to be applied. + * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum + amount of padding, as determined by the internal implementation, is to + be applied. + * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user + provided `options.selectPadding` callback is to be used to determine the + amount of padding. + * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent + streams for the remote peer as if a SETTINGS frame had been received. Will + be overridden if the remote peer sets its own value for + `maxConcurrentStreams`. Defaults to 100. + * `selectPadding` {Function} When `options.paddingStrategy` is equal to + `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function + used to determine the padding. See [Using options.selectPadding][]. + * `settings` {[Settings Object][]} The initial settings to send to the + remote peer upon connection. + * ...: Any [`tls.createServer()`][] options can be provided. For + servers, the identity options (`pfx` or `key`/`cert`) are usually required. +* `onRequestHandler` {Function} See [Compatibility API][] +* Returns {Http2SecureServer} + +Returns a `tls.Server` instance that creates and manages `Http2Session` +instances. + +```js +const http2 = require('http2'); + +const options = { + key: fs.readFileSync('server-key.pem'), + cert: fs.readFileSync('server-cert.pem') +}; + +// Create a plain-text HTTP/2 server +const server = http2.createSecureServer(options); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('

Hello World

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