diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5811ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*~ +*.log +*credentials.json +*node_modules +doc +.project +.settings \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 89576a7..0000000 --- a/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Dominic Bosch - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.js b/LICENSE.js new file mode 100644 index 0000000..0ff9e3e --- /dev/null +++ b/LICENSE.js @@ -0,0 +1,25 @@ +/* + * LICENSE + * ======= + * Copyright (c) 2013 by Dominic Bosch and The + * Regents of the University of Basel. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without written agreement is + * hereby granted, provided that the above copyright notice and the following + * two paragraphs appear in all copies of this software. + * + * IN NO EVENT SHALL THE UNIVERSITY OF BASEL BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT + * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF + * BASEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * THE UNIVERSITY OF BASEL SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF BASEL HAS NO OBLIGATION TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * Authors: Dominic Bosch + * + */ \ No newline at end of file diff --git a/README.md b/README.md index 527acb0..fb7c787 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,53 @@ -webapi-eca -========== +README: webapi-eca +================== -An ECA engine which acts as a middleware between WebAPI's. +>A Modular ECA Engine Server which acts as a middleware between WebAPI's. +>This folder continues examples of an ECA engine and how certain use cases could be implemented together with a rules language. +>Be sure the user which runs the server doesn't have ANY write rights on the server! +>Malicious modules could capture or destroy your server! +> +> +>The server is started through the [rules_server.js](rules_server.html) module by calling `node rule_server.js`. + + +Getting started +--------------- +Clone project: + + git clone https://github.com/dominicbosch/webapi-eca.git + +Download and install dependencies: + + cd webapi-eca + npm install + +Get your [redis](http://redis.io/) instance up and running (and find the port for the config file below) or create your own `db_interface.js`. + +Create the configuration file: + + mkdir config + vi config/config.json + +Insert your settings, for example: + + { + "http_port": 8125, + "db_port": 6379, + "crypto_key": "[your key]" + } + +Start the server: + + node js/server + +*Congratulations, your own WebAPI based ECA engine server is now up and running!* + +Optional command line tools: +---------------------------- +Run test suite: + + node run_tests + +Create the doc *(to be accessed via the webserver, e.g.: localhost:8125/doc/)*: + + node create_doc diff --git a/TODO.js b/TODO.js new file mode 100644 index 0000000..786b9e9 --- /dev/null +++ b/TODO.js @@ -0,0 +1,24 @@ +/* + * TODO's: + * ======= + * - [ ] Redis queue + * - [ ] user handling (personal credentials) + * - [ ] security in terms of users (private key, login) + * - [ ] vm for modules, only give few libraries (no fs!) + * - [ ] rules generator (provide webpage that is used to create rules dependent on the existing modues) + * - [ ] geo location module, test on smartphone + * + * + * TODO's per module: + * ================== + * Testing | clean documentation | Clean error handling (Especially in loading of modules): + * + * - [ ] DB Interface + * - [ ] Engine + * - [ ] Event Poller + * - [ ] HTTP Listener + * - [ ] Logging + * - [ ] Module Loader + * - [ ] Module Manager + * - [ ] Server + */ \ No newline at end of file diff --git a/config/.gitignore b/config/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/config/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/create_doc.js b/create_doc.js new file mode 100644 index 0000000..4410f99 --- /dev/null +++ b/create_doc.js @@ -0,0 +1,20 @@ +/* + * # groc Documentation + * Create the documentation to be displayed through the webserver. + */ +require('groc').CLI([ + "README.md", + "TODO.js", + "LICENSE.js", + "js/*", + "mod_actions/**/*.js", + "mod_events/**/*.js" + // , + // "rules/*.json" + ], + function(error) { + if (error) { + process.exit(1); + } + } +); diff --git a/documentation/user_acceptance_tests.txt b/documentation/user_acceptance_tests.txt new file mode 100644 index 0000000..62ba254 --- /dev/null +++ b/documentation/user_acceptance_tests.txt @@ -0,0 +1,10 @@ +load rules rules +load rules +load action probinder +load actions +load event probinder +load events +-> all these from server started from shell and node (different cwd!) +shutdown +upload rules/actions/events +look at doc ([host]/doc/) diff --git a/js/db_interface.js b/js/db_interface.js new file mode 100644 index 0000000..a1e56e9 --- /dev/null +++ b/js/db_interface.js @@ -0,0 +1,264 @@ +// # 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; + + +// @function init() + +/* + * Initializes the DB connection. Requires a port where the DB listens to requests + * and a key that is used for encryptions. + * @param {int} db_port + */ +exports.init = function(db_port, key, cbDone){ + if(!db_port || !key) { + log.error('DB', 'No DB port or cipher key defined!'); + return null; + } + crypto_key = key; + db = redis.createClient(db_port); + db.on("error", function (err) { + log.error('DB', ' Message from DB: ' + err); + }); + if(cbDone) cbDone(); +}; + +/** + * ### encrypt + */ +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) { + 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: ' + 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} callback the function to be called on success or error, receives + * arguments (err, obj) + */ +function getSetRecords(set, funcSingle, callback) { + db.smembers(set, function(err, reply) { + if(err) log.error('DB', 'fetching ' + set + ': ' + err); + else { + if(reply.length === 0) { + callback(null, null); + } else { + var semaphore = reply.length, objReplies = {}; + setTimeout(function() { + if(semaphore > 0) { + callback('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) callback(null, objReplies); + } + }; + }(reply[i])); + } + } + } + }); +} + +// @method shutDown() + +// Shuts down the db link. +exports.shutDown = function() { 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) { + db.sadd('action_modules', id, replyHandler('storing action module key ' + id)); + db.set('action_module_' + id, data, replyHandler('storing action module ' + id)); +}; + +/** + * ### getActionModule(id, callback) + * Query the DB for an action module. + * @param {String} id the module id + * @param {function} callback the callback to receive the answer (err, obj) + */ +exports.getActionModule = function(id, callback) { + if(callback) db.get('action_module_' + id, callback); +}; + +/** + * ### getActionModules(callback) + * Fetch all action modules. + * @param {function} callback the callback to receive the answer (err, obj) + */ +exports.getActionModules = function(callback) { + getSetRecords('action_modules', exports.getActionModule, callback); +}; + +/** + * 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.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, callback) + * Query the DB for an action module authentication token. + * @param {String} id the module id + * @param {function} callback the callback to receive the answer (err, obj) + */ +exports.getActionModuleAuth = function(id, callback) { + if(callback) db.get('action_module_' + id + '_auth', function(err, txt) { callback(err, decrypt(txt)); }); +}; + +// ## 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) { + db.sadd('event_modules', id, replyHandler('storing event module key ' + id)); + db.set('event_module_' + id, data, replyHandler('storing event module ' + id)); +}; + +/** + * ### getEventModule(id, callback) + * Query the DB for an event module. + * @param {String} id the module id + * @param {function} callback the callback to receive the answer (err, obj) + */ +exports.getEventModule = function(id, callback) { + if(callback) db.get('event_module_' + id, callback); +}; + +/** + * ### getEventModules(callback) + * Fetch all event modules. + * @param {function} callback the callback that receives the arguments (err, obj) + */ +exports.getEventModules = function(callback) { + getSetRecords('event_modules', exports.getEventModule, callback); +}; + +/** + * ### 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.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, callback) + +// Query the DB for an event module authentication token. +// @param {String} id the module id +// @param {function} callback the callback to receive the answer (err, obj) +exports.getEventModuleAuth = function(id, callback) { + if(callback) db.get('event_module_' + id +'_auth', function(err, txt) { callback(err, decrypt(txt)); }); +}; + +// ## 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) { + db.sadd('rules', id, replyHandler('storing rule key ' + id)); + db.set('rule_' + id, data, replyHandler('storing rule ' + id)); +}; + +// @method getRule(id, callback) + +// Query the DB for a rule. +// @param {String} id the rule id +// @param {function} callback the callback to receive the answer (err, obj) +exports.getRule = function(id, callback) { + db.get('rule_' + id, callback); +}; + +// @method getRules(callback) + +// Fetch all rules from the database. +// @param {function} callback the callback to receive the answer (err, obj) +exports.getRules = function(callback) { + getSetRecords('rules', exports.getRule, callback); +}; + diff --git a/js/engine.js b/js/engine.js new file mode 100644 index 0000000..7af816b --- /dev/null +++ b/js/engine.js @@ -0,0 +1,253 @@ +'use strict'; + +var path = require('path'), + cp = require('child_process'), + ml = require('./module_loader'), + log = require('./logging'), + poller, db, isRunning = true, + qEvents = new (require('./queue')).Queue(); // export queue into redis + +var regex = /\$X\.[\w\.\[\]]*/g, // find properties of $X + listRules = {}, + listActionModules = {}, + actionsLoaded = false, eventsLoaded = false; +/* + * 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 + */ +function init(db_link, db_port, crypto_key) { + db = db_link; + loadActions(); + poller = cp.fork(path.resolve(__dirname, 'eventpoller'), [db_port, crypto_key]); + poller.on('message', function(evt) { + if(evt.event === 'ep_finished_loading') { + eventsLoaded = true; + tryToLoadRules(); + } else pushEvent(evt); + }); + //start to poll the event queue + pollQueue(); +} + +function loadActions() { + 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!'); + actionsLoaded = true; + tryToLoadRules(); + } else { + var m, semaphore = 0; + for(var el in obj) { + semaphore++; + log.print('EN', 'Loading Action Module from DB: ' + el); + m = ml.requireFromString(obj[el], el); + db.getActionModuleAuth(el, function(mod) { + return function(err, obj) { + if(--semaphore == 0) { + actionsLoaded = true; + tryToLoadRules(); + } + if(obj && mod.loadCredentials) mod.loadCredentials(JSON.parse(obj)); + }; + }(m)); + listActionModules[el] = m; + } + } + } + }); +} + +function tryToLoadRules() { + if(eventsLoaded && actionsLoaded) { + db.getRules(function(err, obj) { + for(var el in obj) loadRule(JSON.parse(obj[el])); + }); + } +} + +/** + * Insert an action module into the list of available interfaces. + * @param {Object} objModule the action module object + */ +function loadActionModule(name, objModule) { + log.print('EN', 'Action module "' + name + '" loaded'); + listActionModules[name] = objModule; +} + +/** + * Insert a rule into the eca rules repository + * @param {Object} objRule the rule object + */ +function loadRule(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 + */ +function pushEvent(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.data, 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.data, 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; + } + } +} + +function loadEventModule(args, answHandler) { + if(args && args.name) { + answHandler.answerSuccess('Loading event module ' + args.name + '...'); + poller.send('cmd|loadevent|'+args.name); + } else if(args) answHandler.answerError(args.name + ' not found'); +} + +function loadEventModules(args, answHandler) { + answHandler.answerSuccess('Loading event moules...'); + poller.send('cmd|loadevents'); +} + +function shutDown() { + log.print('EN', 'Shutting down Poller and DB Link'); + isRunning = false; + poller.send('cmd|shutdown'); + db.shutDown(); +} + +exports.init = init; +exports.loadActionModule = loadActionModule; +exports.loadRule = loadRule; +exports.loadEventModule = loadEventModule; +exports.loadEventModules = loadEventModules; +exports.pushEvent = pushEvent; +exports.shutDown = shutDown; diff --git a/js/eventpoller.js b/js/eventpoller.js new file mode 100644 index 0000000..de0351b --- /dev/null +++ b/js/eventpoller.js @@ -0,0 +1,146 @@ +// # Event Poller + +'use strict'; + +if(process.argv.length < 3) { + log.error('EP', 'No DB port defined! Not starting poller...'); +} else { + (function() { + var fs = require('fs'), + path = require('path'), + log = require('./logging'), + db = require('./db_interface'), + ml = require('./module_loader'), + 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; + //TODO allow different polling intervals (a wrapper together with settimeout per to be polled could be an easy and solution) + + db.init(process.argv[2], process.argv[3]); + + //TODO eventpoller will not load event modules from filesystem, this will be done by + // the moduel manager and the eventpoller receives messages about new/updated active rules + + db.getEventModules(function(err, obj) { + if(err) log.error('EP', 'retrieving Event Modules from DB!'); + else { + if(!obj) { + log.print('EP', 'No Event Modules found in DB!'); + process.send({ event: 'ep_finished_loading' }); + } else { + var m, semaphore = 0; + for(var el in obj) { + semaphore++; + m = ml.requireFromString(obj[el], el); + db.getEventModuleAuth(el, function(mod) { + return function(err, obj) { + if(--semaphore === 0) process.send({ event: 'ep_finished_loading' }); + if(obj && mod.loadCredentials) mod.loadCredentials(JSON.parse(obj)); + }; + }(m)); + log.print('EP', 'Loading Event Module: ' + el); + listEventModules[el] = m; + } + } + } + }); + + listMessageActions['event'] = function(args) { + var prop = args[1], arrModule = prop.split('->'); + // var arrModule = obj.module.split('->'); + if(arrModule.length > 1){ + var module = listEventModules[arrModule[0]]; + for(var i = 1; i < arrModule.length; i++) { + if(module) module = module[arrModule[i]]; + } + if(module) { + log.print('EP', 'Found active event module "' + prop + '", 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[prop] = module; + } else { + log.print('EP', 'No property "' + prop + '" found'); + } + } + }; + + listAdminCommands['loadevent'] = function(args) { + ml.loadModule('mod_events', args[2], loadEventCallback); + }; + + listAdminCommands['loadevents'] = function(args) { + ml.loadModules('mod_events', loadEventCallback); + }; + + listAdminCommands['shutdown'] = function(args) { + log.print('EP', 'Shutting down DB Link'); + isRunning = false; + db.shutDown(); + }; + + //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 loadEventCallback(name, data, mod, auth) { + db.storeEventModule(name, data); // store module in db + if(auth) db.storeEventModuleAuth(name, auth); + listEventModules[name] = mod; // store compiled module for polling + } + + 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++, + data: obj + }); + } + }; + })(prop) + ); + } catch (e) { + log.error('EP', e); + } + } + } + + function pollLoop() { + if(isRunning) { + checkRemotes(); + setTimeout(pollLoop, 10000); + } + } + + pollLoop(); + })(); +} \ No newline at end of file diff --git a/js/http_listener.js b/js/http_listener.js new file mode 100644 index 0000000..b7e5a72 --- /dev/null +++ b/js/http_listener.js @@ -0,0 +1,96 @@ +// # HTTP Listener +// Isso +'use strict'; +var path = require('path'), + express = require('express'), + port = express(), + log = require('./logging'), + qs = require('querystring'), + adminHandler, eventHandler, server; + +function init(http_port, funcAdminHandler, funcEvtHandler) { + if(!http_port || !funcEvtHandler) { + log.error('HL', 'ERROR: either port or eventHandler function not defined!'); + return; + } + adminHandler = funcAdminHandler; + eventHandler = funcEvtHandler; + port.use('/doc/', express.static(path.resolve(__dirname, '..', 'doc/'))); + port.get('/admin', onAdminCommand); + port.post('/pushEvents', onPushEvent); + server = port.listen(http_port); // inbound event channel + log.print('HL', 'Started listening for http requests on port ' + http_port); +} + +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; } + }; +}; + +/** + * Handles correct event posts, replies thank you. + */ +function answerSuccess(resp, msg){ + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.write(msg); + resp.end(); +} + +/** + * Handles erroneous requests. + * @param {Object} msg the error message to be returned + */ +function answerError(resp, msg) { + resp.writeHead(400, { "Content-Type": "text/plain" }); + resp.write(msg); + resp.end(); +} + +//FIXME this answer handling is a very ugly hack, improve! +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...'); +} + +/** + * If a request is made to the server, this function is used to handle it. + */ +function onPushEvent(request, response) { + var body = ''; + request.on('data', function (data) { body += data; }); + request.on('end', function () { + var obj = qs.parse(body); + /* If required event properties are present we process the event */ + if(obj && obj.event && obj.eventid){ + answerSuccess(response, 'Thank you for the event (' + obj.event + '[' + obj.eventid + '])!'); + eventHandler(obj); + } else answerError(response, 'Your event was missing important parameters!'); + }); +} + +exports.init = init; +exports.shutDown = function() { + log.print('HL', 'Shutting down HTTP listener'); + process.exit(); // This is a bit brute force... +}; diff --git a/js/logging.js b/js/logging.js new file mode 100644 index 0000000..0f14656 --- /dev/null +++ b/js/logging.js @@ -0,0 +1,37 @@ +/* + * Logging + * ======= + * Functions to handle logging and errors. + */ + +// @function print(module, msg) + +/* + * Prints a log to stdout. + * @param {String} module + * @param {String} msg + */ +exports.print = function(module, msg) { + console.log((new Date()).toISOString() + ' | ' + module + ' | ' + msg); +}; + +// @function error(module, msg) + +/* + * Prints a log to stderr. + * @param {String} module + * @param {Error} err + */ +exports.error = function(module, err) { + var ts = (new Date()).toISOString() + ' | ', ai = ''; + if(typeof err === 'string') { + var e = new Error(); + if(module) console.error(ts + module + ' | ERROR AND BAD HANDLING: ' + err + '\n' + e.stack); + else console.error(ts + '!N/A! | ERROR, BAD HANDLING AND NO MODULE NAME: ' + err + '\n' + e.stack); + } else { + if(err.addInfo) ai = ' (' + err.addInfo + ')'; + if(!err.message) err.message = 'UNKNOWN REASON!\n' + err.stack; + if(module) console.error(ts + module + ' | ERROR'+ai+': ' + err.message); + else console.error(ts + '!N/A! | ERROR AND NO MODULE NAME'+ai+': ' + err.message + '\n' + err.stack); + } +}; diff --git a/js/module_loader.js b/js/module_loader.js new file mode 100644 index 0000000..ca6f3ac --- /dev/null +++ b/js/module_loader.js @@ -0,0 +1,69 @@ +var fs = require('fs'), + path = require('path'), + log = require('./logging'); + +function requireFromString(src, name, dir) { + if(!dir) dir = __dirname; + //FIXME load modules only into a safe environment with given modules, no access to whole application + var id = path.resolve(dir, name, name + '.js'); + 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; +} + +function loadModule(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 = 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 + '"'); + } +} + +function loadModules(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()) { + loadModule(directory, file, callback); + } + }); + }); + }); +} + +exports.loadModule = loadModule; +exports.loadModules = loadModules; +exports.requireFromString = requireFromString; + diff --git a/js/module_manager.js b/js/module_manager.js new file mode 100644 index 0000000..d96feda --- /dev/null +++ b/js/module_manager.js @@ -0,0 +1,100 @@ +/* +# 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 and rules + */ +var fs = require('fs'), + path = require('path'), + log = require('./logging'), + ml = require('./module_loader'), + db = null, funcLoadAction, funcLoadRule; + +function init(db_link, fLoadAction, fLoadRule) { + db = db_link; + funcLoadAction = fLoadAction; + funcLoadRule = fLoadRule; +} +/* +# A First Level Header + + +A Second Level Header +--------------------- + +Now is the time for all good men to come to +the aid of their country. This is just a +regular paragraph. + +The quick brown fox jumped over the lazy +dog's back. + +### Header 3 + +> This is a blockquote. +> +> This is the second paragraph in the blockquote. +> +> ## This is an H2 in a blockquote + +This is the function documentation +@param {Object} [args] the optional arguments +@param {String} [args.name] the optional name in the arguments + */ +function loadRulesFile(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)'); + } + }); + } +} + +/** + * + * @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 compiled module back + if(auth) db.storeActionModuleAuth(name, auth); +} + +function loadActionModule(args, answHandler) { + if(args && args.name) { + answHandler.answerSuccess('Loading action module ' + args.name + '...'); + ml.loadModule('mod_actions', args.name, loadActionCallback); + } +} + +function loadActionModules(args, answHandler) { + answHandler.answerSuccess('Loading action modules...'); + ml.loadModules('mod_actions', loadActionCallback); +} + +exports.init = init; +exports.loadRulesFile = loadRulesFile; +exports.loadActionModule = loadActionModule; +exports.loadActionModules = loadActionModules; \ No newline at end of file diff --git a/js/queue.js b/js/queue.js new file mode 100644 index 0000000..906692e --- /dev/null +++ b/js/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/server.js b/js/server.js new file mode 100644 index 0000000..b28b780 --- /dev/null +++ b/js/server.js @@ -0,0 +1,81 @@ +/** @module rules_server */ +'use strict'; +/* +A First Level Header +==================== + +A Second Level Header +--------------------- + +Now is the time for all good men to come to +the aid of their country. This is just a +regular paragraph. + +The quick brown fox jumped over the lazy +dog's back. + +### Header 3 + +> This is a blockquote. +> +> This is the second paragraph in the blockquote. +> +> ## This is an H2 in a blockquote +*/ +var http_listener = require('./http_listener'), + db = require('./db_interface'), + engine = require('./engine'), + mm = require('./module_manager'), + log = require('./logging'), + fs = require('fs'), + path = require('path'), + objCmds = { + 'loadrules': mm.loadRulesFile, + 'loadaction': mm.loadActionModule, + 'loadactions': mm.loadActionModules, + 'loadevent': engine.loadEventModule, + 'loadevents': engine.loadEventModules, + 'shutdown': shutDown, + 'restart': null //TODO implement + }; + +function handleAdminCommands(args, answHandler) { + if(args && args.cmd) { + var func = objCmds[args.cmd]; + if(func) func(args, answHandler); + } else log.print('RS', 'No command in request'); + setTimeout(function(ah) { + answHandler = ah; + return function() { + if(!answHandler.isAnswered()) answHandler.answerError('Not handeled...'); + }; + }, 2000); +} + +function shutDown(args, answHandler) { + answHandler.answerSuccess('Goodbye!'); + log.print('RS', 'Received shut down command!'); + engine.shutDown(); + http_listener.shutDown(); +} + +fs.readFile(path.resolve(__dirname, '..', 'config', 'config.json'), 'utf8', function (err, data) { + if (err) { + console.log(err); + log.error('RS', 'Loading config file'); + return; + } + var config = JSON.parse(data); + if(!config.http_port || !config.db_port || !config.crypto_key) { + log.error('RS', 'you forgot to define either http_port, db_port, crypto_key, or even all of them!'); + } else { + log.print('RS', 'Initialzing DB'); + db.init(config.db_port, config.crypto_key, function() { + engine.init(db, config.db_port, config.crypto_key); + }); + log.print('RS', 'Initialzing http listener'); + http_listener.init(config.http_port, handleAdminCommands, engine.pushEvent); + log.print('RS', 'Initialzing module manager'); + mm.init(db, engine.loadActionModule, engine.loadRule); + } +}); \ No newline at end of file diff --git a/mod_actions/probinder/probinder.js b/mod_actions/probinder/probinder.js new file mode 100644 index 0000000..9bcc825 --- /dev/null +++ b/mod_actions/probinder/probinder.js @@ -0,0 +1,185 @@ +'use strict'; + +var needle = require('needle'); +/** + * ProBinder ACTION MODULE + */ + +var urlService = 'https://probinder.com/service/', + credentials = null; + +function loadCredentials(cred) { + if(!cred || !cred.username || !cred.password) { + console.error('ERROR: ProBinder AM credentials file corrupt'); + } else { + credentials = cred; + console.log('Successfully loaded credentials for ProBinder AM'); + } +} + +/** + * Reset eventually loaded credentials + */ +function purgeCredentials() { + credentials = null; +}; + +/** + * Verify whether the arguments match the existing credentials. + * @param {String} username the username + * @param {String} password the password + */ +function verifyCredentials(username, password) { + if(!credentials) return false; + return credentials.username === username + && credentials.password === password; +}; + +/** + * Call the ProBinder service with the given parameters. + * @param {Object} args the required function arguments object + * @param {Object} [args.data] the data to be posted + * @param {String} args.service the required service identifier to be appended to the url + * @param {String} args.method the required method identifier to be appended to the url + * @param {function} [args.succes] the function to be called on success, + * receives the response body String or Buffer. + * @param {function} [args.error] the function to be called on error, + * receives an error, an http.ClientResponse object and a response body + * String or Buffer. + */ +function call(args) { + if(!args || !args.service || !args.method) { + console.error('ERROR in ProBinder AM call: Too few arguments!'); + return null; + } + if(credentials){ + needle.post(urlService + args.service + '/' + args.method, + args.data, + credentials, + function(error, response, body) { // The callback + if(!error && response && response.statusCode == 200) { + if(args && args.success) args.success(body); + } else { + if(args && args.error) args.error(error, response, body); + else console.error('Error during ProBinder AM call: ' + error.message); + } + } + ); + } else console.error('ERROR ProBinder AM: request or credentials object not ready!'); +}; + +/** + * Calls the user's unread content service. + * @param {Object} [args] the optional object containing the success + * and error callback methods + * @param {function} [args.succes] refer to call function + * @param {function} [args.error] refer to call function + */ +function getUnreadContents(args) { + if(!args) args = {}; + call({ + service: '36', + method: 'unreadcontent', + success: args.success, + error: args.error + }); +}; + +/** + * Calls the content get service with the content id and the service id provided. + * @param {Object} args the object containing the service id and the content id, + * success and error callback methods + * @param {String} args.serviceid the service id that is able to process this content + * @param {String} args.contentid the content id + * @param {function} [args.succes] to be called on success, receives the service, content + * and user id's along with the content + * @param {function} [args.error] refer to call function + */ +function getContent(args){ + if(!args || !args.serviceid || !args.contentid) { + console.error('ERROR in ProBinder AM getContent: Too few arguments!'); + return null; + } + call({ + service: '2', + method: 'get', + data: { id: args.contentid, service: args.serviceid }, + success: args.success, + error: args.error + }); +} + +/** + * Does everything to post something in a binder + * @param {Object} args the object containing the content + * @param {String} args.content the content to be posted + */ +function newContent(args){ + if(!args) args = {}; + if(!args.content) args.content = 'Rule#0 says you received a new mail!'; + call({ + service: '27', + method: 'save', + data: { + companyId: '961', + context: '17936', + text: args.content + } + }); +} + +/** + * Does everything to post a file info in a binder tabe + * @param {Object} args the object containing the content + * @param {String} args.service the content service + * @param {String} args.id the content id + */ +function makeFileEntry(args){ + if(!args || !args.service || !args.id) { + console.error('ERROR in ProBinder AM makeFileEntry: Too few arguments!'); + return null; + } + getContent({ + serviceid: args.service, + contentid: args.id, + success: function(data) { + call({ + service: '27', + method: 'save', + data: { + companyId: '961', + context: '17936', + text: 'New file (' + data.title + ') in tab \"' + data.context[0].name + + '\", find it here!' + } + }); + } + }); +} + +/** + * Does everything to post something in a binder + * @param {Object} args the object containing the content + * @param {String} args.content the content to be posted + */ +function setRead(args){ + call({ + service: '2', + method: 'setread', + data: { + id: args.id + } + }); +} + +exports.loadCredentials = loadCredentials; +exports.purgeCredentials = purgeCredentials; +exports.verifyCredentials = verifyCredentials; +exports.call = call; +exports.getUnreadContents = getUnreadContents; +// exports.getBinderTabContents = getBinderTabContents; +exports.getContent = getContent; +exports.newContent = newContent; +exports.makeFileEntry = makeFileEntry; +exports.setRead = setRead; + diff --git a/mod_actions/testing/test.json b/mod_actions/testing/test.json new file mode 100644 index 0000000..2d284c2 --- /dev/null +++ b/mod_actions/testing/test.json @@ -0,0 +1,3 @@ +{ + "test": "should stay" +} diff --git a/mod_actions/testing/test.json_BACKUP b/mod_actions/testing/test.json_BACKUP new file mode 100644 index 0000000..2d284c2 --- /dev/null +++ b/mod_actions/testing/test.json_BACKUP @@ -0,0 +1,3 @@ +{ + "test": "should stay" +} diff --git a/mod_actions/testing/testing.js b/mod_actions/testing/testing.js new file mode 100644 index 0000000..223c0eb --- /dev/null +++ b/mod_actions/testing/testing.js @@ -0,0 +1,18 @@ +var fs = require('fs'), + path = require('path'); + +/* +// Hacking my own system... + console.log(module.parent.parent.children[0].exports.getEventModuleAuth('probinder', + function(err, obj) {console.log(obj);})); +*/ + +//FIXME do not try to delete a file and rely on it to exist, rather try to create it first! o check for its existance and then delete it +try { + fs.unlinkSync(path.resolve(__dirname, 'event_modules', 'malicious', 'test.json')); + console.error('VERY BAD! NEVER START THIS SERVER WITH A USER THAT HAS WRITE RIGHTS ANYWHERE!!!'); +} catch (err) { + console.log('VERY GOOD! USERS CANNOT WRITE ON YOUR DISK!'); + +} +throw new Error('Testing your error handling'); diff --git a/mod_actions/webapi/webapi.js b/mod_actions/webapi/webapi.js new file mode 100644 index 0000000..927e04c --- /dev/null +++ b/mod_actions/webapi/webapi.js @@ -0,0 +1,31 @@ +'use strict'; + +var needle = require('needle'); + +/** + * Call any arbitrary webAPI. + * @param {Object} args the required function arguments object + * @param {String} args.url the required webAPI url + * @param {Object} [args.data] the data to be posted + * @param {Object} [args.credentials] optional credentials + * @param {String} [args.credentials.username] optional username + * @param {String} [args.credentials.password] optional password + */ +function call(args) { + if(!args || !args.url) { + console.error('ERROR in WebAPI AM call: Too few arguments!'); + return null; + } + needle.post(args.url, + args.data, + args.credentials, + function(error, response, body) { + if (!error) console.log('Successful webAPI AM call to ' + args.url); + else console.error('Error during webAPI AM call to ' + args.url + + ': ' + error.message); + } + ); +}; + +exports.call = call; + diff --git a/mod_actions/wikipedia/wikipedia.js b/mod_actions/wikipedia/wikipedia.js new file mode 100644 index 0000000..868f859 --- /dev/null +++ b/mod_actions/wikipedia/wikipedia.js @@ -0,0 +1,51 @@ +'use strict'; + +var needle = require('needle'); + +var urlService = 'http://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&exchars=200&explaintext&titles=Computer%20science'; +//http://www.mediawiki.org/wiki/API:Search +/* + * 1. try to get title right away: http://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&titles=Computer%20science + * if success add it in comment, else issue search: + * 2. http://en.wikipedia.org/w/api.php?format=json&action=query&list=search&srwhat=text&srsearch=cs&srlimit=3 + * get snippets and add comments + * + */ + +function search(text) { + var ret = requestTitle(text); + if(!ret) ret = searchText(text); +} + +function requestTitle(title, cbDone, cbFail) { + needle.get('http://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&exchars=200&explaintext&titles=' + encodeURI(title), + function handleResponse(error, response, body) { + obj = JSON.parse(body); + if(error || response.statusCode != 200 || !obj || !obj.query || !obj.query.pages || obj.query.pages['-1']) { + searchText(title, cbDone, cbFail); + } else { + var pgs = obj.query.pages; + for(var el in pgs) console.log('found: ' + pgs[el].title); + } + } + ); +} + +function searchText(text, cbDone, cbFail) { + needle.get('http://en.wikipedia.org/w/api.php?format=json&action=query&list=search&srwhat=text&srlimit=3&srsearch=' + encodeURI(text), + function handleResponse(error, response, body) { + obj = JSON.parse(body); + if(error || response.statusCode != 200 || !obj || !obj.query || !obj.query.search || obj.query.search.length == 0) { + console.log('nothing found for this tag'); + if(cbFail) cbFail('nothing found for this tag'); + } else { + var srch = obj.query.search; + for(var i = 0; i < srch.length; i++) { + console.log('found: ' + srch[i].title + ' [' + srch[i].snippet + ']'); + if(cbDone) cbDone('found: ' + srch[i].title + ' [' + srch[i].snippet + ']'); + } + } + } + ); + +} diff --git a/mod_events/emailyak/emailyak.js b/mod_events/emailyak/emailyak.js new file mode 100644 index 0000000..7518e99 --- /dev/null +++ b/mod_events/emailyak/emailyak.js @@ -0,0 +1,57 @@ +'use strict'; + +var needle = require('request'); + +/* + * EmailYak EVENT MODULE + */ + +var credentials = null; + +function loadCredentials(cred) { + if(!cred || !cred.key) { + console.error('ERROR in EmailYak EM: credentials file corrupt'); + } else { + credentials = cred; + console.log('Successfully loaded EmailYak EM credentials'); + } +} + +//FIXME every second mail gets lost? +// 1) for http.request options, set Connection:keep-alive +// 2) set Agent.maxSockets = 1024 (so more connection to play around +// with ) +// 3) very critical: DO a timeout for the http.request. +// +// e.g. +// var responseHdr = function (clientResponse) { + // if (clientResposne) { +// + // } else { + // clientRequest.abort(); + // } +// }; +// +// var timeoutHdr = setTimeout(function() { + // clientRequest.emit('req-timeout'); +// }, 5000); // timeout after 5 secs +// +// clientRequest.on("req-timeout", responseHdr); +// clientRequest.on('error', function(e) { + // clearTimeout(timeoutHdr); + // console.error('Ok.. clientrequest error' + myCounter); + // next({err:JSON.stringify(e)}); +// }); +function newMail(callback) { //FIXME not beautiful to have to set prop each time here + needle.get('https://api.emailyak.com/v1/' + credentials.key + '/json/get/new/email/', + function (error, response, body){ + if (!error && response.statusCode == 200) { + var mails = JSON.parse(body).Emails; + for(var i = 0; i < mails.length; i++) callback(mails[i]); + } else console.error('ERROR in EmailYak EM newMail: ' + error); + } + ); +} + +exports.loadCredentials = loadCredentials; +exports.newMail = newMail; diff --git a/mod_events/probinder/probinder.js b/mod_events/probinder/probinder.js new file mode 100644 index 0000000..1e0c3d7 --- /dev/null +++ b/mod_events/probinder/probinder.js @@ -0,0 +1,75 @@ +'use strict'; + +var needle = require('needle'); + +/* + * ProBinder EVENT MODULE + */ + +var request = require('needle'), + urlService = 'https://probinder.com/service/', + credentials = null; + +function loadCredentials(cred) { + if(!cred || !cred.username || !cred.password) { + console.error('ERROR: ProBinder EM credentials file corrupt'); + } else { + credentials = cred; + console.log('Successfully loaded credentials for ProBinder EM'); + } +} + +/** + * Call the ProBinder service with the given parameters. + * @param {Object} args the required function arguments object + * @param {Object} [args.data] the data to be posted + * @param {String} args.service the required service identifier to be appended to the url + * @param {String} args.method the required method identifier to be appended to the url + * @param {function} [args.succes] the function to be called on success, + * receives the response body String or Buffer. + * @param {function} [args.error] the function to be called on error, + * receives an error, an http.ClientResponse object and a response body + * String or Buffer. + */ +function call(args) { + if(!args || !args.service || !args.method) { + console.error('ERROR in ProBinder EM call: Too few arguments!'); + return null; + } + if(credentials){ + needle.post(urlService + args.service + '/' + args.method, + args.data, + credentials, + function(error, response, body) { // The callback + if (!error) { //) && response.statusCode == 200) { + if(args && args.success) args.success(body); + } else { + if(args && args.error) args.error(error, response, body); + else console.error('ERROR during ProBinder EM call: ' + error.message); + } + } + ); + } else console.error('ProBinder EM request or credentials object not ready!'); +}; + +/** + * Calls the user's unread content service. + * @param {Object} [args] the optional object containing the success + * and error callback methods + */ +function unread(callback) { //FIXME ugly prop in here + + call({ + service: '36', + method: 'unreadcontent', + success: function(data) { + for(var i = 0; i < data.length; i++) callback(null, data[i]); + } + }); + +}; + +exports.loadCredentials = loadCredentials; +exports.call = call; +exports.unread = unread; + diff --git a/mod_events/testing/test.json b/mod_events/testing/test.json new file mode 100644 index 0000000..2d284c2 --- /dev/null +++ b/mod_events/testing/test.json @@ -0,0 +1,3 @@ +{ + "test": "should stay" +} diff --git a/mod_events/testing/test.json_BACKUP b/mod_events/testing/test.json_BACKUP new file mode 100644 index 0000000..2d284c2 --- /dev/null +++ b/mod_events/testing/test.json_BACKUP @@ -0,0 +1,3 @@ +{ + "test": "should stay" +} diff --git a/mod_events/testing/testing.js b/mod_events/testing/testing.js new file mode 100644 index 0000000..aaeaaf6 --- /dev/null +++ b/mod_events/testing/testing.js @@ -0,0 +1,20 @@ +var fs = require('fs'), + path = require('path'); + +/* +// Hacking my own system... + console.log(module.parent.parent.children[0].exports.getEventModuleAuth('probinder', + function(err, obj) {console.log(obj);})); +*/ + +//FIXME do not try to delete a file and rely on it to exist, rather try to create it first! o check for its existance and then delete it +try { + fs.unlinkSync(path.resolve(__dirname, 'event_modules', 'malicious', 'test.json')); + console.error('VERY BAD! NEVER START THIS SERVER WITH A USER THAT HAS WRITE RIGHTS ANYWHERE!!!'); +} catch (err) { + console.log('VERY GOOD! USERS CANNOT WRITE ON YOUR DISK!'); + +} + +//FIXME add several standard methods for testing (also rules to be inserted during testing) +throw new Error('Testing your error handling'); diff --git a/mod_events/weather/weather.js b/mod_events/weather/weather.js new file mode 100644 index 0000000..da57fcd --- /dev/null +++ b/mod_events/weather/weather.js @@ -0,0 +1,31 @@ + +var needle = require('needle'); + +var urlService = 'http://api.openweathermap.org/data/2.5/weather', + credentials, + old_temp; + +function loadCredentials(cred) { + if(!cred || !cred.key) { + console.error('ERROR in Weather EM: Weather event module credentials file corrupt'); + } else { + credentials = cred; + console.log('Successfully loaded credentials for Weather EM'); + } +} + +function tempRaisesAbove(prop, degree) { + needle.get(urlService + '?APPID=' + credentials.key + '&q=Basel', + function(error, response, body) { // The callback + if (!error) { //) && response.statusCode == 200) { + if(args && args.success) args.success(body); + } else { + if(args && args.error) args.error(error, response, body); + else console.error('Error during Weather EM tempRaisesAbove: ' + error.message); + } + } + ); +} + +exports.tempRaisesAbove = tempRaisesAbove; +exports.loadCredentials = loadCredentials; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b47fff2 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "webapi-eca", + "author": "Dominic Bosch", + "description": "An ECA inference engine as a middleware between Web API's", + "version": "0.1.0", + "private": true, + "repository": { + "type" : "git", + "url" : "https://github.com/dominicbosch/webapi-eca.git" + }, + "dependencies": { + "express": "3.4.0", + "groc": "0.6.1", + "needle": "0.6.1", + "nodeunit": "0.8.2", + "redis": "0.9.0", + "request": "2.27.0" + }, + "__comment": { + "dependencies": { + "diff": "1.0.5", + "socket.io": "0.9.16", + "contextify": "0.1.6" + } + } +} \ No newline at end of file diff --git a/rules/rules.json b/rules/rules.json new file mode 100644 index 0000000..74f720e --- /dev/null +++ b/rules/rules.json @@ -0,0 +1,73 @@ +[ + { + "id": "rule_1", + "event": "mail", + "actions": [ + { + "module": "probinder->newContent", + "arguments": { + "content": "Rule#1: $X.subject" + } + } + ] + }, + { + "id": "rule_2", + "event": "mail", + "condition": { "sender": "sender2" }, + "actions": [ + { + "module": "probinder->newContent", + "arguments": { + "content": "Rule#2: $X.subject" + } + } + ] + }, + { + "id": "rule_emailyak", + "event": "yakmail", + "condition": { "FromAddress": "dominic.bosch.db@gmail.com" }, + "actions": [ + { + "module": "probinder->newContent", + "arguments": { + "content": "Received from EmailYak: $X.textbody" + } + } + ] + }, + { + "id": "rule_pull_emailyak", + "event": "emailyak->newMail", + "condition": { "FromAddress": "dominic.bosch.db@gmail.com" }, + "actions": [ + { + "module": "probinder->newContent", + "arguments": { + "content": "Received from EmailYak: $X.textbody" + } + } + ] + }, + { + "id": "rule_pull_probinder", + "event": "probinder->unread", + "condition": { "serviceId": "32" }, + "actions": [ + { + "module": "probinder->makeFileEntry", + "arguments": { + "service": "$X.serviceId", + "id": "$X.id" + } + }, + { + "module": "probinder->setRead", + "arguments": { + "id": "$X.id" + } + } + ] + } +] \ No newline at end of file diff --git a/run_server.sh b/run_server.sh new file mode 100755 index 0000000..57b37df --- /dev/null +++ b/run_server.sh @@ -0,0 +1,3 @@ +#!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +node $DIR/js/webapi_eca_server > $DIR/server.log 2>&1 & diff --git a/run_tests.js b/run_tests.js new file mode 100644 index 0000000..74e1e68 --- /dev/null +++ b/run_tests.js @@ -0,0 +1 @@ +require('nodeunit').reporters.default.run(['testing']); \ No newline at end of file diff --git a/testing/mod_db_interface.js b/testing/mod_db_interface.js new file mode 100644 index 0000000..747bee6 --- /dev/null +++ b/testing/mod_db_interface.js @@ -0,0 +1,270 @@ + +exports.testUnit_DB = function(test){ + test.ok(true, "db"); + test.done(); +}; + +// // # 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; +// +// +// // @function init() +// +// /* + // * Initializes the DB connection. Requires a port where the DB listens to requests + // * and a key that is used for encryptions. + // * @param {int} db_port + // */ +// exports.init = function(db_port, key, cbDone){ + // if(!db_port || !key) { + // log.error('DB', 'No DB port or cipher key defined!'); + // return null; + // } + // crypto_key = key; + // db = redis.createClient(db_port); + // db.on("error", function (err) { + // log.error('DB', ' Message from DB: ' + err); + // }); + // if(cbDone) cbDone(); +// }; +// +// /** + // * ### encrypt + // */ +// 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) { + // 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: ' + 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} callback the function to be called on success or error, receives + // * arguments (err, obj) + // */ +// function getSetRecords(set, funcSingle, callback) { + // db.smembers(set, function(err, reply) { + // if(err) log.error('DB', 'fetching ' + set + ': ' + err); + // else { + // if(reply.length === 0) { + // callback(null, null); + // } else { + // var semaphore = reply.length, objReplies = {}; + // setTimeout(function() { + // if(semaphore > 0) { + // callback('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) callback(null, objReplies); + // } + // }; + // }(reply[i])); + // } + // } + // } + // }); +// } +// +// // @method shutDown() +// +// // Shuts down the db link. +// exports.shutDown = function() { 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) { + // db.sadd('action_modules', id, replyHandler('storing action module key ' + id)); + // db.set('action_module_' + id, data, replyHandler('storing action module ' + id)); +// }; +// +// /** + // * ### getActionModule(id, callback) + // * Query the DB for an action module. + // * @param {String} id the module id + // * @param {function} callback the callback to receive the answer (err, obj) + // */ +// exports.getActionModule = function(id, callback) { + // if(callback) db.get('action_module_' + id, callback); +// }; +// +// /** + // * ### getActionModules(callback) + // * Fetch all action modules. + // * @param {function} callback the callback to receive the answer (err, obj) + // */ +// exports.getActionModules = function(callback) { + // getSetRecords('action_modules', exports.getActionModule, callback); +// }; +// +// /** + // * 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.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, callback) + // * Query the DB for an action module authentication token. + // * @param {String} id the module id + // * @param {function} callback the callback to receive the answer (err, obj) + // */ +// exports.getActionModuleAuth = function(id, callback) { + // if(callback) db.get('action_module_' + id + '_auth', function(err, txt) { callback(err, decrypt(txt)); }); +// }; +// +// // ## 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) { + // db.sadd('event_modules', id, replyHandler('storing event module key ' + id)); + // db.set('event_module_' + id, data, replyHandler('storing event module ' + id)); +// }; +// +// /** + // * ### getEventModule(id, callback) + // * Query the DB for an event module. + // * @param {String} id the module id + // * @param {function} callback the callback to receive the answer (err, obj) + // */ +// exports.getEventModule = function(id, callback) { + // if(callback) db.get('event_module_' + id, callback); +// }; +// +// /** + // * ### getEventModules(callback) + // * Fetch all event modules. + // * @param {function} callback the callback that receives the arguments (err, obj) + // */ +// exports.getEventModules = function(callback) { + // getSetRecords('event_modules', exports.getEventModule, callback); +// }; +// +// /** + // * ### 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.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, callback) +// +// // Query the DB for an event module authentication token. +// // @param {String} id the module id +// // @param {function} callback the callback to receive the answer (err, obj) +// exports.getEventModuleAuth = function(id, callback) { + // if(callback) db.get('event_module_' + id +'_auth', function(err, txt) { callback(err, decrypt(txt)); }); +// }; +// +// // ## 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) { + // db.sadd('rules', id, replyHandler('storing rule key ' + id)); + // db.set('rule_' + id, data, replyHandler('storing rule ' + id)); +// }; +// +// // @method getRule(id, callback) +// +// // Query the DB for a rule. +// // @param {String} id the rule id +// // @param {function} callback the callback to receive the answer (err, obj) +// exports.getRule = function(id, callback) { + // db.get('rule_' + id, callback); +// }; +// +// // @method getRules(callback) +// +// // Fetch all rules from the database. +// // @param {function} callback the callback to receive the answer (err, obj) +// exports.getRules = function(callback) { + // getSetRecords('rules', exports.getRule, callback); +// }; +// diff --git a/testing/mod_engine.js b/testing/mod_engine.js new file mode 100644 index 0000000..72c7fb4 --- /dev/null +++ b/testing/mod_engine.js @@ -0,0 +1,282 @@ + + exports.setUp = function (callback) { + console.log('setup in module'); + callback(); + }; +exports.testUnit_ENG = function(test){ + test.ok(true, "ENG"); + test.done(); +}; + +exports.group = { + setUp: function (callback) { + console.log('setup in group'); + callback(); + }, + test2: function (test) { + test.ok(true, "ENG"); + test.done(); + }, + test3: function (test) { + test.ok(true, "ENG"); + test.done(); + } +}; + +exports.testUnit_Engine = function(test){ + test.ok(true, "engine"); + test.done(); +}; + + +// 'use strict'; +// +// var cp = require('child_process'), ml = require('./module_loader'), + // log = require('./logging'), + // poller, db, isRunning = true, + // qEvents = new (require('./queue')).Queue(); // export queue into redis +// +// var regex = /\$X\.[\w\.\[\]]*/g, // find properties of $X + // listRules = {}, + // listActionModules = {}, + // actionsLoaded = false, eventsLoaded = false; +// /* + // * 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 + // */ +// function init(db_link, db_port, crypto_key) { + // db = db_link; + // loadActions(); + // poller = cp.fork('./eventpoller', [db_port, crypto_key]); + // poller.on('message', function(evt) { + // if(evt.event === 'ep_finished_loading') { + // eventsLoaded = true; + // tryToLoadRules(); + // } else pushEvent(evt); + // }); + // //start to poll the event queue + // pollQueue(); +// } +// +// function loadActions() { + // 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!'); + // actionsLoaded = true; + // tryToLoadRules(); + // } else { + // var m, semaphore = 0; + // for(var el in obj) { + // semaphore++; + // log.print('EN', 'Loading Action Module from DB: ' + el); + // m = ml.requireFromString(obj[el], el); + // db.getActionModuleAuth(el, function(mod) { + // return function(err, obj) { + // if(--semaphore == 0) { + // actionsLoaded = true; + // tryToLoadRules(); + // } + // if(obj && mod.loadCredentials) mod.loadCredentials(JSON.parse(obj)); + // }; + // }(m)); + // listActionModules[el] = m; + // } + // } + // } + // }); +// } +// +// function tryToLoadRules() { + // if(eventsLoaded && actionsLoaded) { + // db.getRules(function(err, obj) { + // for(var el in obj) loadRule(JSON.parse(obj[el])); + // }); + // } +// } +// +// /** + // * Insert an action module into the list of available interfaces. + // * @param {Object} objModule the action module object + // */ +// function loadActionModule(name, objModule) { + // log.print('EN', 'Action module "' + name + '" loaded'); + // listActionModules[name] = objModule; +// } +// +// /** + // * Insert a rule into the eca rules repository + // * @param {Object} objRule the rule object + // */ +// function loadRule(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 + // */ +// function pushEvent(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.data, 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.data, 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; + // } + // } +// } +// +// function loadEventModule(args, answHandler) { + // if(args && args.name) { + // answHandler.answerSuccess('Loading event module ' + args.name + '...'); + // poller.send('cmd|loadevent|'+args.name); + // } else if(args) answHandler.answerError(args.name + ' not found'); +// } +// +// function loadEventModules(args, answHandler) { + // answHandler.answerSuccess('Loading event moules...'); + // poller.send('cmd|loadevents'); +// } +// +// function shutDown() { + // log.print('EN', 'Shutting down Poller and DB Link'); + // isRunning = false; + // poller.send('cmd|shutdown'); + // db.shutDown(); +// } +// +// exports.init = init; +// exports.loadActionModule = loadActionModule; +// exports.loadRule = loadRule; +// exports.loadEventModule = loadEventModule; +// exports.loadEventModules = loadEventModules; +// exports.pushEvent = pushEvent; +// exports.shutDown = shutDown; diff --git a/testing/mod_eventpoller.js b/testing/mod_eventpoller.js new file mode 100644 index 0000000..32a20e7 --- /dev/null +++ b/testing/mod_eventpoller.js @@ -0,0 +1,144 @@ + +exports.testUnit_EventPoller = function(test){ + test.ok(true, "ep"); + test.done(); +}; + +// // # Event Poller +// +// 'use strict'; +// +// if(process.argv.length < 3) { + // log.error('EP', 'No DB port defined! Not starting poller...'); +// } else { + // (function() { + // var fs = require('fs'), + // path = require('path'), + // log = require('./logging'), + // db = require('./db_interface'), + // ml = require('./module_loader'), + // 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; + // //TODO allow different polling intervals (a wrapper together with settimeout per to be polled could be an easy and solution) +// + // db.init(process.argv[2], process.argv[3]); +// + // //TODO eventpoller will not load event modules from filesystem, this will be done by + // // the moduel manager and the eventpoller receives messages about new/updated active rules +// + // db.getEventModules(function(err, obj) { + // if(err) log.error('EP', 'retrieving Event Modules from DB!'); + // else { + // if(!obj) { + // log.print('EP', 'No Event Modules found in DB!'); + // process.send({ event: 'ep_finished_loading' }); + // } else { + // var m, semaphore = 0; + // for(var el in obj) { + // semaphore++; + // m = ml.requireFromString(obj[el], el); + // db.getEventModuleAuth(el, function(mod) { + // return function(err, obj) { + // if(--semaphore === 0) process.send({ event: 'ep_finished_loading' }); + // if(obj && mod.loadCredentials) mod.loadCredentials(JSON.parse(obj)); + // }; + // }(m)); + // log.print('EP', 'Loading Event Module: ' + el); + // listEventModules[el] = m; + // } + // } + // } + // }); +// + // listMessageActions['event'] = function(args) { + // var prop = args[1], arrModule = prop.split('->'); + // // var arrModule = obj.module.split('->'); + // if(arrModule.length > 1){ + // var module = listEventModules[arrModule[0]]; + // for(var i = 1; i < arrModule.length; i++) { + // if(module) module = module[arrModule[i]]; + // } + // if(module) { + // log.print('EP', 'Found active event module "' + prop + '", adding it to polling list'); + // listPoll[prop] = module; + // } else { + // log.print('EP', 'No property "' + prop + '" found'); + // } + // } + // }; +// + // listAdminCommands['loadevent'] = function(args) { + // ml.loadModule('mod_events', args[2], loadEventCallback); + // }; +// + // listAdminCommands['loadevents'] = function(args) { + // ml.loadModules('mod_events', loadEventCallback); + // }; +// + // listAdminCommands['shutdown'] = function(args) { + // log.print('EP', 'Shutting down DB Link'); + // isRunning = false; + // db.shutDown(); + // }; +// + // //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 loadEventCallback(name, data, mod, auth) { + // db.storeEventModule(name, data); // store module in db + // if(auth) db.storeEventModuleAuth(name, auth); + // listEventModules[name] = mod; // store compiled module for polling + // } +// + // function checkRemotes() { + // var txt = 'Polled active event modules: '; + // for(var prop in listPoll) { + // txt += prop + ', '; + // listPoll[prop]( + // /* + // * what a hack to get prop local :-P + // * 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(obj) { + // process.send({ + // event: p, + // eventid: 'polled_' + eId++, + // data: obj + // }); + // }; + // })(prop) + // ); + // } + // log.print('EP', txt); + // } +// + // function pollLoop() { + // if(isRunning) { + // checkRemotes(); + // setTimeout(pollLoop, 10000); + // } + // } +// + // pollLoop(); + // })(); +// } \ No newline at end of file diff --git a/testing/mod_http_listener.js b/testing/mod_http_listener.js new file mode 100644 index 0000000..614cbbd --- /dev/null +++ b/testing/mod_http_listener.js @@ -0,0 +1,110 @@ + +exports.testUnit_HL = function(test){ + test.ok(true, "hl"); + test.done(); +}; + +// // # HTTP Listener +// // Isso +// 'use strict'; +// var express = require('express'), + // port = express(), + // log = require('./logging'), + // qs = require('querystring'), + // adminHandler, eventHandler, server; +// +// function init(http_port, funcAdminHandler, funcEvtHandler) { + // if(!http_port || !funcEvtHandler) { + // log.error('HL', 'ERROR: either port or eventHandler function not defined!'); + // return; + // } + // adminHandler = funcAdminHandler; + // eventHandler = funcEvtHandler; + // // port.get('/doc*', onDocRequest); + // port.use('/doc', express.static(__dirname + '/../doc')); + // port.use('/doc', express.static(__dirname + '/../doc-na')); + // port.get('/admin', onAdminCommand); + // port.post('/pushEvents', onPushEvent); + // server = port.listen(http_port); // inbound event channel + // log.print('HL', 'Started listening for http requests on port ' + http_port); +// } +// +// 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 onDocRequest(request, response) { + // // var pth = request.url; + // // pth = pth.substring(4); + // // if(pth.substring(pth.length-1) === '/') pth += 'index.html'; + // // console.log(pth); +// // } +// +// /** + // * Handles correct event posts, replies thank you. + // */ +// function answerSuccess(resp, msg){ + // resp.writeHead(200, { "Content-Type": "text/plain" }); + // resp.write(msg); + // resp.end(); +// } +// +// /** + // * Handles erroneous requests. + // * @param {Object} msg the error message to be returned + // */ +// function answerError(resp, msg) { + // resp.writeHead(400, { "Content-Type": "text/plain" }); + // resp.write(msg); + // resp.end(); +// } +// +// //FIXME this answer handling is a very ugly hack, improve! +// 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...'); +// } +// +// /** + // * If a request is made to the server, this function is used to handle it. + // */ +// function onPushEvent(request, response) { + // var body = ''; + // request.on('data', function (data) { body += data; }); + // request.on('end', function () { + // var obj = qs.parse(body); + // /* If required event properties are present we process the event */ + // if(obj && obj.event && obj.eventid){ + // answerSuccess(response, 'Thank you for the event (' + obj.event + '[' + obj.eventid + '])!'); + // eventHandler(obj); + // } else answerError(response, 'Your event was missing important parameters!'); + // }); +// } +// +// exports.init = init; +// exports.shutDown = function() { + // log.print('HL', 'Shutting down HTTP listener'); + // process.exit(); // This is a bit brute force... +// }; diff --git a/testing/mod_logging.js b/testing/mod_logging.js new file mode 100644 index 0000000..f444277 --- /dev/null +++ b/testing/mod_logging.js @@ -0,0 +1,32 @@ + +exports.testUnit_LOG = function(test){ + test.ok(true, "log"); + test.done(); +}; + +// /* + // * # Logging + // * Functions to funnel logging + // */ +// +// // @function print(module, msg) +// +// /* + // * Prints a log to stdout. + // * @param {String} module + // * @param {String} msg + // */ +// exports.print = function(module, msg) { + // console.log((new Date()).toISOString() + ' | ' + module + ' | ' + msg); +// }; +// +// // @function error(module, msg) +// +// /* + // * Prints a log to stderr. + // * @param {String} module + // * @param {String} msg + // */ +// exports.error = function(module, msg) { + // console.error((new Date()).toISOString() + ' | ' + module + ' | ERROR: ' + msg); +// }; diff --git a/testing/mod_module_loader.js b/testing/mod_module_loader.js new file mode 100644 index 0000000..d7aa65d --- /dev/null +++ b/testing/mod_module_loader.js @@ -0,0 +1,74 @@ + +exports.testUnit_ML = function(test){ + test.ok(true, "ml"); + test.done(); +}; + +// var fs = require('fs'), + // path = require('path'), + // log = require('./logging'); +// +// function requireFromString(src, name, dir) { + // if(!dir) dir = __dirname; + // // YAH yet another hack, this time to load modules from strings + // var id = path.resolve(dir, name, name + '.js'); + // var m = new module.constructor(id, module); + // m.paths = module.paths; + // try { + // m._compile(src); + // } catch(err) { + // log.error('LM', ' during compilation of ' + name + ': ' + err); + // } + // return m.exports; +// } +// +// function loadModule(directory, name, callback) { + // //FIXME contextualize and only allow small set of modules for safety reasons + // try { + // fs.readFile(path.resolve(directory, name, name + '.js'), 'utf8', function (err, data) { + // if (err) { + // log.error('LM', 'Loading module file!'); + // return; + // } + // var mod = requireFromString(data, name, directory); + // if(mod && fs.existsSync(path.resolve(directory, name, 'credentials.json'))) { + // fs.readFile(path.resolve(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 + '"'); + // } +// } +// +// function loadModules(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()) { + // loadModule(path.resolve(__dirname, directory), file, callback); + // } + // }); + // }); + // }); +// } +// +// exports.loadModule = loadModule; +// exports.loadModules = loadModules; +// exports.requireFromString = requireFromString; +// diff --git a/testing/mod_module_manager.js b/testing/mod_module_manager.js new file mode 100644 index 0000000..622ab7e --- /dev/null +++ b/testing/mod_module_manager.js @@ -0,0 +1,103 @@ + +exports.testUnit_MM = function(test){ + test.ok(true, "mm"); + test.done(); +}; + +// /* +// # 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 and rules + // */ +// var fs = require('fs'), + // path = require('path'), + // log = require('./logging'), + // ml = require('./module_loader'), + // db = null, funcLoadAction, funcLoadRule; +// +// function init(db_link, fLoadAction, fLoadRule) { + // db = db_link; + // funcLoadAction = fLoadAction; + // funcLoadRule = fLoadRule; +// } +// /* +// # A First Level Header +// +// +// A Second Level Header +// --------------------- +// +// Now is the time for all good men to come to +// the aid of their country. This is just a +// regular paragraph. +// +// The quick brown fox jumped over the lazy +// dog's back. +// +// ### Header 3 +// +// > This is a blockquote. +// > +// > This is the second paragraph in the blockquote. +// > +// > ## This is an H2 in a blockquote +// +// This is the function documentation +// @param {Object} [args] the optional arguments +// @param {String} [args.name] the optional name in the arguments + // */ +// function loadRulesFile(args, answHandler) { + // //FIXME if a corrupt rule file is read the system crashes, prevent this also for event and action modules + // 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; + // } + // 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); + // }); + // } +// } +// +// /** + // * + // * @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 compiled module back + // if(auth) db.storeActionModuleAuth(name, auth); +// } +// +// function loadActionModule(args, answHandler) { + // if(args && args.name) { + // answHandler.answerSuccess('Loading action module ' + args.name + '...'); + // ml.loadModule('mod_actions', args.name, loadActionCallback); + // } +// } +// +// function loadActionModules(args, answHandler) { + // answHandler.answerSuccess('Loading action modules...'); + // ml.loadModules('mod_actions', loadActionCallback); +// } +// +// exports.init = init; +// exports.loadRulesFile = loadRulesFile; +// exports.loadActionModule = loadActionModule; +// exports.loadActionModules = loadActionModules; \ No newline at end of file diff --git a/testing/mod_server.js b/testing/mod_server.js new file mode 100644 index 0000000..3a5ce50 --- /dev/null +++ b/testing/mod_server.js @@ -0,0 +1,65 @@ +exports.setUp = function() { + // this.srv = require('../js/webapi_eca_server.js'); +}; + +exports.testUnit_SRV = function(test){ + console.log(this.srv); + test.ok(true, "SRV"); + test.done(); +}; +// var http_listener = require('./http_listener'), + // db = require('./db_interface'), + // engine = require('./engine'), + // mm = require('./module_manager'), + // log = require('./logging'), + // fs = require('fs'), + // path = require('path'), + // objCmds = { + // 'loadrules': mm.loadRulesFile, + // 'loadaction': mm.loadActionModule, + // 'loadactions': mm.loadActionModules, + // 'loadevent': engine.loadEventModule, + // 'loadevents': engine.loadEventModules, + // 'shutdown': shutDown, + // 'restart': null //TODO implement + // }; +// +// function handleAdminCommands(args, answHandler) { + // if(args && args.cmd) { + // var func = objCmds[args.cmd]; + // if(func) func(args, answHandler); + // } else log.print('RS', 'No command in request'); + // setTimeout(function(ah) { + // answHandler = ah; + // return function() { + // if(!answHandler.isAnswered()) answHandler.answerError('Not handeled...'); + // }; + // }, 2000); +// } +// +// function shutDown(args, answHandler) { + // answHandler.answerSuccess('Goodbye!'); + // log.print('RS', 'Received shut down command!'); + // engine.shutDown(); + // http_listener.shutDown(); +// } +// +// fs.readFile(path.resolve(__dirname, 'config', 'config.json'), 'utf8', function (err, data) { + // if (err) { + // log.error('RS', 'Loading config file'); + // return; + // } + // var config = JSON.parse(data); + // if(!config.http_port || !config.db_port || !config.crypto_key) { + // log.error('RS', 'you forgot to define either http_port, db_port, crypto_key, or even all of them!'); + // } else { + // log.print('RS', 'Initialzing DB'); + // db.init(config.db_port, config.crypto_key, function() { + // engine.init(db, config.db_port, config.crypto_key); + // }); + // log.print('RS', 'Initialzing http listener'); + // http_listener.init(config.http_port, handleAdminCommands, engine.pushEvent); + // log.print('RS', 'Initialzing module manager'); + // mm.init(db, engine.loadActionModule, engine.loadRule); + // } +// }); \ No newline at end of file diff --git a/testing/unit_integration.js b/testing/unit_integration.js new file mode 100644 index 0000000..2beb5d5 --- /dev/null +++ b/testing/unit_integration.js @@ -0,0 +1,5 @@ + +exports.testUnitIntegration = function(test){ + test.ok(true, "unit integration"); + test.done(); +}; diff --git a/testing/whole_system.js b/testing/whole_system.js new file mode 100644 index 0000000..969d34e --- /dev/null +++ b/testing/whole_system.js @@ -0,0 +1,5 @@ + +exports.testSystem = function(test){ + test.ok(true, "system"); + test.done(); +};