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.
This commit is contained in:
polaris
2014-04-10 16:53:54 -04:00
parent b334433752
commit 986627e645
13 changed files with 1319 additions and 1140 deletions

View File

@@ -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.

387
gamespy_backend_server.py Normal file
View File

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

109
gamespy_natneg_server.py Normal file
View File

@@ -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("<I", recv_data[8:12])[0]
session_id_raw = recv_data[8:12]
# Handle commands
if recv_data[7] == '\x00':
logger.log(logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1]))
gameid = utils.get_string(recv_data, 0x16)
client_id = "%02x" % ord(recv_data[13])
if gameid not in self.session_list:
self.session_list[gameid] = {}
if session_id not in self.session_list[gameid]:
self.session_list[gameid][session_id] = {}
if client_id not in self.session_list[gameid][session_id]:
self.session_list[gameid][session_id][client_id] = { 'connected': False, 'addr': '' }
self.session_list[gameid][session_id][client_id]['addr'] = addr
clients = len(self.session_list[gameid][session_id])
#if client_id in session_list[gameid][session_id]:
# clients -= 1
if clients > 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()

View File

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

View File

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

248
gamespy_qr_server.py Normal file
View File

@@ -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("<I", recv_data[1:5])[0]
session_id_raw = recv_data[1:5]
if session_id not in self.sessions:
# Found a new session, add to session list
self.sessions[session_id] = self.Session(address)
# Handle commands
if recv_data[0] == '\x00': # Query
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received query from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x01': # Challenge
self.log(logging.DEBUG, address, "Received challenge from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
# Prepare the challenge sent from the server to be compared
challenge = gs_utils.prepare_rc4_base64(self.sessions[session_id].secretkey, self.sessions[session_id].challenge)
# Compare challenge
client_challenge = recv_data[5:-1]
if client_challenge == challenge:
# Challenge succeeded
self.sessions[session_id].sent_challenge = True
# Handle successful challenge stuff here
packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command
packet.extend(session_id_raw) # Get the session ID
self.socket.sendto(packet, address)
self.log(logging.DEBUG, address, "Sent client registered to %s:%s..." % (address[0], address[1]))
elif recv_data[0] == '\x02': # Echo
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x03': # Heartbeat
data = recv_data[5:]
self.log(logging.DEBUG, address, "Received heartbeat from %s:%s... %s" % (address[0], address[1], data))
# Parse information from heartbeat here
d = data.rstrip('\0').split('\0')
# It may be safe to ignore "unknown" keys because the proper key names get filled in later...
k = {}
for i in range(0, len(d), 2):
self.log(logging.DEBUG, address, "%s = %s" % (d[i], d[i+1]))
k[d[i]] = d[i+1]
if "gamename" in k:
self.sessions[session_id].secretkey = self.secret_key_list[k['gamename']]
#print "Got secret key %s for %s" % (self.sessions[session_id].secretkey, k['gamename'])
if self.sessions[session_id].sent_challenge == False:
addr_hex = ''.join(["%02X" % int(x) for x in address[0].split('.')])
port_hex = "%04X" % int(address[1])
server_challenge = utils.generate_random_str(8) + addr_hex + port_hex
self.sessions[session_id].challenge = server_challenge
packet = bytearray([0xfe, 0xfd, 0x01]) # Send challenge command
packet.extend(session_id_raw) # Get the session ID
packet.extend(server_challenge)
packet.extend('\x00')
self.socket.sendto(packet, address)
self.log(logging.DEBUG, address, "Sent challenge to %s:%s..." % (address[0], address[1]))
if "statechanged" in k:
if k['statechanged'] == "1": # Create server
if k['publicport'] != "0" and k['publicip'] != "0" and k['maxplayers'] != "0":
# 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).
# 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)
elif k['statechanged'] == "2": # Close server
self.server_manager.delete_server(k['gamename'] , session_id)
#self.sessions.pop(session_id)
elif recv_data[0] == '\x04': # Add Error
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received add error from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x05': # Echo Response
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received echo response from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x06': # Client Message
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x07': # Client Message Ack
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received client message ack from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x08': # Keep Alive
self.log(logging.DEBUG, address, "Received keep alive from %s:%s..." % (address[0], address[1]))
elif recv_data[0] == '\x09': # Available
# Availability check only sent to *.available.gs.nintendowifi.net
self.log(logging.DEBUG, address, "Received availability request for '%s' from %s:%s..." % (recv_data[5: -1], address[0], address[1]))
self.socket.sendto(bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), address)
elif recv_data[0] == '\x0a': # Client Registered
# Only sent to client, never received?
self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
else:
self.log(logging.DEBUG, address, "Unknown request from %s:%s:" % (address[0], address[1]))
self.log(logging.DEBUG, address, utils.pretty_print_hex(recv_data))

