def tibiaproxy_main(config): """tibiaproxy's entry point.""" if config['debug']: sys.excepthook = run_pdb_hook plugins = [] for filename in os.listdir('plugins'): if filename == "__init__.py" or filename[-3:] != ".py": continue plugin_name = filename[:-3] plugins += [importlib.import_module('plugins.' + plugin_name)] log("Loaded plugin %s." % plugin_name) server = Server(destination_login_host=config['destination_login_host'], destination_login_port=config['destination_login_port'], listen_login_host=config['listen_login_host'], listen_login_port=config['listen_login_port'], listen_game_host=config['listen_game_host'], listen_game_port=config['listen_game_port'], announce_host=config['announce_host'], announce_port=config['announce_port'], real_tibia=config['real_tibia'], debug=config['debug'], plugins=plugins) server.run()
def accept_game_conn(): conn, addr = self.g_s.accept() log("Received a game server connection from %s:%s" % addr) if not self.debug: t = threading.Thread(target=self.handleGame, args=[conn]) t.start() else: self.handleGame(conn)
def accept_login_conn(): conn, addr = self.l_s.accept() log("Received a login connection from %s:%s" % addr) data = conn.recv(1024) msg = NetworkMessage(data) if not self.debug: t = threading.Thread(target=self.handleLogin, args=[conn, msg]) t.start() else: self.handleLogin(conn, msg)
def parseReply(msg, xtea_key): """Parse the reply from the login server. Args: msg (NetworkMessage): the network message to be parsed. Returns dict or None """ size = msg.getU16() # TODO: someday perhaps I'll have enough time to even check the checksums! msg.skipBytes(4) msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) #assert(len(msg.getWithHeader()) == size) decrypted_size = msg.getU16() #assert(decrypted_size == size - 5) packet_type = msg.getByte() if packet_type != 0x14: # The reply doesn't seem to contain character list. return None motd = msg.getString() assert (msg.getByte() == 0x64) num_worlds = msg.getByte() worlds = [] for _ in range(num_worlds): world_id = msg.getByte() world_name = msg.getString() world_hostname = msg.getString() world_port = msg.getU16() log("Received server address %s:%s" % (world_hostname, world_port)) msg.skipBytes(1) # no idea what's that. worlds += [ create_login_world_entry(name=world_name, hostname=world_hostname, port=world_port) ] num_chars = msg.getByte() characters = [] for _ in range(num_chars): world_num = msg.getByte() char_world = worlds[world_num] char_name = msg.getString() characters += [ create_login_character_entry(name=char_name, world=char_world) ] return create_login_reply_info(characters, motd, worlds)
def parseReply(msg, xtea_key): """Parse the reply from the login server. Args: msg (NetworkMessage): the network message to be parsed. Returns dict or None """ size = msg.getU16() # TODO: someday perhaps I'll have enough time to even check the checksums! msg.skipBytes(4) msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) #assert(len(msg.getWithHeader()) == size) decrypted_size = msg.getU16() #assert(decrypted_size == size - 5) packet_type = msg.getByte() if packet_type != 0x14: # The reply doesn't seem to contain character list. return None motd = msg.getString() assert(msg.getByte() == 0x64) num_worlds = msg.getByte() worlds = [] for _ in range(num_worlds): world_id = msg.getByte() world_name = msg.getString() world_hostname = msg.getString() world_port = msg.getU16() log("Received server address %s:%s" % (world_hostname, world_port)) msg.skipBytes(1) # no idea what's that. worlds += [create_login_world_entry(name=world_name, hostname=world_hostname, port=world_port)] num_chars = msg.getByte() characters = [] for _ in range(num_chars): world_num = msg.getByte() char_world = worlds[world_num] char_name = msg.getString() characters += [create_login_character_entry(name=char_name, world=char_world)] return create_login_reply_info(characters, motd, worlds)
def getTile(): got_effect = False for stackPos in range(256): if msg.peekU16() >= 0xff00: return msg.getU16() & 0xff if not got_effect: msg.getU16() got_effect = True continue assert(stackPos <= 10) thingId = msg.getU16() log(thingId) assert(thingId != 0) if thingId == 96: log("strange... got a staticText") elif thingId in [97, 98, 99]: # it's a creature pass else: # it's a thing - might require reading 3 # extra bytes pass
def handleLogin(self, conn, msg): """Handles the login communication, passing it to the destination host, modifying the server IPs and returning the modified character list to the user. Args: conn (socket): the already established connection. msg (NetworkMessage): the first message received. Returns None """ xtea_key = LoginProtocol.parseFirstMessage(msg) # Connect to the destination host, send the request and read the reply. dest_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) log("Connecting to the destination host...") dest_s.connect((self.destination_login_host, self.destination_login_port)) if not self.real_tibia: dest_s.send(msg.getRaw()) else: reencrypted = RSA.RSA_encrypt(RSA.RSA_decrypt(msg.getRaw()[28:])) unencrypted = msg.getRaw()[6:28] new_buf = "" new_buf += msg.getRaw()[:2] new_buf += struct.pack("<I", adlerChecksum(unencrypted+reencrypted)) new_buf += unencrypted new_buf += reencrypted dest_s.send(new_buf) data = dest_s.recv(1024) if data == '': log("Server disconnected.") conn.close() return msg = NetworkMessage(data) reply = LoginProtocol.parseReply(msg, xtea_key) if reply is None: # The reply doesn't seem to contain character list - just forward # it. log("WARNING: Passing through the login request.") conn.send(data) conn.close() return for character in reply['characters']: self.characters[character['name']] = character # Replace the IP and port with the address to the proxy. client_reply = copy.deepcopy(reply) for world in client_reply['worlds']: world['hostname'] = self.announce_host world['port'] = self.announce_port client_reply_msg = LoginProtocol.prepareReply(client_reply) # Send the message and close the connection. conn.send(client_reply_msg.getEncrypted(xtea_key)) conn.close()
def run(self): """Run serveLogin and serveGame threads and sleep forever. Returns None """ log(("Listening on address %s:%s (login), %s:%s (game), connections " + "will be forwarded to %s:%s") % (self.listen_login_host, self.listen_login_port, self.listen_game_host, self.listen_game_port, self.destination_login_host, self.destination_login_port)) self.l_s.listen(1) self.g_s.listen(1) if not self.debug: t_l = threading.Thread(target=self.serveLogin) g_l = threading.Thread(target=self.serveGame) t_l.daemon = True g_l.daemon = True t_l.start() g_l.start() else: self.serveLogin(True) self.serveGame(True) if not self.debug: # http://stackoverflow.com/q/3788208/1091116 try: while True: time.sleep(100) except (KeyboardInterrupt, SystemExit): sys.exit("Received keyboard interrupt, quitting")
def handleGame(self, conn): """Connect to the game server, relay the packets between the the player, the proxy and the game server, reading them and running callbacks based on incoming/outgoing data. TODO: add anything beyond the eval() proof of concept. This function is one giant kludge at the moment. Args: conn (socket): the already established connection. data (str): the raw data sent by the player. Returns None """ # send a bogus challenge = 109, timestamp = 1385139009 conn.send(b'\x0c\x00@\x02!\x07\x06\x00\x1fA\x8b\x8fRm') data = conn.recv(2) size = struct.unpack("<H", data)[0] data += conn.recv(size) # Read the XTEA key from the player, pass on the original packet. msg = NetworkMessage(data) firstmsg_contents = GameProtocol.parseFirstMessage(msg) # Connect to the game server. dest_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) character = self.characters[firstmsg_contents['character_name']] game_host = character['world']['hostname'] game_port = character['world']['port'] log("Connecting to the game server (%s:%s)." % (game_host, game_port)) dest_s.connect((game_host, game_port)) size_raw = dest_s.recv(2) size = struct.unpack("<H", size_raw)[0] checksum = dest_s.recv(4) data = dest_s.recv(size - 4) msg = NetworkMessage(data) challenge_data = GameProtocol.parseChallengeMessage(msg) xtea_key = firstmsg_contents['xtea_key'] firstmsg_contents['timestamp'] = challenge_data['timestamp'] firstmsg_contents['random_number'] = challenge_data['random_number'] dest_s.send(GameProtocol.prepareReply(firstmsg_contents, self.real_tibia)) conn_obj = Connection(conn, xtea_key) received_player = False while True: # Wait until either the player or the server sent some data. has_data, _, _ = select.select([conn, dest_s], [], []) if conn in has_data: data = bytearray() size_raw = conn.recv(2) data += size_raw if size_raw == bytearray(): log("The client disconnected") break size = struct.unpack("<H", size_raw)[0] data += conn.recv(size+4) msg = NetworkMessage(data) msg_size = msg.getU16() msg.getU32() # skip the checksum validation if msg_size != len(data) - 2: log("Strange packet from client: %s" % repr(data)) log("len(data)=%s, msg_size=%s" % (len(data), msg_size)) dest_s.send(data) continue msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) msg.getU16() packet_type = msg.getByte() if packet_type in GameProtocol.client_packet_types: if self.debug: log("C [%s] %s" % (hex(packet_type), GameProtocol.client_packet_types[packet_type])) else: log("Got a packet of type %s from client" % packet_type) should_forward = True if packet_type == 150: # We got a player "say" request. Read what the player # wanted to say, treat it like a Python expression and send # the result back to the user. msg.skipBytes(1) player_said = msg.getString() for plugin in self.plugins: if 'on_client_say' in dir(plugin): plugin_returned = plugin.on_client_say(conn_obj, player_said) if plugin_returned: should_forward = False if should_forward: # Otherwise, just pass the packet to the server. dest_s.send(data) if dest_s in has_data: # Server sent us some data. data = bytearray() size_raw = dest_s.recv(2) data += size_raw if data == bytearray(): conn.close() log("The server disconnected") break size = struct.unpack("<H", size_raw)[0] data += dest_s.recv(size) msg = NetworkMessage(data) msg_size = msg.getU16() msg.getU32() # skip the checksum validation if msg_size != len(data) - 2: log("Strange packet from server: %s" % repr(data)) log("len(data)=%s, msg_size=%s" % (len(data), msg_size)) conn.send(data) continue msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) msg.getU16() while msg.finished(): def getMapDescription(): def getTile(): got_effect = False for stackPos in range(256): if msg.peekU16() >= 0xff00: return msg.getU16() & 0xff if not got_effect: msg.getU16() got_effect = True continue assert(stackPos <= 10) thingId = msg.getU16() log(thingId) assert(thingId != 0) if thingId == 96: log("strange... got a staticText") elif thingId in [97, 98, 99]: # it's a creature pass else: # it's a thing - might require reading 3 # extra bytes pass def getFloorDescription(x, y, z, w, h, offset, skip): for nx in range(w): for ny in range(h): getTile() x, y, z = msg.getCoordinates() w = 18 h = 14 skip = [-1] if z > 7: startz = z - 2 endz = min(16 - 1, z + 2) zstep = 1 else: startz = 7 endz = 0 zstep = -1 nz = startz while nz != endz + zstep: getFloorDescription(x, y, nz, w, h, z - nz, skip) nz += zstep packet_type = msg.getByte() if packet_type in GameProtocol.server_packet_types: if self.debug: log("S [%s] %s" % (hex(packet_type), GameProtocol.server_packet_types[packet_type]) ) else: log("Got a packet of type %s from server" % packet_type) # login successful if packet_type == 0x17: msg.getU32() # player ID msg.getU16() # beat duration msg.getU32() # speed A msg.getU32() # speed B msg.getU32() # speed C msg.getByte() # 1 if can report bugs, 0 otherwise # no idea what's this: assert_equal(msg.getByte(), 0xB6) assert_equal(msg.getByte(), 0x7F) assert_equal(msg.getByte(), 0x00) assert_equal(msg.getByte(), 0x0A) assert_equal(msg.getByte(), 0x0F) assert_equal(msg.getByte(), 0x64) getMapDescription() log("TODO") received_player = True # FYI box if packet_type == 0x15: fyi = msg.getString() log("Got a FYI: %s" % fyi) log("Sending!") conn.send(data)