def do_GET(self, conn, key, append_hash, append_text=""): try: conn.send_response(200) conn.send_header("Content-type", "text/html") conn.send_header("Server", "Microsoft-IIS/6.0") conn.send_header("Server", "GSTPRDSTATSWEB2") conn.send_header("X-Powered-By", "ASP.NET") conn.end_headers() params = conn.str_to_dict(conn.path) ret = "" if "hash" not in params: # The token is used in combination with the game's secret key. # The format of the hash parameter sent from the client is # sha1(secret_key + token). token = utils.generate_random_str(32) ret = token else: # Handle data (generic response for now) ret += append_text if append_hash: h = hashlib.sha1() h.update(key + base64.urlsafe_b64encode(ret) + key) ret += h.hexdigest() conn.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s", traceback.format_exc())
def do_GET(self, conn, key, append_hash, append_text = ""): try: conn.send_response(200) conn.send_header("Content-type", "text/html") conn.send_header("Server", "Microsoft-IIS/6.0") conn.send_header("Server", "GSTPRDSTATSWEB2") conn.send_header("X-Powered-By", "ASP.NET") conn.end_headers() params = conn.str_to_dict(conn.path) ret = "" if "hash" not in params: # The token is used in combination with the game's secret key. # The format of the hash parameter sent from the client is sha1(secret_key + token). token = utils.generate_random_str(32) ret = token else: # Handle data (generic response for now) ret += append_text if append_hash == True: h = hashlib.sha1() h.update(key + base64.urlsafe_b64encode(ret) + key) ret += h.hexdigest() conn.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
def _executeAndMeasure(self, cursor, statement, parameters): logTransactionId = utils.generate_random_str(8) logger.log(SQL_LOGLEVEL, "[%s] STARTING: " % logTransactionId + statement.replace('?', '%s') % parameters) timeStart = time.time() clockStart = time.clock() cursor.execute(statement, parameters) clockEnd = time.clock() timeEnd = time.time() timeDiff = timeEnd - timeStart logger.log(SQL_LOGLEVEL, "[%s] DONE: Took %s real time / %s processor time", logTransactionId, timeDiff, clockEnd - clockStart) if timeDiff > 1.0: logger.log(logging.WARNING, "[%s] WARNING: SQL Statement took %s seconds!", logTransactionId, timeDiff) logger.log(logging.WARNING, "[%s] " % logTransactionId + statement.replace('?', '%s') % parameters) return
def connectionMade(self): try: self.transport.setTcpKeepAlive(1) self.log(logging.INFO, "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( 10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") # The first command sent to the client is always a login challenge # containing the server challenge key. msg = gs_query.create_gamespy_message([ ('__cmd__', "lc"), ('__cmd_val__', "1"), ('challenge', self.challenge), ('id', "1"), ]) self.log(logging.DEBUG, "SENDING: '%s'...", msg) self.transport.write(bytes(msg)) except: self.log(logging.ERROR, "Unknown exception: %s", traceback.format_exc())
def connectionMade(self): try: self.log(logging.INFO, "Received connection from %s:%d", self.address.host, self.address.port) # Generate a random challenge string self.challenge = utils.generate_random_str( 10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) # The first command sent to the client is always a login challenge # containing the server challenge key. msg = gs_query.create_gamespy_message([ ('__cmd__', "lc"), ('__cmd_val__', "1"), ('challenge', self.challenge), ('id', "1"), ]) self.log(logging.DEBUG, "SENDING: '%s'...", msg) msg = self.crypt(msg) self.transport.write(bytes(msg)) except: self.log(logging.ERROR, "Unknown exception: %s", traceback.format_exc())
def generate_authtoken(self, userid, data): # Since the auth token passed back to the game will be random, we can make it small enough that there # should never be a crash due to the size of the token. # ^ real authtoken is 80 + 3 bytes though and I want to figure out what's causing the 52200 # so I'm matching everything as closely as possible to the real thing size = 80 # TODO: Another one of those questionable dupe-preventations while True: with Transaction(self.conn) as tx: authtoken = "NDS" + utils.generate_random_str(size) row = tx.queryone("SELECT COUNT(*) FROM nas_logins WHERE authtoken = ?", (authtoken,)) count = int(row[0]) if count == 0: break with Transaction(self.conn) as tx: row = tx.queryone("SELECT * FROM nas_logins WHERE userid = ?", (userid,)) r = self.get_dict(row) if "devname" in data: data["devname"] = gs_utils.base64_encode(data["devname"]) if "ingamesn" in data: data["ingamesn"] = gs_utils.base64_encode(data["ingamesn"]) data = json.dumps(data) with Transaction(self.conn) as tx: if r == None: # no row, add it tx.nonquery("INSERT INTO nas_logins VALUES (?, ?, ?)", (userid, authtoken, data)) else: tx.nonquery("UPDATE nas_logins SET authtoken = ?, data = ? WHERE userid = ?", (authtoken, data, userid)) return authtoken
def _executeAndMeasure(self, cursor, statement, parameters): logTransactionId = utils.generate_random_str(8) logger.log( SQL_LOGLEVEL, "[%s] STARTING: " % logTransactionId + statement.replace('?', '%s') % parameters) timeStart = time.time() clockStart = time.clock() cursor.execute(statement, parameters) clockEnd = time.clock() timeEnd = time.time() timeDiff = timeEnd - timeStart logger.log(SQL_LOGLEVEL, "[%s] DONE: Took %s real time / %s processor time", logTransactionId, timeDiff, clockEnd - clockStart) if timeDiff > 1.0: logger.log(logging.WARNING, "[%s] WARNING: SQL Statement took %s seconds!", logTransactionId, timeDiff) logger.log( logging.WARNING, "[%s] " % logTransactionId + statement.replace('?', '%s') % parameters) return
def generate_authtoken(self, userid, data): # Since the auth token passed back to the game will be random, we can make it small enough that there # should never be a crash due to the size of the token. # ^ real authtoken is 80 + 3 bytes though and I want to figure out what's causing the 52200 # so I'm matching everything as closely as possible to the real thing size = 80 authtoken = "NDS" + utils.generate_random_str(size) q = "SELECT authtoken FROM nas_logins WHERE authtoken = ?" q2 = q.replace("?", "%s") % (authtoken) logger.log(SQL_LOGLEVEL, q2) c = self.conn.cursor() for r in c.execute(q, [authtoken]): authtoken = "NDS" + utils.generate_random_str(size) q = "SELECT * FROM nas_logins WHERE userid = ?" q2 = q.replace("?", "%s") % (userid) logger.log(SQL_LOGLEVEL, q2) c.execute(q, [userid]) r = self.get_dict(c.fetchone()) if "devname" in data: data["devname"] = gs_utils.base64_encode(data["devname"]) if "ingamesn" in data: data["ingamesn"] = gs_utils.base64_encode(data["ingamesn"]) data = json.dumps(data) if r == None: # no row, add it q = "INSERT INTO nas_logins VALUES (?, ?, ?)" q2 = q.replace("?", "%s") % (userid, authtoken, data) logger.log(SQL_LOGLEVEL, q2) c.execute(q, [userid, authtoken, data]) else: q = "UPDATE nas_logins SET authtoken = ?, data = ? WHERE userid = ?" q2 = q.replace("?", "%s") % (authtoken, data, userid) logger.log(SQL_LOGLEVEL, q2) c.execute(q, [authtoken, data, userid]) c.close() self.conn.commit() return authtoken
def generate_authtoken(self, userid, data): # Since the auth token passed back to the game will be random, we can make it small enough that there # should never be a crash due to the size of the token. size = 16 authtoken = "NDS" + utils.generate_random_str(size) q = "SELECT authtoken FROM nas_logins WHERE authtoken = ?" q2 = q.replace("?", "%s") % (authtoken) logger.log(-1, q2) c = self.conn.cursor() for r in c.execute(q, [authtoken]): authtoken = "NDS" + utils.generate_random_str(size) q = "SELECT * FROM nas_logins WHERE userid = ?" q2 = q.replace("?", "%s") % (userid) logger.log(-1, q2) c.execute(q, [userid]) r = self.get_dict(c.fetchone()) if "devname" in data: data["devname"] = gs_utils.base64_encode(data["devname"]) if "ingamesn" in data: data["ingamesn"] = gs_utils.base64_encode(data["ingamesn"]) data = json.dumps(data) if r == None: # no row, add it q = "INSERT INTO nas_logins VALUES (?, ?, ?)" q2 = q.replace("?", "%s") % (userid, authtoken, data) logger.log(-1, q2) c.execute(q, [userid, authtoken, data]) else: q = "UPDATE nas_logins SET authtoken = ?, data = ? WHERE userid = ?" q2 = q.replace("?", "%s") % (authtoken, data, userid) logger.log(-1, q2) c.execute(q, [authtoken, data, userid]) c.close() self.conn.commit() return authtoken
def generate_authtoken(self, userid, data): # Since the auth token passed back to the game will be random, we can make it small enough that there # should never be a crash due to the size of the token. size = 16 authtoken = "NDS" + utils.generate_random_str(size) q = "SELECT authtoken FROM nas_logins WHERE authtoken = ?" q2 = q.replace("?", "%s") % (authtoken) logger.log(-1, q2) c = self.conn.cursor() for r in c.execute(q, [authtoken]): authtoken = "NDS" + utils.generate_random_str(size) q = "SELECT * FROM nas_logins WHERE userid = ?" q2 = q.replace("?", "%s") % (userid) logger.log(-1, q2) c.execute(q, [userid]) r = self.get_dict(c.fetchone()) if "devname" in data: data["devname"] = gs_utils.base64_encode(data["devname"]) data = json.dumps(data) if r == None: # no row, add it q = "INSERT INTO nas_logins VALUES (?, ?, ?)" q2 = q.replace("?", "%s") % (userid, authtoken, data) logger.log(-1, q2) c.execute(q, [userid, authtoken, data]) else: q = "UPDATE nas_logins SET authtoken = ?, data = ? WHERE userid = ?" q2 = q.replace("?", "%s") % (authtoken, data, userid) logger.log(-1, q2) c.execute(q, [authtoken, data, userid]) c.close() self.conn.commit() return authtoken
def generate_authtoken(self, userid, data): """Generate authentication token. Since the auth token passed back to the game will be random, we can make it small enough that there should never be a crash due to the size of the token. ^ real authtoken is 80 + 3 bytes though and I want to figure out what's causing the 52200 so I'm matching everything as closely as possible to the real thing. """ size = 80 # TODO: Another one of those questionable dupe-preventations while True: with Transaction(self.conn) as tx: authtoken = "NDS" + utils.generate_random_str(size) row = tx.queryone( "SELECT COUNT(*) FROM nas_logins WHERE authtoken = ?", (authtoken,) ) count = int(row[0]) if not count: break with Transaction(self.conn) as tx: row = tx.queryone( "SELECT * FROM nas_logins WHERE userid = ?", (userid,) ) r = self.get_dict(row) if "devname" in data: data["devname"] = gs_utils.base64_encode(data["devname"]) if "ingamesn" in data: data["ingamesn"] = gs_utils.base64_encode(data["ingamesn"]) data = json.dumps(data) with Transaction(self.conn) as tx: if r is None: # no row, add it tx.nonquery( "INSERT INTO nas_logins VALUES (?, ?, ?)", (userid, authtoken, data) ) else: tx.nonquery( "UPDATE nas_logins SET authtoken = ?, data = ?" " WHERE userid = ?", (authtoken, data, userid) ) return authtoken
def do_POST(self): if self.path == "/ac": length = int(self.headers['content-length']) post = urlparse.parse_qs(self.rfile.read(length)) for k, v in post.iteritems(): post[k] = self.base64_dec(v[0]) logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} ret["datetime"] = time.strftime("%Y%m%d%H%M%S") ret["retry"] = "0" action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("Server", "Nintendo Wii (http)") self.send_header("NODE", "wifiappe3") self.end_headers() if action == "acctcreate": # TODO: test for duplicate accounts ret["returncd"] = "002" logger.log(logging.DEBUG, "acctcreate response to %s", self.client_address) logger.log(logging.DEBUG, ret) for k, v in ret.iteritems(): ret[k] = self.base64_enc(v) self.wfile.write(urllib.urlencode(ret).replace("%2A", "*")) elif action == "login": ret["returncd"] = "001" ret["locator"] = "gamespy.com" challenge = utils.generate_random_str(8) ret["challenge"] = challenge post["challenge"] = challenge authtoken = self.server.db.generate_authtoken(post["userid"], json.dumps(post)) ret["token"] = authtoken logger.log(logging.DEBUG, "login response to %s", self.client_address) logger.log(logging.DEBUG, ret) for k, v in ret.iteritems(): ret[k] = self.base64_enc(v) self.wfile.write(urllib.urlencode(ret).replace("%2A", "*"))
def connectionMade(self): self.log(logging.INFO, "Received connection from %s:%d" % (self.address.host, self.address.port)) # Generate a random challenge string self.challenge = utils.generate_random_str(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") # 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")) msg_d.append(('challenge', self.challenge)) msg_d.append(('id', "1")) msg = gs_query.create_gamespy_message(msg_d) self.log(logging.DEBUG, "SENDING: '%s'..." % msg) msg = self.crypt(msg) self.transport.write(bytes(msg))
def handle_ac_login(handler, db, addr, post): """Handle ac login request.""" if db.is_banned(post): ret = { "retry": "1", "returncd": "3914", "locator": "gamespy.com", "reason": "User banned." } logger.log(logging.DEBUG, "Login denied for banned user %s", str(post)) # Un-comment these lines to enable console registration feature # elif not db.pending(post): # logger.log(logging.DEBUG, "Login denied - Unknown console %s", post) # ret = { # "retry": "1", # "returncd": "3921", # "locator": "gamespy.com", # } # elif not db.registered(post): # logger.log(logging.DEBUG, "Login denied - console pending %s", post) # ret = { # "retry": "1", # "returncd": "3888", # "locator": "gamespy.com", # } else: challenge = utils.generate_random_str(8) post["challenge"] = challenge authtoken = db.generate_authtoken(post["userid"], post) ret = { "retry": "0", "returncd": "001", "locator": "gamespy.com", "challenge": challenge, "token": authtoken, } logger.log(logging.DEBUG, "Login response to %s:%d", *addr) logger.log(logging.DEBUG, "%s", ret) return ret
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].playerid == 0 and "dwc_pid" in k: # Get the player's id and then query the profile to figure out what console they are on. # The endianness of some server data depends on the endianness of the console, so we must be able # to account for that. self.sessions[session_id].playerid = int(k['dwc_pid']) profile = self.db.get_profile_from_profileid(self.sessions[session_id].playerid) if "console" in profile: self.sessions[session_id].console = profile['console'] 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, self.sessions[session_id].console) 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.WARNING, 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.WARNING, 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.WARNING, 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.WARNING, 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.WARNING, address, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:])) else: self.log(logging.ERROR, address, "Unknown request from %s:%s:" % (address[0], address[1])) self.log(logging.DEBUG, address, utils.pretty_print_hex(recv_data))
def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'], self.db) #print authtoken_parsed # Track what console is connecting and save it in the database during user creation just in case we can use # the information in the future. console = 0 # 0 = NDS, 1 = Wii # get correct information userid = authtoken_parsed['userid'] # The Wii does not use passwd, so take another uniquely generated string as the password. if "passwd" in authtoken_parsed: password = authtoken_parsed['passwd'] else: password = authtoken_parsed['gsbrcd'] console = 1 gsbrcd = authtoken_parsed['gsbrcd'] gameid = gsbrcd[:4] uniquenick = utils.base32_encode(int(userid)) + gsbrcd email = uniquenick + "@nds" # The Wii also seems to use @nds. # Wii: Serial number if "csnum" in authtoken_parsed: csnum = authtoken_parsed['csnum'] console = 1 else: csnum = "" # Wii: Friend code if "cfc" in authtoken_parsed: cfc = authtoken_parsed['cfc'] console = 1 else: cfc = "" # NDS: Wifi network's BSSID if "bssid" in authtoken_parsed: bssid = authtoken_parsed['bssid'] else: bssid = "" # NDS: Device name if "devname" in authtoken_parsed: devname = authtoken_parsed['devname'] else: devname = "" # NDS: User's birthday if "birth" in authtoken_parsed: birth = authtoken_parsed['birth'] else: birth = "" # 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: self.log( logging.ERROR, "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']) valid_user = self.db.check_user_exists(userid, gsbrcd) if valid_user == False: profileid = self.db.create_user(userid, password, email, uniquenick, gsbrcd, console, csnum, cfc, bssid, devname, birth, gameid) else: profileid = self.db.perform_login(userid, password, gsbrcd) if profileid == None: # Handle case where the user is invalid self.log(logging.ERROR, "Invalid password") if profileid != None: # Successfully logged in or created account, continue creating session. sesskey = self.db.create_session(profileid) self.sessions[profileid] = self msg_d = [] msg_d.append(('__cmd__', "lc")) msg_d.append(('__cmd_val__', "2")) msg_d.append(('sesskey', sesskey)) msg_d.append(('proof', proof)) msg_d.append(('userid', userid)) msg_d.append(('profileid', profileid)) msg_d.append(('uniquenick', uniquenick)) msg_d.append( ('lt', gs_utils.base64_encode(utils.generate_random_str(16))) ) # Some kind of token... don't know it gets used or generated, but it doesn't seem to have any negative effects if it's not properly generated. msg_d.append(('id', data_parsed['id'])) msg = gs_query.create_gamespy_message(msg_d) # Take the first 4 letters of gsbrcd instead of gamecd because they should be consistent across game # regions. For example, the US version of Metroid Prime Hunters has the gamecd "AMHE" and the first 4 letters # of gsbrcd are "AMHE". However, the Japanese version of Metroid Prime Hunters has the gamecd "AMHJ" with # the first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way, with the first 4 letters as the # Japanese version (ATRJ) while the gamecd is region specific (ATRE for US and ATRJ for JP). # gameid is used to send all people on the player's friends list a status updates, so don't make it region # specific. self.gameid = gsbrcd[0:4] self.profileid = int(profileid) self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) self.buddies = self.db.get_buddy_list(self.profileid) self.blocked = self.db.get_blocked_list(self.profileid) # Get pending messages. self.get_pending_messages() # Send any friend statuses when the user logs in. # This will allow the user to see if their friends are hosting a game as soon as they log in. self.get_status_from_friends() self.send_status_to_friends()
def do_POST(self): try: length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) ret = '' if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} ret["datetime"] = time.strftime("%Y%m%d%H%M%S") ret["retry"] = "0" action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") if action == "acctcreate": # TODO: test for duplicate accounts ret["returncd"] = "002" logger.log(logging.DEBUG, "acctcreate response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "login": ret["returncd"] = "001" ret["locator"] = "gamespy.com" challenge = utils.generate_random_str(8) ret["challenge"] = challenge post["challenge"] = challenge authtoken = self.server.db.generate_authtoken(post["userid"], post) ret["token"] = authtoken logger.log(logging.DEBUG, "login response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken(post["userid"], post) if 'svc' in post: if post["svc"] in ("9000", "9001"): # DLC host = 9000 ret["svchost"] = self.headers['host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work ret["svchost"] = ret["svchost"].split(',')[0] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post["svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) else: logger.log(logging.WARNING, "Unknown action request %s from %s!", self.path, self.client_address) ret = '' elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} words = len(post["words"].split('\t')) wordsret = "0" * words ret["prwords"] = wordsret ret["prwordsA"] = wordsret ret["prwordsC"] = wordsret ret["prwordsE"] = wordsret ret["prwordsJ"] = wordsret ret["prwordsK"] = wordsret ret["prwordsP"] = wordsret ret["returncd"] = "000" ret["datetime"] = time.strftime("%Y%m%d%H%M%S") self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") logger.log(logging.DEBUG, "pr response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcdir = os.path.abspath('dlc') dlcpath = os.path.abspath("dlc/" + post["gamecd"]) dlc_contenttype = False if os.path.commonprefix([dlcdir, dlcpath]) != dlcdir: logging.log(logging.WARNING, 'Attempted directory traversal attack "%s", cancelling.', dlcpath) self.send_response(403) return def safeloadfi(fn, mode='rb'): ''' safeloadfi : string -> string Safely load contents of a file, given a filename, and closing the file afterward ''' with open(os.path.join(dlcpath, fn), mode) as fi: return fi.read() if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = None if "attr1" in post: attr1 = post["attr1"] attr2 = None if "attr2" in post: attr2 = post["attr2"] attr3 = None if "attr3" in post: attr3 = post["attr3"] dlcfi = safeloadfi("_list.txt") lst = self.filter_list(dlcfi, attr1, attr2, attr3) count = self.get_file_count(lst) ret = "%d" % count if action == "list": num = int(post["num"]) offset = int(post["offset"]) attr1 = None if "attr1" in post: attr1 = post["attr1"] attr2 = None if "attr2" in post: attr2 = post["attr2"] attr3 = None if "attr3" in post: attr3 = post["attr3"] if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(os.path.join(dlcpath, "_list.txt")): ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) if post["gamecd"] in gamecodes_return_random_file: # allow user to control which file to receive by setting the local date # selected file will be the one at index (day of year) mod (file count) try: userData = self.server.db.get_nas_login(post['token']) date = time.strptime(userData['devtime'], '%y%m%d%H%M%S') files = ret.splitlines() ret = files[int(date.tm_yday) % len(files)] + '\r\n' except: ret = self.filter_list_random_files(ret, 1) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) ret = safeloadfi(contents) self.send_response(200) if dlc_contenttype == True: self.send_header("Content-type", "application/x-dsdl") self.send_header("Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") logger.log(logging.DEBUG, "download response to %s", self.client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) else: logger.log(logging.WARNING, "Unknown path request %s from %s!", self.path, self.client_address) self.send_header("Content-Length", str(len(ret))) self.end_headers() self.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed["authtoken"], self.db) if authtoken_parsed is None: self.log(logging.WARNING, "%s", "Invalid Authtoken.") msg = gs_query.create_gamespy_message( [ ("__cmd__", "error"), ("__cmd_val__", ""), ("err", "266"), ("fatal", ""), ("errmsg", "There was an error validating the" " pre-authentication."), ("id", data_parsed["id"]), ] ) self.transport.write(bytes(msg)) return if "sdkrevision" in data_parsed: self.sdkrevision = data_parsed["sdkrevision"] # 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: self.log( logging.ERROR, "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"] ) userid, profileid, gsbrcd, uniquenick = gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed, self.db) if profileid is not None: # Successfully logged in or created account, continue # creating session. loginticket = gs_utils.base64_encode(utils.generate_random_str(16)) self.sesskey = self.db.create_session(profileid, loginticket) self.sessions[profileid] = self self.buddies = self.db.get_buddy_list(self.profileid) self.blocked = self.db.get_blocked_list(self.profileid) if self.sdkrevision == "11": # Used in Tatsunoko vs Capcom def make_list(data): return [str(d["buddyProfileId"]) for d in data if d["status"] == 1] block_list = make_list(self.blocked) msg = gs_query.create_gamespy_message( [("__cmd__", "blk"), ("__cmd_val__", str(len(block_list))), ("list", ",".join(block_list))] ) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg)) buddy_list = make_list(self.buddies) msg = gs_query.create_gamespy_message( [("__cmd__", "bdy"), ("__cmd_val__", str(len(buddy_list))), ("list", ",".join(buddy_list))] ) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg)) msg = gs_query.create_gamespy_message( [ ("__cmd__", "lc"), ("__cmd_val__", "2"), ("sesskey", self.sesskey), ("proof", proof), ("userid", userid), ("profileid", profileid), ("uniquenick", uniquenick), # Some kind of token... don't know it gets used or generated, # but it doesn't seem to have any negative effects if it's # not properly generated. ("lt", loginticket), ("id", data_parsed["id"]), ] ) # Take the first 4 letters of gsbrcd instead of gamecd because # they should be consistent across game regions. For example, the # US version of Metroid Prime Hunters has the gamecd "AMHE" and # the first 4 letters of gsbrcd are "AMHE". However, the Japanese # version of Metroid Prime Hunters has the gamecd "AMHJ" with the # first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way, # with the first 4 letters as the Japanese version (ATRJ) while # the gamecd is region specific (ATRE for US and ATRJ for JP). # gameid is used to send all people on the player's friends list a # status updates, so don't make it region specific. self.gameid = gsbrcd[:4] self.profileid = int(profileid) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg)) # Get pending messages. self.get_pending_messages() # Send any friend statuses when the user logs in. # This will allow the user to see if their friends are hosting a # game as soon as they log in. self.get_status_from_friends() self.send_status_to_friends() # profile = self.db.get_profile_from_profileid(profileid) # if profile is not None: # self.statstring = profile['stat'] # self.locstring = profile['loc'] else: self.log(logging.INFO, "%s", "Invalid password or banned user") msg = gs_query.create_gamespy_message( [ ("__cmd__", "error"), ("__cmd_val__", ""), ("err", "256"), ("fatal", ""), ("errmsg", "Login failed."), ("id", data_parsed["id"]), ] ) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg))
def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'], self.db) if authtoken_parsed is None: self.log(logging.WARNING, "%s", "Invalid Authtoken.") msg = gs_query.create_gamespy_message([ ('__cmd__', "error"), ('__cmd_val__', ""), ('err', '266'), ('fatal', ''), ('errmsg', 'There was an error validating the' ' pre-authentication.'), ('id', data_parsed['id']), ]) self.transport.write(bytes(msg)) return if 'sdkrevision' in data_parsed: self.sdkrevision = data_parsed['sdkrevision'] # 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: self.log(logging.ERROR, "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']) userid, profileid, gsbrcd, uniquenick = \ gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed, self.db) if profileid is not None: # Successfully logged in or created account, continue # creating session. loginticket = gs_utils.base64_encode(utils.generate_random_str(16)) self.sesskey = self.db.create_session(profileid, loginticket) self.sessions[profileid] = self self.buddies = self.db.get_buddy_list(self.profileid) self.blocked = self.db.get_blocked_list(self.profileid) if self.sdkrevision == "11": # Used in Tatsunoko vs Capcom def make_list(data): return [ str(d['buddyProfileId']) for d in data if d['status'] == 1 ] block_list = make_list(self.blocked) msg = gs_query.create_gamespy_message([ ('__cmd__', "blk"), ('__cmd_val__', str(len(block_list))), ('list', ','.join(block_list)), ]) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg)) buddy_list = make_list(self.buddies) msg = gs_query.create_gamespy_message([ ('__cmd__', "bdy"), ('__cmd_val__', str(len(buddy_list))), ('list', ','.join(buddy_list)), ]) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg)) msg = gs_query.create_gamespy_message([ ('__cmd__', "lc"), ('__cmd_val__', "2"), ('sesskey', self.sesskey), ('proof', proof), ('userid', userid), ('profileid', profileid), ('uniquenick', uniquenick), # Some kind of token... don't know it gets used or generated, # but it doesn't seem to have any negative effects if it's # not properly generated. ('lt', loginticket), ('id', data_parsed['id']), ]) # Take the first 4 letters of gsbrcd instead of gamecd because # they should be consistent across game regions. For example, the # US version of Metroid Prime Hunters has the gamecd "AMHE" and # the first 4 letters of gsbrcd are "AMHE". However, the Japanese # version of Metroid Prime Hunters has the gamecd "AMHJ" with the # first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way, # with the first 4 letters as the Japanese version (ATRJ) while # the gamecd is region specific (ATRE for US and ATRJ for JP). # gameid is used to send all people on the player's friends list a # status updates, so don't make it region specific. self.gameid = gsbrcd[:4] self.profileid = int(profileid) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg)) # Get pending messages. self.get_pending_messages() # Send any friend statuses when the user logs in. # This will allow the user to see if their friends are hosting a # game as soon as they log in. self.get_status_from_friends() self.send_status_to_friends() # profile = self.db.get_profile_from_profileid(profileid) # if profile is not None: # self.statstring = profile['stat'] # self.locstring = profile['loc'] else: self.log(logging.INFO, "%s", "Invalid password or banned user") msg = gs_query.create_gamespy_message([ ('__cmd__', "error"), ('__cmd_val__', ""), ('err', '256'), ('fatal', ''), ('errmsg', 'Login failed.'), ('id', data_parsed['id']), ]) self.log(logging.DEBUG, "SENDING: %s", msg) self.transport.write(bytes(msg))
def handle_packet(self, socket, recv_data, address): # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # # After some more packet inspection, it seems the format goes something like this: # - All server messages seem to always start with \xfe\xfd. # - The first byte from the client (or third byte from the server) is a command. # - Bytes 2 - 5 from the client is some kind of ID. This will have to be inspected later. I believe it's a # session-like ID because the number changes between connections. Copying the client's ID might be enough. # # The above was as guessed. # The code in Tetris DS (overlay 10) @ 0216E974 handles the network command creation. # R1 contains the command to be sent to the server. # R2 contains a pointer to some unknown integer that gets written after the command. # # - Commands # Commands range from 0x00 to 0x09 (for client only at least?) (Tetris DS overlay 10 @ 0216DDCC) # # CLIENT: # 0x01 - Response (Tetris DS overlay 10 @ 216DCA4) # Sends back base64 of RC4 encrypted string that was gotten from the server's 0x01. # # 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30) # Data sent: # 1) Loop for each localip available on the system, write as localip%d\x00(local ip) # 2) localport\x00(local port) # 3) natneg (either 0 or 1) # 4) ONLY IF STATE CHANGED: statechanged\x00(state) (Possible values: 0, 1, 2, 3) # 5) gamename\x00(game name) # 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicip\x00(public ip) # 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicport\x00(public port) # # if statechanged != 2: # Write various other data described here: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation # # 0x07 - Unknown, related to server's 0x06 (returns value sent from server) # # 0x08 - Keep alive? Sent after 0x03 # # 0x09 - Availability check # # SERVER: # 0x01 - Unknown # Data sent: # 8 random ASCII characters (?) followed by the public IP and port of the client as a hex string # # 0x06 - Unknown # First 4 bytes is some kind of id? I believe it's a unique identifier for the data being sent, # seeing how the server can send the same IP information many times in a row. If the IP information has # already been parsed then it doesn't waste time handling it. # # After that is a "SBCM" section which is 0x14 bytes in total. # SBCM information gets parsed at 2141A0C in Tetris DS overlay 10. # Seems to contain IP address information. # # The SBCM seems to contain a little information that must be parsed before. # After the SBCM: # \x03\x00\x00\x00 - Always the same? # \x01 - Found player? # \x04 - Unknown # (2 bytes) - Unknown. Port? # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # \x00\x00\x00\x00 - Unknown but seems to get checked # # Another SBCM, after a player has been found and attempting to start a game: # \x03\x00\x00\x00 - Always the same? # \x05 - Connecting to player? # \x00 - Unknown # (2 bytes) - Unknown. Port? Same as before. # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # # 0x0a - Response to 0x01 # Gets sent after receiving 0x01 from the client. So, server 0x01 -> client 0x01 -> server 0x0a. # Has no other data besides the client ID. # # - \xfd\xfc commands get passed directly between the other player(s)? # # # Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr # Use as reference. session_id = None if recv_data[0] != '\x09': # Don't add a session if the client is trying to check if the game is available or not 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) self.sessions[session_id].session = session_id self.sessions[session_id].keepalive = int(time.time()) self.sessions[session_id].disconnected = False if session_id in self.sessions and self.sessions[ session_id].disconnected == True: return if session_id in self.sessions: self.sessions[session_id].keepalive = int( time.time()) # Make sure the server doesn't get removed # Handle commands if recv_data[0] == '\x00': # Query self.log( logging.DEBUG, address, session_id, "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, session_id, "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 # Send message back to client saying it was accepted packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command packet.extend(session_id_raw) # Get the session ID self.write_queue.put((packet, address)) self.log( logging.DEBUG, address, session_id, "Sent client registered to %s:%s..." % (address[0], address[1])) if self.sessions[session_id].heartbeat_data != None: self.update_server_list( session_id, self.sessions[session_id].heartbeat_data) else: # Failed the challenge, request another during the next heartbeat self.sessions[session_id].sent_challenge = False self.server_manager.delete_server( self.sessions[session_id].gamename, session_id) elif recv_data[0] == '\x02': # Echo self.log( logging.DEBUG, address, session_id, "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, session_id, "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, session_id, "%s = %s" % (d[i], d[i+1])) k[d[i]] = d[i + 1] if self.sessions[session_id].ingamesn != None: if "gamename" in k and "dwc_pid" in k: try: profile = self.db.get_profile_from_profileid( k['dwc_pid']) naslogin = self.db.get_nas_login_from_userid( profile['userid']) self.sessions[session_id].ingamesn = str( naslogin['ingamesn'] ) # convert to string from unicode(which is just a base64 string anyway) except Exception, e: pass # If the game doesn't have, don't worry about it. if self.sessions[ session_id].ingamesn != None and "ingamesn" not in k: k['ingamesn'] = self.sessions[session_id].ingamesn if "gamename" in k: if k['gamename'] in self.secret_key_list: self.sessions[session_id].secretkey = self.secret_key_list[ k['gamename']] else: self.log( logging.INFO, address, session_id, "Connection from unknown game '%s'!" % k['gamename']) if self.sessions[session_id].playerid == 0 and "dwc_pid" in k: # Get the player's id and then query the profile to figure out what console they are on. # The endianness of some server data depends on the endianness of the console, so we must be able # to account for that. self.sessions[session_id].playerid = int(k['dwc_pid']) # Try to detect console without hitting the database first found_console = False if 'gamename' in k: self.sessions[session_id].console = 0 if k['gamename'].endswith('ds') or k['gamename'].endswith( 'dsam') or k['gamename'].endswith( 'dsi') or k['gamename'].endswith('dsiam'): self.sessions[session_id].console = 0 found_console = True elif k['gamename'].endswith( 'wii') or k['gamename'].endswith( 'wiiam') or k['gamename'].endswith( 'wiiware') or k['gamename'].endswith( 'wiiwaream'): self.sessions[session_id].console = 1 found_console = True if found_console == False: # Couldn't detect game, try to get it from the database # Try a 3 times before giving up for i in range(0, 3): try: profile = self.db.get_profile_from_profileid( self.sessions[session_id].playerid) if "console" in profile: self.sessions[session_id].console = profile[ 'console'] break except: time.sleep(0.5) if 'publicip' in k and k[ 'publicip'] == "0": #and k['dwc_hoststate'] == "2": # When dwc_hoststate == 2 then it doesn't send an IP, so calculate it ourselves be = self.sessions[session_id].console != 0 k['publicip'] = str( utils.get_ip( bytearray([int(x) for x in address[0].split('.')]), 0, be)) if 'publicport' in k and 'localport' in k and k['publicport'] != k[ 'localport']: self.log(logging.DEBUG, address, session_id, "publicport %s doesn't match localport %s, so changing publicport to %s..." \ % (k['publicport'], k['localport'], str(address[1]))) k['publicport'] = str(address[1]) if self.sessions[session_id].sent_challenge == True: self.update_server_list(session_id, k) else: addr_hex = ''.join( ["%02X" % int(x) for x in address[0].split('.')]) port_hex = "%04X" % int(address[1]) server_challenge = utils.generate_random_str( 6) + '00' + 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.write_queue.put((packet, address)) self.log( logging.DEBUG, address, session_id, "Sent challenge to %s:%s..." % (address[0], address[1])) self.sessions[session_id].sent_challenge = True self.sessions[session_id].heartbeat_data = k
def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'], self.db) #print authtoken_parsed # Track what console is connecting and save it in the database during user creation just in case we can use # the information in the future. console = 0 # 0 = NDS, 1 = Wii # get correct information userid = authtoken_parsed['userid'] # The Wii does not use passwd, so take another uniquely generated string as the password. if "passwd" in authtoken_parsed: password = authtoken_parsed['passwd'] else: password = authtoken_parsed['gsbrcd'] console = 1 gsbrcd = authtoken_parsed['gsbrcd'] gameid = gsbrcd[:4] uniquenick = utils.base32_encode(int(userid)) + gsbrcd email = uniquenick + "@nds" # The Wii also seems to use @nds. # Wii: Serial number if "csnum" in authtoken_parsed: csnum = authtoken_parsed['csnum'] console = 1 else: csnum = "" # Wii: Friend code if "cfc" in authtoken_parsed: cfc = authtoken_parsed['cfc'] console = 1 else: cfc = "" # NDS: Wifi network's BSSID if "bssid" in authtoken_parsed: bssid = authtoken_parsed['bssid'] else: bssid = "" # NDS: Device name if "devname" in authtoken_parsed: devname = authtoken_parsed['devname'] else: devname = "" # NDS: User's birthday if "birth" in authtoken_parsed: birth = authtoken_parsed['birth'] else: birth = "" # 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: self.log(logging.ERROR, "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']) valid_user = self.db.check_user_exists(userid, gsbrcd) if valid_user == False: profileid = self.db.create_user(userid, password, email, uniquenick, gsbrcd, console, csnum, cfc, bssid, devname, birth, gameid) else: profileid = self.db.perform_login(userid, password, gsbrcd) if profileid == None: # Handle case where the user is invalid self.log(logging.ERROR, "Invalid password") if profileid != None: # Successfully logged in or created account, continue creating session. loginticket = gs_utils.base64_encode(utils.generate_random_str(16)) self.sesskey = self.db.create_session(profileid, loginticket) self.sessions[profileid] = self msg_d = [] msg_d.append(('__cmd__', "lc")) msg_d.append(('__cmd_val__', "2")) msg_d.append(('sesskey', self.sesskey)) msg_d.append(('proof', proof)) msg_d.append(('userid', userid)) msg_d.append(('profileid', profileid)) msg_d.append(('uniquenick', uniquenick)) msg_d.append(('lt', loginticket)) # Some kind of token... don't know it gets used or generated, but it doesn't seem to have any negative effects if it's not properly generated. msg_d.append(('id', data_parsed['id'])) msg = gs_query.create_gamespy_message(msg_d) # Take the first 4 letters of gsbrcd instead of gamecd because they should be consistent across game # regions. For example, the US version of Metroid Prime Hunters has the gamecd "AMHE" and the first 4 letters # of gsbrcd are "AMHE". However, the Japanese version of Metroid Prime Hunters has the gamecd "AMHJ" with # the first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way, with the first 4 letters as the # Japanese version (ATRJ) while the gamecd is region specific (ATRE for US and ATRJ for JP). # gameid is used to send all people on the player's friends list a status updates, so don't make it region # specific. self.gameid = gsbrcd[0:4] self.profileid = int(profileid) self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) self.buddies = self.db.get_buddy_list(self.profileid) self.blocked = self.db.get_blocked_list(self.profileid) # Get pending messages. self.get_pending_messages() # Send any friend statuses when the user logs in. # This will allow the user to see if their friends are hosting a game as soon as they log in. self.get_status_from_friends() self.send_status_to_friends()
def do_POST(self): try: length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "retry": "0" } action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") if action == "acctcreate": # TODO: test for duplicate accounts ret["returncd"] = "002" logger.log(logging.DEBUG, "acctcreate response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "login": challenge = utils.generate_random_str(8) post["challenge"] = challenge authtoken = self.server.db.generate_authtoken(post["userid"], post) ret.update({ "returncd": "001", "locator": "gamespy.com", "challenge": challenge, "token": authtoken, }) logger.log(logging.DEBUG, "login response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken(post["userid"], post) if 'svc' in post: if post["svc"] in ("9000", "9001"): # DLC host = 9000 ret["svchost"] = self.headers['host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work ret["svchost"] = ret["svchost"].split(',')[0] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post["svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) else: logger.log(logging.WARNING, "Unknown action request %s from %s!", self.path, self.client_address) elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) words = len(post["words"].split('\t')) wordsret = "0" * words ret = { "prwords": wordsret, "returncd": "000", "datetime": time.strftime("%Y%m%d%H%M%S") } for l in "ACEJKP": ret["prwords"+l] = wordsret self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") logger.log(logging.DEBUG, "pr response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcdir = os.path.abspath('dlc') dlcpath = os.path.abspath("dlc/" + post["gamecd"]) dlc_contenttype = False if os.path.commonprefix([dlcdir, dlcpath]) != dlcdir: logging.log(logging.WARNING, 'Attempted directory traversal attack "%s", cancelling.', dlcpath) self.send_response(403) return def safeloadfi(fn, mode='rb'): ''' safeloadfi : string -> string Safely load contents of a file, given a filename, and closing the file afterward ''' with open(os.path.join(dlcpath, fn), mode) as fi: return fi.read() if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) dlcfi = safeloadfi("_list.txt") lst = self.filter_list(dlcfi, attr1, attr2, attr3) count = self.get_file_count(lst) ret = "%d" % count if action == "list": num = int(post["num"]) offset = int(post["offset"]) attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(os.path.join(dlcpath, "_list.txt")): ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) # this is super game-specific, but we need to handle gen 5 mystery gifts properly somehow if post["gamecd"].startswith("IRA") and attr1.startswith("MYSTERY"): ret = self.filter_list_g5_mystery_gift(ret, post["rhgamecd"]) ret = self.filter_list_by_date(ret, post["token"]) if post["gamecd"] in gamecodes_return_random_file: ret = self.filter_list_by_date(ret, post["token"]) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) ret = safeloadfi(contents) self.send_response(200) if dlc_contenttype == True: self.send_header("Content-type", "application/x-dsdl") self.send_header("Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") logger.log(logging.DEBUG, "download response to %s", self.client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) else: logger.log(logging.WARNING, "Unknown path request %s from %s!", self.path, self.client_address) self.send_header("Content-Length", str(len(ret))) self.end_headers() self.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
def do_POST(self): length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} ret["datetime"] = time.strftime("%Y%m%d%H%M%S") ret["retry"] = "0" action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("Server", "Nintendo Wii (http)") self.send_header("NODE", "wifiappe3") self.end_headers() if action == "acctcreate": # TODO: test for duplicate accounts ret["returncd"] = "002" logger.log(logging.DEBUG, "acctcreate response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif action == "login": ret["returncd"] = "001" ret["locator"] = "gamespy.com" challenge = utils.generate_random_str(8) ret["challenge"] = challenge post["challenge"] = challenge authtoken = self.server.db.generate_authtoken(post["userid"], post) ret["token"] = authtoken logger.log(logging.DEBUG, "login response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken(post["userid"], post) if 'svc' in post: if post["svc"] == "9000" or post["svc"] == "9001": # DLC host = 9000 ret["svchost"] = self.headers['host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work cindex = ret["svchost"].find(',') if cindex != -1: ret["svchost"] = ret["svchost"][:cindex] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post["svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} words = len(post["words"].split('\t')) wordsret = "0" * words ret["prwords"] = wordsret ret["prwordsA"] = wordsret ret["prwordsC"] = wordsret ret["prwordsE"] = wordsret ret["prwordsJ"] = wordsret ret["prwordsK"] = wordsret ret["prwordsP"] = wordsret ret["returncd"] = "000" ret["datetime"] = time.strftime("%Y%m%d%H%M%S") self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("Server", "Nintendo Wii (http)") self.send_header("NODE", "wifiappe3") self.end_headers() logger.log(logging.DEBUG, "pr response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcpath = "dlc/" + post["gamecd"] dlc_contenttype = False if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = None if "attr1" in post: attr1 = post["attr1"] attr2 = None if "attr2" in post: attr2 = post["attr2"] attr3 = None if "attr3" in post: attr3 = post["attr3"] list = open(dlcpath + "/_list.txt", "rb").read() list = self.filter_list(list, attr1, attr2, attr3) count = self.get_file_count(list) ret = "%d" % count if action == "list": num = int(post["num"]) offset = int(post["offset"]) attr1 = None if "attr1" in post: attr1 = post["attr1"] attr2 = None if "attr2" in post: attr2 = post["attr2"] attr3 = None if "attr3" in post: attr3 = post["attr3"] if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(dlcpath + "/_list.txt"): ret = open(dlcpath + "/_list.txt", "rb").read() ret = self.filter_list(ret, attr1, attr2, attr3) if post["gamecd"] in gamecodes_return_random_file: ret = self.filter_list_random_files(ret, 1) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) if os.path.isfile(dlcpath + "/" + contents): ret = open(dlcpath + "/" + contents, "rb").read() self.send_response(200) if dlc_contenttype == True: self.send_header("Content-Length", str(len(ret))) self.send_header("Content-type", "application/x-dsdl") self.send_header("Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") self.end_headers() logger.log(logging.DEBUG, "download response to %s", self.client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) self.wfile.write(ret)
def do_POST(self): length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} ret["datetime"] = time.strftime("%Y%m%d%H%M%S") ret["retry"] = "0" action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("Server", "Nintendo Wii (http)") self.send_header("NODE", "wifiappe3") self.end_headers() if action == "acctcreate": # TODO: test for duplicate accounts ret["returncd"] = "002" logger.log(logging.DEBUG, "acctcreate response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif action == "login": ret["returncd"] = "001" ret["locator"] = "gamespy.com" challenge = utils.generate_random_str(8) ret["challenge"] = challenge post["challenge"] = challenge authtoken = self.server.db.generate_authtoken( post["userid"], post) ret["token"] = authtoken logger.log(logging.DEBUG, "login response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken( post["userid"], post) if 'svc' in post: if post["svc"] == "9000" or post[ "svc"] == "9001": # DLC host = 9000 ret["svchost"] = self.headers[ 'host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work cindex = ret["svchost"].find(',') if cindex != -1: ret["svchost"] = ret["svchost"][:cindex] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post[ "svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {} words = len(post["words"].split('\t')) wordsret = "0" * words ret["prwords"] = wordsret ret["prwordsA"] = wordsret ret["prwordsC"] = wordsret ret["prwordsE"] = wordsret ret["prwordsJ"] = wordsret ret["prwordsK"] = wordsret ret["prwordsP"] = wordsret ret["returncd"] = "000" ret["datetime"] = time.strftime("%Y%m%d%H%M%S") self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("Server", "Nintendo Wii (http)") self.send_header("NODE", "wifiappe3") self.end_headers() logger.log(logging.DEBUG, "pr response to %s", self.client_address) logger.log(logging.DEBUG, ret) self.wfile.write(self.dict_to_str(ret)) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcpath = "dlc/" + post["gamecd"] dlc_contenttype = False if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = None if "attr1" in post: attr1 = post["attr1"] attr2 = None if "attr2" in post: attr2 = post["attr2"] attr3 = None if "attr3" in post: attr3 = post["attr3"] list = open(dlcpath + "/_list.txt", "rb").read() list = self.filter_list(list, attr1, attr2, attr3) count = self.get_file_count(list) ret = "%d" % count if action == "list": num = int(post["num"]) offset = int(post["offset"]) attr1 = None if "attr1" in post: attr1 = post["attr1"] attr2 = None if "attr2" in post: attr2 = post["attr2"] attr3 = None if "attr3" in post: attr3 = post["attr3"] if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(dlcpath + "/_list.txt"): ret = open(dlcpath + "/_list.txt", "rb").read() ret = self.filter_list(ret, attr1, attr2, attr3) if post["gamecd"] in gamecodes_return_random_file: ret = self.filter_list_random_files(ret, 1) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) if os.path.isfile(dlcpath + "/" + contents): ret = open(dlcpath + "/" + contents, "rb").read() self.send_response(200) if dlc_contenttype == True: self.send_header("Content-Length", str(len(ret))) self.send_header("Content-type", "application/x-dsdl") self.send_header( "Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") self.end_headers() logger.log(logging.DEBUG, "download response to %s", self.client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) self.wfile.write(ret)
def do_POST(self): self.server = lambda:None self.server.db = gs_database.GamespyDatabase() try: length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) if self.client_address[0] == '127.0.0.1': client_address = (self.headers.get('x-forwarded-for', self.client_address[0]), self.client_address[1]) else: client_address = self.client_address post['ipaddr'] = client_address[0] if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address) logger.log(logging.DEBUG, post) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "retry": "0" } action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") if action == "acctcreate": # TODO: test for duplicate accounts if self.server.db.is_ip_banned(post): logger.log(logging.DEBUG, "acctcreate denied for banned user "+str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3913", "locator": "gamespy.com", "retry": "1", "reason": "User banned." } else: ret["returncd"] = "002" ret['userid'] = self.server.db.get_next_available_userid() logger.log(logging.DEBUG, "acctcreate response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "login": if self.server.db.is_ip_banned(post): logger.log(logging.DEBUG, "login denied for banned user "+str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3917", "locator": "gamespy.com", "retry": "1", "reason": "User banned." } elif self.server.db.is_console_macadr_banned(post): logger.log(logging.DEBUG, "login denied for banned console"+str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3914", "locator": "gamespy.com", "retry": "1", "reason": "User's console is banned." } elif self.server.db.is_console_cfc_banned(post): logger.log(logging.DEBUG, "login denied for banned console"+str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3915", "locator": "gamespy.com", "retry": "1", "reason": "User's console is banned." } elif self.server.db.is_console_csnum_banned(post): logger.log(logging.DEBUG, "login denied for banned console"+str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3915", "locator": "gamespy.com", "retry": "1", "reason": "User's console is banned." } #Un0comment the lines below to activate console registration #elif not self.server.db.pending(post): #logger.log(logging.DEBUG, "Login denied - Unknown console"+str(post)) #ret = { #"datetime": time.strftime("%Y%m%d%H%M%S"), #"returncd": "3921", #"locator": "gamespy.com", #"retry": "1", #} #elif not self.server.db.registered(post): #logger.log(logging.DEBUG, "Login denied - console pending"+str(post)) #ret = { #"datetime": time.strftime("%Y%m%d%H%M%S"), #"returncd": "3888", #"locator": "gamespy.com", #"retry": "1", #} else: challenge = utils.generate_random_str(8) post["challenge"] = challenge authtoken = self.server.db.generate_authtoken(post["userid"], post) ret.update({ "returncd": "001", "locator": "gamespy.com", "challenge": challenge, "token": authtoken, }) logger.log(logging.DEBUG, "login response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken(post["userid"], post) if 'svc' in post: if post["svc"] in ("9000", "9001"): # DLC host = 9000 ret["svchost"] = self.headers['host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work ret["svchost"] = ret["svchost"].split(',')[0] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post["svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) else: logger.log(logging.WARNING, "Unknown action request %s from %s!", self.path, client_address) elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address) logger.log(logging.DEBUG, post) words = len(post["words"].split('\t')) wordsret = "0" * words ret = { "prwords": wordsret, "returncd": "000", "datetime": time.strftime("%Y%m%d%H%M%S") } for l in "ACEJKP": ret["prwords"+l] = wordsret self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") logger.log(logging.DEBUG, "pr response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcdir = os.path.abspath('dlc') dlcpath = os.path.abspath("dlc/" + post["gamecd"]) dlc_contenttype = False if os.path.commonprefix([dlcdir, dlcpath]) != dlcdir: logging.log(logging.WARNING, 'Attempted directory traversal attack "%s", cancelling.', dlcpath) self.send_response(403) return def safeloadfi(fn, mode='rb'): ''' safeloadfi : string -> string Safely load contents of a file, given a filename, and closing the file afterward ''' with open(os.path.join(dlcpath, fn), mode) as fi: return fi.read() if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) dlcfi = safeloadfi("_list.txt") lst = self.filter_list(dlcfi, attr1, attr2, attr3) count = self.get_file_count(lst) ret = "%d" % count if action == "list": num = post.get("num", None) offset = post.get("offset", None) if num != None: num = int(num) if offset != None: offset = int(offset) attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(os.path.join(dlcpath, "_list.txt")): if post["gamecd"].startswith("IRA") and attr1.startswith("MYSTERY"): # Pokemon BW Mystery Gifts, until we have a better solution for that ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) ret = self.filter_list_g5_mystery_gift(ret, post["rhgamecd"]) ret = self.filter_list_by_date(ret, post["token"]) elif post["gamecd"] in gamecodes_return_random_file: # Pokemon Gen 4 Mystery Gifts, same here ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) ret = self.filter_list_by_date(ret, post["token"]) else: # default case for most games ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3, num, offset) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) ret = safeloadfi(contents) self.send_response(200) if dlc_contenttype == True: self.send_header("Content-type", "application/x-dsdl") self.send_header("Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") logger.log(logging.DEBUG, "download response to %s", client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) else: self.send_response(404) logger.log(logging.WARNING, "Unknown path request %s from %s!", self.path, client_address) return self.send_header("Content-Length", str(len(ret))) self.end_headers() self.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
def do_POST(self): try: length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) ret = {"datetime": time.strftime("%Y%m%d%H%M%S"), "retry": "0"} action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") if action == "acctcreate": # TODO: test for duplicate accounts ret["returncd"] = "002" logger.log(logging.DEBUG, "acctcreate response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "login": challenge = utils.generate_random_str(8) post["challenge"] = challenge authtoken = self.server.db.generate_authtoken( post["userid"], post) ret.update({ "returncd": "001", "locator": "gamespy.com", "challenge": challenge, "token": authtoken, }) logger.log(logging.DEBUG, "login response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken( post["userid"], post) if 'svc' in post: if post["svc"] in ("9000", "9001"): # DLC host = 9000 ret["svchost"] = self.headers[ 'host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work ret["svchost"] = ret["svchost"].split(',')[0] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post[ "svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) else: logger.log(logging.WARNING, "Unknown action request %s from %s!", self.path, self.client_address) elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) words = len(post["words"].split('\t')) wordsret = "0" * words ret = { "prwords": wordsret, "returncd": "000", "datetime": time.strftime("%Y%m%d%H%M%S") } for l in "ACEJKP": ret["prwords" + l] = wordsret self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") logger.log(logging.DEBUG, "pr response to %s", self.client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, self.client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcdir = os.path.abspath('dlc') dlcpath = os.path.abspath("dlc/" + post["gamecd"]) dlc_contenttype = False if os.path.commonprefix([dlcdir, dlcpath]) != dlcdir: logging.log( logging.WARNING, 'Attempted directory traversal attack "%s", cancelling.', dlcpath) self.send_response(403) return def safeloadfi(fn, mode='rb'): ''' safeloadfi : string -> string Safely load contents of a file, given a filename, and closing the file afterward ''' with open(os.path.join(dlcpath, fn), mode) as fi: return fi.read() if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) dlcfi = safeloadfi("_list.txt") lst = self.filter_list(dlcfi, attr1, attr2, attr3) count = self.get_file_count(lst) ret = "%d" % count if action == "list": num = int(post["num"]) offset = int(post["offset"]) attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(os.path.join(dlcpath, "_list.txt")): ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) # Don't try to filter any further if the list is empty. if ret.strip() != "": # this is super game-specific, but we need to handle gen 5 mystery gifts properly somehow if post["gamecd"].startswith( "IRA") and attr1.startswith("MYSTERY"): ret = self.filter_list_g5_mystery_gift( ret, post["rhgamecd"]) ret = self.filter_list_by_date( ret, post["token"]) if post["gamecd"] in gamecodes_return_random_file: ret = self.filter_list_by_date( ret, post["token"]) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) ret = safeloadfi(contents) self.send_response(200) if dlc_contenttype == True: self.send_header("Content-type", "application/x-dsdl") self.send_header( "Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") logger.log(logging.DEBUG, "download response to %s", self.client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) else: logger.log(logging.WARNING, "Unknown path request %s from %s!", self.path, self.client_address) self.send_header("Content-Length", str(len(ret))) self.end_headers() self.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
def do_POST(self): self.server = lambda: None self.server.db = gs_database.GamespyDatabase() try: length = int(self.headers['content-length']) post = self.str_to_dict(self.rfile.read(length)) if self.client_address[0] == '127.0.0.1': client_address = (self.headers.get('x-forwarded-for', self.client_address[0]), self.client_address[1]) else: client_address = self.client_address post['ipaddr'] = client_address[0] if self.path == "/ac": logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address) logger.log(logging.DEBUG, post) ret = {"datetime": time.strftime("%Y%m%d%H%M%S"), "retry": "0"} action = post["action"] self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") if action == "acctcreate": # TODO: test for duplicate accounts if self.server.db.is_ip_banned(post): logger.log( logging.DEBUG, "acctcreate denied for banned user " + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3913", "locator": "gamespy.com", "retry": "1", "reason": "User banned." } else: ret["returncd"] = "002" ret['userid'] = self.server.db.get_next_available_userid( ) logger.log(logging.DEBUG, "acctcreate response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "login": if self.server.db.is_ip_banned(post): logger.log(logging.DEBUG, "login denied for banned user " + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3917", "locator": "gamespy.com", "retry": "1", "reason": "User banned." } elif self.server.db.is_console_macadr_banned(post): logger.log( logging.DEBUG, "login denied for banned console" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3914", "locator": "gamespy.com", "retry": "1", "reason": "User's console is banned." } elif self.server.db.is_console_cfc_banned(post): logger.log( logging.DEBUG, "login denied for banned console" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3915", "locator": "gamespy.com", "retry": "1", "reason": "User's console is banned." } elif self.server.db.is_console_csnum_banned(post): logger.log( logging.DEBUG, "login denied for banned console" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3915", "locator": "gamespy.com", "retry": "1", "reason": "User's console is banned." } elif self.server.db.is_ingamesn_banned(post): logger.log( logging.DEBUG, "login denied for banned Mii name" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3916", "locator": "gamespy.com", "retry": "1", "reason": "Mii name is black listed" } elif self.server.db.is_devname_banned(post): logger.log( logging.DEBUG, "login denied for banned device name" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3916", "locator": "gamespy.com", "retry": "1", "reason": "device name is black listed" } #Un0comment the lines below to activate console registration elif not self.server.db.pending(post): logger.log( logging.DEBUG, "Login denied - Unknown console" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3921", "locator": "gamespy.com", "retry": "1", } elif not self.server.db.registered(post): logger.log( logging.DEBUG, "Login denied - console pending" + str(post)) ret = { "datetime": time.strftime("%Y%m%d%H%M%S"), "returncd": "3888", "locator": "gamespy.com", "retry": "1", } else: challenge = utils.generate_random_str(8) post["challenge"] = challenge authtoken = self.server.db.generate_authtoken( post["userid"], post) ret.update({ "returncd": "001", "locator": "gamespy.com", "challenge": challenge, "token": authtoken, }) logger.log(logging.DEBUG, "login response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number ret["returncd"] = "007" ret["statusdata"] = "Y" authtoken = self.server.db.generate_authtoken( post["userid"], post) if 'svc' in post: if post["svc"] in ("9000", "9001"): # DLC host = 9000 ret["svchost"] = self.headers[ 'host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net # Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work ret["svchost"] = ret["svchost"].split(',')[0] if post["svc"] == 9000: ret["token"] = authtoken else: ret["servicetoken"] = authtoken elif post[ "svc"] == "0000": # Pokemon requests this for some things ret["servicetoken"] = authtoken ret["svchost"] = "n/a" logger.log(logging.DEBUG, "svcloc response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) else: logger.log(logging.WARNING, "Unknown action request %s from %s!", self.path, client_address) elif self.path == "/pr": logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address) logger.log(logging.DEBUG, post) words = len(post["words"].split('\t')) wordsret = "0" * words ret = { "prwords": wordsret, "returncd": "000", "datetime": time.strftime("%Y%m%d%H%M%S") } for l in "ACEJKP": ret["prwords" + l] = wordsret self.send_response(200) self.send_header("Content-type", "text/plain") self.send_header("NODE", "wifiappe1") logger.log(logging.DEBUG, "pr response to %s", client_address) logger.log(logging.DEBUG, ret) ret = self.dict_to_str(ret) elif self.path == "/download": logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address) logger.log(logging.DEBUG, post) action = post["action"] ret = "" dlcdir = os.path.abspath('dlc') dlcpath = os.path.abspath("dlc/" + post["gamecd"]) dlc_contenttype = False if os.path.commonprefix([dlcdir, dlcpath]) != dlcdir: logging.log( logging.WARNING, 'Attempted directory traversal attack "%s", cancelling.', dlcpath) self.send_response(403) return def safeloadfi(fn, mode='rb'): ''' safeloadfi : string -> string Safely load contents of a file, given a filename, and closing the file afterward ''' with open(os.path.join(dlcpath, fn), mode) as fi: return fi.read() if action == "count": if post["gamecd"] in gamecodes_return_random_file: ret = "1" else: count = 0 if os.path.exists(dlcpath): count = len(os.listdir(dlcpath)) if os.path.isfile(dlcpath + "/_list.txt"): attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) dlcfi = safeloadfi("_list.txt") lst = self.filter_list(dlcfi, attr1, attr2, attr3) count = self.get_file_count(lst) ret = "%d" % count if action == "list": num = post.get("num", None) offset = post.get("offset", None) if num != None: num = int(num) if offset != None: offset = int(offset) attr1 = post.get("attr1", None) attr2 = post.get("attr2", None) attr3 = post.get("attr3", None) if os.path.exists(dlcpath): # Look for a list file first. # If the list file exists, send the entire thing back to the client. if os.path.isfile(os.path.join(dlcpath, "_list.txt")): if post["gamecd"].startswith( "IRA") and attr1.startswith("MYSTERY"): # Pokemon BW Mystery Gifts, until we have a better solution for that ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) ret = self.filter_list_g5_mystery_gift( ret, post["rhgamecd"]) ret = self.filter_list_by_date( ret, post["token"]) elif post[ "gamecd"] in gamecodes_return_random_file: # Pokemon Gen 4 Mystery Gifts, same here ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3) ret = self.filter_list_by_date( ret, post["token"]) else: # default case for most games ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3, num, offset) if action == "contents": # Get only the base filename just in case there is a path involved somewhere in the filename string. dlc_contenttype = True contents = os.path.basename(post["contents"]) ret = safeloadfi(contents) self.send_response(200) if dlc_contenttype == True: self.send_header("Content-type", "application/x-dsdl") self.send_header( "Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"") else: self.send_header("Content-type", "text/plain") self.send_header("X-DLS-Host", "http://127.0.0.1/") logger.log(logging.DEBUG, "download response to %s", client_address) #if dlc_contenttype == False: # logger.log(logging.DEBUG, ret) else: self.send_response(404) logger.log(logging.WARNING, "Unknown path request %s from %s!", self.path, client_address) return self.send_header("Content-Length", str(len(ret))) self.end_headers() self.wfile.write(ret) except: logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
def perform_login(self, data_parsed): authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'], self.db) if authtoken_parsed == None: self.log(logging.WARNING, "Invalid Authtoken.") msg = gs_query.create_gamespy_message([ ('__cmd__', "error"), ('__cmd_val__', ""), ('err', '266'), ('fatal', ''), ('errmsg', 'There was an error validating the pre-authentication.'), ('id', data_parsed['id']), ]) self.transport.write(bytes(msg)) return if 'sdkrevision' in data_parsed: self.sdkrevision = data_parsed['sdkrevision'] # 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: self.log(logging.ERROR, "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']) userid, profileid, gsbrcd, uniquenick = gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed, self.db) if profileid != None: # Successfully logged in or created account, continue creating session. loginticket = gs_utils.base64_encode(utils.generate_random_str(16)) self.sesskey = self.db.create_session(profileid, loginticket) self.sessions[profileid] = self self.buddies = self.db.get_buddy_list(self.profileid) self.blocked = self.db.get_blocked_list(self.profileid) if self.sdkrevision == "11": # Used in Tatsunoko vs Capcom def make_list(data): list = [] for d in data: if d['status'] == 1: list.append(str(d['buddyProfileId'])) return list block_list = make_list(self.blocked) msg = gs_query.create_gamespy_message([ ('__cmd__', "blk"), ('__cmd_val__', str(len(block_list))), ('list', ','.join(block_list)), ]) self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) buddy_list = make_list(self.buddies) msg = gs_query.create_gamespy_message([ ('__cmd__', "bdy"), ('__cmd_val__', str(len(buddy_list))), ('list', ','.join(buddy_list)), ]) self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) msg = gs_query.create_gamespy_message([ ('__cmd__', "lc"), ('__cmd_val__', "2"), ('sesskey', self.sesskey), ('proof', proof), ('userid', userid), ('profileid', profileid), ('uniquenick', uniquenick), # Some kind of token... don't know it gets used or generated, but it doesn't seem to have any negative effects if it's not properly generated. ('lt', loginticket), ('id', data_parsed['id']), ]) # Take the first 4 letters of gsbrcd instead of gamecd because they should be consistent across game # regions. For example, the US version of Metroid Prime Hunters has the gamecd "AMHE" and the first 4 letters # of gsbrcd are "AMHE". However, the Japanese version of Metroid Prime Hunters has the gamecd "AMHJ" with # the first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way, with the first 4 letters as the # Japanese version (ATRJ) while the gamecd is region specific (ATRE for US and ATRJ for JP). # gameid is used to send all people on the player's friends list a status updates, so don't make it region # specific. self.gameid = gsbrcd[:4] self.profileid = int(profileid) self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg)) # Get pending messages. self.get_pending_messages() # Send any friend statuses when the user logs in. # This will allow the user to see if their friends are hosting a game as soon as they log in. self.get_status_from_friends() self.send_status_to_friends() # profile = self.db.get_profile_from_profileid(profileid) # if profile != None: # self.statstring = profile['stat'] # self.locstring = profile['loc'] else: self.log(logging.INFO, "Invalid password or banned user") msg = gs_query.create_gamespy_message([ ('__cmd__', "error"), ('__cmd_val__', ""), ('err', '256'), ('fatal', ''), ('errmsg', 'Login failed.'), ('id', data_parsed['id']), ]) self.log(logging.DEBUG, "SENDING: %s" % msg) self.transport.write(bytes(msg))
def handle_packet(self, socket, recv_data, address): # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # # After some more packet inspection, it seems the format goes something like this: # - All server messages seem to always start with \xfe\xfd. # - The first byte from the client (or third byte from the server) is a command. # - Bytes 2 - 5 from the client is some kind of ID. This will have to be inspected later. I believe it's a # session-like ID because the number changes between connections. Copying the client's ID might be enough. # # The above was as guessed. # The code in Tetris DS (overlay 10) @ 0216E974 handles the network command creation. # R1 contains the command to be sent to the server. # R2 contains a pointer to some unknown integer that gets written after the command. # # - Commands # Commands range from 0x00 to 0x09 (for client only at least?) (Tetris DS overlay 10 @ 0216DDCC) # # CLIENT: # 0x01 - Response (Tetris DS overlay 10 @ 216DCA4) # Sends back base64 of RC4 encrypted string that was gotten from the server's 0x01. # # 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30) # Data sent: # 1) Loop for each localip available on the system, write as localip%d\x00(local ip) # 2) localport\x00(local port) # 3) natneg (either 0 or 1) # 4) ONLY IF STATE CHANGED: statechanged\x00(state) (Possible values: 0, 1, 2, 3) # 5) gamename\x00(game name) # 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicip\x00(public ip) # 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicport\x00(public port) # # if statechanged != 2: # Write various other data described here: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation # # 0x07 - Unknown, related to server's 0x06 (returns value sent from server) # # 0x08 - Keep alive? Sent after 0x03 # # 0x09 - Availability check # # SERVER: # 0x01 - Unknown # Data sent: # 8 random ASCII characters (?) followed by the public IP and port of the client as a hex string # # 0x06 - Unknown # First 4 bytes is some kind of id? I believe it's a unique identifier for the data being sent, # seeing how the server can send the same IP information many times in a row. If the IP information has # already been parsed then it doesn't waste time handling it. # # After that is a "SBCM" section which is 0x14 bytes in total. # SBCM information gets parsed at 2141A0C in Tetris DS overlay 10. # Seems to contain IP address information. # # The SBCM seems to contain a little information that must be parsed before. # After the SBCM: # \x03\x00\x00\x00 - Always the same? # \x01 - Found player? # \x04 - Unknown # (2 bytes) - Unknown. Port? # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # \x00\x00\x00\x00 - Unknown but seems to get checked # # Another SBCM, after a player has been found and attempting to start a game: # \x03\x00\x00\x00 - Always the same? # \x05 - Connecting to player? # \x00 - Unknown # (2 bytes) - Unknown. Port? Same as before. # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # # 0x0a - Response to 0x01 # Gets sent after receiving 0x01 from the client. So, server 0x01 -> client 0x01 -> server 0x0a. # Has no other data besides the client ID. # # - \xfd\xfc commands get passed directly between the other player(s)? # # # Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr # Use as reference. session_id = None if recv_data[0] != '\x09': # Don't add a session if the client is trying to check if the game is available or not 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) self.sessions[session_id].session = session_id self.sessions[session_id].keepalive = int(time.time()) self.sessions[session_id].disconnected = False if session_id in self.sessions and self.sessions[session_id].disconnected == True: return if session_id in self.sessions: self.sessions[session_id].keepalive = int(time.time()) # Make sure the server doesn't get removed # Handle commands if recv_data[0] == '\x00': # Query self.log(logging.DEBUG, address, session_id, "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, session_id, "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 # Send message back to client saying it was accepted packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command packet.extend(session_id_raw) # Get the session ID self.write_queue.put((packet, address)) self.log(logging.DEBUG, address, session_id, "Sent client registered to %s:%s..." % (address[0], address[1])) if self.sessions[session_id].heartbeat_data != None: self.update_server_list(session_id, self.sessions[session_id].heartbeat_data) else: # Failed the challenge, request another during the next heartbeat self.sessions[session_id].sent_challenge = False self.server_manager.delete_server(self.sessions[session_id].gamename, session_id) elif recv_data[0] == '\x02': # Echo self.log(logging.DEBUG, address, session_id, "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, session_id, "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, session_id, "%s = %s" % (d[i], d[i+1])) k[d[i]] = d[i+1] if self.sessions[session_id].ingamesn != None: if "gamename" in k and "dwc_pid" in k: try: profile = self.db.get_profile_from_profileid(k['dwc_pid']) naslogin = self.db.get_nas_login_from_userid(profile['userid']) self.sessions[session_id].ingamesn = str(naslogin['ingamesn']) # convert to string from unicode(which is just a base64 string anyway) except Exception,e: pass # If the game doesn't have, don't worry about it. if self.sessions[session_id].ingamesn != None and "ingamesn" not in k: k['ingamesn'] = self.sessions[session_id].ingamesn if "gamename" in k: if k['gamename'] in self.secret_key_list: self.sessions[session_id].secretkey = self.secret_key_list[k['gamename']] else: self.log(logging.INFO, address, session_id, "Connection from unknown game '%s'!" % k['gamename']) if self.sessions[session_id].playerid == 0 and "dwc_pid" in k: # Get the player's id and then query the profile to figure out what console they are on. # The endianness of some server data depends on the endianness of the console, so we must be able # to account for that. self.sessions[session_id].playerid = int(k['dwc_pid']) # Try to detect console without hitting the database first found_console = False if 'gamename' in k: self.sessions[session_id].console = 0 if k['gamename'].endswith('ds') or k['gamename'].endswith('dsam') or k['gamename'].endswith('dsi') or k['gamename'].endswith('dsiam'): self.sessions[session_id].console = 0 found_console = True elif k['gamename'].endswith('wii') or k['gamename'].endswith('wiiam') or k['gamename'].endswith('wiiware') or k['gamename'].endswith('wiiwaream'): self.sessions[session_id].console = 1 found_console = True if found_console == False: # Couldn't detect game, try to get it from the database # Try a 3 times before giving up for i in range(0, 3): try: profile = self.db.get_profile_from_profileid(self.sessions[session_id].playerid) if "console" in profile: self.sessions[session_id].console = profile['console'] break except: time.sleep(0.5) if 'publicip' in k and k['publicip'] == "0": #and k['dwc_hoststate'] == "2": # When dwc_hoststate == 2 then it doesn't send an IP, so calculate it ourselves be = self.sessions[session_id].console != 0 k['publicip'] = str(utils.get_ip(bytearray([int(x) for x in address[0].split('.')]), 0, be)) if 'publicport' in k and 'localport' in k and k['publicport'] != k['localport']: self.log(logging.DEBUG, address, session_id, "publicport %s doesn't match localport %s, so changing publicport to %s..." \ % (k['publicport'], k['localport'], str(address[1]))) k['publicport'] = str(address[1]) if self.sessions[session_id].sent_challenge == True: self.update_server_list(session_id, k) else: addr_hex = ''.join(["%02X" % int(x) for x in address[0].split('.')]) port_hex = "%04X" % int(address[1]) server_challenge = utils.generate_random_str(6) + '00' + 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.write_queue.put((packet, address)) self.log(logging.DEBUG, address, session_id, "Sent challenge to %s:%s..." % (address[0], address[1])) self.sessions[session_id].sent_challenge = True self.sessions[session_id].heartbeat_data = k
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. if recv_data[0] != '\x09': # Don't add a session if the client is trying to check if the game is available or not 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) self.sessions[session_id].session = session_id self.sessions[session_id].keepalive = int(time.time()) # 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 # Send message back to client saying it was accepted 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])) else: # Failed the challenge, request another during the next heartbeat self.sessions[session_id].sent_challenge = False 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].playerid == 0 and "dwc_pid" in k: # Get the player's id and then query the profile to figure out what console they are on. # The endianness of some server data depends on the endianness of the console, so we must be able # to account for that. self.sessions[session_id].playerid = int(k['dwc_pid']) profile = self.db.get_profile_from_profileid(self.sessions[session_id].playerid) if "console" in profile: self.sessions[session_id].console = profile['console'] 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])) self.sessions[session_id].sent_challenge = True if 'publicip' in k and k['publicip'] == "0": #and k['dwc_hoststate'] == "2": # When dwc_hoststate == 2 then it doesn't send an IP, so calculate it ourselves if self.sessions[session_id].console != 0: k['publicip'] = str(ctypes.c_int32(utils.get_int_be(bytearray([int(x) for x in address[0].split('.')]), 0)).value) # Wii else: k['publicip'] = str(ctypes.c_int32(utils.get_int(bytearray([int(x) for x in address[0].split('.')]), 0)).value) # DS if "statechanged" in k: if k['statechanged'] == "1": # Create server #if k['publicport'] != "0" and k['publicip'] != "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, self.sessions[session_id].console)._getvalue() if session_id in self.sessions: self.sessions[session_id].gamename = k['gamename'] elif k['statechanged'] == "2": # Close server self.server_manager.delete_server(k['gamename'] , session_id) if session_id in self.sessions: self.sessions.pop(session_id) elif recv_data[0] == '\x04': # Add Error self.log(logging.WARNING, 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.WARNING, 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.WARNING, 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.WARNING, address, "NOT IMPLEMENTED! Received client message ack from %s:%s... %s" % (address[0], address[1], recv_data[5:])) self.log(logging.DEBUG, address, "Received client message ack from %s:%s..." % (address[0], address[1])) elif recv_data[0] == '\x08': # Keep Alive self.log(logging.DEBUG, address, "Received keep alive from %s:%s..." % (address[0], address[1])) self.sessions[session_id].keepalive = int(time.time()) 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.WARNING, address, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:])) else: self.log(logging.ERROR, address, "Unknown request from %s:%s:" % (address[0], address[1])) self.log(logging.DEBUG, address, utils.pretty_print_hex(recv_data))
def handle_ac_login(handler, db, addr, post): """Handle ac login request.""" if db.is_ip_banned(post): ret = { "retry": "1", "returncd": "3917", "locator": "gamespy.com", "reason": "User banned." } logger.log(logging.DEBUG, "Login denied for banned user %s", str(post)) elif db.is_console_macadr_banned(post): ret = { "retry": "1", "returncd": "3914", "locator": "gamespy.com", "reason": "User's console banned." } logger.log(logging.DEBUG, "Login denied for " "banned console %s", str(post)) elif db.is_console_cfc_banned(post): ret = { "retry": "1", "returncd": "3915", "locator": "gamespy.com", "reason": "User's console banned." } logger.log(logging.DEBUG, "Login denied " "for banned console " "friend code %s", str(post)) elif db.is_console_csnum_banned(post): ret = { "retry": "1", "returncd": "3915", "locator": "gamespy.com", "reason": "User's console banned." } logger.log(logging.DEBUG, "Login denied for banned console " "serial number %s", str(post)) elif db.console_abuse(post): ret = { "retry": "1", "returncd": "3915", "locator": "gamespy.com", "reason": "User's console banned due to abuse of MAC address." } logger.log( logging.DEBUG, "Login denied for console " "- too many MAC addresses used %s", str(post)) elif not db.pending(post): logger.log(logging.DEBUG, "Login denied - Unknown console %s", post) ret = { "retry": "1", "returncd": "3921", "locator": "gamespy.com", } elif not db.registered(post): logger.log(logging.DEBUG, "Login denied - console pending %s", post) ret = { "retry": "1", "returncd": "3888", "locator": "gamespy.com", } elif not db.allowed_games(post): logger.log(logging.DEBUG, "Login denied - Game not enabled %s", post) ret = { "retry": "1", "returncd": "3800", "locator": "gamespy.com", } else: challenge = utils.generate_random_str(8) post["challenge"] = challenge authtoken = db.generate_authtoken(post["userid"], post) ret = { "retry": "0", "returncd": "001", "locator": "gamespy.com", "challenge": challenge, "token": authtoken, } logger.log(logging.DEBUG, "Login response to %s:%d", *addr) logger.log(logging.DEBUG, "%s", ret) return ret