relu-core
Version:
571 lines (462 loc) • 13.2 kB
JavaScript
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var http = require('http');
var url = require('url');
var sprintf = require('util').format;
var assert = require('assert-plus');
var mime = require('mime');
var Negotatior = require('negotiator');
var uuid = require('node-uuid');
var dtrace = require('./dtrace');
///--- Globals
var Request = http.IncomingMessage;
///-- 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._negotatiator) {
req._negotiator = new Negotatior({
headers: {
accept: h.accept || '*/*',
'accept-encoding': h['accept-encoding'] ||
'identity'
}
});
}
return (req._negotiator);
}
///--- API
///--- Patches
/**
* builds an absolute URI for the request.
* @private
* @function absoluteUri
* @param {String} path a url path
* @returns {String}
*/
Request.prototype.absoluteUri = function absoluteUri(path) {
assert.string(path, 'path');
var protocol = this.secure ? 'https://' : 'http://';
var hostname = this.headers.host;
return (url.resolve(protocol + hostname + this.path() + '/', path));
};
/**
* checks if the accept header is present and has the value requested.
* e.g., req.accepts('html');
* @public
* @function accepts
* @param {String | Array} types an array of accept type headers
* @returns {Boolean}
*/
Request.prototype.accepts = function accepts(types) {
if (typeof (types) === 'string') {
types = [types];
}
types = types.map(function (t) {
assert.string(t, 'type');
if (t.indexOf('/') === -1) {
t = mime.lookup(t);
}
return (t);
});
negotiator(this);
return (this._negotiator.preferredMediaType(types));
};
/**
* checks if the request accepts the encoding types.
* @public
* @function acceptsEncoding
* @param {String | Array} types an array of accept type headers
* @returns {Boolean}
*/
Request.prototype.acceptsEncoding = function acceptsEncoding(types) {
if (typeof (types) === 'string') {
types = [types];
}
assert.arrayOfString(types, 'types');
negotiator(this);
return (this._negotiator.preferredEncoding(types));
};
/**
* gets the content-length header off the request.
* @public
* @function getContentLength
* @returns {Number}
*/
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);
};
/**
* pass through to getContentLength
* @public
* @function contentLength
* @returns {Number}
*/
Request.prototype.contentLength = Request.prototype.getContentLength;
/**
* gets the content-type header.
* @public
* @function getContentType
* @returns {String}
*/
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);
};
Request.prototype.contentType = Request.prototype.getContentType;
/**
* gets the _date property off the request. was created when the request
* was setup.
* @private
* @function date
* @returns {Date}
*/
Request.prototype.date = function date() {
if (this._date !== undefined) {
return (this._date);
}
this._date = new Date(this._time);
return (this._date);
};
/**
* retrieves the complete URI requested by the client.
* @public
* @function getHref
* @returns {String}
*/
Request.prototype.getHref = function getHref() {
return (this.getUrl().href);
};
Request.prototype.href = Request.prototype.getHref;
/**
* retrieves the request uuid. was created when the request was setup.
* @public
* @function getId
* @returns {String}
*/
Request.prototype.getId = function getId() {
if (this._id !== undefined) {
return (this._id);
}
this._id = this.headers['request-id'] ||
this.headers['x-request-id'] ||
uuid.v4();
return (this._id);
};
Request.prototype.id = Request.prototype.getId;
/**
* retrieves the cleaned up url path.
* e.g., /foo?a=1 => /foo
* @public
* @function getPath
* @returns {String}
*/
Request.prototype.getPath = function getPath() {
return (this.getUrl().pathname);
};
Request.prototype.path = Request.prototype.getPath;
/**
* returns the raw query string
* @public
* @function getQuery
* @returns {String}
*/
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 || '');
};
Request.prototype.query = Request.prototype.getQuery;
/**
* returns ms since epoch when request was setup.
* @public
* @function time
* @returns {Number}
*/
Request.prototype.time = function time() {
return (this._time);
};
/**
* returns a parsed URL object.
* @public
* @function getUrl
* @returns {Object}
*/
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.
* @public
* @function getVersion
* @returns {String}
*/
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);
};
Request.prototype.version = Request.prototype.getVersion;
Request.prototype.matchedVersion = function matchedVersion() {
if (this._matchedVersion !== undefined) {
return (this._matchedVersion);
} else {
return (this.version());
}
};
/**
* returns any header off the request. also, 'correct' any
* correctly spelled 'referrer' header to the actual spelling used.
* @public
* @function header
* @param {String} name the name of the header
* @param {String} value default value if header isn't found on the req
* @returns {String}
*/
Request.prototype.header = function header(name, value) {
assert.string(name, 'name');
name = name.toLowerCase();
if (name === 'referer' || name === 'referrer') {
name = 'referer';
}
return (this.headers[name] || value);
};
/**
* returns any trailer header off the request. also, 'correct' any
* correctly spelled 'referrer' header to the actual spelling used.
* @public
* @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}
*/
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
* @function is
* @param {String} type a content-type header value
* @returns {Boolean}
*/
Request.prototype.is = function is(type) {
assert.string(type, 'type');
var contentType = this.getContentType();
var matches = true;
if (!contentType) {
return (false);
}
if (type.indexOf('/') === -1) {
type = mime.lookup(type);
}
if (type.indexOf('*') !== -1) {
type = 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
* @function isChunked
* @returns {Boolean}
*/
Request.prototype.isChunked = function isChunked() {
return (this.headers['transfer-encoding'] === 'chunked');
};
/**
* Check if the incoming request is kept alive.
* @public
* @function isKeepAlive
* @returns {Boolean}
*/
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
* @function isSecure
* @returns {Boolean}
*/
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
* @function isUpgradeRequest
* @returns {Boolean}
*/
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
* @function isUpload
* @returns {Boolean}
*/
Request.prototype.isUpload = function isUpload() {
var m = this.method;
return (m === 'PATCH' || m === 'POST' || m === 'PUT');
};
/**
* toString serialization
* @public
* @function toString
* @returns {String}
*/
Request.prototype.toString = function toString() {
var headers = '';
var self = this;
var str;
Object.keys(this.headers).forEach(function (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);
};
/**
* retrieves the user-agent header.
* @public
* @function userAgent
* @returns {String}
*/
Request.prototype.userAgent = function userAgent() {
return (this.headers['user-agent']);
};
/**
* Start the timer for a request handler function. You must explicitly invoke
* endHandlerTimer() after invoking this function. Otherwise timing information
* will be inaccurate.
* @public
* @function startHandlerTimer
* @param {String} handlerName The name of the handler.
* @returns {undefined}
*/
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 () {
return ([
self.serverName,
self._currentRoute, // set in server._run
name,
self._dtraceId
]);
});
};
/**
* Stop the timer for a request handler function.
* @public
* @function endHandlerTimer
* @param {String} handlerName The name of the handler.
* @returns {undefined}
*/
Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) {
var self = this;
// For nested handlers, we prepend the top level handler func name
var name = (self._currentHandler === handlerName ?
handlerName : self._currentHandler + '-' + handlerName);
if (!self.timers) {
self.timers = [];
}
self._timerMap[name] = process.hrtime(self._timerMap[name]);
self.timers.push({
name: name,
time: self._timerMap[name]
});
dtrace._rstfy_probes['handler-done'].fire(function () {
return ([
self.serverName,
self._currentRoute, // set in server._run
name,
self._dtraceId
]);
});
};