From 35680370d130cfb3520c1f6d1a6b4bf95a865b09 Mon Sep 17 00:00:00 2001 From: polaris Date: Sat, 5 Apr 2014 06:07:18 -0400 Subject: [PATCH] Server browser, natneg Finally have friend games working. Matchmaking games do not work yet, though. There are still a number of bugs to be worked out, but it's possible to complete a few rounds of Tetris DS successfully. --- gamespy/gs_server_database.py | 302 ++++++++++++++++++ gamespy/gs_utility.py | 265 ++++++++++++++- gamespy_server.py | 32 +- natneg_server.py | 95 ++++++ other/utils.py | 56 +++- qr_server.py | 85 +++-- server_browser.py | 220 ++++++++++--- .../public_html/index.html | 1 + .../public_html}/index.html | 0 .../public_html/index.html | 0 .../public_html/tetrisds/store.asp | 0 .../public_html/ac} | 6 +- 12 files changed, 989 insertions(+), 73 deletions(-) create mode 100644 gamespy/gs_server_database.py create mode 100644 natneg_server.py create mode 100644 www/available.nintendowifi.net/public_html/index.html rename www/{conntest => conntest.nintendowifi.net/public_html}/index.html (100%) create mode 100644 www/gamestats.gs.nintendowifi.net/public_html/index.html create mode 100644 www/gamestats.gs.nintendowifi.net/public_html/tetrisds/store.asp rename www/{nas/ac.php => nas.nintendowifi.net/public_html/ac} (94%) diff --git a/gamespy/gs_server_database.py b/gamespy/gs_server_database.py new file mode 100644 index 0000000..6625efd --- /dev/null +++ b/gamespy/gs_server_database.py @@ -0,0 +1,302 @@ +# 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 + +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] == ">" 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] + + return token, i, token_type + +def match(filter, i, search): + start = i + found_match = False + + # Get the next token + token, i, _ = get_token(filter, i) + + # 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): + filters = [] + + i = 0 + found_match = True + # Continue while there's a connecting "and". + while found_match and i < len(filter): + found_match_bracket, i = match(filter, i, "(") + + left, i, _ = get_token(filter, i) + if i >= len(filter): + break + + op, i, _ = get_token(filter, i) + if i >= len(filter): + break + + right, i, token_type = get_token(filter, i) + + if found_match_bracket: + found_closing_match_bracket, i = match(filter, i, ")") + + found_match, i = match(filter, i, "and") + + filters.append({'op': op, 'left': left, 'right': right, 'type': token_type}) + + return filters + +def find_servers(gameid, filter, fields, max_count): + servers = [] + if gameid in server_list: + filters = parse_filter(filter) + + if max_count <= 0: + max_count = 1 + + # Generate a list of servers that match the given criteria. + for server in server_list[gameid]: + if len(servers) > max_count and max_count != -1: + break + + matched_filters = 0 + + for key in filters: + if key['left'] in server: + # Found key, perform actual comparison + right = key['right'] + type = key['type'] + + # Only assume that a field will ever reference another field once, so nested references are not supported. + if type == TokenType.FIELD and key['right'] in server: + right = server[key['right']] + + # Reuse the get_token function to update the new token type of the field in the database. + # Strings will return as fields but the distinction does not matter for the comparisons. + _, _, type = get_token(right, 0) + + if key['op'] == "=" and server[key['left']] == right: + matched_filters += 1 + elif key['op'] == "!=" and server[key['left']] != right: + matched_filters += 1 + elif type == TokenType.NUMBER: + # Only perform greater than/less than comparisons on integer types. + if key['op'] == ">" and server[key['left']] > right: + matched_filters += 1 + elif key['op'] == ">=" and server[key['left']] >= right: + matched_filters += 1 + elif key['op'] == "<" and server[key['left']] < right: + matched_filters += 1 + elif key['op'] == "<=" and server[key['left']] <= right: + matched_filters += 1 + else: + break + + if matched_filters == len(filters): + # 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 + print "Added %s to the server list for %s" % (gameid, value) + server_list[gameid].append(value) + +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. + server_list[gameid] = [x for x in server_list[gameid] if x['__session__'] != session] + + +class GamespyServerDatabase(BaseManager): + pass + +def start_server(): + address = ("127.0.0.1", 27500) + password = "" + + #server_list["tetrisds"] = [] + #server_list["tetrisds"].append({'__session__': 0, 'key': "helloworld", 'value': "Hello, world!", 'extra': "Test"}) + + GamespyServerDatabase.register("get_server_list", callable=lambda:server_list) + GamespyServerDatabase.register("find_servers", callable=find_servers) + 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() \ No newline at end of file diff --git a/gamespy/gs_utility.py b/gamespy/gs_utility.py index 29cb4cb..ca361c5 100644 --- a/gamespy/gs_utility.py +++ b/gamespy/gs_utility.py @@ -1,7 +1,22 @@ import base64 import hashlib +import time import other.utils as utils + +def generate_secret_keys(filename="gslist.cfg"): + key_file = open(filename) + + secret_key_list = {} + for line in key_file.readlines(): + #name = line[:54].strip() # Probably won't do anything with the name for now. + id = line[54:54+19].strip() + key = line[54+19:].strip() + + secret_key_list[id] = key + + return secret_key_list + # GameSpy uses a slightly modified version of base64 which replaces +/= with []_ def base64_encode(input): output = base64.b64encode(input).replace('+', '[').replace('/', ']').replace('=', '_') @@ -119,4 +134,252 @@ def get_friendcode_from_profileid(profileid, gameid): def get_profileid_from_friendcode(friendcode): # Get the lower 32 bits as the profile id profileid = friendcode & 0xffffffff - return profileid \ No newline at end of file + return profileid + +# Code from Luigi Auriemma's enctypex_decoder.c +# It's kind of sloppy in parts, but it works. Unless there's some issues then it'll probably not change any longer. +class EncTypeX: + def __init__(self): + return + + def decrypt(self, key, validate, data): + if not key or not validate or not data: + return None + + encxkey = bytearray([0] * 261) + data = self.init(encxkey, key, validate, data) + self.func6(encxkey, data, len(data)) + + return data + + def encrypt(self, key, validate, data): + if not key or not validate or not data: + return None + + # Convert data from strings to byte arrays before use or else it'll raise an error + key = bytearray(key) + validate = bytearray(validate) + + # Add room for the header + tmp_len = 20 + data = bytearray(tmp_len) + data + + keylen = len(key) + vallen = len(validate) + rnd = ~int(time.time()) + + for i in range(tmp_len): + rnd = (rnd * 0x343FD) + 0x269EC3 + data[i] = (rnd ^ key[i % keylen] ^ validate[i % vallen]) & 0xff + + header_len = 7 + data[0] = (header_len - 2) ^ 0xec + data[1] = 0x00 + data[2] = 0x00 + data[header_len - 1] = (tmp_len - header_len) ^ 0xea + + header = data[:tmp_len] # The header of the data gets chopped off in init(), so save it + encxkey = bytearray([0] * 261) + data = self.init(encxkey, key, validate, data) + self.func6e(encxkey, data, len(data)) + + # Reappend header that we saved earlier before returning to make the complete buffer + return header + data + + + def init(self, encxkey, key, validate, data): + data_len = len(data) + + if data_len < 1: + return None + + header_len = (data[0] ^ 0xec) + 2 + if data_len < header_len: + return None + + data_start = (data[header_len - 1] ^ 0xea) + if data_len < (header_len + data_start): + return None + + data = self.enctypex_funcx(encxkey, bytearray(key), bytearray(validate), data[header_len:], data_start) + return data[data_start:] + + + def enctypex_funcx(self, encxkey, key, validate, data, datalen): + keylen = len(key) + + for i in range(datalen): + validate[(key[i % keylen] * i) & 7] ^= validate[i & 7] ^ data[i] + + self.func4(encxkey, validate, 8) + return data + + def func4(self, encxkey, id, idlen): + if idlen < 1: + return + + for i in range(256): + encxkey[i] = i + + n1 = 0 + n2 = 0 + for i in range(255,-1,-1): + t1, n1, n2 = self.func5(encxkey, i, id, idlen, n1, n2) + t2 = encxkey[i] + encxkey[i] = encxkey[t1] + encxkey[t1] = t2 + + encxkey[256] = encxkey[1] + encxkey[257] = encxkey[3] + encxkey[258] = encxkey[5] + encxkey[259] = encxkey[7] + encxkey[260] = encxkey[n1 & 0xff] + + def func5(self, encxkey, cnt, id, idlen, n1, n2): + if cnt == 0: + return 0, n1, n2 + + mask = 1 + doLoop = True + if cnt > 1: + while doLoop: + mask = (mask << 1) + 1 + doLoop = mask < cnt + + i = 0 + tmp = 0 + doLoop = True + while doLoop: + n1 = encxkey[n1 & 0xff] + id[n2] + n2 += 1 + + if n2 >= idlen: + n2 = 0 + n1 += idlen + + tmp = n1 & mask + + i += 1 + if i > 11: + tmp %= cnt + + doLoop = tmp > cnt + + return tmp, n1, n2 + + def func6(self, encxkey, data, data_len): + for i in range(data_len): + data[i] = self.func7(encxkey, data[i]) + return len(data) + + def func7(self, encxkey, d): + a = encxkey[256] + b = encxkey[257] + c = encxkey[a] + encxkey[256] = (a + 1) & 0xff + encxkey[257] = (b + c) & 0xff + + a = encxkey[260] + b = encxkey[257] + b = encxkey[b] + c = encxkey[a] + encxkey[a] = b + + a = encxkey[259] + b = encxkey[257] + a = encxkey[a] + encxkey[b] = a + + a = encxkey[256] + b = encxkey[259] + a = encxkey[a] + encxkey[b] = a + + a = encxkey[256] + encxkey[a] = c + + b = encxkey[258] + a = encxkey[c] + c = encxkey[259] + b = (a + b) & 0xff + encxkey[258] = b + + a = b + c = encxkey[c] + b = encxkey[257] + b = encxkey[b] + a = encxkey[a] + c = (b + c) & 0xff + b = encxkey[260] + b = encxkey[b] + c = (b + c) & 0xff + b = encxkey[c] + c = encxkey[256] + c = encxkey[c] + a = (a + c) & 0xff + c = encxkey[b] + b = encxkey[a] + encxkey[260] = d + + c ^= b ^ d + encxkey[259] = c + + return c + + def func6e(self, encxkey, data, data_len): + for i in range(data_len): + data[i] = self.func7e(encxkey, data[i]) + return len(data) + + def func7e(self, encxkey, d): + a = encxkey[256] + b = encxkey[257] + c = encxkey[a] + encxkey[256] = (a + 1) & 0xff + encxkey[257] = (b + c) & 0xff + + a = encxkey[260] + b = encxkey[257] + b = encxkey[b] + c = encxkey[a] + encxkey[a] = b + + a = encxkey[259] + b = encxkey[257] + a = encxkey[a] + encxkey[b] = a + + a = encxkey[256] + b = encxkey[259] + a = encxkey[a] + encxkey[b] = a + + a = encxkey[256] + encxkey[a] = c + + b = encxkey[258] + a = encxkey[c] + c = encxkey[259] + b = (a + b) & 0xff + encxkey[258] = b + + a = b + c = encxkey[c] + b = encxkey[257] + b = encxkey[b] + a = encxkey[a] + c = (b + c) & 0xff + b = encxkey[260] + b = encxkey[b] + c = (b + c) & 0xff + b = encxkey[c] + c = encxkey[256] + c = encxkey[c] + a = (a + c) & 0xff + c = encxkey[b] + b = encxkey[a] + c ^= b ^ d + encxkey[260] = c + encxkey[259] = d + + return c \ No newline at end of file diff --git a/gamespy_server.py b/gamespy_server.py index 5351a74..1e20e41 100644 --- a/gamespy_server.py +++ b/gamespy_server.py @@ -70,6 +70,9 @@ class PlayerSession(LineReceiver): self.perform_addbuddy(data_parsed) elif data_parsed['__cmd__'] == "authadd": 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__']) def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken']) @@ -184,12 +187,35 @@ class PlayerSession(LineReceiver): self.send_status_to_friends() def perform_bm(self, data_parsed): - dest_profileid = data_parsed['t'] - dest_msg = data_parsed['msg'] + if data_parsed['__cmd_val__'] == "1": # Message to/from clients? + if "t" in data_parsed: + # Send message to the profile id in "t" + dest_profileid = int(data_parsed['t']) + dest_msg = data_parsed['msg'] + + msg_d = [] + msg_d.append(('__cmd__', "bm")) + msg_d.append(('__cmd_val__', "1")) + msg_d.append(('f', self.profileid)) + 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.sessions[dest_profileid].transport.write(bytes(msg)) + def perform_addbuddy(self, data_parsed): # Sample: \addbuddy\\sesskey\231601763\newprofileid\476756820\reason\\final\ - self.db.add_buddy(self.profileid, data_parsed['newprofileid']) + buddies = self.db.get_buddy_list(self.profileid) + + buddy_exists = False + for buddy in buddies: + if buddy['buddyProfileId'] == data_parsed['newprofileid']: + buddy_exists = True + break + + if not buddy_exists: + self.db.add_buddy(self.profileid, data_parsed['newprofileid']) # In the case that the user is already a buddy: # \bm\2\f\217936895\msg\|signed|f259f26d3273f8bda23c7c5e4bd8c5aa\final\ diff --git a/natneg_server.py b/natneg_server.py new file mode 100644 index 0000000..9508769 --- /dev/null +++ b/natneg_server.py @@ -0,0 +1,95 @@ +# 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: + 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]) + + 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) + + if recv_data[7] == '\x06': # Was able to connect + client_id = "%02x" % ord(recv_data[14]) + + 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 + + + + + + + + diff --git a/other/utils.py b/other/utils.py index 94a7ee7..ceb506b 100644 --- a/other/utils.py +++ b/other/utils.py @@ -1,5 +1,6 @@ import random import time +import sys def generate_random_str(len): return ''.join(random.choice("abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") for _ in range(len)) @@ -82,7 +83,10 @@ def get_num_from_bytes(data, idx, bytes, bigEndian = False): num = 0 i = 0 while i < bytes and i < len(data): - num |= (ord(data[i]) << (8 * i)) + if type(data[i]) is int: + num |= (data[i] << (8 * i)) + else: + num |= (ord(data[i]) << (8 * i)) i += 1 return num @@ -103,4 +107,52 @@ def get_int_be(data, idx): def get_string(data, idx): data = data[idx:] end = data.index('\0') - return data[:end] + return str(data[:end]) + +def get_bytes_from_num(num, size, bigEndian = False): + output = bytearray(size) + + i = 0 + while i < size: + output[i] = (num >> 8 * i) & 0xff + + i += 1 + + if bigEndian: + output = output[::-1] + + return output + +def get_bytes_from_short(num): + return get_bytes_from_num(num, 2, False) + +def get_bytes_from_short_be(num): + return get_bytes_from_num(num, 2, True) + +def get_bytes_from_int(num): + return get_bytes_from_num(num, 4, False) + +def get_bytes_from_int_be(num): + return get_bytes_from_num(num, 4, True) + +def print_hex(data, cols = 16): + for i in range(len(data) / cols + 1): + c = 0 + for x in range(cols): + if (i * cols + x + 1) > len(data): + break + + print "%02x" % data[i * cols + x], + c += 1 + + c = cols - c + sys.stdout.write(" " * (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(".") + else: + sys.stdout.write("%c" % data[i * cols + x]) + print "" \ No newline at end of file diff --git a/qr_server.py b/qr_server.py index c333149..54d3d0c 100644 --- a/qr_server.py +++ b/qr_server.py @@ -1,17 +1,41 @@ # Server emulator for *.available.gs.nintendowifi.net and *.master.gs.nintendowifi.net # Query and Reporting: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview -# TODO: Refactor into a class - import socket +import struct 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 +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 -#address = ('127.0.0.1', 27900) # accessible to only the local computer + +# 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) @@ -19,11 +43,6 @@ s.bind(address) utils.print_log("Server is now listening on %s:%s..." % (address[0], address[1])) -# Temporarily make these global until everything is made into a client class. -server_challenge = "" -secretkey = "JJlSi8" # Parse gslist.cfg later -sent_challenge = False - while 1: recv_data, addr = s.recvfrom(2048) @@ -109,6 +128,13 @@ while 1: # Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr # Use as reference. + session_id = struct.unpack(" 0: + # Write number of fields that will be returned. + key_count = len(server_info['requested']) + output += utils.get_bytes_from_short(key_count) - accept_connection = True - data = client.recv(size) + 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 + return - # Didn't get a valid command. - # Player disconnected? - # Close connection - if not data: - break + 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 key_count > 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. @@ -53,8 +115,32 @@ while 1: # 0x05 - Player search request # # For Tetris DS, at the very least 0x00 and 0x02 need to be implemented. + + if self.forward_to_client: + # 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) + for server in self.server_list: + print "%s %s" % (ip, server['publicip']) + if server['publicip'] == ip and server['publicport'] == str(self.forward_client[1]): + print server + + # 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..." % (addr[0], addr[1])) + 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? @@ -92,38 +178,82 @@ while 1: elif (options & ALTERNATE_SOURCE_IP): source_ip = utils.get_int(data, idx) - print "%02x %02x %08x" % (list_version, encoding_version, game_version) - print "%s" % query_game + 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" % challenge print "%s" % filter print "%s" % fields - print "%08x" % options - print "%d %08x" % (max_servers, source_ip) + #print "%08x" % options + #print "%d %08x" % (max_servers, source_ip) + + # Get dictionary from master server list server. + self.server_list = get_server_list(query_game, filter, fields, max_servers)._getvalue() + + # Generate encrypted server list and send to client. + print self.server_list + for server in self.server_list: + # Generate binary server list data + data = generate_server_list_data(self.addr, fields, server) + + # 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)) + break - # TODO: Handle query elif data[2] == '\x02': # Send message request - dest_addr = '.'.join(["%d" % x for x in addr[3:7]]) - dest_port = utils.get_short_be(addr, 7) # What's the pythonic way to do this? unpack? + 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) - # Wait for message data - msg_data = client.recv(size) + 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_log("Received send message request from %s:%s to %s:%d... %s" % (addr[0], addr[1], dest_addr, dest_port, msg_data)) - - # Create new connection to send to other user over UDP. - # Move this code somewhere else after testing has been finished. - user_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - user_s.bind(dest) - user_s.sendto(msg_data, dest) - - utils.print_log("Sent message to %s:%d... %s" % (dest_addr, dest_port, msg_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..." % (addr[0], addr[1])) + 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... %s" % (ord(data[2]), addr[0], addr[1], data)) + utils.print_log("Received unknown command (%02x) from %s:%s... %s" % (ord(data[2]), self.addr.host, self.addr.port, data)) + + +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") + +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 diff --git a/www/available.nintendowifi.net/public_html/index.html b/www/available.nintendowifi.net/public_html/index.html new file mode 100644 index 0000000..81403e4 --- /dev/null +++ b/www/available.nintendowifi.net/public_html/index.html @@ -0,0 +1 @@ +test 2 \ No newline at end of file diff --git a/www/conntest/index.html b/www/conntest.nintendowifi.net/public_html/index.html similarity index 100% rename from www/conntest/index.html rename to www/conntest.nintendowifi.net/public_html/index.html diff --git a/www/gamestats.gs.nintendowifi.net/public_html/index.html b/www/gamestats.gs.nintendowifi.net/public_html/index.html new file mode 100644 index 0000000..e69de29 diff --git a/www/gamestats.gs.nintendowifi.net/public_html/tetrisds/store.asp b/www/gamestats.gs.nintendowifi.net/public_html/tetrisds/store.asp new file mode 100644 index 0000000..e69de29 diff --git a/www/nas/ac.php b/www/nas.nintendowifi.net/public_html/ac similarity index 94% rename from www/nas/ac.php rename to www/nas.nintendowifi.net/public_html/ac index b85fb59..d6c555d 100644 --- a/www/nas/ac.php +++ b/www/nas.nintendowifi.net/public_html/ac @@ -1,4 +1,6 @@ $value) $key == "passwd" || //$key == "bssid" || //$key == "apinfo" || - //$key == "gamecd" || + $key == "gamecd" || //$key == "makercd" || //$key == "unitcd" || //$key == "macadr" || @@ -90,7 +92,7 @@ foreach ($_GET as $key => $value) $key == "passwd" || //$key == "bssid" || //$key == "apinfo" || - //$key == "gamecd" || + $key == "gamecd" || //$key == "makercd" || //$key == "unitcd" || //$key == "macadr" ||