Created
September 6, 2012 16:55
-
-
Save nvcexploder/3658480 to your computer and use it in GitHub Desktop.
hapi 0.6.0 server file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2012 Walmart. All rights reserved. Copyrights licensed under the New BSD License. | |
* See LICENSE file included with this code project for license terms. | |
*/ | |
// Load modules | |
var Fs = require('fs'); | |
var Http = require('http'); | |
var Https = require('https'); | |
var Url = require('url'); | |
var Querystring = require('querystring'); | |
var NodeUtil = require('util'); | |
var Events = require('events'); | |
var Director = require('director'); | |
var MAC = require('mac'); | |
var Async = require('async'); | |
var Utils = require('./utils'); | |
var Err = require('./error'); | |
var Log = require('./log'); | |
var Process = require('./process'); | |
var Validation = require('./validation'); | |
var Defaults = require('./defaults'); | |
var Monitor = require('./monitor'); | |
var Session = require('./session'); | |
var Cache = require('./cache'); | |
// Declare internals | |
var internals = { | |
// Servers instances by uri or name | |
servers: {} | |
}; | |
// Create and configure server instance | |
exports.Server = Server = function (host, port, options, routes) { | |
var that = this; | |
// Confirm that Server is called as constructor | |
if (this.constructor != Server) { | |
Utils.abort('Server must be instantiated using new'); | |
} | |
// Register as event emitter | |
Events.EventEmitter.call(this); | |
// Set basic configuration | |
this.settings = Utils.merge(Utils.clone(Defaults.server), options || {}); | |
this.settings.host = host.toLowerCase(); | |
this.settings.port = port; | |
this.settings.name = (this.settings.name ? this.settings.name.toLowerCase() : (this.settings.host + ':' + this.settings.port)); | |
this.settings.uri = (this.settings.tls ? 'https://' : 'http://') + this.settings.host + ':' + this.settings.port + '/'; | |
// Initialize authentication configuration and validate | |
if (this.settings.authentication) { | |
this.settings.authentication = Utils.merge(Utils.clone(Defaults.authentication), this.settings.authentication); | |
if (this.settings.authentication.tokenEndpoint === null || | |
this.settings.authentication.loadClientFunc === null || | |
this.settings.authentication.loadUserFunc === null || | |
this.settings.authentication.checkAuthorizationFunc === null || | |
this.settings.authentication.aes256Keys.oauthRefresh === null || | |
this.settings.authentication.aes256Keys.oauthToken === null) { | |
Utils.abort('Invalid authentication configuration'); | |
} | |
} | |
// Verify no existing instances using the same uri or name | |
if (internals.servers[this.settings.name]) { | |
Utils.abort('Cannot configure multiple server instances using the same name or uri'); | |
} | |
// Add to instance list | |
internals.servers[this.settings.name] = this; | |
// Initialize cache engine | |
if (this.settings.cache) { | |
if (this.settings.cache.implementation) { | |
this.cache = this.settings.cache.implementation; | |
this.settings.cache.implementation = null; | |
} | |
else { | |
this.settings.cache = Utils.merge(Utils.clone(Defaults.cache), this.settings.cache); | |
this.cache = new Cache.Client(this.settings.cache.options); | |
this.cache.on('ready', function (err) { | |
if (err) { | |
Utils.abort('Failed to initialize cache engine: ' + err); | |
} | |
}); | |
} | |
} | |
else { | |
this.cache = null; | |
} | |
// Create router | |
this.router = new Director.http.Router(); | |
this.router.configure({ | |
async: true, | |
notfound: this.unhandledRoute() | |
}); | |
var listenerEntryFunc = function (req, res) { | |
var dispatch = function () { | |
that.router.dispatch(req, res, function (err) { | |
if (err) { | |
// Should never get called since 'notfound' is set | |
Log.err('Internal routing error'); | |
res.writeHead(500); | |
res.end(); | |
} | |
}); | |
}; | |
if (that.settings.ext.onRequest) { | |
// onRequest can change internal req values (e.g. url, method) | |
that.settings.ext.onRequest(req, res, function () { | |
dispatch(); | |
}); | |
} | |
else { | |
dispatch(); | |
} | |
}; | |
// Create server | |
if (this.settings.tls) { | |
var tls = { | |
key: Fs.readFileSync(this.settings.tls.key), | |
cert: Fs.readFileSync(this.settings.tls.cert) | |
}; | |
//if certs need a password ... | |
if( this.settings.tls.passphrase ) { | |
tls.passphrase = this.settings.tls.passphrase; | |
} | |
this.listener = Https.createServer(tls, listenerEntryFunc); | |
} | |
else { | |
this.listener = Http.createServer(listenerEntryFunc); | |
} | |
// Initialize Monitoring if set | |
this.monitor = new Monitor(this, this.settings, Log); | |
// Setup OAuth token endpoint | |
if (this.settings.authentication) { | |
this.addRoute({ | |
method: 'POST', | |
path: this.settings.authentication.tokenEndpoint, | |
handler: Session.token, | |
schema: Session.type.endpoint, | |
mode: 'raw', | |
authentication: 'optional', | |
user: 'any', | |
tos: 'none' | |
}); | |
} | |
// Add routes | |
if (routes) { | |
this.addRoutes(routes); | |
} | |
return this; | |
}; | |
NodeUtil.inherits(Server, Events.EventEmitter); | |
// Route preprocessor handler | |
Server.prototype.preRoute = function (config) { | |
var that = this; | |
return function (req, res, next, params) { | |
req._startTime = new Date; // Used to determine request response time | |
Log.info('Received', req); | |
req.hapi = {}; | |
res.hapi = {}; | |
req.hapi.server = that; | |
req.hapi.url = req.url; | |
req.hapi.query = req.url.indexOf('?') >= 0 ? Url.parse(req.url, true).query : {}; | |
// Convert director arguements to parameters object | |
req.hapi.params = {}; | |
if (params.length === config.parameterNames.length) { | |
for (var i = 0, il = config.parameterNames.length; i < il; ++i) { | |
req.hapi.params[config.parameterNames[i]] = params[i]; | |
} | |
} | |
next(); | |
}; | |
}; | |
// Route validator | |
Server.prototype.routeValidator = function (config) { | |
var that = this; | |
return function (req, res, next) { | |
// Authentication | |
internals.authenticate(req, res, config, that, function (err) { | |
if (err === null) { | |
// Query parameters | |
Validation.validateQuery(req, Utils.map(config.query), function (err) { | |
if (err === null) { | |
// Load payload | |
internals.processBody(req, config.payload || (config.schema ? 'parse' : null), that, function (err) { | |
if (err === null) { | |
// Validate payload schema | |
Validation.validateData(req, config.schema || null, function (err) { | |
if (err) { | |
res.hapi.error = err; | |
} | |
next(); | |
}); | |
} | |
else { | |
res.hapi.error = err; | |
next(); | |
} | |
}); | |
} | |
else { | |
res.hapi.error = err; | |
next(); | |
} | |
}); | |
} | |
else { | |
res.hapi.error = err; | |
next(); | |
} | |
}); | |
}; | |
}; | |
// Request handler wrapper | |
Server.prototype.routeHandler = function (config) { | |
// Create cache if configured | |
var cache = null; | |
if (config.cache) { | |
cache = new Cache.Set(config.cache, this.cache); | |
} | |
return function (req, res, next) { | |
var call = function () { | |
var request = (config.mode === 'raw' ? req : req.hapi); | |
config.handler(request, function (result, options) { | |
res.hapi[result instanceof Error ? 'error' : 'result'] = result; | |
res.hapi.options = options || {}; | |
if (cache) { | |
cache.set(req.url, { result: res.hapi.result, error: res.hapi.error, options: res.hapi.options }, function (err) { | |
if (err) { | |
Log.err('Failed saving result to cache'); | |
} | |
}); | |
} | |
next(); | |
}); | |
}; | |
if (!res.hapi.error) { | |
if (cache) { | |
cache.get(req.url, function (err, item) { | |
if (err === null) { | |
if (result) { | |
res.hapi.result = item.result || null; | |
res.hapi.error = item.error || null; | |
res.hapi.options = item.options || {}; | |
next(); | |
} | |
else { | |
call(); | |
} | |
} | |
else { | |
call(); | |
} | |
}); | |
} | |
else { | |
call(); | |
} | |
} | |
else { | |
next(); | |
} | |
}; | |
}; | |
// Set default response headers and send response | |
Server.prototype.postRoute = function () { | |
var that = this; | |
return function (req, res, next) { | |
that.setCorsHeaders(res); | |
res.setHeader('Cache-Control', 'must-revalidate'); | |
if (res.hapi.result) { | |
if (res.hapi.options.headers) { | |
for (var header in res.hapi.options.headers) { | |
if (res.hapi.options.headers.hasOwnProperty(header)) { | |
res.setHeader(header, res.hapi.options.headers[header]); | |
} | |
} | |
} | |
var rev = null; // Need to set to something useful | |
if (req.method === 'GET' && rev) { | |
res.setHeader('ETag', rev); | |
var condition = internals.parseCondition(req.headers['if-none-match']); | |
if (condition[rev] || | |
condition['*']) { | |
internals.respond(res, 304); | |
} | |
else { | |
internals.respond(res, 200, res.hapi.result); | |
} | |
} | |
else if (res.hapi.options.created) { | |
internals.respond(res, 201, res.hapi.result, { 'Location': that.settings.uri + res.hapi.options.created }); | |
} | |
else { | |
internals.respond(res, 200, res.hapi.result); | |
} | |
Log.info('Replied', req); | |
} | |
else if (res.hapi.error) { | |
if (res.hapi.error.type === 'oauth') { | |
internals.respond(res, res.hapi.error.code, { error: res.hapi.error.error, error_description: res.hapi.error.text }); | |
} | |
else { | |
internals.respond(res, res.hapi.error.code, { error: res.hapi.error.text, message: res.hapi.error.message, code: res.hapi.error.code }); | |
} | |
Log.err(res.hapi.error, req); | |
} | |
else { | |
internals.respond(res, 200); | |
Log.info('Replied', req); | |
} | |
that.emit('response', req, res); | |
// Return control to router | |
next(); | |
}; | |
}; | |
// 404 Route handler | |
Server.prototype.unhandledRoute = function () { | |
var that = this; | |
return function (next) { | |
var req = this.req; | |
var res = this.res; | |
req._startTime = new Date; // Used to determine request response time | |
if (that.settings.ext.onUnknownRoute) { | |
that.settings.ext.onUnknownRoute(req, res, function () { | |
that.emit('response', req, res); | |
next(); | |
}); | |
} | |
else { | |
Log.info('Received', req); | |
var error = Err.notFound('No such path or method'); | |
internals.respond(res, error.code, { error: error.text, message: error.message, code: error.code }); | |
Log.info(error, req); | |
that.emit('response', req, res); | |
next(); | |
} | |
}; | |
}; | |
// Set CORS headers | |
Server.prototype.setCorsHeaders = function (res) { | |
res.setHeader('Access-Control-Allow-Origin', '*'); | |
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, POST, PUT, DELETE, OPTIONS'); | |
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, If-None-Match, X-Requested-With'); | |
res.setHeader('Access-Control-Max-Age', this.settings.cors.maxAge); | |
}; | |
// Start server listener | |
Server.prototype.start = function () { | |
this.listener.listen(this.settings.port, this.settings.host); | |
Log.info(Process.settings.name + ' Server instance started at ' + this.settings.uri); | |
}; | |
// Stop server | |
Server.prototype.stop = function () { | |
this.listener.close(); | |
Log.info(Process.settings.name + ' Server instance stopped at ' + this.settings.uri); | |
}; | |
// Set route defauts | |
Server.prototype.setRoutesDefaults = function (config) { | |
this.routeDefaults = config; | |
}; | |
// Add server route | |
Server.prototype.addRoute = function (config) { | |
var that = this; | |
var routeConfig = (this.routeDefaults ? Utils.merge(Utils.clone(this.routeDefaults), config) : config); | |
// Validate configuration | |
if (!routeConfig.path) { | |
Utils.abort('Route missing path'); | |
} | |
if (!routeConfig.method) { | |
Utils.abort('Route missing method'); | |
} | |
if (!routeConfig.handler) { | |
Utils.abort('Route missing handler'); | |
} | |
if (routeConfig.authentication !== 'none' && | |
this.settings.authentication === null) { | |
Utils.abort('Route requires authentication but none configured'); | |
} | |
if (routeConfig.cache) { | |
if (this.cache === null) { | |
Utils.abort('No cache configured for server'); | |
} | |
if (routeConfig.method !== 'GET' && | |
routeConfig.method !== 'HEAD') { | |
Utils.abort('Only GET or HEAD routes can use the cache'); | |
} | |
} | |
// Parse path to identify :parameter names, only if no other regex or wildcards are included | |
routeConfig.parameterNames = []; | |
if (/\*|\(|\)/.test(routeConfig.path) === false) { | |
var names = routeConfig.path.match(/:([^\/]+)/ig); | |
if (names) { | |
for (var i = 0, il = names.length; i < il; ++i) { | |
routeConfig.parameterNames.push(names[i].slice(1)); | |
} | |
} | |
} | |
// Handler wrapper | |
var wrapper = function (func) { | |
return function () { | |
// var next = arguments[arguments.length - 1]; // Does not modify 'arguments' | |
// var params = arguments.slice(0, arguments.length - 1); | |
var args = Array.prototype.slice.call(arguments); // Convert arguments to instanceof Array | |
var next = args[args.length - 1]; | |
var params = args.slice(0, args.length - 1); | |
func(this.req, this.res, next, params); | |
}; | |
}; | |
// Build route chain | |
var chain = [wrapper(this.preRoute(routeConfig))]; | |
chain.push(wrapper(this.routeValidator(routeConfig))); | |
if (this.settings.ext.onPreHandler) { | |
chain.push(wrapper(this.settings.ext.onPreHandler)); | |
} | |
chain.push(wrapper(this.routeHandler(routeConfig))); | |
if (this.settings.ext.onPostHandler) { | |
chain.push(wrapper(this.settings.ext.onPostHandler)); | |
} | |
chain.push(wrapper(this.postRoute())); | |
if (this.settings.ext.onPostRoute) { | |
chain.push(wrapper(this.settings.ext.onPostRoute)); | |
} | |
// Add route to Director | |
this.router[routeConfig.method.toLowerCase()](routeConfig.path, { stream: true }, chain); | |
// Setup CORS 'OPTIONS' handler | |
if (routeConfig.cors !== false) { | |
this.router.options(routeConfig.path, function () { | |
that.setCorsHeaders(this.res); | |
internals.respond(this.res, 200); | |
}); | |
} | |
}; | |
Server.prototype.addRoutes = function (routes) { | |
for (var i = 0, il = routes.length; i < il; ++i) { | |
this.addRoute(routes[i]); | |
} | |
}; | |
// Return server object | |
exports.instance = function (name) { | |
if (name) { | |
name = name.toLowerCase(); | |
var server = internals.servers[name]; | |
if (server) { | |
return server; | |
} | |
else { | |
return null; | |
} | |
} | |
else { | |
var names = Object.keys(internals.servers); | |
if (names.length === 1) { | |
return internals.servers[names[0]]; | |
} | |
else if (names.length === 0) { | |
return null; | |
} | |
else { | |
Utils.abort('Cannot call Server.instance() without uri in a process with multiple server instances'); | |
} | |
} | |
}; | |
// Return server object configuration | |
exports.settings = function (name) { | |
var server = exports.instance(name); | |
if (server) { | |
return server.settings; | |
} | |
else { | |
return null; | |
} | |
}; | |
// Add routes to multiple instances | |
exports.addRoutes = function (arg0, arg1) { // [defaultInstances,] routes | |
// Handle optional arguments | |
// var defaultInstances = (arguments.length === 2 ? (arguments[0] instanceof Array ? arguments[0] : [arguments[0]]) : null); | |
// var routes = (arguments.length === 2 ? arguments[1] : arguments[0]); | |
var args = Array.prototype.slice.call(arguments); // Convert arguments to instanceof Array | |
var defaultInstances = (args.length === 2 ? (args[0] instanceof Array ? args[0] : [args[0]]) : null); | |
var routes = (args.length === 2 ? args[1] : args[0]); | |
// Process each route | |
routes = (routes instanceof Array ? routes : [routes]); | |
for (var i = 0, il = routes.length; i < il; ++i) { | |
var route = routes[i]; | |
if (route.instance || defaultInstances) { | |
// Select instances | |
var instances = (route.instance ? (route.instance instanceof Array ? route.instance : [route.instance]) : defaultInstances); | |
for (var r = 0, rl = instances.length; r < rl; ++r) { | |
var server = internals.servers[instances[r].toLowerCase()]; | |
if (server) { | |
server.addRoute(route); | |
} | |
else { | |
Utils.abort('Cannot find server instance: ' + instances[r]); | |
} | |
} | |
} | |
else { | |
// All instances | |
for (var s in internals.servers) { | |
if (internals.servers.hasOwnProperty(s)) { | |
internals.servers[s].addRoute(route); | |
} | |
} | |
} | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment