hapi
Version:
HTTP Server framework
742 lines (509 loc) • 19.7 kB
JavaScript
// Load modules
var Stream = require('stream');
var Url = require('url');
var Async = require('async');
var Boom = require('boom');
var Utils = require('./utils');
var Payload = require('./payload');
var State = require('./state');
var Auth = require('./auth');
var Validation = require('./validation');
var Response = require('./response');
var Cached = require('./response/cached');
var Closed = require('./response/closed');
var Ext = require('./ext');
// Declare internals
var internals = {};
exports = module.exports = internals.Request = function (server, req, res, options) {
var now = Date.now(); // Take measurement as soon as possible
Utils.assert(this.constructor === internals.Request, 'Request must be instantiated using new');
Utils.assert(server, 'server must be provided');
Utils.assert(req, 'req must be provided');
Utils.assert(res, 'res must be provided');
options = options || {};
// Public members
this.server = server;
this._setUrl(req.url); // Sets: this.url, this.path, this.query
this._setMethod(req.method); // Sets: this.method
this.id = now + '-' + process.pid + '-' + Math.floor(Math.random() * 0x10000);
this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins
this.plugins = {}; // Place for plugins to store state without conflicts with hapi, should be namespaced using plugin name
this.route = {};
this.auth = {
isAuthenticated: false,
credentials: null, // Special keys: 'app', 'user', 'scope', 'tos'
artifacts: null // Scheme-specific artifacts
// session: { set(), clear() }
};
this.session = null; // Special key reserved for plugins implementing session support
this.pre = {};
this.info = {
received: now,
address: (req.connection && req.connection.remoteAddress) || '',
referrer: req.headers.referrer || req.headers.referer || '',
host: req.headers.host ? req.headers.host.replace(/\s/g, '') : ''
};
// Apply options
if (options.credentials) {
this.auth.credentials = options.credentials;
}
// Defined elsewhere:
// this.query
// this.params
// this.rawPayload
// this.payload
// this.state
// this.setUrl()
// this.setMethod()
// this.reply(): { hold(), send(), close(), raw(), payload(), stream(), redirect(), view() }
// this.response()
// Semi-public members
this.raw = {
req: req,
res: res
};
this.setState = this._setState; // Remove once replied
this.clearState = this._clearState; // Remove once replied
this.tail = this.addTail = this._addTail; // Removed once wagging
// Private members
this._timestamp = now;
this._route = null;
this._states = {}; // Appended to response states when setting response headers
this._logger = [];
this._response = null;
this._isReplied = false;
this._tails = {}; // tail id -> name (tracks pending tails)
this._tailIds = 0; // Used to generate a unique tail id
this._isWagging = false; // true when request completed and only waiting on tails to complete
this._paramsArray = []; // Array of path parameters in path order
// Log request
var about = {
id: this.id,
method: this.method,
url: this.url.href,
agent: this.raw.req.headers['user-agent']
};
this._log(['received'], about, now); // Must be last for object to be fully constructed
return this;
};
internals.Request.prototype._setUrl = function (url) {
this.url = Url.parse(url, true);
this.query = this.url.query || {};
this.path = this.url.pathname; // pathname excludes query
if (this.path &&
this.server.settings.router.normalizeRequestPath) {
// Uppercase %encoded values
var uppercase = this.path.replace(/%[0-9a-fA-F][0-9a-fA-F]/g, function (encoded) { return encoded.toUpperCase(); });
// Decode non-reserved path characters: a-z A-Z 0-9 _!$&'()*+,;=:@-.~
// ! (%21) $ (%24) & (%26) ' (%27) ( (%28) ) (%29) * (%2A) + (%2B) , (%2C) - (%2D) . (%2E)
// 0-9 (%30-39) : (%3A) ; (%3B) = (%3D)
// @ (%40) A-Z (%41-5A) _ (%5F) a-z (%61-7A) ~ (%7E)
var decoded = uppercase.replace(/%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g, function (encoded) { return String.fromCharCode(parseInt(encoded.substring(1), 16)); });
this.path = decoded;
}
};
internals.Request.prototype._setMethod = function (method) {
if (method) {
this.method = method.toLowerCase();
}
};
internals.Request.prototype._log = function (tags, data, timestamp) {
this.log(['hapi'].concat(tags), data, timestamp);
};
internals.Request.prototype.log = function (tags, data, timestamp) {
tags = (tags instanceof Array ? tags : [tags]);
// Prepare log item
var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now());
var item = {
request: this.id,
timestamp: now,
tags: tags
};
var tagsMap = Utils.mapToObject(item.tags);
if (data) {
if (data instanceof Error) {
item.tags = tags.concat('error');
tagsMap.error = true;
item.data = { message: data.message };
if (tagsMap.uncaught) {
item.data.trace = data.stack;
}
}
else {
item.data = data;
}
}
// Add to request array
this._logger.push(item);
this.server.emit('request', this, item, tagsMap);
if (this.server.listeners('request').length === 1 && // Pack always listening
this.server.settings.debug &&
this.server.settings.debug.request &&
Utils.intersect(tagsMap, this.server.settings.debug.request).length) {
console.error('Unmonitored error: ' + item.tags.join(', '));
if (data) {
console.error(data instanceof Error ? data.stack : data);
}
}
};
internals.Request.prototype.getLog = function (tags) {
tags = [].concat(tags || []);
if (!tags.length) {
return this._logger;
}
var filter = Utils.mapToObject(tags);
var result = [];
for (var i = 0, il = this._logger.length; i < il; ++i) {
var event = this._logger[i];
for (var t = 0, tl = event.tags.length; t < tl; ++t) {
var tag = event.tags[t];
if (filter[tag]) {
result.push(event);
}
}
}
return result;
};
internals.Request.prototype._onRequestExt = function (callback) {
var self = this;
// Decorate request
this.setUrl = this._setUrl;
this.setMethod = this._setMethod;
this.server._ext.invoke(this, 'onRequest', function (err) {
// Undecorate request
delete self.setUrl;
delete self.setMethod;
if (!err) {
return callback();
}
// Send error response
self._route = self.server._router.notfound; // Only settings are used, not the handler
self.route = self.server._router.notfound.settings;
self._reply(err, function () {
return callback(err);
});
});
};
internals.Request.prototype._execute = function (route) {
var self = this;
this._route = route;
this.route = route.settings;
var serverTimeout = self.server.settings.timeout.server;
if (serverTimeout) {
serverTimeout -= (Date.now() - self._timestamp); // Calculate the timeout from when the request was constructed
var timeoutReply = function () {
self._reply(Boom.serverTimeout());
};
if (serverTimeout <= 0) {
return timeoutReply();
}
self._serverTimeoutId = setTimeout(timeoutReply, serverTimeout);
}
var ext = function (event) {
return function (request, next) {
self.server._ext.invoke(self, event, next);
};
};
var funcs = [
// 'onRequest' in Server
State.parseCookies,
ext('onPreAuth'),
Auth.authenticate, // Authenticates the raw.req object
Payload.read,
Auth.authenticatePayload,
ext('onPostAuth'),
Validation.path,
internals.queryExtensions,
Validation.query,
Validation.payload,
ext('onPreHandler'),
internals.handler, // Must not call next() with an Error
ext('onPostHandler'), // An error from here on will override any result set in handler()
Validation.response
// 'onPreResponse' in _reply // Always called
];
Async.forEachSeries(funcs, function (func, next) {
if (self._isReplied) {
self._log(['server', 'timeout']);
return next(true); // Argument is ignored but aborts the series
}
func(self, next);
},
function (err) {
self._reply(err);
});
};
internals.Request.prototype._reply = function (exit, callback) {
var self = this;
callback = callback || function () { };
if (this._isReplied) { // Prevent any future responses to this request
return Utils.nextTick(callback)();
}
this._isReplied = true;
clearTimeout(self._serverTimeoutId);
var process = function () {
if (self._response &&
self._response.variety === 'closed') {
self.raw.res.end(); // End the response in case it wasn't already closed
return Utils.nextTick(finalize)();
}
if (exit) {
override(exit);
}
self.server._ext.invoke(self, 'onPreResponse', function (err) {
delete self.response;
delete self.setState;
delete self.clearState;
if (err) { // err can be valid response override
override(err);
}
Response._respond(self._response, self, finalize);
});
};
var override = function (value) {
if (self._response &&
!self._response.isBoom &&
!self._response.varieties.error) {
// Got error after valid result was already set
self._route.cache.drop(self.url.path, function (err) {
self._log(['cache', 'drop'], err);
});
}
self._setResponse(Response._generate(value));
};
var finalize = function () {
if (self._response &&
((self._response.isBoom && self._response.response.code === 500) ||
(self._response.varieties && self._response.varieties.error && self._response._code === 500))) {
var error = (self._response.isBoom ? self._response : self._response._err);
self.server.emit('internalError', self, error);
self._log(['internal'], error);
}
self.server.emit('response', self);
self._isWagging = true;
delete self.addTail;
delete self.tail;
if (Object.keys(self._tails).length === 0) {
self.server.emit('tail', self);
}
return callback();
};
process();
};
internals.queryExtensions = function (request, next) {
// JSONP
if (request.route.jsonp) {
var jsonp = request.query[request.route.jsonp];
if (jsonp) {
if (!jsonp.match(/^[\w\$\[\]\.]+$/)) {
return next(Boom.badRequest('Invalid JSONP parameter value'));
}
request.jsonp = jsonp;
delete request.query[request.route.jsonp];
}
}
return next();
};
internals.Request.prototype._replyInterface = function (next, withProperties) {
var self = this;
// All the 'reply' methods execute inside a protected domain and can safetly throw
var response = null;
var wasProcessed = false;
// Chain finalizers
var process = function () {
if (wasProcessed) {
return;
}
wasProcessed = true;
return next(response);
};
var reply = function (result) {
delete self.reply;
Utils.assert(!self.route.cache.mode.server || result instanceof Stream === false, 'Cannot reply using a stream when caching enabled');
response = Response._generate(result, process);
return response;
};
if (!withProperties) {
return reply;
}
if (!this.route.cache.mode.server) {
reply.close = function () {
delete self.reply;
response = new Closed();
process();
};
}
// Chain initializers
reply.redirect = function (uri) {
delete self.reply;
response = Response._generate(new Response.Redirection(uri), process);
return response;
};
if (this.server._views ||
this._route.env.views) {
reply.view = function (template, context, options) {
delete self.reply;
var viewsManager = self._route.env.views || self.server._views;
response = Response._generate(new Response.View(viewsManager, template, context, options), process);
return response;
};
}
return reply;
};
internals.Request.bindPre = function (pre) {
/*
{
method: function (request, next) {},
assign: key,
mode: parallel
}
*/
return function (request, next) {
Ext.runProtected(request._log.bind(request), 'pre', next, function (enter, exit) {
var timer = new Utils.Timer();
var finalize = function (result) {
if (result instanceof Error) {
request._log(['pre', 'error'], { msec: timer.elapsed(), assign: pre.assign, mode: pre.mode, error: result });
return exit(result);
}
request._log(['pre'], { msec: timer.elapsed(), assign: pre.assign, mode: pre.mode });
if (pre.assign) {
request.pre[pre.assign] = result;
}
return exit();
};
enter(function () {
pre.method(request, finalize);
});
});
};
};
internals.handler = function (request, next) {
var check = function () {
// Cached
if (request.route.cache.mode.server) {
return lookup();
}
// Not cached
return generate(function (err, result) {
return store(err || result);
});
};
var lookup = function () {
// Lookun in cache
request._route.cache.getOrGenerate(request.url.path, generate, function (err, value, cached, report) { // request.url.path contains query
request._log(['cache', 'get'], report);
if (err) {
return store(err);
}
if (cached) {
return store(new Cached(value, cached.ttl));
}
return store(value);
});
};
var prerequisites = function (callback) {
if (!request._route.prerequisites.parallel.length &&
!request._route.prerequisites.serial.length) {
return Utils.nextTick(callback)();
}
Async.series([
function (nextSet) {
Async.forEach(request._route.prerequisites.parallel, function (pre, nextPre) {
pre(request, nextPre);
}, nextSet);
},
function (nextSet) {
Async.forEachSeries(request._route.prerequisites.serial, function (pre, nextPre) {
pre(request, nextPre);
}, nextSet);
}
], function (err, results) {
return callback(err);
});
};
var generate = function (callback) {
// Execute prerequisites
prerequisites(function (err) {
if (err) {
return callback(err);
}
Ext.runProtected(request._log.bind(request), 'handler', callback, function (enter, exit) {
var timer = new Utils.Timer();
var finalize = function (response) {
// Check for Error result
if (response &&
(response.isBoom || response.varieties.error)) {
request._log(['handler', 'error'], { msec: timer.elapsed() });
return exit(response);
}
request._log(['handler'], { msec: timer.elapsed() });
return exit(null, response);
};
// Execute handler
enter(function () {
request.reply = request._replyInterface(finalize, true);
request.route.handler.call(request, request, request._replyInterface(finalize, false));
});
});
});
};
var store = function (response) {
request._setResponse(response);
return next(); // Must not include an argument
};
check();
};
internals.Request.prototype._setResponse = function (response) {
var self = this;
this._response = response;
this.response = this.response || function () {
return self._response;
};
};
internals.Request.prototype._addTail = function (name) {
var self = this;
name = name || 'unknown';
var tailId = this._tailIds++;
this._tails[tailId] = name;
this._log(['tail', 'add'], { name: name, id: tailId });
var drop = function () {
if (!self._tails[tailId]) {
self._log(['tail', 'remove', 'error'], { name: name, id: tailId }); // Already removed
return;
}
delete self._tails[tailId];
if (Object.keys(self._tails).length === 0 &&
self._isWagging) {
self._log(['tail', 'remove', 'last'], { name: name, id: tailId });
self.server.emit('tail', self);
}
else {
self._log(['tail', 'remove'], { name: name, id: tailId });
}
};
return drop;
};
internals.Request.prototype._setState = function (name, value, options) {
if (this._response &&
this._response.state) {
this._response.state(name, value, options);
}
else {
Response.Generic.prototype.state.call(this, name, value, options);
}
};
internals.Request.prototype._clearState = function (name) {
if (this._response &&
this._response.unstate) {
this._response.unstate(name);
}
else {
Response.Generic.prototype.unstate.call(this, name);
}
};
internals.Request.prototype.generateView = function (template, context, options) {
var viewsManager = this._route.env.views || this.server._views;
Utils.assert(viewsManager, 'Cannot generate view without a views manager initialized');
return new Response.View(viewsManager, template, context, options);
};