LOC-2 joinRoom now generates and returns a 1024 bit session key
LOC-2 joinRoom now generates and returns a 1024 bit session key

When the user successfully joins a room, a 1024 bit session key is generated and returned. The client stores this for use with later requests.

It's use isn't currently enforced (coming soon) and the table isn't currently tidied when a user leaves/is kicked (again coming soon).

The session key is prefixed by the room id, in the format

[roomid]-[sesskey]

The idea being that when a room is closed, it's trivial to remove all related sessions.

My concern here, though, is that it may leak how many rooms there are. If your session key starts 10 then you know there are (or have been) 10 rooms (including yours).

So, either we need to change the prefix to be the room name, or perhaps just have the server seed the room IDs with a random number at startup.

The problem with the latter is that we risk running out of IDs if the number's truly unbounded, and if it is bounded and we're near the upper bound you can still take an educated guess.

So it's probably better to switch using the room name in hindsight. Will change that next

--- a/client/LocalChatClient.py
+++ b/client/LocalChatClient.py
@@ -5,6 +5,7 @@
 #
 # apt-get install:
 #     python-urwid
+#     python-gnupg
 
 
 import urwid
@@ -14,13 +15,19 @@
 
 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='http://127.0.0.1:8090'
+SERVER='https://127.0.0.1:8090'
 ROOMNAME='BenTest'
 
 
@@ -28,9 +35,12 @@
     
     def __init__(self):
         self.last = 0
-        self.user = USER
+        self.user = False
         self.server = SERVER
-        self.room = ROOMNAME
+        self.room = False
+        self.roompass = False
+        self.sesskey = False
+        self.gpg = gnupg.GPG()
     
     
     def pollForMessage(self):
@@ -62,24 +72,34 @@
         # Otherwise, process the messages
         for i in resp["messages"]:
             self.last = i[0]
-            msgbody = self.decrypt(i[1])
+            
+            try:
+                msgbody = json.loads(self.decrypt(i[1]))
+            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"
-            upstruser = 'foo' # Temporary placeholder
-            
-            if upstruser == USER:
+            upstruser = msgbody['user'] # Temporary placeholder
+            
+            if upstruser == self.user:
                 # One of our own, change the color
-                color = "red"
+                color = "blue"
+            
+            elif upstruser == "SYSTEM":
+                color = "magenta"
+                
             
             ts = dt.datetime.utcfromtimestamp(i[2]).strftime("[%H:%M:%S]")
             
             line = [
                 ts, # timestamp
                 "%s>" % (upstruser,), # To be replaced later
-                msgbody
+                msgbody['text']
                 ]
             
             to_print.append([color,' '.join(line)])
@@ -87,14 +107,224 @@
         return to_print
         
         
-
+    def sendMsg(self,line):
+        ''' 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
+            }
+        
+        payload = {"roomName": self.room, 
+                   "msg":self.encrypt(json.dumps(msg)),
+                   "user": self.user
+                   }
+        
+        request = {"action":"sendMsg",
+                   "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']
+        return True
+
+
+
+
+    def leaveRoom(self):
+        ''' Leave the current room
+        '''
+        if not self.room:
+            return False
+        
+        payload = {"roomName": self.room, 
+                   "user": self.user
+                   }
+        
+        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
+        
+        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,
+                   }
+        
+        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
+                   }
+        
+        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
+                   }
+        
+        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)
+            f = urllib2.urlopen(req,context=ctx)
             response = f.read()
             f.close()
             return json.loads(response)
@@ -106,15 +336,53 @@
     def decrypt(self,msg):
         ''' Placeholder
         '''
-        return msg
-    
-
-
-
-
-class UnknownCommand(Exception):
+        
+        try:
+            # Check if it's json as it may be a system message
+            json.loads(msg)
+            return msg
+        
+        except:
+            return str(self.gpg.decrypt(msg.decode("base64"),passphrase=self.roompass))
+        
+    
+
+    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,'Uknown command: %s'%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
@@ -126,9 +394,110 @@
         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 == "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 == "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:
@@ -136,16 +505,37 @@
         elif hasattr(self, 'do_'+cmd):
             return getattr(self, 'do_'+cmd)(*args)
         else:
-            raise UnknownCommand(cmd)
+            # 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] command_name to get more help about particular command\n' % hc
-            res+='Type [%s] to quit program\n' % qc
-            cl=[name[3:] for name in dir(self) if name.startswith('do_') and len(name)>3]
-            res += 'Available commands: %s' %(' '.join(sorted(cl)))
+            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
+            
+            
+            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()
@@ -235,7 +625,7 @@
               ('magenta', urwid.DARK_MAGENTA, urwid.BLACK), ]
     
     
-    def __init__(self, title, command_caption='Command:  (Tab to switch focus to upper frame, where you can scroll text)', cmd_cb=None, max_size=1000):
+    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 )
@@ -321,16 +711,20 @@
     #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":
-                c.output("Server went away", 'Red')
+                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])
                 

--- 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,9 @@
 import time
 import os
 import json
+import bcrypt
+import random
+import string
 
 app = Flask(__name__)
 
@@ -38,13 +43,11 @@
     a = msghandler.processSubmission(reqjson)
     
     # Check the status
-    if a in [400,403]:
+    if a in [400,403,500]:
         response = make_response("",a)
         return response
         
     return json.dumps(a)
-
-    
 
 
 class MsgHandler(object):
@@ -65,13 +68,12 @@
         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,
             msg TEXT NOT NULL
@@ -81,7 +83,16 @@
         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)
         );
         
         """
@@ -116,8 +127,23 @@
         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'] == 'sendMsg':
             return self.sendMsg(reqjson)
@@ -158,22 +184,34 @@
         }'
         
         '''
