From 27a25ef8531ad38443c4cd68ee2fc7269761d0fc Mon Sep 17 00:00:00 2001 From: payet_s Date: Sun, 2 Aug 2015 23:09:00 +0200 Subject: [PATCH] Cleaned: gamespy_qr_server.py --- gamespy_qr_server.py | 508 ++++++++++++++++++++++++++++--------------- 1 file changed, 331 insertions(+), 177 deletions(-) diff --git a/gamespy_qr_server.py b/gamespy_qr_server.py index 1c55952..aeb748b 100644 --- a/gamespy_qr_server.py +++ b/gamespy_qr_server.py @@ -18,8 +18,10 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - Server emulator for *.available.gs.nintendowifi.net and *.master.gs.nintendowifi.net - Query and Reporting: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview +Server emulator for *.available.gs.nintendowifi.net and + *.master.gs.nintendowifi.net +Query and Reporting: +http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview """ import logging @@ -43,17 +45,20 @@ logger_output_to_console = True logger_output_to_file = True logger_name = "GameSpyQRServer" logger_filename = "gamespy_qr_server.log" -logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file) +logger = utils.create_logger(logger_name, logger_filename, -1, + logger_output_to_console, logger_output_to_file) + class GameSpyServerDatabase(BaseManager): pass + class GameSpyQRServer(object): class Session(object): def __init__(self, address): self.session = "" self.challenge = "" - self.secretkey = "" # Parse gslist.cfg later + self.secretkey = "" # Parse gslist.cfg later self.sent_challenge = False self.heartbeat_data = None self.address = address @@ -64,47 +69,63 @@ class GameSpyQRServer(object): self.gamename = "" self.keepalive = -1 - def __init__(self): self.sessions = {} - # Generate a dictionary "secret_key_list" containing the secret game keys associated with their game IDs. - # The dictionary key will be the game's ID, and the value will be the secret key. + # Generate a dictionary "secret_key_list" containing the secret game + # keys associated with their game IDs. The dictionary key will be the + # game's ID, and the value will be the secret key. self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg") - #self.log(logging.DEBUG, address, session_id, "Generated list of secret game keys...") + # self.log(logging.DEBUG, address, session_id, + # "Generated list of secret game keys...") GameSpyServerDatabase.register("update_server_list") GameSpyServerDatabase.register("delete_server") - def log(self, level, address, session_id, message): + def log(self, level, address, session_id, msg, *args, **kwargs): + """TODO: Use logger format""" if address is None: - logger.log(level, "%s", message) + logger.log(level, msg, *args, **kwargs) else: if session_id is not None: - logger.log(level, "[%s:%d %08x] %s", address[0], address[1], session_id, message) + logger.log(level, "[%s:%d %08x] " + msg, + address[0], address[1], session_id, + *args, **kwargs) else: - logger.log(level, "[%s:%d] %s", address[0], address[1], message) + logger.log(level, "[%s:%d] " + msg, + address[0], address[1], + *args, **kwargs) def start(self): try: manager_address = ("127.0.0.1", 27500) manager_password = "" - self.server_manager = GameSpyServerDatabase(address = manager_address, authkey= manager_password) + self.server_manager = GameSpyServerDatabase( + address=manager_address, + authkey=manager_password + ) self.server_manager.connect() # Start QR server - address = ('0.0.0.0', 27900) # accessible to outside connections (use this if you don't know what you're doing) + # Accessible to outside connections (use this if you don't know + # what you're doing) + address = ('0.0.0.0', 27900) self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.bind(address) self.socket.setblocking(0) - logger.log(logging.INFO, "Server is now listening on %s:%s..." % (address[0], address[1])) + logger.log(logging.INFO, + "Server is now listening on %s:%s...", + address[0], address[1]) - # Dependencies! I don't really like this solution but it's easier than trying to manage it another way. + # Dependencies! I don't really like this solution but it's easier + # than trying to manage it another way. server_browser_server = GameSpyServerBrowserServer(self) - server_browser_server_thread = threading.Thread(target=server_browser_server.start) + server_browser_server_thread = threading.Thread( + target=server_browser_server.start + ) server_browser_server_thread.start() self.write_queue = Queue.Queue() @@ -120,124 +141,153 @@ class GameSpyQRServer(object): self.keepalive_check() except: - logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc()) + logger.log(logging.ERROR, + "Unknown exception: %s", + traceback.format_exc()) def write_queue_send(self, data, address): time.sleep(0.05) self.socket.sendto(data, address) def write_queue_worker(self): - while 1: + while True: data, address = self.write_queue.get() - threading.Thread(target=self.write_queue_send, args=(data, address)).start() + threading.Thread(target=self.write_queue_send, + args=(data, address)).start() self.write_queue.task_done() def update_server_list(self, session_id, k): - if "statechanged" in k and k['statechanged'] == "2": # Close server - self.server_manager.delete_server(k['gamename'] , session_id) + if "statechanged" in k and k['statechanged'] == "2": # Close server + self.server_manager.delete_server(k['gamename'], session_id) - if session_id in self.sessions: - del self.sessions[session_id] + if session_id in self.sessions: + del self.sessions[session_id] else: # dwc_mtype controls what kind of server query we're looking for. # dwc_mtype = 0 is used when looking for a matchmaking game. # dwc_mtype = 1 is unknown. - # dwc_mtype = 2 is used when hosting a friends only game (possibly other uses too). - # dwc_mtype = 3 is used when looking for a friends only game (possibly other uses too). + # dwc_mtype = 2 is used when hosting a friends only game + # (possibly other uses too). + # dwc_mtype = 3 is used when looking for a friends only game + # (possibly other uses too). - # Some memory could be saved by clearing out any unwanted fields from k before sending. - self.server_manager.update_server_list(k['gamename'], session_id, k, self.sessions[session_id].console)._getvalue() + # Some memory could be saved by clearing out any unwanted fields + # from k before sending. + self.server_manager.update_server_list( + k['gamename'], session_id, k, + self.sessions[session_id].console + )._getvalue() if session_id in self.sessions: self.sessions[session_id].gamename = k['gamename'] def handle_packet(self, socket, recv_data, address): - # Tetris DS overlay 10 @ 02144184 - Handle responses back to server - # Tetris DS overlay 10 @ 02144184 - Handle responses back to server - # - # After some more packet inspection, it seems the format goes something like this: - # - All server messages seem to always start with \xfe\xfd. - # - The first byte from the client (or third byte from the server) is a command. - # - Bytes 2 - 5 from the client is some kind of ID. This will have to be inspected later. I believe it's a - # session-like ID because the number changes between connections. Copying the client's ID might be enough. - # - # The above was as guessed. - # The code in Tetris DS (overlay 10) @ 0216E974 handles the network command creation. - # R1 contains the command to be sent to the server. - # R2 contains a pointer to some unknown integer that gets written after the command. - # - # - Commands - # Commands range from 0x00 to 0x09 (for client only at least?) (Tetris DS overlay 10 @ 0216DDCC) - # - # CLIENT: - # 0x01 - Response (Tetris DS overlay 10 @ 216DCA4) - # Sends back base64 of RC4 encrypted string that was gotten from the server's 0x01. - # - # 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30) - # Data sent: - # 1) Loop for each localip available on the system, write as localip%d\x00(local ip) - # 2) localport\x00(local port) - # 3) natneg (either 0 or 1) - # 4) ONLY IF STATE CHANGED: statechanged\x00(state) (Possible values: 0, 1, 2, 3) - # 5) gamename\x00(game name) - # 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicip\x00(public ip) - # 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicport\x00(public port) - # - # if statechanged != 2: - # Write various other data described here: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation - # - # 0x07 - Unknown, related to server's 0x06 (returns value sent from server) - # - # 0x08 - Keep alive? Sent after 0x03 - # - # 0x09 - Availability check - # - # SERVER: - # 0x01 - Unknown - # Data sent: - # 8 random ASCII characters (?) followed by the public IP and port of the client as a hex string - # - # 0x06 - Unknown - # First 4 bytes is some kind of id? I believe it's a unique identifier for the data being sent, - # seeing how the server can send the same IP information many times in a row. If the IP information has - # already been parsed then it doesn't waste time handling it. - # - # After that is a "SBCM" section which is 0x14 bytes in total. - # SBCM information gets parsed at 2141A0C in Tetris DS overlay 10. - # Seems to contain IP address information. - # - # The SBCM seems to contain a little information that must be parsed before. - # After the SBCM: - # \x03\x00\x00\x00 - Always the same? - # \x01 - Found player? - # \x04 - Unknown - # (2 bytes) - Unknown. Port? - # (4 bytes) - Player's IP - # (4 bytes) - Unknown. Some other IP? Remote server IP? - # \x00\x00\x00\x00 - Unknown but seems to get checked - # - # Another SBCM, after a player has been found and attempting to start a game: - # \x03\x00\x00\x00 - Always the same? - # \x05 - Connecting to player? - # \x00 - Unknown - # (2 bytes) - Unknown. Port? Same as before. - # (4 bytes) - Player's IP - # (4 bytes) - Unknown. Some other IP? Remote server IP? - # - # 0x0a - Response to 0x01 - # Gets sent after receiving 0x01 from the client. So, server 0x01 -> client 0x01 -> server 0x0a. - # Has no other data besides the client ID. - # - # - \xfd\xfc commands get passed directly between the other player(s)? - # - # - # Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr - # Use as reference. + """Tetris DS overlay 10 @ 02144184 - Handle responses back to server. + After some more packet inspection, it seems the format goes something + like this: + - All server messages seem to always start with \xfe\xfd. + - The first byte from the client (or third byte from the server) is + a command. + - Bytes 2 - 5 from the client is some kind of ID. This will have to + be inspected later. I believe it's a session-like ID because the + number changes between connections. Copying the client's ID might + be enough. + + The above was as guessed. + The code in Tetris DS (overlay 10) @ 0216E974 handles the network + command creation. + R1 contains the command to be sent to the server. + R2 contains a pointer to some unknown integer that gets written + after the command. + + - Commands + Commands range from 0x00 to 0x09 (for client only at least?) + (Tetris DS overlay 10 @ 0216DDCC) + + CLIENT: + 0x01 - Response (Tetris DS overlay 10 @ 216DCA4) + Sends back base64 of RC4 encrypted string that was + gotten from the server's 0x01. + + 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30) + Data sent: + 1) Loop for each localip available on the system, write + as localip%d\x00(local ip) + 2) localport\x00(local port) + 3) natneg (either 0 or 1) + 4) ONLY IF STATE CHANGED: statechanged\x00(state) + (Possible values: 0, 1, 2, 3) + 5) gamename\x00(game name) + 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: + publicip\x00(public ip) + 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: + publicport\x00(public port) + + if statechanged != 2: + Write various other data described here: + http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation + + 0x07 - Unknown, related to server's 0x06 + (returns value sent from server) + + 0x08 - Keep alive? Sent after 0x03 + + 0x09 - Availability check + + SERVER: + 0x01 - Unknown + Data sent: + 8 random ASCII characters (?) followed by the public IP and + port of the client as a hex string + + 0x06 - Unknown + First 4 bytes is some kind of id? I believe it's a unique + identifier for the data being sent, seeing how the server + can send the same IP information many times in a row. If + the IP information has already been parsed then it doesn't + waste time handling it. + + After that is a "SBCM" section which is 0x14 bytes in + total. SBCM information gets parsed at 2141A0C in Tetris DS + overlay 10. Seems to contain IP address information. + + The SBCM seems to contain a little information that must be + parsed before. + After the SBCM: + \x03\x00\x00\x00 - Always the same? + \x01 - Found player? + \x04 - Unknown + (2 bytes) - Unknown. Port? + (4 bytes) - Player's IP + (4 bytes) - Unknown. Some other IP? Remote server IP? + \x00\x00\x00\x00 - Unknown but seems to get checked + + Another SBCM, after a player has been found and attempting + to start a game: + \x03\x00\x00\x00 - Always the same? + \x05 - Connecting to player? + \x00 - Unknown + (2 bytes) - Unknown. Port? Same as before. + (4 bytes) - Player's IP + (4 bytes) - Unknown. Some other IP? Remote server IP? + + 0x0a - Response to 0x01 + Gets sent after receiving 0x01 from the client. + So, server 0x01 -> client 0x01 -> server 0x0a. + Has no other data besides the client ID. + + - \xfd\xfc commands get passed directly between the other player(s)? + + Open source version of GameSpy found here: + https://github.com/sfcspanky/Openspy-Core/tree/master/qr + Use as reference. + """ session_id = None if recv_data[0] != '\x09': - # Don't add a session if the client is trying to check if the game is available or not + # Don't add a session if the client is trying to check if the game + # is available or not session_id = struct.unpack("= timeout: pruned.append(session_id) - self.server_manager.delete_server(self.sessions[session_id].gamename, self.sessions[session_id].session) - self.log(logging.DEBUG, None, session_id, "Keep alive check removed %s:%s for game %s. Client hasn't responded in %d seconds." % (self.sessions[session_id].address[0], self.sessions[session_id].address[1], self.sessions[session_id].gamename, delta)) + self.server_manager.delete_server( + self.sessions[session_id].gamename, + self.sessions[session_id].session + ) + self.log(logging.DEBUG, None, session_id, + "Keep alive check removed %s:%s for game %s." + " Client hasn't responded in %d seconds.", + self.sessions[session_id].address[0], + self.sessions[session_id].address[1], + self.sessions[session_id].gamename, + delta) for session_id in pruned: del self.sessions[session_id] + if __name__ == "__main__": qr_server = GameSpyQRServer() qr_server.start()