hapi
Version:
HTTP Server framework
445 lines (294 loc) • 12.8 kB
JavaScript
// Load modules
var Events = require('events');
var Http = require('http');
var Https = require('https');
var Shot = require('shot');
var Boom = require('boom');
var Catbox = require('catbox');
var Auth = require('./auth');
var Defaults = require('./defaults');
var Request = require('./request');
var Router = require('./router');
var Schema = require('./schema');
var Views = require('./views');
var Ext = require('./ext');
var Utils = require('./utils');
// Pack delayed required inline
// Declare internals
var internals = {};
module.exports = internals.Server = function (/* host, port, options */) { // all optional
Utils.assert(this.constructor === internals.Server, 'Server must be instantiated using new');
var Pack = require('./pack'); // Delayed required to avoid circular dependencies
// Register as event emitter
Events.EventEmitter.call(this);
// Validate arguments
Utils.assert(arguments.length <= 4, 'Too many arguments'); // 4th is for internal Pack usage
var argMap = {
string: 'host',
number: 'port',
object: 'options'
};
var args = {};
for (var a = 0, al = arguments.length; a < al; ++a) {
if (arguments[a] === undefined) {
continue;
}
if (arguments[a] instanceof Pack) {
args.pack = arguments[a];
continue;
}
var type = typeof arguments[a];
var key = argMap[type];
Utils.assert(key, 'Bad server constructor arguments: no match for arg type:', type);
Utils.assert(!args[key], 'Bad server constructor arguments: duplicated arg type:', type);
args[key] = arguments[a];
}
this.settings = Utils.applyToDefaults(Defaults.server, args.options || {});
var schemaError = Schema.server(this.settings);
Utils.assert(!schemaError, 'Invalid server options:', schemaError);
// Set basic configuration
this._host = args.host ? args.host.toLowerCase() : '';
this._port = typeof args.port !== 'undefined' ? args.port : (this.settings.tls ? 443 : 80);
Utils.assert(!this.settings.location || this.settings.location.charAt(this.settings.location.length - 1) !== '/', 'Location setting must not contain a trailing \'/\'');
var socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket);
Utils.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout');
Utils.assert(!this.settings.timeout.client || !socketTimeout || this.settings.timeout.client < socketTimeout, 'Client timeout must be shorter than socket timeout');
// Server facilities
this._started = false;
this._auth = new Auth(this); // Required before _router
this._router = new Router(this);
this._ext = new Ext();
this._stateDefinitions = {};
if (args.pack) {
this.pack = args.pack;
}
else {
this.pack = new Pack({ cache: this.settings.cache });
this.pack._server(this);
}
this.plugins = {}; // Registered plugin APIs by plugin name
this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins
this.helpers = []; // Helper functions
// Generate CORS headers
this.settings.cors = Utils.applyToDefaults(Defaults.cors, this.settings.cors);
if (this.settings.cors) {
this.settings.cors._headers = (this.settings.cors.headers || []).concat(this.settings.cors.additionalHeaders || []).join(', ');
this.settings.cors._methods = (this.settings.cors.methods || []).concat(this.settings.cors.additionalMethods || []).join(', ');
this.settings.cors._exposedHeaders = (this.settings.cors.exposedHeaders || []).concat(this.settings.cors.additionalExposedHeaders || []).join(', ');
}
// Initialize Views
if (this.settings.views) {
this._views = new Views(this.settings.views);
}
// Create server
if (this.settings.tls) {
this.listener = Https.createServer(this.settings.tls, this._dispatch());
}
else {
this.listener = Http.createServer(this._dispatch());
}
// Authentication
if (this.settings.auth) {
this._auth.addBatch(this.settings.auth);
}
// Server information
this.info = {
host: this._host || '0.0.0.0',
port: this._port || 0,
protocol: (this.settings.tls ? 'https' : 'http')
};
if (this.info.port) {
this.info.uri = this.info.protocol + '://' + this.info.host + ':' + this.info.port;
}
return this;
};
Utils.inherits(internals.Server, Events.EventEmitter);
internals.Server.prototype._dispatch = function (options) {
var self = this;
return function (req, res) {
// Create request object
var request = new Request(self, req, res, options);
if (req.socket &&
self.settings.timeout.socket !== undefined) {
req.socket.setTimeout(self.settings.timeout.socket || 0);
}
// Execute onRequest extensions (can change request method and url)
request._onRequestExt(function (err) {
if (err) {
return; // Handled by the request
}
// Lookup route
request._execute(self._router.route(request));
});
};
};
internals.Server.prototype.routingTable = function () {
return this._router.routingTable();
};
// Start server listener
internals.Server.prototype.start = function (callback) {
this.pack.start(callback);
};
internals.Server.prototype._start = function (callback) {
var self = this;
callback = callback || function () { };
if (this._started) {
return Utils.nextTick(callback)();
}
this._started = true;
this._connections = {};
this.listener.once('listening', function () {
// Update the host, port, and uri with active values
var address = self.listener.address();
self.info.host = self._host || address.address || '0.0.0.0';
self.info.port = address.port;
self.info.uri = self.info.protocol + '://' + self.info.host + ':' + self.info.port;
return callback();
});
this.listener.on('connection', function(connection) {
var key = connection.remoteAddress + ':' + connection.remotePort;
self._connections[key] = connection;
connection.once('close', function() {
delete self._connections[key];
});
});
this.listener.listen(this._port, this._host);
};
// Stop server
internals.Server.prototype.stop = function (options, callback) {
this.pack.stop(options, callback);
};
internals.Server.prototype._stop = function (options, callback) {
var self = this;
options = options || {};
callback = callback || function () { };
options.timeout = options.timeout || 5000; // Default timeout to 5 seconds
if (!this._started) {
return Utils.nextTick(callback)();
}
self._started = false;
var timeoutId = setTimeout(function () {
Object.keys(self._connections).forEach(function (key) {
var connection = self._connections[key];
return connection && connection.destroy();
});
}, options.timeout);
self.listener.close(function () {
clearTimeout(timeoutId);
callback();
});
};
internals.Server.prototype._log = function (tags, data, timestamp) {
this.log(['hapi'].concat(tags), data, timestamp);
};
internals.Server.prototype.log = function (tags, data, timestamp) {
tags = (tags instanceof Array ? tags : [tags]);
var now = (timestamp ? (timestamp instanceof Date ? timestamp : new Date(timestamp)) : new Date());
var event = {
timestamp: now.getTime(),
tags: tags,
data: data
};
this.emit('log', event, Utils.mapToObject(event.tags));
};
// Register an extension function
internals.Server.prototype.ext = function () {
return this._ext.add.apply(this._ext, arguments);
};
internals.Server.prototype._ext = function () {
return this._ext._add.apply(this._ext, arguments);
};
// Add server route
internals.Server.prototype.route = internals.Server.prototype.addRoute = internals.Server.prototype.addRoutes = function (configs) {
this._route(configs);
};
internals.Server.prototype._route = function (configs, env) {
this._router.add(configs, env);
};
internals.Server.prototype.state = internals.Server.prototype.addState = function (name, options) {
Utils.assert(name && typeof name === 'string', 'Invalid name');
Utils.assert(!this._stateDefinitions[name], 'State already defined:', name);
Utils.assert(!options || !options.encoding || ['base64json', 'base64', 'form', 'iron', 'none'].indexOf(options.encoding) !== -1, 'Bad encoding');
this._stateDefinitions[name] = Utils.applyToDefaults(Defaults.state, options || {});
};
internals.Server.prototype.auth = function (name, options) {
this._auth.add(name, options);
};
internals.Server.prototype.views = function (options) {
Utils.assert(!this._views, 'Cannot set server views manager more than once');
this._views = new Views(options);
};
internals.Server.prototype.inject = function (options, callback) {
var requestOptions = (options.credentials ? { credentials: options.credentials } : null);
delete options.credentials;
var needle = this._dispatch(requestOptions);
Shot.inject(needle, options, function (res) {
if (res.raw.res.hapi) {
res.result = res.raw.res.hapi.result;
delete res.raw.res.hapi;
}
else {
res.result = res.result || res.payload;
}
return callback(res);
});
};
internals.Server.prototype.helper = internals.Server.prototype.addHelper = function (name, method, options) {
var self = this;
Utils.assert(typeof method === 'function', 'method must be a function');
Utils.assert(typeof name === 'string', 'name must be a string');
Utils.assert(name.match(/^\w+$/), 'Invalid name:', name);
Utils.assert(!this.helpers[name], 'Helper function name already exists');
Utils.assert(!options || typeof options === 'object', 'options must be an object');
Utils.assert(!options || !options.generateKey || typeof options.generateKey === 'function', 'options.key must be a function');
var settings = Utils.clone(options || {});
settings.generateKey = settings.generateKey || internals.generateKey;
// Create helper
var cache = null;
if (settings.cache) {
Utils.assert(!settings.cache.mode, 'Cache mode not allowed in helper configuration (always server side)');
cache = this.pack._provisionCache(settings.cache, 'helper', name, settings.cache.segment);
}
var helper = function (/* arguments, next */) {
// Prepare arguments
var args = arguments;
var lastArgPos = args.length - 1;
var helperNext = args[lastArgPos];
// Wrap method for Cache.Stale interface 'function (next) { next(err, value); }'
var generateFunc = function (next) {
args[lastArgPos] = function (result) {
if (result instanceof Error) {
return next(result);
}
return next(null, result);
};
method.apply(null, args);
};
if (!cache) {
return generateFunc(function (err, result) {
helperNext(err || result);
});
}
var key = settings.generateKey.apply(null, args);
if (key === null) { // Value can be ''
self._log(['helper', 'key', 'error'], { name: name, args: args });
}
cache.getOrGenerate(key, generateFunc, function (err, value, cached, report) {
return helperNext(err || value);
});
};
this.helpers[name] = helper;
};
internals.generateKey = function () {
var key = '';
for (var i = 0, il = arguments.length - 1; i < il; ++i) { // 'arguments.length - 1' to skip 'next'
var arg = arguments[i];
if (typeof arg !== 'string' &&
typeof arg !== 'number' &&
typeof arg !== 'boolean') {
return null;
}
key += (i > 0 ? ':' : '') + encodeURIComponent(arg);
}
return key;
};