+        
+        # 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 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
         
         
-        self.cursor.execute("INSERT INTO users (username,room) values (?,?)",(reqjson['payload']['owner'],roomid))
+        # 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 {
@@ -184,16 +222,56 @@
         
     
 
+    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 400
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return 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 403
+        
+        m = {
+            "user":"SYSTEM",
+            "text":"Room has been closed. Buh-Bye"
+        }
+        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m))) 
+        self.conn.commit()
+        
+        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.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']:
+        '''
+        
+        if "roomName" not in reqjson['payload'] or "pass" not in reqjson['payload'] or "invite" not in reqjson['payload']:
             return 400
         
         room = self.getRoomID(reqjson['payload']["roomName"])
@@ -201,13 +279,199 @@
         if not room:
             return 400
         
+        if not self.validateUser(reqjson['payload']):
+            return 403
+        
+        
+        if reqjson['payload']['invite'] == "SYSTEM":
+            # Push a notification into the group
+            m = {
+                    "user":"SYSTEM",
+                    "text":"ALERT: User %s tried to invite SYSTEM" % (reqjson['payload']['user'])
+                }
+            self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m))) 
+            self.conn.commit()
+            return 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.conn.commit()
+        self.cursor.execute("INSERT INTO users (username,room,passhash) values (?,?,?)",(reqjson['payload']['invite'],room,passhash))
+        
+        # Push a notification into the group
+        m = {
+                "user":"SYSTEM",
+                "text":"User %s invited %s to the room" % (reqjson['payload']['user'],reqjson['payload']['invite'])
+            }
+        
+        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))        
+        self.conn.commit()        
+        
         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 400
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return 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 403
+        
+        
+        
+        self.cursor.execute("UPDATE users set active=0 where room=? and username=?",(room,reqjson["payload"]["kick"]))
+        m = {
+                "user":"SYSTEM",
+                "text":"User %s kicked %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick'])
+            }
+        
+        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()
+                    
+        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"]))
+            m = {
+                    "user":"SYSTEM",
+                    "text":"User %s banned %s from the room" % (reqjson['payload']['user'],reqjson['payload']['kick'])
+                }
+            
+            self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
+            self.conn.commit()
+        
+        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 400
+                
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return 400
+        
+        
+        if reqjson["payload"]["user"] == "SYSTEM":
+            return 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 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
+        
+        m = {
+                "user":"SYSTEM",
+                "text":"User %s joined the room" % (reqjson['payload']['user'])
+            }
+        
+        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
+        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]
+                
+        # 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" % (room,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}
+        
+        
+    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 400
+        
+        room = self.getRoomID(reqjson['payload']["roomName"])
+        
+        if not room:
+            return 400
+        
+        # Check the user is actually in the room and authorised
+        if not self.validateUser(reqjson['payload']):
+            return 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()
+        
+        # Push a message to the room to note they left
+        m = {
+                "user":"SYSTEM",
+                "text":"User %s left the room" % (reqjson['payload']['user'])
+            }
+        
+        self.cursor.execute("INSERT INTO messages (ts,room,msg) VALUES (?,?,?)",(time.time(),room,json.dumps(m)))
+        msgid = self.cursor.lastrowid
+        self.conn.commit()        
+        
+        return {"status":"ok"}
     
     
     def sendMsg(self,reqjson):
@@ -268,7 +532,7 @@
         if not room:
             return 400        
         
-        self.cursor.execute("""SELECT id,msg FROM messages
+        self.cursor.execute("""SELECT id,msg,ts FROM messages
             WHERE room=? AND
             id > ?
             ORDER BY ts ASC           
@@ -291,13 +555,28 @@
         
     
     def validateUser(self,payload):
-        ''' Placeholder for now. Auth will be handled later
-        '''
-        if "user" not in payload:
+        ''' Placeholder for now. Auth will be handled in more depth later
+        '''
+        if "user" not in payload or "roomName" not in payload:
             return False
         
+        
+        room = self.getRoomID(payload["roomName"])
+        if not room:
+            return 400        
+        
+        
+        
+        # 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
@@ -312,6 +591,23 @@
         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 genSessionKey(self,N=128):
+        return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits).encode('utf-8') for _ in range(N))
+
 
     def test(self):
         return ['foo']
@@ -326,5 +622,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')
+