diff --git a/README.md b/README.md index 40321e8..cdddfb3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,13 @@ README: webapi-eca Getting started --------------- +Prerequisites: + + - node.js & npm (find it [here](http://nodejs.org/)) + - *(optional) coffee, if you want to compile from coffee sources:* + + sudo npm -g install coffee-script + Clone project: git clone https://github.com/dominicbosch/webapi-eca.git diff --git a/coffee/server.coffee b/coffee/server.coffee new file mode 100644 index 0000000..9f1f66a --- /dev/null +++ b/coffee/server.coffee @@ -0,0 +1,24 @@ + +### +Rules Server +============ +This is the main module that is used to run the whole server: + + node server [log_type http_port] + +Valid `log_type`'s are: + +- `0`: standard I/O output (default) +- `1`: log file (server.log) +- `2`: silent + +`http_port` can be set to use another port, than defined in the +[config](config.html) file, to listen to, e.g. used by the test suite. + +### +root = exports ? this +root.foo = -> 'Hello World' +### +My comments will show up here + +### \ No newline at end of file diff --git a/coffee/test.coffee b/coffee/test.coffee new file mode 100644 index 0000000..dfd8c77 --- /dev/null +++ b/coffee/test.coffee @@ -0,0 +1 @@ +efew = require 'fs' diff --git a/compile_coffee.sh b/compile_coffee.sh new file mode 100755 index 0000000..5fd055b --- /dev/null +++ b/compile_coffee.sh @@ -0,0 +1,3 @@ +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +echo "Started listening on file changes to compile them!..." +coffee -wc -o $DIR/js-coffee $DIR/coffee/ diff --git a/create_doc.js b/create_doc.js index f009fba..21815a0 100644 --- a/create_doc.js +++ b/create_doc.js @@ -7,7 +7,7 @@ require('groc').CLI( "README.md", "TODO.js", "LICENSE.js", - "js/*", + "js-coffee/*", "mod_actions/**/*.js", "mod_events/**/*.js", "-o./webpages/doc" diff --git a/js-coffee/config.js b/js-coffee/config.js new file mode 100644 index 0000000..7e1f138 --- /dev/null +++ b/js-coffee/config.js @@ -0,0 +1,80 @@ +'use strict'; + +var path = require('path'), + log = require('./logging'), + config; + +exports = module.exports = function(args) { + args = args || {}; + log(args); + if(typeof args.relPath === 'string') loadConfigFile(args.relPath); + //TODO check all modules whether they can be loaded without calling the module.exports with args + return module.exports; +}; + +loadConfigFile(path.join('config', 'config.json')); + +function loadConfigFile(relPath) { + try { + config = JSON.parse(require('fs').readFileSync(path.resolve(__dirname, '..', relPath))); + if(config && config.http_port && config.db_port + && config.crypto_key && config.session_secret) { + log.print('CF', 'config file loaded successfully!'); + } else { + log.error('CF', new Error('Missing property in config file, requires:\n' + + ' - http_port\n' + + ' - db_port\n' + + ' - crypto_key\n' + + ' - session_secret')); + } + } catch (e) { + e.addInfo = 'no config ready'; + log.error('CF', e); + } +} + +/** + * Answer true if the config file is ready, else false + */ +exports.isReady = function() { + if(config) return true; + else return false; +}; + +/** + * Fetch a property from the configuration + * @param {String} prop + */ +function fetchProp(prop) { + if(config) return config[prop]; +} + +/** + * Get the HTTP port + */ +exports.getHttpPort = function() { + return fetchProp('http_port'); +}; + +/** + * Get the DB port + */ +exports.getDBPort = function() { + return fetchProp('db_port'); +}; + +/** + * Get the crypto key + */ +exports.getCryptoKey = function() { + return fetchProp('crypto_key'); +}; + +/** + * Get the session secret + */ +exports.getSessionSecret = function() { + return fetchProp('session_secret'); +}; + + \ No newline at end of file diff --git a/js-coffee/db_interface.js b/js-coffee/db_interface.js new file mode 100644 index 0000000..b31cfaf --- /dev/null +++ b/js-coffee/db_interface.js @@ -0,0 +1,322 @@ +/** + * # DB Interface + * Handles the connection to the database and provides functionalities for + * event/action modules, rules and the encrypted storing of authentication tokens. + * + * ## General + * General functionality as a wrapper for the module holds initialization, + * encryption/decryption, the retrieval of modules and shut down. + * 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). + */ +'use strict'; + +var redis = require('redis'), + crypto = require('crypto'), + log = require('./logging'), + crypto_key, db; + + +/** + * Initializes the DB connection. Requires a valid configuration file which contains + * a db port and a crypto key. + * + */ +exports = module.exports = function(args) { + args = args || {}; + log(args); + + var config = require('./config')(args); + crypto_key = config.getCryptoKey(); + db = redis.createClient(config.getDBPort(), 'localhost', { connect_timeout: 2000 }); + db.on("error", function (err) { + err.addInfo = 'message from DB'; + log.error('DB', err); + }); + return module.exports; +}; + +exports.isConnected = function(cb) { + if(db.connected) cb(null); + else setTimeout(function() { + if(db.connected) { + log.print('DB', 'Successfully connected to DB!'); + cb(null); + } else { + var e = new Error('Connection to DB failed!'); + log.error('DB', e); + cb(e); + } + }, 3000); +}; + +/** + * ### encrypt + * this is used to decrypt + * @param {String} plainText + */ +function encrypt(plainText) { + if(!plainText) return null; + try { + var enciph = crypto.createCipher('aes-256-cbc', crypto_key); + var et = enciph.update(plainText, 'utf8', 'base64') + enciph.final('base64'); + log.print('DB', 'Encrypted credentials into: ' + et); + return et; + } catch (err) { + log.error('DB', 'in encrypting: ' + err); + return null; + } +} + +/** + * ### decrypt + */ +function decrypt(crypticText, id) { + if(!crypticText) return null; + try { + var deciph = crypto.createDecipher('aes-256-cbc', crypto_key); + return deciph.update(crypticText, 'base64', 'utf8') + deciph.final('utf8'); + } catch (err) { + log.error('DB', 'in decrypting "' + id + '": ' + err); + return null; + } +} + +/** + * ### replyHandler + * Abstraction answer handling for simple information replies from the DB. + * @param {String} action the action to be displayed in the output string. + */ +function replyHandler(action) { + return function(err, reply) { + if(err) log.error('DB', ' during "' + action + '": ' + err); + else log.print('DB', action + ': ' + reply); + }; +} + +/** + * ### getSetRecords + * The general structure for modules is that the key is stored in a set. + * By fetching all set entries we can then fetch all modules, which is + * automated in this function. + * + * @param {String} set the set name how it is stored in the DB + * @param {function} funcSingle the function that fetches single entries from the DB + * @param {function} cb the function to be called on success or error, receives + * arguments (err, obj) + */ +function getSetRecords(set, funcSingle, cb) { + if(db) db.smembers(set, function(err, reply) { + if(err) log.error('DB', 'fetching ' + set + ': ' + err); + else { + if(reply.length === 0) { + cb(null, null); + } else { + var semaphore = reply.length, objReplies = {}; + setTimeout(function() { + if(semaphore > 0) { + cb('Timeout fetching ' + set, null); + } + }, 1000); + for(var i = 0; i < reply.length; i++){ + funcSingle(reply[i], function(prop) { + return function(err, reply) { + if(err) log.error('DB', ' fetching single element: ' + prop); + else { + objReplies[prop] = reply; + if(--semaphore === 0) cb(null, objReplies); + } + }; + }(reply[i])); + } + } + } + }); +} + +// @method shutDown() + +// Shuts down the db link. +exports.shutDown = function() { if(db) db.quit(); }; + + +// ## Action Modules + +/** + * ### storeActionModule + * Store a string representation of an action module in the DB. + * @param {String} id the unique identifier of the module + * @param {String} data the string representation + */ +exports.storeActionModule = function(id, data) { + if(db) { + db.sadd('action_modules', id, replyHandler('storing action module key ' + id)); + db.set('action_module_' + id, data, replyHandler('storing action module ' + id)); + } +}; + +/** + * ### getActionModule(id, cb) + * Query the DB for an action module. + * @param {String} id the module id + * @param {function} cb the cb to receive the answer (err, obj) + */ +exports.getActionModule = function(id, cb) { + if(cb && db) db.get('action_module_' + id, cb); +}; + +/** + * ### getActionModules(cb) + * Fetch all action modules. + * @param {function} cb the cb to receive the answer (err, obj) + */ +exports.getActionModules = function(cb) { + getSetRecords('action_modules', exports.getActionModule, cb); +}; + +/** + * storeActionModuleAuth(id, data) + * Store a string representation of the authentication parameters for an action module. + * @param {String} id the unique identifier of the module + * @param {String} data the string representation + */ +exports.storeActionModuleAuth = function(id, data) { + if(data && db) { + db.sadd('action_modules_auth', id, replyHandler('storing action module auth key ' + id)); + db.set('action_module_' + id +'_auth', encrypt(data), replyHandler('storing action module auth ' + id)); + } +}; + +/** + * ### getActionModuleAuth(id, cb) + * Query the DB for an action module authentication token. + * @param {String} id the module id + * @param {function} cb the cb to receive the answer (err, obj) + */ +exports.getActionModuleAuth = function(id, cb) { + if(cb && db) db.get('action_module_' + id + '_auth', function(id) { + return function(err, txt) { cb(err, decrypt(txt, 'action_module_' + id + '_auth')); }; + }(id)); +}; + +// ## Event Modules + +/** + * ### storeEventModule(id, data) + * Store a string representation of an event module in the DB. + * @param {String} id the unique identifier of the module + * @param {String} data the string representation + */ +exports.storeEventModule = function(id, data) { + if(db) { + db.sadd('event_modules', id, replyHandler('storing event module key ' + id)); + db.set('event_module_' + id, data, replyHandler('storing event module ' + id)); + } +}; + +/** + * ### getEventModule(id, cb) + * Query the DB for an event module. + * @param {String} id the module id + * @param {function} cb the cb to receive the answer (err, obj) + */ +exports.getEventModule = function(id, cb) { + if(cb && db) db.get('event_module_' + id, cb); +}; + +/** + * ### getEventModules(cb) + * Fetch all event modules. + * @param {function} cb the cb that receives the arguments (err, obj) + */ +exports.getEventModules = function(cb) { + getSetRecords('event_modules', exports.getEventModule, cb); +}; + +/** + * ### storeEventModuleAuth(id, data) + * Store a string representation of he authentication parameters for an event module. + * @param {String} id the unique identifier of the module + * @param {String} data the string representation + */ +exports.storeEventModuleAuth = function(id, data) { + if(data && db) { + db.sadd('event_modules_auth', id, replyHandler('storing event module auth key ' + id)); + db.set('event_module_' + id +'_auth', encrypt(data), replyHandler('storing event module auth ' + id)); + } +}; + +// @method getEventModuleAuth(id, cb) + +// Query the DB for an event module authentication token. +// @param {String} id the module id +// @param {function} cb the cb to receive the answer (err, obj) +exports.getEventModuleAuth = function(id, cb) { + if(cb) db.get('event_module_' + id +'_auth', function(id) { + return function(err, txt) { cb(err, decrypt(txt, 'event_module_' + id + '_auth')); }; + }(id)); +}; + +// ## Rules + +// @method storeRule(id, data) + +// Store a string representation of a rule in the DB. +// @param {String} id the unique identifier of the rule +// @param {String} data the string representation +exports.storeRule = function(id, data) { + if(db) { + db.sadd('rules', id, replyHandler('storing rule key ' + id)); + db.set('rule_' + id, data, replyHandler('storing rule ' + id)); + } +}; + +// @method getRule(id, cb) + +// Query the DB for a rule. +// @param {String} id the rule id +// @param {function} cb the cb to receive the answer (err, obj) +exports.getRule = function(id, cb) { + if(db) db.get('rule_' + id, cb); +}; + +// @method getRules(cb) + +// Fetch all rules from the database. +// @param {function} cb +exports.getRules = function(cb) { + getSetRecords('rules', exports.getRule, cb); +}; + +/** + * + * @param {Object} objUser + * @param {function} cb + */ +exports.storeUser = function(objUser, cb) { + if(db && objUser && objUser.username && objUser.password) { + db.sadd('users', objUser.username, replyHandler('storing user key ' + objUser.username)); + objUser.password = encrypt(objUser.password); + db.set('user:' + objUser.username, objUser, replyHandler('storing user properties ' + objUser.username)); + } +}; + +/** + * Checks the credentials and on success returns the user object. + * @param {Object} objUser + * @param {function} cb + */ +exports.loginUser = function(username, password, cb) { + if(typeof cb !== 'function') return; + if(db) db.get('user:' + username, function(p) { + return function(err, obj) { + if(err) cb(err); + else if(encrypt(obj.password) === p) cb(null, obj); + else cb(new Error('Wrong credentials!')); + }; + }(password)); + else cb(new Error('No database link available!')); +}; diff --git a/js-coffee/engine.js b/js-coffee/engine.js new file mode 100644 index 0000000..68664e0 --- /dev/null +++ b/js-coffee/engine.js @@ -0,0 +1,231 @@ +'use strict'; + +var path = require('path'), + cp = require('child_process'), + log = require('./logging'), + qEvents = new (require('./queue')).Queue(), //TODO export queue into redis + regex = /\$X\.[\w\.\[\]]*/g, // find properties of $X + listRules = {}, + listActionModules = {}, + isRunning = true, + ml, poller, db; + +exports = module.exports = function(args) { + args = args || {}; + log(args); + ml = require('./module_loader')(args); + poller = cp.fork(path.resolve(__dirname, 'eventpoller'), [log.getLogType()]); + poller.on('message', function(evt) { + exports.pushEvent(evt); + }); + //start to poll the event queue + pollQueue(); + return module.exports; +}; + +/* + * Initialize the rules engine which initializes the module loader. + * @param {Object} db_link the link to the db, see [db\_interface](db_interface.html) + * @param {String} db_port the db port + * @param {String} crypto_key the key to be used for encryption on the db, max legnth 256 + */ +exports.addDBLinkAndLoadActionsAndRules = function(db_link) { + db = db_link; + if(ml && db) db.getActionModules(function(err, obj) { + if(err) log.error('EN', 'retrieving Action Modules from DB!'); + else { + if(!obj) { + log.print('EN', 'No Action Modules found in DB!'); + loadRulesFromDB(); + } else { + var m; + for(var el in obj) { + log.print('EN', 'Loading Action Module from DB: ' + el); + try{ + m = ml.requireFromString(obj[el], el); + db.getActionModuleAuth(el, function(mod) { + return function(err, obj) { + if(obj && mod.loadCredentials) mod.loadCredentials(JSON.parse(obj)); + }; + }(m)); + listActionModules[el] = m; + } catch(e) { + e.addInfo = 'error in action module "' + el + '"'; + log.error('EN', e); + } + } + loadRulesFromDB(); + } + } + }); + else log.severe('EN', new Error('Module Loader or DB not defined!')); +}; + +function loadRulesFromDB() { + if(db) db.getRules(function(err, obj) { + for(var el in obj) exports.addRule(JSON.parse(obj[el])); + }); +} + +/** + * Insert an action module into the list of available interfaces. + * @param {Object} objModule the action module object + */ +exports.loadActionModule = function(name, objModule) { + log.print('EN', 'Action module "' + name + '" loaded'); + listActionModules[name] = objModule; +}; + +/** + * Add a rule into the working memory + * @param {Object} objRule the rule object + */ +exports.addRule = function(objRule) { + //TODO validate rule + log.print('EN', 'Loading Rule: ' + objRule.id); + if(listRules[objRule.id]) log.print('EN', 'Replacing rule: ' + objRule.id); + listRules[objRule.id] = objRule; + + // Notify poller about eventual candidate + try { + poller.send('event|'+objRule.event); + } catch (err) { + log.print('EN', 'Unable to inform poller about new active rule!'); + } +}; + +function pollQueue() { + if(isRunning) { + var evt = qEvents.dequeue(); + if(evt) { + processEvent(evt); + } + setTimeout(pollQueue, 50); //TODO adapt to load + } +} + +/** + * Stores correctly posted events in the queue + * @param {Object} evt The event object + */ +exports.pushEvent = function(evt) { + qEvents.enqueue(evt); +}; + +/** + * Handles correctly posted events + * @param {Object} evt The event object + */ +function processEvent(evt) { + log.print('EN', 'processing event: ' + evt.event + '(' + evt.eventid + ')'); + var actions = checkEvent(evt); + for(var i = 0; i < actions.length; i++) { + invokeAction(evt, actions[i]); + } +} + +/** + * Check an event against the rules repository and return the actions + * if the conditons are met. + * @param {Object} evt the event to check + */ +function checkEvent(evt) { + var actions = []; + for(var rn in listRules) { + //TODO this needs to get depth safe, not only data but eventually also + // on one level above (eventid and other meta) + if(listRules[rn].event === evt.event && validConditions(evt.payload, listRules[rn])) { + log.print('EN', 'Rule "' + rn + '" fired'); + actions = actions.concat(listRules[rn].actions); + } + } + return actions; +} + +/** + * Checks whether all conditions of the rule are met by the event. + * @param {Object} evt the event to check + * @param {Object} rule the rule with its conditions + */ +function validConditions(evt, rule) { + for(var property in rule.condition){ + if(!evt[property] || evt[property] != rule.condition[property]) return false; + } + return true; +} + +/** + * Invoke an action according to its type. + * @param {Object} evt The event that invoked the action + * @param {Object} action The action to be invoked + */ +function invokeAction(evt, action) { + var actionargs = {}, + arrModule = action.module.split('->'); + if(arrModule.length < 2) { + log.error('EN', 'Invalid rule detected!'); + return; + } + var srvc = listActionModules[arrModule[0]]; + if(srvc && srvc[arrModule[1]]) { + //FIXME preprocessing not only on data + preprocessActionArguments(evt.payload, action.arguments, actionargs); + try { + if(srvc[arrModule[1]]) srvc[arrModule[1]](actionargs); + } catch(err) { + log.error('EN', 'during action execution: ' + err); + } + } + else log.print('EN', 'No api interface found for: ' + action.module); +} + +/** + * Action properties may contain event properties which need to be resolved beforehand. + * @param {Object} evt The event whose property values can be used in the rules action + * @param {Object} act The rules action arguments + * @param {Object} res The object to be used to enter the new properties + */ +function preprocessActionArguments(evt, act, res) { + for(var prop in act) { + /* + * If the property is an object itself we go into recursion + */ + if(typeof act[prop] === 'object') { + res[prop] = {}; + preprocessActionArguments(evt, act[prop], res[prop]); + } + else { + var txt = act[prop]; + var arr = txt.match(regex); + /* + * If rules action property holds event properties we resolve them and + * replace the original action property + */ + // console.log(evt); + if(arr) { + for(var i = 0; i < arr.length; i++) { + /* + * The first three characters are '$X.', followed by the property + */ + var actionProp = arr[i].substring(3).toLowerCase(); + // console.log(actionProp); + for(var eprop in evt) { + // our rules language doesn't care about upper or lower case + if(eprop.toLowerCase() === actionProp) { + txt = txt.replace(arr[i], evt[eprop]); + } + } + txt = txt.replace(arr[i], '[property not available]'); + } + } + res[prop] = txt; + } + } +} + +exports.shutDown = function() { + log.print('EN', 'Shutting down Poller and DB Link'); + isRunning = false; + if(poller) poller.send('cmd|shutdown'); + if(db) db.shutDown(); +}; diff --git a/js-coffee/eventpoller.js b/js-coffee/eventpoller.js new file mode 100644 index 0000000..7198bc1 --- /dev/null +++ b/js-coffee/eventpoller.js @@ -0,0 +1,153 @@ +// # Event Poller + +'use strict'; + +var fs = require('fs'), + path = require('path'), + log = require('./logging'), + listMessageActions = {}, + listAdminCommands = {}, + listEventModules = {}, + listPoll = {}, //TODO this will change in the future because it could have + //several parameterized (user-specific) instances of each event module + isRunning = true, + eId = 0, + db, ml; + +//TODO allow different polling intervals (a wrapper together with settimeout per to be polled could be an easy and solution) + + +function init() { + if(process.argv.length > 2) log({ logType: parseInt(process.argv[2]) || 0 }); + var args = { logType: log.getLogType() }; + ml = require('./module_loader')(args); + db = require('./db_interface')(args); + initAdminCommands(); + initMessageActions(); + pollLoop(); +}; + + +function loadEventModule(el, cb) { + if(db && ml) db.getEventModule(el, function(err, obj) { + if(err || !obj) { + if(typeof cb === 'function') cb(new Error('Retrieving Event Module ' + el + ' from DB: ' + err)); + else log.error('EP', 'Retrieving Event Module ' + el + ' from DB!'); + } + else { + // log.print('EP', 'Loading Event Module: ' + el); + try { + var m = ml.requireFromString(obj, el); + db.getEventModuleAuth(el, function(mod) { + return function(err, objA) { + //TODO authentication needs to be done differently + if(objA && mod.loadCredentials) mod.loadCredentials(JSON.parse(objA)); + }; + }(m)); + listEventModules[el] = m; + if(typeof cb === 'function') cb(null, m); + } catch(e) { + if(typeof cb === 'function') cb(e); + else log.error(e); + } + } + }); +} + +function fetchPollFunctionFromModule(mod, func) { + for(var i = 1; i < func.length; i++) { + if(mod) mod = mod[func[i]]; + } + if(mod) { + log.print('EP', 'Found active event module "' + func.join('->') + '", adding it to polling list'); + //FIXME change this to [module][prop] = module; because like this identical properties get overwritten + // also add some on a per user basis information because this should go into a user context for the users + // that sat up this rule! + listPoll[func.join('->')] = mod; + } else { + log.print('EP', 'No property "' + func.join('->') + '" found'); + } +} + +function initMessageActions() { + listMessageActions['event'] = function(args) { + var prop = args[1], arrModule = prop.split('->'); + if(arrModule.length > 1){ + if(listEventModules[arrModule[0]]) { + fetchPollFunctionFromModule(listEventModules[arrModule[0]], arrModule); + } else { + log.print('EP', 'Event Module ' + arrModule[0] + ' needs to be loaded, doing it now...'); + loadEventModule(arrModule[0], function(err, obj) { + if(err || !obj) log.error('EP', 'Event Module "' + arrModule[0] + '" not found: ' + err); + else { + log.print('EP', 'Event Module ' + arrModule[0] + ' found and loaded'); + fetchPollFunctionFromModule(obj, arrModule); + } + }); + } + } + }; + + //TODO this goes into module_manager, this will receive notification about + // new loaded/stored event modules and fetch them from the db + listMessageActions['cmd'] = function(args) { + var func = listAdminCommands[args[1]]; + if(typeof(func) === 'function') func(args); + }; + + process.on('message', function(strProps) { + var arrProps = strProps.split('|'); + if(arrProps.length < 2) log.error('EP', 'too few parameter in message!'); + else { + var func = listMessageActions[arrProps[0]]; + if(func) func(arrProps); + } + }); +} + +function initAdminCommands() { + listAdminCommands['shutdown'] = function(args) { + log.print('EP', 'Shutting down DB Link'); + isRunning = false; + if(db) db.shutDown(); + }; +} + +function checkRemotes() { + for(var prop in listPoll) { + try { + listPoll[prop]( + /* + * define and immediately call anonymous function with param prop. + * This places the value of prop into the context of the callback + * and thus doesn't change when the for loop keeps iterating over listPoll + */ + (function(p) { + return function(err, obj) { + if(err) { + err.additionalInfo = 'module: ' + p; + log.error('EP', err); + } else { + process.send({ + event: p, + eventid: 'polled_' + eId++, + payload: obj + }); + } + }; + })(prop) + ); + } catch (e) { + log.error('EP', e); + } + } +} + +function pollLoop() { + if(isRunning) { + checkRemotes(); + setTimeout(pollLoop, 10000); + } +} + +init(); \ No newline at end of file diff --git a/js-coffee/http_listener.js b/js-coffee/http_listener.js new file mode 100644 index 0000000..5ec9603 --- /dev/null +++ b/js-coffee/http_listener.js @@ -0,0 +1,116 @@ +// HTTP Listener +// ============= +// +// Handles the HTTP requests to the server at the port specified by the [config](config.html) file. + +'use strict'; + +var path = require('path'), + express = require('express'), + app = express(), + RedisStore = require('connect-redis')(express), + qs = require('querystring'), + log = require('./logging'), + sess_sec = '#C[>;j`@".TXm2TA;A2Tg)', + db_port, http_port, server, + eventHandler, userHandler; + +/* + * The module needs to be called as a function to initialize it. + * After that it fetches the http\_port, db\_port & sess\_sec properties + * from the configuration file. + */ +exports = module.exports = function(args) { + args = args || {}; + log(args); + var config = require('./config')(args); + userHandler = require('./user_handler')(args); + db_port = config.getDBPort(), + sess_sec = config.getSessionSecret(), + http_port = config.getHttpPort(); + return module.exports; +}; + +exports.addHandlers = function(funcAdminHandler, funcEvtHandler) { + if(!funcAdminHandler || !funcEvtHandler) { + log.error('HL', 'ERROR: either adminHandler or eventHandler function not defined!'); + return; + } + userHandler.addHandler(funcAdminHandler); + eventHandler = funcEvtHandler; + // Add cookie support for session handling. + app.use(express.cookieParser()); + app.use(express.session({secret: sess_sec})); + log.print('HL', 'no session backbone'); + + // ^ TODO figure out why redis backbone doesn't work. eventually the db pass has to be set in the DB? + // } session information seems to be stored in DB but not retrieved correctly + // } if(db_port) { + // } app.use(express.session({ + // } store: new RedisStore({ + // } host: 'localhost', + // } port: db_port, + // } db: 2 + // } , + // } pass: null + // } }), + // } secret: sess_sec + // } })); + // } log.print('HL', 'Added redis DB as session backbone'); + // } } else { + // } app.use(express.session({secret: sess_sec})); + // } log.print('HL', 'no session backbone'); + // } } + + // Redirect the requests to the appropriate handler. + app.use('/', express.static(path.resolve(__dirname, '..', 'webpages'))); + // app.use('/doc/', express.static(path.resolve(__dirname, '..', 'webpages', 'doc'))); + // app.get('/mobile', userHandler.handleRequest); + app.get('/rulesforge', userHandler.handleRequest); + // app.use('/mobile', express.static(path.resolve(__dirname, '..', 'webpages', 'mobile'))); + // } app.use('/rulesforge/', express.static(path.resolve(__dirname, '..', 'webpages', 'rulesforge'))); + app.get('/admin', userHandler.handleRequest); + app.post('/login', userHandler.handleLogin); + app.post('/push_event', onPushEvent); + try { + if(http_port) server = app.listen(http_port); // inbound event channel + else log.error('HL', new Error('No HTTP port found!? Nothing to listen on!...')); + } catch(e) { + e.addInfo = 'port unavailable'; + log.error(e); + funcAdminHandler({cmd: 'shutdown'}); + } +}; + +/** + * If a post request reaches the server, this function handles it and treats the request as a possible event. + */ +function onPushEvent(req, resp) { + var body = ''; + req.on('data', function (data) { body += data; }); + req.on('end', function () { + var obj = qs.parse(body); + /* If required event properties are present we process the event */ + if(obj && obj.event && obj.eventid){ + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.write('Thank you for the event (' + obj.event + '[' + obj.eventid + '])!'); + eventHandler(obj); + } else { + resp.writeHead(400, { "Content-Type": "text/plain" }); + resp.write('Your event was missing important parameters!'); + } + resp.end(); + }); +} + +exports.loadUsers = function() { + var users = JSON.parse(require('fs').readFileSync(path.resolve(__dirname, '..', relPath))); + for(var name in users) { + + } +}; + +exports.shutDown = function() { + log.print('HL', 'Shutting down HTTP listener'); + process.exit(); // This is a bit brute force... +}; diff --git a/js-coffee/logging.js b/js-coffee/logging.js new file mode 100644 index 0000000..d4ee683 --- /dev/null +++ b/js-coffee/logging.js @@ -0,0 +1,92 @@ +/* + * Logging + * ======= + * Functions to handle logging and errors. + * + * Valid log types are: + * + * - 0 standard I/O + * - 1 file + * - 2 silent + */ +var fs = require('fs'), + logTypes = [ flushToConsole, flushToFile, null], + logFile = require('path').resolve(__dirname, '..', 'server.log'), + logType = 0; + +exports = module.exports = function(args) { + args = args || {}; + if(args.logType) logType = parseInt(args.logType) || 0; + if(logType == 1) fs.truncateSync(logFile, 0); + if(logType > logTypes.length - 1) logType = 0; + return module.exports; +}; + +exports.getLogType = function() { return logType; }; + +function flush(err, msg) { + if(typeof logTypes[logType] === 'function') logTypes[logType](err, msg); +} + +function flushToConsole(err, msg) { + if(err) console.error(msg); + else console.log(msg); +} + +function flushToFile(err, msg) { + fs.appendFile(logFile, msg + '\n', function (err) {}); +} + +// @function print(module, msg) + +/* + * Prints a log to stdout. + * @param {String} module + * @param {String} msg + */ +exports.print = function(module, msg) { + flush(false, (new Date()).toISOString() + ' | ' + module + ' | ' + msg); +}; + +/** + * Prints a log to stderr. + * @param {String} module + * @param {Error} err + */ +function printError(module, err, isSevere) { + var ts = (new Date()).toISOString() + ' | ', ai = ''; + if(!err) err = new Error('Unexpected error'); + if(typeof err === 'string') err = new Error(err); + // if(module) flush(true, ts + module + ' | ERROR AND BAD HANDLING: ' + err + '\n' + e.stack); + // else flush(true, ts + '!N/A! | ERROR, BAD HANDLING AND NO MODULE NAME: ' + err + '\n' + e.stack); + // } else if(err) { + if(err.addInfo) ai = ' (' + err.addInfo + ')'; + if(!err.message) err.message = 'UNKNOWN REASON!\n' + err.stack; + if(module) { + var msg = ts + module + ' | ERROR'+ai+': ' + err.message; + if(isSevere) msg += '\n' + err.stack; + flush(true, msg); + } else flush(true, ts + '!N/A! | ERROR AND NO MODULE NAME'+ai+': ' + err.message + '\n' + err.stack); + // } else { + // var e = new Error('Unexpected error'); + // flush(true, e.message + ': \n' + e.stack); + // } +}; + +/** + * Prints a message to stderr. + * @param {String} module + * @param {Error} err + */ +exports.error = function(module, err) { + printError(module, err, false); +}; + +/** + * Prints a message with error stack to stderr + * @param {String} module + * @param {Error} err + */ +exports.severe = function(module, err) { + printError(module, err, true); +}; diff --git a/js-coffee/module_loader.js b/js-coffee/module_loader.js new file mode 100644 index 0000000..d2cb0d8 --- /dev/null +++ b/js-coffee/module_loader.js @@ -0,0 +1,81 @@ +'use strict'; + +var fs = require('fs'), + path = require('path'), + log = require('./logging'); + +exports = module.exports = function(args) { + args = args || {}; + log(args); + return module.exports; +}; + +exports.requireFromString = function(src, name, dir) { + if(!dir) dir = __dirname; + var id = path.resolve(dir, name, name + '.vm'); + //FIXME load modules only into a safe environment with given modules, no access to whole application, + var vm = require('vm'), + sandbox = { + log: log, + needle: require('needle') + }; + + var mod = vm.runInNewContext(src, sandbox, 'myfile.vm'); + console.log(mod); + var m = new module.constructor(id, module); + m.paths = module.paths; + try { + m._compile(src); + } catch(err) { + err.addInfo = 'during compilation of module ' + name; + log.error('LM', err); + // log.error('LM', ' during compilation of ' + name + ': ' + err); + } + return m.exports; +}; + +exports.loadModule = function(directory, name, callback) { + try { + fs.readFile(path.resolve(__dirname, '..', directory, name, name + '.js'), 'utf8', function (err, data) { + if (err) { + log.error('LM', 'Loading module file!'); + return; + } + var mod = exports.requireFromString(data, name, directory); + if(mod && fs.existsSync(path.resolve(__dirname, '..', directory, name, 'credentials.json'))) { + fs.readFile(path.resolve(__dirname, '..', directory, name, 'credentials.json'), 'utf8', function (err, auth) { + if (err) { + log.error('LM', 'Loading credentials file for "' + name + '"!'); + callback(name, data, mod, null); + return; + } + if(mod.loadCredentials) mod.loadCredentials(JSON.parse(auth)); + callback(name, data, mod, auth); + }); + } else { + // Hand back the name, the string contents and the compiled module + callback(name, data, mod, null); + } + }); + } catch(err) { + log.error('LM', 'Failed loading module "' + name + '"'); + } +}; + +exports.loadModules = function(directory, callback) { + fs.readdir(path.resolve(__dirname, '..', directory), function (err, list) { + if (err) { + log.error('LM', 'loading modules directory: ' + err); + return; + } + log.print('LM', 'Loading ' + list.length + ' modules from "' + directory + '"'); + list.forEach(function (file) { + fs.stat(path.resolve(__dirname, '..', directory, file), function (err, stat) { + if (stat && stat.isDirectory()) { + exports.loadModule(directory, file, callback); + } + }); + }); + }); +}; + diff --git a/js-coffee/module_manager.js b/js-coffee/module_manager.js new file mode 100644 index 0000000..e41d995 --- /dev/null +++ b/js-coffee/module_manager.js @@ -0,0 +1,119 @@ +/* +# Module Manager +> The module manager takes care of the module and rules loading in the initialization +> phase and on user request. + +> Event and Action modules are loaded as strings and stored in the database, +> then compiled into node modules and rules + */ + +'use strict'; + +var fs = require('fs'), + path = require('path'), + log = require('./logging'), + ml, db, funcLoadAction, funcLoadRule; + +exports = module.exports = function(args) { + args = args || {}; + log(args); + ml = require('./module_loader')(args); + return module.exports; +}; + +exports.addHandlers = function(db_link, fLoadAction, fLoadRule) { + db = db_link; + funcLoadAction = fLoadAction; + funcLoadRule = fLoadRule; +}; + +/* + * Load Rules from fs + * ------------------ + */ +exports.loadRulesFromFS = function(args, answHandler) { + if(!args) args = {}; + if(!args.name) args.name = 'rules'; + if(!funcLoadRule) log.error('ML', 'no rule loader function available'); + else { + fs.readFile(path.resolve(__dirname, '..', 'rules', args.name + '.json'), 'utf8', function (err, data) { + if (err) { + log.error('ML', 'Loading rules file: ' + args.name + '.json'); + return; + } + try { + var arr = JSON.parse(data), txt = ''; + log.print('ML', 'Loading ' + arr.length + ' rules:'); + for(var i = 0; i < arr.length; i++) { + txt += arr[i].id + ', '; + db.storeRule(arr[i].id, JSON.stringify(arr[i])); + funcLoadRule(arr[i]); + } + answHandler.answerSuccess('Yep, loaded rules: ' + txt); + } catch (e) { + log.error('ML', 'rules file was corrupt! (' + args.name + '.json)'); + } + }); + } +}; + +/* + * Load Action Modules from fs + * --------------------------- + */ + +/** + * + * @param {Object} name + * @param {Object} data + * @param {Object} mod + * @param {String} [auth] The string representation of the auth json + */ +function loadActionCallback(name, data, mod, auth) { + db.storeActionModule(name, data); // store module in db + funcLoadAction(name, mod); // hand back compiled module + if(auth) db.storeActionModuleAuth(name, auth); +} + +exports.loadActionModuleFromFS = function (args, answHandler) { + if(ml) { + if(args && args.name) { + answHandler.answerSuccess('Loading action module ' + args.name + '...'); + ml.loadModule('mod_actions', args.name, loadActionCallback); + } else log.error('MM', 'Action Module name not provided!'); + } +}; + +exports.loadActionModulesFromFS = function(args, answHandler) { + if(ml) { + answHandler.answerSuccess('Loading action modules...'); + ml.loadModules('mod_actions', loadActionCallback); + } +}; + +/* + * Load Event Modules from fs + * -------------------------- + */ + +function loadEventCallback(name, data, mod, auth) { + if(db) { + db.storeEventModule(name, data); // store module in db + if(auth) db.storeEventModuleAuth(name, auth); + } +} + +exports.loadEventModuleFromFS = function(args, answHandler) { + if(ml) { + if(args && args.name) { + answHandler.answerSuccess('Loading event module ' + args.name + '...'); + ml.loadModule('mod_events', args.name, loadEventCallback); + } else log.error('MM', 'Event Module name not provided!'); + } +}; + +exports.loadEventModulesFromFS = function(args, answHandler) { + answHandler.answerSuccess('Loading event moules...'); + ml.loadModules('mod_actions', loadEventCallback); +}; + diff --git a/js-coffee/queue.js b/js-coffee/queue.js new file mode 100644 index 0000000..906692e --- /dev/null +++ b/js-coffee/queue.js @@ -0,0 +1,57 @@ +// *(will be replaced by a Redis DB queue)* + +// Queue.js +// ======== +// +// *A function to represent a queue* + +// *Created by Stephen Morley - http://code.stephenmorley.org/ - and released under +// the terms of the CC0 1.0 Universal legal code:* + +// *http://creativecommons.org/publicdomain/zero/1.0/legalcode* + +// *items are added to the end of the queue and removed from the front.* + +exports.Queue = function(){ + // initialise the queue and offset + var queue = []; + var offset = 0; + this.getLength = function(){ + // return the length of the queue + return (queue.length - offset); + }; + + /* Returns true if the queue is empty, and false otherwise. + */ + this.isEmpty = function(){ return (queue.length == 0); }; + + /* Enqueues the specified item. The parameter is: + * item - the item to enqueue + */ + this.enqueue = function(item){ queue.push(item); }; + + /* Dequeues an item and returns it. If the queue is empty then undefined is + * returned. + */ + this.dequeue = function(){ + // if the queue is empty, return undefined + if (queue.length == 0) return undefined; + // store the item at the front of the queue + var item = queue[offset]; + // increment the offset and remove the free space if necessary + if (++ offset * 2 >= queue.length){ + queue = queue.slice(offset); + offset = 0; + } + // return the dequeued item + return item; + }; + + /* Returns the item at the front of the queue (without dequeuing it). If the + * queue is empty then undefined is returned. + */ + this.peek = function(){ + // return the item at the front of the queue + return (queue.length > 0 ? queue[offset] : undefined); + }; +}; diff --git a/js-coffee/server.js b/js-coffee/server.js new file mode 100644 index 0000000..046cee0 --- /dev/null +++ b/js-coffee/server.js @@ -0,0 +1,34 @@ +// Generated by CoffeeScript 1.6.3 +/* +Rules Server +============ +This is the main module that is used to run the whole server: + + node server [log_type http_port] + +Valid `log_type`'s are: + +- `0`: standard I/O output (default) +- `1`: log file (server.log) +- `2`: silent + +`http_port` can be set to use another port, than defined in the +[config](config.html) file, to listen to, e.g. used by the test suite. +*/ + + +(function() { + var root; + + root = typeof exports !== "undefined" && exports !== null ? exports : this; + + root.foo = function() { + return 'Hello World'; + }; + + /* + My comments will show up here + */ + + +}).call(this); diff --git a/js-coffee/user_handler.js b/js-coffee/user_handler.js new file mode 100644 index 0000000..7511ed8 --- /dev/null +++ b/js-coffee/user_handler.js @@ -0,0 +1,88 @@ +var path = require('path'), + qs = require('querystring'), + log = require('./logging'), + db = require('./db_interface'), + adminHandler; + +exports = module.exports = function(args) { + args = args || {}; + log(args); + db(args); + var users = JSON.parse(require('fs').readFileSync(path.resolve(__dirname, '..', 'config', 'users.json'))); + for(var name in users) { + db.storeUser(users[name]); + } + + return module.exports; +}; + +exports.addHandler = function(adminHandl) { + adminHandler = adminHandl; +}; + +exports.handleRequest = function(req, resp) { + req.on('end', function () { + resp.end(); + }); + if(req.session && req.session.user) { + resp.send('You\'re logged in'); + } else resp.sendfile(path.resolve(__dirname, '..', 'webpages', 'handlers', 'login.html')); + // resp.end(); + log.print('UH', 'last: '+ req.session.lastPage); + req.session.lastPage = req.originalUrl; + log.print('UH', 'last: '+ req.session.lastPage); + log.print('UH', 'retrieved req: '+ req.originalUrl); + // console.log(req); +}; + +exports.handleLogin = function(req, resp) { + var body = ''; + req.on('data', function (data) { body += data; }); + req.on('end', function () { + if(!req.session || !req.session.user) { + var obj = qs.parse(body); + db.loginUser(obj.username, obj.password, function(err, obj) { + if(!err) req.session.user = obj; + if(req.session.user) { + resp.write('Welcome ' + req.session.user.name + '!'); + } else { + resp.writeHead(401, { "Content-Type": "text/plain" }); + resp.write('Login failed!'); + } + resp.end(); + }); + } + }); +}; + +function answerHandler(r) { + var response = r, hasBeenAnswered = false; + function postAnswer(msg) { + if(!hasBeenAnswered) { + response.write(msg); + response.end(); + hasBeenAnswered = true; + } + } + return { + answerSuccess: function(msg) { + if(!hasBeenAnswered) response.writeHead(200, { "Content-Type": "text/plain" }); + postAnswer(msg); + }, + answerError: function(msg) { + if(!hasBeenAnswered) response.writeHead(400, { "Content-Type": "text/plain" }); + postAnswer(msg); + }, + isAnswered: function() { return hasBeenAnswered; } + }; +}; + +function onAdminCommand(request, response) { + var q = request.query; + log.print('HL', 'Received admin request: ' + request.originalUrl); + if(q.cmd) { + adminHandler(q, answerHandler(response)); + // answerSuccess(response, 'Thank you, we try our best!'); + } else answerError(response, 'I\'m not sure about what you want from me...'); +} + diff --git a/js-coffee/users.js b/js-coffee/users.js new file mode 100644 index 0000000..813b71f --- /dev/null +++ b/js-coffee/users.js @@ -0,0 +1,81 @@ +'use strict'; + +var log = require('./logging'), + objCmds = { + addUser: addUser, + getUser: getUser, + delUser: delUser, + addRule: addRule, + getRules: getRules, + delRule: delRule + }; + +exports = module.exports = function(args) { + args = args || {}; + log(args); + return module.exports; +}; + +exports.handleCommand = function(args, cb) { + if(!args.cmd) { + var e = new Error('No command defined!'); + if(typeof cb === 'function') cb(e); + else log.error('US', e); + } else { + objCmds[args.cmd](args, cb); + } +}; + +/** + * + * @param {Object} args + * @param {function} cb + */ +function addUser(args, cb) { + +} + +/** + * + * @param {Object} args + * @param {function} cb + */ +function getUser(args, cb) { + +} + +/** + * + * @param {Object} args + * @param {function} cb + */ +function delUser(args, cb) { + +} + +/** + * + * @param {Object} args + * @param {function} cb + */ +function addRule(args, cb) { + +} + +/** + * + * @param {Object} args + * @param {function} cb + */ +function getRule(args, cb) { + +} + +/** + * + * @param {Object} args + * @param {function} cb + */ +function delRule(args, cb) { + +} diff --git a/run_coffee_server.sh b/run_coffee_server.sh new file mode 100755 index 0000000..2e17819 --- /dev/null +++ b/run_coffee_server.sh @@ -0,0 +1,3 @@ +#!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +node $DIR/js-coffee/server diff --git a/run_server.sh b/run_server.sh index fe86b0c..4743381 100755 --- a/run_server.sh +++ b/run_server.sh @@ -1,3 +1,3 @@ #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -node $DIR/js/server > $DIR/server.log 2>&1 & +node $DIR/js/server > $DIR/js/server.log 2>&1 &