diff --git a/README.md b/README.md index 75766ad..fbea956 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,28 @@ README: webapi-eca ================== +# TODO Remake + >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`. +>The server is started through the [server.js](server.html) module by calling `node rule_server.js`. Getting started --------------- -Prerequisites: - - node.js & npm (find it [here](http://nodejs.org/)) - - *(optional) coffee, if you want to compile from coffee sources:* +**Prerequisites:** + + - node.js (find it [here](http://nodejs.org/)) + - *(optional) [CoffeeScript](http://coffeescript.org/), if you want to compile from coffee sources:* - sudo npm -g install coffee-script + sudo npm -g install coffee-script Clone project: - git clone https://github.com/dominicbosch/webapi-eca.git + git clone https://github.com/dominicbosch/webapi-eca.git Download and install dependencies: @@ -33,7 +34,7 @@ Get your [redis](http://redis.io/) instance up and running (and find the port fo Edit the configuration file: vi config/config.json - + Apply your settings, for example: { @@ -49,15 +50,17 @@ Start the 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 + +Run test suite: + + node run_tests _ diff --git a/coffee/config.coffee b/coffee/config.coffee index 8272926..1919f11 100644 --- a/coffee/config.coffee +++ b/coffee/config.coffee @@ -1,17 +1,20 @@ ### -Config -====== +Configuration +============= > Loads the configuration file and acts as an interface to it. ### +# **Requires:** + +# - [Logging](logging.html) +log = require './logging' + +# - Node.js Modules: [fs](http://nodejs.org/api/fs.html) and +# [path](http://nodejs.org/api/path.html) fs = require 'fs' path = require 'path' -# Requires: - -# - The [Logging](logging.html) module -log = require './logging' ### ##Module call @@ -24,7 +27,7 @@ exports = module.exports = ( args ) -> args = args ? {} log args if typeof args.relPath is 'string' - loadConfigFiles args.relPath + loadConfigFile args.relPath module.exports ### @@ -61,35 +64,35 @@ Fetch a property from the configuration fetchProp = ( prop ) => @config?[prop] ### -Answer true if the config file is ready, else false +***Returns*** true if the config file is ready, else false @public isReady() ### exports.isReady = => @config? ### -Returns the HTTP port +***Returns*** the HTTP port @public getHttpPort() ### exports.getHttpPort = -> fetchProp 'http_port' ### -Returns the DB port +***Returns*** the DB port* @public getDBPort() ### exports.getDBPort = -> fetchProp 'db_port' ### -Returns the crypto key +***Returns*** the crypto key @public getCryptoKey() ### exports.getCryptoKey = -> fetchProp 'crypto_key' ### -Returns the session secret +***Returns*** the session secret @public getSessionSecret() ### diff --git a/coffee/db_interface.coffee b/coffee/db_interface.coffee index 6d7ce89..ea6b541 100644 --- a/coffee/db_interface.coffee +++ b/coffee/db_interface.coffee @@ -19,14 +19,15 @@ DB Interface ### -redis = require 'redis' -crypto = require 'crypto' # TODO change to Google's "crypto-js"" +# **Requires:** -# Requires: - -# - The [Logging](logging.html) module +# - [Logging](logging.html) log = require './logging' +# - External Modules: [crypto-js](https://github.com/evanvosberg/crypto-js) and +# [redis](https://github.com/mranney/node_redis) +crypto = require 'crypto-js' +redis = require 'redis' ### Module call @@ -54,7 +55,7 @@ ten attempts within five seconds, or nothing on success to the callback(err). @public isConnected( *cb* ) @param {function} cb ### -#}TODO check if timeout works with func in func +#TODO check if timeout works with func in func exports.isConnected = ( cb ) => if @db.connected then cb() else @@ -72,6 +73,22 @@ exports.isConnected = ( cb ) => setTimeout fCheckConnection, 500 +### +Hashes a string based on SHA-3-512. + +@private hash( *plainText* ) +@param {String} plainText +### +hash = ( plainText ) => + if !plainText? then return null + try + (crypto.SHA3 plainText, { outputLength: 512 }).toString() + catch err + err.addInfo = 'during hashing' + log.error 'DB', err + null + + ### Encrypts a string using the crypto key from the config file, based on aes-256-cbc. @@ -140,7 +157,7 @@ getSetRecords = ( set, fSingle, cb ) => else semaphore = arrReply.length objReplies = {} - # } TODO What if the DB needs longer than two seconds to respond?... + #TODO What if the DB needs longer than two seconds to respond?... setTimeout -> if semaphore > 0 cb new Error('Timeout fetching ' + set) @@ -206,7 +223,7 @@ Store a string representation of the authentication parameters for an action mod ### exports.storeActionAuth = ( userId, moduleId, data ) => log.print 'DB', 'storeActionAuth: ' + userId + ':' + moduleId - @db.set 'action-auth:' + userId + ':' + moduleId, encrypt(data), + @db.set 'action-auth:' + userId + ':' + moduleId, hash(data), replyHandler 'storing action auth ' + userId + ':' + moduleId ### @@ -270,7 +287,7 @@ Store a string representation of he authentication parameters for an event modul ### exports.storeEventAuth = ( userId, moduleId, data ) => log.print 'DB', 'storeEventAuth: ' + userId + ':' + moduleId - @db.set 'event-auth:' + userId + ':' + moduleId, encrypt(data), + @db.set 'event-auth:' + userId + ':' + moduleId, hash(data), replyHandler 'storing event auth ' + userId + ':' + moduleId ### @@ -331,12 +348,12 @@ Store a user object (needs to be a flat structure). @param {Object} objUser ### exports.storeUser = ( objUser ) => - # TODO Only store user if not already existing, or at least only then add a private key - # for his encryption. we would want to have one private key per user, right? + #TODO Only store user if not already existing, or at least only then add a private key + #for his encryption. we would want to have one private key per user, right? log.print 'DB', 'storeUser: ' + objUser.username if objUser and objUser.username and objUser.password @db.sadd 'users', objUser.username, replyHandler 'storing user key ' + objUser.username - objUser.password = encrypt objUser.password + objUser.password = hash objUser.password @db.hmset 'user:' + objUser.username, objUser, replyHandler 'storing user properties ' + objUser.username else log.error 'DB', new Error 'username or password was missing' @@ -374,14 +391,17 @@ exports.getRoleUsers = ( role ) => @db.get 'role-users:' + role, cb ### -Checks the credentials and on success returns the user object to the callback(err, obj) function. +Checks the credentials and on success returns the user object to the +callback(err, obj) function. The password has to be hashed (SHA-3-512) +beforehand by the instance closest to the user that enters the password, +because we only store hashes of passwords for safety reasons. @public loginUser( *username, password, cb* ) @param {String} username @param {String} password @param {function} cb ### -# TODO verify and test whole function +#TODO verify and test whole function exports.loginUser = ( username, password, cb ) => log.print 'DB', 'User "' + username + '" tries to log in' fCheck = ( pw ) -> @@ -389,16 +409,16 @@ exports.loginUser = ( username, password, cb ) => if err cb err else if obj and obj.password - if encrypt(pw) == obj.password + if pw == obj.password log.print 'DB', 'User "' + obj.username + '" logged in!' cb null, obj else cb new Error 'Wrong credentials!' else - cb new Error 'Empty arguments!' + cb new Error 'User not found!' @db.hgetall 'user:' + username, fCheck password -# TODO implement functions required for user sessions and the rule activation +#TODO implement functions required for user sessions and the rule activation ### Shuts down the db link. diff --git a/coffee/http_listener.coffee b/coffee/http_listener.coffee index bd743eb..3d1a27c 100644 --- a/coffee/http_listener.coffee +++ b/coffee/http_listener.coffee @@ -2,54 +2,78 @@ HTTP Listener ============= -> Handles the HTTP requests to the server at the port specified by the -> [config](config.html) file. +> Receives the HTTP requests to the server at the port specified by the +> [config](config.html) file. These requests (bound to a method) are then +> redirected to the appropriate handler which then takes care of the request. ### +# **Requires:** + +# - [Logging](logging.html) +log = require './logging' + +# - [Config](config.html) +config = require './config' + +# - [User Handler](user_handler.html) +requestHandler = require './request_handler' + +# - Node.js Modules: [path](http://nodejs.org/api/path.html) and +# [querystring](http://nodejs.org/api/querystring.html) path = require 'path' -express = require 'express' -app = express() -# } RedisStore = require('connect-redis')(express), # TODO use RedisStore for persistent sessions qs = require 'querystring' -# Requires: +# - External Modules: [express](http://expressjs.com/api.html) +express = require 'express' +app = express() -# - The [Logging](logging.html) module -log = require './logging' -# - The [Config](config.html) module -config = require './config' -# - The [User Handler](user_handler.html) module -userHandler = require './user_handler' +#RedisStore = require('connect-redis')(express), # TODO use RedisStore for persistent sessions + +# Just to have at least something. I know all of you know it now ;-P sess_sec = '#C[>;j`@".TXm2TA;A2Tg)' -# The module needs to be called as a function to initialize it. -# After that it fetches the http\_port, db\_port & sess\_sec properties -# from the configuration file. +### +Module call +----------- +Initializes the HTTP Listener and its child modules Logging, +Configuration and Request Handler, then tries to fetch the session +key from the configuration. + +@param {Object} args +### exports = module.exports = ( args ) -> args = args ? {} log args config args - userHandler args - # TODO check whether this really does what it's supposed to do (fetch wrong sess property) + requestHandler args + #TODO check whether this really does what it's supposed to do (fetch wrong sess property) sess_sec = config.getSessionSecret() || sess_sec module.exports -exports.addHandlers = ( fEvtHandler, fShutDown ) => - userHandler.addShutdownHandler fShutDown - @eventHandler = fEvtHandler +exports.addHandlers = ( fEvtHandler, fShutDown ) -> + requestHandler.addHandlers fEvtHandler, fShutDown # Add cookie support for session handling. app.use express.cookieParser() app.use express.session { secret: sess_sec } + # At the moment there's no redis session backbone (didn't work straight away) log.print 'HL', 'no session backbone' + + # **Accepted requests to paths:** - # Redirect the requests to the appropriate handler. - app.use '/', express.static path.resolve __dirname, '..', 'webpages' - app.get '/rulesforge', userHandler.handleRequest - app.get '/admin', userHandler.handleRequest - app.post '/login', userHandler.handleLogin - app.post '/push_event', onPushEvent + # - **`GET` to _"/"_:** Static redirect to the _"webpages/public"_ directory + app.use '/', express.static path.resolve __dirname, '..', 'webpages', 'public' + # - **`POST` to _"/event"_:** Events coming from remote systems are passed to the engine + app.post '/event', requestHandler.handleEvent + # - **`GET` to _"/user"_:** User requests are possible for all users with an account + app.get '/user', requestHandler.handleUser + # - **`GET` to _"/admin"_:** Only admins can issue requests to this handler + app.get '/admin', requestHandler.handleAdmin + # - **`POST` to _"/login"_:** Credentials will be verified + app.post '/login', requestHandler.handleLogin + # - **`POST` to _"/logout"_:** User will be logged out + app.post '/logout', requestHandler.handleLogout try http_port = config.getHttpPort() if http_port @@ -60,25 +84,6 @@ exports.addHandlers = ( fEvtHandler, fShutDown ) => e.addInfo = 'opening port' log.error e -# -# If a post request reaches the server, this function handles it and treats the request as a possible event. -# -onPushEvent = ( req, resp ) => - body = '' - req.on 'data', ( data ) -> - body += data - req.on 'end', => - obj = qs.parse body - # If required event properties are present we process the event # - if obj and obj.event and obj.eventid - resp.write 'Thank you for the event (' + obj.event + '[' + obj.eventid + '])!' - @eventHandler obj - else - resp.writeHead 400, { "Content-Type": "text/plain" } - resp.write 'Your event was missing important parameters!' - resp.end() - - exports.shutDown = () -> log.print 'HL', 'Shutting down HTTP listener' process.exit() # This is a bit brute force... diff --git a/coffee/request_handler.coffee b/coffee/request_handler.coffee index bed90da..79ffc24 100644 --- a/coffee/request_handler.coffee +++ b/coffee/request_handler.coffee @@ -1,25 +1,35 @@ ### -User Handler +Request Handler ============ > TODO Add documentation ### +# **Requires:** + +# - [Logging](logging.html) +log = require './logging' + +# - [DB Interface](db_interface.html) +db = require './db_interface' + +# - [Module Manager](module_manager.html) +mm = require './module_manager' + +# - Node.js Modules: [fs](http://nodejs.org/api/fs.html), +# [path](http://nodejs.org/api/path.html) and +# [querystring](http://nodejs.org/api/querystring.html) fs = require 'fs' path = require 'path' qs = require 'querystring' -# Requires: +# - External Modules: [mustache](https://github.com/janl/mustache.js) and +# [crypto-js](https://github.com/evanvosberg/crypto-js) +mustache = require 'mustache' +crypto = require 'crypto-js' -# - The [Logging](logging.html) module -log = require './logging' -# - The [DB Interface](db_interface.html) module -db = require './db_interface' -# - The [Module Manager](module_manager.html) module -mm = require './module_manager' - -### Prepare the admin command handlers that are issued via HTTP requests. ### +# Prepare the admin command handlers that are invoked via HTTP requests. objAdminCmds = 'loadrules': mm.loadRulesFromFS, 'loadaction': mm.loadActionModuleFromFS, @@ -38,69 +48,149 @@ exports = module.exports = ( args ) -> db.storeUser user for user in users module.exports +### +This allows the parent to add handlers. The event handler will receive +the events that were received. The shutdown function will be called if the +admin command shutdown is issued. -exports.addShutdownHandler = ( fShutdown ) -> +@public addHandlers( *fEvtHandler, fShutdown* ) +@param {function} fEvtHandler +@param {function} fShutdown +### +exports.addHandlers = ( fEvtHandler, fShutdown ) => + @eventHanlder = fEvtHandler objAdminCmds.shutdown = fShutdown -exports.handleRequest = ( req, resp ) -> - req.on 'end', -> resp.end() - if req.session and req.session.user - resp.send 'You\'re logged in' - else - resp.sendfile path.resolve __dirname, '..', 'webpages', 'handlers', 'login.html' - req.session.lastPage = req.originalUrl +### + +*Requires +the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) +and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) +objects.* + +@public handleEvent( *req, resp* ) +### +exports.handleEvent = ( req, resp ) => + body = '' + req.on 'data', ( data ) -> + body += data + req.on 'end', => + obj = qs.parse body + # If required event properties are present we process the event # + if obj and obj.event and obj.eventid + resp.send 'Thank you for the event (' + obj.event + '[' + obj.eventid + '])!' + @eventHandler obj + else + resp.writeHead 400, { "Content-Type": "text/plain" } + resp.send 'Your event was missing important parameters!' +### + +*Requires +the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) +and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) +objects.* + +@public handleLogin( *req, resp* ) +### exports.handleLogin = ( req, resp ) -> body = '' req.on 'data', ( data ) -> body += data req.on 'end', -> if not req.session or not req.session.user obj = qs.parse body - db.loginUser obj.username, obj.password, ( err, obj ) -> - if not err - req.session.user = obj - if req.session.user - resp.write 'Welcome ' + req.session.user.name + '!' + db.loginUser obj.username, obj.password, ( err, usr ) -> + if(err) + # Tapping on fingers, at least in log... + log.print 'RH', "AUTH-UH-OH (#{obj.username}): " + err.message else - resp.writeHead 401, { "Content-Type": "text/plain" } - resp.write 'Login failed!' - resp.end() + # no error, so we can associate the user object from the DB to the session + req.session.user = usr + if req.session.user + resp.send 'OK!' + else + resp.send 401, 'NO!' else - resp.write 'Welcome ' + req.session.user.name + '!' - resp.end() + resp.send 'Welcome ' + req.session.user.name + '!' + +### +A post request retrieved on this handler causes the user object to be +purged from the session, thus the user will be logged out. + +*Requires +the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) +and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) +objects.* + +@public handleLogout( *req, resp* ) +### +exports.handleLogout = ( req, resp ) -> + if req.session + req.session.user = null + resp.send 'Bye!' -answerHandler = ( resp ) -> - hasBeenAnswered = false - postAnswer( msg ) -> - if not hasBeenAnswered - resp.write msg - resp.end() - hasBeenAnswered = true - { - answerSuccess: ( msg ) -> - if not hasBeenAnswered - postAnswer msg, - answerError: ( msg ) -> - if not hasBeenAnswered - resp.writeHead 400, { "Content-Type": "text/plain" } - postAnswer msg, - isAnswered: -> hasBeenAnswered - } +getHandlerPath = (name) -> + path.resolve __dirname, '..', 'webpages', 'handlers', name + '.html' + + +getHandlerFileAsString = (name) -> + fs.readFileSync getHandlerPath( name ), 'utf8' +### + +*Requires +the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) +and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) +objects.* + +@public handleUser( *req, resp* ) +### +exports.handleUser = ( req, resp ) -> + if req.session and req.session.user + welcome = getHandlerFileAsString 'welcome' + menubar = getHandlerFileAsString 'menubar' + view = { + user: req.session.user, + div_menubar: menubar + } + resp.send mustache.render welcome, view + else + resp.sendfile getHandlerPath 'login' + +### + +*Requires +the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) +and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) +objects.* + +@public handleAdmin( *req, resp* ) +### +exports.handleAdmin = ( req, resp ) -> + if req.session and req.session.user + if req.session.user.isAdmin is "true" + welcome = getHandlerFileAsString 'welcome' + menubar = getHandlerFileAsString 'menubar' + view = + user: req.session.user, + div_menubar: menubar + resp.send mustache.render welcome, view + else + unauthorized = getHandlerFileAsString 'unauthorized' + menubar = getHandlerFileAsString 'menubar' + view = + user: req.session.user, + div_menubar: menubar + resp.send mustache.render unauthorized, view + else + resp.sendfile getHandlerPath 'login' -# TODO add loadUsers as directive to admin commands -# exports.loadUsers = -> - # var users = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'users.json'))); - # for(var name in users) { - # db.storeUser(users[name]); - # } -# }; onAdminCommand = ( req, response ) -> - q = req.query; - log.print 'HL', 'Received admin request: ' + req.originalUrl + q = req.query + log.print 'RH', 'Received admin request: ' + q if q.cmd fAdminCommands q, answerHandler response #answerSuccess(response, 'Thank you, we try our best!'); @@ -117,7 +207,7 @@ fAdminCommands = ( args, answHandler ) -> if args and args.cmd adminCmds[args.cmd]? args, answHandler else - log.print 'RS', 'No command in request' + log.print 'RH', 'No command in request' ### The fAnsw function receives an answerHandler object as an argument when called diff --git a/coffee/server.coffee b/coffee/server.coffee index b650436..ab05be4 100644 --- a/coffee/server.coffee +++ b/coffee/server.coffee @@ -20,18 +20,23 @@ Rules Server ### -# Requires: +# **Requires:** -# - The [Logging](logging.html) module +# - [Logging](logging.html) log = require './logging' -# - The [Config](config.html) module + +# - [Configuration](config.html) conf = require './config' -# - The [DB Interface](db_interface.html) module + +# - [DB Interface](db_interface.html) db = require './db_interface' -# - The [Engine](engine.html) module + +# - [Engine](engine.html) engine = require './engine' -# - The [HTTP Listener](http_listener.html) module + +# - [HTTP Listener](http_listener.html) http_listener = require './http_listener' + args = {} procCmds = {} @@ -48,22 +53,20 @@ process.on 'uncaughtException', ( err ) -> log.error 'RS', err shutDown() else throw err - ### This function is invoked right after the module is loaded and starts the server. @private init() ### - init = -> log.print 'RS', 'STARTING SERVER' - ### Check whether the config file is ready, which is required to start the server. ### + # > Check whether the config file is ready, which is required to start the server. if !conf.isReady() log.error 'RS', 'Config file not ready!' process.exit() - ### Fetch the `log_type` argument and post a log about which log type is used.### + # > Fetch the `log_type` argument and post a log about which log type is used. if process.argv.length > 2 args.logType = parseInt(process.argv[2]) || 0 switch args.logType @@ -75,31 +78,31 @@ init = -> else log.print 'RS', 'No log method argument provided, using standard I/O' - ### Fetch the `http_port` argument ### + # > Fetch the `http_port` argument if process.argv.length > 3 then args.http_port = parseInt process.argv[3] else log.print 'RS', 'No HTTP port passed, using standard port from config file' log.print 'RS', 'Initialzing DB' db args - ### We only proceed with the initialization if the DB is ready ### + # > We only proceed with the initialization if the DB is ready db.isConnected ( err, result ) -> if !err - ### Initialize all required modules with the args object.### + # > Initialize all required modules with the args object. log.print 'RS', 'Initialzing engine' engine args log.print 'RS', 'Initialzing http listener' http_listener args - ### Distribute handlers between modules to link the application. ### + # > Distribute handlers between modules to link the application. log.print 'RS', 'Passing handlers to engine' engine.addDBLinkAndLoadActionsAndRules db log.print 'RS', 'Passing handlers to http listener' - # TODO engine pushEvent needs to go into redis queue + #TODO engine pushEvent needs to go into redis queue http_listener.addHandlers db, engine.pushEvent, shutDown - # log.print 'RS', 'Passing handlers to module manager' - # TODO loadAction and addRule will be removed - # mm.addHandlers db, engine.loadActionModule, engine.addRule + #log.print 'RS', 'Passing handlers to module manager' + #TODO loadAction and addRule will be removed + #mm.addHandlers db, engine.loadActionModule, engine.addRule ### Shuts down the server. @@ -117,17 +120,10 @@ shutDown = -> When the server is run as a child process, this function handles messages from the parent process (e.g. the testing suite) ### - process.on 'message', ( cmd ) -> procCmds[cmd]?() -### -The die command redirects to the shutDown function. -### - +# The die command redirects to the shutDown function. procCmds.die = shutDown -### -*Start initialization* -### - +# *Start initialization* init() diff --git a/create_doc.js b/create_doc.js index 1835898..5de0041 100644 --- a/create_doc.js +++ b/create_doc.js @@ -10,7 +10,7 @@ require('groc').CLI( "coffee/*.coffee", "mod_actions/**/*.js", "mod_events/**/*.js", - "-o./webpages/doc" + "-o./webpages/public/doc" ], function(err) { if (err) console.error(err); diff --git a/js-coffee/config.js b/js-coffee/config.js index 2330f6b..1de8ae2 100644 --- a/js-coffee/config.js +++ b/js-coffee/config.js @@ -1,8 +1,8 @@ // Generated by CoffeeScript 1.6.3 /* -Config -====== +Configuration +============= > Loads the configuration file and acts as an interface to it. */ @@ -11,12 +11,12 @@ Config var exports, fetchProp, fs, loadConfigFile, log, path, _this = this; + log = require('./logging'); + fs = require('fs'); path = require('path'); - log = require('./logging'); - /* ##Module call @@ -30,7 +30,7 @@ Config args = args != null ? args : {}; log(args); if (typeof args.relPath === 'string') { - loadConfigFiles(args.relPath); + loadConfigFile(args.relPath); } return module.exports; }; @@ -76,7 +76,7 @@ Config }; /* - Answer true if the config file is ready, else false + ***Returns*** true if the config file is ready, else false @public isReady() */ @@ -87,7 +87,7 @@ Config }; /* - Returns the HTTP port + ***Returns*** the HTTP port @public getHttpPort() */ @@ -98,7 +98,7 @@ Config }; /* - Returns the DB port + ***Returns*** the DB port* @public getDBPort() */ @@ -109,7 +109,7 @@ Config }; /* - Returns the crypto key + ***Returns*** the crypto key @public getCryptoKey() */ @@ -120,7 +120,7 @@ Config }; /* - Returns the session secret + ***Returns*** the session secret @public getSessionSecret() */ diff --git a/js-coffee/db_interface.js b/js-coffee/db_interface.js index 015879d..15de188 100644 --- a/js-coffee/db_interface.js +++ b/js-coffee/db_interface.js @@ -21,15 +21,15 @@ DB Interface (function() { - var crypto, decrypt, encrypt, exports, getSetRecords, log, redis, replyHandler, + var crypto, decrypt, encrypt, exports, getSetRecords, hash, log, redis, replyHandler, _this = this; - redis = require('redis'); - - crypto = require('crypto'); - log = require('./logging'); + crypto = require('crypto-js'); + + redis = require('redis'); + /* Module call ----------- @@ -88,6 +88,31 @@ DB Interface } }; + /* + Hashes a string based on SHA-3-512. + + @private hash( *plainText* ) + @param {String} plainText + */ + + + hash = function(plainText) { + var err; + if (plainText == null) { + return null; + } + try { + return (crypto.SHA3(plainText, { + outputLength: 512 + })).toString(); + } catch (_error) { + err = _error; + err.addInfo = 'during hashing'; + log.error('DB', err); + return null; + } + }; + /* Encrypts a string using the crypto key from the config file, based on aes-256-cbc. @@ -269,7 +294,7 @@ DB Interface exports.storeActionAuth = function(userId, moduleId, data) { log.print('DB', 'storeActionAuth: ' + userId + ':' + moduleId); - return _this.db.set('action-auth:' + userId + ':' + moduleId, encrypt(data), replyHandler('storing action auth ' + userId + ':' + moduleId)); + return _this.db.set('action-auth:' + userId + ':' + moduleId, hash(data), replyHandler('storing action auth ' + userId + ':' + moduleId)); }; /* @@ -348,7 +373,7 @@ DB Interface exports.storeEventAuth = function(userId, moduleId, data) { log.print('DB', 'storeEventAuth: ' + userId + ':' + moduleId); - return _this.db.set('event-auth:' + userId + ':' + moduleId, encrypt(data), replyHandler('storing event auth ' + userId + ':' + moduleId)); + return _this.db.set('event-auth:' + userId + ':' + moduleId, hash(data), replyHandler('storing event auth ' + userId + ':' + moduleId)); }; /* @@ -427,7 +452,7 @@ DB Interface log.print('DB', 'storeUser: ' + objUser.username); if (objUser && objUser.username && objUser.password) { _this.db.sadd('users', objUser.username, replyHandler('storing user key ' + objUser.username)); - objUser.password = encrypt(objUser.password); + objUser.password = hash(objUser.password); return _this.db.hmset('user:' + objUser.username, objUser, replyHandler('storing user properties ' + objUser.username)); } else { return log.error('DB', new Error('username or password was missing')); @@ -476,7 +501,10 @@ DB Interface }; /* - Checks the credentials and on success returns the user object to the callback(err, obj) function. + Checks the credentials and on success returns the user object to the + callback(err, obj) function. The password has to be hashed (SHA-3-512) + beforehand by the instance closest to the user that enters the password, + because we only store hashes of passwords for safety reasons. @public loginUser( *username, password, cb* ) @param {String} username @@ -493,14 +521,14 @@ DB Interface if (err) { return cb(err); } else if (obj && obj.password) { - if (encrypt(pw) === obj.password) { + if (pw === obj.password) { log.print('DB', 'User "' + obj.username + '" logged in!'); return cb(null, obj); } else { return cb(new Error('Wrong credentials!')); } } else { - return cb(new Error('Empty arguments!')); + return cb(new Error('User not found!')); } }; }; diff --git a/js-coffee/http_listener.js b/js-coffee/http_listener.js index a30ba6d..5603260 100644 --- a/js-coffee/http_listener.js +++ b/js-coffee/http_listener.js @@ -3,54 +3,65 @@ HTTP Listener ============= -> Handles the HTTP requests to the server at the port specified by the -> [config](config.html) file. +> Receives the HTTP requests to the server at the port specified by the +> [config](config.html) file. These requests (bound to a method) are then +> redirected to the appropriate handler which then takes care of the request. */ (function() { - var app, config, exports, express, log, onPushEvent, path, qs, sess_sec, userHandler, - _this = this; - - path = require('path'); - - express = require('express'); - - app = express(); - - qs = require('querystring'); + var app, config, exports, express, log, path, qs, requestHandler, sess_sec; log = require('./logging'); config = require('./config'); - userHandler = require('./user_handler'); + requestHandler = require('./request_handler'); + + path = require('path'); + + qs = require('querystring'); + + express = require('express'); + + app = express(); sess_sec = '#C[>;j`@".TXm2TA;A2Tg)'; + /* + Module call + ----------- + Initializes the HTTP Listener and its child modules Logging, + Configuration and Request Handler, then tries to fetch the session + key from the configuration. + + @param {Object} args + */ + + exports = module.exports = function(args) { args = args != null ? args : {}; log(args); config(args); - userHandler(args); + requestHandler(args); sess_sec = config.getSessionSecret() || sess_sec; return module.exports; }; exports.addHandlers = function(fEvtHandler, fShutDown) { var e, http_port; - userHandler.addShutdownHandler(fShutDown); - _this.eventHandler = fEvtHandler; + requestHandler.addHandlers(fEvtHandler, fShutDown); app.use(express.cookieParser()); app.use(express.session({ secret: sess_sec })); log.print('HL', 'no session backbone'); - app.use('/', express["static"](path.resolve(__dirname, '..', 'webpages'))); - app.get('/rulesforge', userHandler.handleRequest); - app.get('/admin', userHandler.handleRequest); - app.post('/login', userHandler.handleLogin); - app.post('/push_event', onPushEvent); + app.use('/', express["static"](path.resolve(__dirname, '..', 'webpages', 'public'))); + app.post('/event', requestHandler.handleEvent); + app.get('/user', requestHandler.handleUser); + app.get('/admin', requestHandler.handleAdmin); + app.post('/login', requestHandler.handleLogin); + app.post('/logout', requestHandler.handleLogout); try { http_port = config.getHttpPort(); if (http_port) { @@ -65,28 +76,6 @@ HTTP Listener } }; - onPushEvent = function(req, resp) { - var body; - body = ''; - req.on('data', function(data) { - return body += data; - }); - return req.on('end', function() { - var obj; - obj = qs.parse(body); - if (obj && obj.event && obj.eventid) { - resp.write('Thank you for the event (' + obj.event + '[' + obj.eventid + '])!'); - _this.eventHandler(obj); - } else { - resp.writeHead(400, { - "Content-Type": "text/plain" - }); - resp.write('Your event was missing important parameters!'); - } - return resp.end(); - }); - }; - exports.shutDown = function() { log.print('HL', 'Shutting down HTTP listener'); return process.exit(); diff --git a/js-coffee/request_handler.js b/js-coffee/request_handler.js new file mode 100644 index 0000000..f3ae740 --- /dev/null +++ b/js-coffee/request_handler.js @@ -0,0 +1,282 @@ +// Generated by CoffeeScript 1.6.3 +/* + +Request Handler +============ +> TODO Add documentation +*/ + + +(function() { + var crypto, db, exports, fAdminCommands, fs, getHandlerFileAsString, getHandlerPath, log, mm, mustache, objAdminCmds, onAdminCommand, path, qs, + _this = this; + + log = require('./logging'); + + db = require('./db_interface'); + + mm = require('./module_manager'); + + fs = require('fs'); + + path = require('path'); + + qs = require('querystring'); + + mustache = require('mustache'); + + crypto = require('crypto-js'); + + objAdminCmds = { + 'loadrules': mm.loadRulesFromFS, + 'loadaction': mm.loadActionModuleFromFS, + 'loadactions': mm.loadActionModulesFromFS, + 'loadevent': mm.loadEventModuleFromFS, + 'loadevents': mm.loadEventModulesFromFS + }; + + exports = module.exports = function(args) { + var user, users, _i, _len; + args = args != null ? args : {}; + log(args); + db(args); + mm(args); + mm.addDBLink(db); + users = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'config', 'users.json'))); + for (_i = 0, _len = users.length; _i < _len; _i++) { + user = users[_i]; + db.storeUser(user); + } + return module.exports; + }; + + /* + This allows the parent to add handlers. The event handler will receive + the events that were received. The shutdown function will be called if the + admin command shutdown is issued. + + @public addHandlers( *fEvtHandler, fShutdown* ) + @param {function} fEvtHandler + @param {function} fShutdown + */ + + + exports.addHandlers = function(fEvtHandler, fShutdown) { + _this.eventHanlder = fEvtHandler; + return objAdminCmds.shutdown = fShutdown; + }; + + /* + + *Requires + the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) + and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) + objects.* + + @public handleEvent( *req, resp* ) + */ + + + exports.handleEvent = function(req, resp) { + var body; + body = ''; + req.on('data', function(data) { + return body += data; + }); + return req.on('end', function() { + var obj; + obj = qs.parse(body); + if (obj && obj.event && obj.eventid) { + resp.send('Thank you for the event (' + obj.event + '[' + obj.eventid + '])!'); + return _this.eventHandler(obj); + } else { + resp.writeHead(400, { + "Content-Type": "text/plain" + }); + return resp.send('Your event was missing important parameters!'); + } + }); + }; + + /* + + *Requires + the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) + and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) + objects.* + + @public handleLogin( *req, resp* ) + */ + + + exports.handleLogin = function(req, resp) { + var body; + body = ''; + req.on('data', function(data) { + return body += data; + }); + return req.on('end', function() { + var obj; + if (!req.session || !req.session.user) { + obj = qs.parse(body); + return db.loginUser(obj.username, obj.password, function(err, usr) { + if (err) { + log.print('RH', ("AUTH-UH-OH (" + obj.username + "): ") + err.message); + } else { + req.session.user = usr; + } + if (req.session.user) { + return resp.send('OK!'); + } else { + return resp.send(401, 'NO!'); + } + }); + } else { + return resp.send('Welcome ' + req.session.user.name + '!'); + } + }); + }; + + /* + A post request retrieved on this handler causes the user object to be + purged from the session, thus the user will be logged out. + + *Requires + the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) + and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) + objects.* + + @public handleLogout( *req, resp* ) + */ + + + exports.handleLogout = function(req, resp) { + if (req.session) { + req.session.user = null; + return resp.send('Bye!'); + } + }; + + getHandlerPath = function(name) { + return path.resolve(__dirname, '..', 'webpages', 'handlers', name + '.html'); + }; + + getHandlerFileAsString = function(name) { + return fs.readFileSync(getHandlerPath(name), 'utf8'); + }; + + /* + + *Requires + the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) + and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) + objects.* + + @public handleUser( *req, resp* ) + */ + + + exports.handleUser = function(req, resp) { + var menubar, view, welcome; + if (req.session && req.session.user) { + welcome = getHandlerFileAsString('welcome'); + menubar = getHandlerFileAsString('menubar'); + view = { + user: req.session.user, + div_menubar: menubar + }; + return resp.send(mustache.render(welcome, view)); + } else { + return resp.sendfile(getHandlerPath('login')); + } + }; + + /* + + *Requires + the [request](http://nodejs.org/api/http.html#http_class_http_clientrequest) + and [response](http://nodejs.org/api/http.html#http_class_http_serverresponse) + objects.* + + @public handleAdmin( *req, resp* ) + */ + + + exports.handleAdmin = function(req, resp) { + var menubar, unauthorized, view, welcome; + if (req.session && req.session.user) { + if (req.session.user.isAdmin === "true") { + welcome = getHandlerFileAsString('welcome'); + menubar = getHandlerFileAsString('menubar'); + view = { + user: req.session.user, + div_menubar: menubar + }; + return resp.send(mustache.render(welcome, view)); + } else { + unauthorized = getHandlerFileAsString('unauthorized'); + menubar = getHandlerFileAsString('menubar'); + view = { + user: req.session.user, + div_menubar: menubar + }; + return resp.send(mustache.render(unauthorized, view)); + } + } else { + return resp.sendfile(getHandlerPath('login')); + } + }; + + onAdminCommand = function(req, response) { + var q; + q = req.query; + log.print('RH', 'Received admin request: ' + q); + if (q.cmd) { + return fAdminCommands(q, answerHandler(response)); + } else { + return answerError(response, 'I\'m not sure about what you want from me...'); + } + }; + + /* + admin commands handler receives all command arguments and an answerHandler + object that eases response handling to the HTTP request issuer. + + @private fAdminCommands( *args, answHandler* ) + */ + + + fAdminCommands = function(args, answHandler) { + var fAnsw, _name; + if (args && args.cmd) { + if (typeof adminCmds[_name = args.cmd] === "function") { + adminCmds[_name](args, answHandler); + } + } else { + log.print('RH', 'No command in request'); + } + /* + The fAnsw function receives an answerHandler object as an argument when called + and returns an anonymous function + */ + + fAnsw = function(ah) { + /* + The anonymous function checks whether the answerHandler was already used to + issue an answer, if no answer was provided we answer with an error message + */ + + return function() { + if (!ah.isAnswered()) { + return ah.answerError('Not handled...'); + } + }; + }; + /* + Delayed function call of the anonymous function that checks the answer handler + */ + + return setTimeout(fAnsw(answHandler), 2000); + }; + +}).call(this); diff --git a/js-coffee/server.js b/js-coffee/server.js index 7afe549..1e2a262 100644 --- a/js-coffee/server.js +++ b/js-coffee/server.js @@ -65,14 +65,10 @@ Rules Server init = function() { log.print('RS', 'STARTING SERVER'); - /* Check whether the config file is ready, which is required to start the server.*/ - if (!conf.isReady()) { log.error('RS', 'Config file not ready!'); process.exit(); } - /* Fetch the `log_type` argument and post a log about which log type is used.*/ - if (process.argv.length > 2) { args.logType = parseInt(process.argv[2]) || 0; switch (args.logType) { @@ -92,8 +88,6 @@ Rules Server } else { log.print('RS', 'No log method argument provided, using standard I/O'); } - /* Fetch the `http_port` argument*/ - if (process.argv.length > 3) { args.http_port = parseInt(process.argv[3]); } else { @@ -101,18 +95,12 @@ Rules Server } log.print('RS', 'Initialzing DB'); db(args); - /* We only proceed with the initialization if the DB is ready*/ - return db.isConnected(function(err, result) { if (!err) { - /* Initialize all required modules with the args object.*/ - log.print('RS', 'Initialzing engine'); engine(args); log.print('RS', 'Initialzing http listener'); http_listener(args); - /* Distribute handlers between modules to link the application.*/ - log.print('RS', 'Passing handlers to engine'); engine.addDBLinkAndLoadActionsAndRules(db); log.print('RS', 'Passing handlers to http listener'); @@ -148,18 +136,8 @@ Rules Server return typeof procCmds[cmd] === "function" ? procCmds[cmd]() : void 0; }); - /* - The die command redirects to the shutDown function. - */ - - procCmds.die = shutDown; - /* - *Start initialization* - */ - - init(); }).call(this); diff --git a/js-coffee/user_handler.js b/js-coffee/user_handler.js deleted file mode 100644 index 5a1de67..0000000 --- a/js-coffee/user_handler.js +++ /dev/null @@ -1,180 +0,0 @@ -// Generated by CoffeeScript 1.6.3 -/* - -User Handler -============ -> TODO Add documentation -*/ - - -(function() { - var answerHandler, db, exports, fAdminCommands, fs, log, mm, objAdminCmds, onAdminCommand, path, qs; - - fs = require('fs'); - - path = require('path'); - - qs = require('querystring'); - - log = require('./logging'); - - db = require('./db_interface'); - - mm = require('./module_manager'); - - /* Prepare the admin command handlers that are issued via HTTP requests.*/ - - - objAdminCmds = { - 'loadrules': mm.loadRulesFromFS, - 'loadaction': mm.loadActionModuleFromFS, - 'loadactions': mm.loadActionModulesFromFS, - 'loadevent': mm.loadEventModuleFromFS, - 'loadevents': mm.loadEventModulesFromFS - }; - - exports = module.exports = function(args) { - var user, users, _i, _len; - args = args != null ? args : {}; - log(args); - db(args); - mm(args); - mm.addDBLink(db); - users = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'config', 'users.json'))); - for (_i = 0, _len = users.length; _i < _len; _i++) { - user = users[_i]; - db.storeUser(user); - } - return module.exports; - }; - - exports.addShutdownHandler = function(fShutdown) { - return objAdminCmds.shutdown = fShutdown; - }; - - exports.handleRequest = function(req, resp) { - req.on('end', function() { - return resp.end(); - }); - if (req.session && req.session.user) { - resp.send('You\'re logged in'); - } else { - resp.sendfile(path.resolve(__dirname, '..', 'webpages', 'handlers', 'login.html')); - } - return req.session.lastPage = req.originalUrl; - }; - - exports.handleLogin = function(req, resp) { - var body; - body = ''; - req.on('data', function(data) { - return body += data; - }); - return req.on('end', function() { - var obj; - if (!req.session || !req.session.user) { - obj = qs.parse(body); - return db.loginUser(obj.username, obj.password, function(err, obj) { - if (!err) { - req.session.user = obj; - } - if (req.session.user) { - resp.write('Welcome ' + req.session.user.name + '!'); - } else { - resp.writeHead(401, { - "Content-Type": "text/plain" - }); - resp.write('Login failed!'); - } - return resp.end(); - }); - } else { - resp.write('Welcome ' + req.session.user.name + '!'); - return resp.end(); - } - }); - }; - - answerHandler = function(resp) { - var hasBeenAnswered; - hasBeenAnswered = false; - postAnswer(msg)(function() { - if (!hasBeenAnswered) { - resp.write(msg); - resp.end(); - return hasBeenAnswered = true; - } - }); - return { - answerSuccess: function(msg) { - if (!hasBeenAnswered) { - return postAnswer(msg); - } - }, - answerError: function(msg) { - if (!hasBeenAnswered) { - resp.writeHead(400, { - "Content-Type": "text/plain" - }); - } - return postAnswer(msg); - }, - isAnswered: function() { - return hasBeenAnswered; - } - }; - }; - - onAdminCommand = function(req, response) { - var q; - q = req.query; - log.print('HL', 'Received admin request: ' + req.originalUrl); - if (q.cmd) { - return fAdminCommands(q, answerHandler(response)); - } else { - return answerError(response, 'I\'m not sure about what you want from me...'); - } - }; - - /* - admin commands handler receives all command arguments and an answerHandler - object that eases response handling to the HTTP request issuer. - - @private fAdminCommands( *args, answHandler* ) - */ - - - fAdminCommands = function(args, answHandler) { - var fAnsw, _name; - if (args && args.cmd) { - if (typeof adminCmds[_name = args.cmd] === "function") { - adminCmds[_name](args, answHandler); - } - } else { - log.print('RS', 'No command in request'); - } - /* - The fAnsw function receives an answerHandler object as an argument when called - and returns an anonymous function - */ - - fAnsw = function(ah) { - /* - The anonymous function checks whether the answerHandler was already used to - issue an answer, if no answer was provided we answer with an error message - */ - - return function() { - if (!ah.isAnswered()) { - return ah.answerError('Not handled...'); - } - }; - }; - /* - Delayed function call of the anonymous function that checks the answer handler - */ - - return setTimeout(fAnsw(answHandler), 2000); - }; - -}).call(this); diff --git a/package.json b/package.json index 0c5cac3..753c9d4 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ }, "dependencies": { "connect-redis": "1.4.6", + "crypto-js": "3.1.2", "express": "3.4.0", "groc": "0.6.1", + "mustache": "0.7.3", "needle": "0.6.1", "nodeunit": "0.8.2", "redis": "0.9.0", diff --git a/webpages/handlers/command_answer.html b/webpages/handlers/command_answer.html new file mode 100644 index 0000000..eccef13 --- /dev/null +++ b/webpages/handlers/command_answer.html @@ -0,0 +1,18 @@ + + + + Command "{{command}}" Result + + + + + {{{div_menubar}}} +
+
{{user.username}} unauthorized!
+

