From ee2092f9e344d509c37ea75ec9e3feb3b179aa51 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 | 16 +- docs/_api/response.md | 29 +- docs/_api/server.md | 3 + docs/api/server.md | 412 +++++++ examples/http2/http2.js | 30 + examples/http2/keys/http2-cert.pem | 15 + examples/http2/keys/http2-csr.pem | 12 + examples/http2/keys/http2-key.pem | 16 + lib/request.js | 1590 ++++++++++++++-------------- lib/response.js | 1413 ++++++++++++------------ lib/server.js | 24 +- test/keys/http2-cert.pem | 15 + test/keys/http2-csr.pem | 12 + test/keys/http2-key.pem | 16 + test/serverHttp2.test.js | 114 ++ 15 files changed, 2206 insertions(+), 1511 deletions(-) create mode 100644 docs/api/server.md 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..93903a514 100644 --- a/docs/_api/request.md +++ b/docs/_api/request.md @@ -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..15d06ef1f 100644 --- a/docs/_api/response.md +++ b/docs/_api/response.md @@ -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..57045c193 100644 --- a/docs/_api/server.md +++ b/docs/_api/server.md @@ -116,6 +116,9 @@ 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]\( + \#http2_http2_createsecureserver_options_onrequesthandler). - `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/docs/api/server.md b/docs/api/server.md new file mode 100644 index 000000000..193c420d7 --- /dev/null +++ b/docs/api/server.md @@ -0,0 +1,412 @@ +--- +title: Server API +permalink: /docs/server-api/ +--- + +A restify server object is the main interface through which you will register +routes and handlers for incoming requests. A server object will be returned +to you using the `createServer()` method: + +```js +var restify = require('restify'); +var server = restify.createServer(); +``` + +## createServer() + +The `createServer()` method takes the following options: + +|Option|Type|Description| +|----------|--------|---------------| +|certificate|String or Buffer|If you want to create an HTTPS server, pass in a PEM-encoded certificate and key.| +|key|String or Buffer|If you want to create an HTTPS server, pass in a PEM-encoded certificate and key.| +|formatters|Object|Custom response formatters for `res.send()`.| +|handleUncaughtExceptions|Boolean|When true (default is false) restify will use a domain to catch and respond to any uncaught exceptions that occur in it's handler stack.| +|log|Object|You can optionally pass in a [bunyan](https://github.com/trentm/node-bunyan) instance; not required.| +|name|String|By default, this will be set in the `Server` response header, default is `restify`. Pass empty string to unset the header.| +|spdy|Object|Any options accepted by [node-spdy](https://github.com/indutny/node-spdy).| +|http2|Object|Any options accepted by [http2.createSecureServer](https://nodejs.org/dist/latest-v8.x/docs/api/http2.html#http2_http2_createsecureserver_options_onrequesthandler).| +|version|String or Array(String)|Default version(s) to set for all routes.| +|handleUpgrades|Boolean|Hook the `upgrade` event from the node HTTP server, pushing `Connection: Upgrade` requests through the regular request handling chain; defaults to `false`.| +|httpsServerOptions|Object|Any options accepted by [node-https Server](http://nodejs.org/api/https.html#https_https). If provided the following restify server options will be ignored: spdy, http2, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and ciphers; however these can all be specified on httpsServerOptions.| +|strictRouting|Boolean|(Default=`false`). If set, Restify will treat "/foo" and "/foo/" as different paths.| + + +## Properties + +A restify server has the following properties on it: + +|Name|Type|Description| +|--------|--------|---------------| +|name|String|Name of the server.| +|version|String or Array(String)|Default version(s) to use in all routes.| +|log|Object|[bunyan](https://github.com/trentm/node-bunyan) instance.| +|acceptable|Array(String)|List of content-types this server can respond with.| +|url|String|Once listen() is called, this will be filled in with where the server is running.| + +## Methods + +### del(opts, handler) +### get(opts, handler) +### head(opts, handler) +### opts(opts, handler) +### post(opts, handler) +### put(opts, handler) +### patch(opts, handler) + +Install a route into the restify server to handle a specific http verb. + +* `opts` {String | Regex | Object} +* `opts.name` {String} a name for the route +* `opts.path` {String | Regex} a string or regex matching the route +* `opts.version` {String | ArrayOfString} versions supported by this route +* `handler` {Function | ArrayOfFunctions} a function or array of functions to +handle this route + +Some examples: + +```js +// a static route +server.get('/foo', function(req, res, next) {}); +// a parameterized route +server.get('/foo/:bar', function(req, res, next) {}); +// a regular expression +server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next) {}); +// an options object +server.get({ + path: '/foo', + version: ['1.0.0', '2.0.0'] +}, function(req, res, next) {}); +``` + +### address() + +Wraps node's [address()](http://nodejs.org/docs/latest/api/net.html#net_server_address). + +### listen(port, [host], [callback]) + +Wraps node's [listen()](http://nodejs.org/docs/latest/api/net.html#net_server_listen_path_callback). + +### listen(path, [callback]) + +Wraps node's [listen()](http://nodejs.org/docs/latest/api/net.html#net_server_listen_path_callback). + +### close() + +Wraps node's [close()](http://nodejs.org/docs/latest/api/net.html#net_event_close). + +### pre(handler) + +* `handler` {Function | Array} + +Allows you to add handlers that run for all routes. *before* routing occurs. +This gives you a hook to change request headers and the like if you need to. +Note that `req.params` will be undefined, as that's filled in *after* routing. +Takes a function, or an array of functions. + +```js +server.pre(function(req, res, next) { + req.headers.accept = 'application/json'; + return next(); +}); +``` + +For example, `pre()` can be used to deduplicate slashes in URLs + +```js +server.pre(restify.pre.dedupeSlashes()); +``` + +### use(handler) + +* `handler` {Function | Array} + +Allows you to add in handlers that run for all routes. Note that handlers added +via `use()` will run only after the router has found a matching route. If no +match is found, these handlers will never run. Takes a function, or an array +of functions. + +### inflightRequests() + +Returns the number of inflight requests currently being handled by the server. + +### debugInfo() + +Returns debugging information about the current state of the server. + +## Events + +In additional to emitting all the events from node's +[http.Server](http://nodejs.org/docs/latest/api/http.html#http_class_http_server), +restify servers also emit a number of additional events that make building REST +and web applications much easier. + +### Errors + +Restify handles errors as first class citizens. When an error object is passed +to the `next()` function, an event is emitted on the server object, and the +error object will be serialized and sent to the client. An error object is any +object that passes an `instanceof Error` check. + +Before the error object is sent to the client, the server will fire an event +using the name of the error, without the `Error` part of the name. For example, +given an `InternalServerError`, the server will emit an `InternalServer` event. +This creates opportunities to do logging, metrics, or payload mutation based on +the type of error. For example: + +```js +var errs = require('restify-errors'); + +server.get('/', function(req, res, next) { + return next(new errs.InternalServerError('boom!')); +}); + +server.on('InternalServer', function(req, res, err, callback) { + // before the response is sent, this listener will be invoked, allowing + // opportunities to do metrics capturing or logging. + myMetrics.capture(err); + // invoke the callback to complete your work, and the server will send out + // a response. + return callback(); +}); +``` + +Inside the error event listener, it is also possible to change the serialization +method of the error if desired. To do so, simply implement a custom +`toString()` or `toJSON()`. Depending on the content-type and formatter being +used for the response, one of the two serializers will be used. For example, +given the folllwing example: + +```js +server.on('restifyError', function(req, res, err, callback) { + err.toJSON = function customToJSON() { + return { + name: err.name, + message: err.message + }; + }; + err.toString = function customToString() { + return 'i just want a string'; + }; + return callback(); +}); +``` + +A request with an `accept: application/json` will trigger the `toJSON()` +serializer, while a request with `accept: text/plain` will trigger the +`toString()` serializer. + +Note that the function signature for the error listener is identical for all +emitted error events. The signature is as follows: + +```js +function(req, res, err, callback) { } +``` + +* req - the request object +* res - the response object +* err - the error object +* callback - a callback function to invoke + + +When using this feature in conjunction with +[restify-errors](https://github.com/restify/errors), restify will emit events +for all of the basic http errors: + +* 400 BadRequestError +* 401 UnauthorizedError +* 402 PaymentRequiredError +* 403 ForbiddenError +* 404 NotFoundError +* 405 MethodNotAllowedError +* 406 NotAcceptableError +* 407 ProxyAuthenticationRequiredError +* 408 RequestTimeoutError +* 409 ConflictError +* 410 GoneError +* 411 LengthRequiredError +* 412 PreconditionFailedError +* 413 RequestEntityTooLargeError +* 414 RequesturiTooLargeError +* 415 UnsupportedMediaTypeError +* 416 RangeNotSatisfiableError (node >= 4) +* 416 RequestedRangeNotSatisfiableError (node 0.x) +* 417 ExpectationFailedError +* 418 ImATeapotError +* 422 UnprocessableEntityError +* 423 LockedError +* 424 FailedDependencyError +* 425 UnorderedCollectionError +* 426 UpgradeRequiredError +* 428 PreconditionRequiredError +* 429 TooManyRequestsError +* 431 RequestHeaderFieldsTooLargeError +* 500 InternalServerError +* 501 NotImplementedError +* 502 BadGatewayError +* 503 ServiceUnavailableError +* 504 GatewayTimeoutError +* 505 HttpVersionNotSupportedError +* 506 VariantAlsoNegotiatesError +* 507 InsufficientStorageError +* 509 BandwidthLimitExceededError +* 510 NotExtendedError +* 511 NetworkAuthenticationRequiredError + + +Restify will also emit the following events: + +### NotFound + +When a client request is sent for a URL that does not exist, restify +will emit this event. Note that restify checks for listeners on this +event, and if there are none, responds with a default 404 handler. + +### MethodNotAllowed + +When a client request is sent for a URL that exists, but not for the requested +HTTP verb, restify will emit this event. Note that restify checks for listeners +on this event, and if there are none, responds with a default 405 handler. + +### VersionNotAllowed + +When a client request is sent for a route that exists, but does not +match the version(s) on those routes, restify will emit this +event. Note that restify checks for listeners on this event, and if +there are none, responds with a default 400 handler. + +### UnsupportedMediaType + +When a client request is sent for a route that exist, but has a `content-type` +mismatch, restify will emit this event. Note that restify checks for listeners +on this event, and if there are none, responds with a default 415 handler. + +### restifyError + +This event is emitted following all error events as a generic catch all. It is +recommended to use specific error events to handle specific errors, but this +event can be useful for metrics or logging. If you use this in conjunction with +other error events, the most specific event will be fired first, followed by +this one: + +```js +server.get('/', function(req, res, next) { + return next(new InternalServerError('boom')); +}); + +server.on('InternalServer', function(req, res, err, callback) { + // this will get fired first, as it's the most relevant listener + return callback(); +}); + +server.on('restifyError', function(req, res, err, callback) { + // this is fired second. + return callback(); +}); +``` + + +### after + +After each request has been fully serviced, an `after` event is fired. This +event can be hooked into to handle audit logs and other metrics. Note that +flushing a response does not necessarily correspond with an `after` event. +restify considers a request to be fully serviced when either: + +1) The handler chain for a route has been fully completed +2) An error was returned to `next()`, and the corresponding error events have + been fired for that error type + +The signature is for the after event is as follows: + +```js +function(req, res, route, error) { } +``` + +* req - the request object +* res - the response object +* route - the route object that serviced the request +* error - the error passed to `next()`, if applicable + +Note that when the server automatically responds with a +NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired. + + +### pre + +Before each request has been routed, a `pre` event is fired. This event can be +hooked into handle audit logs and other metrics. Since this event fires +*before* routing has occured, it will fire regardless of whether the route is +supported or not, e.g. requests that result in a `404`. + +The signature for the `pre` event is as follows: + +```js +function(req, res) {} +``` +* req - the request object +* res - the response object + +Note that when the server automatically responds with a +NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired. + + +### routed + +A `routed` event is fired after a request has been routed by the router, but +before handlers specific to that route has run. + +The signature for the `routed` event is as follows: + +```js +function(req, res, route) {} +``` + +* req - the request object +* res - the response object +* route - the route object that serviced the request + +Note that this event will *not* fire if a requests comes in that are not +routable, i.e. one that would result in a `404`. + + +### uncaughtException + +If the restify server was created with `handleUncaughtExceptions: true`, +restify will leverage [domains](https://nodejs.org/api/domain.html) to handle +thrown errors in the handler chain. Thrown errors are a result of an explicit +`throw` statement, or as a result of programmer errors like a typo or a null +ref. These thrown errors are caught by the domain, and will be emitted via this +event. For example: + +```js +server.get('/', function(req, res, next) { + res.send(x); // this will cause a ReferenceError + return next(); +}); + +server.on('uncaughtException', function(req, res, route, err) { + // this event will be fired, with the error object from above: + // ReferenceError: x is not defined +}); +``` + +If you listen to this event, you __must__ send a response to the client. This +behavior is different from the standard error events. If you do not listen to +this event, restify's default behavior is to call `res.send()` with the error +that was thrown. + +The signature is for the after event is as follows: + +```js +function(req, res, route, error) { } +``` + +* req - the request object +* res - the response object +* route - the route object that serviced the request +* error - the error passed to `next()`, if applicable + +### close + +Emitted when the server closes. diff --git a/examples/http2/http2.js b/examples/http2/http2.js new file mode 100644 index 000000000..ce60232a9 --- /dev/null +++ b/examples/http2/http2.js @@ -0,0 +1,30 @@ +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 9ff267a5d..46de0e6b2 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,12 +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, @@ -49,832 +42,847 @@ 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#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)); + }; + + + /** + * 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'); -/** - * 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'); + 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); + assert.arrayOfString(types, 'types'); - return (this._negotiator.preferredMediaType(types)); -}; + negotiator(this); + return (this._negotiator.preferredEncoding(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'); + /** + * 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); + } - negotiator(this); + // 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); + } - return (this._negotiator.preferredEncoding(types)); -}; + var len = this.header('content-length'); + 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); - } - - var len = this.header('content-length'); + }; + /** + * 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); + } - if (!len) { - this._clen = false; - } else { - this._clen = parseInt(len, 10); - } + var index; + var type = this.headers['content-type']; - 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; + 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(); -/** - * 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} + */ + 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); -}; - -/** - * 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 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); + } -/** - * 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() { + this._id = uuid.v4(); - 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); -}; - - -/** - * 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); -}; + 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); + } + 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'); - - var contentType = this.getContentType(); - var matches = true; - - if (!contentType) { - return (false); - } + }; + + /** + * 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'; + } - if (type.indexOf('/') === -1) { - type = mime.lookup(type); - } + 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 (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 ((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); + } - return (matches); -}; + 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); + } -/** - * 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'); -}; + 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); + } + 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 - ]); - }); -}; - - -/** - * 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 = []; - } + }; + + + /** + * 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]); + }); - self._timerMap[name] = process.hrtime(self._timerMap[name]); - self.timers.push({ - name: name, - time: self._timerMap[name] - }); + 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 = {}; + } - 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(); + 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 = []; + } -/** - * 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); -}; + 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 + ]); + }); + }; + + + /** + * 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 795fad6de..088d240f9 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. @@ -35,810 +33,831 @@ 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; - } - - return (this.header('Cache-Control', type)); -}; +* 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#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'; + } -/** - * 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'); - return (this); -}; + /** + * 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'); + // 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 || {}); - }; -} + Response.prototype.get = function get(name) { + assert.string(name, 'name'); + return (this.getHeader(name)); + }; -/** - * 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'); - if (value === undefined) { - return (this.getHeader(key)); - } - - 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); - // Check the header blacklist before changing a header to an array - var keyLc = key.toLowerCase(); + /** + * 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'); - if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) { + if (value === undefined) { + return (this.getHeader(key)); + } - 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); + // Check the header blacklist before changing a header to an array + var keyLc = key.toLowerCase(); -/** - * 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'); - } + if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) { - return (this.send(code, body, headers)); -}; + if (Array.isArray(current)) { + current.push(value); + value = current; + } else { + value = [current, value]; + } + } + this.setHeader(key, value); + return (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'); - 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'); + } + return (this.send(code, body, headers)); + }; -/** - * 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); -}; + /** + * 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'); -/** - * 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); -}; + var _link = sprintf('<%s>; rel="%s"', key, value); + return (this.header('Link', _link)); + }; -// 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++]; - } + /** + * 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 body - if (typeof arguments[index] === 'object' || - typeof arguments[index] === 'string') { - body = arguments[index++]; - } - // 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++]; - } - // 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); + // 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++]; + } - // Now lets try to derive values for optional arguments that we were not - // provided, otherwise we choose sane defaults. + // Check to see if the next argument is a body + if (typeof arguments[index] === 'object' || + typeof arguments[index] === 'string') { + body = arguments[index++]; + } - // 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 collection of headers + if (typeof arguments[index] === 'object') { + headers = 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 the format boolean + if (typeof arguments[index] === 'boolean') { + format = arguments[index++]; + } - if (body instanceof Error) { - _props.err = self._body; - } else { - _props.body = self._body; + // 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; } - log.trace(_props, 'response::send entered'); - } - // Flush takes our constructed response object and sends it to the client - function _flush(formattedBody) { - self._data = formattedBody; + // 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]); + }); - // Flush headers - self.writeHead(self.statusCode); + // If log level is set to trace, output our constructed response object + if (log.trace()) { + var _props = { + code: self.statusCode, + headers: self._headers + }; - // Send body if it was provided - if (self._data) { - self.write(self._data); + if (body instanceof Error) { + _props.err = self._body; + } else { + _props.body = self._body; + } + log.trace(_props, 'response::send entered'); } - // Finish request - self.end(); + // Flush takes our constructed response object and sends it + // to the client + function _flush(formattedBody) { + self._data = formattedBody; - // If log level is set to trace, log the entire response object - if (log.trace()) { - log.trace({res: self}, 'response sent'); - } + // Flush headers + self.writeHead(self.statusCode); - // Return the response object back out to the caller of __send - return self; - } + // Send body if it was provided + if (self._data) { + self.write(self._data); + } - // 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(); - } + // Finish request + self.end(); - // 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); - } + // If log level is set to trace, log the entire response object + if (log.trace()) { + log.trace({res: self}, 'response sent'); + } - // 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(); - } + // Return the response object back out to the caller of __send + return self; + } - // 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; + // 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(); } - log.warn({ - req: self.req, - err: err - }, 'error retrieving formatter'); + // 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); + } - return _flush(); - } + // 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(); + } - var formatter; - var type = self.contentType || self.getHeader('Content-Type'); + // 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; + } - // 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' - })); - } + log.warn({ + req: self.req, + err: err + }, 'error retrieving formatter'); - // Derive type if not provided by the user - if (!type) { - type = self.req.accepts(self.acceptable); - } + return _flush(); + } - type = type.split(';')[0]; + var formatter; + var type = self.contentType || self.getHeader('Content-Type'); - if (!self.formatters[type] && type.indexOf('/') === -1) { - type = mime.lookup(type); - } + // 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' + })); + } - // 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'; - } + // Derive type if not provided by the user + if (!type) { + type = self.req.accepts(self.acceptable); + } - formatter = self.formatters[type] || self.formatters['*/*']; + type = type.split(';')[0]; - // 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' - })); - } + if (!self.formatters[type] && type.indexOf('/') === -1) { + type = mime.lookup(type); + } - if (self._charSet) { - type = type + '; charset=' + self._charSet; - } + // 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'; + } - // Update header to the derived content type for our formatter - self.setHeader('Content-Type', type); + formatter = self.formatters[type] || self.formatters['*/*']; - // Finally, invoke the formatter and flush the request with it's results - return _flush(formatter(self.req, self, body)); -}; + // 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' + })); + } + if (self._charSet) { + type = type + '; charset=' + self._charSet; + } -/** - * 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]); - }); - } + // Update header to the derived content type for our formatter + self.setHeader('Content-Type', type); - return (this); -}; + // Finally, invoke the formatter and flush the request with it's results + return _flush(formatter(self.req, self, body)); + }; -/** - * 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'); + /** + * 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]); + }); + } - this.statusCode = code; - return (code); -}; + return (this); + }; -/** - * 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); -}; + /** + * 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'); -if (!Response.prototype.hasOwnProperty('_writeHead')) { - Response.prototype._writeHead = Response.prototype.writeHead; -} + this.statusCode = code; + return (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'); - } + /** + * 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; - this._writeHead.apply(this, arguments); -}; + 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); + }; -/** - * 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 - */ + if (!Response.prototype.hasOwnProperty('_writeHead')) { + Response.prototype._writeHead = Response.prototype.writeHead; + } -/** - * 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 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; + /** + * 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'); + } -/** - * @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; - } + this._writeHead.apply(this, arguments); + }; - // 2) this is signaure number 2 - else if (typeof (arg1) === 'string') { - // otherwise, it's a string, and use it directly - finalUri = arg1; - next = arg2; - } - // 3) signature number 3, using an options object. - else if (typeof (arg1) === '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 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 + */ - // start building url based on options. - // start with the host - if (opt.hostname) { - finalUri.hostname = opt.hostname; - } + /** + * 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); + */ - // then set protocol IFF hostname is set - otherwise we end up with - // malformed URL. - if (finalUri.hostname) { - finalUri.protocol = (secure === true) ? 'https' : 'http'; - } + /** + * 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; - // then set current path after the host - if (opt.pathname) { - finalUri.pathname = opt.pathname; + /** + * @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; } - // then set port - if (opt.port) { - finalUri.port = opt.port; + // 2) this is signaure number 2 + else if (typeof (arg1) === 'string') { + // otherwise, it's a string, and use it directly + finalUri = arg1; + next = arg2; } - // then add query params - if (opt.query) { - if (opt.overrideQuery === true) { - finalUri.query = opt.query; - } else { - finalUri.query = utils.mergeQs(opt.query, finalUri.query); + // 3) signature number 3, using an options object. + else if (typeof (arg1) === '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; } - } - // change status code to 301 permanent if specified - if (opt.permanent) { - statusCode = 301; + // 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 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); + } + } + + // 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 8f014b12e..e14b506d7 100644 --- a/lib/server.js +++ b/lib/server.js @@ -25,9 +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 @@ -75,6 +87,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. @@ -123,6 +137,12 @@ 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..a3f3f312e --- /dev/null +++ b/test/serverHttp2.test.js @@ -0,0 +1,114 @@ +'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); + }); +});