From 45129c5cda7fd3c958e4be885b31709a71fd9678 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 9 Nov 2022 20:25:06 +0400 Subject: [PATCH] Update proxy server --- proxyServer.py | 477 +++++++++++++++++++++++++++++-------------------- 1 file changed, 283 insertions(+), 194 deletions(-) diff --git a/proxyServer.py b/proxyServer.py index 096ccafd1..a6a0262f9 100755 --- a/proxyServer.py +++ b/proxyServer.py @@ -11,14 +11,7 @@ PROTOCOL_VERSION_MAX = 1 SERVER_HOST = "0.0.0.0" SERVER_PORT = 5002 # port we want to use -def send_msg(sock, msg, doPack): - # For 1 byte (bool) send just that - # Prefix each message with a 4-byte length (network byte order) - if doPack and len(msg) > 1: - msg = struct.pack(' None: self.name = name @@ -85,6 +74,22 @@ class Session: self.players.remove(player) self.joined -= 1 + +class Session: + name: str # name of session + host_uuid: str # uuid of vcmiserver for hosting player + clients_uuid: list # list od vcmiclients uuid + players: list # list of sockets of players, joined to the session + connections: list # list of GameConnections for vcmiclient/vcmiserver (game mode) + + def __init__(self) -> None: + self.name = "" + self.host_uuid = "" + self.clients_uuid = [] + self.players = [] + self.connections = [] + pass + def addConnection(self, conn: socket, isServer: bool): #find uninitialized server connection for gc in self.connections: @@ -118,7 +123,63 @@ class Session: if gc.client == conn: return gc.server - + +class Client: + auth: bool + + def __init__(self) -> None: + self.auth = False + + +class ClientLobby(Client): + joined: bool + username: str + room: Room + protocolVersion: int + encoding: str + ready: bool + vcmiversion: str #TODO: check version compatibility + + def __init__(self) -> None: + super().__init__() + self.room = None + self.joined = False + self.username = "" + self.protocolVersion = 0 + self.encoding = 'utf8' + self.ready = False + self.vcmiversion = "" + + +class ClientPipe(Client): + apptype: str #client/server + prevmessages: list + session: Session + uuid: str + + def __init__(self) -> None: + super().__init__() + self.prevmessages = [] + self.session = None + self.apptype = "" + self.uuid = "" + + +class Sender: + address: str #full client address + client: Client + + def __init__(self) -> None: + self.client = None + pass + + def isLobby(self) -> bool: + return isinstance(self.client, ClientLobby) + + def isPipe(self) -> bool: + return isinstance(self.client, ClientPipe) + + # create a TCP socket s = socket.socket() # make the port as reusable port @@ -129,45 +190,57 @@ s.bind((SERVER_HOST, SERVER_PORT)) s.listen(10) print(f"[*] Listening as {SERVER_HOST}:{SERVER_PORT}") +# active rooms +rooms = {} + # list of active sessions -sessions = dict() +sessions = [] + +# initialize list/set of all connected client's sockets +client_sockets = {} def handleDisconnection(client: socket): + if not client in client_sockets: + return + sender = client_sockets[client] - if sender["joined"]: - if not sender["session"].started: - if sender["session"].host == client: + if sender.isLobby() and sender.client.joined: + if not sender.client.room.started: + if sender.client.room.host == client: #destroy the session, sending messages inside the function - deleteSession(sender["session"]) + deleteRoom(sender.client.room) else: - sender["session"].leave(client) - sender["joined"] = False - message = f":>>KICK:{sender['session'].name}:{sender['username']}" - for client_socket in sender["session"].players: - client_socket.send(message.encode()) - updateStatus(sender["session"]) - updateSessions() + sender.client.room.leave(client) + sender.client.joined = False + message = f":>>KICK:{sender.client.room.name}:{sender.client.username}" + broadcast(sender.client.room.players, message.encode()) + updateStatus(sender.client.room) + updateRooms() + + if sender.isPipe(): + pass client.close() - sender["valid"] = False + client_sockets.pop(client) def send(client: socket, message: str): - sender = client_sockets[client] - if "valid" not in sender or sender["valid"]: + if client in client_sockets.keys(): + sender = client_sockets[client] client.send(message.encode(errors='replace')) def broadcast(clients: list, message: str): for c in clients: - send(c, message) + if client_sockets[c].isLobby() and client_sockets[c].client.auth: + send(c, message) -def sendSessions(client: socket): +def sendRooms(client: socket): msg2 = "" counter = 0 - for s in sessions.values(): + for s in rooms.values(): if not s.started: msg2 += f":{s.name}:{s.joined}:{s.total}:{s.protected}" counter += 1 @@ -176,158 +249,166 @@ def sendSessions(client: socket): send(client, msg) -def updateSessions(): +def updateRooms(): for s in client_sockets.keys(): - sendSessions(s) + sendRooms(s) -def deleteSession(session: Session): - msg = f":>>KICK:{session.name}" - for player in session.players: - client_sockets[player]["joined"] = False - msg2 = msg + f":{client_sockets[player]['username']}" +def deleteRoom(room: Room): + msg = f":>>KICK:{room.name}" + for player in room.players: + client_sockets[player].client.joined = False + msg2 = msg + f":{client_sockets[player].client.username}" send(player, msg2) - sessions.pop(session.name) + rooms.pop(room.name) -def updateStatus(session: Session): - msg = f":>>STATUS:{session.joined}" - for player in session.players: - msg += f":{client_sockets[player]['username']}:{client_sockets[player]['ready']}" - broadcast(session.players, msg) +def updateStatus(room: Room): + msg = f":>>STATUS:{room.joined}" + for player in room.players: + msg += f":{client_sockets[player].client.username}:{client_sockets[player].client.ready}" + broadcast(room.players, msg) -def startSession(session: Session): - session.started = True +def startRoom(room: Room): + room.started = True + session = Session() + session.name = room.name + sessions.append(session) session.host_uuid = str(uuid.uuid4()) - hostMessage = f":>>HOST:{session.host_uuid}:{session.joined - 1}" #one client will be connected locally + hostMessage = f":>>HOST:{session.host_uuid}:{room.joined - 1}" #one client will be connected locally #host message must be before start message - send(session.host, hostMessage) + send(room.host, hostMessage) - for player in session.players: - client_sockets[player]['uuid'] = str(uuid.uuid4()) - msg = f":>>START:{client_sockets[player]['uuid']}" + for player in room.players: + _uuid = str(uuid.uuid4()) + session.clients_uuid.append(_uuid) + msg = f":>>START:{_uuid}" send(player, msg) + #remove this connection + player.close + client_sockets.pop(player) + + #this room shall not exist anymore + rooms.pop(room.name) -def dispatch(client: socket, sender: dict, arr: bytes): +def dispatch(cs: socket, sender: Sender, arr: bytes): if arr == None or len(arr) == 0: return - print(f"[{sender['address']}] dispatching message") - #check for game mode connection msg = str(arr) if msg.find("Aiya!") != -1: - sender["pipe"] = True #switch to pipe mode + sender.client = ClientPipe() print(" vcmi recognized") - if sender["pipe"]: - if sender["game"]: #if already playing - sending raw bytes as is - sender["prevmessages"].append(arr) + if sender.isPipe(): + if sender.client.auth: #if already playing - sending raw bytes as is + sender.client.prevmessages.append(arr) print(" storing message") else: - sender["prevmessages"].append(struct.pack(' PROTOCOL_VERSION_MAX: print(f"[!] Error: client has incompatbile protocol version {arr[0]}") - send(client, ":>>ERROR:Cannot connect to remote server due to protocol incompatibility") + send(cs, ":>>ERROR:Cannot connect to remote server due to protocol incompatibility") return - # second byte is a encoding str size + # second byte is an encoding str size if arr[1] == 0: - sender["encoding"] = "utf8" + sender.client.encoding = "utf8" else: if len(arr) < arr[1] + 2: - send(client, ":>>ERROR:Protocol error") + send(cs, ":>>ERROR:Protocol error") return - - sender["encoding"] = arr[2:(arr[1] + 2)].decode(errors='ignore') + # read encoding string + sender.client.encoding = arr[2:(arr[1] + 2)].decode(errors='ignore') arr = arr[(arr[1] + 2):] msg = str(arr) - msg = arr.decode(encoding=sender["encoding"], errors='replace') + msg = arr.decode(encoding=sender.client.encoding, errors='replace') _open = msg.partition('<') _close = _open[2].partition('>') if _open[0] != '' or _open[1] == '' or _open[2] == '' or _close[0] == '' or _close[1] == '': - print(f"[!] Incorrect message from {sender['address']}: {msg}") + print(f"[!] Incorrect message from {sender.address}: {msg}") return _nextTag = _close[2].partition('<') @@ -336,127 +417,128 @@ def dispatch(client: socket, sender: dict, arr: bytes): #greetings to the server if tag == "GREETINGS": - if sender["auth"]: - print(f"[!] Greetings from authorized user {sender['username']} {sender['address']}") - send(client, ":>>ERROR:User already authorized") + if sender.client.auth: + print(f"[!] Greetings from authorized user {sender.client.username} {sender.address}") + send(cs, ":>>ERROR:User already authorized") return if len(tag_value) < 3: - send(client, f":>>ERROR:Too short username {tag_value}") + send(cs, f":>>ERROR:Too short username {tag_value}") return for user in client_sockets.values(): - if user['username'] == tag_value: - send(client, f":>>ERROR:Can't connect with the name {tag_value}. This login is already occpupied") + if user.isLobby() and user.client.username == tag_value: + send(cs, f":>>ERROR:Can't connect with the name {tag_value}. This login is already occpupied") return - print(f"[*] User {sender['address']} autorized as {tag_value}") - sender["username"] = tag_value - sender["auth"] = True - sender["joined"] = False - sendSessions(client) + print(f"[*] {sender.address} autorized as {tag_value}") + sender.client.username = tag_value + sender.client.auth = True + sendRooms(cs) #VCMI version received - if tag == "VER" and sender["auth"]: - print(f"[*] User {sender['username']} has version {tag_value}") + if tag == "VER" and sender.client.auth: + print(f"[*] User {sender.client.username} has version {tag_value}") + sender.client.vcmiversion = tag_value #message received - if tag == "MSG" and sender["auth"]: - message = f":>>MSG:{sender['username']}:{tag_value}" - if sender["joined"]: - broadcast(sender["session"].players, message) + if tag == "MSG" and sender.client.auth: + message = f":>>MSG:{sender.client.username}:{tag_value}" + if sender.client.joined: + broadcast(sender.client.room.players, message) #send message only to players in the room else: - broadcast(client_sockets.keys(), message) + targetClients = [i for i in client_sockets.keys() if not client_sockets[i].client.joined] + broadcast(targetClients, message) - #new session - if tag == "NEW" and sender["auth"] and not sender["joined"]: - if tag_value in sessions: + #new room + if tag == "NEW" and sender.client.auth and not sender.client.joined: + if tag_value in rooms: #refuse creating game message = f":>>ERROR:Cannot create session with name {tag_value}, session with this name already exists" - send(client, message) + send(cs, message) return - sessions[tag_value] = Session(client, tag_value) - sender["joined"] = True - sender["ready"] = False - sender["session"] = sessions[tag_value] + rooms[tag_value] = Room(cs, tag_value) + sender.client.joined = True + sender.client.ready = False + sender.client.room = rooms[tag_value] #set password for the session - if tag == "PSWD" and sender["auth"] and sender["joined"] and sender["session"].host == client: - sender["session"].password = tag_value - sender["session"].protected = tag_value != "" + if tag == "PSWD" and sender.client.auth and sender.client.joined and sender.client.room.host == cs: + sender.client.room.password = tag_value + sender.client.room.protected = bool(tag_value != "") - #set amount of players to the new session - if tag == "COUNT" and sender["auth"] and sender["joined"] and sender["session"].host == client: - if sender["session"].total != 1: + #set amount of players to the new room + if tag == "COUNT" and sender.client.auth and sender.client.joined and sender.client.room.host == cs: + if sender.client.room.total != 1: #refuse changing amount of players message = f":>>ERROR:Changing amount of players is not possible for existing session" - send(client, message) + send(cs, message) return - sender["session"].total = int(tag_value) - message = f":>>CREATED:{sender['session'].name}" - send(client, message) - #now session is ready to be broadcasted - message = f":>>JOIN:{sender['session'].name}:{sender['username']}" - send(client, message) - updateStatus(sender["session"]) - updateSessions() + sender.client.room.total = int(tag_value) + message = f":>>CREATED:{sender.client.room.name}" + send(cs, message) + #now room is ready to be broadcasted + message = f":>>JOIN:{sender.client.room.name}:{sender.client.username}" + send(cs, message) + updateStatus(sender.client.room) + updateRooms() #join session - if tag == "JOIN" and sender["auth"] and not sender["joined"]: - if tag_value not in sessions: - message = f":>>ERROR:Session with name {tag_value} doesn't exist" - send(client, message) + if tag == "JOIN" and sender.client.auth and not sender.client.joined: + if tag_value not in rooms: + message = f":>>ERROR:Room with name {tag_value} doesn't exist" + send(cs, message) return - if sessions[tag_value].joined >= sessions[tag_value].total: - message = f":>>ERROR:Session {tag_value} is full" - send(client, message) + if rooms[tag_value].joined >= rooms[tag_value].total: + message = f":>>ERROR:Room {tag_value} is full" + send(cs, message) return - if sessions[tag_value].started: + if rooms[tag_value].started: message = f":>>ERROR:Session {tag_value} is started" - send(client, message) + send(cs, message) return - sender["joined"] = True - sender["ready"] = False - sender["session"] = sessions[tag_value] + sender.client.joined = True + sender.client.ready = False + sender.client.room = rooms[tag_value] - if tag == "PSWD" and sender["auth"] and sender["joined"] and sender["session"].host != client: - if not sender["session"].protected or sender["session"].password == tag_value: - sender["session"].join(client) - message = f":>>JOIN:{sender['session'].name}:{sender['username']}" - broadcast(sender["session"].players, message) - updateStatus(sender["session"]) - updateSessions() + if tag == "PSWD" and sender.client.auth and sender.client.joined and sender.client.room.host != cs: + if not sender.client.room.protected or sender.client.room.password == tag_value: + sender.client.room.join(cs) + message = f":>>JOIN:{sender.client.room.name}:{sender.client.username}" + broadcast(sender.client.room.players, message) + updateStatus(sender.client.room) + updateRooms() else: - sender["joined"] = False + sender.client.joined = False message = f":>>ERROR:Incorrect password" - send(client, message) + send(cs, message) return #leaving session - if tag == "LEAVE" and sender["auth"] and sender["joined"] and sender["session"].name == tag_value: - if sender["session"].host == client: + if tag == "LEAVE" and sender.client.auth and sender.client.joined and sender.client.room.name == tag_value: + if sender.client.room.host == cs: #destroy the session, sending messages inside the function - deleteSession(sender["session"]) + deleteRoom(sender.client.room) else: - message = f":>>KICK:{sender['session'].name}:{sender['username']}" - broadcast(sender["session"].players, message) - sender["session"].leave(client) - sender["joined"] = False - updateStatus(sender["session"]) - updateSessions() + message = f":>>KICK:{sender.client.room.name}:{sender.client.username}" + broadcast(sender.client.room.players, message) + sender.client.room.leave(cs) + sender.client.joined = False + updateStatus(sender.client.room) + updateRooms() - if tag == "READY" and sender["auth"] and sender["joined"] and sender["session"].name == tag_value: - if sender["session"].joined > 0 and sender["session"].host == client: - startSession(sender["session"]) - updateSessions() + if tag == "READY" and sender.client.auth and sender.client.joined and sender.client.room.name == tag_value: + if sender.client.room.joined > 0 and sender.client.room.host == cs: + startRoom(sender.client.room) + updateRooms() - dispatch(client, sender, (_nextTag[1] + _nextTag[2]).encode()) + dispatch(cs, sender, (_nextTag[1] + _nextTag[2]).encode()) def listen_for_client(cs): @@ -467,17 +549,23 @@ def listen_for_client(cs): while True: try: # keep listening for a message from `cs` socket - if client_sockets[cs]["game"]: + if client_sockets[cs].isPipe() and client_sockets[cs].client.auth: msg = cs.recv(4096) else: - msg = recv_msg(cs) + msg = receive_packed(cs) + + if msg == None or msg == b'': + print(f"[!] Disconnecting client {cs}") + handleDisconnection(cs) + return + + dispatch(cs, client_sockets[cs], msg) + except Exception as e: # client no longer connected print(f"[!] Error: {e}") handleDisconnection(cs) return - - dispatch(cs, client_sockets[cs], msg) while True: @@ -485,7 +573,8 @@ while True: client_socket, client_address = s.accept() print(f"[+] {client_address} connected.") # add the new connected client to connected sockets - client_sockets[client_socket] = {"address": client_address, "auth": False, "username": "", "joined": False, "game": False, "pipe": False, "apptype": "", "prevmessages": []} + client_sockets[client_socket] = Sender() + client_sockets[client_socket].address = client_address # start a new thread that listens for each client's messages t = Thread(target=listen_for_client, args=(client_socket,)) # make the thread daemon so it ends whenever the main thread ends