// Copyright 2012 Mark Cavage, Inc. All rights reserved. 'use strict'; var url = require('url'); var sprintf = require('util').format; var assert = require('assert-plus'); var mime = require('mime'); var Negotiator = require('negotiator'); var uuid = require('uuid'); var dtrace = require('./dtrace'); ///-- Helpers /** * Creates and sets negotiator on request if one doesn't already exist, * then returns it. * * @private * @function negotiator * @param {Object} req - the request object * @returns {Object} a negotiator */ function negotiator(req) { var h = req.headers; if (!req._negotiator) { req._negotiator = new Negotiator({ headers: { accept: h.accept || '*/*', 'accept-encoding': h['accept-encoding'] || 'identity' } }); } return req._negotiator; } ///--- API /** * Patch Request object and extends with extra functionalities * * @private * @function patch * @param {http.IncomingMessage|http2.Http2ServerRequest} Request - * Server Request * @returns {undefined} No return value */ function patch(Request) { /** * Wraps all of the node * [http.IncomingMessage](https://nodejs.org/api/http.html) * APIs, events and properties, plus the following. * @class Request * @extends http.IncomingMessage */ ///--- Patches /** * Builds an absolute URI for the request. * * @private * @memberof Request * @instance * @function absoluteUri * @param {String} path - a url path * @returns {String} uri */ Request.prototype.absoluteUri = function absoluteUri(path) { assert.string(path, 'path'); var protocol = this.isSecure() ? 'https://' : 'http://'; var hostname = this.headers.host; return url.resolve(protocol + hostname + this.path() + '/', path); }; /** * Check if the Accept header is present, and includes the given type. * When the Accept header is not present true is returned. * Otherwise the given type is matched by an exact match, and then subtypes. * * @public * @memberof Request * @instance * @function accepts * @param {String | String[]} types - an array of accept type headers * @returns {Boolean} is accepteed * @example * * You may pass the subtype such as html which is then converted internally * to text/html using the mime lookup table: * * // Accept: text/html * req.accepts('html'); * // => true * * // Accept: text/*; application/json * req.accepts('html'); * req.accepts('text/html'); * req.accepts('text/plain'); * req.accepts('application/json'); * // => true * * req.accepts('image/png'); * req.accepts('png'); * // => false */ Request.prototype.accepts = function accepts(types) { if (typeof types === 'string') { types = [types]; } types = types.map(function map(t) { assert.string(t, 'type'); if (t.indexOf('/') === -1) { t = mime.getType(t); } return t; }); negotiator(this); return this._negotiator.preferredMediaType(types); }; /** * Checks if the request accepts the encoding type(s) specified. * * @public * @memberof Request * @instance * @function acceptsEncoding * @param {String | String[]} types - an array of accept type headers * @returns {Boolean} is accepted encoding */ Request.prototype.acceptsEncoding = function acceptsEncoding(types) { if (typeof types === 'string') { types = [types]; } assert.arrayOfString(types, 'types'); negotiator(this); return this._negotiator.preferredEncoding(types); }; /** * Returns the value of the content-length header. * * @private * @memberof Request * @instance * @function getContentLength * @returns {Number} content length */ Request.prototype.getContentLength = function getContentLength() { if (this._clen !== undefined) { return this._clen === false ? undefined : this._clen; } // 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'); if (!len) { this._clen = false; } else { this._clen = parseInt(len, 10); } 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; /** * 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} content type */ Request.prototype.contentType = Request.prototype.getContentType; /** * Returns a Date object representing when the request was setup. * Like `time()`, but returns a Date object. * * @public * @memberof Request * @instance * @function date * @returns {Date} date when request began being processed */ Request.prototype.date = function date() { 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 request uuid. was created when the request was setup. * * @private * @memberof Request * @instance * @function getId * @returns {String} id */ Request.prototype.getId = function getId() { if (this._id !== undefined) { return this._id; } this._id = uuid.v4(); return this._id; }; /** * Returns the request id. If a `reqId` value is passed in, * this will become the request’s new id. The request id is immutable, * and can only be set once. Attempting to set the request id more than * once will cause restify to throw. * * @public * @memberof Request * @instance * @function id * @param {String} reqId - request id * @returns {String} id */ Request.prototype.id = function id(reqId) { var self = this; if (reqId) { if (self._id) { throw new Error( 'request id is immutable, cannot be set again!' ); } else { assert.string(reqId, 'reqId'); self._id = reqId; return self._id; } } 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 when request began being processed in epoch: * ellapsed milliseconds since * January 1, 1970, 00:00:00 UTC */ Request.prototype.time = function time() { return this._date.getTime(); }; /** * 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'] || '*'; 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; } if (type.indexOf('/') === -1) { type = mime.getType(type); } if (type.indexOf('*') !== -1) { type = type.split('/'); contentType = contentType.split('/'); matches &= type[0] === '*' || type[0] === contentType[0]; matches &= type[1] === '*' || type[1] === contentType[1]; } else { matches = contentType === type; } return matches; }; /** * 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; } 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; } 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(); if (self.dtrace) { dtrace._rstfy_probes['handler-start'].fire(function fire() { return [ self.serverName, self._currentRoute, // set in server._run name, self._dtraceId ]; }); } }; /** * End the timer for a request handler. * You must invoke this function if you called `startRequestHandler` on a * handler. Otherwise the time recorded will be incorrect. * * @public * @memberof Request * @instance * @function endHandlerTimer * @param {String} handlerName - The name of the handler. * @returns {undefined} no return value */ Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) { var self = this; // For nested handlers, we prepend the top level handler func name var name = self._currentHandler === handlerName ? handlerName : self._currentHandler + '-' + handlerName; if (!self.timers) { self.timers = []; } self._timerMap[name] = process.hrtime(self._timerMap[name]); self.timers.push({ name: name, time: self._timerMap[name] }); if (self.dtrace) { 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 * * @public * @memberof Request * @instance * @function connectionState * @returns {String} connection state (`"close"`) */ Request.prototype.connectionState = function connectionState() { var self = this; return self._connectionState; }; /** * Returns true when connection state is "close" * * @private * @memberof Request * @instance * @function closed * @returns {Boolean} is closed */ Request.prototype.closed = function closed() { var self = this; return self.connectionState() === 'close'; }; /** * 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;