LOC-13 - Encrypt system message payloads.
LOC-13 - Encrypt system message payloads.

This is done for two main reasons - firstly to minimise the amount of obvious plaintext in memory (though it is still base64 encoded, so limited effect there).

Secondly, and more importantly, it allows us to adjust the client so that it will never honour a plaintext JSON message payload.

--- /dev/null
+++ b/client/LocalChatClient.py
@@ -1,1 +1,842 @@
-
+#!/usr/bin/env python
+#
+#
+# Interface based upon https://github.com/izderadicka/xmpp-tester/blob/master/commander.py
+#
+# apt-get install:
+#     python-urwid
+#     python-gnupg
+
+
+import urwid
+from collections import deque
+from threading import Thread
+import threading
+
+import json
+import urllib2
+import ssl
+import string
+import random
+
+import datetime as dt
+
+import gnupg
+
+
+
+# We'll get these from the commandline later
+USER='ben2'
+SERVER='https://127.0.0.1:8090'
+ROOMNAME='BenTest'
+
+
+class msgHandler(object):
+    
+    def __init__(self):
+        self.last = 0
+        self.user = False
+        self.server = SERVER
+        self.room = False
+        self.roompass = False
+        self.sesskey = False
+        self.syskey = False
+        self.gpg = gnupg.GPG()
+    
+    
+    def pollForMessage(self):
+        
+        if not self.room:
+            # We're not connected to a room
+            return False
+        
+        
+        payload = {"roomName": self.room, 
+                   "mylast":self.last,
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"pollMsg",
+                   "payload": json.dumps(payload)
+                   }
+
+        resp = self.sendRequest(request)
+        
+        if resp == "BROKENLINK":
+            return resp
+        
+        
+        if resp['status'] == "unchanged":
+            return False
+        
+        
+        if resp['status'] == "errmessage":
+            # Got an error, we need to stop the poll and then return the text
+            self.room = False
+            self.roompass = False
+            self.sesskey = False
+            return [['reversed',resp['text']]]
+        
+        to_print = []
+        # Otherwise, process the messages
+        for i in resp["messages"]:
+            self.last = i[0]
+            upstruser = i[3]
+            
+            try:
+                msgbody = json.loads(self.decrypt(i[1],upstruser))
+            except:
+                # Means an invalid message was received - LOC-8
+                to_print.append(['error','Received message which could not be decrypted'])
+                continue
+            
+            # TODO - We'll need to json decode and extract the sending user's name
+            # but not currently including that info in my curl tests. Also means test that part of the next block
+            
+            color = "green"
+
+            if upstruser == self.user:
+                # One of our own, change the color
+                color = "blue"
+            
+            elif upstruser == "SYSTEM":
+                color = "magenta"
+                if msgbody['verb'] == "sysalert":
+                    color = 'reversed'
+                elif msgbody['verb'] == 'syswarn':
+                    color = 'cyan'
+                
+            
+            ts = dt.datetime.utcfromtimestamp(i[2]).strftime("[%H:%M:%S]")
+            
+            if msgbody["verb"] == "do":
+                color = 'yellow'
+                line = [
+                    "        ** %s %s **" % (upstruser,msgbody['text'])
+                    ]
+            else:
+                
+                if i[4] != "0":
+                    color = 'brown'
+                    upstruser = 'DM %s' % (upstruser,)
+                
+                line = [
+                    ts, # timestamp
+                    "%s>" % (upstruser,), # To be replaced later
+                    msgbody['text']
+                    ]
+            
+            to_print.append([color,' '.join(line)])
+        
+        return to_print
+        
+        
+    def sendMsg(self,line,verb='say'):
+        ''' Send a message 
+        '''
+        
+        if not self.room:
+            # We're not in a room. Abort
+            return False
+        
+        # Otherwise, build a payload
+        
+        
+        msg = {
+            'user': self.user,
+            'text': line,
+            "verb": verb
+            }
+        
+        payload = {"roomName": self.room, 
+                   "msg":self.encrypt(json.dumps(msg)),
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"sendMsg",
+                   "payload": json.dumps(payload)
+                   }
+
+        resp = self.sendRequest(request)        
+        
+        if resp['status'] == "ok":
+            return True
+        
+        return False
+        
+
+
+    def sendDirectMsg(self,line,user,verb='say'):
+        ''' Send a direct message
+        '''
+        
+        if not self.room:
+            # We're not in a room. Abort
+            return False
+        
+        # Otherwise, build a payload
+        
+        msg = {
+            'user': self.user,
+            'text': line,
+            "verb": verb
+            }
+        
+        payload = {"roomName": self.room, 
+                   "msg":self.encrypt(json.dumps(msg)), # TODO this should use the user's key
+                   "to": user,
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"sendDirectMsg",
+                   "payload": json.dumps(payload)
+                   }
+
+        resp = self.sendRequest(request)        
+        
+        if resp['status'] == "ok":
+            return True
+        
+        return False
+
+
+
+
+    def joinRoom(self,user,room,passw):
+        ''' Join a room
+        '''
+        
+        # We only want to send the user password section of the password
+        p = passw.split(":")
+        userpass = p[1]
+        
+        payload = {"roomName": room, 
+                   "userpass": userpass.encode('utf-8'),
+                   "user": user
+                   }
+        
+        request = {"action":"joinRoom",
+                   "payload": json.dumps(payload)
+                   }        
+
+
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        
+        self.room = room
+        self.user = user
+        self.last = resp['last']
+        self.roompass = p[0] # The room password is the first section of the password
+        self.sesskey = resp['session']
+        self.syskey = resp['syskey']
+        return True
+
+
+
+
+    def leaveRoom(self):
+        ''' Leave the current room
+        '''
+        if not self.room:
+            return False
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"leaveRoom",
+                   "payload": json.dumps(payload)
+                   }        
+
+
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        self.room = False
+        self.user = False
+        self.last = 0
+        self.roompass = False
+        self.sesskey = False
+        
+        return True
+                
+
+
+    def createRoom(self,room,user=False):
+        ''' Create a new room
+        '''
+        
+        if not user and not self.user:
+            return False
+        
+        if not user:
+            user = self.user
+
+
+        # Generate a password for the admin
+        passw = self.genpassw()
+                
+        payload = {"roomName": room, 
+                   "owner": user,
+                   "pass": passw
+                   }
+        
+        request = {"action":"createRoom",
+                   "payload": json.dumps(payload)
+                   }        
+        
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return [resp['name'],passw]
+    
+
+    def closeRoom(self):
+        if not self.room:
+            return False
+
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"closeRoom",
+                   "payload": json.dumps(payload)
+                   }        
+
+        resp = self.sendRequest(request)
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return True
+        
+        
+
+    def inviteUser(self,user):
+        ''' Invite a user into a room
+        
+        #TODO - Authentication
+        '''
+        
+        
+        # Generate a password for the new user
+        passw = self.genpassw()
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "invite": user,
+                   "pass": passw,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":"inviteUser",
+                   "payload": json.dumps(payload)
+                   }    
+        
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return [self.room,self.roompass,passw,user]
+
+
+
+    def kickUser(self,user,ban=False):
+        ''' Kick a user out of the room
+        '''
+        
+        action = 'banUser'
+        
+        if not ban:
+            action = 'kickUser'
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user,
+                   "kick": user,
+                   "sesskey": self.sesskey
+                   }
+        
+        request = {"action":action,
+                   "payload": json.dumps(payload)
+                   }    
+                
+        resp = self.sendRequest(request)
+
+        if resp == "BROKENLINK" or resp['status'] != "ok":
+            return False
+        
+        return True
+
+        
+        
+
+    def sendRequest(self,data):
+        data = json.dumps(data)
+        
+        try:
+            # The cert the other end will be considered invalid
+            #
+            # Ignore it
+            ctx = ssl.create_default_context()
+            ctx.check_hostname = False
+            ctx.verify_mode = ssl.CERT_NONE
+
+            req = urllib2.Request(self.server, data, {'Content-Type': 'application/json'})
+            f = urllib2.urlopen(req,context=ctx)
+            response = f.read()
+            f.close()
+            return json.loads(response)
+        except:
+            return "BROKENLINK"
+
+
+
+    def decrypt(self,msg,sender):
+        ''' Placeholder
+        '''
+                
+        try:       
+            key = self.roompass
+            if sender == "SYSTEM":
+                key = self.syskey
+                
+            return str(self.gpg.decrypt(msg.decode("base64"),passphrase=key))
+        
+        except:
+            return False
+        
+    
+
+    def encrypt(self,msg):
+        ''' Placeholder
+        '''
+        
+        crypted = self.gpg.encrypt(msg,None,passphrase=self.roompass,symmetric="AES256",armor=False)
+        return crypted.data.encode('base64')
+
+
+    def hashpw(self,passw):
+        ''' Placeholder
+        '''
+        return passw
+
+
+    def genpassw(self,N=16):
+        ''' Generate a random string of chars to act as a password
+        '''
+        
+        return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits).encode('utf-8') for _ in range(N))
+
+
+
+class NotInRoom(Exception):
+    def __init__(self,cmd):
+        Exception.__init__(self,'Message not sent')
+
+
+class UnableTo(Exception):
+    def __init__(self,action,cmd):
+        Exception.__init__(self,'Could not %s: %s' % (action,cmd))
+
+
+class InvalidCommand(Exception):
+    def __init__(self,cmd):
+        Exception.__init__(self,'Command is invalid: %s' % (cmd,))
+
+
+class Command(object):
+    """ Base class to manage commands in commander
+similar to cmd.Cmd in standard library
+just extend with do_something  method to handle your commands"""
+
+    def __init__(self,quit_commands=['q','quit','exit'], help_commands=['help','?', 'h']):
+        self._quit_cmd=quit_commands
+        self._help_cmd=help_commands
+        
+    def __call__(self,line):
+        global msg
+        tokens=line.split()
+        cmd=tokens[0].lower()
+        args=tokens[1:]
+        
+        if cmd[0] == "/":
+            # It's a command
+            cmd = cmd[1:]
+            
+            
+            if cmd == "me":
+                #/me [string]
+                r = msg.sendMsg(' '.join(args),'do')
+                if not r:
+                    raise NotInRoom(line)
+                return
+
+            
+            if cmd == "ban":
+                # /kick [user]
+                
+                if len(args) < 1:
+                    raise InvalidCommand(line)
+                
+                
+                m = msg.kickUser(args[0],True)
+                return
+                        
+            
+            if cmd == "join":
+                # /join [room] [password] [username] 
+                
+                if len(args) < 3:
+                    raise InvalidCommand(line)
+                    return
+                
+                if not msg.joinRoom(args[2],args[0],args[1]):
+                    raise UnableTo('join',line)
+                return
+            
+            
+            if cmd == "kick":
+                # /kick [user]
+                
+                if len(args) < 1:
+                    raise InvalidCommand(line)
+                
+                
+                m = msg.kickUser(args[0])
+                return
+            
+            
+            if cmd == "leave":
+                # /leave
+                if not msg.leaveRoom():
+                    raise UnableTo('leave',line)
+                    return
+                
+                global c
+                c.output('Left the room','magenta')
+                return
+
+
+            if cmd == 'msg':
+                # /msg ben Hello ben this is a DM
+                line = ' '.join(args[1:])
+                r = msg.sendDirectMsg(line,args[0])
+                if not r:
+                    raise NotInRoom(line)
+                
+                # Otherwise push a copy of the message to display
+                # cos we won't get this one back from pollMsg
+                global c
+                
+                m = "%s DM %s>%s" % (msg.user,args[0],line)
+                
+                c.output(m,'blue')
+                
+                return
+
+        
+            if cmd == "room":
+                
+                # /room close [roompass]
+                if args[0] == "close":
+                    if len(args) < 2:
+                        raise InvalidCommand('/room close [pass]')
+                    
+                    if not msg.closeRoom():
+                        raise UnableTo('Close Room','')
+                                        
+                    return
+                
+                
+                # /room create [roomname] [[user]]
+                if args[0] == "create":
+                    
+                    if len(args) < 3:
+                        args[2] = False
+                    
+                    n = msg.createRoom(args[1],args[2])
+                    if not n:
+                        raise UnableTo('create room',line)
+                        return
+                    
+                    # Seperate out the return value
+                    rm = n[0]
+                    up = n[1] # user specific password
+                    global c
+                    
+                    # Generate a room password
+                    p = msg.genpassw()
+                    c.output('Created Room %s' %(rm))
+                    c.output('To join the room, do /join %s %s:%s %s' %(args[1],p,up,args[2]))
+                    return
+                
+                elif args[0] == "invite":
+                    if len(args) < 2:
+                        raise InvalidCommand(line)
+                    
+                    n = msg.inviteUser(args[1])
+                    if not n:
+                        raise UnableTo('invite user',line)
+                        return
+                    
+                    global c
+                    c.output('User %s may now join room' %(args[1],))
+                    c.output('To join the room, they should do /join %s %s:%s %s' %(n[0],n[1],n[2],n[3]))
+                    return                                        
+                    
+
+        if cmd in self._quit_cmd:
+            return Commander.Exit
+        elif cmd in self._help_cmd:
+            return self.help(args[0] if args else None)
+        elif hasattr(self, 'do_'+cmd):
+            return getattr(self, 'do_'+cmd)(*args)
+        else:
+            # If it's not a command, then we're trying to send a message
+            r = msg.sendMsg(line)
+            if not r:
+                raise NotInRoom(line)
+            
+        
+    def help(self,cmd=None):
+        def std_help():
+            qc='|'.join(self._quit_cmd)
+            hc ='|'.join(self._help_cmd)
+            res='Type [%s] to quit program\n' % qc
+            res += """Available commands: 
+            
+            /join [room] [password] [username]                          Join a room
+            /leave                                                      Leave current room
+            /room create [roomname] [roompass] [admin user]             New room management 
+            
+            
+            /room invite [user]                                         Invite a user into the current room
+            /me [string]                                                Send an 'action' instead of a message
+            
+            Room Admin commands:
+            
+            /kick [user]                                                Kick a user out of the room (they can return)
+            /ban [user]                                                 Kick a user out and disinvite them (they cannot return)
+            /room close [roompass]                                      Kick all users out and close the room
+            
+            Once in a room, to send a message, just type it.
+            
+            
+            """
+            return res
+        if not cmd:
+            return std_help()
+        else:
+            try:
+                fn=getattr(self,'do_'+cmd)
+                doc=fn.__doc__
+                return doc or 'No documentation available for %s'%cmd
+            except AttributeError:
+                return std_help()
+ 
+class FocusMixin(object):
+    def mouse_event(self, size, event, button, x, y, focus):
+        if focus and hasattr(self, '_got_focus') and self._got_focus:
+            self._got_focus()
+        return super(FocusMixin,self).mouse_event(size, event, button, x, y, focus)    
+    
+class ListView(FocusMixin, urwid.ListBox):
+    def __init__(self, model, got_focus, max_size=None):
+        urwid.ListBox.__init__(self,model)
+        self._got_focus=got_focus
+        self.max_size=max_size
+        self._lock=threading.Lock()
+        
+    def add(self,line):
+        with self._lock:
+            was_on_end=self.get_focus()[1] == len(self.body)-1
+            if self.max_size and len(self.body)>self.max_size:
+                del self.body[0]
+            self.body.append(urwid.Text(line))
+            last=len(self.body)-1
+            if was_on_end:
+                self.set_focus(last,'above')
+        
+    
+
+class Input(FocusMixin, urwid.Edit):
+    signals=['line_entered']
+    def __init__(self, got_focus=None):
+        urwid.Edit.__init__(self)
+        self.history=deque(maxlen=1000)
+        self._history_index=-1
+        self._got_focus=got_focus
+    
+    def keypress(self, size, key):
+        if key=='enter':
+            line=self.edit_text.strip()
+            if line:
+                urwid.emit_signal(self,'line_entered', line)
+                self.history.append(line)
+            self._history_index=len(self.history)
+            self.edit_text=u''
+        if key=='up':
+            
+            self._history_index-=1
+            if self._history_index< 0:
+                self._history_index= 0
+            else:
+                self.edit_text=self.history[self._history_index]
+        if key=='down':
+            self._history_index+=1
+            if self._history_index>=len(self.history):
+                self._history_index=len(self.history) 
+                self.edit_text=u''
+            else:
+                self.edit_text=self.history[self._history_index]
+        else:
+            urwid.Edit.keypress(self, size, key)
+        
+
+
+class Commander(urwid.Frame):
+    """ Simple terminal UI with command input on bottom line and display frame above
+similar to chat client etc.
+Initialize with your Command instance to execute commands
+and the start main loop Commander.loop().
+You can also asynchronously output messages with Commander.output('message') """
+
+    class Exit(object):
+        pass
+    
+    PALLETE=[('reversed', urwid.BLACK, urwid.LIGHT_GRAY),
+              ('normal', urwid.LIGHT_GRAY, urwid.BLACK),
+              ('error', urwid.LIGHT_RED, urwid.BLACK),
+              ('green', urwid.DARK_GREEN, urwid.BLACK),
+              ('blue', urwid.LIGHT_BLUE, urwid.BLACK),
+              ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), 
+              ('yellow', urwid.YELLOW, urwid.BLACK), 
+              ('cyan', urwid.LIGHT_CYAN, urwid.BLACK), 
+              ('brown', urwid.BROWN, urwid.BLACK), 
+              
+              ]
+    
+    
+    def __init__(self, title, command_caption='Message:  (Tab to switch focus to upper frame, where you can scroll text)', cmd_cb=None, max_size=1000):
+        self.header=urwid.Text(title)
+        self.model=urwid.SimpleListWalker([])
+        self.body=ListView(self.model, lambda: self._update_focus(False), max_size=max_size )
+        self.input=Input(lambda: self._update_focus(True))
+        foot=urwid.Pile([urwid.AttrMap(urwid.Text(command_caption), 'reversed'),
+                        urwid.AttrMap(self.input,'normal')])
+        urwid.Frame.__init__(self, 
+                             urwid.AttrWrap(self.body, 'normal'),
+                             urwid.AttrWrap(self.header, 'reversed'),
+                             foot)
+        self.set_focus_path(['footer',1])
+        self._focus=True
+        urwid.connect_signal(self.input,'line_entered',self.on_line_entered)
+        self._cmd=cmd_cb
+        self._output_styles=[s[0] for s in self.PALLETE]
+        self.eloop=None
+        
+    def loop(self, handle_mouse=False):
+        self.eloop=urwid.MainLoop(self, self.PALLETE, handle_mouse=handle_mouse)
+        self._eloop_thread=threading.current_thread()
+        self.eloop.run()
+        
+    def on_line_entered(self,line):
+        if self._cmd:
+            try:
+                res = self._cmd(line)
+            except Exception,e:
+                self.output('Error: %s'%e, 'error')
+                return
+            if res==Commander.Exit:
+                raise urwid.ExitMainLoop()
+            elif res:
+                self.output(str(res))
+        else:
+            if line in ('q','quit','exit'):
+                raise urwid.ExitMainLoop()
+            else:
+                self.output(line)
+    
+    def output(self, line, style=None):
+        if style and style in self._output_styles:
+                line=(style,line) 
+        self.body.add(line)
+        #since output could be called asynchronously form other threads we need to refresh screen in these cases
+        if self.eloop and self._eloop_thread != threading.current_thread():
+            self.eloop.draw_screen()
+        
+        
+    def _update_focus(self, focus):
+        self._focus=focus
+        
+    def switch_focus(self):
+        if self._focus:
+            self.set_focus('body')
+            self._focus=False
+        else:
+            self.set_focus_path(['footer',1])
+            self._focus=True
+        
+    def keypress(self, size, key):
+        if key=='tab':
+            self.switch_focus()
+        return urwid.Frame.keypress(self, size, key)
+        
+
+
+    
+if __name__=='__main__':
+    
+    
+    msg = msgHandler()
+    
+    
+    class TestCmd(Command):
+        def do_echo(self, *args):
+            '''echo - Just echos all arguments'''
+            return ' '.join(args)
+        def do_raise(self, *args):
+            raise Exception('Some Error')
+        
+    c=Commander('LocalChat', cmd_cb=TestCmd())
+    
+    #Test asynch output -  e.g. comming from different thread
+    import time
+    def run():
+        state=1
+        while True:
+            time.sleep(1)
+            
+            m = msg.pollForMessage()
+            
+            if m == "BROKENLINK":
+                if state == 1:
+                    c.output("Server went away", 'Red')
+                    state = 0
+                continue
+                
+            if m:
+                state = 1
+                for i in m:
+                    c.output(i[1], i[0])
+                
+    t=Thread(target=run)
+    t.daemon=True
+    t.start()
+    
+    #start main loop
+    c.loop()
+        
+

