webapi-eca/coffee/request-handler.coffee

428 lines
11 KiB
CoffeeScript
Raw Permalink Normal View History

2013-11-26 22:24:15 +00:00
###
Request Handler
2013-11-26 22:24:15 +00:00
============
> The request handler (surprisingly) handles requests made through HTTP to
> the [HTTP Listener](http-listener.html). It will handle user requests for
> pages as well as POST requests such as user login, module storing, event
> invocation and also admin commands.
2013-11-26 22:24:15 +00:00
###
# **Loads Modules:**
# - [Persistence](persistence.html)
db = require './persistence'
# - 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'
# - 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'
2014-04-21 12:42:26 +00:00
pathUsers = path.resolve __dirname, '..', 'config', 'users.json'
# Prepare the user command handlers which are invoked via HTTP requests.
dirHandlers = path.resolve __dirname, '..', 'webpages', 'handlers'
exports = module.exports = ( args ) =>
@log = args.logger
# Register the request service
@userRequestHandler = args[ 'request-service' ]
# Register the shutdown handler to the admin command.
@objAdminCmds =
shutdown: ( obj, cb ) ->
data =
code: 200
message: 'Shutting down... BYE!'
setTimeout args[ 'shutdown-function' ], 500
cb null, data
2014-04-21 12:42:26 +00:00
newuser: ( obj, cb ) ->
data =
code: 200
message: 'User stored thank you!'
if obj.username and obj.password
if obj.roles
try
roles = JSON.parse obj.roles
catch err
@log 'RH | error parsing newuser roles: ' + err.message
roles = []
else
roles = []
oUser =
username: obj.username
password: obj.password
roles: roles
db.storeUser oUser
fPersistNewUser = ( username, password, roles ) ->
( err, data ) ->
users = JSON.parse data
users[ username ] =
password: password
roles: roles
fs.writeFile pathUsers, JSON.stringify( users, undefined, 2 ), 'utf8', ( err ) ->
if err
@log.error "RH | Unable to write new user file! "
@log.error err
2014-04-22 07:59:57 +00:00
2014-04-21 12:42:26 +00:00
fs.readFile pathUsers, 'utf8', fPersistNewUser obj.username, obj.password, roles
else
data.code = 401
data.message = 'Missing parameter for this command'
cb null, data
# Load the standard users from the user config file
2014-04-21 12:42:26 +00:00
users = JSON.parse fs.readFileSync pathUsers, 'utf8'
fStoreUser = ( username, oUser ) ->
oUser.username = username
db.storeUser oUser
fStoreUser user, oUser for user, oUser of users
2014-04-26 00:16:06 +00:00
@allowedHooks = {}
db.getAllWebhooks ( err, oHooks ) =>
if oHooks
@log.info "RH | Initializing #{ Object.keys( oHooks ).length } Webhooks"
@allowedHooks = oHooks
module.exports
2013-11-26 22:24:15 +00:00
###
Handles possible events that were posted to this server and pushes them into the
event queue.
@public handleEvent( *req, resp* )
###
exports.handleEvent = ( req, resp ) ->
body = ''
req.on 'data', ( data ) ->
body += data
req.on 'end', ->
try
obj = JSON.parse body
catch err
resp.send 400, 'Badly formed event!'
# If required event properties are present we process the event #
2014-04-26 00:16:06 +00:00
if obj and obj.eventname and not err
answ =
code: 200
2014-04-26 00:16:06 +00:00
message: "Thank you for the event: #{ obj.eventname }"
resp.send answ.code, answ
db.pushEvent obj
else
resp.send 400, 'Your event was missing important parameters!'
###
Associates the user object with the session if login is successful.
*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.*
2013-11-26 22:24:15 +00:00
@public handleLogin( *req, resp* )
###
exports.handleLogin = ( req, resp ) =>
body = ''
req.on 'data', ( data ) -> body += data
req.on 'end', =>
obj = JSON.parse body
db.loginUser obj.username, obj.password, ( err, usr ) =>
if err
# Tapping on fingers, at least in log...
@log.warn "RH | AUTH-UH-OH ( #{ obj.username } ): #{ err.message }"
else
# 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!'
###
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!'
###
Resolves the path to a handler webpage.
@private getHandlerPath( *name* )
@param {String} name
###
getHandlerPath = ( name ) ->
path.join dirHandlers, name + '.html'
###
Fetches a template.
@private getTemplate( *name* )
@param {String} name
###
getTemplate = ( name ) ->
pth = path.join dirHandlers, 'templates', name + '.html'
fs.readFileSync pth, 'utf8'
2014-02-10 21:28:10 +00:00
###
Fetches a script.
2014-02-10 21:28:10 +00:00
@private getScript( *name* )
2014-02-10 21:28:10 +00:00
@param {String} name
###
getScript = ( name ) ->
pth = path.join dirHandlers, 'js', name + '.js'
fs.readFileSync pth, 'utf8'
###
Fetches remote scripts snippets.
@private getRemoteScripts( *name* )
@param {String} name
###
getRemoteScripts = ( name ) ->
pth = path.join dirHandlers, 'remote-scripts', name + '.html'
fs.readFileSync pth, 'utf8'
###
Renders a page, with helps of mustache, depending on the user session and returns it.
@private renderPage( *name, sess, msg* )
@param {String} name
@param {Object} sess
@param {Object} msg
###
renderPage = ( name, req, resp, msg ) ->
# Grab the skeleton
pathSkel = path.join dirHandlers, 'skeleton.html'
skeleton = fs.readFileSync pathSkel, 'utf8'
code = 200
data =
message: msg
user: req.session.user
# Try to grab the script belonging to this page. But don't bother if it's not existing
try
script = getScript name
# Try to grab the remote scripts belonging to this page. But don't bother if it's not existing
try
remote_scripts = getRemoteScripts name
# Now try to find the page the user requested.
try
content = getTemplate name
catch err
# If the page doesn't exist we return the error page, load the error script into it
# and render the error page with some additional data
content = getTemplate 'error'
script = getScript 'error'
code = 404
data.message = 'Invalid Page!'
if req.session.user
menubar = getTemplate 'menubar'
pageElements =
content: content
script: script
remote_scripts: remote_scripts
menubar: menubar
# First we render the page by including all page elements into the skeleton
page = mustache.render skeleton, pageElements
# Then we complete the rendering by adding the data, and send the result to the user
resp.send code, mustache.render page, data
###
Present the desired forge page to the user.
*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 handleForge( *req, resp* )
###
exports.handleForge = ( req, resp ) ->
page = req.query.page
if not req.session.user
page = 'login'
renderPage page, req, resp
###
Handles the user command requests.
*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.handleUserCommand = ( req, resp ) =>
if req.session and req.session.user
body = ''
#Append data to body while receiving fragments
req.on 'data', ( data ) ->
body += data
req.on 'end', =>
obj = qs.parse body
# Let the user request handler service answer the request
@userRequestHandler req.session.user, obj, ( obj ) ->
resp.send obj.code, obj
else
resp.send 401, 'Login first!'
###
Present the admin console to the user if he's allowed to see it.
*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 handleForge( *req, resp* )
###
exports.handleAdmin = ( req, resp ) ->
if not req.session.user
page = 'login'
#TODO isAdmin should come from the db role
2014-04-21 12:42:26 +00:00
else if req.session.user.roles.indexOf( "admin" ) is -1
page = 'login'
2014-04-21 12:42:26 +00:00
msg = 'You need to be admin for this page!'
else
page = 'admin'
renderPage page, req, resp, msg
###
Handles the admin command requests.
*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 handleAdminCommand( *req, resp* )
###
exports.handleAdminCommand = ( req, resp ) =>
if req.session and
2014-04-21 12:42:26 +00:00
req.session.user and
req.session.user.roles.indexOf( "admin" ) > -1
body = ''
req.on 'data', ( data ) ->
body += data
req.on 'end', =>
obj = qs.parse body
@log.info 'RH | Received admin request: ' + obj.command
2014-04-21 12:42:26 +00:00
arrCmd = obj.command.split( ' ' )
if not arrCmd[ 0 ] or not @objAdminCmds[ arrCmd[ 0 ] ]
resp.send 404, 'Command unknown!'
2014-04-21 12:42:26 +00:00
else
arrParams = arrCmd.slice 1
oParams = {}
for keyVal in arrParams
arrKV = keyVal.split ":"
if arrKV.length is 2
oParams[ arrKV[ 0 ] ] = arrKV[ 1 ]
@objAdminCmds[ arrCmd[ 0 ] ] oParams, ( err, obj ) ->
resp.send obj.code, obj
else
resp.send 401, 'You need to be logged in as admin!'
# Parse events and register to user if it's a user specific event
parsePushAndAnswerEvent = ( eventname, username, body, resp ) ->
# Currently we allow JSON and form data to arrive at webhooks.
# TODO We should allow to choose arriving formats, such as xml too
# TODO We should implement body selectors for webhooks as well to
# add flexibility in the way the data arrives
2014-04-26 00:16:06 +00:00
if typeof body is 'string'
try
body = JSON.parse body
catch err
try
2014-04-26 00:16:06 +00:00
body = qs.parse body
catch err
2014-04-26 00:16:06 +00:00
resp.send 400, 'Badly formed event!'
return
2014-04-26 00:16:06 +00:00
obj =
eventname: eventname
body: body
if username
obj.username = username
2014-04-26 00:16:06 +00:00
db.pushEvent obj
resp.send 200, JSON.stringify
message: "Thank you for the event: '#{ eventname }'"
evt: obj
2014-04-26 00:16:06 +00:00
obj
# ###
# Handles measurement posts
# ###
# exports.handleMeasurements = ( req, resp ) =>
# body = ''
# req.on 'data', ( data ) ->
# body += data
# req.on 'end', ->
# obj = parsePushAndAnswerEvent name, null, body, resp
# if obj.eventname is 'uptimestatistics'
# # This is a hack to quickly allow storing of public accessible data
# fPath = path.resolve __dirname, '..', 'webpages', 'public', 'data', 'histochart.json'
# fs.writeFile fPath, JSON.stringify( obj, undefined, 2 ), 'utf8'
2014-04-26 00:16:06 +00:00
###
Handles webhook posts
###
exports.handleWebhooks = ( req, resp ) =>
hookid = req.url.substring( 10 ).split( '/' )[ 0 ]
oHook = @allowedHooks[ hookid ]
if oHook
body = ''
req.on 'data', ( data ) ->
body += data
2014-04-26 00:16:06 +00:00
req.on 'end', () ->
parsePushAndAnswerEvent oHook.hookname, oHook.username, body, resp
2014-04-26 00:16:06 +00:00
else
resp.send 404, "Webhook not existing!"
2014-04-26 00:16:06 +00:00
# Activate a webhook. the body will be JSON parsed, the name of the webhook will
# be the event name given to the event object, a timestamp will be added
exports.activateWebhook = ( user, hookid, name ) =>
2014-04-26 00:16:06 +00:00
@log.info "HL | Webhook '#{ hookid }' activated"
@allowedHooks[ hookid ] =
hookname: name
username: user
2014-04-26 00:16:06 +00:00
2014-04-26 22:37:15 +00:00
2014-04-26 00:16:06 +00:00
# Deactivate a webhook
exports.deactivateWebhook = ( hookid ) =>
@log.info "HL | Webhook '#{ hookid }' deactivated"
delete @allowedHooks[ hookid ]