From 71e474ba9d1cb7f26e234ce348c15e90718ea80c Mon Sep 17 00:00:00 2001 From: Peter Marton Date: Tue, 12 Sep 2017 18:43:39 +0200 Subject: [PATCH] feat(http2): add HTTP/2 support --- docs/_api/request.md | 18 +- docs/_api/response.md | 31 +- docs/_api/server.md | 2 + examples/http2/http2.js | 33 + examples/http2/keys/http2-cert.pem | 15 + examples/http2/keys/http2-csr.pem | 12 + examples/http2/keys/http2-key.pem | 16 + lib/request.js | 1545 ++++++++++++++-------------- lib/response.js | 1438 +++++++++++++------------- lib/server.js | 28 +- test/keys/http2-cert.pem | 15 + test/keys/http2-csr.pem | 12 + test/keys/http2-key.pem | 16 + test/serverHttp2.test.js | 111 ++ 14 files changed, 1792 insertions(+), 1500 deletions(-) create mode 100644 examples/http2/http2.js create mode 100644 examples/http2/keys/http2-cert.pem create mode 100644 examples/http2/keys/http2-csr.pem create mode 100644 examples/http2/keys/http2-key.pem create mode 100644 test/keys/http2-cert.pem create mode 100644 test/keys/http2-csr.pem create mode 100644 test/keys/http2-key.pem create mode 100644 test/serverHttp2.test.js diff --git a/docs/_api/request.md b/docs/_api/request.md index e7efcb3b5..a4c1a55de 100644 --- a/docs/_api/request.md +++ b/docs/_api/request.md @@ -40,7 +40,7 @@ permalink: /docs/request-api/ **Extends http.IncomingMessage** Wraps all of the node -[http.IncomingMessage](https://nodejs.org/api/http.html#http_http_incomingmessage) +[http.IncomingMessage](https://nodejs.org/api/http.html) APIs, events and properties, plus the following. ### accepts @@ -55,8 +55,8 @@ Otherwise the given type is matched by an exact match, and then subtypes. **Examples** -_You may pass the subtype such as html which is then converted internally to -text/html using the mime lookup table:_ +_You may pass the subtype such as html which is then converted internally +to text/html using the mime lookup table:_ ```javascript // Accept: text/html @@ -95,8 +95,8 @@ Returns **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refer ### getContentType -Returns the value of the content-type header. If a content-type is not set, -this will return a default value of `application/octet-stream` +Returns the value of the content-type header. If a content-type is not +set, this will return a default value of `application/octet-stream` Returns **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** @@ -300,8 +300,8 @@ Returns **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refer Start the timer for a request handler. By default, restify uses calls this automatically for all handlers registered in your handler chain. -However, this can be called manually for nested functions inside the handler -chain to record timing information. +However, this can be called manually for nested functions inside the +handler chain to record timing information. **Parameters** @@ -310,8 +310,8 @@ chain to record timing information. **Examples** _You must explicitly invoke -endHandlerTimer() after invoking this function. Otherwise timing information -will be inaccurate._ +endHandlerTimer() after invoking this function. Otherwise timing +information will be inaccurate._ ```javascript server.get('/', function fooHandler(req, res, next) { diff --git a/docs/_api/response.md b/docs/_api/response.md index 1e543e4e7..a2d026a7d 100644 --- a/docs/_api/response.md +++ b/docs/_api/response.md @@ -27,7 +27,7 @@ permalink: /docs/response-api/ **Extends http.ServerResponse** Wraps all of the node -[http.ServerResponse](https://nodejs.org/docs/latest/api/http.html#http.ServerResponse) +[http.ServerResponse](https://nodejs.org/docs/latest/api/http.html) APIs, events and properties, plus the following. ### cache @@ -154,10 +154,10 @@ formatter based on the `content-type` header. _You can use send() to wrap up all the usual writeHead(), write(), end() calls on the HTTP API of node. -You can pass send either a `code` and `body`, or just a body. body can be an -`Object`, a `Buffer`, or an `Error`. -When you call `send()`, restify figures out how to format the response based -on the `content-type`._ +You can pass send either a `code` and `body`, or just a body. body can be +an `Object`, a `Buffer`, or an `Error`. +When you call `send()`, restify figures out how to format the response +based on the `content-type`._ ```javascript res.send({hello: 'world'}); @@ -169,8 +169,8 @@ Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refer ### sendRaw -Like `res.send()`, but skips formatting. This can be useful when the payload -has already been preformatted. +Like `res.send()`, but skips formatting. This can be useful when the +payload has already been preformatted. Sends the response object. pass through to internal `__send` that skips formatters entirely and sends the content as is. @@ -189,7 +189,8 @@ Uses `header()` underneath the hood, enabling multi-value headers. **Parameters** -- `name` **([String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object))** name of the header or `Object` of headers +- `name` **([String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object))** name of the header or + `Object` of headers - `val` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** value of the header **Examples** @@ -236,13 +237,15 @@ Redirect is sugar method for redirecting. - `options.hostname` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** redirect location's hostname - `options.pathname` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** redirect location's pathname - `options.port` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** redirect location's port number - - `options.query` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** redirect location's query string parameters - - `options.overrideQuery` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** if true, `options.query` stomps over - any existing query parameters on current URL. - by default, will merge the two. + - `options.query` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** redirect location's query string + parameters + - `options.overrideQuery` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** if true, `options.query` + stomps over any existing query + parameters on current URL. + by default, will merge the two. - `options.permanent` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** if true, sets 301. defaults to 302. -- `next` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** mandatory, to complete the response and trigger audit - logger. +- `next` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** mandatory, to complete the response and trigger + audit logger. **Examples** diff --git a/docs/_api/server.md b/docs/_api/server.md index 92839dbfa..d60281eb2 100644 --- a/docs/_api/server.md +++ b/docs/_api/server.md @@ -116,6 +116,8 @@ Creates a new Server. response header, default is `restify`. Pass empty string to unset the header. (optional, default `false`) - `options.spdy` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** Any options accepted by [node-spdy](https://github.com/indutny/node-spdy). + - `options.http2` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** Any options accepted by + [http2.createSecureServer](https://nodejs.org/api/http2.html). - `options.handleUpgrades` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Hook the `upgrade` event from the node HTTP server, pushing `Connection: Upgrade` requests through the regular request handling chain. (optional, default `false`) diff --git a/examples/http2/http2.js b/examples/http2/http2.js new file mode 100644 index 000000000..9491335fa --- /dev/null +++ b/examples/http2/http2.js @@ -0,0 +1,33 @@ +var path = require('path'); +var fs = require('fs'); +var bunyan = require('bunyan'); +var restify = require('../../lib'); + +var srv = restify.createServer({ + http2: { + cert: fs.readFileSync(path.join(__dirname, './keys/http2-cert.pem')), + key: fs.readFileSync(path.join(__dirname, './keys/http2-key.pem')), + ca: fs.readFileSync(path.join(__dirname, 'keys/http2-csr.pem')) + } +}); + +srv.get('/', function(req, res, next) { + res.send({ hello: 'world' }); + next(); +}); + +srv.on( + 'after', + restify.plugins.auditLogger({ + event: 'after', + body: true, + log: bunyan.createLogger({ + name: 'audit', + stream: process.stdout + }) + }) +); + +srv.listen(8080, function() { + console.log('ready on %s', srv.url); +}); diff --git a/examples/http2/keys/http2-cert.pem b/examples/http2/keys/http2-cert.pem new file mode 100644 index 000000000..2f13995ff --- /dev/null +++ b/examples/http2/keys/http2-cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICHzCCAYgCCQCPPSUAa8QZojANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJS +VTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMET21zazEhMB8GA1UEChMY +SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTExMDQwOTEwMDY0NVoXDTExMDUw +OTEwMDY0NVowVDELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxDTAL +BgNVBAcTBE9tc2sxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1bn25sPkv46wl70BffxradlkRd/x +p5Xf8HDhPSfzNNctERYslXT2fX7Dmfd5w1XTVqqGqJ4izp5VewoVOHA8uavo3ovp +gNWasil5zADWaM1T0nnV0RsFbZWzOTmm1U3D48K8rW3F5kOZ6f4yRq9QT1gF/gN7 +5Pt494YyYyJu/a8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQBuRZisIViI2G/R+w79 +vk21TzC/cJ+O7tKsseDqotXYTH8SuimEH5IWcXNgnWhNzczwN8s2362NixyvCipV +yd4wzMpPbjIhnWGM0hluWZiK2RxfcqimIBjDParTv6CMUIuwGQ257THKY8hXGg7j +Uws6Lif3P9UbsuRiYPxMgg98wg== +-----END CERTIFICATE----- + diff --git a/examples/http2/keys/http2-csr.pem b/examples/http2/keys/http2-csr.pem new file mode 100644 index 000000000..b4d764fdc --- /dev/null +++ b/examples/http2/keys/http2-csr.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBkzCB/QIBADBUMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEN +MAsGA1UEBxMET21zazEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF +3/Gnld/wcOE9J/M01y0RFiyVdPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+je +i+mA1ZqyKXnMANZozVPSedXRGwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+ +A3vk+3j3hjJjIm79rwIDAQABoAAwDQYJKoZIhvcNAQEFBQADgYEAiNWhz6EppIVa +FfUaB3sLeqfamb9tg9kBHtvqj/FJni0snqms0kPWaTySEPHZF0irIb7VVdq/sVCb +3gseMVSyoDvPJ4lHC3PXqGQ7kM1mIPhDnR/4HDA3BhlGhTXSDIHgZnvI+HMBdsyC +hC3dz5odyKqe4nmoofomALkBL9t4H8s= +-----END CERTIFICATE REQUEST----- + diff --git a/examples/http2/keys/http2-key.pem b/examples/http2/keys/http2-key.pem new file mode 100644 index 000000000..957810910 --- /dev/null +++ b/examples/http2/keys/http2-key.pem @@ -0,0 +1,16 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF3/Gnld/wcOE9J/M01y0RFiyV +dPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+jei+mA1ZqyKXnMANZozVPSedXR +GwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+A3vk+3j3hjJjIm79rwIDAQAB +AoGAAv2QI9h32epQND9TxwSCKD//dC7W/cZOFNovfKCTeZjNK6EIzKqPTGA6smvR +C1enFl5adf+IcyWqAoe4lkqTvurIj+2EhtXdQ8DBlVuXKr3xvEFdYxXPautdTCF6 +KbXEyS/s1TZCRFjYftvCrXxc3pK45AQX/wg7z1K+YB5pyIECQQD0OJvLoxLYoXAc +FZraIOZiDsEbGuSHqoCReFXH75EC3+XGYkH2bQ/nSIZ0h1buuwQ/ylKXOlTPT3Qt +Xm1OQEBvAkEA4AjWsIO/rRpOm/Q2aCrynWMpoUXTZSbL2yGf8pxp/+8r2br5ier0 +M1LeBb/OPY1+k39NWLXxQoo64xoSFYk2wQJAd2wDCwX4HkR7HNCXw1hZL9QFK6rv +20NN0VSlpboJD/3KT0MW/FiCcVduoCbaJK0Au+zEjDyy4hj5N4I4Mw6KMwJAXVAx +I+psTsxzS4/njXG+BgIEl/C+gRYsuMQDnAi8OebDq/et8l0Tg8ETSu++FnM18neG +ntmBeMacinUUbTXuwQJBAJp/onZdsMzeVulsGrqR1uS+Lpjc5Q1gt5ttt2cxj91D +rio48C/ZvWuKNE8EYj2ALtghcVKRvgaWfOxt2GPguGg= +-----END RSA PRIVATE KEY----- + diff --git a/lib/request.js b/lib/request.js index 807cfb45e..85d035880 100644 --- a/lib/request.js +++ b/lib/request.js @@ -2,7 +2,6 @@ 'use strict'; -var http = require('http'); var url = require('url'); var sprintf = require('util').format; @@ -13,10 +12,6 @@ var uuid = require('uuid'); var dtrace = require('./dtrace'); -///--- Globals - -var Request = http.IncomingMessage; - ///-- Helpers /** * Creates and sets negotiator on request if one doesn't already exist, @@ -45,805 +40,823 @@ function negotiator(req) { ///--- API /** - * Wraps all of the node - * [http.IncomingMessage] - * (https://nodejs.org/api/http.html#http_http_incomingmessage) - * APIs, events and properties, plus the following. - * @class Request - * @extends http.IncomingMessage - */ - -///--- Patches - -/** - * Builds an absolute URI for the request. - * - * @private - * @memberof Request - * @instance - * @function absoluteUri - * @param {String} path - a url path - * @returns {String} uri - */ -Request.prototype.absoluteUri = function absoluteUri(path) { - assert.string(path, 'path'); - - var protocol = this.isSecure() ? 'https://' : 'http://'; - var hostname = this.headers.host; - return url.resolve(protocol + hostname + this.path() + '/', path); -}; +* Patch Request object and extends with extra functionalities +* +* @private +* @function patch +* @param {http.IncomingMessage|http2.Http2ServerRequest} Request - +* Server Request +* @returns {undefined} No return value +*/ +function patch(Request) { + /** + * Wraps all of the node + * [http.IncomingMessage](https://nodejs.org/api/http.html) + * APIs, events and properties, plus the following. + * @class Request + * @extends http.IncomingMessage + */ + + ///--- Patches + + /** + * Builds an absolute URI for the request. + * + * @private + * @memberof Request + * @instance + * @function absoluteUri + * @param {String} path - a url path + * @returns {String} uri + */ + Request.prototype.absoluteUri = function absoluteUri(path) { + assert.string(path, 'path'); + + var protocol = this.isSecure() ? 'https://' : 'http://'; + var hostname = this.headers.host; + return url.resolve(protocol + hostname + this.path() + '/', path); + }; + + /** + * Check if the Accept header is present, and includes the given type. + * When the Accept header is not present true is returned. + * Otherwise the given type is matched by an exact match, and then subtypes. + * + * @public + * @memberof Request + * @instance + * @function accepts + * @param {String | String[]} types - an array of accept type headers + * @returns {Boolean} is accepteed + * @example + * + * You may pass the subtype such as html which is then converted internally + * to text/html using the mime lookup table: + * + * // Accept: text/html + * req.accepts('html'); + * // => true + * + * // Accept: text/*; application/json + * req.accepts('html'); + * req.accepts('text/html'); + * req.accepts('text/plain'); + * req.accepts('application/json'); + * // => true + * + * req.accepts('image/png'); + * req.accepts('png'); + * // => false + */ + Request.prototype.accepts = function accepts(types) { + if (typeof types === 'string') { + types = [types]; + } -/** - * Check if the Accept header is present, and includes the given type. - * When the Accept header is not present true is returned. - * Otherwise the given type is matched by an exact match, and then subtypes. - * - * @public - * @memberof Request - * @instance - * @function accepts - * @param {String | String[]} types - an array of accept type headers - * @returns {Boolean} is accepteed - * @example - * - * You may pass the subtype such as html which is then converted internally to - * text/html using the mime lookup table: - * - * // Accept: text/html - * req.accepts('html'); - * // => true - * - * // Accept: text/*; application/json - * req.accepts('html'); - * req.accepts('text/html'); - * req.accepts('text/plain'); - * req.accepts('application/json'); - * // => true - * - * req.accepts('image/png'); - * req.accepts('png'); - * // => false - */ -Request.prototype.accepts = function accepts(types) { - if (typeof types === 'string') { - types = [types]; - } + types = types.map(function map(t) { + assert.string(t, 'type'); - types = types.map(function map(t) { - assert.string(t, 'type'); + if (t.indexOf('/') === -1) { + t = mime.lookup(t); + } + return t; + }); - if (t.indexOf('/') === -1) { - t = mime.lookup(t); + negotiator(this); + + return this._negotiator.preferredMediaType(types); + }; + + /** + * Checks if the request accepts the encoding type(s) specified. + * + * @public + * @memberof Request + * @instance + * @function acceptsEncoding + * @param {String | String[]} types - an array of accept type headers + * @returns {Boolean} is accepted encoding + */ + Request.prototype.acceptsEncoding = function acceptsEncoding(types) { + if (typeof types === 'string') { + types = [types]; } - return t; - }); - - negotiator(this); - - return this._negotiator.preferredMediaType(types); -}; -/** - * Checks if the request accepts the encoding type(s) specified. - * - * @public - * @memberof Request - * @instance - * @function acceptsEncoding - * @param {String | String[]} types - an array of accept type headers - * @returns {Boolean} is accepted encoding - */ -Request.prototype.acceptsEncoding = function acceptsEncoding(types) { - if (typeof types === 'string') { - types = [types]; - } + assert.arrayOfString(types, 'types'); + + negotiator(this); + + return this._negotiator.preferredEncoding(types); + }; + + /** + * Returns the value of the content-length header. + * + * @private + * @memberof Request + * @instance + * @function getContentLength + * @returns {Number} content length + */ + Request.prototype.getContentLength = function getContentLength() { + if (this._clen !== undefined) { + return this._clen === false ? undefined : this._clen; + } - assert.arrayOfString(types, 'types'); + // We should not attempt to read and parse the body of an + // Upgrade request, so force Content-Length to zero: + if (this.isUpgradeRequest()) { + return 0; + } - negotiator(this); + var len = this.header('content-length'); - return this._negotiator.preferredEncoding(types); -}; + if (!len) { + this._clen = false; + } else { + this._clen = parseInt(len, 10); + } -/** - * Returns the value of the content-length header. - * - * @private - * @memberof Request - * @instance - * @function getContentLength - * @returns {Number} content length - */ -Request.prototype.getContentLength = function getContentLength() { - if (this._clen !== undefined) { return this._clen === false ? undefined : this._clen; - } - - // We should not attempt to read and parse the body of an - // Upgrade request, so force Content-Length to zero: - if (this.isUpgradeRequest()) { - return 0; - } + }; + /** + * Returns the value of the content-length header. + * @public + * @memberof Request + * @instance + * @function contentLength + * @returns {Number} + */ + Request.prototype.contentLength = Request.prototype.getContentLength; + + /** + * Returns the value of the content-type header. If a content-type is not + * set, this will return a default value of `application/octet-stream`. + * + * @private + * @memberof Request + * @instance + * @function getContentType + * @returns {String} content type + */ + Request.prototype.getContentType = function getContentType() { + if (this._contentType !== undefined) { + return this._contentType; + } - var len = this.header('content-length'); + var index; + var type = this.headers['content-type']; - if (!len) { - this._clen = false; - } else { - this._clen = parseInt(len, 10); - } + if (!type) { + // RFC2616 section 7.2.1 + this._contentType = 'application/octet-stream'; + } else if ((index = type.indexOf(';')) === -1) { + this._contentType = type; + } else { + this._contentType = type.substring(0, index); + } - return this._clen === false ? undefined : this._clen; -}; -/** - * Returns the value of the content-length header. - * @public - * @memberof Request - * @instance - * @function contentLength - * @returns {Number} - */ -Request.prototype.contentLength = Request.prototype.getContentLength; + // #877 content-types need to be case insensitive. + this._contentType = this._contentType.toLowerCase(); -/** - * Returns the value of the content-type header. If a content-type is not set, - * this will return a default value of `application/octet-stream`. - * - * @private - * @memberof Request - * @instance - * @function getContentType - * @returns {String} content type - */ -Request.prototype.getContentType = function getContentType() { - if (this._contentType !== undefined) { return this._contentType; - } - - var index; - var type = this.headers['content-type']; - - if (!type) { - // RFC2616 section 7.2.1 - this._contentType = 'application/octet-stream'; - } else if ((index = type.indexOf(';')) === -1) { - this._contentType = type; - } else { - this._contentType = type.substring(0, index); - } - - // #877 content-types need to be case insensitive. - this._contentType = this._contentType.toLowerCase(); - - return this._contentType; -}; - -/** - * Returns the value of the content-type header. If a content-type is not set, - * this will return a default value of `application/octet-stream` - * @public - * @memberof Request - * @instance - * @function getContentType - * @returns {String} - */ -Request.prototype.contentType = Request.prototype.getContentType; + }; + + /** + * Returns the value of the content-type header. If a content-type is not + * set, this will return a default value of `application/octet-stream` + * @public + * @memberof Request + * @instance + * @function getContentType + * @returns {String} content type + */ + Request.prototype.contentType = Request.prototype.getContentType; + + /** + * Returns a Date object representing when the request was setup. + * Like `time()`, but returns a Date object. + * + * @public + * @memberof Request + * @instance + * @function date + * @returns {Date} date + */ + Request.prototype.date = function date() { + if (this._date !== undefined) { + return this._date; + } -/** - * Returns a Date object representing when the request was setup. - * Like `time()`, but returns a Date object. - * - * @public - * @memberof Request - * @instance - * @function date - * @returns {Date} date - */ -Request.prototype.date = function date() { - if (this._date !== undefined) { + this._date = new Date(this._time); return this._date; - } - - this._date = new Date(this._time); - return this._date; -}; - -/** - * Retrieves the complete URI requested by the client. - * - * @private - * @memberof Request - * @instance - * @function getHref - * @returns {String} URI - */ -Request.prototype.getHref = function getHref() { - return this.getUrl().href; -}; + }; + + /** + * Retrieves the complete URI requested by the client. + * + * @private + * @memberof Request + * @instance + * @function getHref + * @returns {String} URI + */ + Request.prototype.getHref = function getHref() { + return this.getUrl().href; + }; + + /** + * Returns the full requested URL. + * @public + * @memberof Request + * @instance + * @function href + * @returns {String} + * @example + * // incoming request is http://localhost:3000/foo/bar?a=1 + * server.get('/:x/bar', function(req, res, next) { + * console.warn(req.href()); + * // => /foo/bar/?a=1 + * }); + */ + Request.prototype.href = Request.prototype.getHref; + + /** + * Retrieves the request uuid. was created when the request was setup. + * + * @private + * @memberof Request + * @instance + * @function getId + * @returns {String} id + */ + Request.prototype.getId = function getId() { + if (this._id !== undefined) { + return this._id; + } -/** - * Returns the full requested URL. - * @public - * @memberof Request - * @instance - * @function href - * @returns {String} - * @example - * // incoming request is http://localhost:3000/foo/bar?a=1 - * server.get('/:x/bar', function(req, res, next) { - * console.warn(req.href()); - * // => /foo/bar/?a=1 - * }); - */ -Request.prototype.href = Request.prototype.getHref; + this._id = uuid.v4(); -/** - * Retrieves the request uuid. was created when the request was setup. - * - * @private - * @memberof Request - * @instance - * @function getId - * @returns {String} id - */ -Request.prototype.getId = function getId() { - if (this._id !== undefined) { return this._id; - } - - this._id = uuid.v4(); - - return this._id; -}; - -/** - * Returns the request id. If a `reqId` value is passed in, - * this will become the request’s new id. The request id is immutable, - * and can only be set once. Attempting to set the request id more than - * once will cause restify to throw. - * - * @public - * @memberof Request - * @instance - * @function id - * @param {String} reqId - request id - * @returns {String} id - */ -Request.prototype.id = function id(reqId) { - var self = this; - - if (reqId) { - if (self._id) { - throw new Error('request id is immutable, cannot be set again!'); - } else { - assert.string(reqId, 'reqId'); - self._id = reqId; - return self._id; + }; + + /** + * Returns the request id. If a `reqId` value is passed in, + * this will become the request’s new id. The request id is immutable, + * and can only be set once. Attempting to set the request id more than + * once will cause restify to throw. + * + * @public + * @memberof Request + * @instance + * @function id + * @param {String} reqId - request id + * @returns {String} id + */ + Request.prototype.id = function id(reqId) { + var self = this; + + if (reqId) { + if (self._id) { + throw new Error( + 'request id is immutable, cannot be set again!' + ); + } else { + assert.string(reqId, 'reqId'); + self._id = reqId; + return self._id; + } } - } - return self.getId(); -}; - -/** - * Retrieves the cleaned up url path. - * e.g., /foo?a=1 => /foo - * - * @private - * @memberof Request - * @instance - * @function getPath - * @returns {String} path - */ -Request.prototype.getPath = function getPath() { - return this.getUrl().pathname; -}; - -/** - * Returns the cleaned up requested URL. - * @public - * @memberof Request - * @instance - * @function getPath - * @returns {String} - * @example - * // incoming request is http://localhost:3000/foo/bar?a=1 - * server.get('/:x/bar', function(req, res, next) { - * console.warn(req.path()); - * // => /foo/bar - * }); - */ -Request.prototype.path = Request.prototype.getPath; - -/** - * Returns the raw query string. Returns empty string - * if no query string is found. - * - * @public - * @memberof Request - * @instance - * @function getQuery - * @returns {String} query - * @example - * // incoming request is /foo?a=1 - * req.getQuery(); - * // => 'a=1' - * @example - * - * If the queryParser plugin is used, the parsed query string is - * available under the req.query: - * - * // incoming request is /foo?a=1 - * server.use(restify.plugins.queryParser()); - * req.query; - * // => { a: 1 } - */ -Request.prototype.getQuery = function getQuery() { - // always return a string, because this is the raw query string. - // if the queryParser plugin is used, req.query will provide an empty - // object fallback. - return this.getUrl().query || ''; -}; - -/** - * Returns the raw query string. Returns empty string - * if no query string is found - * @private - * @memberof Request - * @instance - * @function query - * @returns {String} - */ -Request.prototype.query = Request.prototype.getQuery; - -/** - * The number of ms since epoch of when this request began being processed. - * Like date(), but returns a number. - * - * @public - * @memberof Request - * @instance - * @function time - * @returns {Number} time - */ -Request.prototype.time = function time() { - return this._time; -}; + return self.getId(); + }; + + /** + * Retrieves the cleaned up url path. + * e.g., /foo?a=1 => /foo + * + * @private + * @memberof Request + * @instance + * @function getPath + * @returns {String} path + */ + Request.prototype.getPath = function getPath() { + return this.getUrl().pathname; + }; + + /** + * Returns the cleaned up requested URL. + * @public + * @memberof Request + * @instance + * @function getPath + * @returns {String} + * @example + * // incoming request is http://localhost:3000/foo/bar?a=1 + * server.get('/:x/bar', function(req, res, next) { + * console.warn(req.path()); + * // => /foo/bar + * }); + */ + Request.prototype.path = Request.prototype.getPath; + + /** + * Returns the raw query string. Returns empty string + * if no query string is found. + * + * @public + * @memberof Request + * @instance + * @function getQuery + * @returns {String} query + * @example + * // incoming request is /foo?a=1 + * req.getQuery(); + * // => 'a=1' + * @example + * + * If the queryParser plugin is used, the parsed query string is + * available under the req.query: + * + * // incoming request is /foo?a=1 + * server.use(restify.plugins.queryParser()); + * req.query; + * // => { a: 1 } + */ + Request.prototype.getQuery = function getQuery() { + // always return a string, because this is the raw query string. + // if the queryParser plugin is used, req.query will provide an empty + // object fallback. + return this.getUrl().query || ''; + }; + + /** + * Returns the raw query string. Returns empty string + * if no query string is found + * @private + * @memberof Request + * @instance + * @function query + * @returns {String} + */ + Request.prototype.query = Request.prototype.getQuery; + + /** + * The number of ms since epoch of when this request began being processed. + * Like date(), but returns a number. + * + * @public + * @memberof Request + * @instance + * @function time + * @returns {Number} time + */ + Request.prototype.time = function time() { + return this._time; + }; + + /** + * returns a parsed URL object. + * + * @private + * @memberof Request + * @instance + * @function getUrl + * @returns {Object} url + */ + Request.prototype.getUrl = function getUrl() { + if (this._cacheURL !== this.url) { + this._url = url.parse(this.url); + this._cacheURL = this.url; + } + return this._url; + }; + + /** + * Returns the accept-version header. + * + * @private + * @memberof Request + * @instance + * @function getVersion + * @returns {String} version + */ + Request.prototype.getVersion = function getVersion() { + if (this._version !== undefined) { + return this._version; + } -/** - * returns a parsed URL object. - * - * @private - * @memberof Request - * @instance - * @function getUrl - * @returns {Object} url - */ -Request.prototype.getUrl = function getUrl() { - if (this._cacheURL !== this.url) { - this._url = url.parse(this.url); - this._cacheURL = this.url; - } - return this._url; -}; + this._version = + this.headers['accept-version'] || + this.headers['x-api-version'] || + '*'; -/** - * Returns the accept-version header. - * - * @private - * @memberof Request - * @instance - * @function getVersion - * @returns {String} version - */ -Request.prototype.getVersion = function getVersion() { - if (this._version !== undefined) { return this._version; - } - - this._version = - this.headers['accept-version'] || this.headers['x-api-version'] || '*'; - - return this._version; -}; - -/** - * Returns the accept-version header. - * @public - * @memberof Request - * @instance - * @function version - * @returns {String} - */ -Request.prototype.version = Request.prototype.getVersion; - -/** - * Returns the version of the route that matched. - * - * @private - * @memberof Request - * @instance - * @function matchedVersion - * @returns {String} version - */ -Request.prototype.matchedVersion = function matchedVersion() { - if (this._matchedVersion !== undefined) { - return this._matchedVersion; - } else { - return this.version(); - } -}; - -/** - * Get the case-insensitive request header key, - * and optionally provide a default value (express-compliant). - * Returns any header off the request. also, 'correct' any - * correctly spelled 'referrer' header to the actual spelling used. - * - * @public - * @memberof Request - * @instance - * @function header - * @param {String} key - the key of the header - * @param {String} [defaultValue] - default value if header isn't - * found on the req - * @returns {String} header value - * @example - * req.header('Host'); - * req.header('HOST'); - * req.header('Accept', '*\/*'); - */ -Request.prototype.header = function header(key, defaultValue) { - assert.string(key, 'key'); - - key = key.toLowerCase(); - - if (key === 'referer' || key === 'referrer') { - key = 'referer'; - } - - return this.headers[key] || defaultValue; -}; - -/** - * Returns any trailer header off the request. Also, 'correct' any - * correctly spelled 'referrer' header to the actual spelling used. - * - * @public - * @memberof Request - * @instance - * @function trailer - * @param {String} name - the name of the header - * @param {String} value - default value if header isn't found on the req - * @returns {String} trailer value - */ -Request.prototype.trailer = function trailer(name, value) { - assert.string(name, 'name'); - name = name.toLowerCase(); - - if (name === 'referer' || name === 'referrer') { - name = 'referer'; - } - - return (this.trailers || {})[name] || value; -}; - -/** - * Check if the incoming request contains the `Content-Type` header field, - * and if it contains the given mime type. - * - * @public - * @memberof Request - * @instance - * @function is - * @param {String} type - a content-type header value - * @returns {Boolean} is content-type header - * @example - * // With Content-Type: text/html; charset=utf-8 - * req.is('html'); - * req.is('text/html'); - * // => true - * - * // When Content-Type is application/json - * req.is('json'); - * req.is('application/json'); - * // => true - * - * req.is('html'); - * // => false - */ -Request.prototype.is = function is(type) { - assert.string(type, 'type'); + }; + + /** + * Returns the accept-version header. + * @public + * @memberof Request + * @instance + * @function version + * @returns {String} + */ + Request.prototype.version = Request.prototype.getVersion; + + /** + * Returns the version of the route that matched. + * + * @private + * @memberof Request + * @instance + * @function matchedVersion + * @returns {String} version + */ + Request.prototype.matchedVersion = function matchedVersion() { + if (this._matchedVersion !== undefined) { + return this._matchedVersion; + } else { + return this.version(); + } + }; + + /** + * Get the case-insensitive request header key, + * and optionally provide a default value (express-compliant). + * Returns any header off the request. also, 'correct' any + * correctly spelled 'referrer' header to the actual spelling used. + * + * @public + * @memberof Request + * @instance + * @function header + * @param {String} key - the key of the header + * @param {String} [defaultValue] - default value if header isn't + * found on the req + * @returns {String} header value + * @example + * req.header('Host'); + * req.header('HOST'); + * req.header('Accept', '*\/*'); + */ + Request.prototype.header = function header(key, defaultValue) { + assert.string(key, 'key'); + + key = key.toLowerCase(); + + if (key === 'referer' || key === 'referrer') { + key = 'referer'; + } - var contentType = this.getContentType(); - var matches = true; + return this.headers[key] || defaultValue; + }; + + /** + * Returns any trailer header off the request. Also, 'correct' any + * correctly spelled 'referrer' header to the actual spelling used. + * + * @public + * @memberof Request + * @instance + * @function trailer + * @param {String} name - the name of the header + * @param {String} value - default value if header isn't found on the req + * @returns {String} trailer value + */ + Request.prototype.trailer = function trailer(name, value) { + assert.string(name, 'name'); + name = name.toLowerCase(); + + if (name === 'referer' || name === 'referrer') { + name = 'referer'; + } - if (!contentType) { - return false; - } + return (this.trailers || {})[name] || value; + }; + + /** + * Check if the incoming request contains the `Content-Type` header field, + * and if it contains the given mime type. + * + * @public + * @memberof Request + * @instance + * @function is + * @param {String} type - a content-type header value + * @returns {Boolean} is content-type header + * @example + * // With Content-Type: text/html; charset=utf-8 + * req.is('html'); + * req.is('text/html'); + * // => true + * + * // When Content-Type is application/json + * req.is('json'); + * req.is('application/json'); + * // => true + * + * req.is('html'); + * // => false + */ + Request.prototype.is = function is(type) { + assert.string(type, 'type'); + + var contentType = this.getContentType(); + var matches = true; + + if (!contentType) { + return false; + } - if (type.indexOf('/') === -1) { - type = mime.lookup(type); - } + if (type.indexOf('/') === -1) { + type = mime.lookup(type); + } - if (type.indexOf('*') !== -1) { - type = type.split('/'); - contentType = contentType.split('/'); - matches &= type[0] === '*' || type[0] === contentType[0]; - matches &= type[1] === '*' || type[1] === contentType[1]; - } else { - matches = contentType === type; - } + if (type.indexOf('*') !== -1) { + type = type.split('/'); + contentType = contentType.split('/'); + matches &= type[0] === '*' || type[0] === contentType[0]; + matches &= type[1] === '*' || type[1] === contentType[1]; + } else { + matches = contentType === type; + } - return matches; -}; + return matches; + }; + + /** + * Check if the incoming request is chunked. + * + * @public + * @memberof Request + * @instance + * @function isChunked + * @returns {Boolean} is chunked + */ + Request.prototype.isChunked = function isChunked() { + return this.headers['transfer-encoding'] === 'chunked'; + }; + + /** + * Check if the incoming request is kept alive. + * + * @public + * @memberof Request + * @instance + * @function isKeepAlive + * @returns {Boolean} is keep alive + */ + Request.prototype.isKeepAlive = function isKeepAlive() { + if (this._keepAlive !== undefined) { + return this._keepAlive; + } -/** - * Check if the incoming request is chunked. - * - * @public - * @memberof Request - * @instance - * @function isChunked - * @returns {Boolean} is chunked - */ -Request.prototype.isChunked = function isChunked() { - return this.headers['transfer-encoding'] === 'chunked'; -}; + if (this.headers.connection) { + this._keepAlive = /keep-alive/i.test(this.headers.connection); + } else { + this._keepAlive = this.httpVersion === '1.0' ? false : true; + } -/** - * Check if the incoming request is kept alive. - * - * @public - * @memberof Request - * @instance - * @function isKeepAlive - * @returns {Boolean} is keep alive - */ -Request.prototype.isKeepAlive = function isKeepAlive() { - if (this._keepAlive !== undefined) { return this._keepAlive; - } - - if (this.headers.connection) { - this._keepAlive = /keep-alive/i.test(this.headers.connection); - } else { - this._keepAlive = this.httpVersion === '1.0' ? false : true; - } - - return this._keepAlive; -}; + }; + + /** + * Check if the incoming request is encrypted. + * + * @public + * @memberof Request + * @instance + * @function isSecure + * @returns {Boolean} is secure + */ + Request.prototype.isSecure = function isSecure() { + if (this._secure !== undefined) { + return this._secure; + } -/** - * Check if the incoming request is encrypted. - * - * @public - * @memberof Request - * @instance - * @function isSecure - * @returns {Boolean} is secure - */ -Request.prototype.isSecure = function isSecure() { - if (this._secure !== undefined) { + this._secure = this.connection.encrypted ? true : false; return this._secure; - } - - this._secure = this.connection.encrypted ? true : false; - return this._secure; -}; - -/** - * Check if the incoming request has been upgraded. - * - * @public - * @memberof Request - * @instance - * @function isUpgradeRequest - * @returns {Boolean} is upgraded - */ -Request.prototype.isUpgradeRequest = function isUpgradeRequest() { - if (this._upgradeRequest !== undefined) { - return this._upgradeRequest; - } else { - return false; - } -}; - -/** - * Check if the incoming request is an upload verb. - * - * @public - * @memberof Request - * @instance - * @function isUpload - * @returns {Boolean} is upload - */ -Request.prototype.isUpload = function isUpload() { - var m = this.method; - return m === 'PATCH' || m === 'POST' || m === 'PUT'; -}; - -/** - * toString serialization - * - * @public - * @memberof Request - * @instance - * @function toString - * @returns {String} serialized request - */ -Request.prototype.toString = function toString() { - var headers = ''; - var self = this; - var str; - - Object.keys(this.headers).forEach(function forEach(k) { - headers += sprintf('%s: %s\n', k, self.headers[k]); - }); - - str = sprintf( - '%s %s HTTP/%s\n%s', - this.method, - this.url, - this.httpVersion, - headers - ); - - return str; -}; - -/** - * Returns the user-agent header. - * - * @public - * @memberof Request - * @instance - * @function userAgent - * @returns {String} user agent - */ -Request.prototype.userAgent = function userAgent() { - return this.headers['user-agent']; -}; - -/** - * Start the timer for a request handler. - * By default, restify uses calls this automatically for all handlers - * registered in your handler chain. - * However, this can be called manually for nested functions inside the handler - * chain to record timing information. - * - * @public - * @memberof Request - * @instance - * @function startHandlerTimer - * @param {String} handlerName - The name of the handler. - * @returns {undefined} no return value - * @example - * - * You must explicitly invoke - * endHandlerTimer() after invoking this function. Otherwise timing information - * will be inaccurate. - * - * server.get('/', function fooHandler(req, res, next) { - * vasync.pipeline({ - * funcs: [ - * function nestedHandler1(req, res, next) { - * req.startHandlerTimer('nestedHandler1'); - * // do something - * req.endHandlerTimer('nestedHandler1'); - * return next(); - * }, - * function nestedHandler1(req, res, next) { - * req.startHandlerTimer('nestedHandler2'); - * // do something - * req.endHandlerTimer('nestedHandler2'); - * return next(); - * - * }... - * ]... - * }, next); - * }); - */ -Request.prototype.startHandlerTimer = function startHandlerTimer(handlerName) { - var self = this; - - // For nested handlers, we prepend the top level handler func name - var name = - self._currentHandler === handlerName - ? handlerName - : self._currentHandler + '-' + handlerName; - - if (!self._timerMap) { - self._timerMap = {}; - } - - self._timerMap[name] = process.hrtime(); - - dtrace._rstfy_probes['handler-start'].fire(function fire() { - return [ - self.serverName, - self._currentRoute, // set in server._run - name, - self._dtraceId - ]; - }); -}; + }; + + /** + * Check if the incoming request has been upgraded. + * + * @public + * @memberof Request + * @instance + * @function isUpgradeRequest + * @returns {Boolean} is upgraded + */ + Request.prototype.isUpgradeRequest = function isUpgradeRequest() { + if (this._upgradeRequest !== undefined) { + return this._upgradeRequest; + } else { + return false; + } + }; + + /** + * Check if the incoming request is an upload verb. + * + * @public + * @memberof Request + * @instance + * @function isUpload + * @returns {Boolean} is upload + */ + Request.prototype.isUpload = function isUpload() { + var m = this.method; + return m === 'PATCH' || m === 'POST' || m === 'PUT'; + }; + + /** + * toString serialization + * + * @public + * @memberof Request + * @instance + * @function toString + * @returns {String} serialized request + */ + Request.prototype.toString = function toString() { + var headers = ''; + var self = this; + var str; + + Object.keys(this.headers).forEach(function forEach(k) { + headers += sprintf('%s: %s\n', k, self.headers[k]); + }); -/** - * End the timer for a request handler. - * You must invoke this function if you called `startRequestHandler` on a - * handler. Otherwise the time recorded will be incorrect. - * - * @public - * @memberof Request - * @instance - * @function endHandlerTimer - * @param {String} handlerName - The name of the handler. - * @returns {undefined} no return value - */ -Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) { - var self = this; + str = sprintf( + '%s %s HTTP/%s\n%s', + this.method, + this.url, + this.httpVersion, + headers + ); + + return str; + }; + + /** + * Returns the user-agent header. + * + * @public + * @memberof Request + * @instance + * @function userAgent + * @returns {String} user agent + */ + Request.prototype.userAgent = function userAgent() { + return this.headers['user-agent']; + }; + + /** + * Start the timer for a request handler. + * By default, restify uses calls this automatically for all handlers + * registered in your handler chain. + * However, this can be called manually for nested functions inside the + * handler chain to record timing information. + * + * @public + * @memberof Request + * @instance + * @function startHandlerTimer + * @param {String} handlerName - The name of the handler. + * @returns {undefined} no return value + * @example + * + * You must explicitly invoke + * endHandlerTimer() after invoking this function. Otherwise timing + * information will be inaccurate. + * + * server.get('/', function fooHandler(req, res, next) { + * vasync.pipeline({ + * funcs: [ + * function nestedHandler1(req, res, next) { + * req.startHandlerTimer('nestedHandler1'); + * // do something + * req.endHandlerTimer('nestedHandler1'); + * return next(); + * }, + * function nestedHandler1(req, res, next) { + * req.startHandlerTimer('nestedHandler2'); + * // do something + * req.endHandlerTimer('nestedHandler2'); + * return next(); + * + * }... + * ]... + * }, next); + * }); + */ + Request.prototype.startHandlerTimer = function startHandlerTimer( + handlerName + ) { + var self = this; + + // For nested handlers, we prepend the top level handler func name + var name = + self._currentHandler === handlerName + ? handlerName + : self._currentHandler + '-' + handlerName; + + if (!self._timerMap) { + self._timerMap = {}; + } - // For nested handlers, we prepend the top level handler func name - var name = - self._currentHandler === handlerName - ? handlerName - : self._currentHandler + '-' + handlerName; + self._timerMap[name] = process.hrtime(); - if (!self.timers) { - self.timers = []; - } + dtrace._rstfy_probes['handler-start'].fire(function fire() { + return [ + self.serverName, + self._currentRoute, // set in server._run + name, + self._dtraceId + ]; + }); + }; + + /** + * End the timer for a request handler. + * You must invoke this function if you called `startRequestHandler` on a + * handler. Otherwise the time recorded will be incorrect. + * + * @public + * @memberof Request + * @instance + * @function endHandlerTimer + * @param {String} handlerName - The name of the handler. + * @returns {undefined} no return value + */ + Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) { + var self = this; + + // For nested handlers, we prepend the top level handler func name + var name = + self._currentHandler === handlerName + ? handlerName + : self._currentHandler + '-' + handlerName; + + if (!self.timers) { + self.timers = []; + } - self._timerMap[name] = process.hrtime(self._timerMap[name]); - self.timers.push({ - name: name, - time: self._timerMap[name] - }); - - dtrace._rstfy_probes['handler-done'].fire(function fire() { - return [ - self.serverName, - self._currentRoute, // set in server._run - name, - self._dtraceId - ]; - }); -}; + self._timerMap[name] = process.hrtime(self._timerMap[name]); + self.timers.push({ + name: name, + time: self._timerMap[name] + }); -/** - * Returns the connection state of the request. Current possible values are: - * - `close` - when the request has been closed by the clien - * - `aborted` - when the socket was closed unexpectedly - * - * @public - * @memberof Request - * @instance - * @function connectionState - * @returns {String} connection state (`"closed"`, `"aborted"`) - */ -Request.prototype.connectionState = function connectionState() { - var self = this; - return self._connectionState; -}; + dtrace._rstfy_probes['handler-done'].fire(function fire() { + return [ + self.serverName, + self._currentRoute, // set in server._run + name, + self._dtraceId + ]; + }); + }; + + /** + * Returns the connection state of the request. Current possible values are: + * - `close` - when the request has been closed by the clien + * - `aborted` - when the socket was closed unexpectedly + * + * @public + * @memberof Request + * @instance + * @function connectionState + * @returns {String} connection state (`"closed"`, `"aborted"`) + */ + Request.prototype.connectionState = function connectionState() { + var self = this; + return self._connectionState; + }; + + /** + * Returns the route object to which the current request was matched to. + * + * @public + * @memberof Request + * @instance + * @function getRoute + * @returns {Object} route + * @example + * Route info object structure: + * { + * path: '/ping/:name', + * method: 'GET', + * versions: [], + * name: 'getpingname' + * } + */ + Request.prototype.getRoute = function getRoute() { + var self = this; + return self.route; + }; +} -/** - * Returns the route object to which the current request was matched to. - * - * @public - * @memberof Request - * @instance - * @function getRoute - * @returns {Object} route - * @example - * Route info object structure: - * { - * path: '/ping/:name', - * method: 'GET', - * versions: [], - * name: 'getpingname' - * } - */ -Request.prototype.getRoute = function getRoute() { - var self = this; - return self.route; -}; +module.exports = patch; diff --git a/lib/response.js b/lib/response.js index 6b6d631b9..d12224caf 100644 --- a/lib/response.js +++ b/lib/response.js @@ -17,8 +17,6 @@ var utils = require('./utils'); var InternalServerError = errors.InternalServerError; -var Response = http.ServerResponse; - /** * @private * Headers that cannot be multi-values. @@ -34,804 +32,826 @@ var HEADER_ARRAY_BLACKLIST = { ///--- API /** - * Wraps all of the node - * [http.ServerResponse] - * (https://nodejs.org/docs/latest/api/http.html#http.ServerResponse) - * APIs, events and properties, plus the following. - * @class Response - * @extends http.ServerResponse - */ - -/** - * Sets the `cache-control` header. - * - * @public - * @memberof Response - * @instance - * @function cache - * @param {String} [type="public"] - value of the header - * (`"public"` or `"private"`) - * @param {Object} [options] - an options object - * @param {Number} options.maxAge - max-age in seconds - * @returns {String} the value set to the header - */ -Response.prototype.cache = function cache(type, options) { - if (typeof type !== 'string') { - options = type; - type = 'public'; - } - - if (options && options.maxAge !== undefined) { - assert.number(options.maxAge, 'options.maxAge'); - type += ', max-age=' + options.maxAge; - } +* Patch Response object and extends with extra functionalities +* +* @private +* @function patch +* @param {http.ServerResponse|http2.Http2ServerResponse} Response - +* Server Response +* @returns {undefined} No return value +*/ +function patch(Response) { + /** + * Wraps all of the node + * [http.ServerResponse](https://nodejs.org/docs/latest/api/http.html) + * APIs, events and properties, plus the following. + * @class Response + * @extends http.ServerResponse + */ - return this.header('Cache-Control', type); -}; + /** + * Sets the `cache-control` header. + * + * @public + * @memberof Response + * @instance + * @function cache + * @param {String} [type="public"] - value of the header + * (`"public"` or `"private"`) + * @param {Object} [options] - an options object + * @param {Number} options.maxAge - max-age in seconds + * @returns {String} the value set to the header + */ + Response.prototype.cache = function cache(type, options) { + if (typeof type !== 'string') { + options = type; + type = 'public'; + } -/** - * Turns off all cache related headers. - * - * @public - * @memberof Response - * @instance - * @function noCache - * @returns {Response} self, the response object - */ -Response.prototype.noCache = function noCache() { - // HTTP 1.1 - this.header('Cache-Control', 'no-cache, no-store, must-revalidate'); + if (options && options.maxAge !== undefined) { + assert.number(options.maxAge, 'options.maxAge'); + type += ', max-age=' + options.maxAge; + } - // HTTP 1.0 - this.header('Pragma', 'no-cache'); + return this.header('Cache-Control', type); + }; - // Proxies - this.header('Expires', '0'); + /** + * Turns off all cache related headers. + * + * @public + * @memberof Response + * @instance + * @function noCache + * @returns {Response} self, the response object + */ + Response.prototype.noCache = function noCache() { + // HTTP 1.1 + this.header('Cache-Control', 'no-cache, no-store, must-revalidate'); - return this; -}; + // HTTP 1.0 + this.header('Pragma', 'no-cache'); -/** - * Appends the provided character set to the response's `Content-Type`. - * - * @public - * @memberof Response - * @instance - * @function charSet - * @param {String} type - char-set value - * @returns {Response} self, the response object - * @example - * res.charSet('utf-8'); - */ -Response.prototype.charSet = function charSet(type) { - assert.string(type, 'charset'); + // Proxies + this.header('Expires', '0'); - this._charSet = type; + return this; + }; - return this; -}; + /** + * Appends the provided character set to the response's `Content-Type`. + * + * @public + * @memberof Response + * @instance + * @function charSet + * @param {String} type - char-set value + * @returns {Response} self, the response object + * @example + * res.charSet('utf-8'); + */ + Response.prototype.charSet = function charSet(type) { + assert.string(type, 'charset'); -/** - * Retrieves a header off the response. - * - * @private - * @memberof Response - * @instance - * @function get - * @param {Object} name - the header name - * @returns {String} header value - */ -Response.prototype.get = function get(name) { - assert.string(name, 'name'); + this._charSet = type; - return this.getHeader(name); -}; + return this; + }; -// If getHeaders is not provided by the Node platform, monkey patch our own. -// This is needed since versions of Node <7 did not come with a getHeaders. -// For more see GH-1408 -if (typeof Response.prototype.getHeaders !== 'function') { /** - * Retrieves all headers off the response. + * Retrieves a header off the response. * * @private * @memberof Response * @instance - * @function getHeaders - * @returns {Object} headers + * @function get + * @param {Object} name - the header name + * @returns {String} header value */ - Response.prototype.getHeaders = function getHeaders() { - return this._headers || {}; - }; -} - -/** - * Sets headers on the response. - * - * @public - * @memberof Response - * @instance - * @function header - * @param {String} key - the name of the header - * @param {String} value - the value of the header - * @returns {Object} the retrieved value or the value that was set - * @example - * - * If only key is specified, return the value of the header. - * If both key and value are specified, set the response header. - * - * res.header('Content-Length'); - * // => undefined - * - * res.header('Content-Length', 123); - * // => 123 - * - * res.header('Content-Length'); - * // => 123 - * - * res.header('foo', new Date()); - * // => Fri, 03 Feb 2012 20:09:58 GMT - * @example - * - * `header()` can also be used to automatically chain header values - * when applicable: - * - * res.header('x-foo', 'a'); - * res.header('x-foo', 'b'); - * // => { 'x-foo': ['a', 'b'] } - * @example - * - * Note that certain headers like `set-cookie` and `content-type` - * do not support multiple values, so calling `header()` - * twice for those headers will - * overwrite the existing value. - * - */ -Response.prototype.header = function header(key, value) { - assert.string(key, 'name'); + Response.prototype.get = function get(name) { + assert.string(name, 'name'); - if (value === undefined) { - return this.getHeader(key); - } + return this.getHeader(name); + }; - if (value instanceof Date) { - value = httpDate(value); - } else if (arguments.length > 2) { - // Support res.header('foo', 'bar %s', 'baz'); - var arg = Array.prototype.slice.call(arguments).slice(2); - value = sprintf(value, arg); + // If getHeaders is not provided by the Node platform, monkey patch our own. + // This is needed since versions of Node <7 did not come with a getHeaders. + // For more see GH-1408 + if (typeof Response.prototype.getHeaders !== 'function') { + /** + * Retrieves all headers off the response. + * + * @private + * @memberof Response + * @instance + * @function getHeaders + * @returns {Object} headers + */ + Response.prototype.getHeaders = function getHeaders() { + return this._headers || {}; + }; } - var current = this.getHeader(key); + /** + * Sets headers on the response. + * + * @public + * @memberof Response + * @instance + * @function header + * @param {String} key - the name of the header + * @param {String} value - the value of the header + * @returns {Object} the retrieved value or the value that was set + * @example + * + * If only key is specified, return the value of the header. + * If both key and value are specified, set the response header. + * + * res.header('Content-Length'); + * // => undefined + * + * res.header('Content-Length', 123); + * // => 123 + * + * res.header('Content-Length'); + * // => 123 + * + * res.header('foo', new Date()); + * // => Fri, 03 Feb 2012 20:09:58 GMT + * @example + * + * `header()` can also be used to automatically chain header values + * when applicable: + * + * res.header('x-foo', 'a'); + * res.header('x-foo', 'b'); + * // => { 'x-foo': ['a', 'b'] } + * @example + * + * Note that certain headers like `set-cookie` and `content-type` + * do not support multiple values, so calling `header()` + * twice for those headers will + * overwrite the existing value. + * + */ + Response.prototype.header = function header(key, value) { + assert.string(key, 'name'); - // Check the header blacklist before changing a header to an array - var keyLc = key.toLowerCase(); + if (value === undefined) { + return this.getHeader(key); + } - if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) { - if (Array.isArray(current)) { - current.push(value); - value = current; - } else { - value = [current, value]; + if (value instanceof Date) { + value = httpDate(value); + } else if (arguments.length > 2) { + // Support res.header('foo', 'bar %s', 'baz'); + var arg = Array.prototype.slice.call(arguments).slice(2); + value = sprintf(value, arg); } - } - this.setHeader(key, value); - return value; -}; + var current = this.getHeader(key); -/** - * Syntatic sugar for: - * ```js - * res.contentType = 'json'; - * res.send({hello: 'world'}); - * ``` - * - * @public - * @memberof Response - * @instance - * @function json - * @param {Number} [code] - http status code - * @param {Object} [body] - value to json.stringify - * @param {Object} [headers] - headers to set on the response - * @returns {Object} the response object - * @example - * res.header('content-type', 'json'); - * res.send({hello: 'world'}); - */ -Response.prototype.json = function json(code, body, headers) { - if (!/application\/json/.test(this.header('content-type'))) { - this.header('Content-Type', 'application/json'); - } + // Check the header blacklist before changing a header to an array + var keyLc = key.toLowerCase(); - return this.send(code, body, headers); -}; + if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) { + if (Array.isArray(current)) { + current.push(value); + value = current; + } else { + value = [current, value]; + } + } -/** - * Sets the link header. - * - * @public - * @memberof Response - * @instance - * @function link - * @param {String} key - the link key - * @param {String} value - the link value - * @returns {String} the header value set to res - */ -Response.prototype.link = function link(key, value) { - assert.string(key, 'key'); - assert.string(value, 'value'); + this.setHeader(key, value); + return value; + }; - var _link = sprintf('<%s>; rel="%s"', key, value); - return this.header('Link', _link); -}; + /** + * Syntatic sugar for: + * ```js + * res.contentType = 'json'; + * res.send({hello: 'world'}); + * ``` + * + * @public + * @memberof Response + * @instance + * @function json + * @param {Number} [code] - http status code + * @param {Object} [body] - value to json.stringify + * @param {Object} [headers] - headers to set on the response + * @returns {Object} the response object + * @example + * res.header('content-type', 'json'); + * res.send({hello: 'world'}); + */ + Response.prototype.json = function json(code, body, headers) { + if (!/application\/json/.test(this.header('content-type'))) { + this.header('Content-Type', 'application/json'); + } -/** - * Sends the response object. pass through to internal `__send` that uses a - * formatter based on the `content-type` header. - * - * @public - * @memberof Response - * @instance - * @function send - * @param {Number} [code] - http status code - * @param {Object | Buffer | Error} [body] - the content to send - * @param {Object} [headers] - any add'l headers to set - * @returns {Object} the response object - * @example - * - * You can use send() to wrap up all the usual writeHead(), write(), end() - * calls on the HTTP API of node. - * You can pass send either a `code` and `body`, or just a body. body can be an - * `Object`, a `Buffer`, or an `Error`. - * When you call `send()`, restify figures out how to format the response based - * on the `content-type`. - * - * res.send({hello: 'world'}); - * res.send(201, {hello: 'world'}); - * res.send(new BadRequestError('meh')); - */ -Response.prototype.send = function send(code, body, headers) { - var self = this; - var args = Array.prototype.slice.call(arguments); - args.push(true); // Append format = true to __send invocation - return self.__send.apply(self, args); -}; + return this.send(code, body, headers); + }; -/** - * Like `res.send()`, but skips formatting. This can be useful when the payload - * has already been preformatted. - * Sends the response object. pass through to internal `__send` that skips - * formatters entirely and sends the content as is. - * - * @public - * @memberof Response - * @instance - * @function sendRaw - * @param {Number} [code] - http status code - * @param {Object | Buffer | Error} [body] - the content to send - * @param {Object} [headers] - any add'l headers to set - * @returns {Object} the response object - */ -Response.prototype.sendRaw = function sendRaw(code, body, headers) { - var self = this; - var args = Array.prototype.slice.call(arguments); - args.push(false); // Append format = false to __send invocation - return self.__send.apply(self, args); -}; + /** + * Sets the link header. + * + * @public + * @memberof Response + * @instance + * @function link + * @param {String} key - the link key + * @param {String} value - the link value + * @returns {String} the header value set to res + */ + Response.prototype.link = function link(key, value) { + assert.string(key, 'key'); + assert.string(value, 'value'); -// eslint-disable-next-line jsdoc/check-param-names -/** - * Internal implementation of send. convenience method that handles: - * writeHead(), write(), end(). - * - * Both body and headers are optional, but you MUST provide body if you are - * providing headers. - * - * @private - * @param {Number} [code] - http status code - * @param {Object | Buffer | String | Error} [body] - the content to send - * @param {Object} [headers] - any add'l headers to set - * @param {Boolean} [format] - When false, skip formatting - * @returns {Object} returns the response object - */ -Response.prototype.__send = function __send() { - var self = this; - var isHead = self.req.method === 'HEAD'; - var log = self.log; - var code, body, headers, format; - - // derive arguments from types, one by one - var index = 0; - // Check to see if the first argument is a status code - if (typeof arguments[index] === 'number') { - code = arguments[index++]; - } + var _link = sprintf('<%s>; rel="%s"', key, value); + return this.header('Link', _link); + }; - // Check to see if the next argument is a body - if ( - typeof arguments[index] === 'object' || - typeof arguments[index] === 'string' - ) { - body = arguments[index++]; - } + /** + * Sends the response object. pass through to internal `__send` that uses a + * formatter based on the `content-type` header. + * + * @public + * @memberof Response + * @instance + * @function send + * @param {Number} [code] - http status code + * @param {Object | Buffer | Error} [body] - the content to send + * @param {Object} [headers] - any add'l headers to set + * @returns {Object} the response object + * @example + * + * You can use send() to wrap up all the usual writeHead(), write(), end() + * calls on the HTTP API of node. + * You can pass send either a `code` and `body`, or just a body. body can be + * an `Object`, a `Buffer`, or an `Error`. + * When you call `send()`, restify figures out how to format the response + * based on the `content-type`. + * + * res.send({hello: 'world'}); + * res.send(201, {hello: 'world'}); + * res.send(new BadRequestError('meh')); + */ + Response.prototype.send = function send(code, body, headers) { + var self = this; + var args = Array.prototype.slice.call(arguments); + args.push(true); // Append format = true to __send invocation + return self.__send.apply(self, args); + }; - // Check to see if the next argument is a collection of headers - if (typeof arguments[index] === 'object') { - headers = arguments[index++]; - } + /** + * Like `res.send()`, but skips formatting. This can be useful when the + * payload has already been preformatted. + * Sends the response object. pass through to internal `__send` that skips + * formatters entirely and sends the content as is. + * + * @public + * @memberof Response + * @instance + * @function sendRaw + * @param {Number} [code] - http status code + * @param {Object | Buffer | Error} [body] - the content to send + * @param {Object} [headers] - any add'l headers to set + * @returns {Object} the response object + */ + Response.prototype.sendRaw = function sendRaw(code, body, headers) { + var self = this; + var args = Array.prototype.slice.call(arguments); + args.push(false); // Append format = false to __send invocation + return self.__send.apply(self, args); + }; - // Check to see if the next argument is the format boolean - if (typeof arguments[index] === 'boolean') { - format = arguments[index++]; - } + // eslint-disable-next-line jsdoc/check-param-names + /** + * Internal implementation of send. convenience method that handles: + * writeHead(), write(), end(). + * + * Both body and headers are optional, but you MUST provide body if you are + * providing headers. + * + * @private + * @param {Number} [code] - http status code + * @param {Object | Buffer | String | Error} [body] - the content to send + * @param {Object} [headers] - any add'l headers to set + * @param {Boolean} [format] - When false, skip formatting + * @returns {Object} returns the response object + */ + Response.prototype.__send = function __send() { + var self = this; + var isHead = self.req.method === 'HEAD'; + var log = self.log; + var code, body, headers, format; + + // derive arguments from types, one by one + var index = 0; + // Check to see if the first argument is a status code + if (typeof arguments[index] === 'number') { + code = arguments[index++]; + } - // Ensure the function was provided with arguments of the proper types, - // if we reach this line and there are still arguments, either one of the - // optional arguments was of an invalid type or we were provided with - // too many arguments - assert( - arguments[index] === undefined, - 'Unknown argument: ' + arguments[index] + '\nProvided: ' + arguments - ); - - // Now lets try to derive values for optional arguments that we were not - // provided, otherwise we choose sane defaults. - - // If the body is an error object and we were not given a status code, try - // to derive it from the error object, otherwise default to 500 - if (!code && body instanceof Error) { - code = body.statusCode || 500; - } + // Check to see if the next argument is a body + if ( + typeof arguments[index] === 'object' || + typeof arguments[index] === 'string' + ) { + body = arguments[index++]; + } - // Set sane defaults for optional arguments if they were not provided and - // we failed to derive their values - code = code || self.statusCode || 200; - headers = headers || {}; - - // Populate our response object with the derived arguments - self.statusCode = code; - self._body = body; - Object.keys(headers).forEach(function forEach(k) { - self.setHeader(k, headers[k]); - }); - - // If log level is set to trace, output our constructed response object - if (log.trace()) { - var _props = { - code: self.statusCode, - headers: self._headers - }; + // Check to see if the next argument is a collection of headers + if (typeof arguments[index] === 'object') { + headers = arguments[index++]; + } - if (body instanceof Error) { - _props.err = self._body; - } else { - _props.body = self._body; + // Check to see if the next argument is the format boolean + if (typeof arguments[index] === 'boolean') { + format = arguments[index++]; } - log.trace(_props, 'response::send entered'); - } - // Flush takes our constructed response object and sends it to the client - function _flush(formattedBody) { - self._data = formattedBody; + // Ensure the function was provided with arguments of the proper types, + // if we reach this line and there are still arguments, either one of + // the optional arguments was of an invalid type or we were provided + // with too many arguments + assert( + arguments[index] === undefined, + 'Unknown argument: ' + arguments[index] + '\nProvided: ' + arguments + ); - // Flush headers - self.writeHead(self.statusCode); + // Now lets try to derive values for optional arguments that we were not + // provided, otherwise we choose sane defaults. - // Send body if it was provided - if (self._data) { - self.write(self._data); + // If the body is an error object and we were not given a status code, + // try to derive it from the error object, otherwise default to 500 + if (!code && body instanceof Error) { + code = body.statusCode || 500; } - // Finish request - self.end(); + // Set sane defaults for optional arguments if they were not provided + // and we failed to derive their values + code = code || self.statusCode || 200; + headers = headers || {}; - // If log level is set to trace, log the entire response object + // Populate our response object with the derived arguments + self.statusCode = code; + self._body = body; + Object.keys(headers).forEach(function forEach(k) { + self.setHeader(k, headers[k]); + }); + + // If log level is set to trace, output our constructed response object if (log.trace()) { - log.trace({ res: self }, 'response sent'); + var _props = { + code: self.statusCode, + headers: self._headers + }; + + if (body instanceof Error) { + _props.err = self._body; + } else { + _props.body = self._body; + } + log.trace(_props, 'response::send entered'); } - // Return the response object back out to the caller of __send - return self; - } + // Flush takes our constructed response object and sends it + // to the client + function _flush(formattedBody) { + self._data = formattedBody; - // 204 = No Content and 304 = Not Modified, we don't want to send the - // body in these cases. HEAD never provides a body. - if (isHead || code === 204 || code === 304) { - return _flush(); - } + // Flush headers + self.writeHead(self.statusCode); - // if no formatting, assert that the value to be written is a string - // or a buffer, then send it. - if (format === false) { - assert.ok( - typeof body === 'string' || Buffer.isBuffer(body), - 'res.sendRaw() accepts only strings or buffers' - ); - return _flush(body); - } + // Send body if it was provided + if (self._data) { + self.write(self._data); + } - // if no body, then no need to format. if this was an error caught by a - // domain, don't send the domain error either. - if (body === undefined || (body instanceof Error && body.domain)) { - return _flush(); - } + // Finish request + self.end(); + + // If log level is set to trace, log the entire response object + if (log.trace()) { + log.trace({ res: self }, 'response sent'); + } - // At this point we know we have a body that needs to be formatted, so lets - // derive the formatter based on the response object's properties - - // _formatterError is used to handle any case where we were unable to - // properly format the provided body - function _formatterError(err) { - // If the user provided a non-success error code, we don't want to mess - // with it since their error is probably more important than our - // inability to format their message. - if (self.statusCode >= 200 && self.statusCode < 300) { - self.statusCode = err.statusCode; + // Return the response object back out to the caller of __send + return self; } - log.warn( - { - req: self.req, - err: err - }, - 'error retrieving formatter' - ); + // 204 = No Content and 304 = Not Modified, we don't want to send the + // body in these cases. HEAD never provides a body. + if (isHead || code === 204 || code === 304) { + return _flush(); + } - return _flush(); - } + // if no formatting, assert that the value to be written is a string + // or a buffer, then send it. + if (format === false) { + assert.ok( + typeof body === 'string' || Buffer.isBuffer(body), + 'res.sendRaw() accepts only strings or buffers' + ); + return _flush(body); + } - var formatter; - var type = self.contentType || self.getHeader('Content-Type'); + // if no body, then no need to format. if this was an error caught by a + // domain, don't send the domain error either. + if (body === undefined || (body instanceof Error && body.domain)) { + return _flush(); + } - // Check to see if we can find a valid formatter - if (!type && !self.req.accepts(self.acceptable)) { - return _formatterError( - new errors.NotAcceptableError({ - message: 'could not find suitable formatter' - }) - ); - } + // At this point we know we have a body that needs to be formatted, so + // lets derive the formatter based on the response object's properties + + // _formatterError is used to handle any case where we were unable to + // properly format the provided body + function _formatterError(err) { + // If the user provided a non-success error code, we don't want to + // mess with it since their error is probably more important than + // our inability to format their message. + if (self.statusCode >= 200 && self.statusCode < 300) { + self.statusCode = err.statusCode; + } - // Derive type if not provided by the user - if (!type) { - type = self.req.accepts(self.acceptable); - } + log.warn( + { + req: self.req, + err: err + }, + 'error retrieving formatter' + ); - type = type.split(';')[0]; + return _flush(); + } - if (!self.formatters[type] && type.indexOf('/') === -1) { - type = mime.lookup(type); - } + var formatter; + var type = self.contentType || self.getHeader('Content-Type'); - // If we were unable to derive a valid type, default to treating it as - // arbitrary binary data per RFC 2046 Section 4.5.1 - if (!self.formatters[type] && self.acceptable.indexOf(type) === -1) { - type = 'application/octet-stream'; - } + // Check to see if we can find a valid formatter + if (!type && !self.req.accepts(self.acceptable)) { + return _formatterError( + new errors.NotAcceptableError({ + message: 'could not find suitable formatter' + }) + ); + } - formatter = self.formatters[type] || self.formatters['*/*']; + // Derive type if not provided by the user + if (!type) { + type = self.req.accepts(self.acceptable); + } - // If after the above attempts we were still unable to derive a formatter, - // provide a meaningful error message - if (!formatter) { - return _formatterError( - new errors.InternalServerError({ - message: 'could not find formatter for application/octet-stream' - }) - ); - } + type = type.split(';')[0]; - if (self._charSet) { - type = type + '; charset=' + self._charSet; - } + if (!self.formatters[type] && type.indexOf('/') === -1) { + type = mime.lookup(type); + } - // Update header to the derived content type for our formatter - self.setHeader('Content-Type', type); + // If we were unable to derive a valid type, default to treating it as + // arbitrary binary data per RFC 2046 Section 4.5.1 + if (!self.formatters[type] && self.acceptable.indexOf(type) === -1) { + type = 'application/octet-stream'; + } - // Finally, invoke the formatter and flush the request with it's results - return _flush(formatter(self.req, self, body)); -}; + formatter = self.formatters[type] || self.formatters['*/*']; + + // If after the above attempts we were still unable to derive a + // formatter, provide a meaningful error message + if (!formatter) { + return _formatterError( + new errors.InternalServerError({ + message: + 'could not find formatter for application/octet-stream' + }) + ); + } -/** - * Sets multiple header(s) on the response. - * Uses `header()` underneath the hood, enabling multi-value headers. - * - * @public - * @memberof Response - * @instance - * @function set - * @param {String|Object} name - name of the header or `Object` of headers - * @param {String} val - value of the header - * @returns {Object} self, the response object - * @example - * res.header('x-foo', 'a'); - * res.set({ - * 'x-foo', 'b', - * 'content-type': 'application/json' - * }); - * // => - * // { - * // 'x-foo': [ 'a', 'b' ], - * // 'content-type': 'application/json' - * // } - */ -Response.prototype.set = function set(name, val) { - var self = this; - - if (arguments.length === 2) { - assert.string(name, 'res.set(name, val) requires name to be a string'); - this.header(name, val); - } else { - assert.object( - name, - 'res.set(headers) requires headers to be an object' - ); - Object.keys(name).forEach(function forEach(k) { - self.set(k, name[k]); - }); - } + if (self._charSet) { + type = type + '; charset=' + self._charSet; + } - return this; -}; + // Update header to the derived content type for our formatter + self.setHeader('Content-Type', type); -/** - * Sets the http status code on the response. - * - * @public - * @memberof Response - * @instance - * @function status - * @param {Number} code - http status code - * @returns {Number} the status code passed in - * @example - * res.status(201); - */ -Response.prototype.status = function status(code) { - assert.number(code, 'code'); + // Finally, invoke the formatter and flush the request with it's results + return _flush(formatter(self.req, self, body)); + }; - this.statusCode = code; - return code; -}; + /** + * Sets multiple header(s) on the response. + * Uses `header()` underneath the hood, enabling multi-value headers. + * + * @public + * @memberof Response + * @instance + * @function set + * @param {String|Object} name - name of the header or + * `Object` of headers + * @param {String} val - value of the header + * @returns {Object} self, the response object + * @example + * res.header('x-foo', 'a'); + * res.set({ + * 'x-foo', 'b', + * 'content-type': 'application/json' + * }); + * // => + * // { + * // 'x-foo': [ 'a', 'b' ], + * // 'content-type': 'application/json' + * // } + */ + Response.prototype.set = function set(name, val) { + var self = this; + + if (arguments.length === 2) { + assert.string( + name, + 'res.set(name, val) requires name to be a string' + ); + this.header(name, val); + } else { + assert.object( + name, + 'res.set(headers) requires headers to be an object' + ); + Object.keys(name).forEach(function forEach(k) { + self.set(k, name[k]); + }); + } -/** - * toString() serialization. - * - * @private - * @memberof Response - * @instance - * @function toString - * @returns {String} stringified response - */ -Response.prototype.toString = function toString() { - var headers = this.getHeaders(); - var headerString = ''; - var str; - - Object.keys(headers).forEach(function forEach(k) { - headerString += k + ': ' + headers[k] + '\n'; - }); - str = sprintf( - 'HTTP/1.1 %s %s\n%s', - this.statusCode, - http.STATUS_CODES[this.statusCode], - headerString - ); - - return str; -}; + return this; + }; -if (!Response.prototype.hasOwnProperty('_writeHead')) { - Response.prototype._writeHead = Response.prototype.writeHead; -} + /** + * Sets the http status code on the response. + * + * @public + * @memberof Response + * @instance + * @function status + * @param {Number} code - http status code + * @returns {Number} the status code passed in + * @example + * res.status(201); + */ + Response.prototype.status = function status(code) { + assert.number(code, 'code'); -/** - * Pass through to native response.writeHead() - * - * @private - * @memberof Response - * @instance - * @function writeHead - * @fires header - * @returns {undefined} no return value - */ -Response.prototype.writeHead = function restifyWriteHead() { - this.emit('header'); - - if (this.statusCode === 204 || this.statusCode === 304) { - this.removeHeader('Content-Length'); - this.removeHeader('Content-MD5'); - this.removeHeader('Content-Type'); - this.removeHeader('Content-Encoding'); + this.statusCode = code; + return code; + }; + + /** + * toString() serialization. + * + * @private + * @memberof Response + * @instance + * @function toString + * @returns {String} stringified response + */ + Response.prototype.toString = function toString() { + var headers = this.getHeaders(); + var headerString = ''; + var str; + + Object.keys(headers).forEach(function forEach(k) { + headerString += k + ': ' + headers[k] + '\n'; + }); + str = sprintf( + 'HTTP/1.1 %s %s\n%s', + this.statusCode, + http.STATUS_CODES[this.statusCode], + headerString + ); + + return str; + }; + + if (!Response.prototype.hasOwnProperty('_writeHead')) { + Response.prototype._writeHead = Response.prototype.writeHead; } - this._writeHead.apply(this, arguments); -}; + /** + * Pass through to native response.writeHead() + * + * @private + * @memberof Response + * @instance + * @function writeHead + * @fires header + * @returns {undefined} no return value + */ + Response.prototype.writeHead = function restifyWriteHead() { + this.emit('header'); + + if (this.statusCode === 204 || this.statusCode === 304) { + this.removeHeader('Content-Length'); + this.removeHeader('Content-MD5'); + this.removeHeader('Content-Type'); + this.removeHeader('Content-Encoding'); + } -/** - * Redirect is sugar method for redirecting. - * @public - * @memberof Response - * @instance - * @param {Object} options url or an options object to configure a redirect - * @param {Boolean} [options.secure] whether to redirect to http or https - * @param {String} [options.hostname] redirect location's hostname - * @param {String} [options.pathname] redirect location's pathname - * @param {String} [options.port] redirect location's port number - * @param {String} [options.query] redirect location's query string parameters - * @param {Boolean} [options.overrideQuery] if true, `options.query` stomps over - * any existing query parameters on current URL. - * by default, will merge the two. - * @param {Boolean} [options.permanent] if true, sets 301. defaults to 302. - * @param {Function} next mandatory, to complete the response and trigger audit - * logger. - * @fires redirect - * @function redirect - * @returns {undefined} - * @example - * res.redirect({...}, next); - * @example - * - * A convenience method for 301/302 redirects. Using this method will tell - * restify to stop execution of your handler chain. - * You can also use an options object. `next` is required. - * - * res.redirect({ - * hostname: 'www.foo.com', - * pathname: '/bar', - * port: 80, // defaults to 80 - * secure: true, // sets https - * permanent: true, - * query: { - * a: 1 - * } - * }, next); // => redirects to 301 https://www.foo.com/bar?a=1 - */ + this._writeHead.apply(this, arguments); + }; -/** - * Redirect with code and url. - * @memberof Response - * @instance - * @param {Number} code http redirect status code - * @param {String} url redirect url - * @param {Function} next mandatory, to complete the response and trigger - * audit logger. - * @fires redirect - * @function redirect - * @returns {undefined} - * @example - * res.redirect(301, 'www.foo.com', next); - */ + /** + * Redirect is sugar method for redirecting. + * @public + * @memberof Response + * @instance + * @param {Object} options url or an options object to configure a redirect + * @param {Boolean} [options.secure] whether to redirect to http or https + * @param {String} [options.hostname] redirect location's hostname + * @param {String} [options.pathname] redirect location's pathname + * @param {String} [options.port] redirect location's port number + * @param {String} [options.query] redirect location's query string + * parameters + * @param {Boolean} [options.overrideQuery] if true, `options.query` + * stomps over any existing query + * parameters on current URL. + * by default, will merge the two. + * @param {Boolean} [options.permanent] if true, sets 301. defaults to 302. + * @param {Function} next mandatory, to complete the response and trigger + * audit logger. + * @fires redirect + * @function redirect + * @returns {undefined} + * @example + * res.redirect({...}, next); + * @example + * + * A convenience method for 301/302 redirects. Using this method will tell + * restify to stop execution of your handler chain. + * You can also use an options object. `next` is required. + * + * res.redirect({ + * hostname: 'www.foo.com', + * pathname: '/bar', + * port: 80, // defaults to 80 + * secure: true, // sets https + * permanent: true, + * query: { + * a: 1 + * } + * }, next); // => redirects to 301 https://www.foo.com/bar?a=1 + */ -/** - * Redirect with url. - * @public - * @memberof Response - * @instance - * @param {String} url redirect url - * @param {Function} next mandatory, to complete the response and trigger - * audit logger. - * @fires redirect - * @function redirect - * @returns {undefined} - * @example - * res.redirect('www.foo.com', next); - * res.redirect('/foo', next); - */ -Response.prototype.redirect = redirect; + /** + * Redirect with code and url. + * @memberof Response + * @instance + * @param {Number} code http redirect status code + * @param {String} url redirect url + * @param {Function} next mandatory, to complete the response and trigger + * audit logger. + * @fires redirect + * @function redirect + * @returns {undefined} + * @example + * res.redirect(301, 'www.foo.com', next); + */ -/** - * @private - * @param {*} arg1 - arg1 - * @param {*} arg2 - arg2 - * @param {*} arg3 - arg3 - * @fires redirect - * @function redirect - * @returns {undefined} no return value - */ -function redirect(arg1, arg2, arg3) { - var self = this; - var statusCode = 302; - var finalUri; - var redirectLocation; - var next; - - // 1) this is signature 1, where an explicit status code is passed in. - // MUST guard against null here, passing null is likely indicative - // of an attempt to call res.redirect(null, next); - // as a way to do a reload of the current page. - if (arg1 && !isNaN(arg1)) { - statusCode = arg1; - finalUri = arg2; - next = arg3; - } else if (typeof arg1 === 'string') { - // 2) this is signaure number 2 - // otherwise, it's a string, and use it directly - finalUri = arg1; - next = arg2; - } else if (typeof arg1 === 'object') { - // 3) signature number 3, using an options object. - // set next, then go to work. - next = arg2; - - var req = self.req; - var opt = arg1 || {}; - var currentFullPath = req.href(); - var secure = opt.hasOwnProperty('secure') ? opt.secure : req.isSecure(); - - // if hostname is passed in, use that as the base, - // otherwise fall back on current url. - var parsedUri = url.parse(opt.hostname || currentFullPath, true); - - // create the object we'll use to format for the final uri. - // this object will eventually get passed to url.format(). - // can't use parsedUri to seed it, as it confuses the url module - // with some existing parsed state. instead, we'll pick the things - // we want and use that as a starting point. - finalUri = { - port: parsedUri.port, - hostname: parsedUri.hostname, - query: parsedUri.query, - pathname: parsedUri.pathname - }; + /** + * Redirect with url. + * @public + * @memberof Response + * @instance + * @param {String} url redirect url + * @param {Function} next mandatory, to complete the response and trigger + * audit logger. + * @fires redirect + * @function redirect + * @returns {undefined} + * @example + * res.redirect('www.foo.com', next); + * res.redirect('/foo', next); + */ + Response.prototype.redirect = redirect; - // start building url based on options. - // start with the host - if (opt.hostname) { - finalUri.hostname = opt.hostname; - } + /** + * @private + * @param {*} arg1 - arg1 + * @param {*} arg2 - arg2 + * @param {*} arg3 - arg3 + * @fires redirect + * @function redirect + * @returns {undefined} no return value + */ + function redirect(arg1, arg2, arg3) { + var self = this; + var statusCode = 302; + var finalUri; + var redirectLocation; + var next; + + // 1) this is signature 1, where an explicit status code is passed in. + // MUST guard against null here, passing null is likely indicative + // of an attempt to call res.redirect(null, next); + // as a way to do a reload of the current page. + if (arg1 && !isNaN(arg1)) { + statusCode = arg1; + finalUri = arg2; + next = arg3; + } else if (typeof arg1 === 'string') { + // 2) this is signaure number 2 + // otherwise, it's a string, and use it directly + finalUri = arg1; + next = arg2; + } else if (typeof arg1 === 'object') { + // 3) signature number 3, using an options object. + // set next, then go to work. + next = arg2; + + var req = self.req; + var opt = arg1 || {}; + var currentFullPath = req.href(); + var secure = opt.hasOwnProperty('secure') + ? opt.secure + : req.isSecure(); + + // if hostname is passed in, use that as the base, + // otherwise fall back on current url. + var parsedUri = url.parse(opt.hostname || currentFullPath, true); + + // create the object we'll use to format for the final uri. + // this object will eventually get passed to url.format(). + // can't use parsedUri to seed it, as it confuses the url module + // with some existing parsed state. instead, we'll pick the things + // we want and use that as a starting point. + finalUri = { + port: parsedUri.port, + hostname: parsedUri.hostname, + query: parsedUri.query, + pathname: parsedUri.pathname + }; + + // start building url based on options. + // start with the host + if (opt.hostname) { + finalUri.hostname = opt.hostname; + } - // then set protocol IFF hostname is set - otherwise we end up with - // malformed URL. - if (finalUri.hostname) { - finalUri.protocol = secure === true ? 'https' : 'http'; - } + // then set protocol IFF hostname is set - otherwise we end up with + // malformed URL. + if (finalUri.hostname) { + finalUri.protocol = secure === true ? 'https' : 'http'; + } - // then set current path after the host - if (opt.pathname) { - finalUri.pathname = opt.pathname; - } + // then set current path after the host + if (opt.pathname) { + finalUri.pathname = opt.pathname; + } - // then set port - if (opt.port) { - finalUri.port = opt.port; - } + // then set port + if (opt.port) { + finalUri.port = opt.port; + } - // then add query params - if (opt.query) { - if (opt.overrideQuery === true) { - finalUri.query = opt.query; - } else { - finalUri.query = utils.mergeQs(opt.query, finalUri.query); + // then add query params + if (opt.query) { + if (opt.overrideQuery === true) { + finalUri.query = opt.query; + } else { + finalUri.query = utils.mergeQs(opt.query, finalUri.query); + } } - } - // change status code to 301 permanent if specified - if (opt.permanent) { - statusCode = 301; + // change status code to 301 permanent if specified + if (opt.permanent) { + statusCode = 301; + } } - } - // if we're missing a next we should probably throw. if user wanted - // to redirect but we were unable to do so, we should not continue - // down the handler stack. - assert.func(next, 'res.redirect() requires a next param'); + // if we're missing a next we should probably throw. if user wanted + // to redirect but we were unable to do so, we should not continue + // down the handler stack. + assert.func(next, 'res.redirect() requires a next param'); - // if we are missing a finalized uri - // by this point, pass an error to next. - if (!finalUri) { - return next(new InternalServerError('could not construct url')); - } + // if we are missing a finalized uri + // by this point, pass an error to next. + if (!finalUri) { + return next(new InternalServerError('could not construct url')); + } - redirectLocation = url.format(finalUri); + redirectLocation = url.format(finalUri); - self.emit('redirect', redirectLocation); + self.emit('redirect', redirectLocation); - // now we're done constructing url, send the res - self.send(statusCode, null, { - Location: redirectLocation - }); + // now we're done constructing url, send the res + self.send(statusCode, null, { + Location: redirectLocation + }); - // tell server to stop processing the handler stack. - return next(false); + // tell server to stop processing the handler stack. + return next(false); + } } + +module.exports = patch; diff --git a/lib/server.js b/lib/server.js index 2c214dd2d..fa7e69dd4 100644 --- a/lib/server.js +++ b/lib/server.js @@ -25,8 +25,21 @@ var upgrade = require('./upgrade'); var deprecationWarnings = require('./deprecationWarnings'); // Ensure these are loaded -require('./request'); -require('./response'); +var patchRequest = require('./request'); +var patchResponse = require('./response'); + +var http2; + +// http2 module is not available < v8.4.0 (only with flag <= 8.8.0) +try { + http2 = require('http2'); + patchResponse(http2.Http2ServerResponse); + patchRequest(http2.Http2ServerRequest); + // eslint-disable-next-line no-empty +} catch (err) {} + +patchResponse(http.ServerResponse); +patchRequest(http.IncomingMessage); ///--- Globals @@ -73,6 +86,8 @@ var PROXY_EVENTS = [ * response header, default is `restify`. Pass empty string to unset the header. * @param {Object} [options.spdy] - Any options accepted by * [node-spdy](https://github.com/indutny/node-spdy). + * @param {Object} [options.http2] - Any options accepted by + * [http2.createSecureServer](https://nodejs.org/api/http2.html). * @param {Boolean} [options.handleUpgrades=false] - Hook the `upgrade` event * from the node HTTP server, pushing `Connection: Upgrade` requests through the * regular request handling chain. @@ -121,6 +136,15 @@ function Server(options) { if (options.spdy) { this.spdy = true; this.server = spdy.createServer(options.spdy); + } else if (options.http2) { + assert( + http2, + 'http2 module is not available, ' + + 'upgrade your Node.js version to >= 8.8.0' + ); + + this.http2 = true; + this.server = http2.createSecureServer(options.http2); } else if ((options.cert || options.certificate) && options.key) { this.ca = options.ca; this.certificate = options.certificate || options.cert; diff --git a/test/keys/http2-cert.pem b/test/keys/http2-cert.pem new file mode 100644 index 000000000..2f13995ff --- /dev/null +++ b/test/keys/http2-cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICHzCCAYgCCQCPPSUAa8QZojANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJS +VTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMET21zazEhMB8GA1UEChMY +SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTExMDQwOTEwMDY0NVoXDTExMDUw +OTEwMDY0NVowVDELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxDTAL +BgNVBAcTBE9tc2sxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1bn25sPkv46wl70BffxradlkRd/x +p5Xf8HDhPSfzNNctERYslXT2fX7Dmfd5w1XTVqqGqJ4izp5VewoVOHA8uavo3ovp +gNWasil5zADWaM1T0nnV0RsFbZWzOTmm1U3D48K8rW3F5kOZ6f4yRq9QT1gF/gN7 +5Pt494YyYyJu/a8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQBuRZisIViI2G/R+w79 +vk21TzC/cJ+O7tKsseDqotXYTH8SuimEH5IWcXNgnWhNzczwN8s2362NixyvCipV +yd4wzMpPbjIhnWGM0hluWZiK2RxfcqimIBjDParTv6CMUIuwGQ257THKY8hXGg7j +Uws6Lif3P9UbsuRiYPxMgg98wg== +-----END CERTIFICATE----- + diff --git a/test/keys/http2-csr.pem b/test/keys/http2-csr.pem new file mode 100644 index 000000000..b4d764fdc --- /dev/null +++ b/test/keys/http2-csr.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBkzCB/QIBADBUMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEN +MAsGA1UEBxMET21zazEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF +3/Gnld/wcOE9J/M01y0RFiyVdPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+je +i+mA1ZqyKXnMANZozVPSedXRGwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+ +A3vk+3j3hjJjIm79rwIDAQABoAAwDQYJKoZIhvcNAQEFBQADgYEAiNWhz6EppIVa +FfUaB3sLeqfamb9tg9kBHtvqj/FJni0snqms0kPWaTySEPHZF0irIb7VVdq/sVCb +3gseMVSyoDvPJ4lHC3PXqGQ7kM1mIPhDnR/4HDA3BhlGhTXSDIHgZnvI+HMBdsyC +hC3dz5odyKqe4nmoofomALkBL9t4H8s= +-----END CERTIFICATE REQUEST----- + diff --git a/test/keys/http2-key.pem b/test/keys/http2-key.pem new file mode 100644 index 000000000..957810910 --- /dev/null +++ b/test/keys/http2-key.pem @@ -0,0 +1,16 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF3/Gnld/wcOE9J/M01y0RFiyV +dPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+jei+mA1ZqyKXnMANZozVPSedXR +GwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+A3vk+3j3hjJjIm79rwIDAQAB +AoGAAv2QI9h32epQND9TxwSCKD//dC7W/cZOFNovfKCTeZjNK6EIzKqPTGA6smvR +C1enFl5adf+IcyWqAoe4lkqTvurIj+2EhtXdQ8DBlVuXKr3xvEFdYxXPautdTCF6 +KbXEyS/s1TZCRFjYftvCrXxc3pK45AQX/wg7z1K+YB5pyIECQQD0OJvLoxLYoXAc +FZraIOZiDsEbGuSHqoCReFXH75EC3+XGYkH2bQ/nSIZ0h1buuwQ/ylKXOlTPT3Qt +Xm1OQEBvAkEA4AjWsIO/rRpOm/Q2aCrynWMpoUXTZSbL2yGf8pxp/+8r2br5ier0 +M1LeBb/OPY1+k39NWLXxQoo64xoSFYk2wQJAd2wDCwX4HkR7HNCXw1hZL9QFK6rv +20NN0VSlpboJD/3KT0MW/FiCcVduoCbaJK0Au+zEjDyy4hj5N4I4Mw6KMwJAXVAx +I+psTsxzS4/njXG+BgIEl/C+gRYsuMQDnAi8OebDq/et8l0Tg8ETSu++FnM18neG +ntmBeMacinUUbTXuwQJBAJp/onZdsMzeVulsGrqR1uS+Lpjc5Q1gt5ttt2cxj91D +rio48C/ZvWuKNE8EYj2ALtghcVKRvgaWfOxt2GPguGg= +-----END RSA PRIVATE KEY----- + diff --git a/test/serverHttp2.test.js b/test/serverHttp2.test.js new file mode 100644 index 000000000..78de37901 --- /dev/null +++ b/test/serverHttp2.test.js @@ -0,0 +1,111 @@ +'use strict'; +/* eslint-disable func-names */ + +var path = require('path'); +var fs = require('fs'); +var http2; + +// http2 module is not available < v8.4.0 (only with flag <= 8.8.0) +try { + http2 = require('http2'); +} catch (err) { + console.log('HTTP2 module is not available'); + console.log( + 'Node.js version >= v8.8.8 required, current: ' + process.versions.node + ); + return; +} + +var restify = require('../lib'); + +if (require.cache[__dirname + '/lib/helper.js']) { + delete require.cache[__dirname + '/lib/helper.js']; +} +var helper = require('./lib/helper.js'); + +///--- Globals + +var after = helper.after; +var before = helper.before; +var test = helper.test; + +var CERT = fs.readFileSync(path.join(__dirname, './keys/http2-cert.pem')); +var KEY = fs.readFileSync(path.join(__dirname, './keys/http2-key.pem')); +var CA = fs.readFileSync(path.join(__dirname, 'keys/http2-csr.pem')); + +var PORT = process.env.UNIT_TEST_PORT || 0; +var CLIENT; +var SERVER; + +///--- Tests + +before(function(cb) { + try { + SERVER = restify.createServer({ + dtrace: helper.dtrace, + handleUncaughtExceptions: true, + http2: { + cert: CERT, + key: KEY, + ca: CA + }, + log: helper.getLog('server') + }); + SERVER.listen(PORT, '127.0.0.1', function() { + PORT = SERVER.address().port; + CLIENT = http2.connect('https://127.0.0.1:' + PORT, { + rejectUnauthorized: false + }); + + cb(); + }); + } catch (e) { + console.error(e.stack); + process.exit(1); + } +}); + +after(function(cb) { + try { + CLIENT.destroy(); + SERVER.close(function() { + CLIENT = null; + SERVER = null; + cb(); + }); + } catch (e) { + console.error(e.stack); + process.exit(1); + } +}); + +test('get (path only)', function(t) { + SERVER.get('/foo/:id', function echoId(req, res, next) { + t.ok(req.params); + t.equal(req.params.id, 'bar'); + t.equal(req.isUpload(), false); + res.json({ hello: 'world' }); + next(); + }); + + var req = CLIENT.request({ + ':path': '/foo/bar', + ':method': 'GET' + }); + + req.on('response', function(headers, flags) { + var data = ''; + t.equal(headers[':status'], 200); + + req.on('data', function(chunk) { + data += chunk; + }); + req.on('end', function() { + t.deepEqual(JSON.parse(data), { hello: 'world' }); + t.end(); + }); + }); + req.on('error', function(err) { + t.ifError(err); + }); +});