View File

@@ -0,0 +1,310 @@
# I found an open source implemention of this exact server I'm trying to emulate here: https://github.com/sfcspanky/Openspy-Core/blob/master/serverbrowsing/
# Use as reference later.
import logging
import socket
import ctypes
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_utility as gs_utils
import other.utils as utils
from multiprocessing.managers import BaseManager
class ServerListFlags:
UNSOLICITED_UDP_FLAG = 1
PRIVATE_IP_FLAG = 2
CONNECT_NEGOTIATE_FLAG = 4
ICMP_IP_FLAG = 8
NONSTANDARD_PORT_FLAG = 16
NONSTANDARD_PRIVATE_PORT_FLAG = 32
HAS_KEYS_FLAG = 64
HAS_FULL_RULES_FLAG = 128
logger = utils.create_logger("GameSpyServerBrowserServer", "gamespy_server_browser_server.log", -1)
class GameSpyServerDatabase(BaseManager):
pass
GameSpyServerDatabase.register("get_server_list")
GameSpyServerDatabase.register("modify_server_list")
GameSpyServerDatabase.register("find_servers")
GameSpyServerDatabase.register("find_server_by_address")
class GameSpyServerBrowserServer(object):
def __init__(self):
pass
def start(self):
endpoint = serverFromString(reactor, "tcp:28910")
conn = endpoint.listen(SessionFactory())
try:
if reactor.running == False:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
class SessionFactory(Factory):
def __init__(self):
logger.log(logging.DEBUG, "Now listening for connections...")
self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")
def buildProtocol(self, address):
return Session(address, self.secret_key_list)
class Session(LineReceiver):
def __init__(self, address, secret_key_list):
self.setRawMode() # We're dealing with binary data so set to raw mode
self.address = address
self.forward_to_client = False
self.forward_client = ()
self.secret_key_list = secret_key_list # Don't waste time parsing every session, so just accept it from the parent
manager_address = ("127.0.0.1", 27500)
manager_password = ""
self.server_manager = GameSpyServerDatabase(address = manager_address, authkey= manager_password)
self.server_manager.connect()
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)
logger.log(logging.DEBUG, "Trying to send message to %s:%d..." % (self.forward_client[0], self.forward_client[1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data)))
# Get server based on ip/port
server = self.server_manager.find_server_by_address(ip, self.forward_client[1])._getvalue()
logger.log(logging.DEBUG, 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)
logger.log(logging.DEBUG, "Forwarded data to %s:%s..." % (self.forward_client[0], self.forward_client[1]))
return
if data[2] == '\x00': # Server list request
logger.log(logging.DEBUG, "Received server list request from %s:%s..." % (self.address.host, self.address.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)
logger.log(logging.DEBUG, "list version: %02x / encoding version: %02x / game version: %08x / query game: %s / game name: %s / challenge: %s / filter: %s / fields: %s / options: %08x / max servers: %d / source ip: %08x" % (list_version, encoding_version, game_version, query_game, game_name, challenge, filter, fields, options, max_servers, source_ip))
# Requesting ip and port of client, not server
if filter == "" or fields == "":
output = bytearray([int(x) for x in self.address.host.split('.')])
output += utils.get_bytes_from_short_be(self.address.port)
self.transport.write(bytes(output))
logger.log(logging.DEBUG, "Responding with own IP and port...")
logger.log(logging.DEBUG, utils.pretty_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)
logger.log(logging.DEBUG, "Received send message request from %s:%s to %s:%d..." % (self.address.host, self.address.port, dest_addr, dest_port))
logger.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data)))
self.forward_to_client = True
self.forward_client = dest
elif data[2] == '\x03': # Keep alive reply
logger.log(logging.DEBUG, "Received keep alive from %s:%s..." % (self.address.host, self.address.port))
else:
logger.log(logging.DEBUG, "Received unknown command (%02x) from %s:%s..." % (ord(data[2]), self.address.host, self.address.port))
logger.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data)))
logger.log(logging.DEBUG, utils.pretty_print_hex(data))
def get_game_id(self, data):
game_id = data[5: -1]
return game_id
def get_server_list(self, game, filter, fields, max_count):
results = self.server_manager.find_servers(game, filter, fields, max_count)
return results
def generate_server_list_data(self, address, fields, server_info):
output = bytearray()
# Write the address
output += bytearray([int(x) for x in address.host.split('.')])
# Write the port
output += utils.get_bytes_from_short_be(address.port)
#if len(server_info) > 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))