--- a/server/LocalChat.py
+++ b/server/LocalChat.py
@@ -5,7 +5,9 @@
 #
 #
 # apt-get install:
-#    python-flask
+#   python-flask
+#   python-openssl
+#   python-bcrypt
 #
 
 from flask import Flask
@@ -17,6 +19,10 @@
 import time
 import os
 import json
+import bcrypt
+import random
+import string
+import gnupg
 
 app = Flask(__name__)
 
@@ -38,13 +44,11 @@
     a = msghandler.processSubmission(reqjson)
     
     # Check the status
-    if a in [400]:
+    if a in [400,403,500]:
         response = make_response("",a)
         return response
         
     return json.dumps(a)
-
-    
 
 
 class MsgHandler(object):
@@ -53,6 +57,9 @@
     def __init__(self):
         self.conn = False
         self.cursor = False
+        # Generate a key for encryption of SYSTEM messages (LOC-13)
+        self.syskey = self.genpassw(16)
+        self.gpg = gnupg.GPG()
 
 
 
@@ -65,15 +72,16 @@
         sql = """ CREATE TABLE rooms (
             id INTEGER PRIMARY KEY,
             name TEXT NOT NULL UNIQUE,
-            owner TEXT NOT NULL,
-            pass TEXT NOT NULL
+            owner TEXT NOT NULL
         );
         
         
         CREATE TABLE messages (
-            id INTEGER PRIMARY KEY,
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
             ts INTEGER NOT NULL,
             room INTEGER NOT NULL,
+            user NOT NULL,
+            touser TEXT DEFAULT '0',
             msg TEXT NOT NULL
         );
         
@@ -81,6 +89,24 @@
         CREATE TABLE users (
             username TEXT NOT NULL,
             room INTEGER NOT NULL,
+            active INTEGER DEFAULT 0,
+            passhash TEXT NOT NULL,
+            PRIMARY KEY (username,room)
+        );
+        
+        
+        CREATE TABLE sessions (
+            username TEXT NOT NULL,
+            sesskey TEXT NOT NULL,
+            PRIMARY KEY(sesskey)
+        );
+        
+        
+        CREATE TABLE failuremsgs (
+            username TEXT NOT NULL,
+            room INTEGER NOT NULL,
+            expires INTEGER NOT NULL,
+            msg TEXT NOT NULL,
             PRIMARY KEY (username,room)
         );
         
@@ -102,7 +128,7 @@
         
         print reqjson
         if "action" not in reqjson or "payload" not in reqjson:
-            return 400
+            return self.returnFailure(400)
         
         
         # Decrypt the payload
@@ -111,14 +137,38 @@
         try:
             reqjson['payload'] = json.loads(reqjson['payload'])
         except:
-            return 400
+            return self.returnFailure(400)
         
         if reqjson['action'] == "createRoom":
             return self.createRoom(reqjson)
         
+        if reqjson['action'] == "closeRoom":
+            return self.closeRoom(reqjson)
+        
+        elif reqjson['action'] == "joinRoom":
+            return self.processjoinRoom(reqjson)
+        
+        elif reqjson['action'] == "leaveRoom":
+            return self.processleaveRoom(reqjson)
+
+        elif reqjson['action'] == "banUser":
+            return self.kickUser(reqjson,True)
+        
         elif reqjson['action'] == "inviteUser":
             return self.inviteUser(reqjson)
         
+        elif reqjson['action'] == "kickUser":
+            return self.kickUser(reqjson,False)
+        
+        elif reqjson['action'] == 'sendDirectMsg':
+            return self.sendDirectMsg(reqjson)
+        
+        elif reqjson['action'] == 'sendMsg':
+            return self.sendMsg(reqjson)
+        
+        elif reqjson['action'] == 'pollMsg':
+            return self.fetchMsgs(reqjson)
+         
         
         
         
@@ -152,19 +202,35 @@
         }'
         
         '''
+        
+        # Validate the request
+        #
+        # All validation snippets will change to this format soon
+        required = ['roomName','owner','pass']
+        for i in required:
+            if i not in reqjson['payload']:
+                return self.returnFailure(400)
+        
         print "Creating room %s" % (reqjson['payload'])
         
         # Create a tuple for sqlite3
         t = (reqjson['payload']['roomName'],
-             reqjson['payload']['owner'],
-             reqjson['payload']['passhash'])
+             reqjson['payload']['owner']
+             )
         
         try:
-            self.cursor.execute("INSERT INTO rooms (name,owner,pass) VALUES (?,?,?)",t)
+            self.cursor.execute("INSERT INTO rooms (name,owner) VALUES (?,?)",t)
             roomid = self.cursor.lastrowid
         except:
             # Probably a duplicate name, but we don't want to give the other end a reason anyway
-            return 500
+            return self.returnFailure(500)
+        
+        
+        # Generate a password hash for the owners password
+        passhash = bcrypt.hashpw(reqjson['payload']['pass'].encode('utf-8'),bcrypt.gensalt())
+        
+        self.cursor.execute("INSERT INTO users (username,room,passhash) values (?,?,?)",(reqjson['payload']['owner'],roomid,passhash))
+        self.conn.commit()
         
         return {
                 'status':'ok',
@@ -172,33 +238,388 @@
                 'name' : reqjson['payload']['roomName']
             }
         
-    
+
+
+    def closeRoom(self,reqjson):
+        ''' Close a room.
+        
+        Means we need to
+        
+        - Ban all the users
+        - Scrub the message queue
+        - Remove the room record
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        
+        # Check the requesting user is the admin
+        self.cursor.execute("SELECT * from rooms where id=? and owner=?",(room,reqjson["payload"]["user"]))
+        n = self.cursor.fetchone()
+        
+        if not n:
+            return self.returnFailure(403)
+        
+
+        self.pushSystemMsg("Room has been closed. Buh-Bye",room,'syswarn')
+                
+        self.cursor.execute("DELETE FROM users where room=?",(room,))
+        self.cursor.execute("DELETE FROM rooms where id=?",(room,))
+        self.cursor.execute("DELETE FROM messages where room=?",(room,))
+        self.cursor.execute("DELETE FROM sessions where sesskey like ?", (reqjson['payload']["roomName"] + '-%',))
+        
+        self.conn.commit()
+        
+        return { "status" : "ok" }
+    
+        
+           
+        
 
 
     def inviteUser(self,reqjson):
         ''' Link a username into a room
-
-
-        curl -v -X POST http://127.0.0.1:8090/ -H "Content-Type: application/json" --data '{"action":"inviteUser","payload":"{\"roomName\":\"BenTest\",\"user\":\"ben2\"}"}'
-
-        '''
-        
-        if "roomName" not in reqjson['payload']:
-            return 400
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "pass" not in reqjson['payload'] or "invite" not in reqjson['payload']:
+            return self.returnFailure(400)
         
         room = self.getRoomID(reqjson['payload']["roomName"])
         
         if not room:
-            return 400
+            return self.returnFailure(400)
+        
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403,reqjson['payload'],room)
+        
+        
+        if reqjson['payload']['invite'] == "SYSTEM":
+            # Push a notification into the group
+            self.pushSystemMsg("ALERT: User %s tried to invite SYSTEM" % (reqjson['payload']['user']),room,'sysalert')
+            return self.returnFailure(403)
+       
+        
+        # Generate a hash of the submitted password
+        passhash = bcrypt.hashpw(reqjson['payload']['pass'].encode('utf-8'),bcrypt.gensalt())
         
         # Otherwise, link the user in
-        self.cursor.execute("INSERT INTO users (username,room) values (?,?)",(reqjson['payload']['user'],room))
+        self.cursor.execute("INSERT INTO users (username,room,passhash) values (?,?,?)",(reqjson['payload']['invite'],room,passhash))
+        
+        # Push a notification into the group
+        self.pushSystemMsg("User %s invited %s to the room" % (reqjson['payload']['user'],reqjson['payload']['invite']),room)
         
         return {
                 "status":'ok'
             }
         
-        
+    
+    
+    def kickUser(self,reqjson,ban=False):
+        ''' Kick a user out of room
+        
+        Default is just to boot them out, but can also remove their authorisation to enter
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload'] or "kick" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        
+        # Check the requesting user is the admin
+        self.cursor.execute("SELECT * from rooms where id=? and owner=?",(room,reqjson["payload"]["user"]))
+        n = self.cursor.fetchone()
+        
+        if not n:
+            return self.returnFailure(403)
+        
+        
+        
+        self.cursor.execute("UPDATE users set active=0 where room=? and username=?",(room,reqjson["payload"]["kick"]))
+        
+        # Delete their session
+        self.cursor.execute("DELETE FROM sessions where username=? and sesskey like ?", (reqjson['payload']['kick'],reqjson['payload']["roomName"] + '-%'))
+        
+        self.pushSystemMsg("User %s kicked %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick']),room,'syswarn')
+        
+        self.pushFailureMessage(reqjson['payload']['kick'],room,'You have been kicked from the room')
+        
+        
+        if ban:
+            # If we're banning them, also need to disinvite them
+            self.cursor.execute("DELETE from users where room=? and username=?",(room,reqjson["payload"]["kick"]))
+            self.pushSystemMsg("User %s banned %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick']),room,'syswarn')
+            
+        return { "status" : "ok" }
+    
+
+    
+    def processjoinRoom(self,reqjson):
+        ''' Process a request from a user to login to a room
+        
+        Not yet defined the authentication mechanism to use, so that's a TODO
+        '''
+
+        # Check the required information is present
+        required = ['roomName','user','userpass']
+        for i in required:
+            if i not in reqjson['payload']:
+                return self.returnFailure(400)
+                
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        
+        if reqjson["payload"]["user"] == "SYSTEM":
+            return self.returnFailure(403)
+        
+        # Check whether that user is authorised to connect to that room
+        self.cursor.execute("SELECT username, room,passhash from users where username=? and room=?",(reqjson['payload']['user'],room))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return { "status": "NOK" }
+        
+        
+        # Now we need to verify they've supplied a correct password for that user
+        stored = r[2].encode("utf-8")
+        if stored != bcrypt.hashpw(reqjson['payload']['userpass'].encode('utf-8'),stored):
+            return self.returnFailure(403)
+        
+            
+        # Tidy older messages away.
+        #
+        # We do this so that a user who joins can't then send a poll with last:0 to retrieve the full history
+        #
+        # Basically, anything older than 10 seconds should go. Users who were already present will be able
+        # to scroll up and down in their client anyway
+        self.tidyMsgs(time.time()-10,room)
+        
+        
+        # Push a message to the room to note that the user joined
+        msgid = self.pushSystemMsg("User %s joined the room" % (reqjson['payload']['user']),room)
+
+        # Check the latest message ID for that room
+        self.cursor.execute("SELECT id from messages WHERE room=? and id != ? ORDER BY id DESC",(room,msgid))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            last = 0
+        else:
+            last = r[0]
+                
+        # Mark the user as active in the users table
+        self.cursor.execute("UPDATE users set active=1 where username=? and room=?", (reqjson['payload']['user'],room))
+        
+        
+        # Create a session for the user
+        sesskey = "%s-%s" % (reqjson['payload']["roomName"],self.genSessionKey())
+        self.cursor.execute("INSERT INTO sessions (username,sesskey) values (?,?)", (reqjson['payload']['user'],sesskey))
+        self.conn.commit()
+                
+        return {"status":"ok","last":last,"session":sesskey,"syskey":self.syskey}
+        
+        
+    def processleaveRoom(self,reqjson):
+        ''' Process a user's request to leave a room
+        '''
+        if "roomName" not in reqjson['payload'] or "user" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return self.returnFailure(400)
+        
+        # Check the user is actually in the room and authorised
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(400)
+        
+        # Mark them as not in the room
+        self.cursor.execute("UPDATE users set active=0 where username=? and room=?", (reqjson['payload']['user'],room))
+        self.conn.commit()
+        
+        # Delete their session
+        self.cursor.execute("DELETE FROM sessions where username=? and sesskey = ?", (reqjson['payload']['user'],reqjson['payload']["sesskey"]))
+        
+        # Push a message to the room to note they left
+        self.pushSystemMsg("User %s left the room" % (reqjson['payload']['user']),room)
+        return {"status":"ok"}
+    
+    
+    def sendMsg(self,reqjson):
+        ''' Push a message into a room
+        
+        curl -v -X POST http://127.0.0.1:8090/ -H "Content-Type: application/json" --data '{"action":"sendMsg","payload":"{\"roomName\":\"BenTest\", \"msg\":\"ENCRYPTED-DATA\",\"user\":\"ben2\"}"}'
+        
+        '''
+        
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403)
+        
+        
+        if "roomName" not in reqjson['payload'] or "msg" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        print room
+        if not room:
+            return self.returnFailure(400)
+
+            
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user) VALUES (?,?,?,?)",(time.time(),room,reqjson['payload']['msg'],reqjson['payload']['user']))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()
+        
+        # Check the latest message ID for that room
+        self.cursor.execute("SELECT id from messages WHERE room=? and id != ? ORDER BY id DESC",(room,msgid))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            last = 0
+        else:
+            last = r[0]
+        
+        return {
+                "status" : "ok",
+                "msgid" : msgid,
+                "last" : last
+            }
+        
+
+
+    def sendDirectMsg(self,reqjson):
+        ''' Push a message to a user in the same room
+        '''
+        
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403)
+                
+        required = ['roomName','msg','to']
+        for i in required:
+            if i not in reqjson['payload']:
+                return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        print room
+        if not room:
+            return self.returnFailure(400)
+
+        # Check the other user is in the room and active
+        self.cursor.execute("SELECT username from users where username=? and room=? and active=1",(reqjson['payload']['to'],room))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return self.returnFailure(400)
+            
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user,touser) VALUES (?,?,?,?,?)",(time.time(),room,reqjson['payload']['msg'],reqjson['payload']['user'],reqjson['payload']['to']))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()
+        
+        # Check the latest message ID for that room
+        self.cursor.execute("SELECT id from messages WHERE room=? and id != ? ORDER BY id DESC",(room,msgid))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            last = 0
+        else:
+            last = r[0]
+        
+        return {
+                "status" : "ok",
+                "msgid" : msgid,
+                "last" : last
+            }
+        
+
+
+        
+    def fetchMsgs(self,reqjson):
+        ''' Check to see if there are any new messages in the room
+        
+        curl -v -X POST http://127.0.0.1:8090/ -H "Content-Type: application/json" --data '{"action":"pollMsg","payload":"{\"roomName\":\"BenTest\", \"mylast\":1,\"user\":\"ben2\"}"}'
+        
+        '''
+
+        if "mylast" not in reqjson['payload']:
+            return self.returnFailure(400)
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        print room
+        if not room:
+            return self.returnFailure(400)
+
+
+        if not self.validateUser(reqjson['payload']):
+            return self.returnFailure(403,reqjson['payload'],room)
+        
+        self.cursor.execute("""SELECT id,msg,ts,user,touser FROM messages
+            WHERE room=? AND
+            (touser = 0 OR touser = ?) AND 
+            id > ?
+            ORDER BY ts ASC           
+            """,(room,reqjson['payload']['user'],reqjson['payload']['mylast']))
+        
+        r = self.cursor.fetchall()
+        
+        if not r:
+            # No changes
+            return {"status":"unchanged","last":reqjson['payload']['mylast']}
+        
+        # Otherwise, return the messages
+        return {"status":"updated",
+                "messages" : r
+                }
+    
+    
+        
+        
+        
+    
+    def validateUser(self,payload):
+        ''' Placeholder for now. Auth will be handled in more depth later
+        '''
+        if "user" not in payload or "roomName" not in payload:
+            return False
+        
+        
+        # Validate the session information
+        self.cursor.execute("SELECT username from sessions where username=? and sesskey=?",(payload['user'],payload['sesskey']))
+        r = self.cursor.fetchone();
+        
+        if not r:
+            return False
+        
+        
+        room = self.getRoomID(payload["roomName"])
+        if not room:
+            return False        
+        
+        
+        
+        # Check whether the user has been marked as active
+        self.cursor.execute("SELECT username, room from users where username=? and room=? and active=1",(payload['user'],room))
+        r = self.cursor.fetchone()
+        
+        if not r:
+            return False
+        
+        return True
+
+
+    
     def getRoomID(self,roomname):
         ''' Get a room's ID from its name
         '''
@@ -212,6 +633,105 @@
         return r[0]
     
 
+    def tidyMsgs(self,thresholdtime,room=False):
+        ''' Remove messages older than the threshold time
+        '''
+        
+        if room:
+            # Tidy from a specific room
+            self.cursor.execute("DELETE FROM messages where ts < ? and room = ?",(thresholdtime,room))
+            self.conn.commit()
+            
+        else:
+            self.cursor.execute("DELETE FROM messages where ts < ?",(thresholdtime,))
+            self.conn.commit()
+
+
+
+    def pushSystemMsg(self,msg,room,verb="sysinfo"):
+        ''' Push a message from the SYSTEM user into the queue
+        '''
+        m = {
+            "text":msg,
+            "verb":verb
+        }
+        
+        m = self.encryptSysMsg(json.dumps(m))
+        
+        self.cursor.execute("INSERT INTO messages (ts,room,msg,user) VALUES (?,?,?,'SYSTEM')",(time.time(),room,m))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()
+        return msgid
+
+
+    def pushFailureMessage(self,user,room,msg):
+        ''' Record a failure message against a user
+        
+        See LOC-14
+        
+        '''
+        self.cursor.execute("INSERT INTO failuremsgs (username,room,expires,msg) values (?,?,?,?)",(user,room,time.time() + 300,msg))
+        self.conn.commit()
+        
+        
+
+
+    def returnFailure(self,status,reqjson=False,room=False):
+        ''' For whatever reason, a request isn't being actioned. We need to return a status code
+        
+        However, in some instances, we may allow a HTTP 200 just once in order to send the user
+        information on why their next request will fail 
+        '''
+        
+        # TODO - implement the failure handling stuff
+        
+        if reqjson and room:
+            # Check whether there's a failure message or not 
+            self.cursor.execute("SELECT msg from failuremsgs where username=? and room=?",(reqjson['user'],room))
+            r = self.cursor.fetchone()
+            
+            if not r:
+                # No message to return
+                return status
+            
+            # Otherwise, return the message and remove it
+            self.cursor.execute("DELETE from failuremsgs where username=? and room=?",(reqjson['user'],room))
+            self.conn.commit()
+            return {"status":"errmessage","text":r[0]}
+        
+        
+        return status
+        
+
+
+    def encryptSysMsg(self,msg):
+        ''' Encrypt a message from system - LOC-13
+        
+            This isn't so much for protection of the data in memory (as the key is also in memory) as it
+            is to protect against a couple of things you could otherwise do in the client. See LOC-13 for
+            more info.
+        
+        '''
+        
+        crypted = self.gpg.encrypt(msg,None,passphrase=self.syskey,symmetric="AES256",armor=False)
+        return crypted.data.encode('base64')
+        
+
+
+    def genSessionKey(self,N=128):
+        return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits).encode('utf-8') for _ in range(N))
+
+
+
+    def genpassw(self,N=16):
+        ''' Generate a random string of chars to act as an encryption password
+        '''
+        
+        return ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits).encode('utf-8') for _ in range(N))
+
+    
+
+
 
     def test(self):
         return ['foo']
@@ -226,5 +746,5 @@
 if __name__ == '__main__':
     # Bind to PORT if defined, otherwise default to 8090.
     port = int(os.environ.get('PORT', 8090))
-    app.run(host='0.0.0.0', port=port,debug=True)
-
+    app.run(host='0.0.0.0', port=port,debug=True,ssl_context='adhoc')
+