Login functionality successfully implemented,

switched crypto to crypto-js from google. login.html form loads crypto-js directly from google codebase in order to hash passwords before sending them to the server
This commit is contained in:
Dominic Bosch 2013-11-28 16:05:47 +01:00
parent 8192eee64d
commit 32f5553471
21 changed files with 767 additions and 446 deletions

View file

@ -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
_

View file

@ -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()
###

View file

@ -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.

View file

@ -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...

View file

@ -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

View file

@ -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()

View file

@ -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);

View file

@ -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()
*/

View file

@ -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!'));
}
};
};

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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",

View file

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Command "{{command}}" Result</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src='//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js' type='text/javascript'></script>
</head>
<body>
{{{div_menubar}}}
<div id="mainbody">
<div id="pagetitle">{{user.username}} unauthorized!</div>
<p>
Sorry this roles is missing for you.<br />
You only have these privileges: {{user.roles}}
</p>
</div>
</body>
</html>

View file

@ -4,17 +4,21 @@
<title>Login</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src='//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js' type='text/javascript'></script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha3.js"></script>
</head>
<body>
<h1>Login</h1>
<table>
<tr><td>username: </td><td><input type="text" id="username" /></td></tr>
<tr><td>password: </td><td><input type="password" id="password" /></td></tr>
</table>
<button id="but_submit">login</button>
<div id="mainbody">
<div id="pagetitle">Login</div>
<table>
<tr><td>username: </td><td><input type="text" id="username" /></td></tr>
<tr><td>password: </td><td><input type="password" id="password" /></td></tr>
</table>
<button id="but_submit">login</button>
</div>
<script>
$('#but_submit').click(function() {
$.post('../login', { username: $('#username').val(), password: $('#password').val() })
var hashedPassword = (CryptoJS.SHA3($('#password').val(), { outputLength: 512 })).toString();
$.post('../login', { username: $('#username').val(), password: hashedPassword })
.done(function(data) {
window.location.href = document.URL;
})

View file

@ -0,0 +1,11 @@
<div id="menubar">
<div id="menubar_menu">menu</div>
<div id="menubar_logout">logout</div>
<script>
$('#menubar_logout').click(function() {
$.post('../logout').done(function() {
window.location.href = document.URL;
});
});
</script>
</div>

View file

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Unauthorized {{user.username}}</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src='//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js' type='text/javascript'></script>
</head>
<body>
{{{div_menubar}}}
<div id="mainbody">
<div id="pagetitle">{{user.username}} unauthorized!</div>
<p>
Sorry this roles is missing for you.<br />
You only have these privileges: {{user.roles}}
</p>
</div>
</body>
</html>

View file

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Welcome {{user.username}}</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src='//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js' type='text/javascript'></script>
</head>
<body>
{{{div_menubar}}}
<div id="mainbody">
<div id="pagetitle">Welcome {{user.username}}</div>
<p>
We're glad you're back!<br />
Your roles are: {{user.roles}}
</p>
</div>
</body>
</html>

40
webpages/public/style.css Normal file
View file

@ -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;
}

View file

@ -1,4 +0,0 @@
body {
font-family: sans-serif, "Times New Roman", Georgia, Serif;
}