View File

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

51
master_server.py Normal file
View File

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

View File

@@ -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("<I", recv_data[8:12])[0]
session_id_raw = recv_data[8:12]
# Handle commands
if recv_data[7] == '\x00':
utils.print_log("Received initialization from %s:%s..." % (addr[0], addr[1]))
gameid = utils.get_string(recv_data, 0x16)
client_id = "%02x" % ord(recv_data[13])
if gameid not in session_list:
session_list[gameid] = {}
if session_id not in session_list[gameid]:
session_list[gameid][session_id] = {}
if client_id not in session_list[gameid][session_id]:
session_list[gameid][session_id][client_id] = { 'connected': False, 'addr': '' }
session_list[gameid][session_id][client_id]['addr'] = addr
clients = len(session_list[gameid][session_id])
#if client_id in session_list[gameid][session_id]:
# clients -= 1
if clients > 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]))

View File

@@ -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 ""
output += "%c" % data[i * cols + x]
output += "\n"
return output

View File

@@ -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("<I", recv_data[1:5])[0]
session_id_raw = recv_data[1:5]
if session_id not in session_list:
# Found a new session, add to session list
session_list[session_id] = Session(addr)
# Handle commands
if recv_data[0] == '\x00': # Query
utils.print_log("NOT IMPLEMENTED! Received query from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
elif recv_data[0] == '\x01': # Challenge
utils.print_log("Received challenge from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
# Prepare the challenge sent from the server to be compared
challenge = gs_utils.prepare_rc4_base64(session_list[session_id].secretkey, session_list[session_id].challenge)
# Compare challenge
client_challenge = recv_data[5:-1]
if client_challenge == challenge:
# Challenge succeeded
session_list[session_id].sent_challenge = True
# Handle successful challenge stuff here
packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command
packet.extend(session_id_raw) # Get the session ID
s.sendto(packet, addr)
utils.print_log("Sent client registered to %s:%s..." % (addr[0], addr[1]))
# TODO: Send buddy list to client on successful connection.
elif recv_data[0] == '\x02': # Echo
utils.print_log("NOT IMPLEMENTED! Received echo from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
elif recv_data[0] == '\x03': # Heartbeat
data = recv_data[5:]
utils.print_log("Received heartbeat from %s:%s... %s" % (addr[0], addr[1], data))
# Parse information from heartbeat here
d = data.rstrip('\0').split('\0')
# It may be safe to ignore "unknown" keys because the proper key names get filled in later...
k = {}
i = 0
while i < len(d):
print "%s = %s" % (d[i], d[i+1])
k[d[i]] = d[i+1]
i += 2
if "gamename" in k:
session_list[session_id].secretkey = secret_key_list[k['gamename']]
#print "Got secret key %s for %s" % (session_list[session_id].secretkey, k['gamename'])
if session_list[session_id].sent_challenge == False:
addr_hex = ''.join(["%02X" % int(x) for x in addr[0].split('.')])
port_hex = "%04X" % int(addr[1])
server_challenge = utils.generate_random_str(8) + addr_hex + port_hex
session_list[session_id].challenge = server_challenge
packet = bytearray([0xfe, 0xfd, 0x01]) # Send challenge command
packet.extend(session_id_raw) # Get the session ID
packet.extend(server_challenge)
packet.extend('\x00')
s.sendto(packet, addr)
utils.print_log("Sent challenge to %s:%s..." % (addr[0], addr[1]))
if "statechanged" in k:
if k['statechanged'] == "1": # Create server
if k['publicport'] != "0" and k['publicip'] != "0" and k['maxplayers'] != "0":
# 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).
# Some memory could be saved by clearing out any unwanted fields from k before sending.
server_manager.update_server_list(k['gamename'] , session_id, k)
elif k['statechanged'] == "2": # Close server
server_manager.delete_server(k['gamename'] , session_id)
#session_list.pop(session_id)
elif recv_data[0] == '\x04': # Add Error
utils.print_log("NOT IMPLEMENTED! Received add error from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
elif recv_data[0] == '\x05': # Echo Response
utils.print_log("NOT IMPLEMENTED! Received echo response from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
elif recv_data[0] == '\x06': # Client Message
utils.print_log("NOT IMPLEMENTED! Received echo from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
elif recv_data[0] == '\x07': # Client Message Ack
utils.print_log("NOT IMPLEMENTED! Received client message ack from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
elif recv_data[0] == '\x08': # Keep Alive
utils.print_log("Received keep alive from %s:%s..." % (addr[0], addr[1]))
elif recv_data[0] == '\x09': # Available
# Availability check only sent to *.available.gs.nintendowifi.net
utils.print_log("Received availability request for '%s' from %s:%s..." % (recv_data[5: -1], addr[0], addr[1]))
s.sendto(bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), addr)
elif recv_data[0] == '\x0a': # Client Registered
# Only sent to client, never received?
utils.print_log("NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (addr[0], addr[1], recv_data[5:]))
else:
utils.print_log(
"Unknown request from %s:%s: %s" % (addr[0], addr[1], [elem.encode("hex") for elem in recv_data]))

View File

@@ -1,300 +0,0 @@
# I found an open source implemention of this exact server I'm trying to emulate here: https://github.com/sfcspanky/Openspy-Core/blob/master/serverbrowsing/
# Use as reference later.
from twisted.internet.protocol import Factory
from twisted.internet.endpoints import serverFromString
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor
import socket
import ctypes
import time
from threading import Thread
import gamespy.gs_utility as gs_utils
import other.utils as utils
from multiprocessing.managers import BaseManager
def get_game_id(data):
game_id = data[5: -1]
return game_id
def get_server_list(game, filter, fields, max_count):
results = server_manager.find_servers(game, filter, fields, max_count)
return results
class ServerListFlags:
UNSOLICITED_UDP_FLAG = 1
PRIVATE_IP_FLAG = 2
CONNECT_NEGOTIATE_FLAG = 4
ICMP_IP_FLAG = 8
NONSTANDARD_PORT_FLAG = 16
NONSTANDARD_PRIVATE_PORT_FLAG = 32
HAS_KEYS_FLAG = 64
HAS_FULL_RULES_FLAG = 128
def generate_server_list_data(address, fields, server_info):
output = bytearray()
# Write the address
output += bytearray([int(x) for x in address.host.split('.')])
# Write the port
output += utils.get_bytes_from_short_be(address.port)
#if len(server_info) > 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()