diff --git a/.gitignore b/.gitignore index 7904d2a..bc48362 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules .idea coverage doc +.vscode/.browse* +npm-debug.log +typings \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..efab07f --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +test +.travis.yml diff --git a/HISTORY.md b/HISTORY.md index 081826a..20bd0c3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,31 @@ Version history =============== +### 3.3.6 (2016-09-16) ### +* We were not appropriately sending HPACK context updates when receiving SETTINGS_HEADER_TABLE_SIZE. This release fixes that bug. + +### 3.3.5 (2016-09-06) ### +* Fix issues with large DATA frames (https://github.com/molnarg/node-http2/issues/207) + +### 3.3.4 (2016-04-22) ### +* More PR bugfixes (https://github.com/molnarg/node-http2/issues?q=milestone%3Av3.3.4) + +### 3.3.3 (2016-04-21) ### + +* Bugfixes from pull requests (https://github.com/molnarg/node-http2/search?q=milestone%3Av3.3.3&type=Issues&utf8=%E2%9C%93) + +### 3.3.2 (2016-01-11) ### + +* Fix an incompatibility with Firefox (issue 167) + +### 3.3.1 (2016-01-11) ### + +* Fix some DoS bugs (issues 145, 146, 147, and 148) + +### 3.3.0 (2016-01-10) ### + +* Bugfix updates from pull requests + ### 3.2.0 (2015-02-19) ### * Update ALPN token to final RFC version (h2). diff --git a/LICENSE b/LICENSE index 55c4878..9bb2e9c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (C) 2013 Gábor Molnár +Copyright (C) 2013 Gábor Molnár , Google Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -20,4 +20,3 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index d021049..fa57ae9 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,7 @@ For a server push example, see the source code of the example Status ------ -* ALPN is not yet supported in node.js (see - [this issue](https://github.com/joyent/node/issues/5945)). For ALPN support, you will have to use - [Shigeki Ohtsu's node.js fork](https://github.com/shigeki/node/tree/alpn_support) until this code - gets merged upstream. +* ALPN is only supported in node.js >= 5.0 * Upgrade mechanism to start HTTP/2 over unencrypted channel is not implemented yet (issue [#4](https://github.com/molnarg/node-http2/issues/4)) * Other minor features found in diff --git a/example/server.js b/example/server.js index 455e259..66d8f89 100644 --- a/example/server.js +++ b/example/server.js @@ -30,6 +30,17 @@ function onRequest(request, response) { fileStream.on('finish',response.end); } + // Example for testing large (boundary-sized) frames. + else if (request.url === "/largeframe") { + response.writeHead(200); + var body = 'a'; + for (var i = 0; i < 14; i++) { + body += body; + } + body = body + 'a'; + response.end(body); + } + // Otherwise responding with 404. else { response.writeHead(404); diff --git a/lib/http.js b/lib/http.js index ef1024e..ccc4363 100644 --- a/lib/http.js +++ b/lib/http.js @@ -11,17 +11,15 @@ // ------------------------------------ // // - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation -// see the [lib/endpoint.js](endpoint.html) file. +// see [protocol/endpoint.js](protocol/endpoint.html). // // - **Class: http2.Server** // - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of -// HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the +// HTTP/2 was successful: the reference to the [Endpoint](protocol/endpoint.html) object tied to the // socket. // // - **http2.createServer(options, [requestListener])**: additional option: // - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object -// - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of -// TLS // // - **Class: http2.ServerResponse** // - **response.push(options)**: initiates a server push. `options` describes the 'imaginary' @@ -33,20 +31,17 @@ // - **new Agent(options)**: additional option: // - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object // - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests. -// - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections. +// - **agent.endpoints**: contains [Endpoint](protocol/endpoint.html) objects for HTTP/2 connections. // -// - **http2.request(options, [callback])**: additional option: -// - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use -// the raw TCP stream for HTTP/2 +// - **http2.request(options, [callback])**: +// - similar to http.request // -// - **http2.get(options, [callback])**: additional option: -// - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use -// the raw TCP stream for HTTP/2 -// - this performs request and then also calls request.end() for request instance +// - **http2.get(options, [callback])**: +// - similar to http.get // // - **Class: http2.ClientRequest** // - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference -// to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket). +// to the associated [HTTP/2 Stream](protocol/stream.html) object (and not to the TCP socket). // - **Event: 'push' (promise)**: signals the intention of a server push associated to this // request. `promise` is an IncomingPromise. If there's no listener for this event, the server // push is cancelled. @@ -57,7 +52,7 @@ // - has two subclasses for easier interface description: **IncomingRequest** and // **IncomingResponse** // - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated -// [HTTP/2 Stream](stream.html) object (and not to the TCP socket). +// [HTTP/2 Stream](protocol/stream.html) object (and not to the TCP socket). // // - **Class: http2.IncomingRequest (IncomingMessage)** // - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the @@ -92,11 +87,11 @@ // but will function normally when falling back to using HTTP/1.1. // // - **Class: http2.Server** -// - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue]) +// - **Event: 'checkContinue'**: not in the spec // - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2 // - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive // (PING frames) -// - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect]) +// - **Event: 'connect'**: not yet supported // - **server.setTimeout(msecs, [callback])** // - **server.timeout** // @@ -124,11 +119,9 @@ // - **Event: 'close'** // - **message.setTimeout(timeout, [callback])** // -// [1]: http://nodejs.org/api/https.html -// [2]: http://nodejs.org/api/http.html -// [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.4 -// [expect-continue]: https://github.com/http2/http2-spec/issues/18 -// [connect]: https://github.com/http2/http2-spec/issues/230 +// [1]: https://nodejs.org/api/https.html +// [2]: https://nodejs.org/api/http.html +// [3]: https://tools.ietf.org/html/rfc7540#section-8.1.2.4 // Common server and client side code // ================================== @@ -155,7 +148,6 @@ var deprecatedHeaders = [ 'host', 'keep-alive', 'proxy-connection', - 'te', 'transfer-encoding', 'upgrade' ]; @@ -163,7 +155,7 @@ var deprecatedHeaders = [ // When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback var supportedProtocols = [protocol.VERSION, 'http/1.1', 'http/1.0']; -// Ciphersuite list based on the recommendations of http://wiki.mozilla.org/Security/Server_Side_TLS +// Ciphersuite list based on the recommendations of https://wiki.mozilla.org/Security/Server_Side_TLS // The only modification is that kEDH+AESGCM were placed after DHE and ECDHE suites var cipherSuites = [ 'ECDHE-RSA-AES128-GCM-SHA256', @@ -227,7 +219,7 @@ exports.serializers = protocol.serializers; // --------------------- function IncomingMessage(stream) { - // * This is basically a read-only wrapper for the [Stream](stream.html) class. + // * This is basically a read-only wrapper for the [Stream](protocol/stream.html) class. PassThrough.call(this); stream.pipe(this); this.socket = this.stream = stream; @@ -251,7 +243,7 @@ function IncomingMessage(stream) { } IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } }); -// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.3) +// [Request Header Fields](https://tools.ietf.org/html/rfc7540#section-8.1.2.3) // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series // of key-value pairs. This includes the target URI for the request, the status code for the // response, as well as HTTP header fields. @@ -262,7 +254,11 @@ IncomingMessage.prototype._onHeaders = function _onHeaders(headers) { // * Store the _regular_ headers in `this.headers` for (var name in headers) { if (name[0] !== ':') { - this.headers[name] = headers[name]; + if (name === 'set-cookie' && !Array.isArray(headers[name])) { + this.headers[name] = [headers[name]]; + } else { + this.headers[name] = headers[name]; + } } } @@ -290,12 +286,13 @@ IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key IncomingMessage.prototype._validateHeaders = function _validateHeaders(headers) { // * An HTTP/2.0 request or response MUST NOT include any of the following header fields: - // Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server + // Connection, Host, Keep-Alive, Proxy-Connection, Transfer-Encoding, and Upgrade. A server // MUST treat the presence of any of these header fields as a stream error of type // PROTOCOL_ERROR. + // If the TE header is present, it's only valid value is 'trailers' for (var i = 0; i < deprecatedHeaders.length; i++) { var key = deprecatedHeaders[i]; - if (key in headers) { + if (key in headers || (key === 'te' && headers[key] !== 'trailers')) { this._log.error({ key: key, value: headers[key] }, 'Deprecated header found'); this.stream.reset('PROTOCOL_ERROR'); return; @@ -322,7 +319,7 @@ IncomingMessage.prototype._validateHeaders = function _validateHeaders(headers) // --------------------- function OutgoingMessage() { - // * This is basically a read-only wrapper for the [Stream](stream.html) class. + // * This is basically a read-only wrapper for the [Stream](protocol/stream.html) class. Writable.call(this); this._headers = {}; @@ -360,11 +357,11 @@ OutgoingMessage.prototype._finish = function _finish() { OutgoingMessage.prototype.setHeader = function setHeader(name, value) { if (this.headersSent) { - throw new Error('Can\'t set headers after they are sent.'); + return this.emit('error', new Error('Can\'t set headers after they are sent.')); } else { name = name.toLowerCase(); if (deprecatedHeaders.indexOf(name) !== -1) { - throw new Error('Cannot set deprecated header: ' + name); + return this.emit('error', new Error('Cannot set deprecated header: ' + name)); } this._headers[name] = value; } @@ -372,7 +369,7 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) { OutgoingMessage.prototype.removeHeader = function removeHeader(name) { if (this.headersSent) { - throw new Error('Can\'t remove headers after they are sent.'); + return this.emit('error', new Error('Can\'t remove headers after they are sent.')); } else { delete this._headers[name.toLowerCase()]; } @@ -398,6 +395,36 @@ exports.IncomingRequest = IncomingRequest; exports.OutgoingResponse = OutgoingResponse; exports.ServerResponse = OutgoingResponse; // for API compatibility +// Forward events `event` on `source` to all listeners on `target`. +// +// Note: The calling context is `source`. +function forwardEvent(event, source, target) { + function forward() { + var listeners = target.listeners(event); + + var n = listeners.length; + + // Special case for `error` event with no listeners. + if (n === 0 && event === 'error') { + var args = [event]; + args.push.apply(args, arguments); + + target.emit.apply(target, args); + return; + } + + for (var i = 0; i < n; ++i) { + listeners[i].apply(source, arguments); + } + } + + source.on(event, forward); + + // A reference to the function is necessary to be able to stop + // forwarding. + return forward; +} + // Server class // ------------ @@ -432,6 +459,9 @@ function Server(options) { } }); this._server.on('request', this.emit.bind(this, 'request')); + + forwardEvent('error', this._server, this); + forwardEvent('listening', this._server, this); } // HTTP2 over plain TCP @@ -470,7 +500,7 @@ Server.prototype._start = function _start(socket) { // Some conformance to Node.js Https specs allows to distinguish clients: request.remoteAddress = socket.remoteAddress; request.remotePort = socket.remotePort; - request.socket = socket; + request.connection = request.socket = response.socket = socket; request.once('ready', self.emit.bind(self, 'request', request, response)); }); @@ -498,11 +528,13 @@ Server.prototype._fallback = function _fallback(socket) { // There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to // the backing TCP or HTTPS server. -// [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback +// [1]: https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback Server.prototype.listen = function listen(port, hostname) { this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) }, 'Listening for incoming connections'); this._server.listen.apply(this._server, arguments); + + return this._server; }; Server.prototype.close = function close(callback) { @@ -537,9 +569,9 @@ Object.defineProperty(Server.prototype, 'timeout', { // `server` to `this` since that means a listener. Instead, we forward the subscriptions. Server.prototype.on = function on(event, listener) { if ((event === 'upgrade') || (event === 'timeout')) { - this._server.on(event, listener && listener.bind(this)); + return this._server.on(event, listener && listener.bind(this)); } else { - EventEmitter.prototype.on.call(this, event, listener); + return EventEmitter.prototype.on.call(this, event, listener); } }; @@ -550,6 +582,10 @@ Server.prototype.addContext = function addContext(hostname, credentials) { } }; +Server.prototype.address = function address() { + return this._server.address() +}; + function createServerRaw(options, requestListener) { if (typeof options === 'function') { requestListener = options; @@ -616,7 +652,7 @@ function IncomingRequest(stream) { } IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } }); -// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.3) +// [Request Header Fields](https://tools.ietf.org/html/rfc7540#section-8.1.2.3) // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series // of key-value pairs. This includes the target URI for the request, the status code for the // response, as well as HTTP header fields. @@ -635,6 +671,10 @@ IncomingRequest.prototype._onHeaders = function _onHeaders(headers) { this.scheme = this._checkSpecialHeader(':scheme' , headers[':scheme']); this.host = this._checkSpecialHeader(':authority', headers[':authority'] ); this.url = this._checkSpecialHeader(':path' , headers[':path'] ); + if (!this.method || !this.scheme || !this.host || !this.url) { + // This is invalid, and we've sent a RST_STREAM, so don't continue processing + return; + } // * Host header is included in the headers object for backwards compatibility. this.headers.host = this.host; @@ -699,6 +739,10 @@ OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() { } }; +OutgoingResponse.prototype._implicitHeader = function() { + this._implicitHeaders(); +}; + OutgoingResponse.prototype.write = function write() { this._implicitHeaders(); return OutgoingMessage.prototype.write.apply(this, arguments); @@ -773,7 +817,12 @@ function requestRaw(options, callback) { if (options.protocol && options.protocol !== "http:") { throw new Error('This interface only supports http-schemed URLs'); } - return (options.agent || exports.globalAgent).request(options, callback); + if (options.agent && typeof(options.agent.request) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.request(agentOptions, callback); + } + return exports.globalAgent.request(options, callback); } function requestTLS(options, callback) { @@ -784,7 +833,12 @@ function requestTLS(options, callback) { if (options.protocol && options.protocol !== "https:") { throw new Error('This interface only supports https-schemed URLs'); } - return (options.agent || exports.globalAgent).request(options, callback); + if (options.agent && typeof(options.agent.request) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.request(agentOptions, callback); + } + return exports.globalAgent.request(options, callback); } function getRaw(options, callback) { @@ -795,7 +849,12 @@ function getRaw(options, callback) { if (options.protocol && options.protocol !== "http:") { throw new Error('This interface only supports http-schemed URLs'); } - return (options.agent || exports.globalAgent).get(options, callback); + if (options.agent && typeof(options.agent.get) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.get(agentOptions, callback); + } + return exports.globalAgent.get(options, callback); } function getTLS(options, callback) { @@ -806,7 +865,12 @@ function getTLS(options, callback) { if (options.protocol && options.protocol !== "https:") { throw new Error('This interface only supports https-schemed URLs'); } - return (options.agent || exports.globalAgent).get(options, callback); + if (options.agent && typeof(options.agent.get) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.get(agentOptions, callback); + } + return exports.globalAgent.get(options, callback); } // Agent class @@ -814,6 +878,7 @@ function getTLS(options, callback) { function Agent(options) { EventEmitter.call(this); + this.setMaxListeners(0); options = util._extend({}, options); @@ -849,7 +914,7 @@ Agent.prototype.request = function request(options, callback) { if (!options.plain && options.protocol === 'http:') { this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1'); - throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.'); + this.emit('error', new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.')); } var request = new OutgoingRequest(this._log); @@ -863,6 +928,7 @@ Agent.prototype.request = function request(options, callback) { options.host, options.port ].join(':'); + var self = this; // * There's an existing HTTP/2 connection to this host if (key in this.endpoints) { @@ -879,6 +945,16 @@ Agent.prototype.request = function request(options, callback) { localAddress: options.localAddress }); + endpoint.socket.on('error', function (error) { + self._log.error('Socket error: ' + error.toString()); + request.emit('error', error); + }); + + endpoint.on('error', function(error){ + self._log.error('Connection error: ' + error.toString()); + request.emit('error', error); + }); + this.endpoints[key] = endpoint; endpoint.pipe(endpoint.socket).pipe(endpoint); request._start(endpoint.createStream(), options); @@ -899,6 +975,12 @@ Agent.prototype.request = function request(options, callback) { } var httpsRequest = https.request(options); + httpsRequest.on('error', function (error) { + self._log.error('Socket error: ' + error.toString()); + self.removeAllListeners(key); + request.emit('error', error); + }); + httpsRequest.on('socket', function(socket) { var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol; if (negotiatedProtocol != null) { // null in >=0.11.0, undefined in <0.11.0 @@ -908,7 +990,6 @@ Agent.prototype.request = function request(options, callback) { } }); - var self = this; function negotiated() { var endpoint; var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol; @@ -957,6 +1038,15 @@ Agent.prototype.get = function get(options, callback) { return request; }; +Agent.prototype.destroy = function(error) { + if (this._httpsAgent) { + this._httpsAgent.destroy(); + } + for (var key in this.endpoints) { + this.endpoints[key].close(error); + } +}; + function unbundleSocket(socket) { socket.removeAllListeners('data'); socket.removeAllListeners('end'); @@ -1118,7 +1208,7 @@ function IncomingResponse(stream) { } IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } }); -// [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.4) +// [Response Header Fields](https://tools.ietf.org/html/rfc7540#section-8.1.2.4) // * `headers` argument: HTTP/2.0 request and response header fields carry information as a series // of key-value pairs. This includes the target URI for the request, the status code for the // response, as well as HTTP header fields. diff --git a/lib/index.js b/lib/index.js index 60981c2..c67883d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,4 @@ -// [node-http2][homepage] is an [HTTP/2 (draft 16)][http2] implementation for [node.js][node]. +// [node-http2][homepage] is an [HTTP/2][http2] implementation for [node.js][node]. // // The core of the protocol is implemented in the protocol sub-directory. This directory provides // two important features on top of the protocol: @@ -10,10 +10,10 @@ // (which is in turn very similar to the [HTTP module API][node-http]). // // [homepage]: https://github.com/molnarg/node-http2 -// [http2]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16 -// [node]: http://nodejs.org/ -// [node-https]: http://nodejs.org/api/https.html -// [node-http]: http://nodejs.org/api/http.html +// [http2]: https://tools.ietf.org/html/rfc7540 +// [node]: https://nodejs.org/ +// [node-https]: https://nodejs.org/api/https.html +// [node-http]: https://nodejs.org/api/http.html module.exports = require('./http'); diff --git a/lib/protocol/compressor.js b/lib/protocol/compressor.js index 5aacae5..3923a91 100644 --- a/lib/protocol/compressor.js +++ b/lib/protocol/compressor.js @@ -11,9 +11,9 @@ // provide a layer between the [framer](framer.html) and the // [connection handling component](connection.html). // -// [node-transform]: http://nodejs.org/api/stream.html#stream_class_stream_transform -// [node-objectmode]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options -// [http2-compression]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07 +// [node-transform]: https://nodejs.org/api/stream.html#stream_class_stream_transform +// [node-objectmode]: https://nodejs.org/api/stream.html#stream_new_stream_readable_options +// [http2-compression]: https://tools.ietf.org/html/rfc7541 exports.HeaderTable = HeaderTable; exports.HuffmanTable = HuffmanTable; @@ -35,8 +35,8 @@ var util = require('util'); // The [Header Table] is a component used to associate headers to index values. It is basically an // ordered list of `[name, value]` pairs, so it's implemented as a subclass of `Array`. // In this implementation, the Header Table and the [Static Table] are handled as a single table. -// [Header Table]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#section-3.1.2 -// [Static Table]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#appendix-B +// [Header Table]: https://tools.ietf.org/html/rfc7541#section-2.3.2 +// [Static Table]: https://tools.ietf.org/html/rfc7541#section-2.3.1 function HeaderTable(log, limit) { var self = HeaderTable.staticTable.map(entryFromPair); self._log = log; @@ -70,7 +70,7 @@ function size(entry) { } // The `add(index, entry)` can be used to [manage the header table][tablemgmt]: -// [tablemgmt]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#section-3.3 +// [tablemgmt]: https://tools.ietf.org/html/rfc7541#section-4 // // * it pushes the new `entry` at the beggining of the table // * before doing such a modification, it has to be ensured that the header table size will stay @@ -115,9 +115,8 @@ HeaderTable.prototype.setSizeLimit = function setSizeLimit(limit) { this._enforceLimit(this._limit); }; -// [The Static Table](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#appendix-B) +// [The Static Table](https://tools.ietf.org/html/rfc7541#section-2.3.1) // ------------------ -// [statictable]:http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#appendix-B // The table is generated with feeding the table from the spec to the following sed command: // @@ -208,14 +207,14 @@ function HeaderSetDecompressor(log, table) { // `_transform` is the implementation of the [corresponding virtual function][_transform] of the // TransformStream class. It collects the data chunks for later processing. -// [_transform]: http://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback +// [_transform]: https://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback HeaderSetDecompressor.prototype._transform = function _transform(chunk, encoding, callback) { this._chunks.push(chunk); callback(); }; // `execute(rep)` executes the given [header representation][representation]. -// [representation]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#section-3.1.4 +// [representation]: https://tools.ietf.org/html/rfc7541#section-6 // The *JavaScript object representation* of a header representation: // @@ -281,7 +280,7 @@ HeaderSetDecompressor.prototype._execute = function _execute(rep) { // `_flush` is the implementation of the [corresponding virtual function][_flush] of the // TransformStream class. The whole decompressing process is done in `_flush`. It gets called when // the input stream is over. -// [_flush]: http://nodejs.org/api/stream.html#stream_transform_flush_callback +// [_flush]: https://nodejs.org/api/stream.html#stream_transform_flush_callback HeaderSetDecompressor.prototype._flush = function _flush(callback) { var buffer = concat(this._chunks); @@ -327,7 +326,7 @@ HeaderSetCompressor.prototype.send = function send(rep) { // `_transform` is the implementation of the [corresponding virtual function][_transform] of the // TransformStream class. It processes the input headers one by one: -// [_transform]: http://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback +// [_transform]: https://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, callback) { var name = pair[0].toLowerCase(); var value = pair[1]; @@ -373,12 +372,12 @@ HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, c // `_flush` is the implementation of the [corresponding virtual function][_flush] of the // TransformStream class. It gets called when there's no more header to compress. The final step: -// [_flush]: http://nodejs.org/api/stream.html#stream_transform_flush_callback +// [_flush]: https://nodejs.org/api/stream.html#stream_transform_flush_callback HeaderSetCompressor.prototype._flush = function _flush(callback) { callback(); }; -// [Detailed Format](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#section-4) +// [Detailed Format](https://tools.ietf.org/html/rfc7541#section-5) // ----------------- // ### Integer representation ### @@ -1091,11 +1090,20 @@ function Compressor(log, type) { assert((type === 'REQUEST') || (type === 'RESPONSE')); this._table = new HeaderTable(this._log); + + this.tableSizeChangePending = false; + this.lowestTableSizePending = 0; + this.tableSizeSetting = DEFAULT_HEADER_TABLE_LIMIT; } // Changing the header table size Compressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) { this._table.setSizeLimit(size); + if (!this.tableSizeChangePending || size < this.lowestTableSizePending) { + this.lowestTableSizePending = size; + } + this.tableSizeSetting = size; + this.tableSizeChangePending = true; }; // `compress` takes a header set, and compresses it using a new `HeaderSetCompressor` stream @@ -1103,6 +1111,16 @@ Compressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) { // but the API becomes simpler. Compressor.prototype.compress = function compress(headers) { var compressor = new HeaderSetCompressor(this._log, this._table); + + if (this.tableSizeChangePending) { + if (this.lowestTableSizePending < this.tableSizeSetting) { + compressor.send({contextUpdate: true, newMaxSize: this.lowestTableSizePending, + name: "", value: "", index: 0}); + } + compressor.send({contextUpdate: true, newMaxSize: this.tableSizeSetting, + name: "", value: "", index: 0}); + this.tableSizeChangePending = false; + } var colonHeaders = []; var nonColonHeaders = []; diff --git a/lib/protocol/connection.js b/lib/protocol/connection.js index d917096..2b86b7f 100644 --- a/lib/protocol/connection.js +++ b/lib/protocol/connection.js @@ -353,6 +353,27 @@ Connection.prototype._receive = function _receive(frame, done) { this._onFirstFrameReceived(frame); } + // Do some sanity checking here before we create a stream + if ((frame.type == 'SETTINGS' || + frame.type == 'PING' || + frame.type == 'GOAWAY') && + frame.stream != 0) { + // Got connection-level frame on a stream - EEP! + this.close('PROTOCOL_ERROR'); + return; + } else if ((frame.type == 'DATA' || + frame.type == 'HEADERS' || + frame.type == 'PRIORITY' || + frame.type == 'RST_STREAM' || + frame.type == 'PUSH_PROMISE' || + frame.type == 'CONTINUATION') && + frame.stream == 0) { + // Got stream-level frame on connection - EEP! + this.close('PROTOCOL_ERROR'); + return; + } + // WINDOW_UPDATE can be on either stream or connection + // * gets the appropriate stream from the stream registry var stream = this._streamIds[frame.stream]; diff --git a/lib/protocol/flow.js b/lib/protocol/flow.js index ec4cdc4..4ec5649 100644 --- a/lib/protocol/flow.js +++ b/lib/protocol/flow.js @@ -5,7 +5,7 @@ var assert = require('assert'); // Flow is a [Duplex stream][1] subclass which implements HTTP/2 flow control. It is designed to be // subclassed by [Connection](connection.html) and the `upstream` component of [Stream](stream.html). -// [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex +// [1]: https://nodejs.org/api/stream.html#stream_class_stream_duplex var Duplex = require('stream').Duplex; @@ -19,7 +19,7 @@ exports.Flow = Flow; // * **setInitialWindow(size)**: the initial flow control window size can be changed *any time* // ([as described in the standard][1]) using this method // -// [1]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.9.2 +// [1]: https://tools.ietf.org/html/rfc7540#section-6.9.2 // API for child classes // --------------------- @@ -42,7 +42,7 @@ exports.Flow = Flow; // * **read(limit): frame**: like the regular `read`, but the 'flow control size' (0 for non-DATA // frames, length of the payload for DATA frames) of the returned frame will be under `limit`. // Small exception: pass -1 as `limit` if the max. flow control size is 0. `read(0)` means the -// same thing as [in the original API](http://nodejs.org/api/stream.html#stream_stream_read_0). +// same thing as [in the original API](https://nodejs.org/api/stream.html#stream_stream_read_0). // // * **getLastQueuedFrame(): frame**: returns the last frame in output buffers // @@ -79,7 +79,7 @@ Flow.prototype._receive = function _receive(frame, callback) { // `_receive` is called by `_write` which in turn is [called by Duplex][1] when someone `write()`s // to the flow. It emits the 'receiving' event and notifies the window size tracking code if the // incoming frame is a WINDOW_UPDATE. -// [1]: http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 +// [1]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 Flow.prototype._write = function _write(frame, encoding, callback) { var sentToUs = (this._flowControlId === undefined) || (frame.stream === this._flowControlId); @@ -147,7 +147,7 @@ Flow.prototype._send = function _send() { // `_send` is called by `_read` which is in turn [called by Duplex][1] when it wants to have more // items in the output queue. -// [1]: http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 +// [1]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 Flow.prototype._read = function _read() { // * if the flow control queue is empty, then let the user push more frames if (this._queue.length === 0) { @@ -167,7 +167,7 @@ Flow.prototype._read = function _read() { } while (moreNeeded && (this._queue.length > 0)); this._readableState.sync = false; - assert((moreNeeded == false) || // * output queue is full + assert((!moreNeeded) || // * output queue is full (this._queue.length === 0) || // * flow control queue is empty (!this._window && (this._queue[0].type === 'DATA'))); // * waiting for window update } @@ -249,8 +249,9 @@ Flow.prototype._parentPush = function _parentPush(frame) { // did not push the whole frame to the output queue (but maybe it did push part of the frame). Flow.prototype._push = function _push(frame) { var data = frame && (frame.type === 'DATA') && frame.data; + var maxFrameLength = (this._window < 16384) ? this._window : 16384; - if (!data || (data.length <= this._window)) { + if (!data || (data.length <= maxFrameLength)) { return this._parentPush(frame); } @@ -261,12 +262,12 @@ Flow.prototype._push = function _push(frame) { else { this._log.trace({ frame: frame, size: frame.data.length, forwardable: this._window }, 'Splitting out forwardable part of a DATA frame.'); - frame.data = data.slice(this._window); + frame.data = data.slice(maxFrameLength); this._parentPush({ type: 'DATA', flags: {}, stream: frame.stream, - data: data.slice(0, this._window) + data: data.slice(0, maxFrameLength) }); return null; } @@ -323,7 +324,9 @@ Flow.prototype._increaseWindow = function _increaseWindow(size) { this._log.error('Flow control window grew too large.'); this.emit('error', 'FLOW_CONTROL_ERROR'); } else { - this.emit('window_update'); + if (size != 0) { + this.emit('window_update'); + } } } }; diff --git a/lib/protocol/framer.js b/lib/protocol/framer.js index 1b32236..244e60a 100644 --- a/lib/protocol/framer.js +++ b/lib/protocol/framer.js @@ -1,7 +1,7 @@ // The framer consists of two [Transform Stream][1] subclasses that operate in [object mode][2]: // the Serializer and the Deserializer -// [1]: http://nodejs.org/api/stream.html#stream_class_stream_transform -// [2]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options +// [1]: https://nodejs.org/api/stream.html#stream_class_stream_transform +// [2]: https://nodejs.org/api/stream.html#stream_new_stream_readable_options var assert = require('assert'); var Transform = require('stream').Transform; @@ -146,10 +146,10 @@ Deserializer.prototype._transform = function _transform(chunk, encoding, done) { done(); }; -// [Frame Header](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-4.1) +// [Frame Header](https://tools.ietf.org/html/rfc7540#section-4.1) // -------------------------------------------------------------- // -// HTTP/2.0 frames share a common base format consisting of a 9-byte header followed by 0 to 2^24 - 1 +// HTTP/2 frames share a common base format consisting of a 9-byte header followed by 0 to 2^24 - 1 // bytes of data. // // Additional size limits can be set by specific application uses. HTTP limits the frame size to @@ -235,6 +235,10 @@ Serializer.commonHeader = function writeCommonHeader(frame, buffers) { }; Deserializer.commonHeader = function readCommonHeader(buffer, frame) { + if (buffer.length < 9) { + return 'FRAME_SIZE_ERROR'; + } + var totallyWastedByte = buffer.readUInt8(0); var length = buffer.readUInt16BE(1); // We do this just for sanity checking later on, to make sure no one sent us a @@ -269,7 +273,7 @@ Deserializer.commonHeader = function readCommonHeader(buffer, frame) { // * `typeSpecificAttributes`: a register of frame specific frame object attributes (used by // logging code and also serves as documentation for frame objects) -// [DATA Frames](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.1) +// [DATA Frames](https://tools.ietf.org/html/rfc7540#section-6.1) // ------------------------------------------------------------ // // DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with a @@ -297,18 +301,26 @@ Deserializer.DATA = function readData(buffer, frame) { var dataOffset = 0; var paddingLength = 0; if (frame.flags.PADDED) { + if (buffer.length < 1) { + // We must have at least one byte for padding control, but we don't. Bad peer! + return 'FRAME_SIZE_ERROR'; + } paddingLength = (buffer.readUInt8(dataOffset) & 0xff); dataOffset = 1; } if (paddingLength) { + if (paddingLength >= (buffer.length - 1)) { + // We don't have enough room for the padding advertised - bad peer! + return 'FRAME_SIZE_ERROR'; + } frame.data = buffer.slice(dataOffset, -1 * paddingLength); } else { frame.data = buffer.slice(dataOffset); } }; -// [HEADERS](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.2) +// [HEADERS](https://tools.ietf.org/html/rfc7540#section-6.2) // -------------------------------------------------------------- // // The HEADERS frame (type=0x1) allows the sender to create a stream. @@ -365,6 +377,18 @@ Serializer.HEADERS = function writeHeadersPriority(frame, buffers) { }; Deserializer.HEADERS = function readHeadersPriority(buffer, frame) { + var minFrameLength = 0; + if (frame.flags.PADDED) { + minFrameLength += 1; + } + if (frame.flags.PRIORITY) { + minFrameLength += 5; + } + if (buffer.length < minFrameLength) { + // Peer didn't send enough data - bad peer! + return 'FRAME_SIZE_ERROR'; + } + var dataOffset = 0; var paddingLength = 0; if (frame.flags.PADDED) { @@ -384,13 +408,17 @@ Deserializer.HEADERS = function readHeadersPriority(buffer, frame) { } if (paddingLength) { + if ((buffer.length - dataOffset) < paddingLength) { + // Not enough data left to satisfy the advertised padding - bad peer! + return 'FRAME_SIZE_ERROR'; + } frame.data = buffer.slice(dataOffset, -1 * paddingLength); } else { frame.data = buffer.slice(dataOffset); } }; -// [PRIORITY](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.3) +// [PRIORITY](https://tools.ietf.org/html/rfc7540#section-6.3) // ------------------------------------------------------- // // The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream. @@ -427,6 +455,10 @@ Serializer.PRIORITY = function writePriority(frame, buffers) { }; Deserializer.PRIORITY = function readPriority(buffer, frame) { + if (buffer.length < 5) { + // PRIORITY frames are 5 bytes long. Bad peer! + return 'FRAME_SIZE_ERROR'; + } var dependencyData = new Buffer(4); buffer.copy(dependencyData, 0, 0, 4); frame.exclusiveDependency = !!(dependencyData[0] & 0x80); @@ -435,7 +467,7 @@ Deserializer.PRIORITY = function readPriority(buffer, frame) { frame.priorityWeight = buffer.readUInt8(4); }; -// [RST_STREAM](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.4) +// [RST_STREAM](https://tools.ietf.org/html/rfc7540#section-6.4) // ----------------------------------------------------------- // // The RST_STREAM frame (type=0x3) allows for abnormal termination of a stream. @@ -466,6 +498,10 @@ Serializer.RST_STREAM = function writeRstStream(frame, buffers) { }; Deserializer.RST_STREAM = function readRstStream(buffer, frame) { + if (buffer.length < 4) { + // RST_STREAM is 4 bytes long. Bad peer! + return 'FRAME_SIZE_ERROR'; + } frame.error = errorCodes[buffer.readUInt32BE(0)]; if (!frame.error) { // Unknown error codes are considered equivalent to INTERNAL_ERROR @@ -473,7 +509,7 @@ Deserializer.RST_STREAM = function readRstStream(buffer, frame) { } }; -// [SETTINGS](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.5) +// [SETTINGS](https://tools.ietf.org/html/rfc7540#section-6.5) // ------------------------------------------------------- // // The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints @@ -580,7 +616,7 @@ definedSettings[4] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false }; // indicates the maximum size of a frame the receiver will allow. definedSettings[5] = { name: 'SETTINGS_MAX_FRAME_SIZE', flag: false }; -// [PUSH_PROMISE](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.6) +// [PUSH_PROMISE](https://tools.ietf.org/html/rfc7540#section-6.6) // --------------------------------------------------------------- // // The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams the @@ -626,22 +662,31 @@ Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) { }; Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) { + if (buffer.length < 4) { + return 'FRAME_SIZE_ERROR'; + } var dataOffset = 0; var paddingLength = 0; if (frame.flags.PADDED) { + if (buffer.length < 5) { + return 'FRAME_SIZE_ERROR'; + } paddingLength = (buffer.readUInt8(dataOffset) & 0xff); dataOffset = 1; } frame.promised_stream = buffer.readUInt32BE(dataOffset) & 0x7fffffff; dataOffset += 4; if (paddingLength) { + if ((buffer.length - dataOffset) < paddingLength) { + return 'FRAME_SIZE_ERROR'; + } frame.data = buffer.slice(dataOffset, -1 * paddingLength); } else { frame.data = buffer.slice(dataOffset); } }; -// [PING](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.7) +// [PING](https://tools.ietf.org/html/rfc7540#section-6.7) // ----------------------------------------------- // // The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the @@ -671,7 +716,7 @@ Deserializer.PING = function readPing(buffer, frame) { frame.data = buffer; }; -// [GOAWAY](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.8) +// [GOAWAY](https://tools.ietf.org/html/rfc7540#section-6.8) // --------------------------------------------------- // // The GOAWAY frame (type=0x7) informs the remote peer to stop creating streams on this connection. @@ -714,6 +759,10 @@ Serializer.GOAWAY = function writeGoaway(frame, buffers) { }; Deserializer.GOAWAY = function readGoaway(buffer, frame) { + if (buffer.length !== 8) { + // GOAWAY must have 8 bytes + return 'FRAME_SIZE_ERROR'; + } frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff; frame.error = errorCodes[buffer.readUInt32BE(4)]; if (!frame.error) { @@ -722,7 +771,7 @@ Deserializer.GOAWAY = function readGoaway(buffer, frame) { } }; -// [WINDOW_UPDATE](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.9) +// [WINDOW_UPDATE](https://tools.ietf.org/html/rfc7540#section-6.9) // ----------------------------------------------------------------- // // The WINDOW_UPDATE frame (type=0x8) is used to implement flow control. @@ -760,7 +809,7 @@ Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) { } }; -// [CONTINUATION](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.10) +// [CONTINUATION](https://tools.ietf.org/html/rfc7540#section-6.10) // ------------------------------------------------------------ // // The CONTINUATION frame (type=0x9) is used to continue a sequence of header block fragments. @@ -785,7 +834,7 @@ Deserializer.CONTINUATION = function readContinuation(buffer, frame) { frame.data = buffer; }; -// [ALTSVC](http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06#section-4) +// [ALTSVC](https://tools.ietf.org/html/rfc7838#section-4) // ------------------------------------------------------------ // // The ALTSVC frame (type=0xA) advertises the availability of an alternative service to the client. @@ -810,13 +859,13 @@ frameFlags.ALTSVC = []; // octets, of the Origin field. // // Origin: An OPTIONAL sequence of characters containing ASCII -// serialisation of an origin ([RFC6454](http://tools.ietf.org/html/rfc6454), +// serialisation of an origin ([RFC6454](https://tools.ietf.org/html/rfc6454), // Section 6.2) that the alternate service is applicable to. // // Alt-Svc-Field-Value: A sequence of octets (length determined by // subtracting the length of all preceding fields from the frame // length) containing a value identical to the Alt-Svc field value -// defined in (Section 3)[http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06#section-3] +// defined in (Section 3)[https://tools.ietf.org/html/rfc7838#section-3] // (ABNF production "Alt-Svc"). typeSpecificAttributes.ALTSVC = ['maxAge', 'port', 'protocolID', 'host', @@ -986,7 +1035,13 @@ function unescape(s) { } Deserializer.ALTSVC = function readAltSvc(buffer, frame) { + if (buffer.length < 2) { + return 'FRAME_SIZE_ERROR'; + } var originLength = buffer.readUInt16BE(0); + if ((buffer.length - 2) < originLength) { + return 'FRAME_SIZE_ERROR'; + } frame.origin = buffer.toString('ascii', 2, 2 + originLength); var fieldValue = buffer.toString('ascii', 2 + originLength); var values = parseHeaderValue(fieldValue, ',', splitHeaderParameters); @@ -1034,7 +1089,7 @@ Serializer.BLOCKED = function writeBlocked(frame, buffers) { Deserializer.BLOCKED = function readBlocked(buffer, frame) { }; -// [Error Codes](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-7) +// [Error Codes](https://tools.ietf.org/html/rfc7540#section-7) // ------------------------------------------------------------ var errorCodes = [ diff --git a/lib/protocol/index.js b/lib/protocol/index.js index 39f48f4..0f3720e 100644 --- a/lib/protocol/index.js +++ b/lib/protocol/index.js @@ -1,4 +1,4 @@ -// [node-http2-protocol][homepage] is an implementation of the [HTTP/2 (draft 16)][http2] +// This is an implementation of the [HTTP/2][http2] // framing layer for [node.js][node]. // // The main building blocks are [node.js streams][node-stream] that are connected through pipes. @@ -14,7 +14,7 @@ // lifecycle and settings, and responsible for enforcing the connection level limits (flow // control, initiated stream limit) // -// * [Stream](stream.html): implementation of the [HTTP/2 stream concept](http2-stream). +// * [Stream](stream.html): implementation of the [HTTP/2 stream concept][http2-stream]. // Implements the [stream state machine][http2-streamstate] defined by the standard, provides // management methods and events for using the stream (sending/receiving headers, data, etc.), // and enforces stream level constraints (flow control, sending only legal frames). @@ -27,15 +27,14 @@ // * [Serializer and Deserializer](framer.html): the lowest layer in the stack that transforms // between the binary and the JavaScript object representation of HTTP/2 frames // -// [homepage]: https://github.com/molnarg/node-http2 -// [http2]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16 -// [http2-connheader]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-3.5 -// [http2-stream]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5 -// [http2-streamstate]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.1 -// [node]: http://nodejs.org/ -// [node-stream]: http://nodejs.org/api/stream.html -// [node-https]: http://nodejs.org/api/https.html -// [node-http]: http://nodejs.org/api/http.html +// [http2]: https://tools.ietf.org/html/rfc7540 +// [http2-connheader]: https://tools.ietf.org/html/rfc7540#section-3.5 +// [http2-stream]: https://tools.ietf.org/html/rfc7540#section-5 +// [http2-streamstate]: https://tools.ietf.org/html/rfc7540#section-5.1 +// [node]: https://nodejs.org/ +// [node-stream]: https://nodejs.org/api/stream.html +// [node-https]: https://nodejs.org/api/https.html +// [node-http]: https://nodejs.org/api/http.html exports.VERSION = 'h2'; diff --git a/lib/protocol/stream.js b/lib/protocol/stream.js index c42de87..6d520b9 100644 --- a/lib/protocol/stream.js +++ b/lib/protocol/stream.js @@ -3,8 +3,8 @@ var assert = require('assert'); // The Stream class // ================ -// Stream is a [Duplex stream](http://nodejs.org/api/stream.html#stream_class_stream_duplex) -// subclass that implements the [HTTP/2 Stream](http://http2.github.io/http2-spec/#rfc.section.3.4) +// Stream is a [Duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) +// subclass that implements the [HTTP/2 Stream](https://tools.ietf.org/html/rfc7540#section-5) // concept. It has two 'sides': one that is used by the user to send/receive data (the `stream` // object itself) and one that is used by a Connection to read/write frames to/from the other peer // (`stream.upstream`). @@ -40,7 +40,7 @@ exports.Stream = Stream; // that are to be sent/arrived to/from the peer and are related to this stream. // // Headers are always in the [regular node.js header format][1]. -// [1]: http://nodejs.org/api/http.html#http_message_headers +// [1]: https://nodejs.org/api/http.html#http_message_headers // Constructor // ----------- @@ -182,7 +182,7 @@ Stream.prototype.altsvc = function altsvc(host, port, protocolID, maxAge, origin // [Flow](flow.html). The [Connection](connection.html) object instantiating the stream will read // and write frames to/from it. The stream itself is a regular [Duplex stream][1], and is used by // the user to write or read the body of the request. -// [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex +// [1]: https://nodejs.org/api/stream.html#stream_class_stream_duplex // upstream side stream user side // @@ -352,7 +352,7 @@ Stream.prototype._finishing = function _finishing() { } }; -// [Stream States](http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.1) +// [Stream States](https://tools.ietf.org/html/rfc7540#section-5.1) // ---------------- // // +--------+ @@ -567,8 +567,9 @@ Stream.prototype._transition = function transition(sending, frame) { // can be used to close any of those streams. case 'CLOSED': if (PRIORITY || (sending && RST_STREAM) || + (receiving && WINDOW_UPDATE) || (receiving && this._closedByUs && - (this._closedWithRst || WINDOW_UPDATE || RST_STREAM || ALTSVC))) { + (this._closedWithRst || RST_STREAM || ALTSVC))) { /* No state change */ } else { streamError = 'STREAM_CLOSED'; @@ -624,7 +625,7 @@ Stream.prototype._transition = function transition(sending, frame) { // * When sending something invalid, throwing an exception, since it is probably a bug. if (sending) { this._log.error(info, 'Sending illegal frame.'); - throw new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.'); + return this.emit('error', new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.')); } // * In case of a serious problem, emitting and error and letting someone else handle it diff --git a/package.json b/package.json index 68f715b..5372f17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http2", - "version": "3.2.0", + "version": "3.3.6", "description": "An HTTP/2 client and server implementation", "main": "lib/index.js", "engines" : { @@ -15,7 +15,7 @@ }, "scripts": { "test": "istanbul test _mocha -- --reporter spec --slow 500 --timeout 15000", - "doc": "docco lib/* --output doc --layout parallel --css doc/docco.css" + "doc": "docco lib/* --output doc --layout parallel --template root.jst --css doc/docco.css && docco lib/protocol/* --output doc/protocol --layout parallel --template protocol.jst --css doc/docco.css" }, "repository": { "type": "git", diff --git a/test/flow.js b/test/flow.js index a77507a..a077c68 100644 --- a/test/flow.js +++ b/test/flow.js @@ -3,7 +3,7 @@ var util = require('./util'); var Flow = require('../lib/protocol/flow').Flow; -var MAX_PAYLOAD_SIZE = 4096; +var MAX_PAYLOAD_SIZE = 16384; function createFlow(log) { var flowControlId = util.random(10, 100); diff --git a/test/http.js b/test/http.js index c1b440b..95a074e 100644 --- a/test/http.js +++ b/test/http.js @@ -3,6 +3,7 @@ var util = require('./util'); var fs = require('fs'); var path = require('path'); var url = require('url'); +var net = require('net'); var http2 = require('../lib/http'); var https = require('https'); @@ -38,6 +39,36 @@ describe('http.js', function() { }).to.throw(Error); }); }); + describe('method `listen()`', function () { + it('should emit `listening` event', function (done) { + var server = http2.createServer(serverOptions); + + server.on('listening', function () { + server.close(); + + done(); + }) + + server.listen(0); + }); + it('should emit `error` on failure', function (done) { + var server = http2.createServer(serverOptions); + + // This TCP server is used to explicitly take a port to make + // server.listen() fails. + var net = require('net').createServer(); + + server.on('error', function () { + net.close() + + done(); + }); + + net.listen(0, function () { + server.listen(this.address().port); + }); + }); + }); describe('property `timeout`', function() { it('should be a proxy for the backing HTTPS server\'s `timeout` property', function() { var server = new http2.Server(serverOptions); @@ -318,6 +349,9 @@ describe('http.js', function() { response.removeHeader('nonexistent'); expect(response.getHeader('nonexistent')).to.equal(undefined); + // A set-cookie header which should always be an array + response.setHeader('set-cookie', 'foo'); + // Don't send date response.sendDate = false; @@ -344,6 +378,8 @@ describe('http.js', function() { request.on('response', function(response) { expect(response.headers[headerName]).to.equal(headerValue); expect(response.headers['nonexistent']).to.equal(undefined); + expect(response.headers['set-cookie']).to.an.instanceof(Array) + expect(response.headers['set-cookie']).to.deep.equal(['foo']) expect(response.headers['date']).to.equal(undefined); response.on('data', function(data) { expect(data.toString()).to.equal(message); @@ -613,6 +649,26 @@ describe('http.js', function() { }); }); }); + it('should expose net.Socket as .socket and .connection', function(done) { + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.socket).to.equal(request.connection); + expect(request.socket).to.be.instanceof(net.Socket); + response.write('Pong'); + response.end(); + done(); + }); + + server.listen(1248, 'localhost', function() { + var request = https.request({ + host: 'localhost', + port: 1248, + path: '/', + ca: serverOptions.cert + }); + request.write('Ping'); + request.end(); + }); + }); }); describe('request and response with trailers', function() { it('should work as expected', function(done) { @@ -646,6 +702,52 @@ describe('http.js', function() { }); }); }); + describe('Handle socket error', function () { + it('HTTPS on Connection Refused error', function (done) { + var path = '/x'; + var request = http2.request('https://127.0.0.1:6666' + path); + + request.on('error', function (err) { + expect(err.errno).to.equal('ECONNREFUSED'); + done(); + }); + + request.on('response', function (response) { + server._server._handle.destroy(); + + response.on('data', util.noop); + + response.once('end', function () { + done(new Error('Request should have failed')); + }); + }); + + request.end(); + + }); + it('HTTP on Connection Refused error', function (done) { + var path = '/x'; + + var request = http2.raw.request('http://127.0.0.1:6666' + path); + + request.on('error', function (err) { + expect(err.errno).to.equal('ECONNREFUSED'); + done(); + }); + + request.on('response', function (response) { + server._server._handle.destroy(); + + response.on('data', util.noop); + + response.once('end', function () { + done(new Error('Request should have failed')); + }); + }); + + request.end(); + }); + }); describe('server push', function() { it('should work as expected', function(done) { var path = '/x'; diff --git a/test/stream.js b/test/stream.js index 87af55f..90e0ef6 100644 --- a/test/stream.js +++ b/test/stream.js @@ -192,7 +192,7 @@ describe('stream.js', function() { stream.headers({}); stream.end(); stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); - example_frames.slice(1).forEach(function(invalid_frame) { + example_frames.slice(2).forEach(function(invalid_frame) { invalid_frame.count_change = util.noop; expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); });