// Copyright 2012 Mark Cavage, Inc. All rights reserved. 'use strict'; var http = require('http'); var sprintf = require('util').format; var url = require('url'); var assert = require('assert-plus'); var mime = require('mime'); var errors = require('restify-errors'); var httpDate = require('./http_date'); var utils = require('./utils'); ///--- Globals var InternalServerError = errors.InternalServerError; /** * @private * Headers that cannot be multi-values. * @see #779, multiple set-cookie values are allowed only as multiple headers. * @see #986, multiple content-type values / headers disallowed. */ var HEADER_ARRAY_BLACKLIST = { 'content-type': true }; ///--- API /** * 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) { assert.func(Response, 'Response'); /** * Wraps all of the node * [http.ServerResponse](https://nodejs.org/docs/latest/api/http.html) * APIs, events and properties, plus the following. * @class Response * @extends http.ServerResponse */ /** * 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.setHeader('Cache-Control', type); }; /** * 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.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTTP 1.0 this.setHeader('Pragma', 'no-cache'); // Proxies this.setHeader('Expires', '0'); 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'); this._charSet = type; return this; }; /** * 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'); return this.getHeader(name); }; // 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 || {}; }; } /** * 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 `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); } var current = this.getHeader(key); // Check the header blacklist before changing a header to an array var keyLc = key.toLowerCase(); if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) { if (Array.isArray(current)) { current.push(value); value = current; } else { value = [current, value]; } } this.setHeader(key, value); return value; }; /** * 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) { this.setHeader('Content-Type', 'application/json'); return this.send(code, body, headers); }; /** * 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); }; /** * 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 sendArgs; if (typeof code === 'number') { sendArgs = { code: code, body: body, headers: headers }; } else { sendArgs = { body: code, headers: body }; } sendArgs.format = true; return self.__send(sendArgs); }; /** * 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 sendArgs; if (typeof code === 'number') { sendArgs = { code: code, body: body, headers: headers }; } else { sendArgs = { body: code, headers: body }; } assert.ok( typeof sendArgs.body === 'string' || Buffer.isBuffer(sendArgs.body), 'res.sendRaw() accepts only strings or buffers' ); sendArgs.format = false; return self.__send(sendArgs); }; // 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 {Object} opts - an option sobject * @param {Object | Buffer | String | Error} opts.body - the content to send * @param {Boolean} opts.format - When false, skip formatting * @param {Number} [opts.code] - http status code * @param {Object} [opts.headers] - any add'l headers to set * @returns {Object} - returns the response object */ Response.prototype.__send = function __send(opts) { var self = this; var isHead = self.req.method === 'HEAD'; var log = self.log; var code = opts.code; var body = opts.body; var headers = opts.headers || {}; self._sent = true; // 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; } // Set sane defaults for optional arguments if they were not provided // and we failed to derive their values code = code || self.statusCode || 200; // 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 }; if (body instanceof Error) { _props.err = self._body; } else { _props.body = self._body; } log.trace(_props, 'response::send entered'); } // 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(self); } if (opts.format === true) { // 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(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 var formatter; var type = self.contentType || self.getHeader('Content-Type'); // Set Content-Type to application/json when // res.send is called with an Object instead of calling res.json if (!type && typeof body === 'object' && !Buffer.isBuffer(body)) { type = 'application/json'; } // Derive type if not provided by the user type = type || self.req.accepts(self.acceptable); // Check to see if we could find a content type to use for the // response. if (!type) { return formatterError( self, new errors.NotAcceptableError({ message: 'could not find suitable content-type to use ' + 'for the response' }) ); } type = type.split(';')[0]; if (!self.formatters[type] && type.indexOf('/') === -1) { type = mime.getType(type); } // If finding a formatter matching the negotiated content-type is // required, and we were unable to derive a valid type, default to // treating it as arbitrary binary data per RFC 2046 Section 4.5.1 if ( this._strictFormatters && !self.formatters[type] && self.acceptable.indexOf(type) === -1 ) { type = 'application/octet-stream'; } formatter = self.formatters[type] || self.formatters['*/*']; // If after the above attempts we were still unable to derive a // formatter, provide a meaningful error message if (this._strictFormatters && !formatter) { return formatterError( self, new errors.InternalServerError({ message: 'could not find formatter for response ' + 'content-type "' + type + '"' }) ); } var formatterType = type; if (self._charSet) { type = type + '; charset=' + self._charSet; } // Update Content-Type header to the one originally set or to the // type inferred from the most relevant formatter found. self.setHeader('Content-Type', type); if (formatter) { // Finally, invoke the formatter and flush the request with it's // results var formattedBody; try { formattedBody = formatter(self.req, self, body); } catch (e) { if ( e instanceof errors.RestError || e instanceof errors.HttpError ) { var res = formatterError( self, e, 'error in formatter (' + formatterType + ') formatting response body' ); return res; } throw e; } return flush(self, formattedBody); } } return flush(self, body); }; /** * 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]); }); } return this; }; /** * 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'); this.statusCode = code; return code; }; /** * toString() serialization. * * @private * @memberof Response * @instance * @function toString * @returns {String} stringified response */ Response.prototype.toString = function toString() { var headers = this.getHeaders(); var headerString = ''; var str; Object.keys(headers).forEach(function forEach(k) { headerString += k + ': ' + headers[k] + '\n'; }); str = sprintf( 'HTTP/1.1 %s %s\n%s', this.statusCode, http.STATUS_CODES[this.statusCode], headerString ); return str; }; if (!Response.prototype.hasOwnProperty('_writeHead')) { Response.prototype._writeHead = Response.prototype.writeHead; } /** * Pass through to native response.writeHead() * * @private * @memberof Response * @instance * @function writeHead * @fires header * @returns {undefined} no return value */ Response.prototype.writeHead = function restifyWriteHead() { this.emit('header'); if (this.statusCode === 204 || this.statusCode === 304) { this.removeHeader('Content-Length'); this.removeHeader('Content-MD5'); this.removeHeader('Content-Type'); this.removeHeader('Content-Encoding'); } this._writeHead.apply(this, arguments); }; /** * Redirect is sugar method for redirecting. * @public * @memberof Response * @instance * @param {Object} options url or an options object to configure a redirect * @param {Boolean} [options.secure] whether to redirect to http or https * @param {String} [options.hostname] redirect location's hostname * @param {String} [options.pathname] redirect location's pathname * @param {String} [options.port] redirect location's port number * @param {String} [options.query] redirect location's query string * parameters * @param {Boolean} [options.overrideQuery] if true, `options.query` * stomps over any existing query * parameters on current URL. * by default, will merge the two. * @param {Boolean} [options.permanent] if true, sets 301. defaults to 302. * @param {Function} next mandatory, to complete the response and trigger * audit logger. * @fires redirect * @function redirect * @returns {undefined} * @example * res.redirect({...}, next); * @example * * A convenience method for 301/302 redirects. Using this method will tell * restify to stop execution of your handler chain. * You can also use an options object. `next` is required. * * res.redirect({ * hostname: 'www.foo.com', * pathname: '/bar', * port: 80, // defaults to 80 * secure: true, // sets https * permanent: true, * query: { * a: 1 * } * }, next); // => redirects to 301 https://www.foo.com/bar?a=1 */ /** * Redirect with 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; /** * @private * @param {*} arg1 - arg1 * @param {*} arg2 - arg2 * @param {*} arg3 - arg3 * @fires redirect * @function redirect * @returns {undefined} no return value */ function redirect(arg1, arg2, arg3) { var self = this; var statusCode = 302; var finalUri; var redirectLocation; var next; // 1) this is signature 1, where an explicit status code is passed in. // MUST guard against null here, passing null is likely indicative // of an attempt to call res.redirect(null, next); // as a way to do a reload of the current page. if (arg1 && !isNaN(arg1)) { statusCode = arg1; finalUri = arg2; next = arg3; } else if (typeof arg1 === 'string') { // 2) this is signaure number 2 // otherwise, it's a string, and use it directly finalUri = arg1; next = arg2; } else if (typeof arg1 === 'object') { // 3) signature number 3, using an options object. // set next, then go to work. next = arg2; var req = self.req; var opt = arg1 || {}; var currentFullPath = req.href(); var secure = opt.hasOwnProperty('secure') ? opt.secure : req.isSecure(); // if hostname is passed in, use that as the base, // otherwise fall back on current url. var parsedUri = url.parse(opt.hostname || currentFullPath, true); // create the object we'll use to format for the final uri. // this object will eventually get passed to url.format(). // can't use parsedUri to seed it, as it confuses the url module // with some existing parsed state. instead, we'll pick the things // we want and use that as a starting point. finalUri = { port: parsedUri.port, hostname: parsedUri.hostname, query: parsedUri.query, pathname: parsedUri.pathname }; // start building url based on options. // start with the host if (opt.hostname) { finalUri.hostname = opt.hostname; } // then set protocol IFF hostname is set - otherwise we end up with // malformed URL. if (finalUri.hostname) { finalUri.protocol = secure === true ? 'https' : 'http'; } // then set 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 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); self.emit('redirect', 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); } } /** * Flush takes our constructed response object and sends it to the client * * @private * @function flush * @param {Response} res - response * @param {String|Buffer} body - response body * @returns {Response} response */ function flush(res, body) { assert.ok( body === null || body === undefined || typeof body === 'string' || Buffer.isBuffer(body), 'body must be a string or a Buffer instance' ); res._data = body; // Flush headers res.writeHead(res.statusCode); // Send body if it was provided if (res._data) { res.write(res._data); } // Finish request res.end(); // If log level is set to trace, log the entire response object if (res.log.trace()) { res.log.trace({ res: res }, 'response sent'); } // Return the response object back out to the caller of __send return res; } /** * formatterError is used to handle any case where we were unable to * properly format the provided body * * @private * @function formatterError * @param {Response} res - response * @param {Error} err - error * @param {String} [msg] - custom log message * @returns {Response} response */ function formatterError(res, err, msg) { // 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 (res.statusCode >= 200 && res.statusCode < 300) { res.statusCode = err.statusCode; } if (typeof msg !== 'string') { msg = 'error retrieving formatter'; } res.log.warn( { req: res.req, err: err }, msg ); return flush(res); } module.exports = patch;