From e1d31cc2917731279355d8b0a83f78a1c1b2d3b4 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Wed, 27 Mar 2019 17:23:04 +0100 Subject: [PATCH] hardening server --- connection_handler.py | 18 +++++++------ database_connection.py | 20 +++++++++----- main.py | 23 +++++++++------- match.py | 6 +++-- match_manager.py | 61 ++++++++++++++++++++++-------------------- session_manager.py | 57 ++++++++++++++++++++------------------- tools.py | 5 ++++ user_manager.py | 40 +++++++++++++-------------- 8 files changed, 128 insertions(+), 102 deletions(-) create mode 100644 tools.py diff --git a/connection_handler.py b/connection_handler.py index 08c257b..27dca22 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -6,12 +6,14 @@ from user_manager import UserManager from match_manager import MatchManager from match import Match +from tools import debug + def parse_message(msg: str): # TODO: make it more robust by validating each part of a message msg_obj = json.loads(msg) if "type" not in msg_obj or "data" not in msg_obj: - print("got strange message") + debug("got strange message") return None return msg_obj @@ -33,7 +35,7 @@ class Connection(object): try: await self.websocket.send(msg) except Exception as e: - print("error sending message to user " + + debug("error sending message to user " + self.user_name + ". Reason: " + str(e)) async def close(self): @@ -44,7 +46,7 @@ class Connection(object): self.is_closed = True except Exception as e: - print("error closing session to user " + + debug("error closing session to user " + self.user_name + ". Reason: " + str(e)) @@ -114,7 +116,7 @@ class ConnectionHandler(object): socket: websockets.WebSocketServerProtocol, login_msg: str): msg = parse_message(login_msg) - print("new incomming connection...") + debug("new incomming connection...") if msg is None: return None @@ -395,7 +397,7 @@ class ConnectionHandler(object): ) except Exception as e: - print("error processing match request: " + str(data) + str(e)) + debug("error processing match request: " + str(data) + str(e)) async def _on_match_state_req(self, conn, data): db_matches = self.match_manager.get_matches_for_user(conn.user_name) @@ -432,7 +434,7 @@ class ConnectionHandler(object): if match is not None: match_state = match.to_json_state() - print(match_state) + debug(match_state) await conn.send(json.dumps({ 'type': 'match_update', @@ -580,7 +582,7 @@ class ConnectionHandler(object): async def handle_message(self, conn, msg_str): msg = parse_message(msg_str) - print("incoming message" + str(msg)) + debug("incoming message" + str(msg)) if msg is None: return None @@ -607,4 +609,4 @@ class ConnectionHandler(object): await self._on_unfriend_request(conn, msg['data']) else: - print("could not interpret message: " + msg_str) + debug("could not interpret message: " + msg_str) diff --git a/database_connection.py b/database_connection.py index 6784c1b..2df6201 100644 --- a/database_connection.py +++ b/database_connection.py @@ -36,23 +36,30 @@ class DatabaseConnection(object): DatabaseConnection.instance.commit() @staticmethod - def global_single_query(query): + def global_single_query(query, params=None): if ';' in query: # Possible injection! raise SQLInjectionError() with DatabaseConnection.global_cursor() as c: - c.execute(query) + if params is None: + c.execute(query) + else: + c.execute(query, params) + return c.fetchall() @staticmethod - def global_single_execution(sql_statement): + def global_single_execution(sql_statement, params=None): if ';' in sql_statement: # Possible injection detected! raise SQLInjectionError() with DatabaseConnection.global_cursor() as c: - c.execute(sql_statement) + if params is None: + c.execute(sql_statement) + else: + c.execute(sql_statement, params) DatabaseConnection.global_commit() def __init__(self, @@ -84,7 +91,7 @@ class DatabaseConnection(object): ":" + str(port) + "\nCheck the configuration in settings.py!\n") - raise Exception('could not connec to database') + raise Exception('could not connect to database') def get_cursor(self): return self.connection.cursor() @@ -96,6 +103,7 @@ class DatabaseConnection(object): def commit(self): self.connection.commit() + def test_connection(): import settings DatabaseConnection(settings.db_host, @@ -103,4 +111,4 @@ def test_connection(): settings.db_user, settings.db_pw, settings.db_db, - settings.db_charset) \ No newline at end of file + settings.db_charset) diff --git a/main.py b/main.py index 0e158ec..fa26284 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,8 @@ from user_manager import UserManager from database_connection import DatabaseConnection import datetime +from tools import debug + um = UserManager() sm = SessionManager(datetime.timedelta(hours=12)) mm = MatchManager() @@ -29,7 +31,6 @@ DatabaseConnection(db_host, async def socket_worker(websocket, path): connection = None - try: raw_msg = await websocket.recv() @@ -37,21 +38,25 @@ async def socket_worker(websocket, path): if connection is None: return - - print("successfull logged in user: " + connection.user_name) - + + debug("successfull logged in user: " + connection.user_name) + async for m in websocket: await ch.handle_message(connection, m) + except websockets.ConnectionClosed as e: + # nothing suspicious here + pass + except Exception as e: # TODO: each disconnect is an exception so far if connection is not None: - print("catched exception in worker for user: " + + debug("catched exception in worker for user: " + connection.user_name + ": " + str(e)) else: - print("catched exception in worker for unknown user: " + str(e)) - - print(traceback.print_exc()) + debug("catched exception in worker for unknown user: " + str(e)) + + debug(traceback.debug_exc()) finally: id = None @@ -62,7 +67,7 @@ async def socket_worker(websocket, path): if connection is None: id = "unknown_user" - print("close connection to user: " + id) + debug("close connection to user: " + id) if cert_file is not None and key_file is not None: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) diff --git a/match.py b/match.py index e1fa5cf..4c7e43c 100644 --- a/match.py +++ b/match.py @@ -3,6 +3,8 @@ import numpy as np import json import base64 +from tools import debug + # decoder/encoder took from: https://stackoverflow.com/a/19271311 @@ -158,7 +160,7 @@ class Match(object): player_mark = FIELD_USER_A if self.is_player_a else FIELD_USER_B if not self.is_move_valid(sub_x, sub_y, x, y): - print("invalid move") + debug("invalid move") return False # else: move! @@ -169,7 +171,7 @@ class Match(object): # check whether this indicates changes in the global field: if self.global_field[sub_y, sub_x] != FIELD_EMPTY: - print("field not empty") + debug("field not empty") return False subgrid = self.complete_field[sub_y * self.n: ( diff --git a/match_manager.py b/match_manager.py index 438c28c..1451e34 100644 --- a/match_manager.py +++ b/match_manager.py @@ -5,14 +5,16 @@ import uuid import settings from match import Match +from tools import debug + class MatchManager(object): def __init__(self): pass def get_match(self, id): - query = f"SELECT * FROM matches WHERE id='{id}'" - result = DatabaseConnection.global_single_query(query) + query = "SELECT * FROM matches WHERE id=%s" + result = DatabaseConnection.global_single_query(query, (id)) if len(result) == 0: return None match = Match(n=settings.n, match_id=id, player_a_name=result[0]['user_a'], @@ -20,45 +22,46 @@ class MatchManager(object): return match def get_matches_for_user(self, user_name): - query = f"SELECT * FROM matches WHERE user_a='{user_name}' OR user_b='{user_name}'" - return DatabaseConnection.global_single_query(query) + query = "SELECT * FROM matches WHERE user_a=%s OR user_b=%s" + return DatabaseConnection.global_single_query(query, (user_name, user_name)) def create_new_match(self, user_a, user_b): match_id = uuid.uuid4().hex # check if already existent (but should not be the case) - if len(DatabaseConnection.global_single_query(f"SELECT id FROM matches WHERE id='{match_id}'")) > 0: + if len(DatabaseConnection.global_single_query("SELECT id FROM matches WHERE id=%s", (match_id))) > 0: return self.create_new_match(user_a, user_b) - match = Match(n=settings.n, match_id=match_id, player_a_name=user_a, player_b_name=user_b) + match = Match(n=settings.n, match_id=match_id, + player_a_name=user_a, player_b_name=user_b) now = datetime.datetime.now() - query = f"INSERT INTO matches (id, user_a, user_b, match_state, active_user, last_active) VALUES ('{match_id}', '{user_a}', '{user_b}', '{match.to_json_state()}', '{match.get_current_player()}','{get_sql_time(now)}')" - DatabaseConnection.global_single_execution(query) + query = "INSERT INTO matches (id, user_a, user_b, match_state, active_user, last_active) VALUES (%s, %s, %s, %s, %s,%s)" + DatabaseConnection.global_single_execution( + query, (match_id, user_a, user_b, match.to_json_state(), match.get_current_player(), get_sql_time(now))) return match - + def update_match(self, match_id, match): now = get_sql_time(datetime.datetime.now()) - query = f"UPDATE matches SET match_state='{match.to_json_state()}', active_user='{match.get_current_player()}', last_active='{now}' WHERE id='{match_id}'" - DatabaseConnection.global_single_execution(query) - + query = "UPDATE matches SET match_state=%s, active_user=%s, last_active=%s WHERE id=%s" + DatabaseConnection.global_single_execution( + query, (match.to_json_state(), match.get_current_player(), now, match_id)) + def apply_move(self, move_data): match = self.get_match(move_data['id']) if match is None: return None - - if not match.move(move_data): - print("error applying match move") - return None - - - self.update_match(move_data['id'], match) - print("updated match") - return match - - def is_match(self, player_a, player_b): - query = f"SELECT * FROM matches WHERE (user_a='{player_a}' AND user_b='{player_b}') OR (user_a='{player_b}' AND user_b='{player_a}')" - return len(DatabaseConnection.global_single_query(query)) - - def delete_match(self, match_id): - query = f"DELETE FROM matches WHERE id='{match_id}'" - DatabaseConnection.global_single_execution(query) + if not match.move(move_data): + debug("error applying match move") + return None + + self.update_match(move_data['id'], match) + debug("updated match") + return match + + def is_match(self, player_a, player_b): + query = "SELECT * FROM matches WHERE (user_a=%s AND user_b=%s) OR (user_a=%s AND user_b=%s)" + return len(DatabaseConnection.global_single_query(query, (player_a, player_b, player_b, player_a))) + + def delete_match(self, match_id): + query = "DELETE FROM matches WHERE id=%s" + DatabaseConnection.global_single_execution(query, (match_id)) diff --git a/session_manager.py b/session_manager.py index 543143f..9b6d4c1 100644 --- a/session_manager.py +++ b/session_manager.py @@ -9,59 +9,60 @@ class SessionManager(object): self.session_lifespan_timedelta = session_lifespan_timedelta def get_session_by_id(self, session_id): - query = f"SELECT * FROM sessions WHERE id='{session_id}'" - return DatabaseConnection.global_single_query(query) + query = "SELECT * FROM sessions WHERE id=%s" + return DatabaseConnection.global_single_query(query, (session_id)) def touch_session(self, session_id): - query = f"UPDATE sessions SET last_seen='{datetime.datetime.now()}' WHERE id='{session_id}'" - DatabaseConnection.global_single_execution(query) + query = "UPDATE sessions SET last_seen=%s WHERE id=%s" + DatabaseConnection.global_single_execution(query, (datetime.datetime.now(), session_id)) def get_session_by_registered_user(self, user_name): - query = f"SELECT * FROM sessions WHERE registered_user='{user_name}'" - return DatabaseConnection.global_single_query(query) + query = "SELECT * FROM sessions WHERE registered_user=%s" + return DatabaseConnection.global_single_query(query, (user_name)) def get_session_by_temp_user(self, user_name): - query = f"SELECT * FROM sessions WHERE temp_user='{user_name}'" - return DatabaseConnection.global_single_query(query) + query = "SELECT * FROM sessions WHERE temp_user=%s" + return DatabaseConnection.global_single_query(query, (user_name)) def create_session_for_registered_user(self, user_name): new_id = uuid.uuid4().hex # check if already existent (but should not be the case) - if len(DatabaseConnection.global_single_query(f"SELECT id FROM sessions WHERE id='{new_id}'")) > 0: + if len(DatabaseConnection.global_single_query("SELECT id FROM sessions WHERE id=%s", (str(new_id)))) > 0: # okay, next try: return self.create_session_for_registered_user(user_name) - - # delete other active sessions: - query = f"DELETE FROM sessions WHERE registered_user='{user_name}'" - DatabaseConnection.global_single_execution(query) - query = f"INSERT INTO sessions (id, registered_user, temp_user, last_seen) VALUES ( '{new_id}', '{user_name}', NULL, '{datetime.datetime.now()}')" - DatabaseConnection.global_single_execution(query) + # delete other active sessions: + query = "DELETE FROM sessions WHERE registered_user=%s" + DatabaseConnection.global_single_execution(query, (user_name)) + + query = "INSERT INTO sessions (id, registered_user, temp_user, last_seen) VALUES ( %s, %s, NULL, %s)" + DatabaseConnection.global_single_execution(query, (new_id, user_name, datetime.datetime.now())) + return new_id def create_session_for_temp_user(self, user_name): new_id = uuid.uuid4().hex # check if already existent (but should not be the case) - if len(DatabaseConnection.global_single_query(f"SELECT id FROM sessions WHERE id='{new_id}'")) > 0: + if len(DatabaseConnection.global_single_query("SELECT id FROM sessions WHERE id=%s", (new_id))) > 0: # okay, next try: return self.create_session_for_registered_user(user_name) - + # delete other active sessions: - query = f"DELETE FROM sessions WHERE temp_user='{user_name}'" - DatabaseConnection.global_single_execution(query) - - query = f"INSERT INTO sessions (id, registered_user, temp_user, last_seen) VALUES ( '{new_id}', NULL, '{user_name}', '{datetime.datetime.now()}')" - DatabaseConnection.global_single_execution(query) + query = "DELETE FROM sessions WHERE temp_user=%s" + DatabaseConnection.global_single_execution(query, (user_name)) + + query = "INSERT INTO sessions (id, registered_user, temp_user, last_seen) VALUES ( %s, NULL, %s, %s)" + DatabaseConnection.global_single_execution(query, (new_id, user_name, datetime.datetime.now())) return new_id def delete_session(self, session_id): - query = f"DELETE FROM sessions WHERE id='{session_id}'" - DatabaseConnection.global_single_execution(query) + query = "DELETE FROM sessions WHERE id=%s" + DatabaseConnection.global_single_execution(query, (session_id)) def revoke_inactive_sessions(self): revoke_time = datetime.datetime.now() - self.session_lifespan_timedelta - query = f"SELECT * from sessions WHERE last_seen < '{get_sql_time(revoke_time)}'" - revoked_sessions = DatabaseConnection.global_single_query(query) - query = f"DELETE FROM sessions WHERE last_seen < '{get_sql_time(revoke_time)}'" - DatabaseConnection.global_single_execution(query) + query = "SELECT * from sessions WHERE last_seen < %s" + revoked_sessions = DatabaseConnection.global_single_query(query, (get_sql_time(revoke_time))) + query = "DELETE FROM sessions WHERE last_seen < %s" + DatabaseConnection.global_single_execution(query, (get_sql_time(revoke_time))) return revoked_sessions diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..38d9228 --- /dev/null +++ b/tools.py @@ -0,0 +1,5 @@ +import datetime + + +def debug(msg): + print("[" + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "]: " + msg) diff --git a/user_manager.py b/user_manager.py index da6af5a..90402e4 100644 --- a/user_manager.py +++ b/user_manager.py @@ -10,23 +10,23 @@ class UserManager(object): pass def get_user(self, user_name): - query = f"SELECT name, last_seen FROM users where name='{user_name}'" - return DatabaseConnection.global_single_query(query) + query = "SELECT name, last_seen FROM users where name=%s" + return DatabaseConnection.global_single_query(query, (user_name)) def delete_user(self, user_name): - query = f"DELETE FROM users where name='{user_name}'" - DatabaseConnection.global_single_execution(query) + query = "DELETE FROM users where name=%s" + DatabaseConnection.global_single_execution(query, (user_name)) # remove from friends: - query = f"DELETE FROM friends WHERE user='{user_name}'" - DatabaseConnection.global_single_execution(query) + query = "DELETE FROM friends WHERE user=%s" + DatabaseConnection.global_single_execution(query, (user_name)) - query = f"DELETE FROM friends WHERE friend='{user_name}'" - DatabaseConnection.global_single_execution(query) + query = "DELETE FROM friends WHERE friend=%s" + DatabaseConnection.global_single_execution(query, (user_name)) def verify_user(self, user_name, pw): - query = f"SELECT * FROM users where name='{user_name}'" - users = DatabaseConnection.global_single_query(query) + query = "SELECT * FROM users where name=%s" + users = DatabaseConnection.global_single_query(query, (user_name)) if len(self.get_user(user_name)) == 0: return False @@ -41,33 +41,33 @@ class UserManager(object): assert len(self.get_user(user_name)) == 0 pw_salt = uuid.uuid4().hex pw_hash = hashlib.sha512(pw.encode() + pw_salt.encode()).hexdigest() - query = f"INSERT INTO users (name, pw_hash, pw_salt, last_seen) VALUES ( '{user_name}', '{pw_hash}', '{pw_salt}', '{datetime.datetime.now()}')" - DatabaseConnection.global_single_execution(query) + query = "INSERT INTO users (name, pw_hash, pw_salt, last_seen) VALUES ( %s, %s, %s, %s)" + DatabaseConnection.global_single_execution(query, (user_name, pw_hash, pw_salt, datetime.datetime.now())) def touch_user(self, user_name): matches = self.get_user(user_name) assert len(matches) == 1 - query = f"UPDATE users SET last_seen='{datetime.datetime.now()}' WHERE name='{user_name}'" - DatabaseConnection.global_single_execution(query) + query = "UPDATE users SET last_seen=%s WHERE name=%s" + DatabaseConnection.global_single_execution(query, (datetime.datetime.now(), user_name)) def add_friend_to_user(self, user_name, friend_name): if len(self.get_user(friend_name)) > 0: - query = f"INSERT INTO friends (user, friend) VALUES ( '{user_name}', '{friend_name}')" - DatabaseConnection.global_single_execution(query) + query = "INSERT INTO friends (user, friend) VALUES ( %s, %s)" + DatabaseConnection.global_single_execution(query, (user_name, friend_name)) return True return False def get_friends_for_user(self, user_name): - query = f"SELECT friend FROM friends WHERE user='{user_name}'" - tmp = DatabaseConnection.global_single_query(query) + query = "SELECT friend FROM friends WHERE user=%s" + tmp = DatabaseConnection.global_single_query(query, (user_name)) friends = set() for entry in tmp: friends.add(entry['friend']) return friends def remove_friend_from_user(self, user_name, friend_name): - query = f"DELETE FROM friends WHERE user='{user_name}' AND friend='{friend_name}'" - DatabaseConnection.global_single_execution(query) + query = "DELETE FROM friends WHERE user=%s AND friend=%s" + DatabaseConnection.global_single_execution(query, (user_name, friend_name)) def get_all_users(self): query = "SELECT name, last_seen FROM users"