From 33ba2d8c66c5ebdb85774fe911b15c0382a62e7e Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Oct 2016 11:38:52 -0700 Subject: [PATCH 01/26] http: move utcDate to internal/http.js --- lib/_http_outgoing.js | 19 ++----------------- lib/internal/http.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) mode change 100644 => 100755 lib/_http_outgoing.js mode change 100644 => 100755 lib/internal/http.js diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js old mode 100644 new mode 100755 index 7a63406d51575b..f8ae30d4dee4f2 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -23,9 +23,9 @@ const assert = require('assert').ok; const Stream = require('stream'); -const timers = require('timers'); const util = require('util'); const internalUtil = require('internal/util'); +const internalHttp = require('internal/http'); const Buffer = require('buffer').Buffer; const common = require('_http_common'); const checkIsHttpToken = common._checkIsHttpToken; @@ -37,6 +37,7 @@ const errors = require('internal/errors'); const CRLF = common.CRLF; const debug = common.debug; +const utcDate = internalHttp.utcDate; var RE_FIELDS = /^(?:Connection|Transfer-Encoding|Content-Length|Date|Expect|Trailer|Upgrade)$/i; @@ -64,24 +65,8 @@ function isCookieField(s) { return true; } -var dateCache; -function utcDate() { - if (!dateCache) { - var d = new Date(); - dateCache = d.toUTCString(); - timers.enroll(utcDate, 1000 - d.getMilliseconds()); - timers._unrefActive(utcDate); - } - return dateCache; -} -utcDate._onTimeout = function _onTimeout() { - dateCache = undefined; -}; - - function noopPendingOutput(amount) {} - function OutgoingMessage() { Stream.call(this); diff --git a/lib/internal/http.js b/lib/internal/http.js old mode 100644 new mode 100755 index 0b12bc7c8fbe85..71e32498f359a1 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -1,5 +1,21 @@ 'use strict'; +const timers = require('timers'); + +var dateCache; +function utcDate() { + if (!dateCache) { + const d = new Date(); + dateCache = d.toUTCString(); + timers.enroll(utcDate, 1000 - d.getMilliseconds()); + timers._unrefActive(utcDate); + } + return dateCache; +} +utcDate._onTimeout = function() { + dateCache = undefined; +}; + function ondrain() { if (this._httpMessage) this._httpMessage.emit('drain'); } @@ -7,4 +23,5 @@ function ondrain() { module.exports = { outHeadersKey: Symbol('outHeadersKey'), ondrain, + utcDate }; From 1cc4d8607b7feb9ebde0c207ef3500bec7760088 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 4 Nov 2016 12:37:36 -0700 Subject: [PATCH 02/26] tls: add tlsSocket.disableRenegotiation() Allows TLS renegotiation to be disabled per `TLSSocket` instance. Per HTTP/2, TLS renegotiation is forbidden after the initial connection prefix is exchanged. --- doc/api/tls.md | 8 +++ lib/_tls_wrap.js | 10 +++ .../test-tls-disable-renegotiation.js | 67 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100755 test/parallel/test-tls-disable-renegotiation.js diff --git a/doc/api/tls.md b/doc/api/tls.md index 80cd2e87263566..2b27189e7604d4 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -552,6 +552,14 @@ added: v0.11.4 Returns `true` if the peer certificate was signed by one of the CAs specified when creating the `tls.TLSSocket` instance, otherwise `false`. +### tlsSocket.disableRenegotiation() + + +Disables TLS renegotiation for this `TLSSocket` instance. Once called, attempts +to renegotiate will trigger an `'error'` event on the `TLSSocket`. + ### tlsSocket.encrypted + +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 ca142a2cab152b..c97a9d812d5a78 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 feb6b3e3660c5e..3ba685b9424984 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -478,6 +478,11 @@ NativeModule._source = process.binding('natives'); NativeModule._cache = {}; + const config = process.binding('config'); + + if (!config.exposeHTTP2) + delete NativeModule._source.http2; + NativeModule.require = function(id) { if (id === 'native_module') { return NativeModule; @@ -516,8 +521,6 @@ return NativeModule._source.hasOwnProperty(id); }; - const config = process.binding('config'); - if (config.exposeInternals) { NativeModule.nonInternalExists = NativeModule.exists; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 9989528f76ea01..be057259d74ea1 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -118,6 +118,69 @@ E('ERR_HTTP_HEADERS_SENT', E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s'); E('ERR_HTTP_TRAILER_INVALID', 'Trailers are invalid with this transfer encoding'); +E('ERR_HTTP_INVALID_CHAR', 'Invalid character in statusMessage.'); +E('ERR_HTTP_INVALID_STATUS_CODE', + (originalStatusCode) => `Invalid status code: ${originalStatusCode}`); +E('ERR_HTTP2_CONNECT_AUTHORITY', + ':authority header is required for CONNECT requests'); +E('ERR_HTTP2_CONNECT_PATH', + 'The :path header is forbidden for CONNECT requests'); +E('ERR_HTTP2_CONNECT_SCHEME', + 'The :scheme header is forbidden for CONNECT requests'); +E('ERR_HTTP2_FRAME_ERROR', + (type, code, id) => { + let msg = `Error sending frame type ${type}`; + if (id !== undefined) + msg += ` for stream ${id}`; + msg += ` with code ${code}`; + return msg; + }); +E('ERR_HTTP2_HEADER_REQUIRED', + (name) => `The ${name} header is required`); +E('ERR_HTTP2_HEADER_SINGLE_VALUE', + (name) => `Header field "${name}" must have only a single value`); +E('ERR_HTTP2_HEADERS_OBJECT', 'Headers must be an object'); +E('ERR_HTTP2_HEADERS_SENT', 'Response has already been initiated.'); +E('ERR_HTTP2_HEADERS_AFTER_RESPOND', + 'Cannot specify additional headers after response initiated'); +E('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND', + 'Cannot send informational headers after the HTTP message has been sent'); +E('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + 'Informational status codes cannot be used'); +E('ERR_HTTP2_INVALID_CONNECTION_HEADERS', + 'HTTP/1 Connection specific headers are forbidden'); +E('ERR_HTTP2_INVALID_HEADER_VALUE', 'Value must not be undefined or null'); +E('ERR_HTTP2_INVALID_INFO_STATUS', + (code) => `Invalid informational status code: ${code}`); +E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + 'Packed settings length must be a multiple of six'); +E('ERR_HTTP2_INVALID_PSEUDOHEADER', + (name) => `"${name}" is an invalid pseudoheader or is used incorrectly`); +E('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed'); +E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed'); +E('ERR_HTTP2_INVALID_SETTING_VALUE', + (name, value) => `Invalid value for setting "${name}": ${value}`); +E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + (max) => `Maximum number of pending settings acknowledgements (${max})`); +E('ERR_HTTP2_PAYLOAD_FORBIDDEN', + (code) => `Responses with ${code} status must not have a payload`); +E('ERR_HTTP2_OUT_OF_STREAMS', + 'No stream ID is available because maximum stream ID has been reached'); +E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers'); +E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams'); +E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent'); +E('ERR_HTTP2_SOCKET_BOUND', + 'The socket is already bound to an Http2Session'); +E('ERR_HTTP2_STATUS_INVALID', + (code) => `Invalid status code: ${code}`); +E('ERR_HTTP2_STATUS_101', + 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2'); +E('ERR_HTTP2_STREAM_CLOSED', 'The stream is already closed'); +E('ERR_HTTP2_STREAM_ERROR', + (code) => `Stream closed with error code ${code}`); +E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', 'A stream cannot depend on itself'); +E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', + (protocol) => `protocol "${protocol}" is unsupported.`); E('ERR_INDEX_OUT_OF_RANGE', 'Index out of range'); E('ERR_INVALID_ARG_TYPE', invalidArgType); E('ERR_INVALID_ARRAY_LENGTH', @@ -173,6 +236,7 @@ E('ERR_SOCKET_BAD_TYPE', E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data'); E('ERR_SOCKET_CLOSED', 'Socket is closed'); E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running'); +E('ERR_OUTOFMEMORY', 'Out of memory'); E('ERR_STDERR_CLOSE', 'process.stderr cannot be closed'); E('ERR_STDOUT_CLOSE', 'process.stdout cannot be closed'); E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode'); diff --git a/lib/internal/http.js b/lib/internal/http.js 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 79332d9a42f2cd..a02356e84547be 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -40,6 +40,8 @@ namespace node { V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ + V(HTTP2SESSION) \ + V(HTTP2SESSIONSHUTDOWNWRAP) \ V(HTTPPARSER) \ V(JSSTREAM) \ V(PIPECONNECTWRAP) \ diff --git a/src/env-inl.h b/src/env-inl.h index 6753979ef57bf5..021d50841d498e 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -302,6 +302,7 @@ inline Environment::Environment(IsolateData* isolate_data, #endif handle_cleanup_waiting_(0), http_parser_buffer_(nullptr), + http2_socket_buffer_(nullptr), fs_stats_field_array_(nullptr), context_(context->GetIsolate(), context) { // We'll be creating new objects so make sure we've entered the context. @@ -328,6 +329,12 @@ inline Environment::~Environment() { delete[] heap_statistics_buffer_; delete[] heap_space_statistics_buffer_; delete[] http_parser_buffer_; + delete[] http2_socket_buffer_; + delete[] http2_settings_buffer_; + delete[] http2_options_buffer_; + delete[] http2_session_state_buffer_; + delete[] http2_stream_state_buffer_; + delete[] http2_padding_buffer_; } inline v8::Isolate* Environment::isolate() const { @@ -468,6 +475,55 @@ inline void Environment::set_heap_space_statistics_buffer(double* pointer) { heap_space_statistics_buffer_ = pointer; } +inline uint32_t* Environment::http2_settings_buffer() const { + CHECK_NE(http2_settings_buffer_, nullptr); + return http2_settings_buffer_; +} + +inline void Environment::set_http2_settings_buffer(uint32_t* pointer) { + CHECK_EQ(http2_settings_buffer_, nullptr); // Should be set only once + http2_settings_buffer_ = pointer; +} + +inline uint32_t* Environment::http2_options_buffer() const { + CHECK_NE(http2_options_buffer_, nullptr); + return http2_options_buffer_; +} + +inline void Environment::set_http2_options_buffer(uint32_t* pointer) { + CHECK_EQ(http2_options_buffer_, nullptr); // Should be set only once + http2_options_buffer_ = pointer; +} + +inline double* Environment::http2_session_state_buffer() const { + CHECK_NE(http2_session_state_buffer_, nullptr); + return http2_session_state_buffer_; +} + +inline void Environment::set_http2_session_state_buffer(double* pointer) { + CHECK_EQ(http2_session_state_buffer_, nullptr); + http2_session_state_buffer_ = pointer; +} + +inline double* Environment::http2_stream_state_buffer() const { + CHECK_NE(http2_stream_state_buffer_, nullptr); + return http2_stream_state_buffer_; +} + +inline void Environment::set_http2_stream_state_buffer(double* pointer) { + CHECK_EQ(http2_stream_state_buffer_, nullptr); + http2_stream_state_buffer_ = pointer; +} + +inline uint32_t* Environment::http2_padding_buffer() const { + CHECK_NE(http2_padding_buffer_, nullptr); + return http2_padding_buffer_; +} + +inline void Environment::set_http2_padding_buffer(uint32_t* pointer) { + CHECK_EQ(http2_padding_buffer_, nullptr); + http2_padding_buffer_ = pointer; +} inline char* Environment::http_parser_buffer() const { return http_parser_buffer_; @@ -487,6 +543,15 @@ inline void Environment::set_fs_stats_field_array(double* fields) { fs_stats_field_array_ = fields; } +inline char* Environment::http2_socket_buffer() const { + return http2_socket_buffer_; +} + +inline void Environment::set_http2_socket_buffer(char* buffer) { + CHECK_EQ(http2_socket_buffer_, nullptr); // Should be set only once. + http2_socket_buffer_ = buffer; +} + inline IsolateData* Environment::isolate_data() const { return isolate_data_; } diff --git a/src/env.h b/src/env.h index e1375080d17f07..59fe10448a59fd 100644 --- a/src/env.h +++ b/src/env.h @@ -104,6 +104,7 @@ namespace node { V(configurable_string, "configurable") \ V(cwd_string, "cwd") \ V(dest_string, "dest") \ + V(destroy_string, "destroy") \ V(detached_string, "detached") \ V(disposed_string, "_disposed") \ V(dns_a_string, "A") \ @@ -117,11 +118,13 @@ namespace node { V(dns_srv_string, "SRV") \ V(dns_txt_string, "TXT") \ V(domain_string, "domain") \ + V(emit_string, "emit") \ V(emitting_top_level_domain_error_string, "_emittingTopLevelDomainError") \ V(exchange_string, "exchange") \ V(enumerable_string, "enumerable") \ V(idle_string, "idle") \ V(irq_string, "irq") \ + V(enablepush_string, "enablePush") \ V(encoding_string, "encoding") \ V(enter_string, "enter") \ V(entries_string, "entries") \ @@ -148,8 +151,11 @@ namespace node { V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \ V(gid_string, "gid") \ V(handle_string, "handle") \ + V(heap_total_string, "heapTotal") \ + V(heap_used_string, "heapUsed") \ V(homedir_string, "homedir") \ V(hostmaster_string, "hostmaster") \ + V(id_string, "id") \ V(ignore_string, "ignore") \ V(immediate_callback_string, "_immediateCallback") \ V(infoaccess_string, "infoAccess") \ @@ -174,6 +180,7 @@ namespace node { V(netmask_string, "netmask") \ V(nice_string, "nice") \ V(nsname_string, "nsname") \ + V(nexttick_string, "nextTick") \ V(ocsp_request_string, "OCSPRequest") \ V(onchange_string, "onchange") \ V(onclienthello_string, "onclienthello") \ @@ -182,19 +189,27 @@ namespace node { V(ondone_string, "ondone") \ V(onerror_string, "onerror") \ V(onexit_string, "onexit") \ + V(onframeerror_string, "onframeerror") \ + V(ongetpadding_string, "ongetpadding") \ V(onhandshakedone_string, "onhandshakedone") \ V(onhandshakestart_string, "onhandshakestart") \ + V(onheaders_string, "onheaders") \ V(onmessage_string, "onmessage") \ V(onnewsession_string, "onnewsession") \ V(onnewsessiondone_string, "onnewsessiondone") \ V(onocspresponse_string, "onocspresponse") \ + V(ongoawaydata_string, "ongoawaydata") \ + V(onpriority_string, "onpriority") \ V(onread_string, "onread") \ V(onreadstart_string, "onreadstart") \ V(onreadstop_string, "onreadstop") \ V(onselect_string, "onselect") \ + V(onsettings_string, "onsettings") \ V(onshutdown_string, "onshutdown") \ V(onsignal_string, "onsignal") \ V(onstop_string, "onstop") \ + V(onstreamclose_string, "onstreamclose") \ + V(ontrailers_string, "ontrailers") \ V(onwrite_string, "onwrite") \ V(output_string, "output") \ V(order_string, "order") \ @@ -234,6 +249,7 @@ namespace node { V(stack_string, "stack") \ V(status_string, "status") \ V(stdio_string, "stdio") \ + V(stream_string, "stream") \ V(subject_string, "subject") \ V(subjectaltname_string, "subjectaltname") \ V(sys_string, "sys") \ @@ -262,7 +278,7 @@ namespace node { V(write_host_object_string, "_writeHostObject") \ V(write_queue_size_string, "writeQueueSize") \ V(x_forwarded_string, "x-forwarded-for") \ - V(zero_return_string, "ZERO_RETURN") \ + V(zero_return_string, "ZERO_RETURN") #define ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) \ V(as_external, v8::External) \ @@ -579,8 +595,25 @@ class Environment { inline double* heap_space_statistics_buffer() const; inline void set_heap_space_statistics_buffer(double* pointer); + inline uint32_t* http2_settings_buffer() const; + inline void set_http2_settings_buffer(uint32_t* pointer); + + inline uint32_t* http2_options_buffer() const; + inline void set_http2_options_buffer(uint32_t* pointer); + + inline double* http2_session_state_buffer() const; + inline void set_http2_session_state_buffer(double* pointer); + + inline double* http2_stream_state_buffer() const; + inline void set_http2_stream_state_buffer(double* pointer); + + inline uint32_t* http2_padding_buffer() const; + inline void set_http2_padding_buffer(uint32_t* pointer); + inline char* http_parser_buffer() const; inline void set_http_parser_buffer(char* buffer); + inline char* http2_socket_buffer() const; + inline void set_http2_socket_buffer(char* buffer); inline double* fs_stats_field_array() const; inline void set_fs_stats_field_array(double* fields); @@ -686,8 +719,14 @@ class Environment { double* heap_statistics_buffer_ = nullptr; double* heap_space_statistics_buffer_ = nullptr; + uint32_t* http2_settings_buffer_ = nullptr; + uint32_t* http2_options_buffer_ = nullptr; + double* http2_session_state_buffer_ = nullptr; + double* http2_stream_state_buffer_ = nullptr; + uint32_t* http2_padding_buffer_ = nullptr; char* http_parser_buffer_; + char* http2_socket_buffer_; double* fs_stats_field_array_; diff --git a/src/freelist.h b/src/freelist.h new file mode 100644 index 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 741b5dea82b7ba..5b1b9cadd93cb3 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, @@ -3648,6 +3656,7 @@ static void PrintHelp() { " --abort-on-uncaught-exception\n" " aborting instead of exiting causes a\n" " core file to be generated for analysis\n" + " --expose-http2 enable experimental HTTP2 support\n" " --trace-warnings show stack traces on process warnings\n" " --redirect-warnings=file\n" " write warnings to file instead of\n" @@ -3768,6 +3777,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--throw-deprecation", "--no-warnings", "--napi-modules", + "--expose-http2", "--trace-warnings", "--redirect-warnings", "--trace-sync-io", @@ -3965,6 +3975,9 @@ static void ParseArgs(int* argc, } else if (strcmp(arg, "--expose-internals") == 0 || strcmp(arg, "--expose_internals") == 0) { config_expose_internals = true; + } else if (strcmp(arg, "--expose-http2") == 0 || + strcmp(arg, "--expose_http2") == 0) { + config_expose_http2 = true; } else if (strcmp(arg, "-") == 0) { break; } else if (strcmp(arg, "--") == 0) { diff --git a/src/node.h b/src/node.h index b1d179bd1604bc..59728685f713db 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 d6bdf9b5ba4399..5d437fa3025739 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -83,6 +83,9 @@ extern std::string openssl_config; // that is used by lib/module.js extern bool config_preserve_symlinks; +// Set in node.cc by ParseArgs when --expose-http2 is used. +extern bool config_expose_http2; + // Set in node.cc by ParseArgs when --expose-internals or --expose_internals is // used. // Used in node_config.cc to set a constant on process.binding('config') diff --git a/src/stream_base.cc b/src/stream_base.cc index 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 From 998a4adde2967ee1feab392a752b421138ed9713 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:29:42 -0700 Subject: [PATCH 05/26] http2: add tests and benchmarks --- benchmark/README.md | 6 + benchmark/_http-benchmarkers.js | 55 +++- benchmark/http2/respond-with-fd.js | 43 +++ benchmark/http2/simple.js | 38 +++ test/common/index.js | 9 + test/parallel/test-async-wrap-getasyncid.js | 5 + .../test-dgram-bind-default-address.js | 0 test/parallel/test-http2-binding.js | 229 ++++++++++++++++ test/parallel/test-http2-client-data-end.js | 90 +++++++ ...est-http2-client-destroy-before-connect.js | 28 ++ ...est-http2-client-destroy-before-request.js | 28 ++ test/parallel/test-http2-client-destroy.js | 54 ++++ ...st-http2-client-priority-before-connect.js | 37 +++ ...t-http2-client-rststream-before-connect.js | 34 +++ .../test-http2-client-set-priority.js | 49 ++++ ...st-http2-client-settings-before-connect.js | 63 +++++ ...st-http2-client-shutdown-before-connect.js | 23 ++ .../test-http2-client-socket-destroy.js | 46 ++++ ...p2-client-stream-destroy-before-connect.js | 63 +++++ .../test-http2-client-unescaped-path.js | 37 +++ test/parallel/test-http2-client-upload.js | 44 ++++ .../test-http2-client-write-before-connect.js | 53 ++++ ...test-http2-compat-serverrequest-headers.js | 70 +++++ .../test-http2-compat-serverrequest.js | 52 ++++ ...ompat-serverresponse-createpushresponse.js | 79 ++++++ .../test-http2-compat-serverresponse-end.js | 77 ++++++ ...st-http2-compat-serverresponse-finished.js | 37 +++ ...ttp2-compat-serverresponse-flushheaders.js | 43 +++ ...est-http2-compat-serverresponse-headers.js | 83 ++++++ ...-http2-compat-serverresponse-statuscode.js | 76 ++++++ ...tp2-compat-serverresponse-statusmessage.js | 52 ++++ ...http2-compat-serverresponse-write-no-cb.js | 98 +++++++ ...t-http2-compat-serverresponse-writehead.js | 44 ++++ test/parallel/test-http2-connect-method.js | 71 +++++ test/parallel/test-http2-connect.js | 29 ++ test/parallel/test-http2-cookies.js | 62 +++++ .../test-http2-create-client-connect.js | 88 +++++++ ...test-http2-create-client-secure-session.js | 75 ++++++ .../test-http2-create-client-session.js | 61 +++++ test/parallel/test-http2-date-header.js | 28 ++ test/parallel/test-http2-dont-override.js | 48 ++++ test/parallel/test-http2-getpackedsettings.js | 131 +++++++++ test/parallel/test-http2-goaway-opaquedata.js | 38 +++ test/parallel/test-http2-head-request.js | 57 ++++ test/parallel/test-http2-https-fallback.js | 146 +++++++++++ test/parallel/test-http2-info-headers.js | 85 ++++++ .../test-http2-max-concurrent-streams.js | 67 +++++ test/parallel/test-http2-methods.js | 48 ++++ .../test-http2-misused-pseudoheaders.js | 61 +++++ .../test-http2-multi-content-length.js | 58 ++++ test/parallel/test-http2-multiheaders.js | 60 +++++ test/parallel/test-http2-multiplex.js | 59 +++++ test/parallel/test-http2-noflag.js | 8 + ...-http2-options-max-headers-block-length.js | 48 ++++ ...test-http2-options-max-reserved-streams.js | 73 ++++++ test/parallel/test-http2-padding-callback.js | 50 ++++ test/parallel/test-http2-priority-event.js | 60 +++++ test/parallel/test-http2-respond-file-204.js | 41 +++ test/parallel/test-http2-respond-file-304.js | 44 ++++ .../test-http2-respond-file-compat.js | 23 ++ .../test-http2-respond-file-fd-invalid.js | 37 +++ test/parallel/test-http2-respond-file-fd.js | 46 ++++ test/parallel/test-http2-respond-file-push.js | 81 ++++++ test/parallel/test-http2-respond-file.js | 51 ++++ .../parallel/test-http2-response-splitting.js | 75 ++++++ test/parallel/test-http2-serve-file.js | 82 ++++++ ...-http2-server-destroy-before-additional.js | 38 +++ .../test-http2-server-destroy-before-push.js | 38 +++ ...est-http2-server-destroy-before-respond.js | 38 +++ .../test-http2-server-destroy-before-write.js | 38 +++ .../test-http2-server-push-disabled.js | 53 ++++ .../parallel/test-http2-server-push-stream.js | 58 ++++ .../test-http2-server-rst-before-respond.js | 45 ++++ test/parallel/test-http2-server-rst-stream.js | 72 +++++ test/parallel/test-http2-server-set-header.js | 36 +++ ...st-http2-server-shutdown-before-respond.js | 32 +++ .../test-http2-server-socket-destroy.js | 57 ++++ test/parallel/test-http2-server-startup.js | 78 ++++++ test/parallel/test-http2-session-settings.js | 110 ++++++++ .../test-http2-session-stream-state.js | 97 +++++++ test/parallel/test-http2-single-headers.js | 59 +++++ .../test-http2-status-code-invalid.js | 40 +++ test/parallel/test-http2-status-code.js | 40 +++ test/parallel/test-http2-timeouts.js | 32 +++ test/parallel/test-http2-too-many-settings.js | 60 +++++ test/parallel/test-http2-trailers.js | 44 ++++ test/parallel/test-http2-util-asserts.js | 43 +++ test/parallel/test-http2-util-headers-list.js | 248 ++++++++++++++++++ test/parallel/test-http2-window-size.js | 102 +++++++ test/parallel/test-http2-withflag.js | 7 + test/parallel/test-http2-write-callbacks.js | 36 +++ .../parallel/test-http2-write-empty-string.js | 40 +++ test/parallel/test-http2-zero-length-write.js | 50 ++++ test/parallel/test-process-versions.js | 2 +- .../test-tls-disable-renegotiation.js | 0 95 files changed, 5327 insertions(+), 2 deletions(-) create mode 100644 benchmark/http2/respond-with-fd.js create mode 100644 benchmark/http2/simple.js mode change 100755 => 100644 test/parallel/test-dgram-bind-default-address.js create mode 100644 test/parallel/test-http2-binding.js create mode 100644 test/parallel/test-http2-client-data-end.js create mode 100644 test/parallel/test-http2-client-destroy-before-connect.js create mode 100644 test/parallel/test-http2-client-destroy-before-request.js create mode 100644 test/parallel/test-http2-client-destroy.js create mode 100644 test/parallel/test-http2-client-priority-before-connect.js create mode 100644 test/parallel/test-http2-client-rststream-before-connect.js create mode 100644 test/parallel/test-http2-client-set-priority.js create mode 100644 test/parallel/test-http2-client-settings-before-connect.js create mode 100644 test/parallel/test-http2-client-shutdown-before-connect.js create mode 100644 test/parallel/test-http2-client-socket-destroy.js create mode 100644 test/parallel/test-http2-client-stream-destroy-before-connect.js create mode 100644 test/parallel/test-http2-client-unescaped-path.js create mode 100644 test/parallel/test-http2-client-upload.js create mode 100644 test/parallel/test-http2-client-write-before-connect.js create mode 100644 test/parallel/test-http2-compat-serverrequest-headers.js create mode 100644 test/parallel/test-http2-compat-serverrequest.js create mode 100644 test/parallel/test-http2-compat-serverresponse-createpushresponse.js create mode 100644 test/parallel/test-http2-compat-serverresponse-end.js create mode 100644 test/parallel/test-http2-compat-serverresponse-finished.js create mode 100644 test/parallel/test-http2-compat-serverresponse-flushheaders.js create mode 100644 test/parallel/test-http2-compat-serverresponse-headers.js create mode 100644 test/parallel/test-http2-compat-serverresponse-statuscode.js create mode 100644 test/parallel/test-http2-compat-serverresponse-statusmessage.js create mode 100644 test/parallel/test-http2-compat-serverresponse-write-no-cb.js create mode 100644 test/parallel/test-http2-compat-serverresponse-writehead.js create mode 100644 test/parallel/test-http2-connect-method.js create mode 100644 test/parallel/test-http2-connect.js create mode 100644 test/parallel/test-http2-cookies.js create mode 100644 test/parallel/test-http2-create-client-connect.js create mode 100644 test/parallel/test-http2-create-client-secure-session.js create mode 100644 test/parallel/test-http2-create-client-session.js create mode 100644 test/parallel/test-http2-date-header.js create mode 100644 test/parallel/test-http2-dont-override.js create mode 100644 test/parallel/test-http2-getpackedsettings.js create mode 100644 test/parallel/test-http2-goaway-opaquedata.js create mode 100644 test/parallel/test-http2-head-request.js create mode 100644 test/parallel/test-http2-https-fallback.js create mode 100755 test/parallel/test-http2-info-headers.js create mode 100644 test/parallel/test-http2-max-concurrent-streams.js create mode 100644 test/parallel/test-http2-methods.js create mode 100644 test/parallel/test-http2-misused-pseudoheaders.js create mode 100644 test/parallel/test-http2-multi-content-length.js create mode 100644 test/parallel/test-http2-multiheaders.js create mode 100644 test/parallel/test-http2-multiplex.js create mode 100644 test/parallel/test-http2-noflag.js create mode 100644 test/parallel/test-http2-options-max-headers-block-length.js create mode 100644 test/parallel/test-http2-options-max-reserved-streams.js create mode 100644 test/parallel/test-http2-padding-callback.js create mode 100644 test/parallel/test-http2-priority-event.js create mode 100644 test/parallel/test-http2-respond-file-204.js create mode 100644 test/parallel/test-http2-respond-file-304.js create mode 100644 test/parallel/test-http2-respond-file-compat.js create mode 100644 test/parallel/test-http2-respond-file-fd-invalid.js create mode 100644 test/parallel/test-http2-respond-file-fd.js create mode 100644 test/parallel/test-http2-respond-file-push.js create mode 100644 test/parallel/test-http2-respond-file.js create mode 100644 test/parallel/test-http2-response-splitting.js create mode 100644 test/parallel/test-http2-serve-file.js create mode 100644 test/parallel/test-http2-server-destroy-before-additional.js create mode 100644 test/parallel/test-http2-server-destroy-before-push.js create mode 100644 test/parallel/test-http2-server-destroy-before-respond.js create mode 100644 test/parallel/test-http2-server-destroy-before-write.js create mode 100644 test/parallel/test-http2-server-push-disabled.js create mode 100644 test/parallel/test-http2-server-push-stream.js create mode 100644 test/parallel/test-http2-server-rst-before-respond.js create mode 100644 test/parallel/test-http2-server-rst-stream.js create mode 100644 test/parallel/test-http2-server-set-header.js create mode 100644 test/parallel/test-http2-server-shutdown-before-respond.js create mode 100644 test/parallel/test-http2-server-socket-destroy.js create mode 100644 test/parallel/test-http2-server-startup.js create mode 100644 test/parallel/test-http2-session-settings.js create mode 100644 test/parallel/test-http2-session-stream-state.js create mode 100644 test/parallel/test-http2-single-headers.js create mode 100644 test/parallel/test-http2-status-code-invalid.js create mode 100644 test/parallel/test-http2-status-code.js create mode 100644 test/parallel/test-http2-timeouts.js create mode 100644 test/parallel/test-http2-too-many-settings.js create mode 100644 test/parallel/test-http2-trailers.js create mode 100644 test/parallel/test-http2-util-asserts.js create mode 100644 test/parallel/test-http2-util-headers-list.js create mode 100644 test/parallel/test-http2-window-size.js create mode 100644 test/parallel/test-http2-withflag.js create mode 100644 test/parallel/test-http2-write-callbacks.js create mode 100644 test/parallel/test-http2-write-empty-string.js create mode 100644 test/parallel/test-http2-zero-length-write.js mode change 100755 => 100644 test/parallel/test-tls-disable-renegotiation.js diff --git a/benchmark/README.md b/benchmark/README.md index 17c733e6eb0b6c..dfdf319b9cb311 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -97,6 +97,12 @@ directory, see [the guide on benchmarks](../doc/guides/writing-and-running-bench Benchmarks for the http subsystem. + + http2 + + Benchmarks for the http2 subsystem. + + misc diff --git a/benchmark/_http-benchmarkers.js b/benchmark/_http-benchmarkers.js index 3f17f05f831170..f9359b13d5e9e4 100644 --- a/benchmark/_http-benchmarkers.js +++ b/benchmark/_http-benchmarkers.js @@ -111,10 +111,63 @@ class TestDoubleBenchmarker { } } +/** + * HTTP/2 Benchmarker + */ +class H2LoadBenchmarker { + constructor() { + this.name = 'h2load'; + this.executable = 'h2load'; + const result = child_process.spawnSync(this.executable, ['-h']); + this.present = !(result.error && result.error.code === 'ENOENT'); + } + + create(options) { + const args = []; + if (typeof options.requests === 'number') + args.push('-n', options.requests); + if (typeof options.clients === 'number') + args.push('-c', options.clients); + if (typeof options.threads === 'number') + args.push('-t', options.threads); + if (typeof options.maxConcurrentStreams === 'number') + args.push('-m', options.maxConcurrentStreams); + if (typeof options.initialWindowSize === 'number') + args.push('-w', options.initialWindowSize); + if (typeof options.sessionInitialWindowSize === 'number') + args.push('-W', options.sessionInitialWindowSize); + if (typeof options.rate === 'number') + args.push('-r', options.rate); + if (typeof options.ratePeriod === 'number') + args.push(`--rate-period=${options.ratePeriod}`); + if (typeof options.duration === 'number') + args.push('-T', options.duration); + if (typeof options.timeout === 'number') + args.push('-N', options.timeout); + if (typeof options.headerTableSize === 'number') + args.push(`--header-table-size=${options.headerTableSize}`); + if (typeof options.encoderHeaderTableSize === 'number') { + args.push( + `--encoder-header-table-size=${options.encoderHeaderTableSize}`); + } + const scheme = options.scheme || 'http'; + const host = options.host || '127.0.0.1'; + args.push(`${scheme}://${host}:${options.port}${options.path}`); + const child = child_process.spawn(this.executable, args); + return child; + } + + processResults(output) { + const rex = /(\d+(?:\.\d+)) req\/s/; + return rex.exec(output)[1]; + } +} + const http_benchmarkers = [ new WrkBenchmarker(), new AutocannonBenchmarker(), - new TestDoubleBenchmarker() + new TestDoubleBenchmarker(), + new H2LoadBenchmarker() ]; const benchmarkers = {}; diff --git a/benchmark/http2/respond-with-fd.js b/benchmark/http2/respond-with-fd.js new file mode 100644 index 00000000000000..d7a312c78bf4da --- /dev/null +++ b/benchmark/http2/respond-with-fd.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common.js'); +const PORT = common.PORT; +const path = require('path'); +const fs = require('fs'); + +const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html'); + +var bench = common.createBenchmark(main, { + requests: [100, 1000, 10000, 100000, 1000000], + streams: [100, 200, 1000], + clients: [1, 2] +}, { flags: ['--expose-http2', '--no-warnings'] }); + +function main(conf) { + + fs.open(file, 'r', (err, fd) => { + if (err) + throw err; + + const n = +conf.requests; + const m = +conf.streams; + const c = +conf.clients; + const http2 = require('http2'); + const server = http2.createServer(); + server.on('stream', (stream) => { + stream.respondWithFD(fd); + stream.on('error', (err) => {}); + }); + server.listen(PORT, () => { + bench.http({ + path: '/', + requests: n, + maxConcurrentStreams: m, + clients: c, + threads: c + }, () => server.close()); + }); + + }); + +} diff --git a/benchmark/http2/simple.js b/benchmark/http2/simple.js new file mode 100644 index 00000000000000..d12b20fc5ac773 --- /dev/null +++ b/benchmark/http2/simple.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common.js'); +const PORT = common.PORT; + +const path = require('path'); +const fs = require('fs'); + +const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html'); + +var bench = common.createBenchmark(main, { + requests: [100, 1000, 10000, 100000], + streams: [100, 200, 1000], + clients: [1, 2] +}, { flags: ['--expose-http2', '--no-warnings'] }); + +function main(conf) { + const n = +conf.requests; + const m = +conf.streams; + const c = +conf.clients; + const http2 = require('http2'); + const server = http2.createServer(); + server.on('stream', (stream) => { + const out = fs.createReadStream(file); + stream.respond(); + out.pipe(stream); + stream.on('error', (err) => {}); + }); + server.listen(PORT, () => { + bench.http({ + path: '/', + requests: n, + maxConcurrentStreams: m, + clients: c, + threads: c + }, () => { server.close(); }); + }); +} diff --git a/test/common/index.js b/test/common/index.js index fc14cdacacc587..a5ca4cec576e74 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -816,3 +816,12 @@ exports.hijackStdout = hijackStdWritable.bind(null, 'stdout'); exports.hijackStderr = hijackStdWritable.bind(null, 'stderr'); exports.restoreStdout = restoreWritable.bind(null, 'stdout'); exports.restoreStderr = restoreWritable.bind(null, 'stderr'); + +let fd = 2; +exports.firstInvalidFD = function firstInvalidFD() { + // Get first known bad file descriptor. + try { + while (fs.fstatSync(++fd)); + } catch (e) {} + return fd; +}; diff --git a/test/parallel/test-async-wrap-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js index 57d6f86ebe5ca8..ce51408a6b678b 100644 --- a/test/parallel/test-async-wrap-getasyncid.js +++ b/test/parallel/test-async-wrap-getasyncid.js @@ -19,6 +19,11 @@ const providers = Object.assign({}, process.binding('async_wrap').Providers); process.removeAllListeners('uncaughtException'); hooks.disable(); delete providers.NONE; // Should never be used. + + // TODO(jasnell): Test for these + delete providers.HTTP2SESSION; + delete providers.HTTP2SESSIONSHUTDOWNWRAP; + const obj_keys = Object.keys(providers); if (obj_keys.length > 0) process._rawDebug(obj_keys); diff --git a/test/parallel/test-dgram-bind-default-address.js b/test/parallel/test-dgram-bind-default-address.js old mode 100755 new mode 100644 diff --git a/test/parallel/test-http2-binding.js b/test/parallel/test-http2-binding.js new file mode 100644 index 00000000000000..c26549d3615981 --- /dev/null +++ b/test/parallel/test-http2-binding.js @@ -0,0 +1,229 @@ +// Flags: --expose-http2 +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.doesNotThrow(() => process.binding('http2')); + +const binding = process.binding('http2'); +const http2 = require('http2'); + +assert(binding.Http2Session); +assert.strictEqual(typeof binding.Http2Session, 'function'); + +const settings = http2.getDefaultSettings(); +assert.strictEqual(settings.headerTableSize, 4096); +assert.strictEqual(settings.enablePush, true); +assert.strictEqual(settings.initialWindowSize, 65535); +assert.strictEqual(settings.maxFrameSize, 16384); + +assert.strictEqual(binding.nghttp2ErrorString(-517), + 'GOAWAY has already been sent'); + +// assert constants are present +assert(binding.constants); +assert.strictEqual(typeof binding.constants, 'object'); +const constants = binding.constants; + +const expectedStatusCodes = { + HTTP_STATUS_CONTINUE: 100, + HTTP_STATUS_SWITCHING_PROTOCOLS: 101, + HTTP_STATUS_PROCESSING: 102, + HTTP_STATUS_OK: 200, + HTTP_STATUS_CREATED: 201, + HTTP_STATUS_ACCEPTED: 202, + HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: 203, + HTTP_STATUS_NO_CONTENT: 204, + HTTP_STATUS_RESET_CONTENT: 205, + HTTP_STATUS_PARTIAL_CONTENT: 206, + HTTP_STATUS_MULTI_STATUS: 207, + HTTP_STATUS_ALREADY_REPORTED: 208, + HTTP_STATUS_IM_USED: 226, + HTTP_STATUS_MULTIPLE_CHOICES: 300, + HTTP_STATUS_MOVED_PERMANENTLY: 301, + HTTP_STATUS_FOUND: 302, + HTTP_STATUS_SEE_OTHER: 303, + HTTP_STATUS_NOT_MODIFIED: 304, + HTTP_STATUS_USE_PROXY: 305, + HTTP_STATUS_TEMPORARY_REDIRECT: 307, + HTTP_STATUS_PERMANENT_REDIRECT: 308, + HTTP_STATUS_BAD_REQUEST: 400, + HTTP_STATUS_UNAUTHORIZED: 401, + HTTP_STATUS_PAYMENT_REQUIRED: 402, + HTTP_STATUS_FORBIDDEN: 403, + HTTP_STATUS_NOT_FOUND: 404, + HTTP_STATUS_METHOD_NOT_ALLOWED: 405, + HTTP_STATUS_NOT_ACCEPTABLE: 406, + HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: 407, + HTTP_STATUS_REQUEST_TIMEOUT: 408, + HTTP_STATUS_CONFLICT: 409, + HTTP_STATUS_GONE: 410, + HTTP_STATUS_LENGTH_REQUIRED: 411, + HTTP_STATUS_PRECONDITION_FAILED: 412, + HTTP_STATUS_PAYLOAD_TOO_LARGE: 413, + HTTP_STATUS_URI_TOO_LONG: 414, + HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: 415, + HTTP_STATUS_RANGE_NOT_SATISFIABLE: 416, + HTTP_STATUS_EXPECTATION_FAILED: 417, + HTTP_STATUS_TEAPOT: 418, + HTTP_STATUS_MISDIRECTED_REQUEST: 421, + HTTP_STATUS_UNPROCESSABLE_ENTITY: 422, + HTTP_STATUS_LOCKED: 423, + HTTP_STATUS_FAILED_DEPENDENCY: 424, + HTTP_STATUS_UNORDERED_COLLECTION: 425, + HTTP_STATUS_UPGRADE_REQUIRED: 426, + HTTP_STATUS_PRECONDITION_REQUIRED: 428, + HTTP_STATUS_TOO_MANY_REQUESTS: 429, + HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: 431, + HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: 451, + HTTP_STATUS_INTERNAL_SERVER_ERROR: 500, + HTTP_STATUS_NOT_IMPLEMENTED: 501, + HTTP_STATUS_BAD_GATEWAY: 502, + HTTP_STATUS_SERVICE_UNAVAILABLE: 503, + HTTP_STATUS_GATEWAY_TIMEOUT: 504, + HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: 505, + HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: 506, + HTTP_STATUS_INSUFFICIENT_STORAGE: 507, + HTTP_STATUS_LOOP_DETECTED: 508, + HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: 509, + HTTP_STATUS_NOT_EXTENDED: 510, + HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: 511 +}; + +const expectedHeaderNames = { + HTTP2_HEADER_STATUS: ':status', + HTTP2_HEADER_METHOD: ':method', + HTTP2_HEADER_AUTHORITY: ':authority', + HTTP2_HEADER_SCHEME: ':scheme', + HTTP2_HEADER_PATH: ':path', + HTTP2_HEADER_DATE: 'date', + HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset', + HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding', + HTTP2_HEADER_ACCEPT_LANGUAGE: 'accept-language', + HTTP2_HEADER_ACCEPT_RANGES: 'accept-ranges', + HTTP2_HEADER_ACCEPT: 'accept', + HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: 'access-control-allow-origin', + HTTP2_HEADER_AGE: 'age', + HTTP2_HEADER_ALLOW: 'allow', + HTTP2_HEADER_AUTHORIZATION: 'authorization', + HTTP2_HEADER_CACHE_CONTROL: 'cache-control', + HTTP2_HEADER_CONTENT_DISPOSITION: 'content-disposition', + HTTP2_HEADER_CONTENT_ENCODING: 'content-encoding', + HTTP2_HEADER_CONTENT_LANGUAGE: 'content-language', + HTTP2_HEADER_CONTENT_LENGTH: 'content-length', + HTTP2_HEADER_CONTENT_LOCATION: 'content-location', + HTTP2_HEADER_CONTENT_RANGE: 'content-range', + HTTP2_HEADER_CONTENT_TYPE: 'content-type', + HTTP2_HEADER_COOKIE: 'cookie', + HTTP2_HEADER_CONNECTION: 'connection', + HTTP2_HEADER_ETAG: 'etag', + HTTP2_HEADER_EXPECT: 'expect', + HTTP2_HEADER_EXPIRES: 'expires', + HTTP2_HEADER_FROM: 'from', + HTTP2_HEADER_HOST: 'host', + HTTP2_HEADER_IF_MATCH: 'if-match', + HTTP2_HEADER_IF_MODIFIED_SINCE: 'if-modified-since', + HTTP2_HEADER_IF_NONE_MATCH: 'if-none-match', + HTTP2_HEADER_IF_RANGE: 'if-range', + HTTP2_HEADER_IF_UNMODIFIED_SINCE: 'if-unmodified-since', + HTTP2_HEADER_LAST_MODIFIED: 'last-modified', + HTTP2_HEADER_LINK: 'link', + HTTP2_HEADER_LOCATION: 'location', + HTTP2_HEADER_MAX_FORWARDS: 'max-forwards', + HTTP2_HEADER_PREFER: 'prefer', + HTTP2_HEADER_PROXY_AUTHENTICATE: 'proxy-authenticate', + HTTP2_HEADER_PROXY_AUTHORIZATION: 'proxy-authorization', + HTTP2_HEADER_PROXY_CONNECTION: 'proxy-connection', + HTTP2_HEADER_RANGE: 'range', + HTTP2_HEADER_REFERER: 'referer', + HTTP2_HEADER_REFRESH: 'refresh', + HTTP2_HEADER_RETRY_AFTER: 'retry-after', + HTTP2_HEADER_SERVER: 'server', + HTTP2_HEADER_SET_COOKIE: 'set-cookie', + HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: 'strict-transport-security', + HTTP2_HEADER_TRANSFER_ENCODING: 'transfer-encoding', + HTTP2_HEADER_USER_AGENT: 'user-agent', + HTTP2_HEADER_VARY: 'vary', + HTTP2_HEADER_VIA: 'via', + HTTP2_HEADER_WWW_AUTHENTICATE: 'www-authenticate', + HTTP2_HEADER_KEEP_ALIVE: 'keep-alive', + HTTP2_HEADER_CONTENT_MD5: 'content-md5', + HTTP2_HEADER_TE: 'te', + HTTP2_HEADER_UPGRADE: 'upgrade', + HTTP2_HEADER_HTTP2_SETTINGS: 'http2-settings' +}; + +const expectedNGConstants = { + NGHTTP2_SESSION_SERVER: 0, + NGHTTP2_SESSION_CLIENT: 1, + NGHTTP2_STREAM_STATE_IDLE: 1, + NGHTTP2_STREAM_STATE_OPEN: 2, + NGHTTP2_STREAM_STATE_RESERVED_LOCAL: 3, + NGHTTP2_STREAM_STATE_RESERVED_REMOTE: 4, + NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: 5, + NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: 6, + NGHTTP2_STREAM_STATE_CLOSED: 7, + NGHTTP2_HCAT_REQUEST: 0, + NGHTTP2_HCAT_RESPONSE: 1, + NGHTTP2_HCAT_PUSH_RESPONSE: 2, + NGHTTP2_HCAT_HEADERS: 3, + NGHTTP2_NO_ERROR: 0, + NGHTTP2_PROTOCOL_ERROR: 1, + NGHTTP2_INTERNAL_ERROR: 2, + NGHTTP2_FLOW_CONTROL_ERROR: 3, + NGHTTP2_SETTINGS_TIMEOUT: 4, + NGHTTP2_STREAM_CLOSED: 8, + NGHTTP2_FRAME_SIZE_ERROR: 6, + NGHTTP2_REFUSED_STREAM: 7, + NGHTTP2_CANCEL: 8, + NGHTTP2_COMPRESSION_ERROR: 9, + NGHTTP2_CONNECT_ERROR: 10, + NGHTTP2_ENHANCE_YOUR_CALM: 11, + NGHTTP2_INADEQUATE_SECURITY: 12, + NGHTTP2_HTTP_1_1_REQUIRED: 13, + NGHTTP2_NV_FLAG_NONE: 0, + NGHTTP2_NV_FLAG_NO_INDEX: 1, + NGHTTP2_ERR_DEFERRED: -508, + NGHTTP2_ERR_NOMEM: -901, + NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: -509, + NGHTTP2_ERR_INVALID_ARGUMENT: -501, + NGHTTP2_ERR_STREAM_CLOSED: -510, + NGHTTP2_ERR_FRAME_SIZE_ERROR: -522, + NGHTTP2_FLAG_NONE: 0, + NGHTTP2_FLAG_END_STREAM: 1, + NGHTTP2_FLAG_END_HEADERS: 4, + NGHTTP2_FLAG_ACK: 1, + NGHTTP2_FLAG_PADDED: 8, + NGHTTP2_FLAG_PRIORITY: 32, + NGHTTP2_DEFAULT_WEIGHT: 16, + NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1, + NGHTTP2_SETTINGS_ENABLE_PUSH: 2, + NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3, + NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4, + NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5, + NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6 +}; + +const defaultSettings = { + DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096, + DEFAULT_SETTINGS_ENABLE_PUSH: 1, + DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535, + DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384 +}; + +for (const name of Object.keys(constants)) { + if (name.startsWith('HTTP_STATUS_')) { + assert.strictEqual(expectedStatusCodes[name], constants[name], + `Expected status code match for ${name}`); + } else if (name.startsWith('HTTP2_HEADER_')) { + assert.strictEqual(expectedHeaderNames[name], constants[name], + `Expected header name match for ${name}`); + } else if (name.startsWith('NGHTTP2_')) { + assert.strictEqual(expectedNGConstants[name], constants[name], + `Expected ng constant match for ${name}`); + } else if (name.startsWith('DEFAULT_SETTINGS_')) { + assert.strictEqual(defaultSettings[name], constants[name], + `Expected default setting match for ${name}`); + } +} diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js new file mode 100644 index 00000000000000..3bd72f138ba60e --- /dev/null +++ b/test/parallel/test-http2-client-data-end.js @@ -0,0 +1,90 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers, flags) => { + const port = server.address().port; + if (headers[':path'] === '/') { + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${port}`, + }, (push, headers) => { + push.respond({ + 'content-type': 'text/html', + ':status': 200, + 'x-push-data': 'pushed by server', + }); + push.write('pushed by server '); + // Sending in next immediate ensures that a second data frame + // will be sent to the client, which will cause the 'data' event + // to fire multiple times. + setImmediate(() => { + push.end('data'); + }); + stream.end('st'); + }); + } + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write('te'); +})); + + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const headers = { ':path': '/' }; + const client = http2.connect(`http://localhost:${port}`); + + const req = client.request(headers); + + let expected = 2; + function maybeClose() { + if (--expected === 0) { + server.close(); + client.destroy(); + } + } + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + })); + + client.on('stream', common.mustCall((stream, headers, flags) => { + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':path'], '/foobar'); + assert.strictEqual(headers[':authority'], `localhost:${port}`); + stream.on('push', common.mustCall((headers, flags) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + assert.strictEqual(headers['x-push-data'], 'pushed by server'); + })); + + stream.setEncoding('utf8'); + let pushData = ''; + stream.on('data', common.mustCall((d) => { + pushData += d; + }, 2)); + stream.on('end', common.mustCall(() => { + assert.strictEqual(pushData, 'pushed by server data'); + maybeClose(); + })); + })); + + let data = ''; + + req.setEncoding('utf8'); + req.on('data', common.mustCall((d) => data += d)); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'test'); + maybeClose(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-client-destroy-before-connect.js b/test/parallel/test-http2-client-destroy-before-connect.js new file mode 100644 index 00000000000000..1f6b087dd220b3 --- /dev/null +++ b/test/parallel/test-http2-client-destroy-before-connect.js @@ -0,0 +1,28 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + client.destroy(); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-destroy-before-request.js b/test/parallel/test-http2-client-destroy-before-request.js new file mode 100644 index 00000000000000..71519d5903b58f --- /dev/null +++ b/test/parallel/test-http2-client-destroy-before-request.js @@ -0,0 +1,28 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + client.destroy(); + + assert.throws(() => client.request({ ':path': '/' }), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SESSION', + message: /^The session has been destroyed$/ + })); + + server.close(); + +})); diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js new file mode 100644 index 00000000000000..56cfec5d65a223 --- /dev/null +++ b/test/parallel/test-http2-client-destroy.js @@ -0,0 +1,54 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const destroyCallbacks = [ + (client) => client.destroy(), + (client) => client.socket.destroy() + ]; + + let remaining = destroyCallbacks.length; + + destroyCallbacks.forEach((destroyCallback) => { + const client = h2.connect(`http://localhost:${port}`); + client.on('connect', common.mustCall(() => { + const socket = client.socket; + + assert(client.socket, 'client session has associated socket'); + assert(!client.destroyed, + 'client has not been destroyed before destroy is called'); + assert(!socket.destroyed, + 'socket has not been destroyed before destroy is called'); + + // Ensure that 'close' event is emitted + client.on('close', common.mustCall()); + + destroyCallback(client); + + assert(!client.socket, 'client.socket undefined after destroy is called'); + + // Must must be closed + client.on('close', common.mustCall(() => { + assert(client.destroyed); + })); + + // socket will close on process.nextTick + socket.on('close', common.mustCall(() => { + assert(socket.destroyed); + })); + + if (--remaining === 0) { + server.close(); + } + })); + }); +})); diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js new file mode 100644 index 00000000000000..68933b2d83bbf1 --- /dev/null +++ b/test/parallel/test-http2-client-priority-before-connect.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + client.priority(req, {}); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js new file mode 100644 index 00000000000000..33b6cb354fe225 --- /dev/null +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -0,0 +1,34 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + client.rstStream(req, 0); + assert.strictEqual(req.rstCode, 0); + + // make sure that destroy is called + req._destroy = common.mustCall(req._destroy.bind(req)); + + req.on('streamClosed', common.mustCall((code) => { + assert.strictEqual(req.destroyed, true); + assert.strictEqual(code, 0); + server.close(); + client.destroy(); + })); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall()); + req.end(); +})); diff --git a/test/parallel/test-http2-client-set-priority.js b/test/parallel/test-http2-client-set-priority.js new file mode 100644 index 00000000000000..314a88a63c2d16 --- /dev/null +++ b/test/parallel/test-http2-client-set-priority.js @@ -0,0 +1,49 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const checkWeight = (actual, expect) => { + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, headers, flags) => { + assert.strictEqual(stream.state.weight, expect); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('test'); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const headers = { ':path': '/' }; + const req = client.request(headers, { weight: actual }); + + req.on('data', common.mustCall(() => {})); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + })); +}; + +// when client weight is lower than 1, weight is 1 +checkWeight(-1, 1); +checkWeight(0, 1); + +// 1 - 256 is correct weight +checkWeight(1, 1); +checkWeight(16, 16); +checkWeight(256, 256); + +// when client weight is higher than 256, weight is 256 +checkWeight(257, 256); +checkWeight(512, 256); + +// when client weight is undefined, weight is default 16 +checkWeight(undefined, 16); diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js new file mode 100644 index 00000000000000..9391502479a7f9 --- /dev/null +++ b/test/parallel/test-http2-client-settings-before-connect.js @@ -0,0 +1,63 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + assert.throws(() => client.settings({headerTableSize: -1}), + RangeError); + assert.throws(() => client.settings({headerTableSize: 2 ** 32}), + RangeError); + assert.throws(() => client.settings({initialWindowSize: -1}), + RangeError); + assert.throws(() => client.settings({initialWindowSize: 2 ** 32}), + RangeError); + assert.throws(() => client.settings({maxFrameSize: 1}), + RangeError); + assert.throws(() => client.settings({maxFrameSize: 2 ** 24}), + RangeError); + assert.throws(() => client.settings({maxConcurrentStreams: -1}), + RangeError); + assert.throws(() => client.settings({maxConcurrentStreams: 2 ** 31}), + RangeError); + assert.throws(() => client.settings({maxHeaderListSize: -1}), + RangeError); + assert.throws(() => client.settings({maxHeaderListSize: 2 ** 32}), + RangeError); + ['a', 1, 0, null, {}].forEach((i) => { + assert.throws(() => client.settings({enablePush: i}), TypeError); + }); + + client.settings({ maxFrameSize: 1234567 }); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-shutdown-before-connect.js b/test/parallel/test-http2-client-shutdown-before-connect.js new file mode 100644 index 00000000000000..203963bf57721e --- /dev/null +++ b/test/parallel/test-http2-client-shutdown-before-connect.js @@ -0,0 +1,23 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + client.shutdown({graceful: true}, common.mustCall(() => { + server.close(); + client.destroy(); + })); + +})); diff --git a/test/parallel/test-http2-client-socket-destroy.js b/test/parallel/test-http2-client-socket-destroy.js new file mode 100644 index 00000000000000..fe2d92753172a8 --- /dev/null +++ b/test/parallel/test-http2-client-socket-destroy.js @@ -0,0 +1,46 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); +const body = + '

this is some data

'; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream) { + // The stream aborted event must have been triggered + stream.on('aborted', common.mustCall()); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write(body); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall(() => { + // send a premature socket close + client.socket.destroy(); + })); + req.on('data', common.mustNotCall()); + + req.on('end', common.mustCall(() => { + server.close(); + })); + + // On the client, the close event must call + client.on('close', common.mustCall()); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-stream-destroy-before-connect.js b/test/parallel/test-http2-client-stream-destroy-before-connect.js new file mode 100644 index 00000000000000..5ab0cac5082aed --- /dev/null +++ b/test/parallel/test-http2-client-stream-destroy-before-connect.js @@ -0,0 +1,63 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const NGHTTP2_INTERNAL_ERROR = h2.constants.NGHTTP2_INTERNAL_ERROR; + +const server = h2.createServer(); + +// Do not mustCall the server side callbacks, they may or may not be called +// depending on the OS. The determination is based largely on operating +// system specific timings +server.on('stream', (stream) => { + // Do not wrap in a must call or use common.expectsError (which now uses + // must call). The error may or may not be reported depending on operating + // system specific timings. + stream.on('error', (err) => { + if (err) { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); + assert.strictEqual(err.message, 'Stream closed with error code 2'); + } + }); + stream.respond({}); + stream.end(); +}); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + const err = new Error('test'); + req.destroy(err); + + req.on('error', common.mustCall((err) => { + const fn = err.code === 'ERR_HTTP2_STREAM_ERROR' ? + common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + }) : + common.expectsError({ + type: Error, + message: 'test' + }); + fn(err); + }, 2)); + + req.on('streamClosed', common.mustCall((code) => { + assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); + assert.strictEqual(code, NGHTTP2_INTERNAL_ERROR); + server.close(); + client.destroy(); + })); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall()); + +})); diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js new file mode 100644 index 00000000000000..d92d40492e204c --- /dev/null +++ b/test/parallel/test-http2-client-unescaped-path.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustNotCall()); + +const count = 32; + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = count; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + // nghttp2 will catch the bad header value for us. + function doTest(i) { + const req = client.request({ ':path': `bad${String.fromCharCode(i)}path` }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 1' + })); + req.on('streamClosed', common.mustCall(maybeClose)); + } + + for (let i = 0; i <= count; i += 1) + doTest(i); +})); diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js new file mode 100644 index 00000000000000..4ce7da878e1fd2 --- /dev/null +++ b/test/parallel/test-http2-client-upload.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const path = require('path'); + +const loc = path.join(common.fixturesDir, 'person.jpg'); +let fileData; + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustCall((err, data) => { + assert.ifError(err); + fileData = data; + + const server = http2.createServer(); + + server.on('stream', common.mustCall((stream) => { + let data = Buffer.alloc(0); + stream.on('data', (chunk) => data = Buffer.concat([data, chunk])); + stream.on('end', common.mustCall(() => { + assert.deepStrictEqual(data, fileData); + })); + stream.respond(); + stream.end(); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + fs.createReadStream(loc).pipe(req); + })); +})); diff --git a/test/parallel/test-http2-client-write-before-connect.js b/test/parallel/test-http2-client-write-before-connect.js new file mode 100644 index 00000000000000..f58fc5c43f69b6 --- /dev/null +++ b/test/parallel/test-http2-client-write-before-connect.js @@ -0,0 +1,53 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_METHOD, + HTTP2_METHOD_POST +} = h2.constants; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'some data more data'); + })); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); + req.write('some data '); + req.write('more data'); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-compat-serverrequest-headers.js b/test/parallel/test-http2-compat-serverrequest-headers.js new file mode 100644 index 00000000000000..32af86314b1675 --- /dev/null +++ b/test/parallel/test-http2-compat-serverrequest-headers.js @@ -0,0 +1,70 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerRequest should have header helpers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expected = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + 'foo-bar': 'abc123' + }; + + assert.strictEqual(request.method, expected[':method']); + assert.strictEqual(request.scheme, expected[':scheme']); + assert.strictEqual(request.path, expected[':path']); + assert.strictEqual(request.url, expected[':path']); + assert.strictEqual(request.authority, expected[':authority']); + + const headers = request.headers; + for (const [name, value] of Object.entries(expected)) { + assert.strictEqual(headers[name], value); + } + + const rawHeaders = request.rawHeaders; + for (const [name, value] of Object.entries(expected)) { + const position = rawHeaders.indexOf(name); + assert.notStrictEqual(position, -1); + assert.strictEqual(rawHeaders[position + 1], value); + } + + request.url = '/one'; + assert.strictEqual(request.url, '/one'); + assert.strictEqual(request.path, '/one'); + + request.path = '/two'; + assert.strictEqual(request.url, '/two'); + assert.strictEqual(request.path, '/two'); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + 'foo-bar': 'abc123' + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverrequest.js b/test/parallel/test-http2-compat-serverrequest.js new file mode 100644 index 00000000000000..d54f554848ce09 --- /dev/null +++ b/test/parallel/test-http2-compat-serverrequest.js @@ -0,0 +1,52 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const net = require('net'); + +// Http2ServerRequest should expose convenience properties + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expected = { + statusCode: null, + version: '2.0', + httpVersionMajor: 2, + httpVersionMinor: 0 + }; + + assert.strictEqual(request.statusCode, expected.statusCode); + assert.strictEqual(request.httpVersion, expected.version); + assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor); + assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor); + + assert.ok(request.socket instanceof net.Socket); + assert.ok(request.connection instanceof net.Socket); + assert.strictEqual(request.socket, request.connection); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js new file mode 100644 index 00000000000000..68e438d62ff96d --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -0,0 +1,79 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Push a request & response + +const pushExpect = 'This is a server-initiated response'; +const servExpect = 'This is a client-initiated response'; + +const server = h2.createServer((request, response) => { + assert.strictEqual(response.stream.id % 2, 1); + response.write(servExpect); + + response.createPushResponse({ + ':path': '/pushed', + ':method': 'GET' + }, common.mustCall((error, push) => { + assert.ifError(error); + assert.strictEqual(push.stream.id % 2, 0); + push.end(pushExpect); + response.end(); + })); +}); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + const client = h2.connect(`http://localhost:${port}`, common.mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + }; + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + const req = client.request(headers); + + client.on('stream', common.mustCall((pushStream, headers) => { + assert.strictEqual(headers[':path'], '/pushed'); + assert.strictEqual(headers[':method'], 'GET'); + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':authority'], `localhost:${port}`); + + let actual = ''; + pushStream.on('push', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert(headers['date']); + })); + pushStream.setEncoding('utf8'); + pushStream.on('data', (chunk) => actual += chunk); + pushStream.on('end', common.mustCall(() => { + assert.strictEqual(actual, pushExpect); + maybeClose(); + })); + })); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert(headers['date']); + })); + + let actual = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(actual, servExpect); + maybeClose(); + })); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js new file mode 100644 index 00000000000000..1274f3d6b3c148 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-end.js @@ -0,0 +1,77 @@ +// Flags: --expose-http2 +'use strict'; + +const { strictEqual } = require('assert'); +const { mustCall, mustNotCall } = require('../common'); +const { + createServer, + connect, + constants: { + HTTP2_HEADER_STATUS, + HTTP_STATUS_OK + } +} = require('http2'); + +{ + // Http2ServerResponse.end callback is called only the first time, + // but may be invoked repeatedly without throwing errors. + const server = createServer(mustCall((request, response) => { + response.end(mustCall(() => { + server.close(); + })); + response.end(mustNotCall()); + })); + server.listen(0, mustCall(() => { + const { port } = server.address(); + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('data', mustNotCall()); + request.on('end', mustCall(() => client.destroy())); + request.end(); + request.resume(); + })); + })); +} + +{ + // Http2ServerResponse.end is not necessary on HEAD requests since the stream + // is already closed. Headers, however, can still be sent to the client. + const server = createServer(mustCall((request, response) => { + strictEqual(response.finished, true); + response.writeHead(HTTP_STATUS_OK, { foo: 'bar' }); + response.flushHeaders(); + response.end(mustNotCall()); + })); + server.listen(0, mustCall(() => { + const { port } = server.address(); + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'HEAD', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', mustCall((headers, flags) => { + strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK); + strictEqual(flags, 5); // the end of stream flag is set + strictEqual(headers.foo, 'bar'); + })); + request.on('data', mustNotCall()); + request.on('end', mustCall(() => { + client.destroy(); + server.close(); + })); + request.end(); + request.resume(); + })); + })); +} diff --git a/test/parallel/test-http2-compat-serverresponse-finished.js b/test/parallel/test-http2-compat-serverresponse-finished.js new file mode 100644 index 00000000000000..e5739e5ac3e2f6 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-finished.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.finished + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.on('finish', common.mustCall(function() { + server.close(); + })); + assert.strictEqual(response.finished, false); + response.end(); + assert.strictEqual(response.finished, true); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/parallel/test-http2-compat-serverresponse-flushheaders.js new file mode 100644 index 00000000000000..4bfe4909121c19 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-flushheaders.js @@ -0,0 +1,43 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.flushHeaders + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.flushHeaders(); + response.flushHeaders(); // Idempotent + response.writeHead(400, {'foo-bar': 'abc123'}); // Ignored + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers, flags) { + assert.strictEqual(headers['foo-bar'], undefined); + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-headers.js b/test/parallel/test-http2-compat-serverresponse-headers.js new file mode 100644 index 00000000000000..28bd36ce2e46c5 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-headers.js @@ -0,0 +1,83 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse should support checking and reading custom headers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const real = 'foo-bar'; + const fake = 'bar-foo'; + const denormalised = ` ${real.toUpperCase()}\n\t`; + const expectedValue = 'abc123'; + + response.setHeader(real, expectedValue); + + assert.strictEqual(response.hasHeader(real), true); + assert.strictEqual(response.hasHeader(fake), false); + assert.strictEqual(response.hasHeader(denormalised), true); + assert.strictEqual(response.getHeader(real), expectedValue); + assert.strictEqual(response.getHeader(denormalised), expectedValue); + assert.strictEqual(response.getHeader(fake), undefined); + + response.removeHeader(fake); + assert.strictEqual(response.hasHeader(fake), false); + + response.setHeader(real, expectedValue); + assert.strictEqual(response.getHeader(real), expectedValue); + assert.strictEqual(response.hasHeader(real), true); + response.removeHeader(real); + assert.strictEqual(response.hasHeader(real), false); + + response.setHeader(denormalised, expectedValue); + assert.strictEqual(response.getHeader(denormalised), expectedValue); + assert.strictEqual(response.hasHeader(denormalised), true); + response.removeHeader(denormalised); + assert.strictEqual(response.hasHeader(denormalised), false); + + assert.throws(function() { + response.setHeader(':status', 'foobar'); + }, Error); + assert.throws(function() { + response.setHeader(real, null); + }, TypeError); + assert.throws(function() { + response.setHeader(real, undefined); + }, TypeError); + + response.setHeader(real, expectedValue); + const expectedHeaderNames = [real]; + assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames); + const expectedHeaders = {[real]: expectedValue}; + assert.deepStrictEqual(response.getHeaders(), expectedHeaders); + + response.getHeaders()[fake] = fake; + assert.strictEqual(response.hasHeader(fake), false); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-statuscode.js b/test/parallel/test-http2-compat-serverresponse-statuscode.js new file mode 100644 index 00000000000000..201a63c379bc8b --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statuscode.js @@ -0,0 +1,76 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse should have a statusCode property + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expectedDefaultStatusCode = 200; + const realStatusCodes = { + continue: 100, + ok: 200, + multipleChoices: 300, + badRequest: 400, + internalServerError: 500 + }; + const fakeStatusCodes = { + tooLow: 99, + tooHigh: 600 + }; + + assert.strictEqual(response.statusCode, expectedDefaultStatusCode); + + assert.doesNotThrow(function() { + response.statusCode = realStatusCodes.ok; + response.statusCode = realStatusCodes.multipleChoices; + response.statusCode = realStatusCodes.badRequest; + response.statusCode = realStatusCodes.internalServerError; + }); + + assert.throws(function() { + response.statusCode = realStatusCodes.continue; + }, common.expectsError({ + code: 'ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + type: RangeError + })); + assert.throws(function() { + response.statusCode = fakeStatusCodes.tooLow; + }, common.expectsError({ + code: 'ERR_HTTP2_STATUS_INVALID', + type: RangeError + })); + assert.throws(function() { + response.statusCode = fakeStatusCodes.tooHigh; + }, common.expectsError({ + code: 'ERR_HTTP2_STATUS_INVALID', + type: RangeError + })); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage.js b/test/parallel/test-http2-compat-serverresponse-statusmessage.js new file mode 100644 index 00000000000000..08822c99390835 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage.js @@ -0,0 +1,52 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.writeHead should accept an optional status message + +const unsupportedWarned = common.mustCall(1); +process.on('warning', ({name, message}) => { + const expectedMessage = + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)'; + if (name === 'UnsupportedWarning' && message === expectedMessage) + unsupportedWarned(); +}); + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const statusCode = 200; + const statusMessage = 'OK'; + const headers = {'foo-bar': 'abc123'}; + response.writeHead(statusCode, statusMessage, headers); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['foo-bar'], 'abc123'); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js new file mode 100644 index 00000000000000..2428cebd8bcf09 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js @@ -0,0 +1,98 @@ +// Flags: --expose-http2 +'use strict'; + +const { throws } = require('assert'); +const { mustCall, mustNotCall, expectsError } = require('../common'); +const { createServer, connect } = require('http2'); + +// Http2ServerResponse.write does not imply there is a callback + +const expectedError = expectsError({ + code: 'ERR_HTTP2_STREAM_CLOSED', + message: 'The stream is already closed' +}, 2); + +{ + const server = createServer(); + server.listen(0, mustCall(() => { + const port = server.address().port; + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.end(); + request.resume(); + })); + + server.once('request', mustCall((request, response) => { + client.destroy(); + response.stream.session.on('close', mustCall(() => { + response.on('error', mustNotCall()); + throws( + () => { response.write('muahaha'); }, + /The stream is already closed/ + ); + server.close(); + })); + })); + })); +} + +{ + const server = createServer(); + server.listen(0, mustCall(() => { + const port = server.address().port; + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'get', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.end(); + request.resume(); + })); + + server.once('request', mustCall((request, response) => { + client.destroy(); + response.stream.session.on('close', mustCall(() => { + response.write('muahaha', mustCall(expectedError)); + server.close(); + })); + })); + })); +} + +{ + const server = createServer(); + server.listen(0, mustCall(() => { + const port = server.address().port; + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'get', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.end(); + request.resume(); + })); + + server.once('request', mustCall((request, response) => { + response.stream.session.on('close', mustCall(() => { + response.write('muahaha', 'utf8', mustCall(expectedError)); + server.close(); + })); + client.destroy(); + })); + })); +} diff --git a/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/parallel/test-http2-compat-serverresponse-writehead.js new file mode 100644 index 00000000000000..b4c531d3393282 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-writehead.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.writeHead should override previous headers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.setHeader('foo-bar', 'def456'); + response.writeHead(500); + response.writeHead(418, {'foo-bar': 'abc123'}); // Override + + response.on('finish', common.mustCall(function() { + assert.doesNotThrow(() => { response.writeHead(300); }); + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers['foo-bar'], 'abc123'); + assert.strictEqual(headers[':status'], 418); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-connect-method.js b/test/parallel/test-http2-connect-method.js new file mode 100644 index 00000000000000..05ff96a3cd1320 --- /dev/null +++ b/test/parallel/test-http2-connect-method.js @@ -0,0 +1,71 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); +const http2 = require('http2'); +const { URL } = require('url'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + NGHTTP2_CONNECT_ERROR +} = http2.constants; + +const server = net.createServer(common.mustCall((socket) => { + let data = ''; + socket.setEncoding('utf8'); + socket.on('data', (chunk) => data += chunk); + socket.on('end', common.mustCall(() => { + assert.strictEqual(data, 'hello'); + })); + socket.on('close', common.mustCall()); + socket.end('hello'); +})); + +server.listen(0, common.mustCall(() => { + + const port = server.address().port; + + const proxy = http2.createServer(); + proxy.on('stream', common.mustCall((stream, headers) => { + if (headers[HTTP2_HEADER_METHOD] !== 'CONNECT') { + stream.rstWithRefused(); + return; + } + const auth = new URL(`tcp://${headers[HTTP2_HEADER_AUTHORITY]}`); + assert.strictEqual(auth.hostname, 'localhost'); + assert.strictEqual(+auth.port, port); + const socket = net.connect(auth.port, auth.hostname, () => { + stream.respond(); + socket.pipe(stream); + stream.pipe(socket); + }); + socket.on('close', common.mustCall()); + socket.on('error', (error) => { + stream.rstStream(NGHTTP2_CONNECT_ERROR); + }); + })); + + proxy.listen(0, () => { + const client = http2.connect(`http://localhost:${proxy.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_METHOD]: 'CONNECT', + [HTTP2_HEADER_AUTHORITY]: `localhost:${port}`, + }); + + req.on('response', common.mustCall()); + let data = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => data += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'hello'); + client.destroy(); + proxy.close(); + server.close(); + })); + req.end('hello'); + }); +})); diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js new file mode 100644 index 00000000000000..305ea034c902e4 --- /dev/null +++ b/test/parallel/test-http2-connect.js @@ -0,0 +1,29 @@ +// Flags: --expose-http2 +'use strict'; + +const { mustCall } = require('../common'); +const { doesNotThrow } = require('assert'); +const { createServer, connect } = require('http2'); + +const server = createServer(); +server.listen(0, mustCall(() => { + const authority = `http://localhost:${server.address().port}`; + const options = {}; + const listener = () => mustCall(); + + const clients = new Set(); + doesNotThrow(() => clients.add(connect(authority))); + doesNotThrow(() => clients.add(connect(authority, options))); + doesNotThrow(() => clients.add(connect(authority, options, listener()))); + doesNotThrow(() => clients.add(connect(authority, listener()))); + + for (const client of clients) { + client.once('connect', mustCall((headers) => { + client.destroy(); + clients.delete(client); + if (clients.size === 0) { + server.close(); + } + })); + } +})); diff --git a/test/parallel/test-http2-cookies.js b/test/parallel/test-http2-cookies.js new file mode 100644 index 00000000000000..297b3966df9f18 --- /dev/null +++ b/test/parallel/test-http2-cookies.js @@ -0,0 +1,62 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const setCookie = [ + 'a=b', + 'c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly' +]; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + assert(Array.isArray(headers.abc)); + assert.strictEqual(headers.abc.length, 3); + assert.strictEqual(headers.abc[0], '1'); + assert.strictEqual(headers.abc[1], '2'); + assert.strictEqual(headers.abc[2], '3'); + assert.strictEqual(typeof headers.cookie, 'string'); + assert.strictEqual(headers.cookie, 'a=b; c=d; e=f'); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200, + 'set-cookie': setCookie + }); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + ':path': '/', + abc: [1, 2, 3], + cookie: ['a=b', 'c=d', 'e=f'], + }); + req.resume(); + + req.on('response', common.mustCall((headers) => { + assert(Array.isArray(headers['set-cookie'])); + assert.deepStrictEqual(headers['set-cookie'], setCookie, + 'set-cookie header does not match'); + })); + + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js new file mode 100644 index 00000000000000..8173dc3d08658f --- /dev/null +++ b/test/parallel/test-http2-create-client-connect.js @@ -0,0 +1,88 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests http2.connect() + +const common = require('../common'); +const fs = require('fs'); +const h2 = require('http2'); +const path = require('path'); +const url = require('url'); +const URL = url.URL; + +{ + const server = h2.createServer(); + server.listen(0); + + server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const items = [ + [`http://localhost:${port}`], + [new URL(`http://localhost:${port}`)], + [url.parse(`http://localhost:${port}`)], + [{port: port}, {protocol: 'http:'}], + [{port: port, hostname: '127.0.0.1'}, {protocol: 'http:'}] + ]; + + let count = items.length; + + const maybeClose = common.mustCall((client) => { + client.destroy(); + if (--count === 0) { + setImmediate(() => server.close()); + } + }, items.length); + + items.forEach((i) => { + const client = + h2.connect.apply(null, i) + .on('connect', common.mustCall(() => maybeClose(client))); + }); + + // Will fail because protocol does not match the server. + h2.connect({port: port, protocol: 'https:'}) + .on('socketError', common.mustCall()); + })); +} + + +{ + + const options = { + key: fs.readFileSync(path.join(common.fixturesDir, 'keys/agent3-key.pem')), + cert: fs.readFileSync(path.join(common.fixturesDir, 'keys/agent3-cert.pem')) + }; + + const server = h2.createSecureServer(options); + server.listen(0); + + server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const opts = {rejectUnauthorized: false}; + + const items = [ + [`https://localhost:${port}`, opts], + [new URL(`https://localhost:${port}`), opts], + [url.parse(`https://localhost:${port}`), opts], + [{port: port, protocol: 'https:'}, opts], + [{port: port, hostname: '127.0.0.1', protocol: 'https:'}, opts] + ]; + + let count = items.length; + + const maybeClose = common.mustCall((client) => { + client.destroy(); + if (--count === 0) { + setImmediate(() => server.close()); + } + }, items.length); + + items.forEach((i) => { + const client = + h2.connect.apply(null, i) + .on('connect', common.mustCall(() => maybeClose(client))); + }); + })); +} diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js new file mode 100644 index 00000000000000..9b1cf4a0c9ee86 --- /dev/null +++ b/test/parallel/test-http2-create-client-secure-session.js @@ -0,0 +1,75 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const tls = require('tls'); +const h2 = require('http2'); + +function loadKey(keyname) { + return fs.readFileSync( + path.join(common.fixturesDir, 'keys', keyname), 'binary'); +} + +function onStream(stream, headers) { + const socket = stream.session.socket; + assert(headers[':authority'].startsWith(socket.servername)); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end(JSON.stringify({ + servername: socket.servername, + alpnProtocol: socket.alpnProtocol + })); +} + +function verifySecureSession(key, cert, ca, opts) { + const server = h2.createSecureServer({cert, key}); + server.on('stream', common.mustCall(onStream)); + server.listen(0); + server.on('listening', common.mustCall(function() { + const headers = { ':path': '/' }; + if (!opts) { + opts = {}; + } + opts.secureContext = tls.createSecureContext({ca}); + const client = h2.connect(`https://localhost:${this.address().port}`, opts, function() { + const req = client.request(headers); + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200, 'status code is set'); + assert.strictEqual(headers['content-type'], 'text/html', + 'content type is set'); + assert(headers['date'], 'there is a date'); + })); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (d) => data += d); + req.on('end', common.mustCall(() => { + const jsonData = JSON.parse(data); + assert.strictEqual(jsonData.servername, opts.servername || 'localhost'); + assert.strictEqual(jsonData.alpnProtocol, 'h2'); + server.close(); + client.socket.destroy(); + })); + req.end(); + }); + })); +} + +// The server can be connected as 'localhost'. +verifySecureSession( + loadKey('agent8-key.pem'), + loadKey('agent8-cert.pem'), + loadKey('fake-startcom-root-cert.pem')); + +// Custom servername is specified. +verifySecureSession( + loadKey('agent1-key.pem'), + loadKey('agent1-cert.pem'), + loadKey('ca1-cert.pem'), + {servername: 'agent1'}); diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js new file mode 100644 index 00000000000000..c1c6ce1bfea62e --- /dev/null +++ b/test/parallel/test-http2-create-client-session.js @@ -0,0 +1,61 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const body = + '

