// Generated by CoffeeScript 1.6.3 /* DB Interface ============ > Handles the connection to the database and provides functionalities for > event/action modules, rules and the encrypted storing of authentication tokens. > General functionality as a wrapper for the module holds initialization, > encryption/decryption, the retrieval of modules and shut down. > > The general structure for linked data is that the key is stored in a set. > By fetching all set entries we can then fetch all elements, which is > automated in this function. > For example modules of the same group, e.g. action modules are registered in an > unordered set in the database, from where they can be retrieved again. For example > a new action module has its ID (e.g 'probinder') first registered in the set > 'action_modules' and then stored in the db with the key 'action\_module\_' + ID > (e.g. action\_module\_probinder). > */ (function() { var crypto, decrypt, encrypt, exports, getSetRecords, hash, log, redis, replyHandler, _this = this; log = require('./logging'); crypto = require('crypto-js'); redis = require('redis'); /* Module call ----------- Initializes the DB connection. Requires a valid configuration file which contains a db port and a crypto key. @param {Object} args */ exports = module.exports = function(args) { var config, _ref; args = args != null ? args : {}; log(args); config = require('./config'); config(args); if ((_ref = _this.db) != null) { _ref.quit(); } if (config.isReady()) { _this.crypto_key = config.getCryptoKey(); _this.db = redis.createClient(config.getDBPort(), 'localhost', { connect_timeout: 2000 }); return _this.db.on('error', function(err) { err.addInfo = 'message from DB'; return log.error('DB', err); }); } else { return log.error('DB', 'Initialization failed because of missing config file!'); } }; /* Checks whether the db is connected and passes either an error on failure after ten attempts within five seconds, or nothing on success to the callback(err). @public isConnected( *cb* ) @param {function} cb */ exports.isConnected = function(cb) { var fCheckConnection, numAttempts; if (_this.db.connected) { return cb(); } else { numAttempts = 0; fCheckConnection = function() { if (_this.db.connected) { log.print('DB', 'Successfully connected to DB!'); return cb(); } else if (numAttempts++ < 10) { return setTimeout(fCheckConnection, 100); } else { return cb(new Error('Connection to DB failed!')); } }; return setTimeout(fCheckConnection, 100); } }; /* Abstracts logging for simple action replies from the DB. @private replyHandler( *action* ) @param {String} action */ replyHandler = function(action) { return function(err, reply) { if (err) { err.addInfo = "during \"" + action + "\""; return log.error('DB', err); } else { return log.print('DB', "" + action + ": " + reply); } }; }; /* Push an event into the event queue. @public pushEvent( *oEvent* ) @param {Object} oEvent */ exports.pushEvent = function(oEvent) { if (oEvent) { log.print('DB', "Event pushed into the queue: " + oEvent.eventid); return _this.db.rpush('event_queue', JSON.stringify(oEvent)); } else { return log.error('DB', 'Why would you give me an empty event...'); } }; /* Pop an event from the event queue and pass it to the callback(err, obj) function. @public popEvent( *cb* ) @param {function} cb */ exports.popEvent = function(cb) { var makeObj; makeObj = function(pcb) { return function(err, obj) { return pcb(err, JSON.parse(obj)); }; }; return _this.db.lpop('event_queue', makeObj(cb)); }; /* Purge the event queue. @public purgeEventQueue() */ exports.purgeEventQueue = function() { return _this.db.del('event_queue', replyHandler('purging event queue')); }; /* Hashes a string based on SHA-3-512. @private hash( *plainText* ) @param {String} plainText */ hash = function(plainText) { var err; if (plainText == null) { return null; } try { return (crypto.SHA3(plainText, { outputLength: 512 })).toString(); } catch (_error) { err = _error; err.addInfo = 'during hashing'; log.error('DB', err); return null; } }; /* Encrypts a string using the crypto key from the config file, based on aes-256-cbc. @private encrypt( *plainText* ) @param {String} plainText */ encrypt = function(plainText) { var enciph, err, et; if (plainText == null) { return null; } try { enciph = crypto.createCipher('aes-256-cbc', _this.crypto_key); et = enciph.update(plainText, 'utf8', 'base64'); return et + enciph.final('base64'); } catch (_error) { err = _error; err.addInfo = 'during encryption'; log.error('DB', err); return null; } }; /* Decrypts an encrypted string and hands it back on success or null. @private decrypt( *crypticText* ) @param {String} crypticText */ decrypt = function(crypticText) { var deciph, dt, err; if (crypticText == null) { return null; } try { deciph = crypto.createDecipher('aes-256-cbc', _this.crypto_key); dt = deciph.update(crypticText, 'base64', 'utf8'); return dt + deciph.final('utf8'); } catch (_error) { err = _error; err.addInfo = 'during decryption'; log.error('DB', err); return null; } }; /* Fetches all linked data set keys from a linking set, fetches the single data objects via the provided function and returns the results to the callback(err, obj) function. @private getSetRecords( *set, fSingle, cb* ) @param {String} set the set name how it is stored in the DB @param {function} fSingle a function to retrieve a single data element per set entry @param {function} cb the callback(err, obj) function that receives all the retrieved data or an error */ getSetRecords = function(set, fSingle, cb) { log.print('DB', "Fetching set records: " + set); return _this.db.smembers(set, function(err, arrReply) { var fCallback, objReplies, reply, semaphore, _i, _len, _results; if (err) { err.addInfo = "fetching " + set; return log.error('DB', err); } else if (arrReply.length === 0) { return cb(); } else { semaphore = arrReply.length; objReplies = {}; setTimeout(function() { if (semaphore > 0) { return cb(new Error("Timeout fetching " + set)); } }, 2000); fCallback = function(prop) { return function(err, data) { --semaphore; if (err) { err.addInfo = "fetching single element: " + prop; log.error('DB', err); } else if (!data) { log.error('DB', new Error("Empty key in DB: " + prop)); } else { objReplies[prop] = data; } if (semaphore === 0) { return cb(null, objReplies); } }; }; _results = []; for (_i = 0, _len = arrReply.length; _i < _len; _i++) { reply = arrReply[_i]; _results.push(fSingle(reply, fCallback(reply))); } return _results; } }); }; /* ## Action Modules #TODO Rename Action Modules into something like Action Caller */ /* Store a string representation of an action module in the DB. @public storeActionModule ( *amId, data* ) @param {String} amId @param {String} data */ exports.storeActionModule = function(amId, data) { log.print('DB', "storeActionModule: " + amId); _this.db.sadd('action-modules', amId, replyHandler("storing action module key " + amId)); return _this.db.set("action-module:" + amId, data, replyHandler("storing action module " + amId)); }; /* Query the DB for an action module and pass it to the callback(err, obj) function. @public getActionModule( *amId, cb* ) @param {String} amId @param {function} cb */ exports.getActionModule = function(amId, cb) { log.print('DB', "getActionModule: " + amId); return _this.db.get("action-module:" + amId, cb); }; /* Fetch all action modules and hand them to the callback(err, obj) function. @public getActionModules( *cb* ) @param {function} cb */ exports.getActionModules = function(cb) { return getSetRecords('action-modules', exports.getActionModule, cb); }; /* Store user-specific action module parameters . @public storeActionParams( *userId, amId, data* ) @param {String} userId @param {String} amId @param {String} data */ exports.storeActionParams = function(userId, amId, data) { log.print('DB', "storeActionParams: " + amId + ":" + userId); return _this.db.set("action-params:" + amId + ":" + userId, hash(data), replyHandler("storing action params " + amId + ":" + userId)); }; /* Query the DB for user-specific action module parameters, and pass it to the callback(err, obj) function. @public getActionParams( *userId, amId, cb* ) @param {String} userId @param {String} amId @param {function} cb */ exports.getActionParams = function(userId, amId, cb) { log.print('DB', "getActionParams: " + amId + ":" + userId); return _this.db.get("action-params:" + amId + ":" + userId, function(err, data) { return cb(err, decrypt(data)); }); }; /* ## Event Modules #TODO rename event modules to event puller or something like that */ /* Store a string representation of an event module in the DB. @public storeEventModule( *emId, data* ) @param {String} emId @param {String} data */ exports.storeEventModule = function(emId, data) { log.print('DB', "storeEventModule: " + emId); _this.db.sadd('event-modules', emId, replyHandler("storing event module key " + emId)); return _this.db.set('event-module:#{ emId }', data, replyHandler("storing event module " + emId)); }; /* Query the DB for an event module and pass it to the callback(err, obj) function. @public getEventModule( *emId, cb* ) @param {String} emId @param {function} cb */ exports.getEventModule = function(emId, cb) { log.print('DB', "getEventModule: " + emId); return _this.db.get("event-module:" + emId, cb); }; /* Fetch all event modules and pass them to the callback(err, obj) function. @public getEventModules( *cb* ) @param {function} cb */ exports.getEventModules = function(cb) { return getSetRecords('event-modules', exports.getEventModule, cb); }; /* Store a string representation of user-specific parameters for an event module. @public storeEventParams( *userId, emId, data* ) @param {String} userId @param {String} emId @param {Object} data */ exports.storeEventParams = function(userId, emId, data) { log.print('DB', "storeEventParams: " + emId + ":" + userId); return _this.db.set("event-params:" + emId + ":" + userId, encrypt(data), replyHandler("storing event auth " + emId + ":" + userId)); }; /* Query the DB for an action module authentication token, associated with a user. @public getEventAuth( *userId, emId, data* ) @param {String} userId @param {String} emId @param {function} cb */ exports.getEventAuth = function(userId, emId, cb) { log.print('DB', "getEventAuth: " + emId + ":" + userId); return _this.db.get("event-auth:" + emId + ":" + userId, function(err, data) { return cb(err, decrypt(data)); }); }; /* ## Rules */ /* Store a string representation of a rule in the DB. @public storeRule( *ruleId, userId, data* ) @param {String} ruleId @param {String} userId @param {String} data */ exports.storeRule = function(ruleId, userId, data) { log.print('DB', "storeRule: " + ruleId); _this.db.sadd('rules', "" + ruleId + ":" + userId, replyHandler("storing rule key \"" + ruleId + ":" + userId + "\"")); _this.db.sadd("user-set:" + userId + ":rules", ruleId, replyHandler("storing rule key to \"user:" + userId + ":rules\"")); _this.db.sadd("rule-set:" + ruleId + ":users", user, replyHandler("storing user key to \"rule:" + ruleId + ":users\"")); return _this.db.set("rule:" + ruleId + ":" + userId, data, replyHandler("storing rule \"" + ruleId + ":" + userId + "\"")); }; /* Query the DB for a rule and pass it to the callback(err, obj) function. @public getRule( *ruleId, cb* ) @param {String} ruleId @param {function} cb */ exports.getRule = function(ruleId, cb) { log.print('DB', "getRule: " + ruleId); return _this.db.get("rule:" + ruleId, cb); }; /* Fetch all rules from the database and pass them to the callback function. @public getRules( *cb* ) @param {function} cb */ exports.getRules = function(cb) { log.print('DB', 'Fetching all Rules'); return getSetRecords('rules', exports.getRule, cb); }; /* Store a user object (needs to be a flat structure). @public storeUser( *objUser* ) @param {Object} objUser */ exports.storeUser = function(objUser) { log.print('DB', "storeUser: " + objUser.username); if (objUser && objUser.username && objUser.password) { _this.db.sadd('users', objUser.username, replyHandler("storing user key " + objUser.username)); objUser.password = hash(objUser.password); return _this.db.hmset("user:" + objUser.username, objUser, replyHandler("storing user properties " + objUser.username)); } else { return log.error('DB', new Error('username or password was missing')); } }; /* Associate a role with a user. @public storeUserRole( *userId, role* ) @param {String} userId @param {String} role */ exports.storeUserRole = function(userId, role) { log.print('DB', "storeUserRole: " + userId + ":" + role); _this.db.sadd('roles', role, replyHandler("adding role " + role + " to role index set")); _this.db.sadd("user:" + userId + ":roles", role, replyHandler("adding role " + role + " to user " + userId)); return _this.db.sadd("role:" + role + ":users", userId, replyHandler("adding user " + userId + " to role " + role)); }; /* Fetch all roles of a user and pass them to the callback(err, obj) @public getUserRoles( *userId* ) @param {String} userId */ exports.getUserRoles = function(userId) { log.print('DB', "getUserRole: " + userId); return _this.db.get("user-roles:" + userId, cb); }; /* Fetch all users of a role and pass them to the callback(err, obj) @public getUserRoles( *role* ) @param {String} role */ exports.getRoleUsers = function(role) { log.print('DB', "getRoleUsers: " + role); return _this.db.get("role-users:" + role, cb); }; /* Checks the credentials and on success returns the user object to the callback(err, obj) function. The password has to be hashed (SHA-3-512) beforehand by the instance closest to the user that enters the password, because we only store hashes of passwords for safety reasons. @public loginUser( *userId, password, cb* ) @param {String} userId @param {String} password @param {function} cb */ exports.loginUser = function(userId, password, cb) { var fCheck; log.print('DB', "User \"" + userId + "\" tries to log in"); fCheck = function(pw) { return function(err, obj) { if (err) { return cb(err); } else if (obj && obj.password) { if (pw === obj.password) { log.print('DB', "User \"" + obj.username + "\" logged in!"); return cb(null, obj); } else { return cb(new Error('Wrong credentials!')); } } else { return cb(new Error('User not found!')); } }; }; return _this.db.hgetall("user:" + userId, fCheck(password)); }; /* Shuts down the db link. @public shutDown() */ exports.shutDown = function() { return _this.db.quit(); }; }).call(this);