From 986627e6451d9af2eb572f85e22664fa4c3c74ef Mon Sep 17 00:00:00 2001 From: polaris Date: Thu, 10 Apr 2014 16:53:54 -0400 Subject: [PATCH] Significantly changed the layout of the server. All parts of the server can now be run on their own or used as a class. master_server.py will start running all of the servers at once. --- gamespy/gs_database.py | 9 +- gamespy_backend_server.py | 387 ++++++++++++++++++ gamespy_natneg_server.py | 109 +++++ gamespy_player_search_server.py | 96 +++++ ...spy_server.py => gamespy_profile_server.py | 174 ++++---- gamespy_qr_server.py | 248 +++++++++++ gamespy_server_browser_server.py | 310 ++++++++++++++ gs_server_database.py | 374 ----------------- master_server.py | 51 +++ natneg_server.py | 111 ----- other/utils.py | 49 ++- qr_server.py | 241 ----------- server_browser.py | 300 -------------- 13 files changed, 1319 insertions(+), 1140 deletions(-) create mode 100644 gamespy_backend_server.py create mode 100644 gamespy_natneg_server.py create mode 100644 gamespy_player_search_server.py rename gamespy_server.py => gamespy_profile_server.py (77%) create mode 100644 gamespy_qr_server.py create mode 100644 gamespy_server_browser_server.py delete mode 100644 gs_server_database.py create mode 100644 master_server.py delete mode 100644 natneg_server.py delete mode 100644 qr_server.py delete mode 100644 server_browser.py diff --git a/gamespy/gs_database.py b/gamespy/gs_database.py index 57b65cb..d1016ea 100644 --- a/gamespy/gs_database.py +++ b/gamespy/gs_database.py @@ -142,7 +142,7 @@ class GamespyDatabase(object): return users - def update_profile(self, session_key, fields): + def update_profile(self, session_key, field): profileid = self.get_profileid_from_session_key(session_key) if profileid != -1: @@ -151,10 +151,9 @@ class GamespyDatabase(object): # TODO: Optimize this so it's done all in one update. # FIXME: Possible security issue due to embedding an unsanitized string directly into the statement. c = self.conn.cursor() - for field in fields: - print "UPDATE users SET %s = %s WHERE profileid = %s" % (field[0], field[1], profileid) - c.execute("UPDATE users SET %s = ? WHERE profileid = ?" % (field[0]), [field[1], profileid]) - self.conn.commit() + print "UPDATE users SET %s = %s WHERE profileid = %s" % (field[0], field[1], profileid) + c.execute("UPDATE users SET %s = ? WHERE profileid = ?" % (field[0]), [field[1], profileid]) + self.conn.commit() # Session functions # TODO: Cache session keys so we don't have to query the database every time we get a profile id. diff --git a/gamespy_backend_server.py b/gamespy_backend_server.py new file mode 100644 index 0000000..078c844 --- /dev/null +++ b/gamespy_backend_server.py @@ -0,0 +1,387 @@ +# Master server list server +# +# Basic idea: +# The server listing does not need to be persistent, and it must be easily searchable for any unknown parameters. +# So instead of using a SQL database, I've opted to create a server list database server which communicates between the +# server browser and the qr server. The server list database will be stored in dictionaries as to allow dynamic columns +# that can be easily searched. The main reason for this configuration is because it cannot be guaranteed what data +# a game's server will required. For example, in addition to the common fields such as publicip, numplayers, dwc_pid, etc, +# Lost Magic also uses fields such as LMname, LMsecN, LMrating, LMbtmode, and LMversion. +# +# It would be possible to create game-specific databases but this would be more of a hassle and less universal. It would +# also be possible pickle a dictionary containing all of the fields and store it in a SQL database instead, but that +# would require unpickling every server each time you want to match search queries which would cause overhead if there +# are a lot of running servers. One trade off here is that we'll be using more memory by storing each server as a +# dictionary in the memory instead of storing it in a SQL database. +# +# qr_server and server_browser both will act as clients to gs_server_database. +# qr_server will send an add and/or delete to add or remove servers from the server list. +# server_browser will send a request with the game name followed by optional search parameters to get a list of servers. + +import logging + +from multiprocessing.managers import BaseManager +from multiprocessing import freeze_support +import other.utils as utils + +class TokenType: + UNKNOWN = 0 + FIELD = 1 + STRING = 2 + NUMBER = 3 + TOKEN = 4 + +logger = utils.create_logger("GamespyBackendServer", "gamespy_backend_server.log", -1) + +class GameSpyServerDatabase(BaseManager): + pass + +class GameSpyBackendServer(object): + def __init__(self): + self.server_list = {} + + GameSpyServerDatabase.register("get_server_list", callable=lambda:self.server_list) + GameSpyServerDatabase.register("find_servers", callable=self.find_servers) + GameSpyServerDatabase.register("find_server_by_address", callable=self.find_server_by_address) + GameSpyServerDatabase.register("update_server_list", callable=self.update_server_list) + GameSpyServerDatabase.register("delete_server", callable=self.delete_server) + + def start(self): + address = ("127.0.0.1", 27500) + password = "" + + logger.log(logging.INFO, "Started server on %s:%d..." % (address[0], address[1])) + + manager = GameSpyServerDatabase(address = address, authkey = password) + server = manager.get_server() + server.serve_forever() + + def get_token(self, filters, i): + # Complex example from Dungeon Explorer: Warriors of Ancient Arts + # dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (MatchType='english') + # + # Even more complex example from Phantasy Star Zero: + # dwc_mver = 3 and dwc_pid != 4 and maxplayers = 3 and numplayers < 3 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (((20=auth)AND((1&mskdif)=mskdif)AND((14&mskstg)=mskstg))) + # + # Digging into a few DS games, and these hardcoded search queries seem to be consistent between them: + # %s = %d and %s != %u and maxplayers = %d and numplayers < %d and %s = %d and %s != %s + # %s and (%s) + # %s = %u + # + # It does not look like OR commands are (at least by default) supported, so for now they won't be implemented. + # + # Things that have been implemented: + # - and operator (assuming all commands are linked by ANDs) + # - integer comparisons + # - string literals comparisons + # - comparison between two fields + # - comparison operators (<, >, =, !=, <=, >=) + # - bitwise and operator (&) + # + # Things that won't be supported for now unless required: + # - or operator + # - grouping of operators, e.g.: (x or y) and (y or z) + start = i + special_chars = "_" + + token_type = TokenType.UNKNOWN + + # Skip whitespace + while i < len(filters) and filters[i].isspace(): + i += 1 + start += 1 + + if i < len(filters): + if filters[i] == "(" or filters[i] == ")": + i += 1 + token_type = TokenType.TOKEN + + elif filters[i] == "&": + i += 1 + token_type = TokenType.TOKEN + + elif filters[i] == "=": + i += 1 + token_type = TokenType.TOKEN + + elif filters[i] == ">" or filters[i] == "<": + i += 1 + token_type = TokenType.TOKEN + + if i + 1 < len(filters) and filters[i+1] == "=": + # >= or <= + i += 1 + + elif i + 1 < len(filters) and filters[i] == "!" and filters[i + 1] == "=": + i += 2 + token_type = TokenType.TOKEN + + elif filters[i] == "'": + # String literal + token_type = TokenType.STRING + + i += 1 # Skip quotation mark + while i < len(filters) and filters[i] != "'": + i += 1 + + if i < len(filters) and filters[i] == "'": + i += 1 # Skip quotation mark + + elif filters[i] == "\"": + # I don't know if it's in the spec or not, but I added "" string literals as well just in case. + token_type = TokenType.STRING + + i += 1 # Skip quotation mark + while i < len(filters) and filters[i] != "\"": + i += 1 + + if i < len(filters) and filters[i] == "\"": + i += 1 # Skip quotation mark + + elif filters[i].isalnum() or filters[i] in special_chars: + # Whole numbers or words + if filters[i].isdigit(): + token_type = TokenType.NUMBER + if filters[i].isalpha(): + token_type = TokenType.FIELD + + while i < len(filters) and (filters[i].isalnum() or filters[i] in special_chars) and filters[i] not in "!=>< ": + i += 1 + + if token_type == TokenType.STRING: + token = filters[start + 1:i - 1] + else: + token = filters[start:i] + + if token_type == TokenType.NUMBER: + token = int(token) + + return token, i, token_type + + def match(self, filters, i, search, case_insensitive=False): + start = i + found_match = False + + # Get the next token + token, i, _ = self.get_token(filters, i) + + if case_insensitive == True: + token = token.lower() + search = search.lower() + + # If the token isn't the same as what we're searching for, don't move forward. + if token != search: + i = start + else: + found_match = True + + return found_match, i + + def parse_filter(self, filters): + ops = [] + values = [] + i = 0 + found_match = True + filter_count = 0 + + # Continue while there's a connecting "and". + while found_match and i < len(filters): + found_match_bracket, i = self.match(filters, i, "(") + + if found_match_bracket: + a, b, filters, f = self.parse_filter(filters[i:]) + found_closing_match_bracket, i = self.match(filters, i, ")") + found_match = found_closing_match_bracket + a.reverse() + b.reverse() + ops = a + ops + values = b + values + filter_count += f + + else: + l, i, token_type = self.get_token(filters, i) + + if token_type == TokenType.FIELD and l.lower() == "and": + filter_count += 1 + continue + + elif l == "(" or l == ")": + continue + + if token_type == TokenType.TOKEN: + ops.append(l) + else: + values.append({'value': l, 'type': token_type}) + + return ops, values, filters[i:], filter_count + + def find_servers(self, gameid, filters, fields, max_count): + servers = [] + + if gameid in self.server_list: + ops_parsed, values_parsed, _, filter_count = self.parse_filter(filters) + + # In the case that there were no "AND" commands, check to make sure there was at least one other command + if len(ops_parsed) > 0: + filter_count += 1 + + if max_count <= 0: + max_count = 1 + + # Generate a list of servers that match the given criteria. + for server in self.server_list[gameid]: + ops = ops_parsed + values = values_parsed + + if len(servers) > max_count and max_count != -1: + break + + matched_filters = 0 + while len(ops) > 0: + op = ops.pop() + r = None + l = None + + if len(values) != 0: + r = values.pop() + if len(values) != 0: + l = values.pop() + + if r == None or l == None: + break + + lval = l['value'] + rval = r['value'] + + # If the left value is a field name and it's in the server variables, get its value. + if l['type'] == TokenType.FIELD and l['value'] in server: + lval = server[l['value']] + _, _, l['type'] = self.get_token(lval, 0) + + # If the value is a number then convert it to an integer for proper integer comparison. + if l['type'] == TokenType.NUMBER: + lval = int(lval) + + # If the right value is a field name and it's in the server variables, get its value. + if r['type'] == TokenType.FIELD and r['value'] in server: + rval = server[r['value']] + _, _, r['type'] = self.get_token(rval, 0) + + # If the value is a number then convert it to an integer for proper integer comparison. + if r['type'] == TokenType.NUMBER: + rval = int(rval) + + match = False + if op == "=" and lval == rval: + match = True + elif op == "!=" and lval != rval: + match = True + elif op == "<" and lval < rval: + match = True + elif op == ">" and lval > rval: + match = True + elif op == ">=" and lval >= rval: + match = True + elif op == "<=" and lval <= rval: + match = True + elif op == "&": + values.append({ 'value': int(lval & rval), 'type': TokenType.NUMBER}) + + if match == True: + #print "Matched: %s %s %s" % (l['value'], op, r['value']) + matched_filters += 1 + elif op != "&": # & doesn't need to be matched, so don't display a message for it + #print "Not matched: %s %s %s" % (l['value'], op, r['value']) + pass + + #print "Matched %d/%d" % (matched_filters, filter_count) + + # Add the server if everything was matched + if matched_filters == filter_count: + # Create a result with only the fields requested + result = {} + + if 'localip0' in server: + # localip1, localip2, ... are possible, but are they ever used? + # Small chance this might cause an issue later. + result['localip0'] = server['localip0'] + + if 'localport' in server: + result['localport'] = server['localport'] + + if 'localport' in server: + result['localport'] = server['localport'] + + if 'natneg' in server: + result['natneg'] = server['natneg'] + + if 'publicip' in server: + result['publicip'] = server['publicip'] + + if 'publicport' in server: + result['publicport'] = server['publicport'] + + if '__session__' in server: + result['__session__'] = server['__session__'] + + requested = {} + for field in fields: + if not field in result: + if field in server: + requested[field] = server[field] + else: + # Return a dummy value. What's the normal behavior of the real server in this case? + requested[field] = "" + + + result['requested'] = requested + servers.append(result) + + return servers + + def update_server_list(self, gameid, session, value): + # Make sure the user isn't hosting multiple servers or there isn't some left over server information that + # never got handled properly (game crashed, etc). + self.delete_server(gameid, session) + + # If the game doesn't exist already, create a new list. + if not gameid in self.server_list: + self.server_list[gameid] = [] + + # Add new server + value['__session__'] = session + + logger.log(logging.DEBUG, "Added %s to the server list for %s" % (value, gameid)) + self.server_list[gameid].append(value) + logger.log(logging.DEBUG, "%s servers: %d" % (gameid, len(self.server_list[gameid]))) + + def delete_server(self, gameid, session): + if not gameid in self.server_list: + # Nothing to do if no servers for that game even exist. + return + + # Remove all servers hosted by the given session id. + count = len(self.server_list[gameid]) + self.server_list[gameid] = [x for x in self.server_list[gameid] if x['__session__'] != session] + count -= len(self.server_list[gameid]) + logger.log(logging.DEBUG, "Deleted %d %s servers where session = %d" % (count, gameid, session)) + + def find_server_by_address(self, ip, port, gameid = None): + if gameid == None: + # Search all servers + for gameid in self.server_list: + for server in self.server_list[gameid]: + if server['publicip'] == ip and server['publicport'] == str(port): + return server + else: + for server in self.server_list[gameid]: + if server['publicip'] == ip and server['publicport'] == str(port): + return server + + return None + +if __name__ == '__main__': + freeze_support() + + backend_server = GameSpyBackendServer() + backend_server.start() \ No newline at end of file diff --git a/gamespy_natneg_server.py b/gamespy_natneg_server.py new file mode 100644 index 0000000..eaed07f --- /dev/null +++ b/gamespy_natneg_server.py @@ -0,0 +1,109 @@ +# 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 +import socket +import struct +import gamespy.gs_utility as gs_utils +import other.utils as utils + +logger = utils.create_logger("GameSpyNatNegServer", "gamespy_natneg_server.log", -1) +class GameSpyNatNegServer(object): + def __init__(self): + self.session_list = {} + self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg") + + def start(self): + # Start QR server + address = ('0.0.0.0', 27901) # accessible to outside connections (use this if you don't know what you're doing) + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.bind(address) + + logger.log(logging.DEBUG, "Server is now listening on %s:%s..." % (address[0], address[1])) + + while 1: + recv_data, addr = s.recvfrom(2048) + + logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1])) + + # Make sure it's a legal packet + if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): + continue + + session_id = struct.unpack(" 0: + # Someone else is waiting to connect, send message + for client in self.session_list[gameid][session_id]: + if self.session_list[gameid][session_id][client]['connected'] == True or client == client_id: + continue + + output = bytearray(recv_data[0:12]) + if client == client_id: + output += bytearray(recv_data[15:15+4+2]) # IP Address and port + else: + output += bytearray([int(x) for x in addr[0].split('.')]) + output += utils.get_bytes_from_short_be(addr[1]) + + output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00 + output[7] = 0x05 + s.sendto(output, (self.session_list[gameid][session_id][client]['addr'])) + + logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[gameid][session_id][client]['addr'][0], self.session_list[gameid][session_id][client]['addr'][1])) + logger.log(logging.DEBUG, utils.pretty_print_hex(output)) + logger.log(logging.DEBUG, "") + #session_list[gameid][session_id][client_id]['connected'] = True + + output = bytearray(recv_data[0:14]) + output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea ]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 + output[7] = 0x01 # Initialization response + s.sendto(output, addr) + + elif recv_data[7] == '\x06': # Was able to connect + client_id = "%02x" % ord(recv_data[13]) + logger.log(logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1])) + + if gameid not in self.session_list: + pass + if session_id not in self.session_list[gameid]: + pass + if client_id not in self.session_list[gameid][session_id]: + pass + + #session_list[gameid][session_id][client_id]['connected'] = True + + elif recv_data[7] == '\x0d': + client_id = "%02x" % ord(recv_data[13]) + logger.log(logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1])) + logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) + + output[7] = 0x0e # Report response + s.sendto(output, addr) + + else: # Was able to connect + logger.log(logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1])) + +if __name__ == "__main__": + natneg_server = GameSpyNatNegServer() + natneg_server.start() \ No newline at end of file diff --git a/gamespy_player_search_server.py b/gamespy_player_search_server.py new file mode 100644 index 0000000..29ac7d5 --- /dev/null +++ b/gamespy_player_search_server.py @@ -0,0 +1,96 @@ +import logging + +from twisted.internet.protocol import Factory +from twisted.internet.endpoints import serverFromString +from twisted.protocols.basic import LineReceiver +from twisted.internet import reactor +from twisted.internet.error import ReactorAlreadyRunning + +import gamespy.gs_database as gs_database +import gamespy.gs_query as gs_query +import other.utils as utils + +logger = utils.create_logger("GameSpyPlayerSearchServer", "gamespy_player_search_server.log", -1) +class GameSpyPlayerSearchServer(object): + def __init__(self): + pass + + def start(self): + endpoint_search = serverFromString(reactor, "tcp:29901") + conn_search = endpoint_search.listen(PlayerSearchFactory()) + + try: + if reactor.running == False: + reactor.run(installSignalHandlers=0) + except ReactorAlreadyRunning: + pass + + +class PlayerSearchFactory(Factory): + def __init__(self): + logger.log(logging.INFO, "Now listening for player search connections...") + + def buildProtocol(self, address): + return PlayerSearch(address) + + +class PlayerSearch(LineReceiver): + def __init__(self, address): + self.setRawMode() + self.db = gs_database.GamespyDatabase() + + self,address = address + self.leftover = "" + + def connectionMade(self): + pass + + def connectionLost(self, reason): + pass + + def rawDataReceived(self, data): + self.log(logging.DEBUG, "SEARCH RESPONSE: %s" % data) + + data = self.leftover + data + commands, self.leftover = gs_query.parse_gamespy_message(data) + + for data_parsed in commands: + print data_parsed + + if data_parsed['__cmd__'] == "otherslist": + self.perform_otherslist(data_parsed) + else: + self.log(logging.DEBUG, "Found unknown search command, don't know how to handle '%s'." % data_parsed['__cmd__']) + + def perform_otherslist(self, data_parsed): + # Reference: http://wiki.tockdom.com/wiki/MKWii_Network_Protocol/Server/gpsp.gs.nintendowifi.net + # Example from: filtered-mkw-log-2014-01-01-ct1310.eth + # \otherslist\\o\146376154\uniquenick\2m0isbjmvRMCJ2i5321j\o\192817284\uniquenick\1jhggtmghRMCJ2jrsh23\o\302594991\uniquenick\7dkjp51v5RMCJ2nr3vs9\o\368031897\uniquenick\1v7p3qmkpRMCJ1o8f56p\o\447214276\uniquenick\7dkt0p6gtRMCJ2ljh72h\o\449615791\uniquenick\4puvrm1g4RMCJ00ho3v1\o\460250854\uniquenick\4rik5l1u1RMCJ0tc3fii\o\456284963\uniquenick\1unitvi86RMCJ1b10u02\o\453830866\uniquenick\7de3q52dbRMCJ2877ss2\o\450197498\uniquenick\3qtutr1ikRMCJ38gem1n\o\444241868\uniquenick\67tp53bs9RMCJ1abs7ej\o\420030955\uniquenick\5blesqia3RMCJ322bbd6\o\394609454\uniquenick\0hddp7mq2RMCJ30uv7r7\o\369478991\uniquenick\59de9c2bhRMCJ0re0fii\o\362755626\uniquenick\5tte2lif7RMCJ0cscgtg\o\350951571\uniquenick\7aeummjlaRMCJ3li4ls2\o\350740680\uniquenick\484uiqhr4RMCJ18opoj0\o\349855648\uniquenick\5blesqia3RMCJ1c245dn\o\324078642\uniquenick\62go5gpt0RMCJ0v0uhc9\o\304111337\uniquenick\4lcg6ampvRMCJ1gjre51\o\301273266\uniquenick\1dhdpjhn8RMCJ2da6f9h\o\193178453\uniquenick\3pcgu0299RMCJ3nhu50f\o\187210028\uniquenick\3tau15a9lRMCJ2ar247h\o\461622261\uniquenick\59epddrnkRMCJ1t2ge7l\oldone\\final\ + + msg_d = [] + msg_d.append(('__cmd__', "otherslist")) + msg_d.append(('__cmd_val__', "")) + + if "numopids" in data_parsed and "opids" in data_parsed: + numopids = int(data_parsed['numopids']) + opids = data_parsed['opids'].split('|') + + if len(opids) != numopids: + print "Unexpected number of opids, got %d, expected %d." % (len(opids), numopids) + + # Return all uniquenicks despite any unexpected/missing opids + for opid in opids: + profile = self.db.get_profile_from_profileid(opid) + + if profile != None: + msg_d.append(('o', opid)) + msg_d.append(('uniquenick', profile['uniquenick'])) + + msg_d.append(('oldone', "")) + msg = gs_query.create_gamespy_message(msg_d) + + self.transport.write(bytes(msg)) + +if __name__ == "__main__": + gsps = GameSpyPlayerSearchServer() + gsps.start() \ No newline at end of file diff --git a/gamespy_server.py b/gamespy_profile_server.py similarity index 77% rename from gamespy_server.py rename to gamespy_profile_server.py index 74854be..721601b 100644 --- a/gamespy_server.py +++ b/gamespy_profile_server.py @@ -1,23 +1,67 @@ +import logging +import time + from twisted.internet.protocol import Factory from twisted.internet.endpoints import serverFromString from twisted.protocols.basic import LineReceiver from twisted.internet import reactor +from twisted.internet.error import ReactorAlreadyRunning import gamespy.gs_database as gs_database import gamespy.gs_query as gs_query import gamespy.gs_utility as gs_utils import other.utils as utils + +logger = utils.create_logger("GameSpyProfileServer", "gamespy_profile_server.log", -1) +class GameSpyProfileServer(object): + def __init__(self): + pass + + def start(self): + endpoint = serverFromString(reactor, "tcp:29900") + conn = endpoint.listen(PlayerFactory()) + + try: + if reactor.running == False: + reactor.run(installSignalHandlers=0) + except ReactorAlreadyRunning: + pass + +class PlayerFactory(Factory): + def __init__(self): + # Instead of storing the sessions in the database, it might make more sense to store them in the PlayerFactory. + logger.log(logging.INFO, "Now listening for connections...") + self.sessions = {} + + def buildProtocol(self, address): + return PlayerSession(self.sessions, address) + class PlayerSession(LineReceiver): - def __init__(self, sessions, addr): - self.sessions = sessions - self.leftover = "" + def __init__(self, sessions, address): self.setRawMode() # We're dealing with binary data so set to raw mode + self.db = gs_database.GamespyDatabase() + + self.sessions = sessions + self.address = address + self.remaining_message = "" # Stores any unparsable/incomplete commands until the next rawDataReceived + self.profileId = 0 - self.address = addr self.gameid = "" + def log(self, level, message): + if self.profileId == 0: + if self.gameid == "": + logger.log(level, "[%s:%d] %s", self.address.host, self.address.port,message) + else: + logger.log(level, "[%s:%d | %s] %s", self.address.host, self.address.port, self.gameid, message) + else: + if self.gameid == "": + logger.log(level, "[%s:%d | %d] %s", self.address.host, self.address.port, self.profileId, message) + else: + logger.log(level, "[%s:%d | %d | %s] %s", self.address.host, self.address.port, self.profileId, self.gameid, message) + def get_ip_as_int(self, address): ipaddress = 0 @@ -28,10 +72,15 @@ class PlayerSession(LineReceiver): return ipaddress def connectionMade(self): + self.log(logging.DEBUG, "Received connection from %s:%d" % (self.address.host, self.address.port)) + # Create new session id self.session = "" + + # Generate a random challenge string self.challenge = utils.generate_random_str(8) + # The first command sent to the client is always a login challenge containing the server challenge key. msg_d = [] msg_d.append(('__cmd__', "lc")) msg_d.append(('__cmd_val__', "1")) @@ -39,21 +88,27 @@ class PlayerSession(LineReceiver): msg_d.append(('id', "1")) msg = gs_query.create_gamespy_message(msg_d) - utils.print_log("SENDING: '%s'..." % msg) + self.log(logging.DEBUG, "SENDING: '%s'..." % msg) self.transport.write(bytes(msg)) def connectionLost(self, reason): + self.log(logging.DEBUG, "Client disconnected") + if self.session in self.sessions: del self.sessions[self.session] + self.log(logging.DEBUG, "Deleted session %d" % self.sessions) def rawDataReceived(self, data): - utils.print_log("RESPONSE: %s" % data) + self.log(logging.DEBUG, "RESPONSE: '%s'..." % data) - data = self.leftover + data - commands, self.leftover = gs_query.parse_gamespy_message(data) + # In the case where command string is too big to fit into one read, any parts that could not be successfully + # parsed are stored in the variable remaining_message. On the next rawDataReceived command, the remaining + # message and the data are combined to create a full command. + data = self.remaining_message + data + commands, self.remaining_message = gs_query.parse_gamespy_message(data) for data_parsed in commands: - print data_parsed + self.log(-1, data_parsed) if data_parsed['__cmd__'] == "login": self.perform_login(data_parsed) @@ -77,7 +132,7 @@ class PlayerSession(LineReceiver): self.perform_authadd(data_parsed) else: # Maybe write unknown commands to a separate file later so new data can be collected more easily? - utils.print_log("Found unknown command, don't know how to handle '%s'." % data_parsed['__cmd__']) + self.log(logging.DEBUG, "Found unknown command, don't know how to handle '%s'." % data_parsed['__cmd__']) def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken']) @@ -136,7 +191,7 @@ class PlayerSession(LineReceiver): # Verify the client's response valid_response = gs_utils.generate_response(self.challenge, authtoken_parsed['challenge'], data_parsed['challenge'], data_parsed['authtoken']) if data_parsed['response'] != valid_response: - utils.print_log("ERROR: Got invalid response. Got %s, expected %s" % (data_parsed['response'], valid_response)) + self.log(logging.DEBUG, "ERROR: Got invalid response. Got %s, expected %s" % (data_parsed['response'], valid_response)) proof = gs_utils.generate_proof(self.challenge, authtoken_parsed['challenge'], data_parsed['challenge'], data_parsed['authtoken']) @@ -178,7 +233,7 @@ class PlayerSession(LineReceiver): self.gameid = gsbrcd[0:4] self.profileid = profileid - utils.print_log("SENDING: %s" % msg) + self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) # Send any friend statuses when the user logs in. @@ -210,14 +265,14 @@ class PlayerSession(LineReceiver): if profile['lastname'] != "": msg_d.append(('lastname', profile['lastname'])) - + msg_d.append(('lon', profile['lon'])) msg_d.append(('lat', profile['lat'])) msg_d.append(('loc', profile['loc'])) msg_d.append(('id', data_parsed['id'])) msg = gs_query.create_gamespy_message(msg_d) - utils.print_log("SENDING: %s" % msg) + self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) @@ -239,11 +294,9 @@ class PlayerSession(LineReceiver): data_parsed.pop('sesskey') # Create a list of fields to be updated. - fields = [] for f in data_parsed: - fields.append((f, data_parsed[f])) + self.db.update_profile(sesskey, (f, data_parsed[f])) - self.db.update_profile(sesskey, fields) def perform_ka(self, data_parsed): # No op @@ -284,7 +337,7 @@ class PlayerSession(LineReceiver): msg_d.append(('msg', dest_msg)) msg = gs_query.create_gamespy_message(msg_d) - utils.print_log("SENDING TO %s:%s: %s" % (self.sessions[dest_profileid].address.host, self.sessions[dest_profileid].address.port, msg)) + self.log(logging.DEBUG, "SENDING TO %s:%s: %s" % (self.sessions[dest_profileid].address.host, self.sessions[dest_profileid].address.port, msg)) self.sessions[dest_profileid].transport.write(bytes(msg)) @@ -394,87 +447,8 @@ class PlayerSession(LineReceiver): self.transport.write(bytes(msg)) -class PlayerSearch(LineReceiver): - def __init__(self, sessions, addr): - self.sessions = sessions - self.setRawMode() - self.db = gs_database.GamespyDatabase() - self.leftover = "" - - def connectionMade(self): - pass - - def connectionLost(self, reason): - pass - - def rawDataReceived(self, data): - utils.print_log("SEARCH RESPONSE: %s" % data) - - data = self.leftover + data - commands, self.leftover = gs_query.parse_gamespy_message(data) - - for data_parsed in commands: - print data_parsed - - if data_parsed['__cmd__'] == "otherslist": - self.perform_otherslist(data_parsed) - else: - utils.print_log("Found unknown search command, don't know how to handle '%s'." % data_parsed['__cmd__']) - - def perform_otherslist(self, data_parsed): - # Reference: http://wiki.tockdom.com/wiki/MKWii_Network_Protocol/Server/gpsp.gs.nintendowifi.net - # Example from: filtered-mkw-log-2014-01-01-ct1310.eth - # \otherslist\\o\146376154\uniquenick\2m0isbjmvRMCJ2i5321j\o\192817284\uniquenick\1jhggtmghRMCJ2jrsh23\o\302594991\uniquenick\7dkjp51v5RMCJ2nr3vs9\o\368031897\uniquenick\1v7p3qmkpRMCJ1o8f56p\o\447214276\uniquenick\7dkt0p6gtRMCJ2ljh72h\o\449615791\uniquenick\4puvrm1g4RMCJ00ho3v1\o\460250854\uniquenick\4rik5l1u1RMCJ0tc3fii\o\456284963\uniquenick\1unitvi86RMCJ1b10u02\o\453830866\uniquenick\7de3q52dbRMCJ2877ss2\o\450197498\uniquenick\3qtutr1ikRMCJ38gem1n\o\444241868\uniquenick\67tp53bs9RMCJ1abs7ej\o\420030955\uniquenick\5blesqia3RMCJ322bbd6\o\394609454\uniquenick\0hddp7mq2RMCJ30uv7r7\o\369478991\uniquenick\59de9c2bhRMCJ0re0fii\o\362755626\uniquenick\5tte2lif7RMCJ0cscgtg\o\350951571\uniquenick\7aeummjlaRMCJ3li4ls2\o\350740680\uniquenick\484uiqhr4RMCJ18opoj0\o\349855648\uniquenick\5blesqia3RMCJ1c245dn\o\324078642\uniquenick\62go5gpt0RMCJ0v0uhc9\o\304111337\uniquenick\4lcg6ampvRMCJ1gjre51\o\301273266\uniquenick\1dhdpjhn8RMCJ2da6f9h\o\193178453\uniquenick\3pcgu0299RMCJ3nhu50f\o\187210028\uniquenick\3tau15a9lRMCJ2ar247h\o\461622261\uniquenick\59epddrnkRMCJ1t2ge7l\oldone\\final\ - - msg_d = [] - msg_d.append(('__cmd__', "otherslist")) - msg_d.append(('__cmd_val__', "")) - - if "numopids" in data_parsed and "opids" in data_parsed: - numopids = int(data_parsed['numopids']) - opids = data_parsed['opids'].split('|') - - if len(opids) != numopids: - print "Unexpected number of opids, got %d, expected %d." % (len(opids), numopids) - - # Return all uniquenicks despite any unexpected/missing opids - for opid in opids: - profile = self.db.get_profile_from_profileid(opid) - - if profile != None: - msg_d.append(('o', opid)) - msg_d.append(('uniquenick', profile['uniquenick'])) - - msg_d.append(('oldone', "")) - msg = gs_query.create_gamespy_message(msg_d) - - self.transport.write(bytes(msg)) -sessions = {} -class PlayerFactory(Factory): - def __init__(self): - # Instead of storing the sessions in the database, it might make more sense to store them in the PlayerFactory. - print "Now listening for connections..." - - def buildProtocol(self, addr): - return PlayerSession(sessions, addr) - - -class PlayerSearchFactory(Factory): - def __init__(self): - self.sessions = {} - print "Now listening for player search connections..." - - def buildProtocol(self, addr): - return PlayerSearch(sessions, addr) - - -endpoint = serverFromString(reactor, "tcp:29900") -conn = endpoint.listen(PlayerFactory()) - -endpoint_search = serverFromString(reactor, "tcp:29901") -conn_search = endpoint.listen(PlayerSearchFactory()) - -reactor.run() - +if __name__ == "__main__": + gsps = GameSpyProfileServer() + gsps.start() diff --git a/gamespy_qr_server.py b/gamespy_qr_server.py new file mode 100644 index 0000000..71f3571 --- /dev/null +++ b/gamespy_qr_server.py @@ -0,0 +1,248 @@ +# 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 +import socket +import struct +import gamespy.gs_utility as gs_utils +import other.utils as utils + +from multiprocessing.managers import BaseManager + +logger = utils.create_logger("GameSpyQRServer", "gamespy_qr_server.log", -1) + +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.sent_challenge = False + self.address = address + + 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. + self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg") + #self.log(logging.DEBUG, address, "Generated list of secret game keys...") + + GameSpyServerDatabase.register("update_server_list") + GameSpyServerDatabase.register("delete_server") + + def log(self, level, address, message): + logger.log(level, "[%s:%d] %s", address[0], address[1],message) + + def start(self): + manager_address = ("127.0.0.1", 27500) + 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) + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind(address) + + logger.log(logging.DEBUG, "Server is now listening on %s:%s..." % (address[0], address[1])) + + self.wait_loop() + + def wait_loop(self): + while 1: + recv_data, address = self.socket.recvfrom(2048) + + # 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. + + session_id = struct.unpack(" 0: + if True: + # Write number of fields that will be returned. + key_count = len(fields) + output += utils.get_bytes_from_short(key_count) + + if key_count != len(fields): + # For some reason we didn't get all of the expected data. + logger.log(logging.WARNING, "key_count[%d] != len(fields)[%d]" % (key_count, len(fields))) + logger.log(logging.WARNING, fields) + + flags_buffer = bytearray() + + # Write the fields + for field in fields: + output += bytearray(field) + '\0\0' + + # Start server loop here instead of including all of the fields and stuff again + flags = 0 + if len(server_info) != 0: + flags |= ServerListFlags.HAS_KEYS_FLAG + + if "natneg" in server_info: + flags |= ServerListFlags.CONNECT_NEGOTIATE_FLAG + + flags_buffer += utils.get_bytes_from_int(int(server_info['publicip'])) + + flags |= ServerListFlags.NONSTANDARD_PORT_FLAG + flags_buffer += utils.get_bytes_from_short_be(int(server_info['publicport'])) + + if "localip0" in server_info: + flags |= ServerListFlags.PRIVATE_IP_FLAG + flags_buffer += bytearray([int(x) for x in server_info['localip0'].split('.')]) + + if "localport" in server_info: + flags |= ServerListFlags.NONSTANDARD_PRIVATE_PORT_FLAG + flags_buffer += utils.get_bytes_from_short_be(int(server_info['localport'])) + + flags |= ServerListFlags.ICMP_IP_FLAG + flags_buffer += bytearray([int(x) for x in "0.0.0.0".split('.')]) + + output += bytearray([flags & 0xff]) + output += flags_buffer + + if (flags & ServerListFlags.HAS_KEYS_FLAG): + # Write data for associated fields + for field in fields: + output += '\xff' + bytearray(server_info['requested'][field]) + '\0' + + output += '\0' + output += utils.get_bytes_from_int(-1) + + return output + + def find_server(self, query_game, filter, fields, max_servers, game_name, challenge): + # Get dictionary from master server list server. + logger.log(logging.DEBUG, "Searching for server matching '%s' with the fields '%s'" % (filter, fields)) + + self.server_list = self.server_manager.find_servers(query_game, filter, fields, max_servers)._getvalue() + + logger.log(logging.DEBUG, "Found server(s):") + logger.log(logging.DEBUG, self.server_list) + + if self.server_list == []: + self.server_list.append({}) + + for _server in self.server_list: + server = _server + if len(server) > 0 and len(fields) > 0 and server['requested'] == {}: + # If the requested fields weren't found then don't return a server. + # This fixes a bug with Mario Kart DS. + #print "Requested was empty" + server = {} + + # Generate binary server list data + data = self.generate_server_list_data(self.address, fields, server) + logger.log(logging.DEBUG, utils.pretty_print_hex(data)) + + # Encrypt data + enc = gs_utils.EncTypeX() + data = enc.encrypt(self.secret_key_list[game_name], challenge, data) + + # Send to client + self.transport.write(bytes(data)) + logger.log(logging.DEBUG, "Sent server list message to %s:%s..." % (self.address.host, self.address.port)) diff --git a/gs_server_database.py b/gs_server_database.py deleted file mode 100644 index 64aa322..0000000 --- a/gs_server_database.py +++ /dev/null @@ -1,374 +0,0 @@ -# Master server list server -# -# Basic idea: -# The server listing does not need to be persistent, and it must be easily searchable for any unknown parameters. -# So instead of using a SQL database, I've opted to create a server list database server which communicates between the -# server browser and the qr server. The server list database will be stored in dictionaries as to allow dynamic columns -# that can be easily searched. The main reason for this configuration is because it cannot be guaranteed what data -# a game's server will required. For example, in addition to the common fields such as publicip, numplayers, dwc_pid, etc, -# Lost Magic also uses fields such as LMname, LMsecN, LMrating, LMbtmode, and LMversion. -# -# It would be possible to create game-specific databases but this would be more of a hassle and less universal. It would -# also be possible pickle a dictionary containing all of the fields and store it in a SQL database instead, but that -# would require unpickling every server each time you want to match search queries which would cause overhead if there -# are a lot of running servers. One trade off here is that we'll be using more memory by storing each server as a -# dictionary in the memory instead of storing it in a SQL database. -# -# qr_server and server_browser both will act as clients to gs_server_database. -# qr_server will send an add and/or delete to add or remove servers from the server list. -# server_browser will send a request with the game name followed by optional search parameters to get a list of servers. - -from multiprocessing.managers import BaseManager -from multiprocessing import freeze_support -import other.utils as utils - -server_list = {} - -class TokenType: - UNKNOWN = 0 - FIELD = 1 - STRING = 2 - NUMBER = 3 - TOKEN = 4 - -def get_token(filter, i): - # Complex example from Dungeon Explorer: Warriors of Ancient Arts - # dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (MatchType='english') - # - # Digging into a few DS games, and these hardcoded search queries seem to be consistent between them: - # %s = %d and %s != %u and maxplayers = %d and numplayers < %d and %s = %d and %s != %s - # %s and (%s) - # %s = %u - # - # It does not look like OR commands are (at least by default) supported, so for now they won't be implemented. - # - # Things that must be implemented: - # - and operator - # - integer comparisons - # - string literals comparisons - # - comparison between two fields - # - comparison operators (<, >, =, !=) - # - if not already available, maybe extend the comparison operators to include <= and >= just to be safe - # - # Things that won't be supported for now unless required: - # - or operator - # - grouping of operators, e.g.: (x or y) and (y or z) - - start = i - special_chars = "_" - - token_type = TokenType.UNKNOWN - - # Skip whitespace - while i < len(filter) and filter[i].isspace(): - i += 1 - start += 1 - - if i < len(filter): - if filter[i] == "(" or filter[i] == ")": - i += 1 - token_type = TokenType.TOKEN - - elif filter[i] == "&": - i += 1 - token_type = TokenType.TOKEN - - elif filter[i] == "=": - i += 1 - token_type = TokenType.TOKEN - - elif filter[i] == ">" or filter[i] == "<": - i += 1 - token_type = TokenType.TOKEN - - if i + 1 < len(filter) and filter[i+1] == "=": - # >= or <= - i += 1 - - elif i + 1 < len(filter) and filter[i] == "!" and filter[i + 1] == "=": - i += 2 - token_type = TokenType.TOKEN - - elif filter[i] == "'": - token_type = TokenType.STRING - - i += 1 # Skip quotation mark - while i < len(filter) and filter[i] != "'": - i += 1 - - if i < len(filter) and filter[i] == "'": - i += 1 # Skip quotation mark - - elif filter[i] == "\"": - # I don't know if it's in the spec or not, but I added "" string literals as well just in case. - token_type = TokenType.STRING - - i += 1 # Skip quotation mark - while i < len(filter) and filter[i] != "\"": - i += 1 - - if i < len(filter) and filter[i] == "\"": - i += 1 # Skip quotation mark - - elif filter[i].isalnum() or filter[i] in special_chars: - # Get whole numbers or words - if filter[i].isdigit(): - token_type = TokenType.NUMBER - if filter[i].isalpha(): - token_type = TokenType.FIELD - - while i < len(filter) and (filter[i].isalnum() or filter[i] in special_chars) and filter[i] not in "!=>< ": - i += 1 - - if token_type == TokenType.STRING: - token = filter[start + 1:i - 1] - else: - token = filter[start:i] - - if token_type == TokenType.NUMBER: - token = int(token) - - return token, i, token_type - -def match(filter, i, search, case_insensitive=False): - start = i - found_match = False - - # Get the next token - token, i, _ = get_token(filter, i) - - if case_insensitive == True: - token = token.lower() - search = search.lower() - - # If the token isn't the same as what we're searching for, don't move forward. - if token != search: - i = start - else: - found_match = True - - return found_match, i - -def parse_filter(filter): - ops = [] - values = [] - i = 0 - found_match = True - filter_count = 0 - - # Continue while there's a connecting "and". - while found_match and i < len(filter): - found_match_bracket, i = match(filter, i, "(") - - if found_match_bracket: - a, b, filter, f = parse_filter(filter[i:]) - found_closing_match_bracket, i = match(filter, i, ")") - found_match = found_closing_match_bracket - a.reverse() - b.reverse() - ops = a + ops - values = b + values - filter_count += f - else: - l, i, token_type = get_token(filter, i) - - if token_type == TokenType.FIELD and l.lower() == "and": - filter_count += 1 - continue - - elif l == "(" or l == ")": - continue - - if token_type == TokenType.TOKEN: - ops.append(l) - else: - values.append({'value': l, 'type': token_type}) - - return ops, values, filter[i:], filter_count - -def find_servers(gameid, filter, fields, max_count): - servers = [] - - if gameid in server_list: - ops_parsed, values_parsed, _, filter_count = parse_filter(filter) - - # In the case that there were no "AND" commands, check to make sure there was at least one other command - if len(ops_parsed) > 0: - filter_count += 1 - - if max_count <= 0: - max_count = 1 - - # Generate a list of servers that match the given criteria. - for server in server_list[gameid]: - ops = ops_parsed - values = values_parsed - - if len(servers) > max_count and max_count != -1: - break - - matched_filters = 0 - while len(ops) > 0: - op = ops.pop() - r = None - l = None - - if len(values) != 0: - r = values.pop() - if len(values) != 0: - l = values.pop() - - if r == None or l == None: - break - - lval = l['value'] - rval = r['value'] - - if l['type'] == TokenType.FIELD and l['value'] in server: - lval = server[l['value']] - _, _, l['type'] = get_token(lval, 0) - - if l['type'] == TokenType.NUMBER: - lval = int(lval) - - if r['type'] == TokenType.FIELD and r['value'] in server: - rval = server[r['value']] - _, _, r['type'] = get_token(rval, 0) - - if r['type'] == TokenType.NUMBER: - rval = int(rval) - - match = False - if op == "=" and lval == rval: - match = True - elif op == "!=" and lval != rval: - match = True - elif op == "<" and lval < rval: - match = True - elif op == ">" and lval > rval: - match = True - elif op == ">=" and lval >= rval: - match = True - elif op == "<=" and lval <= rval: - match = True - elif op == "&": - values.append({ 'value': int(lval & rval), 'type': TokenType.NUMBER}) - - if match == True: - #print "Matched: %s %s %s" % (l['value'], op, r['value']) - matched_filters += 1 - elif op != "&": # & doesn't need to be matched, so don't display a message for it - #print "Not matched: %s %s %s" % (l['value'], op, r['value']) - pass - - #print "Matched %d/%d" % (matched_filters, filter_count) - - # Add the server if everything was matched - if matched_filters == filter_count: - # Create a result with only the fields requested - result = {} - - if 'localip0' in server: - # localip1, localip2, ... are possible, but are they ever used? - # Small chance this might cause an issue later. - result['localip0'] = server['localip0'] - - if 'localport' in server: - result['localport'] = server['localport'] - - if 'localport' in server: - result['localport'] = server['localport'] - - if 'natneg' in server: - result['natneg'] = server['natneg'] - - if 'publicip' in server: - result['publicip'] = server['publicip'] - - if 'publicport' in server: - result['publicport'] = server['publicport'] - - if '__session__' in server: - result['__session__'] = server['__session__'] - - requested = {} - for field in fields: - if not field in result: - if field in server: - requested[field] = server[field] - else: - # Return a dummy value. What's the normal behavior of the real server in this case? - requested[field] = "" - - - result['requested'] = requested - servers.append(result) - - return servers - -def update_server_list(gameid, session, value): - # Make sure the user isn't hosting multiple servers or there isn't some left over server information that - # never got handled properly (game crashed, etc). - delete_server(gameid, session) - - # If the game doesn't exist already, create a new list. - if not gameid in server_list: - server_list[gameid] = [] - - # Add new server - value['__session__'] = session - - utils.print_log("Added %s to the server list for %s" % (value, gameid)) - server_list[gameid].append(value) - print "%s servers: %d" % (gameid, len(server_list[gameid])) - -def delete_server(gameid, session): - if not gameid in server_list: - # Nothing to do if no servers for that game even exist. - return - - # Remove all servers hosted by the given session id. - count = len(server_list[gameid]) - server_list[gameid] = [x for x in server_list[gameid] if x['__session__'] != session] - count -= len(server_list[gameid]) - print "Deleted %d %s servers where session = %d" % (count, gameid, session) - -def find_server_by_address(ip, port, gameid = None): - if gameid == None: - # Search all servers - for gameid in server_list: - for server in server_list[gameid]: - print server - if server['publicip'] == ip and server['publicport'] == str(port): - return server - else: - for server in server_list[gameid]: - print server - if server['publicip'] == ip and server['publicport'] == str(port): - return server - - return None - - -class GamespyServerDatabase(BaseManager): - pass - -def start_server(): - address = ("127.0.0.1", 27500) - password = "" - - GamespyServerDatabase.register("get_server_list", callable=lambda:server_list) - GamespyServerDatabase.register("find_servers", callable=find_servers) - GamespyServerDatabase.register("find_server_by_address", callable=find_server_by_address) - GamespyServerDatabase.register("update_server_list", callable=update_server_list) - GamespyServerDatabase.register("delete_server", callable=delete_server) - - print "Started server on %s:%d..." % (address[0], address[1]) - - manager = GamespyServerDatabase(address = address, authkey = password) - server = manager.get_server() - server.serve_forever() - -if __name__ == '__main__': - freeze_support() - start_server() diff --git a/master_server.py b/master_server.py new file mode 100644 index 0000000..5e77721 --- /dev/null +++ b/master_server.py @@ -0,0 +1,51 @@ +from gamespy_player_search_server import GameSpyPlayerSearchServer +from gamespy_profile_server import GameSpyProfileServer +from gamespy_backend_server import GameSpyBackendServer +from gamespy_natneg_server import GameSpyNatNegServer +from gamespy_qr_server import GameSpyQRServer +from gamespy_server_browser_server import GameSpyServerBrowserServer + +import threading + +def start_backend_server(): + backend_server = GameSpyBackendServer() + backend_server.start() + +def start_qr_server(): + qr_server = GameSpyQRServer() + qr_server.start() + +def start_profile_server(): + profile_server = GameSpyProfileServer() + profile_server.start() + +def start_player_search_server(): + player_search_server = GameSpyPlayerSearchServer() + player_search_server.start() + +def start_server_browser_server(): + server_browser_server = GameSpyServerBrowserServer() + server_browser_server.start() + +def start_natneg_server(): + natneg_server = GameSpyNatNegServer() + natneg_server.start() + +if __name__ == "__main__": + backend_server_thread = threading.Thread(target=start_backend_server) + backend_server_thread.start() + + qr_server_thread = threading.Thread(target=start_qr_server) + qr_server_thread.start() + + profile_server_thread = threading.Thread(target=start_profile_server) + profile_server_thread.start() + + player_search_server_thread = threading.Thread(target=start_player_search_server) + player_search_server_thread.start() + + server_browser_server_thread = threading.Thread(target=start_server_browser_server) + server_browser_server_thread.start() + + natneg_server_thread = threading.Thread(target=start_natneg_server) + natneg_server_thread.start() \ No newline at end of file diff --git a/natneg_server.py b/natneg_server.py deleted file mode 100644 index 4675dc5..0000000 --- a/natneg_server.py +++ /dev/null @@ -1,111 +0,0 @@ -# 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 socket -import struct -import gamespy.gs_utility as gs_utils -import other.utils as utils -from multiprocessing.managers import BaseManager - -session_list = {} - -secret_key_list = gs_utils.generate_secret_keys("gslist.cfg") - -# Start QR server -address = ('0.0.0.0', 27901) # accessible to outside connections (use this if you don't know what you're doing) - -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -s.bind(address) - -utils.print_log("Server is now listening on %s:%s..." % (address[0], address[1])) - -while 1: - recv_data, addr = s.recvfrom(2048) - - print "Connection from %s:%d..." % (addr[0], addr[1]) - - # Make sure it's a legal packet - if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): - continue - - session_id = struct.unpack(" 0: - # Someone else is waiting to connect, send message - for client in session_list[gameid][session_id]: - if session_list[gameid][session_id][client]['connected'] == True or client == client_id: - continue - - output = bytearray(recv_data[0:12]) - if client == client_id: - output += bytearray(recv_data[15:15+4+2]) # IP Address and port - else: - output += bytearray([int(x) for x in addr[0].split('.')]) - output += utils.get_bytes_from_short_be(addr[1]) - - output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00 - output[7] = 0x05 - s.sendto(output, (session_list[gameid][session_id][client]['addr'])) - - print "Sent connection request to %s:%d..." % (session_list[gameid][session_id][client]['addr'][0], session_list[gameid][session_id][client]['addr'][1]) - utils.print_hex(output) - print "" - #session_list[gameid][session_id][client_id]['connected'] = True - - output = bytearray(recv_data[0:14]) - output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea ]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 - output[7] = 0x01 # Initialization response - s.sendto(output, addr) - - elif recv_data[7] == '\x06': # Was able to connect - client_id = "%02x" % ord(recv_data[13]) - utils.print_log("Received connected command from %s:%s..." % (addr[0], addr[1])) - - if gameid not in session_list: - pass - if session_id not in session_list[gameid]: - pass - if client_id not in session_list[gameid][session_id]: - pass - - #session_list[gameid][session_id][client_id]['connected'] = True - - elif recv_data[7] == '\x0d': - client_id = "%02x" % ord(recv_data[13]) - utils.print_log("Received report command from %s:%s..." % (addr[0], addr[1])) - - utils.print_hex(bytearray(recv_data)) - - output[7] = 0x0e # Report response - s.sendto(output, addr) - - else: # Was able to connect - utils.print_log("Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1])) - - - - - - - - diff --git a/other/utils.py b/other/utils.py index ceb506b..e50e302 100644 --- a/other/utils.py +++ b/other/utils.py @@ -1,6 +1,7 @@ import random import time import sys +import logging def generate_random_str(len): return ''.join(random.choice("abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") for _ in range(len)) @@ -65,10 +66,6 @@ def base32_decode(str, reverse = False): return orig -# For server logging -def print_log(text): - print "[%s] %s" % (time.strftime("%c"), text) - print "" # Number routines # I'm not sure what the pythonic way to do this is, so I'm making the code explicit by giving myself functions to @@ -135,24 +132,58 @@ def get_bytes_from_int(num): def get_bytes_from_int_be(num): return get_bytes_from_num(num, 4, True) + +# For server logging +def create_logger(loggername, filename = None, level = logging.DEBUG): + logging.addLevelName(-1, "TRACE") + + format= "[%(asctime)s | " + loggername + "] %(message)s" + date_format = "%Y-%m-%d %H:%M:%S" + + #logging.basicConfig(format=format, datefmt=date_format) + logger = logging.getLogger(loggername) + logger.setLevel(level) + + # Only needed when logging.basicConfig isn't set. + console_logger = logging.StreamHandler() + console_logger.setFormatter(logging.Formatter(format, datefmt=date_format)) + logger.addHandler(console_logger) + + if filename != None: + file_logger = logging.FileHandler(filename) + file_logger.setFormatter(logging.Formatter(format, datefmt=date_format)) + logger.addHandler(file_logger) + + return logger + def print_hex(data, cols = 16): + print pretty_print_hex(data, cols) + +def pretty_print_hex(orig_data, cols = 16): + data = bytearray(orig_data) + output = "\n" + for i in range(len(data) / cols + 1): + output += "%08x | " % (i * 16) + c = 0 for x in range(cols): if (i * cols + x + 1) > len(data): break - print "%02x" % data[i * cols + x], + output += "%02x " % data[i * cols + x] c += 1 c = cols - c - sys.stdout.write(" " * (c * 3 + 1)) + output += " " * (c * 3 + 1) for x in range(cols): if (i * cols + x + 1) > len(data): break if data[i * cols + x] < 0x21 or data[i * cols + x] >= 0x7f: - sys.stdout.write(".") + output += "." else: - sys.stdout.write("%c" % data[i * cols + x]) - print "" \ No newline at end of file + output += "%c" % data[i * cols + x] + output += "\n" + + return output \ No newline at end of file diff --git a/qr_server.py b/qr_server.py deleted file mode 100644 index 4b4ff9a..0000000 --- a/qr_server.py +++ /dev/null @@ -1,241 +0,0 @@ -# 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 socket -import struct -import gamespy.gs_utility as gs_utils -import other.utils as utils -from multiprocessing.managers import BaseManager - -session_list = {} -class Session(object): - def __init__(self, address): - self.session = "" - self.challenge = "" - self.secretkey = "" # Parse gslist.cfg later - self.sent_challenge = False - self.address = addr - - -# 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. -secret_key_list = gs_utils.generate_secret_keys("gslist.cfg") -utils.print_log("Generated list of secret game keys...") - -# Initialize server list server connection -class GamespyServerDatabase(BaseManager): - pass - -GamespyServerDatabase.register("update_server_list") -GamespyServerDatabase.register("delete_server") - -manager_address = ("127.0.0.1", 27500) -manager_password = "" - -server_manager = GamespyServerDatabase(address = manager_address, authkey= manager_password) -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) - -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -s.bind(address) - -utils.print_log("Server is now listening on %s:%s..." % (address[0], address[1])) - -while 1: - recv_data, addr = s.recvfrom(2048) - - # 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. - - session_id = struct.unpack(" 0: - if True: - # Write number of fields that will be returned. - key_count = len(fields) - output += utils.get_bytes_from_short(key_count) - - if key_count != len(fields): - # For some reason we didn't get all of the expected data. - print "key_count[%d] != len(fields)[%d]" % (key_count, len(fields)) - print fields - - flags_buffer = bytearray() - - # Write the fields - for field in fields: - output += bytearray(field) + '\0\0' - - # Start server loop here instead of including all of the fields and stuff again - flags = 0 - if len(server_info) != 0: - flags |= ServerListFlags.HAS_KEYS_FLAG - - if "natneg" in server_info: - flags |= ServerListFlags.CONNECT_NEGOTIATE_FLAG - - flags_buffer += utils.get_bytes_from_int(int(server_info['publicip'])) - - flags |= ServerListFlags.NONSTANDARD_PORT_FLAG - flags_buffer += utils.get_bytes_from_short_be(int(server_info['publicport'])) - - if "localip0" in server_info: - flags |= ServerListFlags.PRIVATE_IP_FLAG - flags_buffer += bytearray([int(x) for x in server_info['localip0'].split('.')]) - - if "localport" in server_info: - flags |= ServerListFlags.NONSTANDARD_PRIVATE_PORT_FLAG - flags_buffer += utils.get_bytes_from_short_be(int(server_info['localport'])) - - flags |= ServerListFlags.ICMP_IP_FLAG - flags_buffer += bytearray([int(x) for x in "0.0.0.0".split('.')]) - - output += bytearray([flags & 0xff]) - output += flags_buffer - - if (flags & ServerListFlags.HAS_KEYS_FLAG): - # Write data for associated fields - for field in fields: - output += '\xff' + bytearray(server_info['requested'][field]) + '\0' - - output += '\0' - output += utils.get_bytes_from_int(-1) - - return output - -class Session(LineReceiver): - def __init__(self, addr): - self.setRawMode() # We're dealing with binary data so set to raw mode - self.addr = addr - self.forward_to_client = False - self.forward_client = () - - def rawDataReceived(self, data): - # First 2 bytes are the packet size. - # - # Third byte is the command byte. - # According to Openspy-Core: - # 0x00 - Server list request - # 0x01 - Server info request - # 0x02 - Send message request - # 0x03 - Keep alive reply - # 0x04 - Map loop request (?) - # 0x05 - Player search request - # - # For Tetris DS, at the very least 0x00 and 0x02 need to be implemented. - - if self.forward_to_client: - self.forward_to_client = False - - # Find session id of server - # Iterate through the list of servers sent to the client and match by IP and port. - # Is there a better way to determine this information? - ip = str(ctypes.c_int32(utils.get_int(bytearray([int(x) for x in self.forward_client[0].split('.')]), 0)).value) - print "Trying to send message to %s:%d..." % (self.forward_client[0], self.forward_client[1]) - utils.print_hex(bytearray(data)) - - # Get server based on ip/port - server = server_manager.find_server_by_address(ip, self.forward_client[1])._getvalue() - print server - - if server == None: - pass - - #print "%s %s" % (ip, server['publicip']) - if server['publicip'] == ip and server['publicport'] == str(self.forward_client[1]): - # Send command to server to get it to connect to natneg - natneg_session = int(utils.generate_random_hex_str(8), 16) # Quick and lazy way to get a random 32bit integer. Replace with something else late.r - - output = bytearray([0xfe, 0xfd, 0x06]) - output += utils.get_bytes_from_int(server['__session__']) - output += bytearray(utils.get_bytes_from_int(natneg_session)) - output += bytearray(data) - - client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - client_s.sendto(output, self.forward_client) - utils.print_log("Forwarded data to %s:%s..." % (self.forward_client[0], self.forward_client[1])) - return - - if data[2] == '\x00': # Server list request - utils.print_log("Received server list request from %s:%s..." % (self.addr.host, self.addr.port)) - - # This code is so... not python. The C programmer in me is coming out strong. - # TODO: Rewrite this section later? - idx = 3 - list_version = ord(data[idx]) - idx += 1 - encoding_version = ord(data[idx]) - idx += 1 - game_version = utils.get_int(data, idx) - idx += 4 - - query_game = utils.get_string(data, idx) - idx += len(query_game) + 1 - game_name = utils.get_string(data, idx) - idx += len(game_name) + 1 - - challenge = data[idx:idx+8] - idx += 8 - - filter = utils.get_string(data, idx) - idx += len(filter) + 1 - fields = utils.get_string(data, idx) - idx += len(fields) + 1 - - options = utils.get_int_be(data, idx) - idx += 4 - - source_ip = 0 - max_servers = 0 - - ALTERNATE_SOURCE_IP = 0x08 - LIMIT_RESULT_COUNT = 0x80 - if (options & LIMIT_RESULT_COUNT): - max_servers = utils.get_int(data, idx) - elif (options & ALTERNATE_SOURCE_IP): - source_ip = utils.get_int(data, idx) - - if '\\' in fields: - fields = [x for x in fields.split('\\') if x and not x.isspace()] - - #print "%02x %02x %08x" % (list_version, encoding_version, game_version) - #print "%s" % query_game - print "%s" % game_name - #print "%s" % challenge - print "%s" % filter - print "%s" % fields - - #print "%08x" % options - #print "%d %08x" % (max_servers, source_ip) - - # Requesting ip and port of client, not server - if filter == "" or fields == "": - output = bytearray([int(x) for x in self.addr.host.split('.')]) - output += utils.get_bytes_from_short_be(self.addr.port) - self.transport.write(bytes(output)) - print "Responding with own IP and port..." - utils.print_hex(output) - else: - self.find_server(query_game, filter, fields, max_servers, game_name, challenge) - - - - elif data[2] == '\x02': # Send message request - dest_addr = '.'.join(["%d" % ord(x) for x in data[3:7]]) - dest_port = utils.get_short_be(data, 7) # What's the pythonic way to do this? unpack? - dest = (dest_addr, dest_port) - - utils.print_log("Received send message request from %s:%s to %s:%d..." % (self.addr.host, self.addr.port, dest_addr, dest_port)) - utils.print_hex(bytearray(data)) - - self.forward_to_client = True - self.forward_client = dest - - elif data[2] == '\x03': # Keep alive reply - utils.print_log("Received keep alive from %s:%s..." % (self.addr.host, self.addr.port)) - - else: - utils.print_log("Received unknown command (%02x) from %s:%s..." % (ord(data[2]), self.addr.host, self.addr.port)) - utils.print_hex(bytearray(data)) - utils.print_hex(data) - - def find_server(self, query_game, filter, fields, max_servers, game_name, challenge): - # Get dictionary from master server list server. - print "Searching for server matching '%s' with the fields '%s'" % (filter, fields) - - self.server_list = get_server_list(query_game, filter, fields, max_servers)._getvalue() - - print "Found server(s):" - print self.server_list - - if self.server_list == []: - self.server_list.append({}) - - for _server in self.server_list: - server = _server - if len(server) > 0 and len(fields) > 0 and server['requested'] == {}: - # If the requested fields weren't found then don't return a server. - # This fixes a bug with Mario Kart DS. - print "Requested was empty" - server = {} - - # Generate binary server list data - data = generate_server_list_data(self.addr, fields, server) - utils.print_hex(data) - - # Encrypt data - enc = gs_utils.EncTypeX() - data = enc.encrypt(secret_key_list[game_name], challenge, data) - - # Send to client - self.transport.write(bytes(data)) - utils.print_log("Sent server list message to %s:%s..." % (self.addr.host, self.addr.port)) - - - -class SessionFactory(Factory): - def __init__(self): - print "Now listening for connections..." - - def buildProtocol(self, addr): - return Session(addr) - - - - - -# Initialize server list server connection -class GamespyServerDatabase(BaseManager): - pass - -GamespyServerDatabase.register("get_server_list") -GamespyServerDatabase.register("modify_server_list") -GamespyServerDatabase.register("find_servers") -GamespyServerDatabase.register("find_server_by_address") - -manager_address = ("127.0.0.1", 27500) -manager_password = "" - -server_manager = GamespyServerDatabase(address = manager_address, authkey= manager_password) -server_manager.connect() - -secret_key_list = gs_utils.generate_secret_keys("gslist.cfg") - -endpoint = serverFromString(reactor, "tcp:28910") -conn = endpoint.listen(SessionFactory()) -reactor.run() \ No newline at end of file