proxyServer.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. import socket
  2. import re
  3. import uuid
  4. import struct
  5. import logging
  6. from threading import Thread
  7. PROTOCOL_VERSION_MIN = 1
  8. PROTOCOL_VERSION_MAX = 1
  9. # server's IP address
  10. SERVER_HOST = "0.0.0.0"
  11. SERVER_PORT = 5002 # port we want to use
  12. #logging
  13. logHandlerHighlevel = logging.FileHandler('proxyServer.log')
  14. logHandlerHighlevel.setLevel(logging.WARNING)
  15. logHandlerLowlevel = logging.FileHandler('proxyServer_debug.log')
  16. logHandlerLowlevel.setLevel(logging.DEBUG)
  17. logging.basicConfig(handlers=[logHandlerHighlevel, logHandlerLowlevel], level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S')
  18. def receive_packed(sock):
  19. # Read message length and unpack it into an integer
  20. raw_msglen = recvall(sock, 4)
  21. if not raw_msglen:
  22. return None
  23. msglen = struct.unpack('<I', raw_msglen)[0]
  24. # Read the message data
  25. return recvall(sock, msglen)
  26. def recvall(sock, n):
  27. # Helper function to recv n bytes or return None if EOF is hit
  28. data = bytearray()
  29. while len(data) < n:
  30. packet = sock.recv(n - len(data))
  31. if not packet:
  32. return None
  33. data.extend(packet)
  34. return data
  35. class GameConnection:
  36. server: socket # socket to vcmiserver
  37. client: socket # socket to vcmiclient
  38. serverInit = False # if vcmiserver already connected
  39. clientInit = False # if vcmiclient already connected
  40. def __init__(self) -> None:
  41. self.server = None
  42. self.client = None
  43. pass
  44. class Room:
  45. total = 1 # total amount of players
  46. joined = 0 # amount of players joined to the session
  47. password = "" # password to connect
  48. protected = False # if True, password is required to join to the session
  49. name: str # name of session
  50. host: socket # player socket who created the room
  51. players = [] # list of sockets of players, joined to the session
  52. started = False
  53. def __init__(self, host: socket, name: str) -> None:
  54. self.name = name
  55. self.host = host
  56. self.players = [host]
  57. self.joined += 1
  58. def isJoined(self, player: socket) -> bool:
  59. return player in self.players
  60. def join(self, player: socket):
  61. if not self.isJoined(player) and self.joined < self.total:
  62. self.players.append(player)
  63. self.joined += 1
  64. def leave(self, player: socket):
  65. if not self.isJoined(player) or player == self.host:
  66. return
  67. self.players.remove(player)
  68. self.joined -= 1
  69. class Session:
  70. name: str # name of session
  71. host_uuid: str # uuid of vcmiserver for hosting player
  72. clients_uuid: list # list od vcmiclients uuid
  73. players: list # list of sockets of players, joined to the session
  74. connections: list # list of GameConnections for vcmiclient/vcmiserver (game mode)
  75. def __init__(self) -> None:
  76. self.name = ""
  77. self.host_uuid = ""
  78. self.clients_uuid = []
  79. self.connections = []
  80. pass
  81. def addConnection(self, conn: socket, isServer: bool):
  82. #find uninitialized server connection
  83. for gc in self.connections:
  84. if isServer and not gc.serverInit:
  85. gc.server = conn
  86. gc.serverInit = True
  87. if not isServer and not gc.clientInit:
  88. gc.client = conn
  89. gc.clientInit = True
  90. #no existing connection - create the new one
  91. gc = GameConnection()
  92. if isServer:
  93. gc.server = conn
  94. gc.serverInit = True
  95. else:
  96. gc.client = conn
  97. gc.clientInit = True
  98. self.connections.append(gc)
  99. def removeConnection(self, conn: socket):
  100. newConnections = []
  101. for c in self.connections:
  102. if c.server == conn:
  103. c.server = None
  104. c.serverInit = False
  105. if c.client == conn:
  106. c.client = None
  107. c.clientInit = False
  108. if c.server != None or c.client != None:
  109. newConnections.append(c)
  110. self.connections = newConnections
  111. def validPipe(self, conn) -> bool:
  112. for gc in self.connections:
  113. if gc.server == conn or gc.client == conn:
  114. return gc.serverInit and gc.clientInit
  115. return False
  116. def getPipe(self, conn) -> socket:
  117. for gc in self.connections:
  118. if gc.server == conn:
  119. return gc.client
  120. if gc.client == conn:
  121. return gc.server
  122. class Client:
  123. auth: bool
  124. def __init__(self) -> None:
  125. self.auth = False
  126. class ClientLobby(Client):
  127. joined: bool
  128. username: str
  129. room: Room
  130. protocolVersion: int
  131. encoding: str
  132. ready: bool
  133. vcmiversion: str #TODO: check version compatibility
  134. def __init__(self) -> None:
  135. super().__init__()
  136. self.room = None
  137. self.joined = False
  138. self.username = ""
  139. self.protocolVersion = 0
  140. self.encoding = 'utf8'
  141. self.ready = False
  142. self.vcmiversion = ""
  143. class ClientPipe(Client):
  144. apptype: str #client/server
  145. prevmessages: list
  146. session: Session
  147. uuid: str
  148. def __init__(self) -> None:
  149. super().__init__()
  150. self.prevmessages = []
  151. self.session = None
  152. self.apptype = ""
  153. self.uuid = ""
  154. class Sender:
  155. address: str #full client address
  156. client: Client
  157. def __init__(self) -> None:
  158. self.client = None
  159. pass
  160. def isLobby(self) -> bool:
  161. return isinstance(self.client, ClientLobby)
  162. def isPipe(self) -> bool:
  163. return isinstance(self.client, ClientPipe)
  164. # create a TCP socket
  165. s = socket.socket()
  166. # make the port as reusable port
  167. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  168. # bind the socket to the address we specified
  169. s.bind((SERVER_HOST, SERVER_PORT))
  170. # listen for upcoming connections
  171. s.listen(10)
  172. logging.critical(f"[*] Listening as {SERVER_HOST}:{SERVER_PORT}")
  173. # active rooms
  174. rooms = {}
  175. # list of active sessions
  176. sessions = []
  177. # initialize list/set of all connected client's sockets
  178. client_sockets = {}
  179. def handleDisconnection(client: socket):
  180. logging.warning(f"[!] Disconnecting client {client}")
  181. if not client in client_sockets:
  182. return
  183. sender = client_sockets[client]
  184. if sender.isLobby() and sender.client.joined:
  185. if not sender.client.room.started:
  186. if sender.client.room.host == client:
  187. #destroy the session, sending messages inside the function
  188. deleteRoom(sender.client.room)
  189. else:
  190. sender.client.room.leave(client)
  191. sender.client.joined = False
  192. message = f":>>KICK:{sender.client.room.name}:{sender.client.username}"
  193. broadcast(sender.client.room.players, message.encode())
  194. updateStatus(sender.client.room)
  195. updateRooms()
  196. if sender.isPipe() and sender.client.auth:
  197. if sender.client.session in sessions:
  198. sender.client.session.removeConnection(client)
  199. if not len(sender.client.session.connections):
  200. logging.warning(f"[*] Destroying session {sender.client.session.name}")
  201. sessions.remove(sender.client.session)
  202. client.close()
  203. client_sockets.pop(client)
  204. def send(client: socket, message: str):
  205. if client in client_sockets.keys():
  206. sender = client_sockets[client]
  207. client.send(message.encode(errors='replace'))
  208. def broadcast(clients: list, message: str):
  209. for c in clients:
  210. if client_sockets[c].isLobby() and client_sockets[c].client.auth:
  211. send(c, message)
  212. def sendRooms(client: socket):
  213. msg2 = ""
  214. counter = 0
  215. for s in rooms.values():
  216. if not s.started:
  217. msg2 += f":{s.name}:{s.joined}:{s.total}:{s.protected}"
  218. counter += 1
  219. msg = f":>>SESSIONS:{counter}{msg2}"
  220. send(client, msg)
  221. def updateRooms():
  222. for s in client_sockets.keys():
  223. sendRooms(s)
  224. def deleteRoom(room: Room):
  225. msg = f":>>KICK:{room.name}"
  226. for player in room.players:
  227. client_sockets[player].client.joined = False
  228. msg2 = msg + f":{client_sockets[player].client.username}"
  229. send(player, msg2)
  230. logging.warning(f"[*] Destroying room {room.name}")
  231. rooms.pop(room.name)
  232. def updateStatus(room: Room):
  233. msg = f":>>STATUS:{room.joined}"
  234. for player in room.players:
  235. msg += f":{client_sockets[player].client.username}:{client_sockets[player].client.ready}"
  236. broadcast(room.players, msg)
  237. def startRoom(room: Room):
  238. room.started = True
  239. session = Session()
  240. session.name = room.name
  241. sessions.append(session)
  242. logging.warning(f"[*] Starting session {session.name}")
  243. session.host_uuid = str(uuid.uuid4())
  244. hostMessage = f":>>HOST:{session.host_uuid}:{room.joined - 1}" #one client will be connected locally
  245. #host message must be before start message
  246. send(room.host, hostMessage)
  247. for player in room.players:
  248. _uuid = str(uuid.uuid4())
  249. session.clients_uuid.append(_uuid)
  250. msg = f":>>START:{_uuid}"
  251. send(player, msg)
  252. #remove this connection
  253. player.close
  254. client_sockets.pop(player)
  255. #this room shall not exist anymore
  256. logging.info(f"[*] Destroying room {room.name}")
  257. rooms.pop(room.name)
  258. def dispatch(cs: socket, sender: Sender, arr: bytes):
  259. if arr == None or len(arr) == 0:
  260. return
  261. #check for game mode connection
  262. msg = str(arr)
  263. if msg.find("Aiya!") != -1:
  264. sender.client = ClientPipe()
  265. logging.debug(" vcmi recognized")
  266. if sender.isPipe():
  267. if sender.client.auth: #if already playing - sending raw bytes as is
  268. sender.client.prevmessages.append(arr)
  269. else:
  270. sender.client.prevmessages.append(struct.pack('<I', len(arr)) + arr) #pack message
  271. logging.debug(" packing message")
  272. #search fo application type in the message
  273. match = re.search(r"\((\w+)\)", msg)
  274. _appType = ''
  275. if match != None:
  276. _appType = match.group(1)
  277. sender.client.apptype = _appType
  278. #extract uuid from message
  279. _uuid = arr.decode()
  280. logging.debug(f" decoding {_uuid}")
  281. if not _uuid == '' and not sender.client.apptype == '':
  282. #search for uuid
  283. for session in sessions:
  284. #verify uuid of connected application
  285. if _uuid.find(session.host_uuid) != -1 and sender.client.apptype == "server":
  286. logging.debug(f" apptype {sender.client.apptype} uuid {_uuid}")
  287. session.addConnection(cs, True)
  288. sender.client.session = session
  289. sender.client.auth = True
  290. #read boolean flag for the endian
  291. # this is workaround to send only one remaining byte
  292. # WARNING: reversed byte order is not supported
  293. sender.client.prevmessages.append(cs.recv(1))
  294. logging.debug(f" binding server connection to session {session.name}")
  295. break
  296. if sender.client.apptype == "client":
  297. for p in session.clients_uuid:
  298. if _uuid.find(p) != -1:
  299. logging.debug(f" apptype {sender.client.apptype} uuid {_uuid}")
  300. #client connection
  301. session.addConnection(cs, False)
  302. sender.client.session = session
  303. sender.client.auth = True
  304. #read boolean flag for the endian
  305. # this is workaround to send only one remaining byte
  306. # WARNING: reversed byte order is not supported
  307. sender.client.prevmessages.append(cs.recv(1))
  308. logging.debug(f" binding client connection to session {session.name}")
  309. break
  310. #game mode
  311. if sender.isPipe() and sender.client.auth and sender.client.session.validPipe(cs):
  312. #send messages from queue
  313. opposite = sender.client.session.getPipe(cs)
  314. for x in client_sockets[opposite].client.prevmessages:
  315. cs.sendall(x)
  316. client_sockets[opposite].client.prevmessages.clear()
  317. for x in sender.client.prevmessages:
  318. opposite.sendall(x)
  319. sender.client.prevmessages.clear()
  320. return
  321. #we are in pipe mode but game still not started - waiting other clients to connect
  322. if sender.isPipe():
  323. logging.debug(f" waiting other clients")
  324. return
  325. #intialize lobby mode
  326. if not sender.isLobby():
  327. if len(arr) < 2:
  328. logging.critical("[!] Error: unknown client tries to connect")
  329. #TODO: block address? close the socket?
  330. return
  331. sender.client = ClientLobby()
  332. # first byte is protocol version
  333. sender.client.protocolVersion = arr[0]
  334. if arr[0] < PROTOCOL_VERSION_MIN or arr[0] > PROTOCOL_VERSION_MAX:
  335. logging.critical(f"[!] Error: client has incompatbile protocol version {arr[0]}")
  336. send(cs, ":>>ERROR:Cannot connect to remote server due to protocol incompatibility")
  337. return
  338. # second byte is an encoding str size
  339. if arr[1] == 0:
  340. sender.client.encoding = "utf8"
  341. else:
  342. if len(arr) < arr[1] + 2:
  343. send(cs, ":>>ERROR:Protocol error")
  344. return
  345. # read encoding string
  346. sender.client.encoding = arr[2:(arr[1] + 2)].decode(errors='ignore')
  347. arr = arr[(arr[1] + 2):]
  348. msg = str(arr)
  349. msg = arr.decode(encoding=sender.client.encoding, errors='replace')
  350. _open = msg.partition('<')
  351. _close = _open[2].partition('>')
  352. if _open[0] != '' or _open[1] == '' or _open[2] == '' or _close[0] == '' or _close[1] == '':
  353. logging.error(f"[!] Incorrect message from {sender.address}: {msg}")
  354. return
  355. _nextTag = _close[2].partition('<')
  356. tag = _close[0]
  357. tag_value = _nextTag[0]
  358. #greetings to the server
  359. if tag == "GREETINGS":
  360. if sender.client.auth:
  361. logging.error(f"[!] Greetings from authorized user {sender.client.username} {sender.address}")
  362. send(cs, ":>>ERROR:User already authorized")
  363. return
  364. if len(tag_value) < 3:
  365. send(cs, f":>>ERROR:Too short username {tag_value}")
  366. return
  367. for user in client_sockets.values():
  368. if user.isLobby() and user.client.username == tag_value:
  369. send(cs, f":>>ERROR:Can't connect with the name {tag_value}. This login is already occpupied")
  370. return
  371. logging.info(f"[*] {sender.address} autorized as {tag_value}")
  372. sender.client.username = tag_value
  373. sender.client.auth = True
  374. sendRooms(cs)
  375. #VCMI version received
  376. if tag == "VER" and sender.client.auth:
  377. logging.info(f"[*] User {sender.client.username} has version {tag_value}")
  378. sender.client.vcmiversion = tag_value
  379. #message received
  380. if tag == "MSG" and sender.client.auth:
  381. message = f":>>MSG:{sender.client.username}:{tag_value}"
  382. if sender.client.joined:
  383. broadcast(sender.client.room.players, message) #send message only to players in the room
  384. else:
  385. targetClients = [i for i in client_sockets.keys() if not client_sockets[i].client.joined]
  386. broadcast(targetClients, message)
  387. #new room
  388. if tag == "NEW" and sender.client.auth and not sender.client.joined:
  389. if tag_value in rooms:
  390. #refuse creating game
  391. message = f":>>ERROR:Cannot create session with name {tag_value}, session with this name already exists"
  392. send(cs, message)
  393. return
  394. if tag_value == "" or tag_value.startswith(" ") or len(tag_value) < 3:
  395. #refuse creating game
  396. message = f":>>ERROR:Cannot create session with invalid name {tag_value}"
  397. send(cs, message)
  398. return
  399. rooms[tag_value] = Room(cs, tag_value)
  400. sender.client.joined = True
  401. sender.client.ready = False
  402. sender.client.room = rooms[tag_value]
  403. #set password for the session
  404. if tag == "PSWD" and sender.client.auth and sender.client.joined and sender.client.room.host == cs:
  405. sender.client.room.password = tag_value
  406. sender.client.room.protected = bool(tag_value != "")
  407. #set amount of players to the new room
  408. if tag == "COUNT" and sender.client.auth and sender.client.joined and sender.client.room.host == cs:
  409. if sender.client.room.total != 1:
  410. #refuse changing amount of players
  411. message = f":>>ERROR:Changing amount of players is not possible for existing session"
  412. send(cs, message)
  413. return
  414. if int(tag_value) < 2 or int(tag_value) > 8:
  415. #refuse and cleanup room
  416. deleteRoom(sender.client.room)
  417. message = f":>>ERROR:Cannot create room with invalid amount of players"
  418. send(cs, message)
  419. return
  420. sender.client.room.total = int(tag_value)
  421. message = f":>>CREATED:{sender.client.room.name}"
  422. send(cs, message)
  423. #now room is ready to be broadcasted
  424. message = f":>>JOIN:{sender.client.room.name}:{sender.client.username}"
  425. send(cs, message)
  426. updateStatus(sender.client.room)
  427. updateRooms()
  428. #join session
  429. if tag == "JOIN" and sender.client.auth and not sender.client.joined:
  430. if tag_value not in rooms:
  431. message = f":>>ERROR:Room with name {tag_value} doesn't exist"
  432. send(cs, message)
  433. return
  434. if rooms[tag_value].joined >= rooms[tag_value].total:
  435. message = f":>>ERROR:Room {tag_value} is full"
  436. send(cs, message)
  437. return
  438. if rooms[tag_value].started:
  439. message = f":>>ERROR:Session {tag_value} is started"
  440. send(cs, message)
  441. return
  442. sender.client.joined = True
  443. sender.client.ready = False
  444. sender.client.room = rooms[tag_value]
  445. if tag == "PSWD" and sender.client.auth and sender.client.joined and sender.client.room.host != cs:
  446. if not sender.client.room.protected or sender.client.room.password == tag_value:
  447. sender.client.room.join(cs)
  448. message = f":>>JOIN:{sender.client.room.name}:{sender.client.username}"
  449. broadcast(sender.client.room.players, message)
  450. updateStatus(sender.client.room)
  451. updateRooms()
  452. else:
  453. sender.client.joined = False
  454. message = f":>>ERROR:Incorrect password"
  455. send(cs, message)
  456. return
  457. #leaving session
  458. if tag == "LEAVE" and sender.client.auth and sender.client.joined and sender.client.room.name == tag_value:
  459. if sender.client.room.host == cs:
  460. #destroy the session, sending messages inside the function
  461. deleteRoom(sender.client.room)
  462. else:
  463. message = f":>>KICK:{sender.client.room.name}:{sender.client.username}"
  464. broadcast(sender.client.room.players, message)
  465. sender.client.room.leave(cs)
  466. sender.client.joined = False
  467. updateStatus(sender.client.room)
  468. updateRooms()
  469. if tag == "READY" and sender.client.auth and sender.client.joined and sender.client.room.name == tag_value:
  470. if sender.client.room.joined > 0 and sender.client.room.host == cs:
  471. startRoom(sender.client.room)
  472. updateRooms()
  473. dispatch(cs, sender, (_nextTag[1] + _nextTag[2]).encode())
  474. def listen_for_client(cs):
  475. """
  476. This function keep listening for a message from `cs` socket
  477. Whenever a message is received, broadcast it to all other connected clients
  478. """
  479. while True:
  480. try:
  481. # keep listening for a message from `cs` socket
  482. if client_sockets[cs].isPipe() and client_sockets[cs].client.auth:
  483. msg = cs.recv(4096)
  484. else:
  485. msg = receive_packed(cs)
  486. if msg == None or msg == b'':
  487. handleDisconnection(cs)
  488. return
  489. dispatch(cs, client_sockets[cs], msg)
  490. except Exception as e:
  491. # client no longer connected
  492. logging.error(f"[!] Error: {e}")
  493. handleDisconnection(cs)
  494. return
  495. while True:
  496. # we keep listening for new connections all the time
  497. client_socket, client_address = s.accept()
  498. logging.warning(f"[+] {client_address} connected.")
  499. # add the new connected client to connected sockets
  500. client_sockets[client_socket] = Sender()
  501. client_sockets[client_socket].address = client_address
  502. # start a new thread that listens for each client's messages
  503. t = Thread(target=listen_for_client, args=(client_socket,))
  504. # make the thread daemon so it ends whenever the main thread ends
  505. t.daemon = True
  506. # start the thread
  507. t.start()
  508. # close client sockets
  509. for cs in client_sockets:
  510. cs.close()
  511. # close server socket
  512. s.close()