From df6b1d8c599bc0fafc934a38dd43a460ce152ff1 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Mon, 11 Mar 2019 19:03:03 +0100 Subject: [PATCH 01/10] filled settings file --- settings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 12b0b68..28f82d5 100644 --- a/settings.py +++ b/settings.py @@ -1,2 +1,12 @@ cert_file = None -key_file = None \ No newline at end of file +key_file = None + +server_port = 5556 + +db_host = "127.0.0.1" +db_port = 3306 + +db_user = "tictactoe" +dp_pw = "" + +charset = 'utf8mb4' From d6b68de0088a60eaf96de004ae74362178dd80c0 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 14 Mar 2019 00:01:02 +0100 Subject: [PATCH 02/10] work in progress, commit just for sync --- README.md | 170 +++++++++++++++++++++++++++++- connection_handler.py | 227 +++++++++++++++++++++++++++++++++++++++++ create_database.py | 33 ++++++ database_connection.py | 106 +++++++++++++++++++ main.py | 2 +- match.py | 188 ++++++++++++++++++++++++++++++++++ match_manager.py | 46 +++++++++ message_handler.py | 1 + session_manager.py | 59 +++++++++++ user.py | 10 ++ user_manager.py | 65 ++++++++++++ 11 files changed, 904 insertions(+), 3 deletions(-) create mode 100644 connection_handler.py create mode 100755 create_database.py create mode 100644 database_connection.py create mode 100644 match.py create mode 100644 match_manager.py create mode 100644 message_handler.py create mode 100644 session_manager.py create mode 100644 user.py create mode 100644 user_manager.py diff --git a/README.md b/README.md index 13ccf38..72e4525 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ communication with the web client is done by a (far from any standard and almost -**register as player and in game queue:** +**register as player:** ```json { @@ -88,4 +88,170 @@ response: } ``` -(response?) \ No newline at end of file +(response?) + + + +## new version: + +**json match state:** + +```json +{ + complete_field: '[[...],[...],...]', + global_field: '[[...],[...],...]', + last_move: { + "sub_x": "...", + "sub_y": "...", + "x": "...", + "y": "..." + } + game_over: , + player_won: > + current_player: > +} +``` + +**new temp session** + +client + +```json +{ + "type": "temp_session", + "data": { + "name": "" + } +} +``` + +server response: + +```json +{ + "type": "temp_session_response", + "data": { + "success": , + "id": "", + "message": "..." + } +} +``` + +**connect by session id** + +client + +```json +{ + "type": "reconnect", + "data": { + "id": "", + } +} +``` + +server response: + +```json +{ + "type": "reconnect_response", + "data": { + "success": , + "msg": "..." + } +} +``` + +**register**: + +TODO + + + +**match_request**: + +client + +```json +{ + "type": "match_request", + "data": { + "player": > + } +} +``` + +server_response: + +```json +{ + "type": "match_request_response", + "data": { + "success": + "msg": "..." + } +} +``` + + + +**match_move**: + +client + +```json +{ + "type": "move", + "data": { + "sub_x": "...", + "sub_y": "...", + "x": "...", + "y": "..." + } +} +``` + +server response: (maybe useless?) + +```json +{ + "type": "move_response", + "data": { + "success": true, + "msg": "..." + } +} +``` + + + +**match update** + +(also send on match start and send for all matches after login) + +server: + +```json +{ + "type": "match_update", + "data": { + "id": "", + "match_state": "" + } +} +``` + +**match close** + +client: + +```json +{ + "type": "end_match", + "data": { + "id": "" + } +} +``` + diff --git a/connection_handler.py b/connection_handler.py new file mode 100644 index 0000000..1223d5e --- /dev/null +++ b/connection_handler.py @@ -0,0 +1,227 @@ +import asyncio +import websockets +import json +from session_manager import SessionManager +from user_manager import UserManager +from match_manager import MatchManager +from match import Match + + +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") + return None + + return msg_obj + + +class Connection(object): + def __init__(self, + id, + user_name: str, + registered: bool, + websocket: websocket.WebSocketServerProtocol): + self.id = id + self.user_name = user_name + self.websocket = websocket + self.is_registered_user = registered + + +class ConnectionHandler(object): + def __init__(self, + session_manager: SessionManager, + user_manager: UserManager, + match_manager: MatchManager): + self.session_manager = session_manager + self.user_manager = user_manager + self.match_manager = match_manager + + self.open_connections_by_user = {} + self.open_connections_by_id = {} + + self.match_queue = set() + + def reconnect_session(self, + socket: websockets.WebSocketServerProtocol, + session_id: str): + # check whether id exists + tmp = self.session_manager.get_session_by_id(session_id) + if len(tmp) == 0: + # session not available! + return None + + session_obj = tmp[0] + + is_registerd = session_obj['registered_user'] is not None + + user_name = session_obj['registered_user'] if is_registerd else session_obj['temp_user'] + + conn = Connection(id=session_id, + user_name=user_name, + registered=is_registerd, + websocket=socket) + + self.session_manager.touch_session(session_id) + + return conn + + def close_connection(self, conn: Connection): + if conn.id in self.open_connections_by_id: + del(self.open_connections_by_id[conn.id]) + del(self.open_connections_by_user[conn.user_name]) + conn.websocket.close() + + def close_session(self, id): + tmp = self.session_manager.get_session_by_id(id) + if len(tmp) == 0: + return + + if id in self.open_connections_by_id: + self.close_connection([id]) + + self.session_manager.delete_session(id) + + def _add_connection(self, conn): + self.open_connections_by_id[conn.id] = conn + self.open_connections_by_user[conn.user] = conn + + def _del_connection(self, conn): + del(self.open_connections_by_id[conn.id]) + del(self.open_connections_by_user[conn.user]) + + async def new_connection(self, + socket: websockets.WebSocketServerProtocol, + login_msg: str): + + msg = parse_message(login_msg) + + if msg is None: + return None + + if msg['type'] == 'reconnect': + conn = self.reconnect_session(socket, msg['data']['id']) + if conn is not None: + self._add_connection(conn) + await conn.websocket.send(json.dumps({ + "type": "reconnect_response", + "data": { + "success": True, + "msg": "" + } + })) + return conn + await conn.websocket.send(json.dumps({ + "type": "reconnect_response", + "data": { + "success": False, + "msg": "session not available" + } + })) + return None + + elif msg['type'] == 'temp_session': + name = msg['data']['name'] + if len(self.session_manager.get_session_by_temp_user(name)) == 0: + if len(self.user_manager.get_user(name)) == 0: + if len(msg['data']['name']) < 16 and ';' not in name and '\'' not in name and '\"' not in name: + id = self.session_manager.create_session_for_temp_user( + name) + conn = Connection( + id=id, user_name=name, registered=False, websocket=socket) + self._add_connection(conn) + await socket.send(json.dumps({ + "type": "temp_session_response", + "data": { + "success": True, + "id": id, + "message": "logged in as temporary user " + name + } + })) + return conn + + await socket.send(json.dumps({ + "type": "temp_session_response", + "data": { + "success": False, + "id": None, + "message": "user name not available" + } + })) + return None + + elif msg['type'] == 'login': + # TODO + pass + + elif msg['type'] == 'register': + # TODO + pass + + async def _start_match(self, user_a, user_b): + m = self.match_manager.create_new_match(user_a, user_b) + state = m.to_json_state() + if user_a in self.open_connections_by_user: + await self.open_connections_by_user[user_a].websocket.send( + json.dumps({ + { + "type": "match_update", + "data": { + "id": m.id, + "match_state": state + } + } + }) + ) + if user_b in self.open_connections_by_user: + await self.open_connections_by_user[user_b].websocket.send( + json.dumps({ + { + "type": "match_update", + "data": { + "id": m.id, + "match_state": state + } + } + }) + ) + + async def _on_match_req(self, conn, data): + if len(self.match_queue) > 0: + # it's a match! + user_a = self.match_queue.pop() + self._start_match(user_a, conn.user_name) + + else: + self.match_queue.append(conn.user_name) + + async def _on_match_move(self, conn, data): + pass + + async def _on_match_close(self, conn, data): + pass + + async def disconnect(self, conn): + self._del_connection(conn) + + async def handle_message(self, conn, msg_str): + msg = parse_message(msg_str) + + if msg is None: + return None + + t = msg['type'] + + self.user_manager.touch_user(conn.user_name) + if t == "match_request": + await self._on_match_req(conn, msg['data']) + + elif t == "move": + await self._on_match_move(conn, msg['data']) + + elif t == "end_match": + await self._on_match_close(conn, msg['data']) + + else: + print("could not interpret message: " + msg_str) diff --git a/create_database.py b/create_database.py new file mode 100755 index 0000000..c2adf94 --- /dev/null +++ b/create_database.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# this script creates the necessary tables in the databse + +import settings +from database_connection import DatabaseConnection + + +def create_tables(): + + DatabaseConnection(settings.db_host, + settings.db_port, + settings.db_user, + settings.db_pw, + settings.db_db, + settings.db_charset) + + queries = [ + "DROP TABLE IF EXISTS matches", + "DROP TABLE IF EXISTS sessions", + "DROP TABLE IF EXISTS users", + "CREATE TABLE users (name varchar(16) NOT NULL, pw_hash varchar(128) NOT NULL, pw_salt varchar(32) NOT NULL, last_seen datetime NOT NULL, PRIMARY KEY (name)) CHARACTER SET " + settings.db_charset, + "CREATE TABLE matches (id varchar(32) NOT NULL, user_a varchar(16) NOT NULL, user_b varchar(16) NOT NULL, match_state varchar(4096) NOT NULL, active_user varchar(16), last_active datetime NOT NULL, FOREIGN KEY (user_a) REFERENCES users(name), FOREIGN KEY (user_b) REFERENCES users(name), FOREIGN KEY (active_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset, + "CREATE TABLE sessions (id varchar(32) NOT NULL, registered_user varchar(16), temp_user varchar(16), last_seen datetime NOT NULL, PRIMARY KEY (id), FOREIGN KEY(registered_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset + ] + + for query in queries: + DatabaseConnection.global_single_execution(query) + + DatabaseConnection.global_close() + + +if __name__ == "__main__": + create_tables() diff --git a/database_connection.py b/database_connection.py new file mode 100644 index 0000000..6784c1b --- /dev/null +++ b/database_connection.py @@ -0,0 +1,106 @@ +import pymysql.cursors +import sys + + +def get_sql_time(datetime_object): + return datetime_object.strftime('%Y-%m-%d %H:%M:%S') + + +class SQLInjectionError(Exception): + def __init__(self): + + # Call the base class constructor with the parameters it needs + super().__init__("Detected possible SQL injection attack!") + + +class DatabaseConnection(object): + """ + a singleton class for a global database connection + """ + + instance = None + + @staticmethod + def global_cursor(): + assert DatabaseConnection.instance is not None + return DatabaseConnection.instance.get_cursor() + + @staticmethod + def global_close(): + assert DatabaseConnection.instance is not None + DatabaseConnection.instance.close() + + @staticmethod + def global_commit(): + assert DatabaseConnection.instance is not None + DatabaseConnection.instance.commit() + + @staticmethod + def global_single_query(query): + if ';' in query: + # Possible injection! + raise SQLInjectionError() + + with DatabaseConnection.global_cursor() as c: + c.execute(query) + return c.fetchall() + + @staticmethod + def global_single_execution(sql_statement): + if ';' in sql_statement: + # Possible injection detected! + raise SQLInjectionError() + + with DatabaseConnection.global_cursor() as c: + c.execute(sql_statement) + DatabaseConnection.global_commit() + + def __init__(self, + host: str, + port: int, + user: str, + password: str, + db: str, + charset: str): + + assert DatabaseConnection.instance is None + try: + self.connection = pymysql.connect( + host=host, + port=port, + user=user, + password=password, + db=db, + charset=charset, + cursorclass=pymysql.cursors.DictCursor) + DatabaseConnection.instance = self + except Exception as e: + sys.stderr.write("could not connect to database '" + + str(db) + + "' at " + + user + + "@" + + host + + ":" + + str(port) + + "\nCheck the configuration in settings.py!\n") + raise Exception('could not connec to database') + + def get_cursor(self): + return self.connection.cursor() + + def close(self): + self.connection.close() + DatabaseConnection.instance = None + + def commit(self): + self.connection.commit() + +def test_connection(): + import settings + DatabaseConnection(settings.db_host, + settings.db_port, + settings.db_user, + settings.db_pw, + settings.db_db, + settings.db_charset) \ No newline at end of file diff --git a/main.py b/main.py index 7068a18..ac582ce 100644 --- a/main.py +++ b/main.py @@ -64,7 +64,7 @@ async def socket_worker(websocket, path): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(cert_file , keyfile=key_file) -start_server = websockets.serve(socket_worker, host='', port=5555, ssl=ssl_context) +start_server = websockets.serve(socket_worker, host='', port=server_port, ssl=ssl_context) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() diff --git a/match.py b/match.py new file mode 100644 index 0000000..3a84ae7 --- /dev/null +++ b/match.py @@ -0,0 +1,188 @@ + +import numpy as np +import json +import base64 + +# decoder/encoder took from: https://stackoverflow.com/a/19271311 + + +def Base64Encode(ndarray): + return json.dumps([str(ndarray.dtype), base64.b64encode(ndarray), ndarray.shape]) + + +def Base64Decode(jsonDump): + loaded = json.loads(jsonDump) + dtype = np.dtype(loaded[0]) + arr = np.frombuffer(base64.decodestring(loaded[1]), dtype) + if len(loaded) > 2: + return arr.reshape(loaded[2]) + return arr + + +def SimpleEncode(ndarray): + return json.dumps(ndarray.tolist()) + + +def SimpleDecode(jsonDump): + return np.array(json.loads(jsonDump)) + + +FIELD_EMPTY = 0 +FIELD_USER_A = 1 +FIELD_USER_B = 2 +FIELD_DRAW = 3 + + +class Match(object): + def __init__(self, n, match_id, player_a_name, player_b_name, json_state=None): + self.n = n + self.id = match_id + self.complete_field = np.zeros(shape=(n*n, n*n), dtype=int) + self.global_field = np.zeros(shape=(n, n), dtype=int) + self.player_won = None + self.game_over = False + self.last_move = None + self.is_player_a = True + self.player_a_name = player_a_name + self.player_b_name = player_b_name + + if json_state is not None: + self.from_json_state(json_state) + + def from_json_state(self, json_state): + match_obj = json.loads(json_state) + self.complete_field = np.array(match_obj['complete_field'], dtype=int) + self.global_field = np.array(match_obj['global_field'], dtype=int) + self.player_won = match_obj['player_won'] + self.game_over = match_obj['game_over'] + self.last_move = match_obj['last_move'] + self.is_player_a = match_obj['active_player'] == self.player_a_name + + def to_json_state(self): + match_obj = { + 'complete_field': self.complete_field.tolist(), + 'global_field': self.global_field.tolist(), + 'last_move': self.last_move, + 'game_over': self.game_over, + 'player_won': self.player_won, + 'active_player': self.player_a_name if self.is_player_a else self.player_b_name + } + + return json.dumps(match_obj) + + def switch_player_names(self): + tmp = self.player_a_name + self.player_a_name = self.player_b_name + self.player_b_name = tmp + + def get_current_player(self): + return self.player_a_name if self.is_player_a else self.player_b_name + + def is_move_valid(self, sub_x, sub_y, x, y): + if sub_x < 0 or sub_x >= self.n: + return False + if sub_y < 0 or sub_y >= self.n: + return False + + if x < 0 or x >= self.n: + return False + if y < 0 or y >= self.n: + return False + + if (self.last_move is not None): + last_x = self.last_move['x'] + last_y = self.last_move['y'] + last_sub_x = self.last_move['sub_x'] + last_sub_y = self.last_move['sub_y'] + + if sub_x != last_x and self.global_field[last_sub_y, last_sub_x] == FIELD_EMPTY: + # user is not allowed to place everywhere! wrong move! + return False + + if sub_y != last_y and self.global_field[last_sub_y, last_sub_x] == FIELD_EMPTY: + return False + + if self.complete_field[sub_y * self.n + y][sub_x * self.n + x] != FIELD_EMPTY: + return False + + def is_full(self, field): + return not field.__contains__(FIELD_EMPTY) + + def check_win(self, field, x, y): + is_col = True + is_row = True + is_main_diag = False + is_sec_diag = False + + val = field[y, x] + + for i in range(self.n): + if (field[i, x] != val): + is_col = False + break + + for i in range(self.n): + if (field[y, i] != val): + is_row = False + break + + if x == y: + is_main_diag = True + for i in range(self.n): + if field[i, i] != val: + is_main_diag = False + break + + if x + y == self.n - 1: + is_sec_diag = True + for i in range(self.n): + if field[i, -i] != val: + is_sec_diag = False + break + + return is_col or is_row or is_main_diag or is_sec_diag + + def move(self, move_dict): + sub_x = move_dict['sub_x'] + sub_y = move_dict['sub_y'] + x = move_dict['x'] + y = move_dict['y'] + + abs_x = sub_x * self.n + x + abs_y = sub_y * self.n + y + + 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): + return False + + # else: move! + self.complete_field[abs_y, abs_x] = player_mark + + # encode move: + self.last_move = move_dict + + # check whether this indicates changes in the global field: + assert self.global_field[sub_y, sub_x] == FIELD_EMPTY + + subgrid = self.complete_field[sub_y * self.n: ( + sub_y + 1) * self.n, sub_x * self.n: (sub_x + 1) * self.n] + + if self.check_win(subgrid, x, y): + self.global_field[sub_x, sub_y] = player_mark + + elif self.is_full(subgrid): + self.global_field[sub_x, sub_y] = FIELD_DRAW + + # check global state: + if self.check_win(self.global_field, sub_x, sub_y): + self.game_over = True + self.player_won = self.player_a_name if self.is_player_a else self.player_b_name + + elif self.is_full(self.global_field): + self.game_over = True + self.player_won = None + + self.is_player_a = not self.is_player_a + + return True diff --git a/match_manager.py b/match_manager.py new file mode 100644 index 0000000..ab75d45 --- /dev/null +++ b/match_manager.py @@ -0,0 +1,46 @@ +from database_connection import DatabaseConnection, SQLInjectionError, get_sql_time +from user import User +import datetime +import uuid +import settings +from match import Match + + +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) + if len(result) == 0: + return None + match = Match(n=settings.n, match_id=id, player_a_name=result[0]['user_a'], + player_b_name=result[0]['user_b'], json_state=result[0]['match_state']) + 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) + + 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: + 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) + 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) + 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) + + def delete_match(self, match_id): + query = f"DELETE FROM matches WHERE id='{match_id}'" + DatabaseConnection.global_single_execution(query) + diff --git a/message_handler.py b/message_handler.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/message_handler.py @@ -0,0 +1 @@ + diff --git a/session_manager.py b/session_manager.py new file mode 100644 index 0000000..a8d316e --- /dev/null +++ b/session_manager.py @@ -0,0 +1,59 @@ +from database_connection import DatabaseConnection, SQLInjectionError, get_sql_time +from user import User +import datetime +import uuid + + +class SessionManager(object): + def __init__(self, session_lifespan_timedelta): + 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) + + 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) + + 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) + + 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) + + 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: + # okay, next try: + return self.create_session_for_registered_user(self, user_name) + + 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) + 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: + # okay, next try: + return self.create_session_for_registered_user(self, user_name) + + 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) + return new_id + + def delete_session(self, session_id): + query = f"DELETE FROM sessions WHERE id='{session_id}'" + DatabaseConnection.global_single_execution(query) + + 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) + return revoked_sessions diff --git a/user.py b/user.py new file mode 100644 index 0000000..e3deb64 --- /dev/null +++ b/user.py @@ -0,0 +1,10 @@ +class User(object): + def __init__(self, id, name): + self.id = id + self.name = name + + def get_id(self): + return self.id + + def get_name(self): + return self.name diff --git a/user_manager.py b/user_manager.py new file mode 100644 index 0000000..c5a4e4e --- /dev/null +++ b/user_manager.py @@ -0,0 +1,65 @@ +from database_connection import DatabaseConnection, SQLInjectionError +from user import User +import datetime +import hashlib +import uuid + + +class UserManager(object): + def __init__(self): + 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) + + def delete_user(self, user_name): + query = f"DELETE FROM users where name='{user_name}'" + DatabaseConnection.global_single_execution(query) + + def verify_user(self, user_name, pw): + query = f"SELECT * FROM users where name='{user_name}'" + users = DatabaseConnection.global_single_query(query) + if len(self.get_user(user_name)) == 0: + return False + + user = users[0] + pw_salt = user['pw_salt'] + stored_pw_hash = user['pw_hash'] + + pw_hash = hashlib.sha512(pw.encode() + pw_salt.encode()).hexdigest() + return stored_pw_hash == pw_hash + + def create_user(self, user_name, pw): + 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) + + 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) + + def get_all_users(self): + query = "SELECT name, last_seen FROM users" + return DatabaseConnection.global_single_query(query) + + +def create_test_manager(): + import settings + DatabaseConnection(settings.db_host, + settings.db_port, + settings.db_user, + settings.db_pw, + settings.db_db, + settings.db_charset) + um = UserManager() + return um + + +# test routines: +if __name__ == "__main__": + pass From 070a60a2723a84655c459de2d7a1cf4f24fb1bd5 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 21 Mar 2019 12:06:22 +0100 Subject: [PATCH 03/10] almost working version --- .vscode/tags | 100 +++++++++++++++ README.md | 70 ++++++++--- connection_handler.py | 284 +++++++++++++++++++++++++++++++++++++----- main.py | 72 ++++++++++- match.py | 46 +++---- match_manager.py | 14 +++ session_manager.py | 12 +- user_manager.py | 16 --- 8 files changed, 524 insertions(+), 90 deletions(-) create mode 100644 .vscode/tags diff --git a/.vscode/tags b/.vscode/tags new file mode 100644 index 0000000..6d8d9b2 --- /dev/null +++ b/.vscode/tags @@ -0,0 +1,100 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ +!_TAG_PROGRAM_NAME Exuberant Ctags // +!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ +!_TAG_PROGRAM_VERSION 5.9~svn20110310 // +Base64Decode ../match.py /^def Base64Decode(jsonDump):$/;" kind:function line:13 +Base64Encode ../match.py /^def Base64Encode(ndarray):$/;" kind:function line:9 +DatabaseConnection ../database_connection.py /^class DatabaseConnection(object):$/;" kind:class line:16 +FIELD_DRAW ../match.py /^FIELD_DRAW = 3$/;" kind:variable line:33 +FIELD_EMPTY ../match.py /^FIELD_EMPTY = 0$/;" kind:variable line:30 +FIELD_USER_A ../match.py /^FIELD_USER_A = 1$/;" kind:variable line:31 +FIELD_USER_B ../match.py /^FIELD_USER_B = 2$/;" kind:variable line:32 +GameManager ../game_manager.py /^class GameManager(object):$/;" kind:class line:69 +Match ../match.py /^class Match(object):$/;" kind:class line:36 +MatchManager ../match_manager.py /^class MatchManager(object):$/;" kind:class line:6 +SQLInjectionError ../database_connection.py /^class SQLInjectionError(Exception):$/;" kind:class line:9 +SessionManager ../session_manager.py /^class SessionManager(object):$/;" kind:class line:7 +SimpleDecode ../match.py /^def SimpleDecode(jsonDump):$/;" kind:function line:26 +SimpleEncode ../match.py /^def SimpleEncode(ndarray):$/;" kind:function line:22 +User ../user.py /^class User(object):$/;" kind:class line:1 +UserManager ../user_manager.py /^class UserManager(object):$/;" kind:class line:8 +__init__ ../database_connection.py /^ def __init__(self):$/;" kind:member line:10 +__init__ ../database_connection.py /^ def __init__(self,$/;" kind:member line:58 +__init__ ../game_manager.py /^ def __init__(self, player_a_id, player_b_id, start_player, socket_a, socket_b):$/;" kind:member line:70 +__init__ ../match.py /^ def __init__(self, n, a_begins, player_a_name, player_b_name, json_state=None):$/;" kind:member line:37 +__init__ ../match_manager.py /^ def __init__(self):$/;" kind:member line:7 +__init__ ../session_manager.py /^ def __init__(self, session_lifespan_timedelta):$/;" kind:member line:8 +__init__ ../user.py /^ def __init__(self, id, name):$/;" kind:member line:2 +__init__ ../user_manager.py /^ def __init__(self):$/;" kind:member line:9 +_on_end_game ../game_manager.py /^ async def _on_end_game(self, player_id):$/;" kind:member line:172 +_on_move ../game_manager.py /^ async def _on_move(self, player_id, move_data):$/;" kind:member line:129 +cert_file ../settings.py /^cert_file = None$/;" kind:variable line:1 +check_win ../match.py /^ def check_win(self, field, x, y):$/;" kind:member line:105 +close ../database_connection.py /^ def close(self):$/;" kind:member line:92 +commit ../database_connection.py /^ def commit(self):$/;" kind:member line:96 +create_database.py ../create_database.py 1;" kind:file line:1 +create_new_match ../game_manager.py /^async def create_new_match():$/;" kind:function line:42 +create_session_for_registered_user ../session_manager.py /^ def create_session_for_registered_user(self, user_name):$/;" kind:member line:27 +create_session_for_temp_user ../session_manager.py /^ def create_session_for_temp_user(self, user_name):$/;" kind:member line:38 +create_tables ../create_database.py /^def create_tables():$/;" kind:function line:8 +create_test_manager ../user_manager.py /^def create_test_manager():$/;" kind:function line:51 +create_user ../user_manager.py /^ def create_user(self, user_name, pw):$/;" kind:member line:33 +database_connection.py ../database_connection.py 1;" kind:file line:1 +db_charset ../settings.py /^db_charset = 'utf8mb4'$/;" kind:variable line:13 +db_db ../settings.py /^db_db = "tictactoe"$/;" kind:variable line:11 +db_host ../settings.py /^db_host = "127.0.0.1"$/;" kind:variable line:6 +db_port ../settings.py /^db_port = 3306$/;" kind:variable line:7 +db_pw ../settings.py /^db_pw = "T0eT4cT!c"$/;" kind:variable line:10 +db_user ../settings.py /^db_user = "tictactoe"$/;" kind:variable line:9 +delete_session ../session_manager.py /^ def delete_session(self, session_id):$/;" kind:member line:49 +delete_user ../user_manager.py /^ def delete_user(self, user_name):$/;" kind:member line:16 +from_json_state ../match.py /^ def from_json_state(self, json_state):$/;" kind:member line:51 +game_manager.py ../game_manager.py 1;" kind:file line:1 +get_all_users ../user_manager.py /^ def get_all_users(self):$/;" kind:member line:46 +get_current_player ../match.py /^ def get_current_player(self):$/;" kind:member line:72 +get_cursor ../database_connection.py /^ def get_cursor(self):$/;" kind:member line:89 +get_id ../user.py /^ def get_id(self):$/;" kind:member line:6 +get_match ../match_manager.py /^ def get_match(self, id):$/;" kind:member line:10 +get_name ../user.py /^ def get_name(self):$/;" kind:member line:9 +get_session_by_id ../session_manager.py /^ def get_session_by_id(self, session_id):$/;" kind:member line:11 +get_session_by_registered_user ../session_manager.py /^ def get_session_by_registered_user(self, user_name):$/;" kind:member line:19 +get_session_by_temp_user ../session_manager.py /^ def get_session_by_temp_user(self, user_name):$/;" kind:member line:23 +get_sql_time ../database_connection.py /^def get_sql_time(datetime_object):$/;" kind:function line:5 +get_user ../user_manager.py /^ def get_user(self, user_name):$/;" kind:member line:12 +global_close ../database_connection.py /^ def global_close():$/;" kind:member line:29 +global_commit ../database_connection.py /^ def global_commit():$/;" kind:member line:34 +global_cursor ../database_connection.py /^ def global_cursor():$/;" kind:member line:24 +global_single_execution ../database_connection.py /^ def global_single_execution(sql_statement):$/;" kind:member line:49 +global_single_query ../database_connection.py /^ def global_single_query(query):$/;" kind:member line:39 +instance ../database_connection.py /^ instance = None$/;" kind:variable line:21 +is_full ../match.py /^ def is_full(self, field):$/;" kind:member line:102 +is_move_valid ../match.py /^ def is_move_valid(self, sub_x, sub_y, x, y):$/;" kind:member line:75 +key_file ../settings.py /^key_file = None$/;" kind:variable line:2 +main.py ../main.py 1;" kind:file line:1 +match.py ../match.py 1;" kind:file line:1 +match_manager.py ../match_manager.py 1;" kind:file line:1 +matchmaking ../game_manager.py /^async def matchmaking():$/;" kind:function line:60 +move ../match.py /^ def move(self, move_dict):$/;" kind:member line:139 +player_games ../game_manager.py /^player_games = {}$/;" kind:variable line:10 +player_id_queue ../game_manager.py /^player_id_queue = set()$/;" kind:variable line:7 +processPlayerMessage ../game_manager.py /^ async def processPlayerMessage(self, player_id, json_str):$/;" kind:member line:108 +process_message ../game_manager.py /^async def process_message(id, json):$/;" kind:function line:37 +register_user ../game_manager.py /^async def register_user(id, socket):$/;" kind:function line:13 +revoke_inactive_sessions ../session_manager.py /^ def revoke_inactive_sessions(self):$/;" kind:member line:53 +server_port ../settings.py /^server_port = 5556$/;" kind:variable line:4 +session_manager.py ../session_manager.py 1;" kind:file line:1 +settings.py ../settings.py 1;" kind:file line:1 +socket_worker ../main.py /^async def socket_worker(websocket, path):$/;" kind:function line:9 +sockets ../game_manager.py /^sockets = {}$/;" kind:variable line:8 +ssl_context ../main.py /^ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)$/;" kind:variable line:64 +startMatch ../game_manager.py /^ async def startMatch(self):$/;" kind:member line:81 +start_server ../main.py /^start_server = websockets.serve(socket_worker, host='', port=server_port, ssl=ssl_context)$/;" kind:variable line:67 +to_json_state ../match.py /^ def to_json_state(self):$/;" kind:member line:60 +touch_session ../session_manager.py /^ def touch_session(self, session_id):$/;" kind:member line:15 +touch_user ../user_manager.py /^ def touch_user(self, user_name):$/;" kind:member line:40 +unregister_user ../game_manager.py /^async def unregister_user(id):$/;" kind:function line:26 +user.py ../user.py 1;" kind:file line:1 +user_manager.py ../user_manager.py 1;" kind:file line:1 +verify_user ../user_manager.py /^ def verify_user(self, user_name, pw):$/;" kind:member line:20 diff --git a/README.md b/README.md index 72e4525..c7c619a 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,29 @@ response: game_over: , player_won: > current_player: > + player_a: "..." + player_b: "..." } ``` + + +**match**: + +```json +{ + "type": "match_update", + "data": { + "id": "...", + "match_state": > + } +} +``` + + + + + **new temp session** client @@ -129,11 +149,11 @@ server response: ```json { - "type": "temp_session_response", + "type": "login_response", "data": { "success": , "id": "", - "message": "..." + "msg": "..." } } ``` @@ -163,15 +183,38 @@ server response: } ``` -**register**: +**login or register**: + +```json +{ + "type": "login", + "data": { + "name": "", + "pw": "" + } +} +``` + +response: + +```json +{ + "type": "login_response", + "data": { + "success": , + "id": "", + "msg": "..." + } +} +``` + -TODO **match_request**: -client +client (or server for sending invites) ```json { @@ -194,8 +237,6 @@ server_response: } ``` - - **match_move**: client @@ -204,6 +245,7 @@ client { "type": "move", "data": { + "id": "match_id", "sub_x": "...", "sub_y": "...", "x": "...", @@ -212,18 +254,6 @@ client } ``` -server response: (maybe useless?) - -```json -{ - "type": "move_response", - "data": { - "success": true, - "msg": "..." - } -} -``` - **match update** @@ -255,3 +285,5 @@ client: } ``` + + diff --git a/connection_handler.py b/connection_handler.py index 1223d5e..405856c 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -22,11 +22,30 @@ class Connection(object): id, user_name: str, registered: bool, - websocket: websocket.WebSocketServerProtocol): + websocket: websockets.WebSocketServerProtocol): self.id = id self.user_name = user_name self.websocket = websocket self.is_registered_user = registered + self.is_closed = False + + async def send(self, msg): + try: + await self.websocket.send(msg) + except Exception as e: + print("error sending message to user " + + self.user_name + ". Reason: " + str(e)) + + async def close(self): + try: + if self.is_closed: + return + self.websocket.close() + self.is_closed = True + + except Exception as e: + print("error closing session to user " + + self.user_name + ". Reason: " + str(e)) class ConnectionHandler(object): @@ -71,7 +90,7 @@ class ConnectionHandler(object): if conn.id in self.open_connections_by_id: del(self.open_connections_by_id[conn.id]) del(self.open_connections_by_user[conn.user_name]) - conn.websocket.close() + conn.close() def close_session(self, id): tmp = self.session_manager.get_session_by_id(id) @@ -85,18 +104,18 @@ class ConnectionHandler(object): def _add_connection(self, conn): self.open_connections_by_id[conn.id] = conn - self.open_connections_by_user[conn.user] = conn + self.open_connections_by_user[conn.user_name] = conn def _del_connection(self, conn): del(self.open_connections_by_id[conn.id]) - del(self.open_connections_by_user[conn.user]) + del(self.open_connections_by_user[conn.user_name]) + async def new_connection(self, socket: websockets.WebSocketServerProtocol, login_msg: str): - msg = parse_message(login_msg) - + print(msg) if msg is None: return None @@ -104,7 +123,7 @@ class ConnectionHandler(object): conn = self.reconnect_session(socket, msg['data']['id']) if conn is not None: self._add_connection(conn) - await conn.websocket.send(json.dumps({ + await conn.send(json.dumps({ "type": "reconnect_response", "data": { "success": True, @@ -112,7 +131,7 @@ class ConnectionHandler(object): } })) return conn - await conn.websocket.send(json.dumps({ + await conn.send(json.dumps({ "type": "reconnect_response", "data": { "success": False, @@ -128,43 +147,100 @@ class ConnectionHandler(object): if len(msg['data']['name']) < 16 and ';' not in name and '\'' not in name and '\"' not in name: id = self.session_manager.create_session_for_temp_user( name) + conn = Connection( id=id, user_name=name, registered=False, websocket=socket) + self._add_connection(conn) + await socket.send(json.dumps({ - "type": "temp_session_response", + "type": "login_response", "data": { "success": True, "id": id, - "message": "logged in as temporary user " + name + "msg": "logged in as temporary user " + name } })) + + + return conn await socket.send(json.dumps({ - "type": "temp_session_response", + "type": "login_response", "data": { "success": False, "id": None, - "message": "user name not available" + "msg": "user name not available" } })) return None elif msg['type'] == 'login': - # TODO - pass + response_msg = "" + success = False + session_id = None + name = None + pw = None + conn = None - elif msg['type'] == 'register': - # TODO - pass + try: + + name = msg['data']['name'] + pw = msg['data']['pw'] + + if len(name) <= 16 and len(pw) <= 32 and len(name) > 0 and len(pw) > 0: + + users = self.user_manager.get_user(name) + if len(users) == 0: + # user does not exists: + self.user_manager.create_user(name, pw) + session_id = self.session_manager.create_session_for_registered_user( + name) + response_msg = "successful registered and logged in user " + name + success = True + + elif self.user_manager.verify_user(name, pw): + session_id = self.session_manager.create_session_for_registered_user( + name) + response_msg = "successful logged in as user " + name + success = True + + else: + response_msg = "invalid password for user " + name + else: + response_msg = "invalid username or pw" + + except Exception as e: + response_msg = "invalid username or pw" + + if success: + conn = Connection(id=session_id, user_name=name, + registered=True, websocket=socket) + self._add_connection(conn) + + await socket.send(json.dumps({ + "type": "login_response", + "data": { + "success": success, + "id": session_id, + "msg": response_msg + } + })) + + if success: + await self._on_match_state_req(conn, None) + + return conn + + return None async def _start_match(self, user_a, user_b): m = self.match_manager.create_new_match(user_a, user_b) - state = m.to_json_state() + state = json.loads(m.to_json_state()) if user_a in self.open_connections_by_user: await self.open_connections_by_user[user_a].websocket.send( - json.dumps({ + json.dumps( { "type": "match_update", "data": { @@ -172,11 +248,11 @@ class ConnectionHandler(object): "match_state": state } } - }) + ) ) if user_b in self.open_connections_by_user: await self.open_connections_by_user[user_b].websocket.send( - json.dumps({ + json.dumps( { "type": "match_update", "data": { @@ -184,23 +260,168 @@ class ConnectionHandler(object): "match_state": state } } - }) + ) ) async def _on_match_req(self, conn, data): - if len(self.match_queue) > 0: - # it's a match! - user_a = self.match_queue.pop() - self._start_match(user_a, conn.user_name) + if data['player'] is None: + if len(self.match_queue) > 0: + # it's a match! + user_a = self.match_queue.pop() + await self._start_match(user_a, conn.user_name) + + else: + if conn.user_name not in self.match_queue: + self.match_queue.add(conn.user_name) + + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": True, + "msg": "created match request" + } + } + ) + ) else: - self.match_queue.append(conn.user_name) + opponent = data['player'] + try: + if len(opponent) <= 16 and '\'' not in opponent and '"' not in opponent: + if len(self.user_manager.get_user(opponent)) > 0: + await self._start_match(conn.user_name, opponent) + + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": True, + "msg": "startet match against " + opponent + } + } + ) + ) + else: + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": False, + "msg": "user " + opponent + " not found :(" + } + } + ) + ) + + + except Exception as e: + print("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) + for db_match in db_matches: + match = self.match_manager.get_match(db_match['id']) + await conn.send(json.dumps({ + "type": "match_update", + "data": { + "id": db_match['id'], + "match_state": json.loads(match.to_json_state()) + } + })) + if match.game_over: + if match.player_won is None or match.player_won != conn.user_name: + self.match_manager.delete_match(match.id) async def _on_match_move(self, conn, data): - pass + + match = None + + try: + sub_x = int(data['sub_x']) + sub_y = int(data['sub_y']) + x = int(data['x']) + y = int(data['y']) + + if type(sub_x) is int and type(sub_y) is int: + if type(x) is int and type(y) is int: + if type(data['id']) is str: + match = self.match_manager.apply_move(data) + + finally: + match_state = None + if match is not None: + match_state = match.to_json_state() + + print(match_state) + + await conn.send(json.dumps({ + 'type': 'match_update', + 'data': { + 'id': match.id, + 'match_state': json.loads(match_state) + } + })) + + other_user = match.player_a_name if conn.user_name == match.player_b_name else match.player_b_name + + if other_user in self.open_connections_by_user: + other_conn = self.open_connections_by_user[other_user] + await other_conn.send(json.dumps({ + 'type': 'match_update', + 'data': { + 'id': match.id, + 'match_state': json.loads(match_state) + } + })) + if match.game_over: + self.match_manager.delete_match(match.id) async def _on_match_close(self, conn, data): - pass + match = None + try: + match_id = data['id'] + if type(match_id) is str: + match = self.match_manager.get_match(match_id) + + if (match is None): + return + + match.game_over = True + + match_state = match.to_json_state() + + opponent = match.player_a_name if match.player_a_name != conn.user_name else match.player_b_name + + response = json.dumps({ + 'type': 'match_update', + 'data': { + 'id': match_id, + 'match_state': json.loads(match_state) + } + }) + + if opponent in self.open_connections_by_user: + await self.open_connections_by_user[opponent].websocket.send(response) + + await conn.send(response) + + self.match_manager.delete_match(match_id) + + except Exception as e: + match_state = None + if match is not None: + match_state = match.to_json_state() + + conn.send(json.dumps({ + 'type': 'match_update', + 'data': { + 'match_state': json.loads(match_state) + } + })) async def disconnect(self, conn): self._del_connection(conn) @@ -208,6 +429,8 @@ class ConnectionHandler(object): async def handle_message(self, conn, msg_str): msg = parse_message(msg_str) + print(msg) + if msg is None: return None @@ -223,5 +446,8 @@ class ConnectionHandler(object): elif t == "end_match": await self._on_match_close(conn, msg['data']) + elif t == "match_states_request": + await self._on_match_state_req(conn, msg['data']) + else: print("could not interpret message: " + msg_str) diff --git a/main.py b/main.py index ac582ce..edb9f53 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,70 @@ import websockets from settings import * import ssl +import traceback + from game_manager import * +from session_manager import SessionManager +from connection_handler import Connection, ConnectionHandler +from match_manager import MatchManager +from user_manager import UserManager +from database_connection import DatabaseConnection +import datetime + +um = UserManager() +sm = SessionManager(datetime.timedelta(hours=12)) +mm = MatchManager() +ch = ConnectionHandler(sm, um, mm) + + +DatabaseConnection(db_host, + db_port, + db_user, + db_pw, + db_db, + db_charset) + + +async def new_socket_worker(websocket, path): + connection = None + + print("new incomin connection") + + try: + raw_msg = await websocket.recv() + + connection = await ch.new_connection(websocket, raw_msg) + + print(ch.open_connections_by_id) + print(ch.open_connections_by_user) + + if connection is None: + return + + async for m in websocket: + await ch.handle_message(connection, m) + + except Exception as e: + # TODO: each disconnect is an exception so far + if connection is not None: + print("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()) + + finally: + id = None + if connection: + id = connection.user_name + print(ch.open_connections_by_id) + await ch.disconnect(connection) + await connection.close() + + if connection is None: + id = "unknown_user" + print("close connection to user: " + id) async def socket_worker(websocket, path): @@ -52,6 +115,7 @@ async def socket_worker(websocket, path): print("catched exception in worker for user: " + id + ": " + str(e)) else: print("catched exception in worker for unknown user") + finally: if registered: @@ -61,10 +125,12 @@ async def socket_worker(websocket, path): id = "unknown_user" print("close connection to user: " + id) -ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) -ssl_context.load_cert_chain(cert_file , keyfile=key_file) -start_server = websockets.serve(socket_worker, host='', port=server_port, ssl=ssl_context) +#ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +#ssl_context.load_cert_chain(cert_file, keyfile=key_file) + +start_server = websockets.serve( + new_socket_worker, host='', port=server_port) # , ssl=ssl_context) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() diff --git a/match.py b/match.py index 3a84ae7..e1fa5cf 100644 --- a/match.py +++ b/match.py @@ -65,7 +65,9 @@ class Match(object): 'last_move': self.last_move, 'game_over': self.game_over, 'player_won': self.player_won, - 'active_player': self.player_a_name if self.is_player_a else self.player_b_name + 'active_player': self.player_a_name if self.is_player_a else self.player_b_name, + 'player_a': self.player_a_name, + 'player_b': self.player_b_name } return json.dumps(match_obj) @@ -95,15 +97,17 @@ class Match(object): last_sub_x = self.last_move['sub_x'] last_sub_y = self.last_move['sub_y'] - if sub_x != last_x and self.global_field[last_sub_y, last_sub_x] == FIELD_EMPTY: + if sub_x != last_x and self.global_field[last_y, last_x] == FIELD_EMPTY: # user is not allowed to place everywhere! wrong move! return False - if sub_y != last_y and self.global_field[last_sub_y, last_sub_x] == FIELD_EMPTY: + if sub_y != last_y and self.global_field[last_y, last_x] == FIELD_EMPTY: return False if self.complete_field[sub_y * self.n + y][sub_x * self.n + x] != FIELD_EMPTY: return False + + return True def is_full(self, field): return not field.__contains__(FIELD_EMPTY) @@ -136,17 +140,17 @@ class Match(object): if x + y == self.n - 1: is_sec_diag = True for i in range(self.n): - if field[i, -i] != val: + if field[i, self.n - i - 1] != val: is_sec_diag = False break return is_col or is_row or is_main_diag or is_sec_diag def move(self, move_dict): - sub_x = move_dict['sub_x'] - sub_y = move_dict['sub_y'] - x = move_dict['x'] - y = move_dict['y'] + sub_x = int(move_dict['sub_x']) + sub_y = int(move_dict['sub_y']) + x = int(move_dict['x']) + y = int(move_dict['y']) abs_x = sub_x * self.n + x abs_y = sub_y * self.n + y @@ -154,34 +158,34 @@ 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") return False # else: move! self.complete_field[abs_y, abs_x] = player_mark # encode move: - self.last_move = move_dict + self.last_move = {'sub_x': sub_x, 'sub_y': sub_y, 'x': x, 'y': y} # check whether this indicates changes in the global field: - assert self.global_field[sub_y, sub_x] == FIELD_EMPTY + if self.global_field[sub_y, sub_x] != FIELD_EMPTY: + print("field not empty") + return False subgrid = self.complete_field[sub_y * self.n: ( sub_y + 1) * self.n, sub_x * self.n: (sub_x + 1) * self.n] if self.check_win(subgrid, x, y): - self.global_field[sub_x, sub_y] = player_mark + self.global_field[sub_y, sub_x] = player_mark + if self.check_win(self.global_field, sub_x, sub_y): + self.game_over = True + self.player_won = self.player_a_name if self.is_player_a else self.player_b_name elif self.is_full(subgrid): - self.global_field[sub_x, sub_y] = FIELD_DRAW - - # check global state: - if self.check_win(self.global_field, sub_x, sub_y): - self.game_over = True - self.player_won = self.player_a_name if self.is_player_a else self.player_b_name - - elif self.is_full(self.global_field): - self.game_over = True - self.player_won = None + self.global_field[sub_y, sub_x] = FIELD_DRAW + if self.is_full(self.global_field): + self.game_over = True + self.player_won = None self.is_player_a = not self.is_player_a diff --git a/match_manager.py b/match_manager.py index ab75d45..4d0c578 100644 --- a/match_manager.py +++ b/match_manager.py @@ -40,6 +40,20 @@ class MatchManager(object): 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) + 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 delete_match(self, match_id): query = f"DELETE FROM matches WHERE id='{match_id}'" DatabaseConnection.global_single_execution(query) diff --git a/session_manager.py b/session_manager.py index a8d316e..543143f 100644 --- a/session_manager.py +++ b/session_manager.py @@ -29,7 +29,11 @@ class SessionManager(object): # 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: # okay, next try: - return self.create_session_for_registered_user(self, user_name) + 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) @@ -40,7 +44,11 @@ class SessionManager(object): # 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: # okay, next try: - return self.create_session_for_registered_user(self, user_name) + 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) diff --git a/user_manager.py b/user_manager.py index c5a4e4e..ae243b8 100644 --- a/user_manager.py +++ b/user_manager.py @@ -47,19 +47,3 @@ class UserManager(object): query = "SELECT name, last_seen FROM users" return DatabaseConnection.global_single_query(query) - -def create_test_manager(): - import settings - DatabaseConnection(settings.db_host, - settings.db_port, - settings.db_user, - settings.db_pw, - settings.db_db, - settings.db_charset) - um = UserManager() - return um - - -# test routines: -if __name__ == "__main__": - pass From a5cfccbdfcc0b2e2cfc4b8244693174b7a08f713 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 21 Mar 2019 15:42:34 +0100 Subject: [PATCH 04/10] handle reconnects correctly --- connection_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/connection_handler.py b/connection_handler.py index 405856c..940ab66 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -123,10 +123,12 @@ class ConnectionHandler(object): conn = self.reconnect_session(socket, msg['data']['id']) if conn is not None: self._add_connection(conn) - await conn.send(json.dumps({ + await socket.send(json.dumps({ "type": "reconnect_response", "data": { "success": True, + "id": conn.id, + "user": conn.user_name, "msg": "" } })) @@ -135,6 +137,8 @@ class ConnectionHandler(object): "type": "reconnect_response", "data": { "success": False, + "id": conn.id, + "user": conn.user_name, "msg": "session not available" } })) From 972d438bdf70727725db0af4c72bf73e13e68dec Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 21 Mar 2019 15:59:18 +0100 Subject: [PATCH 05/10] little changes --- connection_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/connection_handler.py b/connection_handler.py index 940ab66..e98851c 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -132,6 +132,7 @@ class ConnectionHandler(object): "msg": "" } })) + await self._on_match_state_req(conn, None) return conn await conn.send(json.dumps({ "type": "reconnect_response", From 181686189c208d324c0fd5d5cb58f8ce69b65629 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 21 Mar 2019 17:02:17 +0100 Subject: [PATCH 06/10] better matchmaking --- connection_handler.py | 48 ++++++++++- game_manager.py | 185 ------------------------------------------ main.py | 67 +-------------- settings.py | 10 ++- 4 files changed, 55 insertions(+), 255 deletions(-) delete mode 100644 game_manager.py diff --git a/connection_handler.py b/connection_handler.py index e98851c..9af7468 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -110,7 +110,6 @@ class ConnectionHandler(object): del(self.open_connections_by_id[conn.id]) del(self.open_connections_by_user[conn.user_name]) - async def new_connection(self, socket: websockets.WebSocketServerProtocol, login_msg: str): @@ -167,8 +166,6 @@ class ConnectionHandler(object): } })) - - return conn await socket.send(json.dumps({ @@ -269,7 +266,38 @@ class ConnectionHandler(object): ) async def _on_match_req(self, conn, data): + n_open_matches = len( + self.match_manager.get_matches_for_user(conn.user_name)) + + if n_open_matches >= 5: + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": False, + "msg": "you have too many active matches to search for a new one" + } + } + ) + ) + return + if data['player'] is None: + if conn.user_name in self.match_queue: + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": False, + "msg": "you are already searching for a random match" + } + } + ) + ) + return + if len(self.match_queue) > 0: # it's a match! user_a = self.match_queue.pop() @@ -293,6 +321,19 @@ class ConnectionHandler(object): else: opponent = data['player'] + if opponent == conn.user_name: + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": False, + "msg": "you cannot play against yourself" + } + } + ) + ) + return try: if len(opponent) <= 16 and '\'' not in opponent and '"' not in opponent: if len(self.user_manager.get_user(opponent)) > 0: @@ -322,7 +363,6 @@ class ConnectionHandler(object): ) ) - except Exception as e: print("error processing match request: " + str(data) + str(e)) diff --git a/game_manager.py b/game_manager.py deleted file mode 100644 index c89221b..0000000 --- a/game_manager.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import websockets -import json - -player_id_queue = set() -sockets = {} - -player_games = {} - - -async def register_user(id, socket): - - if id in player_id_queue or id in player_games: - return False - - player_id_queue.add(id) - sockets[id] = socket - - await matchmaking() - - return True - - -async def unregister_user(id): - if id in player_id_queue: - player_id_queue.remove(id) - del(sockets[id]) - - elif id in player_games: - # we have an active game and have to end it - await player_games[id]._on_end_game(id) - del(player_games[id]) - - -async def process_message(id, json): - if id in player_games: - await player_games[id].processPlayerMessage(id, json) - - -async def create_new_match(): - p_a = player_id_queue.pop() - p_b = player_id_queue.pop() - - s_a = sockets[p_a] - s_b = sockets[p_b] - - del(sockets[p_a]) - del(sockets[p_b]) - - new_game = GameManager(p_a, p_b, p_a, s_a, s_b) - - player_games[p_a] = new_game - player_games[p_b] = new_game - - await new_game.startMatch() - - -async def matchmaking(): - if len(player_id_queue) < 2: - # we need at least 2 users for that - return - - else: - asyncio.ensure_future(create_new_match()) - - -class GameManager(object): - def __init__(self, player_a_id, player_b_id, start_player, socket_a, socket_b): - self.player_a_id = player_a_id - self.player_b_id = player_b_id - - self.socket_a = socket_a - self.socket_b = socket_b - - self.current_player = start_player - - self.game_finished = False - - async def startMatch(self): - - print("match starts") - - start_msg_a = { - 'type': 'game_starts', - 'data': { - 'msg': '...', - 'opponent_name': self.player_b_id, - 'is_first_move': True - } - } - - start_msg_b = { - 'type': 'game_starts', - 'data': { - 'msg': '...', - 'opponent_name': self.player_a_id, - 'is_first_move': False - } - } - - await self.socket_a.send(json.dumps(start_msg_a)) - await self.socket_b.send(json.dumps(start_msg_b)) - - print("start message send to all players") - - async def processPlayerMessage(self, player_id, json_str): - if len(json_str) > 4096: - # something is fishy here - print("received strange message from client") - - print("received message: " + json_str) - - try: - json_dict = json.loads(json_str) - type = json_dict['type'] - data = json_dict['data'] - - if type == "move": - await self._on_move(player_id, data) - - elif type == "end_game": - await self._on_end_game(player_id) - - except Exception as e: - print("" + str(e) + ": received wrong formated message") - - async def _on_move(self, player_id, move_data): - response = {'type': 'move_response'} - response_data = {} - - opponent_response = {'type': 'move'} - opponent_response_data = {} - - opponent_response_data['sub_x'] = move_data['sub_x'] - opponent_response_data['sub_y'] = move_data['sub_y'] - opponent_response_data['x'] = move_data['x'] - opponent_response_data['y'] = move_data['y'] - opponent_response['data'] = opponent_response_data - - if player_id == self.current_player: - - is_a = (self.player_a_id == player_id) - current_socket = self.socket_a if is_a else self.socket_b - opponent_socket = self.socket_b if is_a else self.socket_a - - response_data['success'] = True - response_data['msg'] = "move successful" - - response['data'] = response_data - - await opponent_socket.send(json.dumps(opponent_response)) - await current_socket.send(json.dumps(response)) - - # switch player - self.current_player = self.player_b_id if is_a else self.player_a_id - - else: - print("received move from wrong player") - - is_a = (self.player_a_id == player_id) - current_socket = self.socket_a if is_a else self.socket_b - - response_data["success"] = False - response_data["msg"] = "not your turn!" - - response['data'] = response_data - - await current_socket.send(json.dumps(response)) - - async def _on_end_game(self, player_id): - - if self.game_finished: - return - - is_a = (self.player_a_id == player_id) - opponent_socket = self.socket_b if is_a else self.socket_a - - opponent_response = {'type': 'end_game'} - opponent_response['data'] = {'msg': 'game closed by opponent'} - - await opponent_socket.send(json.dumps(opponent_response)) - - self.game_finished = True diff --git a/main.py b/main.py index edb9f53..e5d4531 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,6 @@ import ssl import traceback -from game_manager import * from session_manager import SessionManager from connection_handler import Connection, ConnectionHandler from match_manager import MatchManager @@ -27,7 +26,7 @@ DatabaseConnection(db_host, db_charset) -async def new_socket_worker(websocket, path): +async def socket_worker(websocket, path): connection = None print("new incomin connection") @@ -68,69 +67,11 @@ async def new_socket_worker(websocket, path): id = "unknown_user" print("close connection to user: " + id) - -async def socket_worker(websocket, path): - - registered = False - id = None - - print("new connection") - - try: - # get first message as register message - raw_msg = await websocket.recv() - - msg = json.loads(raw_msg) - - if msg['type'] != 'register': - print("got wrong registration") - websocket.close() - return - - id = msg['data']['id'] - - registered = await register_user(id, websocket) - - register_response = { - 'type': 'register_response', 'data': { - 'success': True, 'msg': '...'}} - - if not registered: - register_response['data']['success'] = False - - await websocket.send(json.dumps(register_response)) - websocket.close() - return - - await websocket.send(json.dumps(register_response)) - - print("successful redisterd user " + id) - - async for m in websocket: - await process_message(id, m) - - except Exception as e: - # TODO: each disconnect is an exception so far - if id is not None: - print("catched exception in worker for user: " + id + ": " + str(e)) - else: - print("catched exception in worker for unknown user") - - - finally: - if registered: - await unregister_user(id) - - if id is None: - id = "unknown_user" - print("close connection to user: " + id) - - -#ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) -#ssl_context.load_cert_chain(cert_file, keyfile=key_file) +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +ssl_context.load_cert_chain(cert_file, keyfile=key_file) start_server = websockets.serve( - new_socket_worker, host='', port=server_port) # , ssl=ssl_context) + socket_worker, host='', port=server_port, ssl=ssl_context) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() diff --git a/settings.py b/settings.py index 28f82d5..971723c 100644 --- a/settings.py +++ b/settings.py @@ -6,7 +6,11 @@ server_port = 5556 db_host = "127.0.0.1" db_port = 3306 -db_user = "tictactoe" -dp_pw = "" +db_user = None +db_pw = None +db_db = None -charset = 'utf8mb4' +db_charset = 'utf8mb4' + +# field dimension +n = 3 From 8dabfec8855da47039f995f3e5dcb33e332d4613 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 21 Mar 2019 17:08:49 +0100 Subject: [PATCH 07/10] cleaned up debug messages --- connection_handler.py | 4 ++-- main.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/connection_handler.py b/connection_handler.py index 9af7468..f3c62a9 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -114,7 +114,7 @@ class ConnectionHandler(object): socket: websockets.WebSocketServerProtocol, login_msg: str): msg = parse_message(login_msg) - print(msg) + print("new incomming connection...") if msg is None: return None @@ -474,7 +474,7 @@ class ConnectionHandler(object): async def handle_message(self, conn, msg_str): msg = parse_message(msg_str) - print(msg) + print("incoming message" + msg) if msg is None: return None diff --git a/main.py b/main.py index e5d4531..967ab9b 100644 --- a/main.py +++ b/main.py @@ -29,19 +29,17 @@ DatabaseConnection(db_host, async def socket_worker(websocket, path): connection = None - print("new incomin connection") try: raw_msg = await websocket.recv() connection = await ch.new_connection(websocket, raw_msg) - print(ch.open_connections_by_id) - print(ch.open_connections_by_user) - if connection is None: return + print("successfull logged in user: " + connection.user_name) + async for m in websocket: await ch.handle_message(connection, m) @@ -59,7 +57,6 @@ async def socket_worker(websocket, path): id = None if connection: id = connection.user_name - print(ch.open_connections_by_id) await ch.disconnect(connection) await connection.close() From 56f4fecdcd0b927b66b0ba01a4bcdd6659dd2b16 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Thu, 21 Mar 2019 17:21:36 +0100 Subject: [PATCH 08/10] hotfix --- connection_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection_handler.py b/connection_handler.py index f3c62a9..23256bd 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -474,7 +474,7 @@ class ConnectionHandler(object): async def handle_message(self, conn, msg_str): msg = parse_message(msg_str) - print("incoming message" + msg) + print("incoming message" + str(msg)) if msg is None: return None From 049cb432641df6edbe6a630676ab20ac6842c6cc Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Fri, 22 Mar 2019 23:04:32 +0100 Subject: [PATCH 09/10] names are only lowercase --- connection_handler.py | 6 +++--- main.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/connection_handler.py b/connection_handler.py index 23256bd..959c7cf 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -145,7 +145,7 @@ class ConnectionHandler(object): return None elif msg['type'] == 'temp_session': - name = msg['data']['name'] + name = msg['data']['name'].lower() if len(self.session_manager.get_session_by_temp_user(name)) == 0: if len(self.user_manager.get_user(name)) == 0: if len(msg['data']['name']) < 16 and ';' not in name and '\'' not in name and '\"' not in name: @@ -188,7 +188,7 @@ class ConnectionHandler(object): try: - name = msg['data']['name'] + name = msg['data']['name'].lower() pw = msg['data']['pw'] if len(name) <= 16 and len(pw) <= 32 and len(name) > 0 and len(pw) > 0: @@ -320,7 +320,7 @@ class ConnectionHandler(object): ) else: - opponent = data['player'] + opponent = data['player'].lower() if opponent == conn.user_name: await conn.websocket.send( json.dumps( diff --git a/main.py b/main.py index 967ab9b..0e158ec 100644 --- a/main.py +++ b/main.py @@ -64,11 +64,16 @@ async def socket_worker(websocket, path): id = "unknown_user" print("close connection to user: " + id) -ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) -ssl_context.load_cert_chain(cert_file, keyfile=key_file) +if cert_file is not None and key_file is not None: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(cert_file, keyfile=key_file) -start_server = websockets.serve( - socket_worker, host='', port=server_port, ssl=ssl_context) + start_server = websockets.serve( + socket_worker, host='', port=server_port, ssl=ssl_context) + +else: + start_server = websockets.serve( + socket_worker, host='', port=server_port) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() From eb5f473de0d13e3e219ba8ac49550c20c45880c5 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Sun, 24 Mar 2019 16:44:20 +0100 Subject: [PATCH 10/10] friends --- README.md | 59 ++++++++++++++++++++++ connection_handler.py | 114 +++++++++++++++++++++++++++++++++++++++++- create_database.py | 4 +- match_manager.py | 4 ++ user_manager.py | 26 ++++++++++ 5 files changed, 205 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7c619a..82b1743 100644 --- a/README.md +++ b/README.md @@ -287,3 +287,62 @@ client: +**friend request**: + +```json +{ + "type": "friend_request", + "data" : { + "user": "" + } +} +``` + +response: + +```json +{ + "type": "friend_request_response", + "data": { + "success": + "msg": "..." + } +} +``` + +**unfriend**: + +```json +{ + "type": "unfriend_request", + "data" : { + "user": "" + } +} +``` + +response: + +```json +{ + "type": "unfriend_request_response", + "data": { + "success": + "msg": "..." + } +} +``` + + + +**friend update**: + +```json +{ + "type": "friends_update", + "data": { + "friends": "" + } +} +``` + diff --git a/connection_handler.py b/connection_handler.py index 959c7cf..08c257b 100644 --- a/connection_handler.py +++ b/connection_handler.py @@ -131,6 +131,7 @@ class ConnectionHandler(object): "msg": "" } })) + await self.send_friends(conn) await self._on_match_state_req(conn, None) return conn await conn.send(json.dumps({ @@ -231,6 +232,7 @@ class ConnectionHandler(object): })) if success: + await self.send_friends(conn) await self._on_match_state_req(conn, None) return conn @@ -337,6 +339,35 @@ class ConnectionHandler(object): try: if len(opponent) <= 16 and '\'' not in opponent and '"' not in opponent: if len(self.user_manager.get_user(opponent)) > 0: + + if len(self.match_manager.get_matches_for_user(conn.user_name)) >= 5: + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": False, + "msg": "player " + opponent + " has too many open matches" + } + } + ) + ) + return + + if (self.match_manager.is_match(conn.user_name, opponent)): + await conn.websocket.send( + json.dumps( + { + "type": "match_request_response", + "data": { + "success": False, + "msg": "you are already plaing against " + opponent + } + } + ) + ) + return + await self._start_match(conn.user_name, opponent) await conn.websocket.send( @@ -461,13 +492,88 @@ class ConnectionHandler(object): if match is not None: match_state = match.to_json_state() - conn.send(json.dumps({ + await conn.send(json.dumps({ 'type': 'match_update', 'data': { 'match_state': json.loads(match_state) } })) + async def _on_friend_request(self, conn, data): + msg = "error in handling friend request" + success = False + try: + friend = data['user'].lower() + + # check for user: + if "\"" not in friend and "'" not in friend and ";" not in friend: + if friend in self.user_manager.get_friends_for_user(conn.user_name): + success = False + msg = f"'{friend}' is already your friend" + + elif self.user_manager.add_friend_to_user(conn.user_name, friend): + success = True + msg = f"added '{friend}' as a friend" + + else: + success = False + msg = f"player '{friend}' not found" + + else: + success = False + msg = "misformated friend request" + + finally: + await conn.send(json.dumps({ + 'type': 'friend_request_response', + 'data': { + 'success': success, + 'msg': msg + } + })) + + if success: + await self.send_friends(conn) + + async def _on_unfriend_request(self, conn, data): + success = False + msg = "error in handling unfriend request" + try: + friend = data['user'].lower() + + if "\"" not in friend and "'" not in friend and ";" not in friend: + if friend not in self.user_manager.get_friends_for_user(conn.user_name): + success = False + msg = f"cannot end friendship with '{friend}': it's not one of your friends" + else: + self.user_manager.remove_friend_from_user( + conn.user_name, friend) + + success = True + msg = f"removed '{friend}' from your friend list" + + finally: + await conn.send(json.dumps({ + 'type': 'unfriend_request_response', + 'data': { + 'success': success, + 'msg': msg + } + })) + + if success: + await self.send_friends(conn) + + async def send_friends(self, conn): + friends = list(self.user_manager.get_friends_for_user(conn.user_name)) + + await conn.send(json.dumps({ + 'type': 'friends_update', + 'data': { + 'friends': friends + } + })) + async def disconnect(self, conn): self._del_connection(conn) @@ -494,5 +600,11 @@ class ConnectionHandler(object): elif t == "match_states_request": await self._on_match_state_req(conn, msg['data']) + elif t == "friend_request": + await self._on_friend_request(conn, msg['data']) + + elif t == "unfriend_request": + await self._on_unfriend_request(conn, msg['data']) + else: print("could not interpret message: " + msg_str) diff --git a/create_database.py b/create_database.py index c2adf94..914a4eb 100755 --- a/create_database.py +++ b/create_database.py @@ -18,9 +18,11 @@ def create_tables(): "DROP TABLE IF EXISTS matches", "DROP TABLE IF EXISTS sessions", "DROP TABLE IF EXISTS users", + "DROP TABLE IF EXISTS friends", "CREATE TABLE users (name varchar(16) NOT NULL, pw_hash varchar(128) NOT NULL, pw_salt varchar(32) NOT NULL, last_seen datetime NOT NULL, PRIMARY KEY (name)) CHARACTER SET " + settings.db_charset, "CREATE TABLE matches (id varchar(32) NOT NULL, user_a varchar(16) NOT NULL, user_b varchar(16) NOT NULL, match_state varchar(4096) NOT NULL, active_user varchar(16), last_active datetime NOT NULL, FOREIGN KEY (user_a) REFERENCES users(name), FOREIGN KEY (user_b) REFERENCES users(name), FOREIGN KEY (active_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset, - "CREATE TABLE sessions (id varchar(32) NOT NULL, registered_user varchar(16), temp_user varchar(16), last_seen datetime NOT NULL, PRIMARY KEY (id), FOREIGN KEY(registered_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset + "CREATE TABLE sessions (id varchar(32) NOT NULL, registered_user varchar(16), temp_user varchar(16), last_seen datetime NOT NULL, PRIMARY KEY (id), FOREIGN KEY(registered_user) REFERENCES users(name)) CHARACTER SET " + settings.db_charset, + "CREATE TABLE friends (user varchar(16) NOT NULL, friend varchar(16) NOT NULL, FOREIGN KEY(user) REFERENCES users(name), FOREIGN KEY(friend) REFERENCES users(name)) CHARACTER SET " + settings.db_charset ] for query in queries: diff --git a/match_manager.py b/match_manager.py index 4d0c578..438c28c 100644 --- a/match_manager.py +++ b/match_manager.py @@ -54,6 +54,10 @@ class MatchManager(object): 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) diff --git a/user_manager.py b/user_manager.py index ae243b8..da6af5a 100644 --- a/user_manager.py +++ b/user_manager.py @@ -17,6 +17,13 @@ class UserManager(object): query = f"DELETE FROM users where name='{user_name}'" DatabaseConnection.global_single_execution(query) + # remove from friends: + query = f"DELETE FROM friends WHERE user='{user_name}'" + DatabaseConnection.global_single_execution(query) + + query = f"DELETE FROM friends WHERE friend='{user_name}'" + DatabaseConnection.global_single_execution(query) + def verify_user(self, user_name, pw): query = f"SELECT * FROM users where name='{user_name}'" users = DatabaseConnection.global_single_query(query) @@ -42,6 +49,25 @@ class UserManager(object): assert len(matches) == 1 query = f"UPDATE users SET last_seen='{datetime.datetime.now()}' WHERE name='{user_name}'" DatabaseConnection.global_single_execution(query) + + 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) + 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) + 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) def get_all_users(self): query = "SELECT name, last_seen FROM users"