def handle(self, client): """ The main thread to each connection. will continuously receive messages from a client and perform an operation based on the header value. if header value reads 'bcast', the message is to be sent to every connected client. if header value reads 'dm', the message is private between two clients and is only sent to the user matching the tuple index[1]. Message examples: data = {'head': 'bcast', 'body': ('ExampleUSer1', 'This is my message to be broadcast!')} data = {'head': 'dm', 'body': ('TO_ExampleUSer1', 'FROM_ExampleUser2', 'Hello, this is a private message.')} """ if self.authenticate(client): try: while True: data = do_decrypt(client.key, client.sock.recv(4096)) if data is None: self.disconnect(client) break if data['head'] == 'bcast': for user in self.clients: if user != client: user.sock.sendall(do_encrypt(user.key, data)) elif data['head'] == 'dm': for user in self.clients: if user.fullname == data['body'][0]: user.sock.sendall(do_encrypt(user.key, data)) except ConnectionResetError: self.disconnect(client)
def send(self, **data): """ Create structured message objects and send to the server. Messages are constructed using nested dictionaries and container types to indicate the purpose and destination of a message. Using dictionaries and key/value pairs, every message is constructed with a head & body field. The header indicates the type of message and can be either a broadcast for all to receive or a direct message with a single destination. The body field contains the written message and in the case of Dm's includes the names of the message's recipient and sender. Example messages: Broadcasted message == {'head': 'bcast', 'body': TextObject} Direct message == {'head': 'dm', 'body': ('NameOfDestination, NameOfSource, TextObject)} """ try: if data['head'] == 'bcast': data = {'head': 'bcast', 'body': data['message']} elif data['head'] == 'dm': data = { 'head': 'dm', 'body': (data['recipient'], data['sender'], data['message']) } self.sock.sendall(do_encrypt(self.key, data)) except ConnectionAbortedError: return # server has disconneced or stopped
def login(self, username, password): """ Receives the user's credentials from Login page and packages the information using the set message standard. Enryption is applied and the ciphertext is sent to the server for authentication. The server replies with a boolean indicating success or failiure. """ credentials = {'head': 'login', 'body': (username, password)} encrypted_data = do_encrypt(self.key, credentials) self.sock.sendall(encrypted_data) data = do_decrypt(self.key, self.sock.recv(4096)) if not data: return False else: return data
def is_online(self): """ Broadcasts hidden messages to all connected clients containing information of who is online. The method is a loop with a two seconds sleep interval and use the Client.state attribute. When a client successfully authenticates, his state is first set to 1. This indicates that the client has no prior data of who else is currently connected to the chat room. The method then creates a list containing the fullname of all currently connected clients and sends it out to the client. The new client is now up-to-date with the rest of the connected clients and can be put in state 2. State two is triggered by a new connection setting the is_online_flag to True and creates a list of the newly connected clients (which would have state 1) and sends it out to all previous connections, allowing them to update their display of online clients. State 1: Receives list containing fullname of all current connections. State 2: Receives list containing fullname of the new connections. Example meta message: data = {'head': 'meta', 'body': {'online': ['ExampleName1', ExampleName2', 'ExampleName3']}} """ while True: time.sleep(2) clients = self.clients.copy() # Triggered by a new connection, sends out the fullname of the new client to all # previously connected clients. if self.is_online_flag: for user in clients: if user.state == 2: online = [client.username for client in clients if client.state == 1] data = {'head': 'meta', 'body': {'online': online}} user.sock.sendall(do_encrypt(user.key, data)) self.is_online_flag.clear() # Sends out a list containing fullname of all connected clients, to the new client. for user in clients: if user.state == 1: online = [client.username for client in clients] data = {'head': 'meta', 'body': {'online': online}} user.sock.sendall(do_encrypt(user.key, data)) user.state = 2
def disconnect(self, client): """ Remove a client from inventory and close the socket. A client has either executed a socket.shutdown(1) and is waiting for a (FIN - ACK) or the client application has been killed. Either way, the socket is closed and the client's class object is deleted. When a client disconnects, the server sends a special hidden message out to every connection to update the client applications 'online users' side panel. This message is handled by the clients MainWindow.is_online method. """ client.sock.close() send = {'head': 'meta', 'body': {'offline': client.username}} for user in self.clients: if user != client: user.sock.sendall(do_encrypt(user.key, send)) self.clients.remove(client) logging.info(f'Client disconnected: {client.username}@{client.address[0]}')
def authenticate(self, client): """ Receive client username/password and compare credentials with backend database. The method receives login credentials from the connected client and compares the received credentials with the user data stored in the database. The method has three outcomes: 1. The received username does not match any entries in the chatroom.users table. result: server sends a False boolean back to client application. 2. the received username matches with a table entry but the password hash comparison fails. result: server sends a False boolean back to client application. 3. The received username matches with a table entry and the password hash comparison is successful. result: servers sends a True boolean and the client's full name back to client application. :param client: Client class object established upon connection to server. """ while True: try: data = do_decrypt(client.key, client.sock.recv(4096)) # Unpack received login credentials from the connected client. username = data['body'][0] password = data['body'][1] except (ConnectionResetError, TypeError): client.sock.close() break with self.database_sock.cursor() as cursor: # A SELECT query is performed to return user_id, username, and hashed password from the database, # based upon the username sent by the connected client. If the received username does not match any # table entries, the query will return a NoneType object which and is handled by the try/except # clause below. query = "SELECT USER_NAME, PASSWORD FROM users where USER_NAME=%s" values = (username,) cursor.execute(query, values) try: # Unpacking query results ret_username, ret_password = cursor.fetchone() # Using bcrypt module to perform a hash comparison with on the password hash from the database and # the submitted password from the client. The client is successfully authenticated if both the # password and username matches the database entry. if bcrypt.checkpw(password.encode('utf-8'), ret_password.encode('utf-8')) and ret_username == username: # the class object's attributes are updated accordingly and the connected client recieves his # full name and a boolean signaling the GUI application to enter the MainPage. client.fullname = ret_username # should be removed but too lazy atm client.username = username send = {'head': 'login', 'body': (True, ret_username)} client.sock.sendall(do_encrypt(client.key, send)) self.clients.append(client) self.is_online_flag.append(username) client.state = 1 logging.info(f'Client authenticated: {client.username}@{client.address[0]}') return True else: # else statement is executed if the password does not match with the hash stored in the database. logging.warning(f'Authentication failed - wrong username/password: {username}@{client.address[0]}') send = {'head': 'login', 'body': (False,)} client.sock.sendall(do_encrypt(client.key, send)) continue except TypeError: logging.warning(f'Authentication failed - no user found: {username}@{client.address[0]}') send = {'head': 'login', 'body': (False,)} client.sock.sendall(do_encrypt(client.key, send)) continue