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()