this is some data

'; + +const server = h2.createServer(); +const count = 100; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream, count)); + +function onStream(stream, headers, flags) { + assert.strictEqual(headers[':scheme'], 'http'); + assert.ok(headers[':authority']); + assert.strictEqual(headers[':method'], 'GET'); + assert.strictEqual(flags, 5); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end(body); +} + +server.listen(0); + +let expected = count; + +server.on('listening', common.mustCall(function() { + + const client = h2.connect(`http://localhost:${this.address().port}`); + + const headers = { ':path': '/' }; + + for (let n = 0; n < count; n++) { + const req = client.request(headers); + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200, 'status code is set'); + assert.strictEqual(headers['content-type'], 'text/html', + 'content type is set'); + assert(headers['date'], 'there is a date'); + })); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (d) => data += d); + req.on('end', common.mustCall(() => { + assert.strictEqual(body, data); + if (--expected === 0) { + server.close(); + client.destroy(); + } + })); + req.end(); + } + +})); diff --git a/test/parallel/test-http2-date-header.js b/test/parallel/test-http2-date-header.js new file mode 100644 index 00000000000000..d9a73b2ef61d4b --- /dev/null +++ b/test/parallel/test-http2-date-header.js @@ -0,0 +1,28 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + // Date header is defaulted + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall((headers) => { + // The date header must be set to a non-invalid value + assert.notStrictEqual((new Date()).toString(), 'Invalid Date'); + })); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-dont-override.js b/test/parallel/test-http2-dont-override.js new file mode 100644 index 00000000000000..55b29580fbc9f4 --- /dev/null +++ b/test/parallel/test-http2-dont-override.js @@ -0,0 +1,48 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const options = {}; + +const server = http2.createServer(options); + +// options are defaulted but the options are not modified +assert.deepStrictEqual(Object.keys(options), []); + +server.on('stream', common.mustCall((stream) => { + const headers = {}; + const options = {}; + stream.respond(headers, options); + + // The headers are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(headers), []); + + // Options are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(options), []); + + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const headers = {}; + const options = {}; + + const req = client.request(headers, options); + + // The headers are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(headers), []); + + // Options are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(options), []); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js new file mode 100644 index 00000000000000..0c1a1bcceea255 --- /dev/null +++ b/test/parallel/test-http2-getpackedsettings.js @@ -0,0 +1,131 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); +const val = http2.getPackedSettings(http2.getDefaultSettings()); +assert.deepStrictEqual(val, check); + +[ + ['headerTableSize', 0], + ['headerTableSize', 2 ** 32 - 1], + ['initialWindowSize', 0], + ['initialWindowSize', 2 ** 32 - 1], + ['maxFrameSize', 16384], + ['maxFrameSize', 2 ** 24 - 1], + ['maxConcurrentStreams', 0], + ['maxConcurrentStreams', 2 ** 31 - 1], + ['maxHeaderListSize', 0], + ['maxHeaderListSize', 2 ** 32 - 1] +].forEach((i) => { + assert.doesNotThrow(() => http2.getPackedSettings({ [i[0]]: i[1] })); +}); + +assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: true })); +assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false })); + +[ + ['headerTableSize', -1], + ['headerTableSize', 2 ** 32], + ['initialWindowSize', -1], + ['initialWindowSize', 2 ** 32], + ['maxFrameSize', 16383], + ['maxFrameSize', 2 ** 24], + ['maxConcurrentStreams', -1], + ['maxConcurrentStreams', 2 ** 31], + ['maxHeaderListSize', -1], + ['maxHeaderListSize', 2 ** 32] +].forEach((i) => { + assert.throws(() => { + http2.getPackedSettings({ [i[0]]: i[1] }); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: RangeError, + message: `Invalid value for setting "${i[0]}": ${i[1]}` + })); +}); + +[ + 1, null, '', Infinity, new Date(), {}, NaN, [false] +].forEach((i) => { + assert.throws(() => { + http2.getPackedSettings({ enablePush: i }); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: TypeError, + message: `Invalid value for setting "enablePush": ${i}` + })); +}); + +{ + const check = Buffer.from([ + 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00, + 0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); + + const packed = http2.getPackedSettings({ + headerTableSize: 100, + initialWindowSize: 100, + maxFrameSize: 20000, + maxConcurrentStreams: 200, + maxHeaderListSize: 100, + enablePush: true, + foo: 'ignored' + }); + assert.strictEqual(packed.length, 36); + assert.deepStrictEqual(packed, check); +} + +{ + const packed = Buffer.from([ + 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00, + 0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); + + [1, true, '', [], {}, NaN].forEach((i) => { + assert.throws(() => { + http2.getUnpackedSettings(i); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "buf" argument must be one of type Buffer or Uint8Array' + })); + }); + + assert.throws(() => { + http2.getUnpackedSettings(packed.slice(5)); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + type: RangeError, + message: 'Packed settings length must be a multiple of six' + })); + + const settings = http2.getUnpackedSettings(packed); + + assert(settings); + assert.strictEqual(settings.headerTableSize, 100); + assert.strictEqual(settings.initialWindowSize, 100); + assert.strictEqual(settings.maxFrameSize, 20000); + assert.strictEqual(settings.maxConcurrentStreams, 200); + assert.strictEqual(settings.maxHeaderListSize, 100); + assert.strictEqual(settings.enablePush, true); +} + +{ + const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]); + + assert.throws(() => { + http2.getUnpackedSettings(packed, {validate: true}); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: RangeError, + message: 'Invalid value for setting "maxConcurrentStreams": 4294967295' + })); +} diff --git a/test/parallel/test-http2-goaway-opaquedata.js b/test/parallel/test-http2-goaway-opaquedata.js new file mode 100644 index 00000000000000..e5904adf3bee99 --- /dev/null +++ b/test/parallel/test-http2-goaway-opaquedata.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); + +server.on('stream', common.mustCall((stream) => { + stream.session.shutdown({ + errorCode: 1, + opaqueData: data + }); + stream.end(); + stream.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + }))); +})); + +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + client.on('goaway', common.mustCall((code, lastStreamID, buf) => { + assert.deepStrictEqual(code, 1); + assert.deepStrictEqual(lastStreamID, 0); + assert.deepStrictEqual(data, buf); + server.close(); + })); + const req = client.request({ ':path': '/' }); + req.resume(); + req.on('end', common.mustCall()); + req.end(); + +}); diff --git a/test/parallel/test-http2-head-request.js b/test/parallel/test-http2-head-request.js new file mode 100644 index 00000000000000..07f0eb6c93298f --- /dev/null +++ b/test/parallel/test-http2-head-request.js @@ -0,0 +1,57 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const errCheck = common.expectsError({ + type: Error, + message: 'write after end' +}, 2); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_STATUS, + HTTP2_METHOD_HEAD, +} = http2.constants; + +const server = http2.createServer(); +server.on('stream', (stream, headers) => { + + assert.strictEqual(headers[HTTP2_HEADER_METHOD], HTTP2_METHOD_HEAD); + + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + + // Because this is a head request, the outbound stream is closed automatically + stream.on('error', common.mustCall(errCheck)); + stream.write('data'); +}); + + +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_HEAD, + [HTTP2_HEADER_PATH]: '/' + }); + + // Because it is a HEAD request, the payload is meaningless. The + // option.endStream flag is set automatically making the stream + // non-writable. + req.on('error', common.mustCall(errCheck)); + req.write('data'); + + req.on('response', common.mustCall((headers, flags) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200); + assert.strictEqual(flags, 5); // the end of stream flag is set + })); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +}); diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js new file mode 100644 index 00000000000000..b0424397f22696 --- /dev/null +++ b/test/parallel/test-http2-https-fallback.js @@ -0,0 +1,146 @@ +// Flags: --expose-http2 +'use strict'; + +const { + fixturesDir, + mustCall, + mustNotCall +} = require('../common'); +const { strictEqual } = require('assert'); +const { join } = require('path'); +const { readFileSync } = require('fs'); +const { createSecureContext } = require('tls'); +const { createSecureServer, connect } = require('http2'); +const { get } = require('https'); +const { parse } = require('url'); +const { connect: tls } = require('tls'); + +const countdown = (count, done) => () => --count === 0 && done(); + +function loadKey(keyname) { + return readFileSync(join(fixturesDir, 'keys', keyname)); +} + +const key = loadKey('agent8-key.pem'); +const cert = loadKey('agent8-cert.pem'); +const ca = loadKey('fake-startcom-root-cert.pem'); + +const clientOptions = { secureContext: createSecureContext({ ca }) }; + +function onRequest(request, response) { + const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ? + request.stream.session : request; + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ + alpnProtocol, + httpVersion: request.httpVersion + })); +} + +function onSession(session) { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'https', + ':authority': `localhost:${this.server.address().port}` + }; + + const request = session.request(headers); + request.on('response', mustCall((headers) => { + strictEqual(headers[':status'], 200); + strictEqual(headers['content-type'], 'application/json'); + })); + request.setEncoding('utf8'); + let raw = ''; + request.on('data', (chunk) => { raw += chunk; }); + request.on('end', mustCall(() => { + const { alpnProtocol, httpVersion } = JSON.parse(raw); + strictEqual(alpnProtocol, 'h2'); + strictEqual(httpVersion, '2.0'); + + session.destroy(); + this.cleanup(); + })); + request.end(); +} + +// HTTP/2 & HTTP/1.1 server +{ + const server = createSecureServer( + { cert, key, allowHTTP1: true }, + mustCall(onRequest, 2) + ); + + server.listen(0); + + server.on('listening', mustCall(() => { + const { port } = server.address(); + const origin = `https://localhost:${port}`; + + const cleanup = countdown(2, () => server.close()); + + // HTTP/2 client + connect( + origin, + clientOptions, + mustCall(onSession.bind({ cleanup, server })) + ); + + // HTTP/1.1 client + get( + Object.assign(parse(origin), clientOptions), + mustCall((response) => { + strictEqual(response.statusCode, 200); + strictEqual(response.statusMessage, 'OK'); + strictEqual(response.headers['content-type'], 'application/json'); + + response.setEncoding('utf8'); + let raw = ''; + response.on('data', (chunk) => { raw += chunk; }); + response.on('end', mustCall(() => { + const { alpnProtocol, httpVersion } = JSON.parse(raw); + strictEqual(alpnProtocol, false); + strictEqual(httpVersion, '1.1'); + + cleanup(); + })); + }) + ); + })); +} + +// HTTP/2-only server +{ + const server = createSecureServer( + { cert, key }, + mustCall(onRequest) + ); + + server.on('unknownProtocol', mustCall((socket) => { + socket.destroy(); + }, 2)); + + server.listen(0); + + server.on('listening', mustCall(() => { + const { port } = server.address(); + const origin = `https://localhost:${port}`; + + const cleanup = countdown(3, () => server.close()); + + // HTTP/2 client + connect( + origin, + clientOptions, + mustCall(onSession.bind({ cleanup, server })) + ); + + // HTTP/1.1 client + get(Object.assign(parse(origin), clientOptions), mustNotCall()) + .on('error', mustCall(cleanup)); + + // Incompatible ALPN TLS client + tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions)) + .on('error', mustCall(cleanup)); + })); +} diff --git a/test/parallel/test-http2-info-headers.js b/test/parallel/test-http2-info-headers.js new file mode 100755 index 00000000000000..c5d93d514f5d28 --- /dev/null +++ b/test/parallel/test-http2-info-headers.js @@ -0,0 +1,85 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +const status101regex = + /^HTTP status code 101 \(Switching Protocols\) is forbidden in HTTP\/2$/; +const afterRespondregex = + /^Cannot specify additional headers after response initiated$/; + +function onStream(stream, headers, flags) { + + assert.throws(() => stream.additionalHeaders({ ':status': 201 }), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_INFO_STATUS', + type: RangeError, + message: /^Invalid informational status code: 201$/ + })); + + assert.throws(() => stream.additionalHeaders({ ':status': 101 }), + common.expectsError({ + code: 'ERR_HTTP2_STATUS_101', + type: Error, + message: status101regex + })); + + // Can send more than one + stream.additionalHeaders({ ':status': 100 }); + stream.additionalHeaders({ ':status': 100 }); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + + assert.throws(() => stream.additionalHeaders({ abc: 123 }), + common.expectsError({ + code: 'ERR_HTTP2_HEADERS_AFTER_RESPOND', + type: Error, + message: afterRespondregex + })); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/'}); + + // The additionalHeaders method does not exist on client stream + assert.strictEqual(req.additionalHeaders, undefined); + + // Additional informational headers + req.on('headers', common.mustCall((headers) => { + assert.notStrictEqual(headers, undefined); + assert.strictEqual(headers[':status'], 100); + }, 2)); + + // Response headers + req.on('response', common.mustCall((headers) => { + assert.notStrictEqual(headers, undefined); + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + })); + + req.resume(); + + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-max-concurrent-streams.js b/test/parallel/test-http2-max-concurrent-streams.js new file mode 100644 index 00000000000000..6725a7c7545a90 --- /dev/null +++ b/test/parallel/test-http2-max-concurrent-streams.js @@ -0,0 +1,67 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_PATH, + HTTP2_METHOD_POST +} = h2.constants; + +// Only allow one stream to be open at a time +const server = h2.createServer({ settings: { maxConcurrentStreams: 1 }}); + +// The stream handler must be called only once +server.on('stream', common.mustCall((stream) => { + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + stream.end('hello world'); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + let reqs = 2; + function onEnd() { + if (--reqs === 0) { + server.close(); + client.destroy(); + } + } + + client.on('remoteSettings', common.mustCall((settings) => { + assert.strictEqual(settings.maxConcurrentStreams, 1); + })); + + // This one should go through with no problems + const req1 = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST + }); + req1.on('aborted', common.mustNotCall()); + req1.on('response', common.mustCall()); + req1.resume(); + req1.on('end', onEnd); + req1.end(); + + // This one should be aborted + const req2 = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST + }); + req2.on('aborted', common.mustCall()); + req2.on('response', common.mustNotCall()); + req2.resume(); + req2.on('end', onEnd); + req2.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + }))); + +})); diff --git a/test/parallel/test-http2-methods.js b/test/parallel/test-http2-methods.js new file mode 100644 index 00000000000000..1a8828f22c7363 --- /dev/null +++ b/test/parallel/test-http2-methods.js @@ -0,0 +1,48 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const methods = [undefined, 'GET', 'POST', 'PATCH', 'FOO', 'A B C']; +let expected = methods.length; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream, expected)); + +function onStream(stream, headers, flags) { + const method = headers[':method']; + assert.notStrictEqual(method, undefined); + assert(methods.includes(method), `method ${method} not included`); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const headers = { ':path': '/' }; + + methods.forEach((method) => { + headers[':method'] = method; + const req = client.request(headers); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + if (--expected === 0) { + server.close(); + client.destroy(); + } + })); + req.end(); + }); +})); diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js new file mode 100644 index 00000000000000..e356169d26e7e6 --- /dev/null +++ b/test/parallel/test-http2-misused-pseudoheaders.js @@ -0,0 +1,61 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + [ + ':path', + ':authority', + ':method', + ':scheme' + ].forEach((i) => { + assert.throws(() => stream.respond({[i]: '/'}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' + })); + }); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + + // This will cause an error to be emitted on the stream because + // using a pseudo-header in a trailer is forbidden. + stream.on('fetchTrailers', (trailers) => { + trailers[':status'] = 'bar'; + }); + + stream.on('error', common.expectsError({ + code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' + })); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-multi-content-length.js b/test/parallel/test-http2-multi-content-length.js new file mode 100644 index 00000000000000..5dcd56990be5dd --- /dev/null +++ b/test/parallel/test-http2-multi-content-length.js @@ -0,0 +1,58 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = 3; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + // Request 1 will fail because there are two content-length header values + const req = client.request({ + ':method': 'POST', + 'content-length': 1, + 'Content-Length': 2 + }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: 'Header field "content-length" must have only a single value' + })); + req.on('error', common.mustCall(maybeClose)); + req.end('a'); + + // Request 2 will succeed + const req2 = client.request({ + ':method': 'POST', + 'content-length': 1 + }); + req2.resume(); + req2.on('end', common.mustCall(maybeClose)); + req2.end('a'); + + // Request 3 will fail because nghttp2 does not allow the content-length + // header to be set for non-payload bearing requests... + const req3 = client.request({ 'content-length': 1}); + req3.resume(); + req3.on('end', common.mustCall(maybeClose)); + req3.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 1' + })); +})); diff --git a/test/parallel/test-http2-multiheaders.js b/test/parallel/test-http2-multiheaders.js new file mode 100644 index 00000000000000..d7b8f56d51ac30 --- /dev/null +++ b/test/parallel/test-http2-multiheaders.js @@ -0,0 +1,60 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +const src = Object.create(null); +src.accept = [ 'abc', 'def' ]; +src.Accept = 'ghijklmnop'; +src['www-authenticate'] = 'foo'; +src['WWW-Authenticate'] = 'bar'; +src['WWW-AUTHENTICATE'] = 'baz'; +src['proxy-authenticate'] = 'foo'; +src['Proxy-Authenticate'] = 'bar'; +src['PROXY-AUTHENTICATE'] = 'baz'; +src['x-foo'] = 'foo'; +src['X-Foo'] = 'bar'; +src['X-FOO'] = 'baz'; +src.constructor = 'foo'; +src.Constructor = 'bar'; +src.CONSTRUCTOR = 'baz'; +// eslint-disable-next-line no-proto +src['__proto__'] = 'foo'; +src['__PROTO__'] = 'bar'; +src['__Proto__'] = 'baz'; + +function checkHeaders(headers) { + assert.deepStrictEqual(headers['accept'], + [ 'abc', 'def', 'ghijklmnop' ]); + assert.deepStrictEqual(headers['www-authenticate'], + [ 'foo', 'bar', 'baz' ]); + assert.deepStrictEqual(headers['proxy-authenticate'], + [ 'foo', 'bar', 'baz' ]); + assert.deepStrictEqual(headers['x-foo'], [ 'foo', 'bar', 'baz' ]); + assert.deepStrictEqual(headers['constructor'], [ 'foo', 'bar', 'baz' ]); + // eslint-disable-next-line no-proto + assert.deepStrictEqual(headers['__proto__'], [ 'foo', 'bar', 'baz' ]); +} + +server.on('stream', common.mustCall((stream, headers) => { + assert.strictEqual(headers[':path'], '/'); + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':method'], 'GET'); + checkHeaders(headers); + stream.respond(src); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(src); + req.on('response', common.mustCall(checkHeaders)); + req.on('streamClosed', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-multiplex.js b/test/parallel/test-http2-multiplex.js new file mode 100644 index 00000000000000..b6b81c73a654c6 --- /dev/null +++ b/test/parallel/test-http2-multiplex.js @@ -0,0 +1,59 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests opening 100 concurrent simultaneous uploading streams over a single +// connection and makes sure that the data for each is appropriately echoed. + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +const count = 100; + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.pipe(stream); +}, count)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = count; + + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + function doRequest() { + const req = client.request({ ':method': 'POST '}); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => data += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'abcdefghij'); + maybeClose(); + })); + + let n = 0; + function writeChunk() { + if (n < 10) { + req.write(String.fromCharCode(97 + n)); + setTimeout(writeChunk, 10); + } else { + req.end(); + } + n++; + } + + writeChunk(); + } + + for (let n = 0; n < count; n++) + doRequest(); +})); diff --git a/test/parallel/test-http2-noflag.js b/test/parallel/test-http2-noflag.js new file mode 100644 index 00000000000000..a1e0e8b72c79e9 --- /dev/null +++ b/test/parallel/test-http2-noflag.js @@ -0,0 +1,8 @@ +// The --expose-http2 flag is not set +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.throws(() => require('http2'), + /^Error: Cannot find module 'http2'$/); diff --git a/test/parallel/test-http2-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js new file mode 100644 index 00000000000000..41e8d549b4a9a3 --- /dev/null +++ b/test/parallel/test-http2-options-max-headers-block-length.js @@ -0,0 +1,48 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + // Setting the maxSendHeaderBlockLength, then attempting to send a + // headers block that is too big should cause a 'meError' to + // be emitted, and will cause the stream to be shutdown. + const options = { + maxSendHeaderBlockLength: 10 + }; + + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + + req.on('frameError', common.mustCall((type, code) => { + assert.strictEqual(code, h2.constants.NGHTTP2_ERR_FRAME_SIZE_ERROR); + })); + + req.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + }))); + + req.end(); + +})); diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js new file mode 100644 index 00000000000000..1173b58e287de2 --- /dev/null +++ b/test/parallel/test-http2-options-max-reserved-streams.js @@ -0,0 +1,73 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall((stream) => { + stream.respond({ ':status': 200 }); + + // The first pushStream will complete as normal + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${server.address().port}`, + }, common.mustCall((pushedStream) => { + pushedStream.respond({ ':status': 200 }); + pushedStream.end(); + pushedStream.on('aborted', common.mustNotCall()); + })); + + // The second pushStream will be aborted because the client + // will reject it due to the maxReservedRemoteStreams option + // being set to only 1 + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${server.address().port}`, + }, common.mustCall((pushedStream) => { + pushedStream.respond({ ':status': 200 }); + pushedStream.on('aborted', common.mustCall()); + pushedStream.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 8' + }))); + })); + + stream.end('hello world'); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const options = { + maxReservedRemoteStreams: 1 + }; + + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + const req = client.request({ ':path': '/' }); + + // Because maxReservedRemoteStream is 1, the stream event + // must only be emitted once, even tho the server sends + // two push streams. + client.on('stream', common.mustCall((stream) => { + stream.resume(); + stream.on('end', common.mustCall()); + })); + + req.on('response', common.mustCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-padding-callback.js b/test/parallel/test-http2-padding-callback.js new file mode 100644 index 00000000000000..610b636fdcc263 --- /dev/null +++ b/test/parallel/test-http2-padding-callback.js @@ -0,0 +1,50 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const { PADDING_STRATEGY_CALLBACK } = h2.constants; + +function selectPadding(frameLen, max) { + assert.strictEqual(typeof frameLen, 'number'); + assert.strictEqual(typeof max, 'number'); + assert(max >= frameLen); + return max; +} + +// selectPadding will be called three times: +// 1. For the client request headers frame +// 2. For the server response headers frame +// 3. For the server response data frame +const options = { + paddingStrategy: PADDING_STRATEGY_CALLBACK, + selectPadding: common.mustCall(selectPadding, 3) +}; + +const server = h2.createServer(options); +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + const req = client.request({ ':path': '/' }); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-priority-event.js b/test/parallel/test-http2-priority-event.js new file mode 100644 index 00000000000000..bbb248265e8402 --- /dev/null +++ b/test/parallel/test-http2-priority-event.js @@ -0,0 +1,60 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onPriority(stream, parent, weight, exclusive) { + assert.strictEqual(stream, 1); + assert.strictEqual(parent, 0); + assert.strictEqual(weight, 1); + assert.strictEqual(exclusive, false); +} + +function onStream(stream, headers, flags) { + stream.priority({ + parent: 0, + weight: 1, + exclusive: false + }); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('priority', common.mustCall(onPriority)); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':path': '/'}); + + client.on('connect', () => { + req.priority({ + parent: 0, + weight: 1, + exclusive: false + }); + }); + + req.on('priority', common.mustCall(onPriority)); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-respond-file-204.js b/test/parallel/test-http2-respond-file-204.js new file mode 100644 index 00000000000000..66840e57adac91 --- /dev/null +++ b/test/parallel/test-http2-respond-file-204.js @@ -0,0 +1,41 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_STATUS +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + assert.throws(() => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_STATUS]: 204, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + }, common.expectsError({ + code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN', + type: Error, + message: 'Responses with 204 status must not have a payload' + })); + stream.respond({}); + stream.end(); +}); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-304.js b/test/parallel/test-http2-respond-file-304.js new file mode 100644 index 00000000000000..0b279223f14d5d --- /dev/null +++ b/test/parallel/test-http2-respond-file-304.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_STATUS +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }, { + statCheck(stat, headers) { + // abort the send and return a 304 Not Modified instead + stream.respond({ [HTTP2_HEADER_STATUS]: 304 }); + return false; + } + }); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 304); + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE, undefined]); + })); + + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-compat.js b/test/parallel/test-http2-respond-file-compat.js new file mode 100644 index 00000000000000..be256ee2bfa416 --- /dev/null +++ b/test/parallel/test-http2-respond-file-compat.js @@ -0,0 +1,23 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const path = require('path'); + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(common.mustCall((request, response) => { + response.stream.respondWithFile(fname); +})); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); + req.resume(); +}); diff --git a/test/parallel/test-http2-respond-file-fd-invalid.js b/test/parallel/test-http2-respond-file-fd-invalid.js new file mode 100644 index 00000000000000..f46dbd9dc1d1a4 --- /dev/null +++ b/test/parallel/test-http2-respond-file-fd-invalid.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); + +const { + NGHTTP2_INTERNAL_ERROR +} = http2.constants; + +const errorCheck = common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: `Stream closed with error code ${NGHTTP2_INTERNAL_ERROR}` +}, 2); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFD(common.firstInvalidFD()); + stream.on('error', common.mustCall(errorCheck)); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall()); + req.on('error', common.mustCall(errorCheck)); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-fd.js b/test/parallel/test-http2-respond-file-fd.js new file mode 100644 index 00000000000000..4e982bca3cfd35 --- /dev/null +++ b/test/parallel/test-http2-respond-file-fd.js @@ -0,0 +1,46 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_CONTENT_LENGTH +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); +const data = fs.readFileSync(fname); +const stat = fs.statSync(fname); +const fd = fs.openSync(fname, 'r'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFD(fd, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', + [HTTP2_HEADER_CONTENT_LENGTH]: stat.size, + }); +}); +server.on('close', common.mustCall(() => fs.closeSync(fd))); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); + })); + req.setEncoding('utf8'); + let check = ''; + req.on('data', (chunk) => check += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8')); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-push.js b/test/parallel/test-http2-respond-file-push.js new file mode 100644 index 00000000000000..1c2476f173463a --- /dev/null +++ b/test/parallel/test-http2-respond-file-push.js @@ -0,0 +1,81 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_LAST_MODIFIED +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); +const data = fs.readFileSync(fname); +const stat = fs.statSync(fname); +const fd = fs.openSync(fname, 'r'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond({}); + stream.end(); + + stream.pushStream({ + ':path': '/file.txt', + ':method': 'GET' + }, (stream) => { + stream.respondWithFD(fd, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', + [HTTP2_HEADER_CONTENT_LENGTH]: stat.size, + [HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString() + }); + }); + + stream.end(); +}); + +server.on('close', common.mustCall(() => fs.closeSync(fd))); + +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + + let expected = 2; + function maybeClose() { + if (--expected === 0) { + server.close(); + client.destroy(); + } + } + + const req = client.request({}); + + req.on('response', common.mustCall()); + + client.on('stream', common.mustCall((stream, headers) => { + + stream.on('push', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); + assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED], + stat.mtime.toUTCString()); + })); + + stream.setEncoding('utf8'); + let check = ''; + stream.on('data', (chunk) => check += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8')); + maybeClose(); + })); + + })); + + req.resume(); + req.on('end', maybeClose); + + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js new file mode 100644 index 00000000000000..81babb58fadcd1 --- /dev/null +++ b/test/parallel/test-http2-respond-file.js @@ -0,0 +1,51 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_LAST_MODIFIED +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); +const data = fs.readFileSync(fname); +const stat = fs.statSync(fname); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }, { + statCheck(stat, headers) { + headers[HTTP2_HEADER_LAST_MODIFIED] = stat.mtime.toUTCString(); + headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size; + } + }); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); + assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED], + stat.mtime.toUTCString()); + })); + req.setEncoding('utf8'); + let check = ''; + req.on('data', (chunk) => check += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8')); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js new file mode 100644 index 00000000000000..088c675389f5ba --- /dev/null +++ b/test/parallel/test-http2-response-splitting.js @@ -0,0 +1,75 @@ +// Flags: --expose-http2 +'use strict'; + +// Response splitting is no longer an issue with HTTP/2. The underlying +// nghttp2 implementation automatically strips out the header values that +// contain invalid characters. + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const { URL } = require('url'); + +// Response splitting example, credit: Amit Klein, Safebreach +const str = '/welcome?lang=bar%c4%8d%c4%8aContent­Length:%200%c4%8d%c4%8a%c' + + '4%8d%c4%8aHTTP/1.1%20200%20OK%c4%8d%c4%8aContent­Length:%202' + + '0%c4%8d%c4%8aLast­Modified:%20Mon,%2027%20Oct%202003%2014:50:18' + + '%20GMT%c4%8d%c4%8aContent­Type:%20text/html%c4%8d%c4%8a%c4%8' + + 'd%c4%8a%3chtml%3eGotcha!%3c/html%3e'; + +// Response splitting example, credit: Сковорода Никита Андреевич (@ChALkeR) +const x = 'fooഊSet-Cookie: foo=barഊഊ'; +const y = 'foo⠊Set-Cookie: foo=bar'; + +let remaining = 3; + +function makeUrl(headers) { + return `${headers[':scheme']}://${headers[':authority']}${headers[':path']}`; +} + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers) => { + + const obj = Object.create(null); + switch (remaining--) { + case 3: + const url = new URL(makeUrl(headers)); + obj[':status'] = 302; + obj.Location = `/foo?lang=${url.searchParams.get('lang')}`; + break; + case 2: + obj.foo = x; + break; + case 1: + obj.foo = y; + break; + } + stream.respond(obj); + stream.end(); +}, 3)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + function maybeClose() { + if (remaining === 0) { + server.close(); + client.destroy(); + } + } + + function doTest(path, key, expected) { + const req = client.request({ ':path': path }); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers.foo, undefined); + assert.strictEqual(headers.location, undefined); + })); + req.resume(); + req.on('end', common.mustCall(maybeClose)); + } + + doTest(str, 'location', str); + doTest('/', 'foo', x); + doTest('/', 'foo', y); + +})); diff --git a/test/parallel/test-http2-serve-file.js b/test/parallel/test-http2-serve-file.js new file mode 100644 index 00000000000000..109270327489eb --- /dev/null +++ b/test/parallel/test-http2-serve-file.js @@ -0,0 +1,82 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const path = require('path'); +const tls = require('tls'); + +const ajs_data = fs.readFileSync(path.resolve(common.fixturesDir, 'a.js'), + 'utf8'); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS +} = http2.constants; + +function loadKey(keyname) { + return fs.readFileSync( + path.join(common.fixturesDir, 'keys', keyname), 'binary'); +} + +const key = loadKey('agent8-key.pem'); +const cert = loadKey('agent8-cert.pem'); +const ca = loadKey('fake-startcom-root-cert.pem'); + +const server = http2.createSecureServer({key, cert}); + +server.on('stream', (stream, headers) => { + const name = headers[HTTP2_HEADER_PATH].slice(1); + const file = path.resolve(common.fixturesDir, name); + fs.stat(file, (err, stat) => { + if (err != null || stat.isDirectory()) { + stream.respond({ [HTTP2_HEADER_STATUS]: 404 }); + stream.end(); + } else { + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + const str = fs.createReadStream(file); + str.pipe(stream); + } + }); +}); + +server.listen(8000, () => { + + const secureContext = tls.createSecureContext({ca}); + const client = http2.connect(`https://localhost:${server.address().port}`, + { secureContext }); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + // Request for a file that does exist, response is 200 + const req1 = client.request({ [HTTP2_HEADER_PATH]: '/a.js' }, + { endStream: true }); + req1.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200); + })); + let req1_data = ''; + req1.setEncoding('utf8'); + req1.on('data', (chunk) => req1_data += chunk); + req1.on('end', common.mustCall(() => { + assert.strictEqual(req1_data, ajs_data); + maybeClose(); + })); + + // Request for a file that does not exist, response is 404 + const req2 = client.request({ [HTTP2_HEADER_PATH]: '/does_not_exist' }, + { endStream: true }); + req2.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 404); + })); + req2.on('data', common.mustNotCall()); + req2.on('end', common.mustCall(() => maybeClose())); + +}); diff --git a/test/parallel/test-http2-server-destroy-before-additional.js b/test/parallel/test-http2-server-destroy-before-additional.js new file mode 100644 index 00000000000000..9aff3b6abf987b --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-additional.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.additionalHeaders({}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-destroy-before-push.js b/test/parallel/test-http2-server-destroy-before-push.js new file mode 100644 index 00000000000000..9c1628a18bf671 --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-push.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.pushStream({}, common.mustNotCall()), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-destroy-before-respond.js b/test/parallel/test-http2-server-destroy-before-respond.js new file mode 100644 index 00000000000000..acb020d5bd7e3e --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-respond.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.respond({}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-destroy-before-write.js b/test/parallel/test-http2-server-destroy-before-write.js new file mode 100644 index 00000000000000..533aace208c44b --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-write.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.write('data'), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + type: Error + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-push-disabled.js b/test/parallel/test-http2-server-push-disabled.js new file mode 100644 index 00000000000000..1fedf2229378ef --- /dev/null +++ b/test/parallel/test-http2-server-push-disabled.js @@ -0,0 +1,53 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('session', common.mustCall((session) => { + // Verify that the settings disabling push is received + session.on('remoteSettings', common.mustCall((settings) => { + assert.strictEqual(settings.enablePush, false); + })); +})); + +server.on('stream', common.mustCall((stream) => { + + // The client has disabled push streams, so pushAllowed must be false, + // and pushStream() must throw. + assert.strictEqual(stream.pushAllowed, false); + + assert.throws(() => { + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${server.address().port}`, + }, common.mustNotCall()); + }, common.expectsError({ + code: 'ERR_HTTP2_PUSH_DISABLED', + type: Error + })); + + stream.respond({ ':status': 200 }); + stream.end('test'); +})); + +server.listen(0, common.mustCall(() => { + const options = {settings: { enablePush: false }}; + const client = http2.connect(`http://localhost:${server.address().port}`, + options); + const req = client.request({ ':path': '/' }); + + // Because push stream sre disabled, this must not be called. + client.on('stream', common.mustNotCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-server-push-stream.js b/test/parallel/test-http2-server-push-stream.js new file mode 100644 index 00000000000000..c2f34ed517c6c9 --- /dev/null +++ b/test/parallel/test-http2-server-push-stream.js @@ -0,0 +1,58 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers) => { + const port = server.address().port; + if (headers[':path'] === '/') { + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${port}`, + }, (push, headers) => { + push.respond({ + 'content-type': 'text/html', + ':status': 200, + 'x-push-data': 'pushed by server', + }); + push.end('pushed by server data'); + stream.end('test'); + }); + } + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const headers = { ':path': '/' }; + const client = http2.connect(`http://localhost:${port}`); + const req = client.request(headers); + + client.on('stream', common.mustCall((stream, headers) => { + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':path'], '/foobar'); + assert.strictEqual(headers[':authority'], `localhost:${port}`); + stream.on('push', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + assert.strictEqual(headers['x-push-data'], 'pushed by server'); + })); + })); + + let data = ''; + + req.on('data', common.mustCall((d) => data += d)); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'test'); + server.close(); + client.destroy(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-server-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js new file mode 100644 index 00000000000000..015e11311f7544 --- /dev/null +++ b/test/parallel/test-http2-server-rst-before-respond.js @@ -0,0 +1,45 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.rstStream(); + + assert.throws(() => { + stream.additionalHeaders({ + ':status': 123, + abc: 123 + }); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('headers', common.mustNotCall()); + + req.on('streamClosed', common.mustCall((code) => { + assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, code); + server.close(); + client.destroy(); + })); + + req.on('response', common.mustNotCall()); + +})); diff --git a/test/parallel/test-http2-server-rst-stream.js b/test/parallel/test-http2-server-rst-stream.js new file mode 100644 index 00000000000000..30a9db49afc239 --- /dev/null +++ b/test/parallel/test-http2-server-rst-stream.js @@ -0,0 +1,72 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_METHOD_POST, + NGHTTP2_CANCEL, + NGHTTP2_NO_ERROR, + NGHTTP2_PROTOCOL_ERROR, + NGHTTP2_REFUSED_STREAM, + NGHTTP2_INTERNAL_ERROR +} = http2.constants; + +const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 8); + +function checkRstCode(rstMethod, expectRstCode) { + const server = http2.createServer(); + server.on('stream', (stream, headers, flags) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write('test'); + if (rstMethod === 'rstStream') + stream[rstMethod](expectRstCode); + else + stream[rstMethod](); + + if (expectRstCode > NGHTTP2_NO_ERROR) { + stream.on('error', common.mustCall(errCheck)); + } + }); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const headers = { + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST + }; + const req = client.request(headers); + + req.setEncoding('utf8'); + req.on('streamClosed', common.mustCall((actualRstCode) => { + assert.strictEqual( + expectRstCode, actualRstCode, `${rstMethod} is not match rstCode`); + server.close(); + client.destroy(); + })); + req.on('data', common.mustCall()); + req.on('aborted', common.mustCall()); + req.on('end', common.mustCall()); + + if (expectRstCode > NGHTTP2_NO_ERROR) { + req.on('error', common.mustCall(errCheck)); + } + + })); +} + +checkRstCode('rstStream', NGHTTP2_NO_ERROR); +checkRstCode('rstWithNoError', NGHTTP2_NO_ERROR); +checkRstCode('rstWithProtocolError', NGHTTP2_PROTOCOL_ERROR); +checkRstCode('rstWithCancel', NGHTTP2_CANCEL); +checkRstCode('rstWithRefuse', NGHTTP2_REFUSED_STREAM); +checkRstCode('rstWithInternalError', NGHTTP2_INTERNAL_ERROR); diff --git a/test/parallel/test-http2-server-set-header.js b/test/parallel/test-http2-server-set-header.js new file mode 100644 index 00000000000000..8b0e82fd8a50d5 --- /dev/null +++ b/test/parallel/test-http2-server-set-header.js @@ -0,0 +1,36 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const body = + '

this is some data

'; + +const server = http2.createServer((req, res) => { + res.setHeader('foobar', 'baz'); + res.setHeader('X-POWERED-BY', 'node-test'); + res.end(body); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const headers = { ':path': '/' }; + const req = client.request(headers); + req.setEncoding('utf8'); + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers['foobar'], 'baz'); + assert.strictEqual(headers['x-powered-by'], 'node-test'); + })); + + let data = ''; + req.on('data', (d) => data += d); + req.on('end', () => { + assert.strictEqual(body, data); + server.close(); + client.destroy(); + }); + req.end(); +})); + +server.on('error', common.mustNotCall()); diff --git a/test/parallel/test-http2-server-shutdown-before-respond.js b/test/parallel/test-http2-server-shutdown-before-respond.js new file mode 100644 index 00000000000000..c7ffee7e31532e --- /dev/null +++ b/test/parallel/test-http2-server-shutdown-before-respond.js @@ -0,0 +1,32 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + const session = stream.session; + stream.session.shutdown({graceful: true}, common.mustCall(() => { + session.destroy(); + })); + stream.respond({}); + stream.end('data'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.resume(); + req.on('end', common.mustCall(() => server.close())); + req.end(); +})); diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js new file mode 100644 index 00000000000000..c10bbd0ccbe0c5 --- /dev/null +++ b/test/parallel/test-http2-server-socket-destroy.js @@ -0,0 +1,57 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); +const assert = require('assert'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_METHOD_POST +} = h2.constants; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write('test'); + + const socket = stream.session.socket; + + // When the socket is destroyed, the close events must be triggered + // on the socket, server and session. + socket.on('close', common.mustCall()); + server.on('close', common.mustCall()); + stream.session.on('close', common.mustCall(() => server.close())); + + // Also, the aborted event must be triggered on the stream + stream.on('aborted', common.mustCall()); + + assert.notStrictEqual(stream.session, undefined); + socket.destroy(); + assert.strictEqual(stream.session, undefined); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); + + req.on('aborted', common.mustCall()); + req.on('end', common.mustCall()); + req.on('response', common.mustCall()); + req.on('data', common.mustCall()); + + client.on('close', common.mustCall()); +})); diff --git a/test/parallel/test-http2-server-startup.js b/test/parallel/test-http2-server-startup.js new file mode 100644 index 00000000000000..c2e94f3ac4502a --- /dev/null +++ b/test/parallel/test-http2-server-startup.js @@ -0,0 +1,78 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests the basic operation of creating a plaintext or TLS +// HTTP2 server. The server does not do anything at this point +// other than start listening. + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const path = require('path'); +const tls = require('tls'); +const net = require('net'); +const fs = require('fs'); + +const options = { + key: fs.readFileSync( + path.resolve(common.fixturesDir, 'keys/agent2-key.pem')), + cert: fs.readFileSync( + path.resolve(common.fixturesDir, 'keys/agent2-cert.pem')) +}; + +// There should not be any throws +assert.doesNotThrow(() => { + + const serverTLS = http2.createSecureServer(options, () => {}); + + serverTLS.listen(0, common.mustCall(() => serverTLS.close())); + + // There should not be an error event reported either + serverTLS.on('error', common.mustNotCall()); +}); + +// There should not be any throws +assert.doesNotThrow(() => { + const server = http2.createServer(options, common.mustNotCall()); + + server.listen(0, common.mustCall(() => server.close())); + + // There should not be an error event reported either + server.on('error', common.mustNotCall()); +}); + +// Test the plaintext server socket timeout +{ + let client; + const server = http2.createServer(); + server.on('timeout', common.mustCall(() => { + server.close(); + if (client) + client.end(); + })); + server.setTimeout(common.platformTimeout(1000)); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + client = net.connect(port, common.mustCall()); + })); +} + +// Test the secure server socket timeout +{ + let client; + const server = http2.createSecureServer(options); + server.on('timeout', common.mustCall(() => { + server.close(); + if (client) + client.end(); + })); + server.setTimeout(common.platformTimeout(1000)); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + client = tls.connect({ + port: port, + rejectUnauthorized: false, + ALPNProtocols: ['h2'] + }, common.mustCall()); + })); +} diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js new file mode 100644 index 00000000000000..bc9877e23e38f6 --- /dev/null +++ b/test/parallel/test-http2-session-settings.js @@ -0,0 +1,110 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +server.on('stream', common.mustCall(onStream)); + +function assertSettings(settings) { + assert.strictEqual(typeof settings, 'object'); + assert.strictEqual(typeof settings.headerTableSize, 'number'); + assert.strictEqual(typeof settings.enablePush, 'boolean'); + assert.strictEqual(typeof settings.initialWindowSize, 'number'); + assert.strictEqual(typeof settings.maxFrameSize, 'number'); + assert.strictEqual(typeof settings.maxConcurrentStreams, 'number'); + assert.strictEqual(typeof settings.maxHeaderListSize, 'number'); +} + +function onStream(stream, headers, flags) { + + assertSettings(stream.session.localSettings); + assertSettings(stream.session.remoteSettings); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`, { + settings: { + enablePush: false, + initialWindowSize: 123456 + } + }); + + client.on('localSettings', common.mustCall((settings) => { + assert(settings); + assert.strictEqual(settings.enablePush, false); + assert.strictEqual(settings.initialWindowSize, 123456); + assert.strictEqual(settings.maxFrameSize, 16384); + }, 2)); + client.on('remoteSettings', common.mustCall((settings) => { + assert(settings); + })); + + const headers = { ':path': '/' }; + + const req = client.request(headers); + + req.on('connect', common.mustCall(() => { + // pendingSettingsAck will be true if a SETTINGS frame + // has been sent but we are still waiting for an acknowledgement + assert(client.pendingSettingsAck); + })); + + // State will only be valid after connect event is emitted + req.on('ready', common.mustCall(() => { + assert.doesNotThrow(() => { + client.settings({ + maxHeaderListSize: 1 + }); + }); + + // Verify valid error ranges + [ + ['headerTableSize', -1], + ['headerTableSize', 2 ** 32], + ['initialWindowSize', -1], + ['initialWindowSize', 2 ** 32], + ['maxFrameSize', 16383], + ['maxFrameSize', 2 ** 24], + ['maxHeaderListSize', -1], + ['maxHeaderListSize', 2 ** 32] + ].forEach((i) => { + const settings = {}; + settings[i[0]] = i[1]; + assert.throws(() => client.settings(settings), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: RangeError + })); + }); + [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { + assert.throws(() => client.settings({enablePush: i}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: TypeError + })); + }); + + })); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-session-stream-state.js b/test/parallel/test-http2-session-stream-state.js new file mode 100644 index 00000000000000..9ba56f958c43da --- /dev/null +++ b/test/parallel/test-http2-session-stream-state.js @@ -0,0 +1,97 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + // Test Stream State. + { + const state = stream.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.state, 'number'); + assert.strictEqual(typeof state.weight, 'number'); + assert.strictEqual(typeof state.sumDependencyWeight, 'number'); + assert.strictEqual(typeof state.localClose, 'number'); + assert.strictEqual(typeof state.remoteClose, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + } + + // Test Session State. + { + const state = stream.session.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.effectiveLocalWindowSize, 'number'); + assert.strictEqual(typeof state.effectiveRecvDataLength, 'number'); + assert.strictEqual(typeof state.nextStreamID, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + assert.strictEqual(typeof state.lastProcStreamID, 'number'); + assert.strictEqual(typeof state.remoteWindowSize, 'number'); + assert.strictEqual(typeof state.outboundQueueSize, 'number'); + assert.strictEqual(typeof state.deflateDynamicTableSize, 'number'); + assert.strictEqual(typeof state.inflateDynamicTableSize, 'number'); + } + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const headers = { ':path': '/' }; + + const req = client.request(headers); + + // State will only be valid after connect event is emitted + req.on('connect', common.mustCall(() => { + + // Test Stream State. + { + const state = req.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.state, 'number'); + assert.strictEqual(typeof state.weight, 'number'); + assert.strictEqual(typeof state.sumDependencyWeight, 'number'); + assert.strictEqual(typeof state.localClose, 'number'); + assert.strictEqual(typeof state.remoteClose, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + } + + // Test Session State + { + const state = req.session.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.effectiveLocalWindowSize, 'number'); + assert.strictEqual(typeof state.effectiveRecvDataLength, 'number'); + assert.strictEqual(typeof state.nextStreamID, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + assert.strictEqual(typeof state.lastProcStreamID, 'number'); + assert.strictEqual(typeof state.remoteWindowSize, 'number'); + assert.strictEqual(typeof state.outboundQueueSize, 'number'); + assert.strictEqual(typeof state.deflateDynamicTableSize, 'number'); + assert.strictEqual(typeof state.inflateDynamicTableSize, 'number'); + } + })); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-single-headers.js b/test/parallel/test-http2-single-headers.js new file mode 100644 index 00000000000000..49918acc474bcb --- /dev/null +++ b/test/parallel/test-http2-single-headers.js @@ -0,0 +1,59 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); + +const server = http2.createServer(); + +// Each of these headers must appear only once +const singles = [ + 'content-type', + 'user-agent', + 'referer', + 'authorization', + 'proxy-authorization', + 'if-modified-since', + 'if-unmodified-since', + 'from', + 'location', + 'max-forwards' +]; + +server.on('stream', common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = singles.length * 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + singles.forEach((i) => { + const req = client.request({ + [i]: 'abc', + [i.toUpperCase()]: 'xyz' + }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: `Header field "${i}" must have only a single value` + })); + req.on('error', common.mustCall(maybeClose)); + + const req2 = client.request({ + [i]: ['abc', 'xyz'] + }); + req2.on('error', common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: `Header field "${i}" must have only a single value` + })); + req2.on('error', common.mustCall(maybeClose)); + }); + +})); diff --git a/test/parallel/test-http2-status-code-invalid.js b/test/parallel/test-http2-status-code-invalid.js new file mode 100644 index 00000000000000..cb8c9072f73c3b --- /dev/null +++ b/test/parallel/test-http2-status-code-invalid.js @@ -0,0 +1,40 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +function expectsError(code) { + return common.expectsError({ + code: 'ERR_HTTP2_STATUS_INVALID', + type: RangeError, + message: `Invalid status code: ${code}` + }); +} + +server.on('stream', common.mustCall((stream) => { + + // Anything lower than 100 and greater than 599 is rejected + [ 99, 700, 1000 ].forEach((i) => { + assert.throws(() => stream.respond({ ':status': i }), expectsError(i)); + }); + + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + })); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-status-code.js b/test/parallel/test-http2-status-code.js new file mode 100644 index 00000000000000..f094d981c36eec --- /dev/null +++ b/test/parallel/test-http2-status-code.js @@ -0,0 +1,40 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const codes = [ 200, 202, 300, 400, 404, 451, 500 ]; +let test = 0; + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + const status = codes[test++]; + stream.respond({ ':status': status }, { endStream: true }); +}, 7)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = codes.length; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + function doTest(expected) { + const req = client.request(); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], expected); + })); + req.resume(); + req.on('end', common.mustCall(maybeClose)); + } + + for (let n = 0; n < codes.length; n++) + doTest(codes[n]); +})); diff --git a/test/parallel/test-http2-timeouts.js b/test/parallel/test-http2-timeouts.js new file mode 100644 index 00000000000000..132496e1fcdc33 --- /dev/null +++ b/test/parallel/test-http2-timeouts.js @@ -0,0 +1,32 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall((stream) => { + stream.setTimeout(1, common.mustCall(() => { + stream.respond({':status': 200}); + stream.end('hello world'); + })); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.setTimeout(1, common.mustCall(() => { + const req = client.request({ ':path': '/' }); + req.setTimeout(1, common.mustCall(() => { + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + })); + })); +})); diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js new file mode 100644 index 00000000000000..4a64645df14174 --- /dev/null +++ b/test/parallel/test-http2-too-many-settings.js @@ -0,0 +1,60 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests that attempting to send too many non-acknowledged +// settings frames will result in a throw. + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const maxPendingAck = 2; +const server = h2.createServer({ maxPendingAck: maxPendingAck + 1 }); + +let clients = 2; + +function doTest(session) { + for (let n = 0; n < maxPendingAck; n++) + assert.doesNotThrow(() => session.settings({ enablePush: false })); + assert.throws(() => session.settings({ enablePush: false }), + common.expectsError({ + code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + type: Error + })); +} + +server.on('stream', common.mustNotCall()); + +server.once('session', common.mustCall((session) => doTest(session))); + +server.listen(0); + +const closeServer = common.mustCall(() => { + if (--clients === 0) + server.close(); +}, clients); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + { maxPendingAck: maxPendingAck + 1 }); + let remaining = maxPendingAck + 1; + + client.on('close', closeServer); + client.on('localSettings', common.mustCall(() => { + if (--remaining <= 0) { + client.destroy(); + } + }, maxPendingAck + 1)); + client.on('connect', common.mustCall(() => doTest(client))); +})); + +// Setting maxPendingAck to 0, defaults it to 1 +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + { maxPendingAck: 0 }); + + client.on('close', closeServer); + client.on('localSettings', common.mustCall(() => { + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js new file mode 100644 index 00000000000000..35a5dbf28a1289 --- /dev/null +++ b/test/parallel/test-http2-trailers.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const body = + '

this is some data

'; +const trailerKey = 'test-trailer'; +const trailerValue = 'testing'; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.on('fetchTrailers', function(trailers) { + trailers[trailerKey] = trailerValue; + }); + stream.end(body); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + const req = client.request({':path': '/'}); + req.on('data', common.mustCall()); + req.on('trailers', common.mustCall((headers) => { + assert.strictEqual(headers[trailerKey], trailerValue); + req.end(); + })); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-util-asserts.js b/test/parallel/test-http2-util-asserts.js new file mode 100644 index 00000000000000..fd902f01a3d29a --- /dev/null +++ b/test/parallel/test-http2-util-asserts.js @@ -0,0 +1,43 @@ +// Flags: --expose-internals --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + assertIsObject, + assertWithinRange, +} = require('internal/http2/util'); + +[ + undefined, + {}, + Object.create(null), + new Date(), + new (class Foo {})() +].forEach((i) => { + assert.doesNotThrow(() => assertIsObject(i, 'foo', 'object')); +}); + +[ + 1, + false, + 'hello', + NaN, + Infinity, + [], + [{}] +].forEach((i) => { + assert.throws(() => assertIsObject(i, 'foo', 'object'), + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "foo" argument must be of type object$/ + })); +}); + +assert.doesNotThrow(() => assertWithinRange('foo', 1, 0, 2)); + +assert.throws(() => assertWithinRange('foo', 1, 2, 3), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + message: /^Invalid value for setting "foo": 1$/ + })); diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js new file mode 100644 index 00000000000000..d19c78a2b3c3ff --- /dev/null +++ b/test/parallel/test-http2-util-headers-list.js @@ -0,0 +1,248 @@ +// Flags: --expose-internals --expose-http2 +'use strict'; + +// Tests the internal utility function that is used to prepare headers +// to pass to the internal binding layer. + +const common = require('../common'); +const assert = require('assert'); +const { mapToHeaders } = require('internal/http2/util'); + +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_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_MAX_FORWARDS, + HTTP2_HEADER_PROXY_AUTHORIZATION, + HTTP2_HEADER_RANGE, + HTTP2_HEADER_REFERER, + HTTP2_HEADER_RETRY_AFTER, + HTTP2_HEADER_USER_AGENT, + + HTTP2_HEADER_ACCEPT_CHARSET, + HTTP2_HEADER_ACCEPT_ENCODING, + HTTP2_HEADER_ACCEPT_LANGUAGE, + HTTP2_HEADER_ACCEPT_RANGES, + HTTP2_HEADER_ACCEPT, + HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + HTTP2_HEADER_ALLOW, + HTTP2_HEADER_CACHE_CONTROL, + HTTP2_HEADER_CONTENT_DISPOSITION, + HTTP2_HEADER_COOKIE, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_LINK, + HTTP2_HEADER_PREFER, + HTTP2_HEADER_PROXY_AUTHENTICATE, + HTTP2_HEADER_REFRESH, + HTTP2_HEADER_SERVER, + HTTP2_HEADER_SET_COOKIE, + HTTP2_HEADER_STRICT_TRANSPORT_SECURITY, + HTTP2_HEADER_VARY, + HTTP2_HEADER_VIA, + HTTP2_HEADER_WWW_AUTHENTICATE, + + 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 +} = process.binding('http2').constants; + +{ + const headers = { + 'abc': 1, + ':status': 200, + ':path': 'abc', + 'xyz': [1, '2', { toString() { return '3'; } }, 4], + 'foo': [], + 'BAR': [1] + }; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':path', 'abc' ], + [ ':status', '200' ], + [ 'abc', '1' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ], + [ 'bar', '1' ] + ]); +} + +{ + const headers = { + 'abc': 1, + ':path': 'abc', + ':status': [200], + ':authority': [], + 'xyz': [1, 2, 3, 4] + }; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':status', '200' ], + [ ':path', 'abc' ], + [ 'abc', '1' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ] + ]); +} + +{ + const headers = { + 'abc': 1, + ':path': 'abc', + 'xyz': [1, 2, 3, 4], + '': 1, + ':status': 200, + [Symbol('test')]: 1 // Symbol keys are ignored + }; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':status', '200' ], + [ ':path', 'abc' ], + [ 'abc', '1' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ] + ]); +} + +{ + // Only own properties are used + const base = { 'abc': 1 }; + const headers = Object.create(base); + headers[':path'] = 'abc'; + headers.xyz = [1, 2, 3, 4]; + headers.foo = []; + headers[':status'] = 200; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':status', '200' ], + [ ':path', 'abc' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ] + ]); +} + +// The following are not allowed to have multiple values +[ + 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_MAX_FORWARDS, + HTTP2_HEADER_PROXY_AUTHORIZATION, + HTTP2_HEADER_RANGE, + HTTP2_HEADER_REFERER, + HTTP2_HEADER_RETRY_AFTER, + HTTP2_HEADER_USER_AGENT +].forEach((name) => { + const msg = `Header field "${name}" must have only a single value`; + common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + message: msg + })(mapToHeaders({[name]: [1, 2, 3]})); +}); + +[ + HTTP2_HEADER_ACCEPT_CHARSET, + HTTP2_HEADER_ACCEPT_ENCODING, + HTTP2_HEADER_ACCEPT_LANGUAGE, + HTTP2_HEADER_ACCEPT_RANGES, + HTTP2_HEADER_ACCEPT, + HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + HTTP2_HEADER_ALLOW, + HTTP2_HEADER_CACHE_CONTROL, + HTTP2_HEADER_CONTENT_DISPOSITION, + HTTP2_HEADER_COOKIE, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_LINK, + HTTP2_HEADER_PREFER, + HTTP2_HEADER_PROXY_AUTHENTICATE, + HTTP2_HEADER_REFRESH, + HTTP2_HEADER_SERVER, + HTTP2_HEADER_SET_COOKIE, + HTTP2_HEADER_STRICT_TRANSPORT_SECURITY, + HTTP2_HEADER_VARY, + HTTP2_HEADER_VIA, + HTTP2_HEADER_WWW_AUTHENTICATE +].forEach((name) => { + assert(!(mapToHeaders({[name]: [1, 2, 3]}) instanceof Error), name); +}); + +const regex = + /^HTTP\/1 Connection specific headers are forbidden$/; +[ + HTTP2_HEADER_CONNECTION, + HTTP2_HEADER_UPGRADE, + HTTP2_HEADER_HTTP2_SETTINGS, + HTTP2_HEADER_TE, + HTTP2_HEADER_TRANSFER_ENCODING, + HTTP2_HEADER_HOST, + HTTP2_HEADER_PROXY_CONNECTION, + HTTP2_HEADER_KEEP_ALIVE, + 'Connection', + 'Upgrade', + 'HTTP2-Settings', + 'TE', + 'Transfer-Encoding', + 'Proxy-Connection', + 'Keep-Alive' +].forEach((name) => { + common.expectsError({ + code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', + message: regex + })(mapToHeaders({[name]: 'abc'})); +}); + +assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error)); diff --git a/test/parallel/test-http2-window-size.js b/test/parallel/test-http2-window-size.js new file mode 100644 index 00000000000000..d914e99f6aa195 --- /dev/null +++ b/test/parallel/test-http2-window-size.js @@ -0,0 +1,102 @@ +// Flags: --expose-http2 +'use strict'; + +// This test ensures that servers are able to send data independent of window +// size. +// TODO: This test makes large buffer allocations (128KiB) and should be tested +// on smaller / IoT platforms in case this poses problems for these targets. + +const assert = require('assert'); +const common = require('../common'); +const h2 = require('http2'); + +// Given a list of buffers and an initial window size, have a server write +// each buffer to the HTTP2 Writable stream, and let the client verify that +// all of the bytes were sent correctly +function run(buffers, initialWindowSize) { + return new Promise((resolve, reject) => { + const expectedBuffer = Buffer.concat(buffers); + + const server = h2.createServer(); + server.on('stream', (stream) => { + let i = 0; + const writeToStream = () => { + const cont = () => { + i++; + if (i < buffers.length) { + setImmediate(writeToStream); + } else { + stream.end(); + } + }; + const drained = stream.write(buffers[i]); + if (drained) { + cont(); + } else { + stream.once('drain', cont); + } + }; + writeToStream(); + }); + server.listen(0); + + server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const client = + h2.connect({ + authority: 'localhost', + protocol: 'http:', + port + }, { + settings: { + initialWindowSize + } + }).on('connect', common.mustCall(() => { + const req = client.request({ + ':method': 'GET', + ':path': '/' + }); + const responses = []; + req.on('data', (data) => { + responses.push(data); + }); + req.on('end', common.mustCall(() => { + const actualBuffer = Buffer.concat(responses); + assert.strictEqual(Buffer.compare(actualBuffer, expectedBuffer), 0); + // shut down + client.destroy(); + server.close(() => { + resolve(); + }); + })); + req.end(); + })); + })); + }); +} + +const bufferValueRange = [0, 1, 2, 3]; +const buffersList = [ + bufferValueRange.map((a) => Buffer.alloc(1 << 4, a)), + bufferValueRange.map((a) => Buffer.alloc((1 << 8) - 1, a)), +// Specifying too large of a value causes timeouts on some platforms +// bufferValueRange.map((a) => Buffer.alloc(1 << 17, a)) +]; +const initialWindowSizeList = [ + 1 << 4, + (1 << 8) - 1, + 1 << 8, + 1 << 17, + undefined // use default window size which is (1 << 16) - 1 +]; + +// Call `run` on each element in the cartesian product of buffersList and +// initialWindowSizeList. +let p = Promise.resolve(); +for (const buffers of buffersList) { + for (const initialWindowSize of initialWindowSizeList) { + p = p.then(() => run(buffers, initialWindowSize)); + } +} +p.then(common.mustCall(() => {})); diff --git a/test/parallel/test-http2-withflag.js b/test/parallel/test-http2-withflag.js new file mode 100644 index 00000000000000..557ec40e643eba --- /dev/null +++ b/test/parallel/test-http2-withflag.js @@ -0,0 +1,7 @@ +// Flags: --expose-http2 +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.doesNotThrow(() => require('http2')); diff --git a/test/parallel/test-http2-write-callbacks.js b/test/parallel/test-http2-write-callbacks.js new file mode 100644 index 00000000000000..b371ebf681e472 --- /dev/null +++ b/test/parallel/test-http2-write-callbacks.js @@ -0,0 +1,36 @@ +// Flags: --expose-http2 +'use strict'; + +// Verifies that write callbacks are called + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + stream.write('abc', common.mustCall(() => { + stream.end('xyz'); + })); + let actual = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => actual += chunk); + stream.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); + req.write('abc', common.mustCall(() => { + req.end('xyz'); + })); + let actual = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); + req.on('streamClosed', common.mustCall(() => { + client.destroy(); + server.close(); + })); +})); diff --git a/test/parallel/test-http2-write-empty-string.js b/test/parallel/test-http2-write-empty-string.js new file mode 100644 index 00000000000000..74675fd67d9ef2 --- /dev/null +++ b/test/parallel/test-http2-write-empty-string.js @@ -0,0 +1,40 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(function(request, response) { + response.writeHead(200, {'Content-Type': 'text/plain'}); + response.write('1\n'); + response.write(''); + response.write('2\n'); + response.write(''); + response.end('3\n'); + + this.close(); +}); + +server.listen(0, common.mustCall(function() { + const client = http2.connect(`http://localhost:${this.address().port}`); + const headers = { ':path': '/' }; + const req = client.request(headers).setEncoding('ascii'); + + let res = ''; + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(200, headers[':status']); + })); + + req.on('data', (chunk) => { + res += chunk; + }); + + req.on('end', common.mustCall(function() { + assert.strictEqual('1\n2\n3\n', res); + client.destroy(); + })); + + req.end(); +})); diff --git a/test/parallel/test-http2-zero-length-write.js b/test/parallel/test-http2-zero-length-write.js new file mode 100644 index 00000000000000..5f4f0681d4baf4 --- /dev/null +++ b/test/parallel/test-http2-zero-length-write.js @@ -0,0 +1,50 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const { Readable } = require('stream'); + +function getSrc() { + const chunks = [ '', 'asdf', '', 'foo', '', 'bar', '' ]; + return new Readable({ + read() { + const chunk = chunks.shift(); + if (chunk !== undefined) + this.push(chunk); + else + this.push(null); + } + }); +} + +const expect = 'asdffoobar'; + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + let actual = ''; + stream.respond(); + stream.resume(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => actual += chunk); + stream.on('end', common.mustCall(() => { + getSrc().pipe(stream); + assert.strictEqual(actual, expect); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + let actual = ''; + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(actual, expect); + server.close(); + client.destroy(); + })); + getSrc().pipe(req); +})); diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js index 02eeb93cb1779e..dd51b36dbb8080 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -3,7 +3,7 @@ const common = require('../common'); const assert = require('assert'); const expected_keys = ['ares', 'http_parser', 'modules', 'node', - 'uv', 'v8', 'zlib']; + 'uv', 'v8', 'zlib', 'nghttp2']; if (common.hasCrypto) { expected_keys.push('openssl'); diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js old mode 100755 new mode 100644 From 148dedd88bf532dcabbf4906e7473871a5b65e51 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:32:25 -0700 Subject: [PATCH 06/26] http2: remove redundant return in test --- test/parallel/test-tls-disable-renegotiation.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index 9e9a672768649c..0efa935e432950 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -5,10 +5,9 @@ const fs = require('fs'); // Tests that calling disableRenegotiation on a TLSSocket stops renegotiation. -if (!common.hasCrypto) { +if (!common.hasCrypto) common.skip('missing crypto'); - return; -} + const tls = require('tls'); const options = { From 322355572666fd565bb68382df38f64d63bfc527 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:43:33 -0700 Subject: [PATCH 07/26] http2: fix documentation nits --- doc/api/errors.md | 2 +- doc/api/http2.md | 12 ++++++------ doc/guides/writing-and-running-benchmarks.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index d46ba363bec4f8..cc4a5bb7c9244a 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -793,7 +793,7 @@ been closed. ### ERR_HTTP2_STREAM_ERROR -Used when a non-zero error code has been specified in an RST_STREAM frame. +Used when a non-zero error code has been specified in an `RST_STREAM` frame. ### ERR_HTTP2_STREAM_SELF_DEPENDENCY diff --git a/doc/api/http2.md b/doc/api/http2.md index ca696de81e3aac..9beb2cac922986 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -559,7 +559,7 @@ 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 +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 @@ -1451,7 +1451,7 @@ added: REPLACEME * `settings` {[Settings Object][]} * Returns: {Buffer} -Returns a [Buffer][] instance containing serialized representation of the given +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. @@ -1691,13 +1691,13 @@ TBD [`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 +[`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 +[`Http2Stream`]: #http2_class_http2stream [Http2Session and Sockets]: #http2_http2sesion_and_sockets -[ServerHttp2Stream]: #http2_class_serverhttp2stream +[`ServerHttp2Stream`]: #http2_class_serverhttp2stream [Settings Object]: #http2_settings_object [Using options.selectPadding]: #http2_using_options_selectpadding [error code]: #error_codes diff --git a/doc/guides/writing-and-running-benchmarks.md b/doc/guides/writing-and-running-benchmarks.md index 7aeb9728aaedf0..d418ed6974bf80 100644 --- a/doc/guides/writing-and-running-benchmarks.md +++ b/doc/guides/writing-and-running-benchmarks.md @@ -45,7 +45,7 @@ benchmarker to be used should be specified by providing it as an argument: 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. +from [nghttp2.org][] or built from source. `node benchmark/http2/simple.js benchmarker=autocannon` From fb3a9c61caa96c0f80070d2f688fbe6a12fe5611 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 15:47:24 -0700 Subject: [PATCH 08/26] doc: include http2.md in all.md --- doc/api/all.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/all.md b/doc/api/all.md index 24eda32f44d3b5..425513e2568d03 100644 --- a/doc/api/all.md +++ b/doc/api/all.md @@ -19,6 +19,7 @@ @include fs @include globals @include http +@include http2 @include https @include inspector @include intl From e721353ea6a165a5179225d31ae8c19a8836ac38 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 18 Jul 2017 08:45:41 -0700 Subject: [PATCH 09/26] test: fix flakiness in test-http2-client-upload Race condition in the closing of the stream causing failure on some platforms. --- test/parallel/test-http2-client-upload.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) mode change 100644 => 100755 test/parallel/test-http2-client-upload.js diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js old mode 100644 new mode 100755 index 4ce7da878e1fd2..f65ae09c3a205b --- a/test/parallel/test-http2-client-upload.js +++ b/test/parallel/test-http2-client-upload.js @@ -32,13 +32,21 @@ fs.readFile(loc, common.mustCall((err, data) => { server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + const req = client.request({ ':method': 'POST' }); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); - fs.createReadStream(loc).pipe(req); + req.on('end', common.mustCall(maybeClose)); + const str = fs.createReadStream(loc); + str.on('end', common.mustCall(maybeClose)); + str.pipe(req); })); })); From f4f0a2875593233525e7e7b928510cfd04365573 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 18 Jul 2017 09:31:38 -0700 Subject: [PATCH 10/26] test: fix flaky test-http2-client-unescaped-path on osx --- test/parallel/test-http2-client-unescaped-path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 test/parallel/test-http2-client-unescaped-path.js diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js old mode 100644 new mode 100755 index d92d40492e204c..95b5c04b0fed6a --- a/test/parallel/test-http2-client-unescaped-path.js +++ b/test/parallel/test-http2-client-unescaped-path.js @@ -13,7 +13,7 @@ const count = 32; server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - let remaining = count; + let remaining = count + 1; function maybeClose() { if (--remaining === 0) { server.close(); From b9848a1015d80dd814712aeabdc45a701086af76 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 18 Jul 2017 17:24:41 -0700 Subject: [PATCH 11/26] http2: fix abort when client.destroy inside end event --- lib/internal/http2/core.js | 68 ++++++++++--------- src/node_http2.cc | 0 src/node_http2.h | 6 +- src/node_http2_core-inl.h | 7 +- src/node_http2_core.h | 8 +-- ...test-http2-options-max-reserved-streams.js | 17 +++-- .../parallel/test-http2-response-splitting.js | 3 +- .../test-http2-server-socket-destroy.js | 7 +- 8 files changed, 58 insertions(+), 58 deletions(-) mode change 100644 => 100755 lib/internal/http2/core.js mode change 100644 => 100755 src/node_http2.cc mode change 100644 => 100755 src/node_http2.h mode change 100644 => 100755 src/node_http2_core-inl.h mode change 100644 => 100755 src/node_http2_core.h mode change 100644 => 100755 test/parallel/test-http2-options-max-reserved-streams.js mode change 100644 => 100755 test/parallel/test-http2-response-splitting.js mode change 100644 => 100755 test/parallel/test-http2-server-socket-destroy.js diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js old mode 100644 new mode 100755 index 1bdd57926c4e62..5c99dbc897ba6c --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -289,11 +289,9 @@ function onSessionRead(nread, buf, handle) { _unrefActive(this); // Reset the session timeout timer _unrefActive(stream); // Reset the stream timeout timer - if (nread >= 0) { + if (nread >= 0 && !stream.destroyed) { 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`); + this.streamReadStop(id); state.reading = false; } } else { @@ -1475,44 +1473,48 @@ class Http2Stream extends Duplex { 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); - } - + process.nextTick(() => { + 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 && !session.destroyed) { + 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); + // Remove the close handler on the session + session.removeListener('close', this[kState].closeHandler); - // Unenroll the timer - unenroll(this); + // Unenroll the timer + unenroll(this); - setImmediate(finishStreamDestroy.bind(this, handle)); - session[kState].streams.delete(this[kID]); - delete this[kSession]; + setImmediate(finishStreamDestroy.bind(this, handle)); - // 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); + // 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) { + const id = this[kID]; + const session = this[kSession]; + session[kState].streams.delete(id); + delete this[kSession]; if (handle !== undefined) - handle.destroyStream(this[kID]); + handle.destroyStream(id); + this.emit('destroy'); } function processHeaders(headers) { diff --git a/src/node_http2.cc b/src/node_http2.cc old mode 100644 new mode 100755 diff --git a/src/node_http2.h b/src/node_http2.h old mode 100644 new mode 100755 index f6ccad29846d4a..c2dcd82e35948c --- a/src/node_http2.h +++ b/src/node_http2.h @@ -329,7 +329,6 @@ class Http2Session : public AsyncWrap, padding_strategy_ = opts.GetPaddingStrategy(); Init(env->event_loop(), type, *opts); - stream_buf_.AllocateSufficientStorage(kAllocBufferSize); } ~Http2Session() override { @@ -456,7 +455,7 @@ class Http2Session : public AsyncWrap, } char* stream_alloc() { - return *stream_buf_; + return stream_buf_; } private: @@ -464,7 +463,8 @@ class Http2Session : public AsyncWrap, StreamResource::Callback prev_alloc_cb_; StreamResource::Callback prev_read_cb_; padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; - MaybeStackBuffer stream_buf_; + + char stream_buf_[kAllocBufferSize]; }; class ExternalHeader : diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h old mode 100644 new mode 100755 index 49ec63b59bd581..0659cb65a36940 --- a/src/node_http2_core-inl.h +++ b/src/node_http2_core-inl.h @@ -221,10 +221,7 @@ inline int Nghttp2Session::Free() { 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); @@ -302,9 +299,9 @@ inline void Nghttp2Stream::ResetState( inline void Nghttp2Stream::Destroy() { DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); // Do nothing if this stream instance is already destroyed - if (IsDestroyed() || IsDestroying()) + if (IsDestroyed()) return; - flags_ |= NGHTTP2_STREAM_DESTROYING; + flags_ |= NGHTTP2_STREAM_DESTROYED; Nghttp2Session* session = this->session_; if (session != nullptr) { diff --git a/src/node_http2_core.h b/src/node_http2_core.h old mode 100644 new mode 100755 index 10acd7736b419f..3efeda69b58135 --- a/src/node_http2_core.h +++ b/src/node_http2_core.h @@ -65,9 +65,7 @@ enum nghttp2_stream_flags { // Stream is closed NGHTTP2_STREAM_CLOSED = 0x8, // Stream is destroyed - NGHTTP2_STREAM_DESTROYED = 0x10, - // Stream is being destroyed - NGHTTP2_STREAM_DESTROYING = 0x20 + NGHTTP2_STREAM_DESTROYED = 0x10 }; @@ -290,10 +288,6 @@ class Nghttp2Stream { 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, diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js old mode 100644 new mode 100755 index 1173b58e287de2..b01ed89de0a384 --- a/test/parallel/test-http2-options-max-reserved-streams.js +++ b/test/parallel/test-http2-options-max-reserved-streams.js @@ -51,6 +51,14 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`, options); + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + const req = client.request({ ':path': '/' }); // Because maxReservedRemoteStream is 1, the stream event @@ -59,15 +67,12 @@ server.on('listening', common.mustCall(() => { client.on('stream', common.mustCall((stream) => { stream.resume(); stream.on('end', common.mustCall()); + stream.on('streamClosed', common.mustCall(maybeClose)); })); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); - req.end(); - + req.on('end', common.mustCall()); + req.on('streamClosed', common.mustCall(maybeClose)); })); diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js old mode 100644 new mode 100755 index 088c675389f5ba..cd3a5d39d7af01 --- a/test/parallel/test-http2-response-splitting.js +++ b/test/parallel/test-http2-response-splitting.js @@ -65,7 +65,8 @@ server.listen(0, common.mustCall(() => { assert.strictEqual(headers.location, undefined); })); req.resume(); - req.on('end', common.mustCall(maybeClose)); + req.on('end', common.mustCall()); + req.on('streamClosed', common.mustCall(maybeClose)); } doTest(str, 'location', str); diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js old mode 100644 new mode 100755 index c10bbd0ccbe0c5..15b19ca1786f53 --- a/test/parallel/test-http2-server-socket-destroy.js +++ b/test/parallel/test-http2-server-socket-destroy.js @@ -36,7 +36,9 @@ function onStream(stream) { assert.notStrictEqual(stream.session, undefined); socket.destroy(); - assert.strictEqual(stream.session, undefined); + stream.on('destroy', common.mustCall(() => { + assert.strictEqual(stream.session, undefined); + })); } server.listen(0); @@ -49,9 +51,8 @@ server.on('listening', common.mustCall(() => { [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); req.on('aborted', common.mustCall()); + req.resume(); req.on('end', common.mustCall()); - req.on('response', common.mustCall()); - req.on('data', common.mustCall()); client.on('close', common.mustCall()); })); From 8dcad26bd3f255a1495c511d5ff52e80ed4b40d3 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 19 Jul 2017 07:26:15 -0700 Subject: [PATCH 12/26] http2: refinement and test for socketError Fixes: https://github.com/nodejs/http2/issues/184 Refines the `'socketError'` event a bit and adds a test for the emission of the `'socketError'` event on the server. Client side is tested separately --- doc/api/http2.md | 16 +++--- lib/internal/http2/core.js | 31 ++++------- .../parallel/test-http2-server-socketerror.js | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 29 deletions(-) create mode 100755 test/parallel/test-http2-server-socketerror.js diff --git a/doc/api/http2.md b/doc/api/http2.md index 9beb2cac922986..6bc1543c079f8a 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -254,9 +254,9 @@ 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`. +For `ServerHttp2Session` instances, a `'socketError'` event listener is always +registered that will, by default, forward the event on to the owning +`Http2Server` instance if no additional handlers are registered. #### Event: 'timeout' -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. +The `'socketError'` event is emitted when a `'socketError'` event is emitted by +an `Http2Session` associated with the server. #### Event: 'stream' -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. +The `'socketError'` event is emitted when a `'socketError'` event is emitted by +an `Http2Session` associated with the server. #### Event: 'unknownProtocol' * `fd` {number} A readable file descriptor * `headers` {[Headers Object][]} +* `options` {Object} + * `statCheck` {Function} + * `offset` {number} The offset position at which to begin reading + * `length` {number} The amount of data from the fd to send 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 @@ -1034,6 +1038,16 @@ server.on('stream', (stream) => { server.on('close', () => fs.closeSync(fd)); ``` +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 fd. If the `statCheck` function is provided, the +`http2stream.respondWithFD()` method will perform an `fs.fstat()` call to +collect details on the provided file descriptor. + +The `offset` and `length` options may be used to limit the response to a +specific range subset. This can be used, for instance, to support HTTP Range +requests. + #### http2stream.respondWithFile(path[, headers[, options]]) + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API](compatiblity-api). + #### Event: 'timeout' + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API](compatiblity-api). + #### Event: 'timeout' + +A `Http2ServerRequest` object is created by [`http2.Server`][] or +[`http2.SecureServer`][] and passed as the first argument to the [`'request'`][] event. It may be used to access a request status, +headers and data. + +It implements the [Readable Stream][] interface, as well as the +following additional events, methods, and properties. + +#### Event: 'aborted' + + +The `'aborted'` event is emitted whenever a `Http2ServerRequest` instance is +abnormally aborted in mid-communication. + +*Note*: The `'aborted'` event will only be emitted if the +`Http2ServerRequest` writable side has not been ended. + +#### Event: 'close' + + +Indicates that the underlying [`Http2Stream`]() was closed. +Just like `'end'`, this event occurs only once per response. + +#### request.destroy([error]) + + +* `error` {Error} + +Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If `error` +is provided, an `'error'` event is emitted and `error` is passed as an argument +to any listeners on the event. + +It does nothing if the stream was already destroyed. + +#### request.headers + + +* {Object} + +The request/response headers object. + +Key-value pairs of header names and values. Header names are lower-cased. +Example: + +```js +// Prints something like: +// +// { 'user-agent': 'curl/7.22.0', +// host: '127.0.0.1:8000', +// accept: '*/*' } +console.log(request.headers); +``` + +See [Headers Object][]. + +### request.httpVersion + + +* {string} + +In case of server request, the HTTP version sent by the client. In the case of +client response, the HTTP version of the connected-to server. Returns +`'2.0'`. + +Also `message.httpVersionMajor` is the first integer and +`message.httpVersionMinor` is the second. + +#### request.method + + +* {string} + +The request method as a string. Read only. Example: +`'GET'`, `'DELETE'`. + +#### request.rawHeaders + + +* {Array} + +The raw request/response headers list exactly as they were received. + +Note that the keys and values are in the same list. It is *not* a +list of tuples. So, the even-numbered offsets are key values, and the +odd-numbered offsets are the associated values. + +Header names are not lowercased, and duplicates are not merged. + +```js +// Prints something like: +// +// [ 'user-agent', +// 'this is invalid because there can be only one', +// 'User-Agent', +// 'curl/7.22.0', +// 'Host', +// '127.0.0.1:8000', +// 'ACCEPT', +// '*/*' ] +console.log(request.rawHeaders); +``` + +#### request.rawTrailers + + +* {Array} + +The raw request/response trailer keys and values exactly as they were +received. Only populated at the `'end'` event. + +#### request.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} + +Calls `request.connection.setTimeout(msecs, callback)`. + +Returns `request`. + +#### request.socket + + +* {net.Socket} + +The [`net.Socket`][] object associated with the connection. + +With TLS support, use [`request.socket.getPeerCertificate()`][] to obtain the +client's authentication details. + +*Note*: do not use this socket object to send or receive any data. All +data transfers are managed by HTTP/2 and data might be lost. + +#### request.stream + + +* {http2.Http2Stream} + +The [`Http2Stream`][] object backing the request. + +#### request.trailers + + +* {Object} + +The request/response trailers object. Only populated at the `'end'` event. + +#### request.url + + +* {string} + +Request URL string. This contains only the URL that is +present in the actual HTTP request. If the request is: + +```txt +GET /status?name=ryan HTTP/1.1\r\n +Accept: text/plain\r\n +\r\n +``` + +Then `request.url` will be: + + +```js +'/status?name=ryan' +``` + +To parse the url into its parts `require('url').parse(request.url)` +can be used. Example: + +```txt +$ node +> require('url').parse('/status?name=ryan') +Url { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '?name=ryan', + query: 'name=ryan', + pathname: '/status', + path: '/status?name=ryan', + href: '/status?name=ryan' } +``` + +To extract the parameters from the query string, the +`require('querystring').parse` function can be used, or +`true` can be passed as the second argument to `require('url').parse`. +Example: + +```txt +$ node +> require('url').parse('/status?name=ryan', true) +Url { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '?name=ryan', + query: { name: 'ryan' }, + pathname: '/status', + path: '/status?name=ryan', + href: '/status?name=ryan' } +``` + +### Class: http2.Http2ServerResponse + + +This object is created internally by an HTTP server--not by the user. It is +passed as the second parameter to the [`'request'`][] event. + +The response implements, but does not inherit from, the [Writable Stream][] +interface. This is an [`EventEmitter`][] with the following events: + +### Event: 'close' + + +Indicates that the underlying [`Http2Stream`]() was terminated before +[`response.end()`][] was called or able to flush. + +### Event: 'finish' + + +Emitted when the response has been sent. More specifically, this event is +emitted when the last segment of the response headers and body have been +handed off to the HTTP/2 multiplexing for transmission over the network. It +does not imply that the client has received anything yet. + +After this event, no more events will be emitted on the response object. + +### response.addTrailers(headers) + + +* `headers` {Object} + +This method adds HTTP trailing headers (a header but at the end of the +message) to the response. + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +### response.connection + + +* {net.Socket} + +See [`response.socket`][]. + +### response.end([data][, encoding][, callback]) + + +* `data` {string|Buffer} +* `encoding` {string} +* `callback` {Function} + +This method signals to the server that all of the response headers and body +have been sent; that server should consider this message complete. +The method, `response.end()`, MUST be called on each response. + +If `data` is specified, it is equivalent to calling +[`response.write(data, encoding)`][] followed by `response.end(callback)`. + +If `callback` is specified, it will be called when the response stream +is finished. + +### response.finished + + +* {boolean} + +Boolean value that indicates whether the response has completed. Starts +as `false`. After [`response.end()`][] executes, the value will be `true`. + +### response.getHeader(name) + + +* `name` {string} +* Returns: {string} + +Reads out a header that's already been queued but not sent to the client. +Note that the name is case insensitive. + +Example: + +```js +const contentType = response.getHeader('content-type'); +``` + +### response.getHeaderNames() + + +* Returns: {Array} + +Returns an array containing the unique names of the current outgoing headers. +All header names are lowercase. + +Example: + +```js +response.setHeader('Foo', 'bar'); +response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + +const headerNames = response.getHeaderNames(); +// headerNames === ['foo', 'set-cookie'] +``` + +### response.getHeaders() + + +* Returns: {Object} + +Returns a shallow copy of the current outgoing headers. Since a shallow copy +is used, array values may be mutated without additional calls to various +header-related http module methods. The keys of the returned object are the +header names and the values are the respective header values. All header names +are lowercase. + +*Note*: The object returned by the `response.getHeaders()` method _does not_ +prototypically inherit from the JavaScript `Object`. This means that typical +`Object` methods such as `obj.toString()`, `obj.hasOwnProperty()`, and others +are not defined and *will not work*. + +Example: + +```js +response.setHeader('Foo', 'bar'); +response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + +const headers = response.getHeaders(); +// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] } +``` + +### response.hasHeader(name) + + +* `name` {string} +* Returns: {boolean} + +Returns `true` if the header identified by `name` is currently set in the +outgoing headers. Note that the header name matching is case-insensitive. + +Example: + +```js +const hasContentType = response.hasHeader('content-type'); +``` + +### response.headersSent + + +* {boolean} + +Boolean (read-only). True if headers were sent, false otherwise. + +### response.removeHeader(name) + + +* `name` {string} + +Removes a header that's queued for implicit sending. + +Example: + +```js +response.removeHeader('Content-Encoding'); +``` + +### response.sendDate + + +* {boolean} + +When true, the Date header will be automatically generated and sent in +the response if it is not already present in the headers. Defaults to true. + +This should only be disabled for testing; HTTP requires the Date header +in responses. + +### response.setHeader(name, value) + + +* `name` {string} +* `value` {string | string[]} + +Sets a single header value for implicit headers. If this header already exists +in the to-be-sent headers, its value will be replaced. Use an array of strings +here to send multiple headers with the same name. + +Example: + +```js +response.setHeader('Content-Type', 'text/html'); +``` + +or + +```js +response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']); +``` + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +When headers have been set with [`response.setHeader()`][], they will be merged with +any headers passed to [`response.writeHead()`][], with the headers passed to +[`response.writeHead()`][] given precedence. + +```js +// returns content-type = text/plain +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Foo', 'bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + +### response.setTimeout(msecs[, callback]) + + +* `msecs` {number} +* `callback` {Function} + +Sets the [`Http2Stream`]()'s timeout value to `msecs`. If a callback is +provided, then it is added as a listener on the `'timeout'` event on +the response object. + +If no `'timeout'` listener is added to the request, the response, or +the server, then [`Http2Stream`]()s are destroyed when they time out. If a handler is +assigned to the request, the response, or the server's `'timeout'` events, +timed out sockets must be handled explicitly. + +Returns `response`. + +### response.socket + + +* {net.Socket} + +Reference to the underlying socket. Usually users will not want to access +this property. In particular, the socket will not emit `'readable'` events +because of how the protocol parser attaches to the socket. After +`response.end()`, the property is nulled. The `socket` may also be accessed +via `response.connection`. + +Example: + +```js +const http = require('http'); +const server = http.createServer((req, res) => { + const ip = req.socket.remoteAddress; + const port = req.socket.remotePort; + res.end(`Your IP address is ${ip} and your source port is ${port}.`); +}).listen(3000); +``` + +### response.statusCode + + +* {number} + +When using implicit headers (not calling [`response.writeHead()`][] explicitly), +this property controls the status code that will be sent to the client when +the headers get flushed. + +Example: + +```js +response.statusCode = 404; +``` + +After response header was sent to the client, this property indicates the +status code which was sent out. + +### response.statusMessage + + +* {string} + +Status message is not supported by HTTP/2 (RFC7540 8.1.2.4). It returns +an empty string. + +#### response.stream + + +* {http2.Http2Stream} + +The [`Http2Stream`][] object backing the response. + +### response.write(chunk[, encoding][, callback]) + + +* `chunk` {string|Buffer} +* `encoding` {string} +* `callback` {Function} +* Returns: {boolean} + +If this method is called and [`response.writeHead()`][] has not been called, +it will switch to implicit header mode and flush the implicit headers. + +This sends a chunk of the response body. This method may +be called multiple times to provide successive parts of the body. + +Note that in the `http` module, the response body is omitted when the +request is a HEAD request. Similarly, the `204` and `304` responses +_must not_ include a message body. + +`chunk` can be a string or a buffer. If `chunk` is a string, +the second parameter specifies how to encode it into a byte stream. +By default the `encoding` is `'utf8'`. `callback` will be called when this chunk +of data is flushed. + +*Note*: This is the raw HTTP body and has nothing to do with +higher-level multi-part body encodings that may be used. + +The first time [`response.write()`][] is called, it will send the buffered +header information and the first chunk of the body to the client. The second +time [`response.write()`][] is called, Node.js assumes data will be streamed, +and sends the new data separately. That is, the response is buffered up to the +first chunk of the body. + +Returns `true` if the entire data was flushed successfully to the kernel +buffer. Returns `false` if all or part of the data was queued in user memory. +`'drain'` will be emitted when the buffer is free again. + +### response.writeContinue() + + +Does nothing. Added for parity with [HTTP/1](). + +### response.writeHead(statusCode[, statusMessage][, headers]) + + +* `statusCode` {number} +* `statusMessage` {string} +* `headers` {Object} + +Sends a response header to the request. The status code is a 3-digit HTTP +status code, like `404`. The last argument, `headers`, are the response headers. +For compatibility with [HTTP/1](), one can give a human-readable `statusMessage` as the second argument, which will be silenty ignored and emit a warning. + +Example: + +```js +const body = 'hello world'; +response.writeHead(200, { + 'Content-Length': Buffer.byteLength(body), + 'Content-Type': 'text/plain' }); +``` + +This method must only be called once on a message and it must +be called before [`response.end()`][] is called. + +If [`response.write()`][] or [`response.end()`][] are called before calling +this, the implicit/mutable headers will be calculated and call this function. + +When headers have been set with [`response.setHeader()`][], they will be merged with +any headers passed to [`response.writeHead()`][], with the headers passed to +[`response.writeHead()`][] given precedence. + +```js +// returns content-type = text/plain +const server = http2.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Foo', 'bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + +Note that Content-Length is given in bytes not characters. The above example +works because the string `'hello world'` contains only single byte characters. +If the body contains higher coded characters then `Buffer.byteLength()` +should be used to determine the number of bytes in a given encoding. +And Node.js does not check whether Content-Length and the length of the body +which has been transmitted are equal or not. + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +### response.createPushResponse(headers, callback) + + +Call [`stream.pushStream()`]() with the given headers, and wraps the +given newly created [`Http2Stream`] on `Http2ServerRespose`. +The callback will be called with an error with code `ERR_HTTP2_STREAM_CLOSED` +if the stream is closed. [HTTP/2]: https://tools.ietf.org/html/rfc7540 [HTTP/1]: http.html +[https]: https.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 +[alpn-negotiation]: #http2_alpn_negotiation [`Duplex`]: stream.html#stream_class_stream_duplex [Headers Object]: #http2_headers_object [`Http2Stream`]: #http2_class_http2stream @@ -1720,3 +2485,7 @@ TBD [Using options.selectPadding]: #http2_using_options_selectpadding [error code]: #error_codes [`'unknownProtocol'`]: #http2_event_unknownprotocol +[`'request'`]: #http2_event_request +[Readable Stream]: stream.html#stream_class_stream_readable +[`ServerRequest`]: #http2_class_server_request +[`stream.pushStream()`]: #http2_stream-pushstream diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index c258079356183d..1d93ae14a0bfa2 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -192,7 +192,7 @@ class Http2ServerRequest extends Readable { if (stream) { stream.resume(); } else { - throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED')); } } @@ -391,6 +391,18 @@ class Http2ServerResponse extends Stream { this[kBeginSend](); } + get statusMessage() { + if (statusMessageWarned === false) { + process.emitWarning( + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', + 'UnsupportedWarning' + ); + statusMessageWarned = true; + } + + return ''; + } + writeHead(statusCode, statusMessage, headers) { if (typeof statusMessage === 'string' && statusMessageWarned === false) { process.emitWarning( @@ -411,6 +423,7 @@ class Http2ServerResponse extends Stream { } } this.statusCode = statusCode; + // TODO mcollina this should probably call sendInfo } write(chunk, encoding, cb) { @@ -497,7 +510,8 @@ class Http2ServerResponse extends Stream { createPushResponse(headers, callback) { const stream = this[kStream]; if (stream === undefined) { - throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED')); + return; } stream.pushStream(headers, {}, function(stream, headers, options) { const response = new Http2ServerResponse(stream); @@ -529,6 +543,11 @@ class Http2ServerResponse extends Stream { this[kStream] = undefined; this.emit('finish'); } + + // added for parity with HTTP/1 + writeContinue() { + // TODO mcollina this should probably be sendContinue + } } function onServerStream(stream, headers, flags) { diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js index 68e438d62ff96d..9679215abb0772 100644 --- a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -22,6 +22,16 @@ const server = h2.createServer((request, response) => { assert.strictEqual(push.stream.id % 2, 0); push.end(pushExpect); response.end(); + + // wait for a tick, so the stream is actually closed + setImmediate(function() { + response.createPushResponse({ + ':path': '/pushed', + ':method': 'GET' + }, common.mustCall((error) => { + assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_CLOSED'); + })); + }); })); }); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js new file mode 100644 index 00000000000000..50d2970d9ea95f --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js @@ -0,0 +1,47 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.statusMessage should warn + +const unsupportedWarned = common.mustCall(1); +process.on('warning', ({ name, message }) => { + const expectedMessage = + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)'; + if (name === 'UnsupportedWarning' && message === expectedMessage) + unsupportedWarned(); +}); + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.on('finish', common.mustCall(function() { + assert.strictEqual(response.statusMessage, ''); + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); From 2f51e7d087e18a46de1f33dd3bd4f441f9b9562a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 24 Jul 2017 19:04:53 +0100 Subject: [PATCH 16/26] http2: make writeHead behave like HTTP/1. PR-URL: https://github.com/nodejs/http2/pull/186 Reviewed-By: James M Snell --- doc/api/http2.md | 2 +- lib/internal/http2/compat.js | 43 +++++++------------ .../test-http2-compat-serverresponse-end.js | 1 - ...ttp2-compat-serverresponse-flushheaders.js | 11 ++++- ...t-http2-compat-serverresponse-writehead.js | 6 ++- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/doc/api/http2.md b/doc/api/http2.md index 6f106c69c1b18b..bf2f6a3c526e7b 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -2402,7 +2402,7 @@ buffer. Returns `false` if all or part of the data was queued in user memory. added: REPLACEME --> -Does nothing. Added for parity with [HTTP/1](). +Throws an error as the `'continue'` flow is not current implemented. Added for parity with [HTTP/1](). ### response.writeHead(statusCode[, statusMessage][, headers]) - -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' A `Http2ServerRequest` object is created by [`http2.Server`][] or -[`http2.SecureServer`][] and passed as the first argument to the [`'request'`][] event. It may be used to access a request status, -headers and data. +[`http2.SecureServer`][] and passed as the first argument to the +[`'request'`][] event. It may be used to access a request status, headers and +data. It implements the [Readable Stream][] interface, as well as the following additional events, methods, and properties. @@ -1907,9 +1908,9 @@ added: REPLACEME * `error` {Error} -Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If `error` -is provided, an `'error'` event is emitted and `error` is passed as an argument -to any listeners on the event. +Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If +`error` is provided, an `'error'` event is emitted and `error` is passed as an +argument to any listeners on the event. It does nothing if the stream was already destroyed. @@ -2201,7 +2202,7 @@ added: REPLACEME * `name` {string} * Returns: {string} -Reads out a header that's already been queued but not sent to the client. +Reads out a header that has already been queued but not sent to the client. Note that the name is case insensitive. Example: @@ -2291,7 +2292,7 @@ added: REPLACEME * `name` {string} -Removes a header that's queued for implicit sending. +Removes a header that has been queued for implicit sending. Example: @@ -2339,9 +2340,9 @@ response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']); Attempting to set a header field name or value that contains invalid characters will result in a [`TypeError`][] being thrown. -When headers have been set with [`response.setHeader()`][], they will be merged with -any headers passed to [`response.writeHead()`][], with the headers passed to -[`response.writeHead()`][] given precedence. +When headers have been set with [`response.setHeader()`][], they will be merged +with any headers passed to [`response.writeHead()`][], with the headers passed +to [`response.writeHead()`][] given precedence. ```js // returns content-type = text/plain @@ -2366,9 +2367,9 @@ provided, then it is added as a listener on the `'timeout'` event on the response object. If no `'timeout'` listener is added to the request, the response, or -the server, then [`Http2Stream`]()s are destroyed when they time out. If a handler is -assigned to the request, the response, or the server's `'timeout'` events, -timed out sockets must be handled explicitly. +the server, then [`Http2Stream`]()s are destroyed when they time out. If a +handler is assigned to the request, the response, or the server's `'timeout'` +events, timed out sockets must be handled explicitly. Returns `response`. @@ -2478,7 +2479,8 @@ buffer. Returns `false` if all or part of the data was queued in user memory. added: REPLACEME --> -Throws an error as the `'continue'` flow is not current implemented. Added for parity with [HTTP/1](). +Throws an error as the `'continue'` flow is not current implemented. Added for +parity with [HTTP/1](). ### response.writeHead(statusCode[, statusMessage][, headers])