+ Sorry this roles is missing for you.
+ You only have these privileges: {{user.roles}} +

+
+ + \ No newline at end of file diff --git a/webpages/handlers/login.html b/webpages/handlers/login.html index 6e8a4be..73cf3c0 100644 --- a/webpages/handlers/login.html +++ b/webpages/handlers/login.html @@ -4,17 +4,21 @@ Login + -

Login

- - - -
username:
password:
- +
+
Login
+ + + +
username:
password:
+ +
+ \ No newline at end of file diff --git a/webpages/handlers/unauthorized.html b/webpages/handlers/unauthorized.html new file mode 100644 index 0000000..49c07cb --- /dev/null +++ b/webpages/handlers/unauthorized.html @@ -0,0 +1,18 @@ + + + + Unauthorized {{user.username}} + + + + + {{{div_menubar}}} +
+
{{user.username}} unauthorized!
+

+ Sorry this roles is missing for you.
+ You only have these privileges: {{user.roles}} +

+
+ + \ No newline at end of file diff --git a/webpages/handlers/welcome.html b/webpages/handlers/welcome.html new file mode 100644 index 0000000..ef796aa --- /dev/null +++ b/webpages/handlers/welcome.html @@ -0,0 +1,18 @@ + + + + Welcome {{user.username}} + + + + + {{{div_menubar}}} +
+
Welcome {{user.username}}
+

+ We're glad you're back!
+ Your roles are: {{user.roles}} +

+
+ + \ No newline at end of file diff --git a/webpages/public/style.css b/webpages/public/style.css new file mode 100644 index 0000000..2cea4c8 --- /dev/null +++ b/webpages/public/style.css @@ -0,0 +1,40 @@ + +body { + font-family: sans-serif, "Times New Roman", Georgia, Serif; + font-size: 80%; + margin: 0px; +} + +#menubar { + font-size: 0.75em; + width: 100%; + padding: 2px; + height: 1em; + background-color: #DDD; +} + +#menubar_menu { + float: left; + margin:0 auto; +} + +#menubar_logout { + height: 100%; + float: right; + padding-left: 10px; + padding-right: 10px; + cursor: pointer; +} + +#menubar_logout:hover { + background-color: #AAA; +} + +#mainbody { + padding: 5px; +} + +#pagetitle { + font-size: 1.5em; + font-weight: bold; +} diff --git a/webpages/style.css b/webpages/style.css deleted file mode 100644 index 7bc36cc..0000000 --- a/webpages/style.css +++ /dev/null @@ -1,4 +0,0 @@ - -body { - font-family: sans-serif, "Times New Roman", Georgia, Serif; -} \ No newline at end of file