confection
Version:
A configuration management server written in node, using redis for a backend.
443 lines (350 loc) • 9.6 kB
JavaScript
var async = require( 'async' );
var moduleConfig = null;
module.exports.init = function ( config, callback ) {
moduleConfig = {
express: null,
storage: null,
outputFilters: {
json: function ( conf, callback ) {
callback( JSON.stringify( conf ), "application/json; charset=utf-8" );
}
}
}.mixin( config );
if ( !config.express ) {
throw "no express supplied to core";
}
if ( !config.storage ) {
throw "no storage supplied to core";
}
configureExpress();
callback( null );
};
function getAuthKey( req ) {
return { key: null }.mixin( req.query ).key || req.header( 'key' );
}
function checkAuth( req, res, next ) {
var key = getAuthKey( req );
moduleConfig.storage.isAuthorized( key, function ( authorized ) {
if ( authorized ) {
next();
}
else {
getResponder( req, res )( 401 );
}
} );
}
function onPostConf( req, res ) {
res = getResponder( req, res );
var path = req.confPath;
if ( !path ) {
res( 400 );
return;
}
moduleConfig.storage.setConf( path, JSON.parse( req.body ), function ( err ) {
if ( err ) {
res( 500 );
}
else {
res( 200, true );
}
} );
}
function onGetConf( req, res ) {
res = getResponder( req, res );
var path = req.confPath;
if ( !path ) {
res( 400 );
return;
}
// strip environment from path
var environment = path.replace( /^\.([^\.]*)\.?.*/, '$1' );
path = path.replace( /^\.[^\.]*\.?/, '' );
if ( path.length < 1 ) {
res( 400 );
}
getConf( path, environment, function ( conf ) {
if ( conf === null ) {
res( 404 );
}
else {
try {
if ( typeof moduleConfig.outputFilters[req.outputFilter] === 'function' ) {
moduleConfig.outputFilters[req.outputFilter]( conf, function ( conf, contentType ) {
res( 200, conf, contentType );
} );
}
else {
res( 500 );
}
}
catch ( e ) {
res( 500 );
}
}
} );
}
/**
* getConf is responsible for the core logic in the API. It handles applying all extensions and wildcard lookups.
*
* External Params:
* @param path the dot notation path to pull from the storage engine
* @param environment the name of the environment to pull the configuration for
* @param callback
*
* Recursive Params:
* @param context
*/
function getConf( path, environment, callback, context ) {
// relative paths need to be auto-prefixed with the environment
if ( path.match( /^[^\.]/ ) ) {
path = "." + environment + "." + path;
}
if ( !context ) {
context = {
pathsSeen: {}
};
}
// avoid circular references
if ( context.pathsSeen[path] ) {
callback( undefined );
return;
}
context.pathsSeen[path] = true;
// calculate catch all path
var catchAllPath = ".*." + path.replace( /^\.[^\.]*\./, '' );
if ( catchAllPath === path ) {
catchAllPath = false;
}
// try exact path
moduleConfig.storage.getConf( path, function ( err, conf ) {
// if there was an error, don't try for wildcard
if ( err ) {
callback( undefined );
}
// not found, try wildcard
else if ( conf === null && catchAllPath ) {
moduleConfig.storage.getConf( catchAllPath, function ( err, conf ) {
// error searching for wild card
if ( err ) {
callback( undefined );
}
// wild card found
else {
applyAbstractions( conf, environment, context, callback );
}
} );
}
// exact path found
else {
applyAbstractions( conf, environment, context, callback );
}
} );
}
function applyAbstractions( conf, environment, context, callback ) {
// only arrays and objects can have abstraction, everything else just returns
if ( !Array.isArray( conf ) && !Object.isObject( conf ) ) {
callback( conf );
return;
}
var pathsToExtend = [];
var currentPath = null;
var currentPathRef = null;
var currentPathRefTemp = null;
var currentSubTree = null;
var currentPathTail = null;
// seed the inspection loop
var pathsToCheck = [
['seed']
];
conf = {
seed: conf
};
function extendPath( extendedPath, currentPathRef, override ) {
pathsToExtend.push( function ( callback ) {
getConf( extendedPath, environment, function ( conf ) {
if ( override && conf ) {
conf = conf.mixin( override );
}
callback( null, {
path: currentPathRef,
conf: conf
} );
}, context );
} );
}
while ( pathsToCheck.length > 0 ) {
// get current path and subtree on that path
currentPath = pathsToCheck.pop();
currentPathRef = currentPath.clone(); // we need an unmodified current path for later
currentSubTree = conf; // we start at the root
currentPathTail = currentPath.pop(); // we don't dereference the last field, because we want to modify it in place
while ( currentPath.length > 0 ) {
currentSubTree = currentSubTree[ currentPath.shift() ];
}
// seed pathsToCheck
if ( Array.isArray( currentSubTree[currentPathTail] ) ) {
for ( var i = 0; i < currentSubTree[currentPathTail].length; i++ ) {
currentPathRefTemp = currentPathRef.clone();
currentPathRefTemp.push( i );
pathsToCheck.push( currentPathRefTemp );
}
}
else if ( Object.isObject( currentSubTree[currentPathTail] ) ) {
if ( currentSubTree[currentPathTail].__extend ) {
extendPath( currentSubTree[currentPathTail].__extend, currentPathRef.clone(), currentSubTree[currentPathTail].__override || null );
}
else {
for ( var field in currentSubTree[currentPathTail] ) {
if ( currentSubTree[currentPathTail].hasOwnProperty( field ) ) {
currentPathRefTemp = currentPathRef.clone();
currentPathRefTemp.push( field );
pathsToCheck.push( currentPathRefTemp );
}
}
}
}
}
if ( pathsToExtend.length > 0 ) {
async.parallel( pathsToExtend, function ( err, results ) {
if ( Array.isArray( results ) ) {
for ( var i = 0; i < results.length; i++ ) {
currentPath = results[i].path;
currentSubTree = conf;
currentPathTail = currentPath.pop(); // we don't dereference the last field, because we want to modify it in place
while ( currentPath.length > 0 ) {
currentSubTree = currentSubTree[ currentPath.shift() ];
}
currentSubTree[currentPathTail] = results[i].conf;
}
}
callback( conf.seed );
} );
}
else {
callback( conf.seed );
}
}
function onDeleteConf( req, res ) {
res = getResponder( req, res );
var path = req.confPath;
if ( !path ) {
res( 400 );
return;
}
moduleConfig.storage.delConf( path, function ( err ) {
if ( err ) {
res( 500 );
}
else {
res( 200, true );
}
} );
}
function onPostAuth( req, res ) {
moduleConfig.storage.generateAuthKey( function ( key ) {
res = getResponder( req, res );
if ( key ) {
res( 200, key );
}
else {
res( 500 );
}
} );
}
function onDeleteAuth( req, res ) {
var key = getAuthKey( req );
moduleConfig.storage.delAuthKey( key, function ( err ) {
res = getResponder( req, res );
if ( err ) {
res( 500 );
}
else {
res( 200, true );
}
} );
}
/**
* Get the conf path from the URL path.
*
* @param req The express request handle
* @param res The response object passed to the middleware
* @param next The next function to trigger the next middleware
* @return {null}
*/
function getPath( req, res, next ) {
var path = req.path.replace( /\..*$/, '' ).replace( /\//g, '.' ).replace( /^\.conf/, '' ).replace( /\.$/, '' ).trim();
if ( path.length < 1 ) {
path = null;
}
var outputFilter = req.path.trim().match( /\.(.*)$/ );
if ( outputFilter ) {
outputFilter = outputFilter[1].trim();
}
if ( typeof outputFilter !== 'string' || outputFilter.length < 1 ) {
outputFilter = 'json';
}
req.confPath = path;
req.outputFilter = outputFilter;
return next();
}
function storeRequestBody( req, res, next ) {
var body = "";
req.setEncoding( 'utf8' );
req.on( 'data', function ( data ) {
body += data;
} );
req.on( 'end', function () {
req.body = body;
next();
} );
}
/**
* Configures the express instance.
*/
function configureExpress() {
// create configuration routes
moduleConfig.express.get( /^\/conf.*/, checkAuth, getPath, getMiddlewareWrapper( onGetConf ) );
moduleConfig.express.post( /^\/conf.*/, storeRequestBody, checkAuth, getPath, getMiddlewareWrapper( onPostConf ) );
moduleConfig.express.delete( /^\/conf.*/, checkAuth, getPath, getMiddlewareWrapper( onDeleteConf ) );
// create auth management routes
moduleConfig.express.post( "/auth", checkAuth, getMiddlewareWrapper( onPostAuth ) );
moduleConfig.express.delete( "/auth", checkAuth, getMiddlewareWrapper( onDeleteAuth ) );
}
/**
* This wrapper simply traps any uncaught exceptions.
*
* @param middleware The middleware function to wrap
* @return {Function} The wrapper function to pass to express
*/
function getMiddlewareWrapper( middleware ) {
return function ( req, res, next ) {
try {
middleware( req, res, next );
}
catch ( e ) {
getResponder( req, res )( 500 );
}
};
}
/**
* Gets a response generating function. Used in middleware to simplify response logic.
*
* @param res A handle to a response object to send the response to when the responder function is called
* @return {Function} The responder function for middleware to call to send a response to a request
*/
function getResponder( req, res ) {
return function ( code, body, contentType ) {
if ( code !== 200 ) {
body = "";
contentType = "text/html; charset=utf-8";
}
if ( !contentType ) {
contentType = "text/html; charset=utf-8";
}
res.writeHead( code, {
"Content-type": contentType
} );
res.end( body );
};
}
;