|
@@ -0,0 +1,274 @@
|
|
|
+import socket
|
|
|
+from threading import Thread
|
|
|
+
|
|
|
+# server's IP address
|
|
|
+SERVER_HOST = "0.0.0.0"
|
|
|
+SERVER_PORT = 5002 # port we want to use
|
|
|
+
|
|
|
+# initialize list/set of all connected client's sockets
|
|
|
+client_sockets = dict()
|
|
|
+
|
|
|
+class Session:
|
|
|
+ total = 1
|
|
|
+ joined = 0
|
|
|
+ password = ""
|
|
|
+ protected = False
|
|
|
+ name: str
|
|
|
+ host: socket
|
|
|
+ players = []
|
|
|
+ started = False
|
|
|
+
|
|
|
+ def __init__(self, host: socket, name: str) -> None:
|
|
|
+ self.name = name
|
|
|
+ self.host = host
|
|
|
+ self.players = [host]
|
|
|
+ self.joined += 1
|
|
|
+
|
|
|
+ def isJoined(self, player: socket) -> bool:
|
|
|
+ return player in self.players
|
|
|
+
|
|
|
+ def join(self, player: socket):
|
|
|
+ if not self.isJoined(player) and self.joined < self.total:
|
|
|
+ self.players.append(player)
|
|
|
+ self.joined += 1
|
|
|
+
|
|
|
+ def leave(self, player: socket):
|
|
|
+ if not self.isJoined(player) or player == self.host:
|
|
|
+ return
|
|
|
+
|
|
|
+ self.players.remove(player)
|
|
|
+ self.joined -= 1
|
|
|
+
|
|
|
+
|
|
|
+# create a TCP socket
|
|
|
+s = socket.socket()
|
|
|
+# make the port as reusable port
|
|
|
+s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
+# bind the socket to the address we specified
|
|
|
+s.bind((SERVER_HOST, SERVER_PORT))
|
|
|
+# listen for upcoming connections
|
|
|
+s.listen(5)
|
|
|
+print(f"[*] Listening as {SERVER_HOST}:{SERVER_PORT}")
|
|
|
+
|
|
|
+# list of active sessions
|
|
|
+sessions = dict()
|
|
|
+
|
|
|
+
|
|
|
+def handleDisconnection(client: socket):
|
|
|
+ sender = client_sockets[client]
|
|
|
+ if sender["joined"]:
|
|
|
+ if sender["session"].host == client:
|
|
|
+ #destroy the session, sending messages inside the function
|
|
|
+ deleteSession(sender["session"])
|
|
|
+ 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()
|
|
|
+
|
|
|
+ client.close()
|
|
|
+ sender["valid"] = False
|
|
|
+
|
|
|
+
|
|
|
+def send(client: socket, message: str):
|
|
|
+ sender = client_sockets[client]
|
|
|
+ if "valid" not in sender or sender["valid"]:
|
|
|
+ client.send(message.encode())
|
|
|
+
|
|
|
+
|
|
|
+def broadcast(clients: list, message: str):
|
|
|
+ for c in clients:
|
|
|
+ send(c, message)
|
|
|
+
|
|
|
+
|
|
|
+def sendSessions(client: socket):
|
|
|
+ msg = f":>>SESSIONS:{len(sessions.keys())}"
|
|
|
+ for s in sessions.values():
|
|
|
+ msg += f":{s.name}:{s.joined}:{s.total}:{s.protected}"
|
|
|
+
|
|
|
+ send(client, msg)
|
|
|
+
|
|
|
+
|
|
|
+def updateSessions():
|
|
|
+ for s in client_sockets.keys():
|
|
|
+ sendSessions(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']}"
|
|
|
+ send(player, msg2)
|
|
|
+
|
|
|
+ sessions.pop(session.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 dispatch(client: socket, sender: dict, msg: str):
|
|
|
+ if msg == '':
|
|
|
+ return
|
|
|
+
|
|
|
+ _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}")
|
|
|
+ return
|
|
|
+
|
|
|
+ _nextTag = _close[2].partition('<')
|
|
|
+ tag = _close[0]
|
|
|
+ tag_value = _nextTag[0]
|
|
|
+
|
|
|
+ #greetings to the server
|
|
|
+ if tag == "GREETINGS":
|
|
|
+ if sender["auth"]:
|
|
|
+ print(f"[!] Greetings from authorized user {sender['username']} {sender['address']}")
|
|
|
+ return
|
|
|
+ print(f"[*] User {sender['address']} autorized as {tag_value}")
|
|
|
+ sender["username"] = tag_value
|
|
|
+ sender["auth"] = True
|
|
|
+ sender["joined"] = False
|
|
|
+ sendSessions(client)
|
|
|
+
|
|
|
+ #VCMI version received
|
|
|
+ if tag == "VER" and sender["auth"]:
|
|
|
+ print(f"[*] User {sender['username']} has version {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)
|
|
|
+ else:
|
|
|
+ broadcast(client_sockets.keys(), message)
|
|
|
+
|
|
|
+ #new session
|
|
|
+ if tag == "NEW" and sender["auth"] and not sender["joined"]:
|
|
|
+ if tag_value in sessions:
|
|
|
+ #refuse creating game
|
|
|
+ message = f":>>ERROR:Cannot create session with name {tag_value}, session with this name already exists"
|
|
|
+ send(client, message)
|
|
|
+ return
|
|
|
+
|
|
|
+ sessions[tag_value] = Session(client, tag_value)
|
|
|
+ sender["joined"] = True
|
|
|
+ sender["ready"] = False
|
|
|
+ sender["session"] = sessions[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 != ""
|
|
|
+
|
|
|
+ #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:
|
|
|
+ #refuse changing amount of players
|
|
|
+ message = f":>>ERROR:Changing amount of players is not possible for existing session"
|
|
|
+ send(client, message)
|
|
|
+ return
|
|
|
+
|
|
|
+ sender["session"].total = int(tag_value)
|
|
|
+ #now session is ready to be broadcasted
|
|
|
+ message = f":>>JOIN:{sender['session'].name}:{sender['username']}"
|
|
|
+ send(client, message)
|
|
|
+ updateStatus(sender["session"])
|
|
|
+ updateSessions()
|
|
|
+
|
|
|
+ #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)
|
|
|
+ return
|
|
|
+
|
|
|
+ if sessions[tag_value].joined >= sessions[tag_value].total:
|
|
|
+ message = f":>>ERROR:Session {tag_value} is full"
|
|
|
+ send(client, message)
|
|
|
+ return
|
|
|
+
|
|
|
+ if sessions[tag_value].started:
|
|
|
+ message = f":>>ERROR:Session {tag_value} is started"
|
|
|
+ send(client, message)
|
|
|
+ return
|
|
|
+
|
|
|
+ sender["joined"] = True
|
|
|
+ sender["ready"] = False
|
|
|
+ sender["session"] = sessions[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()
|
|
|
+
|
|
|
+ else:
|
|
|
+ sender["joined"] = False
|
|
|
+ message = f":>>ERROR:Incorrect password"
|
|
|
+ send(client, message)
|
|
|
+ return
|
|
|
+
|
|
|
+ #leaving session
|
|
|
+ if tag == "LEAVE" and sender["auth"] and sender["joined"] and sender["session"].name == tag_value:
|
|
|
+ if sender["session"].host == client:
|
|
|
+ #destroy the session, sending messages inside the function
|
|
|
+ deleteSession(sender["session"])
|
|
|
+ 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()
|
|
|
+
|
|
|
+
|
|
|
+ dispatch(client, sender, _nextTag[1] + _nextTag[2])
|
|
|
+
|
|
|
+
|
|
|
+def listen_for_client(cs):
|
|
|
+ """
|
|
|
+ This function keep listening for a message from `cs` socket
|
|
|
+ Whenever a message is received, broadcast it to all other connected clients
|
|
|
+ """
|
|
|
+ while True:
|
|
|
+ try:
|
|
|
+ # keep listening for a message from `cs` socket
|
|
|
+ msg = cs.recv(2048).decode()
|
|
|
+ except Exception as e:
|
|
|
+ # client no longer connected
|
|
|
+ print(f"[!] Error: {e}")
|
|
|
+ handleDisconnection(cs)
|
|
|
+ return
|
|
|
+
|
|
|
+ dispatch(cs, client_sockets[cs], msg)
|
|
|
+
|
|
|
+
|
|
|
+while True:
|
|
|
+ # we keep listening for new connections all the time
|
|
|
+ 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": ""}
|
|
|
+ # 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
|
|
|
+ t.daemon = True
|
|
|
+ # start the thread
|
|
|
+ t.start()
|
|
|
+
|
|
|
+ # close client sockets
|
|
|
+for cs in client_sockets:
|
|
|
+ cs.close()
|
|
|
+# close server socket
|
|
|
+s.close()
|