// 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;