diff --git a/CMakeLists.txt b/CMakeLists.txt index 42e809b0..795d4b28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,36 +32,37 @@ set (LIBEVENT_LIBRARIES ${LIBEVENT_THREAD}) add_executable(newserv - src/FileContentsCache.cc - src/Menu.cc - src/PSOProtocol.cc - src/PSOEncryption.cc - src/Client.cc - src/Lobby.cc - src/ServerState.cc - src/Server.cc - src/License.cc - src/Player.cc - src/SendCommands.cc src/ChatCommands.cc - src/ReceiveSubcommands.cc - src/ReceiveCommands.cc - src/Version.cc - src/Items.cc - src/LevelTable.cc + src/Client.cc src/Compression.cc - src/Quest.cc - src/RareItemSet.cc - src/Map.cc - src/NetworkAddresses.cc - src/Text.cc src/DNSServer.cc - src/ProxyServer.cc - src/Shell.cc - src/ServerShell.cc + src/FileContentsCache.cc src/IPFrameInfo.cc src/IPStackSimulator.cc + src/Items.cc + src/LevelTable.cc + src/License.cc + src/Lobby.cc src/Main.cc + src/Map.cc + src/Menu.cc + src/NetworkAddresses.cc + src/Player.cc + src/ProxyCommands.cc + src/ProxyServer.cc + src/PSOEncryption.cc + src/PSOProtocol.cc + src/Quest.cc + src/RareItemSet.cc + src/ReceiveCommands.cc + src/ReceiveSubcommands.cc + src/SendCommands.cc + src/Server.cc + src/ServerShell.cc + src/ServerState.cc + src/Shell.cc + src/Text.cc + src/Version.cc ) target_include_directories(newserv PUBLIC ${LIBEVENT_INCLUDE_DIR}) target_link_libraries(newserv phosg ${LIBEVENT_LIBRARIES}) diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc new file mode 100644 index 00000000..6342c18b --- /dev/null +++ b/src/ProxyCommands.cc @@ -0,0 +1,956 @@ +#include "ProxyServer.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PSOProtocol.hh" +#include "SendCommands.hh" +#include "ReceiveCommands.hh" +#include "ReceiveSubcommands.hh" + +using namespace std; + + + +static void forward_command(ProxyServer::LinkedSession& session, bool to_server, + uint16_t command, uint32_t flag, string& data) { + auto* bev = to_server ? session.server_bev.get() : session.client_bev.get(); + if (!bev) { + session.log(WARNING, "No endpoint is present; dropping command"); + } else { + // Note: we intentionally don't pass name_str here because we already + // printed the command before calling the handler + send_command( + bev, + session.version, + to_server ? session.server_output_crypt.get() : session.client_output_crypt.get(), + command, + flag, + data.data(), + data.size()); + } +} + +static void check_implemented_subcommand(uint64_t id, const string& data) { + if (data.size() < 4) { + log(WARNING, "[ProxyServer/%08" PRIX64 "] Received broadcast/target command with no contents", id); + } else { + if (!subcommand_is_implemented(data[0])) { + log(WARNING, "[ProxyServer/%08" PRIX64 "] Received subcommand %02hhX which is not implemented on the server", + id, data[0]); + } + } +} + + + +// Command handlers. These are called to preprocess or react to specific +// commands in either direction. If they return true, the command (which the +// function may have modified) is forwarded to the other end; if they return +// false; it is not. + +static bool process_default(shared_ptr, + ProxyServer::LinkedSession&, uint16_t, uint32_t, string&) { + return true; +} + +static bool process_server_gc_9A(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) { + if (!session.license) { + return true; + } + + C_LoginWithUnusedSpace_GC_9E cmd; + if (session.remote_guild_card_number == 0) { + cmd.player_tag = 0xFFFF0000; + cmd.guild_card_number = 0xFFFFFFFF; + } else { + cmd.player_tag = 0x00010000; + cmd.guild_card_number = session.remote_guild_card_number; + } + cmd.sub_version = session.sub_version; + cmd.unused2.data()[1] = 1; + cmd.serial_number = string_printf("%08" PRIX32 "", session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.serial_number2 = cmd.serial_number; + cmd.access_key2 = cmd.access_key; + cmd.name = session.character_name; + cmd.client_config.data = session.remote_client_config_data; + + // If there's a guild card number, a shorter 9E is sent that ends + // right after the client config data + + session.send_to_end( + true, 0x9E, 0x01, &cmd, + sizeof(C_LoginWithUnusedSpace_GC_9E) - (session.remote_guild_card_number ? sizeof(cmd.unused_space) : 0)); + return false; +} + +static bool process_server_pc_gc_patch_02_17(shared_ptr s, + ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) { + if (session.version == GameVersion::PATCH && command == 0x17) { + throw invalid_argument("patch server sent 17 server init"); + } + + // Most servers don't include after_message or have a shorter + // after_message than newserv does, so don't require it + const auto& cmd = check_size_t(data, + offsetof(S_ServerInit_DC_PC_GC_02_17, after_message), + sizeof(S_ServerInit_DC_PC_GC_02_17)); + + if (!session.license) { + session.log(INFO, "No license in linked session"); + + // We have to forward the command before setting up encryption, so the + // client will be able to understand it. + forward_command(session, false, command, flag, data); + + if (session.version == GameVersion::GC) { + session.server_input_crypt.reset(new PSOGCEncryption(cmd.server_key)); + session.server_output_crypt.reset(new PSOGCEncryption(cmd.client_key)); + session.client_input_crypt.reset(new PSOGCEncryption(cmd.client_key)); + session.client_output_crypt.reset(new PSOGCEncryption(cmd.server_key)); + } else { // PC or patch server (they both use PC encryption) + session.server_input_crypt.reset(new PSOPCEncryption(cmd.server_key)); + session.server_output_crypt.reset(new PSOPCEncryption(cmd.client_key)); + session.client_input_crypt.reset(new PSOPCEncryption(cmd.client_key)); + session.client_output_crypt.reset(new PSOPCEncryption(cmd.server_key)); + } + + return false; + } + + session.log(INFO, "Existing license in linked session"); + + // This isn't forwarded to the client, so don't recreate the client's crypts + if (session.version == GameVersion::PATCH) { + throw logic_error("patch session is indirect"); + } else if (session.version == GameVersion::PC) { + session.server_input_crypt.reset(new PSOPCEncryption(cmd.server_key)); + session.server_output_crypt.reset(new PSOPCEncryption(cmd.client_key)); + } else if (session.version == GameVersion::GC) { + session.server_input_crypt.reset(new PSOGCEncryption(cmd.server_key)); + session.server_output_crypt.reset(new PSOGCEncryption(cmd.client_key)); + } else { + throw invalid_argument("unsupported version"); + } + + // Respond with an appropriate login command. We don't let the + // client do this because it believes it already did (when it was + // in an unlinked session). + if (session.version == GameVersion::PC) { + C_Login_PC_9D cmd; + if (session.remote_guild_card_number == 0) { + cmd.player_tag = 0xFFFF0000; + cmd.guild_card_number = 0xFFFFFFFF; + } else { + cmd.player_tag = 0x00010000; + cmd.guild_card_number = session.remote_guild_card_number; + } + cmd.unused = 0xFFFFFFFFFFFF0000; + cmd.sub_version = session.sub_version; + cmd.unused2.data()[1] = 1; + cmd.serial_number = string_printf("%08" PRIX32 "", + session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.serial_number2 = cmd.serial_number; + cmd.access_key2 = cmd.access_key; + cmd.name = session.character_name; + session.send_to_end(true, 0x9D, 0x00, &cmd, sizeof(cmd)); + return false; + + } else if (session.version == GameVersion::GC) { + if (command == 0x17) { + C_VerifyLicense_GC_DB cmd; + cmd.serial_number = string_printf("%08" PRIX32 "", + session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.sub_version = session.sub_version; + cmd.serial_number2 = cmd.serial_number; + cmd.access_key2 = cmd.access_key; + cmd.password = session.license->gc_password; + session.send_to_end(true, 0xDB, 0x00, &cmd, sizeof(cmd)); + return false; + + } else { + // For command 02, send the same as if we had received 9A from the server + return process_server_gc_9A(s, session, command, flag, data); + } + + } else { + throw logic_error("invalid game version in server init handler"); + } +} + +static bool process_server_dc_pc_gc_04(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + // Some servers send a short 04 command if they don't use all of the 0x20 + // bytes available. We should be prepared to handle that. + auto& cmd = check_size_t(data, + offsetof(S_UpdateClientConfig_DC_PC_GC_04, cfg), + sizeof(S_UpdateClientConfig_DC_PC_GC_04)); + + // If this is a licensed session, hide the guild card number assigned by the + // remote server so the client doesn't see it change. If this is an unlicensed + // session, then the client never received a guild card number from newserv + // anyway, so we can let the client see the number from the remote server. + bool had_guild_card_number = (session.remote_guild_card_number != 0); + session.remote_guild_card_number = cmd.guild_card_number; + session.log(INFO, "Remote guild card number set to %" PRIX32, + session.remote_guild_card_number); + if (session.license) { + cmd.guild_card_number = session.license->serial_number; + } + + // It seems the client ignores the length of the 04 command, and always copies + // 0x20 bytes to its config data. So if the server sends a short 04 command, + // part of the previous command ends up in the security data (usually part of + // the copyright string from the server init command). We simulate that here. + // If there was previously a guild card number, assume we got the lobby server + // init text instead of the port map init text. + memcpy(session.remote_client_config_data.data(), + had_guild_card_number + ? "t Lobby Server. Copyright SEGA E" + : "t Port Map. Copyright SEGA Enter", + session.remote_client_config_data.bytes()); + memcpy(session.remote_client_config_data.data(), &cmd.cfg, + min(data.size() - sizeof(S_UpdateClientConfig_DC_PC_GC_04), + session.remote_client_config_data.bytes())); + + // If the guild card number was not set, pretend (to the server) that this is + // the first 04 command the client has received. The client responds with a 96 + // (checksum) in that case. + if (!had_guild_card_number) { + // We don't actually have a client checksum, of course... hopefully just + // random data will do (probably no private servers check this at all) + le_uint64_t checksum = random_object() & 0x0000FFFFFFFFFFFF; + session.send_to_end(true, 0x96, 0x00, &checksum, sizeof(checksum)); + } + + return true; +} + +static bool process_server_dc_pc_gc_06(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.license) { + auto& cmd = check_size_t(data, + sizeof(SC_TextHeader_01_06_11_B0), 0xFFFF); + if (cmd.guild_card_number == session.remote_guild_card_number) { + cmd.guild_card_number = session.license->serial_number; + } + } + return true; +} + +template +static bool process_server_41(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.license) { + auto& cmd = check_size_t(data); + if (cmd.searcher_guild_card_number == session.remote_guild_card_number) { + cmd.searcher_guild_card_number = session.license->serial_number; + } + if (cmd.result_guild_card_number == session.remote_guild_card_number) { + cmd.result_guild_card_number = session.license->serial_number; + } + } + return true; +} + +template +static bool process_server_81(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.license) { + auto& cmd = check_size_t(data); + if (cmd.from_guild_card_number == session.remote_guild_card_number) { + cmd.from_guild_card_number = session.license->serial_number; + } + if (cmd.to_guild_card_number == session.remote_guild_card_number) { + cmd.to_guild_card_number = session.license->serial_number; + } + } + return true; +} + +static bool process_server_88(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) { + if (session.license) { + size_t expected_size = sizeof(S_ArrowUpdateEntry_88) * flag; + auto* entries = &check_size_t(data, + expected_size, expected_size); + for (size_t x = 0; x < flag; x++) { + if (entries[x].guild_card_number == session.remote_guild_card_number) { + entries[x].guild_card_number = session.license->serial_number; + } + } + } + return true; +} + +template +static bool process_server_C4(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) { + if (session.license) { + size_t expected_size = sizeof(CmdT) * flag; + auto* entries = &check_size_t(data, expected_size, expected_size); + for (size_t x = 0; x < flag; x++) { + if (entries[x].guild_card_number == session.remote_guild_card_number) { + entries[x].guild_card_number = session.license->serial_number; + } + } + } + return true; +} + +static bool process_server_gc_E4(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + auto& cmd = check_size_t(data); + for (size_t x = 0; x < 4; x++) { + if (cmd.entries[x].guild_card_number == session.remote_guild_card_number) { + cmd.entries[x].guild_card_number = session.license->serial_number; + } + } + return true; +} + +static bool process_server_19(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + // This weird maximum size is here to properly handle the version-split + // command that some servers (including newserv) use on port 9100 + auto& cmd = check_size_t(data, sizeof(S_Reconnect_19), 0xB0); + memset(&session.next_destination, 0, sizeof(session.next_destination)); + struct sockaddr_in* sin = reinterpret_cast( + &session.next_destination); + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = cmd.address.load_raw(); + sin->sin_port = htons(cmd.port); + + if (!session.client_bev.get()) { + session.log(WARNING, "Received reconnect command with no destination present"); + + } else { + // If the client is on a virtual connection (fd < 0), only change + // the port (so we'll know which version to treat the next + // connection as). It's better to leave the address as-is so we + // can circumvent the Plus/Ep3 same-network-server check. + int fd = bufferevent_getfd(session.client_bev.get()); + if (fd >= 0) { + struct sockaddr_storage sockname_ss; + socklen_t len = sizeof(sockname_ss); + getsockname(fd, reinterpret_cast(&sockname_ss), &len); + if (sockname_ss.ss_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); + } + + struct sockaddr_in* sockname_sin = reinterpret_cast( + &sockname_ss); + cmd.address.store_raw(sockname_sin->sin_addr.s_addr); + cmd.port = ntohs(sockname_sin->sin_port); + + } else { + cmd.port = session.local_port; + } + } + return true; +} + +static bool process_server_gc_1A_D5(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) { + // If the client has the no-close-confirmation flag set in its + // newserv client config, send a fake confirmation to the remote + // server immediately. + if (session.newserv_client_config.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) { + session.send_to_end(true, 0xD6, 0x00, "", 0); + } + return true; +} + +static bool process_server_60_62_6C_6D_C9_CB(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + check_implemented_subcommand(session.id, data); + return true; +} + +template +static bool process_server_44_A6(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) { + if (session.save_files) { + const auto& cmd = check_size_t(data); + bool is_download_quest = (command == 0xA6); + + string output_filename = string_printf("%s.%s.%" PRIu64, + cmd.filename.c_str(), + is_download_quest ? "download" : "online", now()); + for (size_t x = 0; x < output_filename.size(); x++) { + if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') { + output_filename[x] = '_'; + } + } + if (output_filename[0] == '.') { + output_filename[0] = '_'; + } + + ProxyServer::LinkedSession::SavingFile sf( + cmd.filename, output_filename, cmd.file_size); + session.saving_files.emplace(cmd.filename, move(sf)); + session.log(INFO, "Opened file %s", output_filename.c_str()); + } + return true; +} + +static bool process_server_13_A7(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.save_files) { + const auto& cmd = check_size_t(data); + + ProxyServer::LinkedSession::SavingFile* sf = nullptr; + try { + sf = &session.saving_files.at(cmd.filename); + } catch (const out_of_range&) { + session.log(WARNING, "Received data for non-open file %s", cmd.filename.c_str()); + return true; + } + + size_t bytes_to_write = cmd.data_size; + if (bytes_to_write > 0x400) { + session.log(WARNING, "Chunk data size is invalid; truncating to 0x400"); + bytes_to_write = 0x400; + } + + session.log(INFO, "Writing %zu bytes to %s", bytes_to_write, sf->output_filename.c_str()); + fwritex(sf->f.get(), cmd.data, bytes_to_write); + if (bytes_to_write > sf->remaining_bytes) { + session.log(WARNING, "Chunk size extends beyond original file size; file may be truncated"); + sf->remaining_bytes = 0; + } else { + sf->remaining_bytes -= bytes_to_write; + } + + if (sf->remaining_bytes == 0) { + session.log(INFO, "File %s is complete", sf->output_filename.c_str()); + session.saving_files.erase(cmd.filename); + } + } + return true; +} + +static bool process_server_gc_B8(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.save_files) { + if (data.size() < 4) { + session.log(WARNING, "Card list data size is too small; skipping file"); + return true; + } + + StringReader r(data); + size_t size = r.get_u32l(); + if (r.remaining() < size) { + session.log(WARNING, "Card list data size extends beyond end of command; skipping file"); + return true; + } + + string output_filename = string_printf("cardupdate.mnr.%" PRIu64, now()); + save_file(output_filename, r.read(size)); + + session.log(INFO, "Wrote %zu bytes to %s", size, output_filename.c_str()); + } + return true; +} + +template +static bool process_server_65_67_68(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) { + if (command == 0x67) { + session.lobby_players.clear(); + session.lobby_players.resize(12); + session.log(INFO, "Cleared lobby players"); + + // This command can cause the client to no longer send D6 responses when + // 1A/D5 large message boxes are closed. newserv keeps track of this + // behavior in the client config, so if it happens during a proxy session, + // update the client config that we'll restore if the client uses the change + // ship or change block command. + if (session.newserv_client_config.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN) { + session.newserv_client_config.flags |= Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION; + } + } + + size_t expected_size = offsetof(CmdT, entries) + sizeof(typename CmdT::Entry) * flag; + auto& cmd = check_size_t(data, expected_size, expected_size); + + session.lobby_client_id = cmd.client_id; + for (size_t x = 0; x < flag; x++) { + size_t index = cmd.entries[x].lobby_data.client_id; + if (index >= session.lobby_players.size()) { + session.log(WARNING, "Ignoring invalid player index %zu at position %zu", index, x); + } else { + if (session.license && (cmd.entries[x].lobby_data.guild_card == session.remote_guild_card_number)) { + cmd.entries[x].lobby_data.guild_card = session.license->serial_number; + } + session.lobby_players[index].guild_card_number = cmd.entries[x].lobby_data.guild_card; + ptext name = cmd.entries[x].disp.name; + session.lobby_players[index].name = name; + session.log(INFO, "Added lobby player: (%zu) %" PRIu32 " %s", + index, + session.lobby_players[index].guild_card_number, + session.lobby_players[index].name.c_str()); + } + } + + if (session.override_lobby_event >= 0) { + cmd.event = session.override_lobby_event; + } + if (session.override_lobby_number >= 0) { + cmd.lobby_number = session.override_lobby_number; + } + + return true; +} + +template +static bool process_server_64(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) { + // We don't need to clear lobby_players here because we always + // overwrite all 4 entries for this command + session.lobby_players.resize(4); + session.log(INFO, "Cleared lobby players"); + + const size_t expected_size = session.sub_version >= 0x40 + ? sizeof(CmdT) + : offsetof(CmdT, players_ep3); + auto& cmd = check_size_t(data, expected_size, expected_size); + + session.lobby_client_id = cmd.client_id; + for (size_t x = 0; x < flag; x++) { + if (cmd.lobby_data[x].guild_card == session.remote_guild_card_number) { + cmd.lobby_data[x].guild_card = session.license->serial_number; + } + session.lobby_players[x].guild_card_number = cmd.lobby_data[x].guild_card; + if (data.size() == sizeof(CmdT)) { + ptext name = cmd.players_ep3[x].disp.name; + session.lobby_players[x].name = name; + } else { + session.lobby_players[x].name.clear(); + } + session.log(INFO, "Added lobby player: (%zu) %" PRIu32 " %s", + x, + session.lobby_players[x].guild_card_number, + session.lobby_players[x].name.c_str()); + } + + if (session.override_section_id >= 0) { + cmd.section_id = session.override_section_id; + } + if (session.override_lobby_event >= 0) { + cmd.event = session.override_lobby_event; + } + + return true; +} + +static bool process_server_66_69(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + const auto& cmd = check_size_t(data); + size_t index = cmd.client_id; + if (index >= session.lobby_players.size()) { + session.log(WARNING, "Lobby leave command references missing position"); + } else { + session.lobby_players[index].guild_card_number = 0; + session.lobby_players[index].name.clear(); + session.log(INFO, "Removed lobby player (%zu)", index); + } + return true; +} + + + + + +static bool process_client_06(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (data.size() >= 12) { + // If this chat message looks like a newserv chat command, suppress it + if (session.suppress_newserv_commands && + (data[8] == '$' || (data[8] == '\t' && data[9] != 'C' && data[10] == '$'))) { + session.log(WARNING, "Chat message appears to be a server command; dropping it"); + return false; + } else if (session.enable_chat_filter) { + add_color_inplace(data.data() + 8, data.size() - 8); + } + } + return true; +} + +static bool process_client_40(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.license) { + auto& cmd = check_size_t(data); + if (cmd.searcher_guild_card_number == session.license->serial_number) { + cmd.searcher_guild_card_number = session.remote_guild_card_number; + } + if (cmd.target_guild_card_number == session.license->serial_number) { + cmd.target_guild_card_number = session.remote_guild_card_number; + } + } + return true; +} + +template +static bool process_client_81(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + if (session.license) { + auto& cmd = check_size_t(data); + if (cmd.from_guild_card_number == session.license->serial_number) { + cmd.from_guild_card_number = session.remote_guild_card_number; + } + if (cmd.to_guild_card_number == session.license->serial_number) { + cmd.to_guild_card_number = session.remote_guild_card_number; + } + } + return true; +} + +template +static bool process_client_60_62_6C_6D_C9_CB(shared_ptr s, + ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) { + if (session.license && !data.empty() && (data[0] == 0x06)) { + auto& cmd = check_size_t(data); + if (cmd.guild_card_number == session.license->serial_number) { + cmd.guild_card_number = session.remote_guild_card_number; + } + } + return process_client_60_62_6C_6D_C9_CB(s, session, command, flag, data); +} + +template <> +bool process_client_60_62_6C_6D_C9_CB(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { + check_implemented_subcommand(session.id, data); + + if (!data.empty() && (data[0] == 0x05) && (data[data.size() - 1] == 0x01) && session.enable_switch_assist) { + if (!session.last_switch_enabled_subcommand.empty()) { + session.log(WARNING, "Switch assist: replaying previous enable subcommand"); + session.send_to_end(true, 0x60, 0x00, session.last_switch_enabled_subcommand); + session.send_to_end(false, 0x60, 0x00, session.last_switch_enabled_subcommand); + } + session.last_switch_enabled_subcommand = data; + } + + return true; +} + +static bool process_client_dc_pc_gc_A0_A1(shared_ptr s, + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) { + if (!session.license) { + return true; + } + + // For licensed sessions, send them back to newserv's main menu instead of + // going to the remote server's ship/block select menu + + // Delete all the other players + for (size_t x = 0; x < session.lobby_players.size(); x++) { + if (session.lobby_players[x].guild_card_number == 0) { + continue; + } + uint8_t leaving_id = x; + uint8_t leader_id = session.lobby_client_id; + S_LeaveLobby_66_69 cmd = {leaving_id, leader_id, 0}; + session.send_to_end(false, 0x69, leaving_id, &cmd, sizeof(cmd)); + } + + // Restore newserv_client_config, so the login server gets the client flags + S_UpdateClientConfig_DC_PC_GC_04 update_client_config_cmd = { + 0x00010000, + session.license->serial_number, + session.newserv_client_config, + }; + session.send_to_end(false, 0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); + + static const vector version_to_port_name({ + "dc-login", "pc-login", "bb-patch", "gc-us3", "bb-login"}); + const auto& port_name = version_to_port_name.at(static_cast( + session.version)); + + S_Reconnect_19 reconnect_cmd = { + 0, s->name_to_port_config.at(port_name)->port, 0}; + + // If the client is on a virtual connection, we can use any address + // here and they should be able to connect back to the game server. If + // the client is on a real connection, we'll use the sockname of the + // existing connection (like we do in the server 19 command handler). + int fd = bufferevent_getfd(session.client_bev.get()); + if (fd < 0) { + struct sockaddr_in* dest_sin = reinterpret_cast(&session.next_destination); + if (dest_sin->sin_family != AF_INET) { + throw logic_error("ss not AF_INET"); + } + reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr); + } else { + struct sockaddr_storage sockname_ss; + socklen_t len = sizeof(sockname_ss); + getsockname(fd, reinterpret_cast(&sockname_ss), &len); + if (sockname_ss.ss_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); + } + + struct sockaddr_in* sockname_sin = reinterpret_cast( + &sockname_ss); + reconnect_cmd.address.store_raw(sockname_sin->sin_addr.s_addr); + } + + session.send_to_end(false, 0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd)); + + return false; +} + + + +typedef bool (*process_command_t)( + shared_ptr s, + ProxyServer::LinkedSession& session, + uint16_t command, + uint32_t flag, + string& data); + +// The entries in these arrays correspond to the ID of the command received. For +// instance, if a command 6C is received, the function at position 0x6C in the +// array corresponding to the client's version is called. +auto defh = process_default; + +static process_command_t dc_server_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_19, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, defh, defh, process_server_66_69, defh, defh, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t pc_server_handlers[0x100] = { + /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_19, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_64, process_server_65_67_68, process_server_66_69, process_server_65_67_68, process_server_65_67_68, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t gc_server_handlers[0x100] = { + /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_19, process_server_gc_1A_D5, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_64, process_server_65_67_68, process_server_66_69, process_server_65_67_68, process_server_65_67_68, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, process_server_81, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_server_gc_9A, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, process_server_gc_B8, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, process_server_C4, defh, defh, defh, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, process_server_gc_1A_D5, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, process_server_gc_E4, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t bb_server_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_19, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_64, process_server_65_67_68, process_server_66_69, process_server_65_67_68, process_server_65_67_68, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t patch_server_handlers[0x100] = { + /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; + + + +static process_command_t dc_client_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, process_client_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ process_client_40, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_gc_A0_A1, process_client_dc_pc_gc_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t pc_client_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, process_client_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ process_client_40, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_gc_A0_A1, process_client_dc_pc_gc_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t gc_client_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, process_client_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ process_client_40, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, process_client_81, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_gc_A0_A1, process_client_dc_pc_gc_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t bb_client_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, process_client_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ process_client_40, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t patch_client_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; + + + +static process_command_t* server_handlers[] = { + dc_server_handlers, pc_server_handlers, patch_server_handlers, gc_server_handlers, bb_server_handlers}; +static process_command_t* client_handlers[] = { + dc_client_handlers, pc_client_handlers, patch_client_handlers, gc_client_handlers, bb_client_handlers}; + +static process_command_t get_handler(GameVersion version, bool from_server, uint8_t command) { + size_t version_index = static_cast(version); + if (version_index >= 5) { + throw logic_error("invalid game version on proxy server"); + } + return (from_server ? server_handlers : client_handlers)[version_index][command]; +} + +void process_proxy_command( + shared_ptr s, + ProxyServer::LinkedSession& session, + bool from_server, + uint16_t command, + uint32_t flag, + string& data) { + auto fn = get_handler(session.version, from_server, command); + try { + bool should_forward = fn(s, session, command, flag, data); + if (should_forward) { + forward_command(session, !from_server, command, flag, data); + } + } catch (const exception& e) { + session.log(ERROR, "Failed to process command: %s", e.what()); + session.disconnect(); + } +} diff --git a/src/ProxyCommands.hh b/src/ProxyCommands.hh new file mode 100644 index 00000000..542758f1 --- /dev/null +++ b/src/ProxyCommands.hh @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include + +#include "ServerState.hh" +#include "ProxyServer.hh" + + + +void process_proxy_command( + std::shared_ptr s, + ProxyServer::LinkedSession& session, + bool from_server, + uint16_t command, + uint32_t flag, + std::string& data); diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 6dfca525..e99fe993 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -26,6 +26,7 @@ #include "SendCommands.hh" #include "ReceiveCommands.hh" #include "ReceiveSubcommands.hh" +#include "ProxyCommands.hh" using namespace std; @@ -157,7 +158,7 @@ void ProxyServer::on_client_connect( throw logic_error("linked session already exists for unlicensed client"); } auto session = emplace_ret.first->second; - log(INFO, "[ProxyServer/%08" PRIX64 "] Opened linked session", session->id); + session->log(INFO, "Opened linked session"); session->resume(bev); // If no default destination exists, create an unlinked session - we'll have @@ -293,7 +294,7 @@ void ProxyServer::UnlinkedSession::on_client_input() { shared_ptr session; try { session = this->server->id_to_session.at(license->serial_number); - log(INFO, "[ProxyServer/%08" PRIX64 "] Resuming linked session from unlinked session", session->id); + session->log(INFO, "Resuming linked session from unlinked session"); } catch (const out_of_range&) { // If there's no open session for this license, then there must be a valid @@ -309,13 +310,12 @@ void ProxyServer::UnlinkedSession::on_client_input() { license, client_config)); this->server->id_to_session.emplace(license->serial_number, session); - log(INFO, "[ProxyServer/%08" PRIX64 "] Opened licensed session for unlinked session", session->id); + session->log(INFO, "Opened licensed session for unlinked session"); } } if (session.get() && (session->version != this->version)) { - log(ERROR, "[ProxyServer/%08" PRIX64 "] Linked session has different game version", - session->id); + session->log(ERROR, "Linked session has different game version"); } else { // Resume the linked session using the unlinked session try { @@ -323,8 +323,7 @@ void ProxyServer::UnlinkedSession::on_client_input() { this->crypt_in.reset(); this->crypt_out.reset(); } catch (const exception& e) { - log(ERROR, "[ProxyServer/%08" PRIX64 "] Failed to resume linked session: %s", - session->id, e.what()); + session->log(ERROR, "Failed to resume linked session: %s", e.what()); } } } @@ -356,6 +355,9 @@ ProxyServer::LinkedSession::LinkedSession( GameVersion version) : server(server), id(id), + client_name(string_printf("LinkedSession:%08" PRIX64 ":client", this->id)), + server_name(string_printf("LinkedSession:%08" PRIX64 ":server", this->id)), + log(string_printf("[LinkedSession:%08" PRIX64 "] ", this->id)), timeout_event(event_new(this->server->base.get(), -1, EV_TIMEOUT, &LinkedSession::dispatch_on_timeout, this), event_free), license(nullptr), @@ -471,7 +473,7 @@ void ProxyServer::LinkedSession::connect() { local_sin->sin_addr.s_addr = dest_sin->sin_addr.s_addr; string netloc_str = render_sockaddr_storage(local_ss); - log(INFO, "[ProxyServer/%08" PRIX64 "] Connecting to %s", this->id, netloc_str.c_str()); + this->log(INFO, "Connecting to %s", netloc_str.c_str()); if (bufferevent_socket_connect(this->server_bev.get(), reinterpret_cast(local_sin), sizeof(*local_sin)) != 0) { throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); @@ -526,7 +528,7 @@ void ProxyServer::LinkedSession::dispatch_on_timeout( void ProxyServer::LinkedSession::on_timeout() { - log(INFO, "[ProxyServer/%08" PRIX64 "] Session timed out", this->id); + this->log(INFO, "Session timed out"); this->server->delete_session(this->id); } @@ -536,13 +538,13 @@ void ProxyServer::LinkedSession::on_stream_error( short events, bool is_server_stream) { if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); - log(WARNING, "[ProxyServer/%08" PRIX64 "] Error %d (%s) in %s stream", - this->id, err, evutil_socket_error_to_string(err), + this->log(WARNING, "Error %d (%s) in %s stream", + err, evutil_socket_error_to_string(err), is_server_stream ? "server" : "client"); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - log(INFO, "[ProxyServer/%08" PRIX64 "] %s has disconnected", - this->id, is_server_stream ? "Server" : "Client"); + this->log(INFO, "%s has disconnected", + is_server_stream ? "Server" : "Client"); this->disconnect(); } } @@ -566,951 +568,82 @@ void ProxyServer::LinkedSession::disconnect() { -static void check_implemented_subcommand(uint64_t id, const string& data) { - if (data.size() < 4) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Received broadcast/target command with no contents", id); - } else { - if (!subcommand_is_implemented(data[0])) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Received subcommand %02hhX which is not implemented on the server", - id, data[0]); - } - } -} - void ProxyServer::LinkedSession::on_client_input() { - string name = string_printf("ProxySession:%08" PRIX64 ":client", this->id); - try { - for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(), - [&](uint16_t command, uint32_t flag, string& data) { - print_received_command(command, flag, data.data(), data.size(), - this->version, name.c_str()); - - bool should_forward = true; - switch (command) { - case 0x06: - if (this->version != GameVersion::GC) { - break; - } - if (data.size() < 12) { - break; - } - // If this chat message looks like a newserv chat command, suppress it - if (this->suppress_newserv_commands && - (data[8] == '$' || (data[8] == '\t' && data[9] != 'C' && data[10] == '$'))) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Chat message appears to be a server command; dropping it", - this->id); - should_forward = false; - } else if (this->enable_chat_filter) { - // Turn all $ into \t and all # into \n - add_color_inplace(data.data() + 8, data.size() - 8); - } - break; - - case 0x40: { - if (this->license) { - auto& cmd = check_size_t(data); - if (cmd.searcher_guild_card_number == this->license->serial_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.searcher_guild_card_number = this->remote_guild_card_number; - } - if (cmd.target_guild_card_number == this->license->serial_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.target_guild_card_number = this->remote_guild_card_number; - } - } - break; - } - - case 0x81: { - if (this->license) { - // TODO: Handle this on PC as well (the format isn't in - // CommandFormats yet) - if (this->version == GameVersion::GC) { - auto& cmd = check_size_t(data); - if (cmd.from_guild_card_number == this->license->serial_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.from_guild_card_number = this->remote_guild_card_number; - } - if (cmd.to_guild_card_number == this->license->serial_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.to_guild_card_number = this->remote_guild_card_number; - } - } - } - break; - } - - case 0x60: - case 0x62: - case 0x6C: - case 0x6D: - case 0xC9: - case 0xCB: - check_implemented_subcommand(this->id, data); - - if (this->license && !data.empty() && (data[0] == 0x06)) { - auto& cmd = check_size_t(data); - if (cmd.guild_card_number == this->license->serial_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.guild_card_number = this->remote_guild_card_number; - } - - } else if (!data.empty() && (data[0] == 0x05) && (data[data.size() - 1] == 0x01) && this->enable_switch_assist) { - if (!this->last_switch_enabled_subcommand.empty()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Switch assist: replaying previous enable subcommand", - this->id); - send_command(this->client_bev.get(), this->version, - this->client_output_crypt.get(), 0x60, 0x00, - this->last_switch_enabled_subcommand.data(), - this->last_switch_enabled_subcommand.size(), name.c_str()); - } - this->last_switch_enabled_subcommand = data; - } - break; - - case 0xA0: // Change ship - case 0xA1: { // Change block - if ((this->version == GameVersion::PATCH) || (this->version == GameVersion::BB)) { - break; - } - if (!this->license) { - break; - } - - // These will take you back to the newserv main menu instead of the - // proxied service's menu - - // Delete all the other players - for (size_t x = 0; x < this->lobby_players.size(); x++) { - if (this->lobby_players[x].guild_card_number == 0) { - continue; - } - uint8_t leaving_id = x; - uint8_t leader_id = this->lobby_client_id; - S_LeaveLobby_66_69 cmd = {leaving_id, leader_id, 0}; - send_command(this->client_bev.get(), this->version, - this->client_output_crypt.get(), 0x69, leaving_id, &cmd, - sizeof(cmd), name.c_str()); - } - - // Restore the newserv client config, so the client gets its newserv - // guild card number back and the login server knows e.g. not to show - // the welcome message (if the appropriate flag is set) - S_UpdateClientConfig_DC_PC_GC_04 update_client_config_cmd = { - 0x00010000, - this->license->serial_number, - this->newserv_client_config, - }; - send_command(this->client_bev.get(), this->version, - this->client_output_crypt.get(), 0x04, 0x00, - &update_client_config_cmd, sizeof(update_client_config_cmd), - name.c_str()); - - static const vector version_to_port_name({ - "dc-login", "pc-login", "bb-patch", "gc-us3", "bb-login"}); - const auto& port_name = version_to_port_name.at(static_cast( - this->version)); - - S_Reconnect_19 reconnect_cmd = { - 0, this->server->state->name_to_port_config.at(port_name)->port, 0}; - - // If the client is on a virtual connection, we can use any address - // here and they should be able to connect back to the game server. If - // the client is on a real connection, we'll use the sockname of the - // existing connection (like we do in the server 19 command handler). - int fd = bufferevent_getfd(this->client_bev.get()); - if (fd < 0) { - struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); - if (dest_sin->sin_family != AF_INET) { - throw logic_error("ss not AF_INET"); - } - reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr); - } else { - struct sockaddr_storage sockname_ss; - socklen_t len = sizeof(sockname_ss); - getsockname(fd, reinterpret_cast(&sockname_ss), &len); - if (sockname_ss.ss_family != AF_INET) { - throw logic_error("existing connection is not ipv4"); - } - - struct sockaddr_in* sockname_sin = reinterpret_cast( - &sockname_ss); - reconnect_cmd.address.store_raw(sockname_sin->sin_addr.s_addr); - } - - send_command(this->client_bev.get(), this->version, - this->client_output_crypt.get(), 0x19, 0x00, &reconnect_cmd, - sizeof(reconnect_cmd), name.c_str()); - break; - } - } - - if (should_forward) { - if (!this->client_bev.get()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] No server is present; dropping command", - this->id); - } else { - // Note: we intentionally don't pass a name string here because we already - // printed the command above - send_command(this->server_bev.get(), this->version, - this->server_output_crypt.get(), command, flag, - data.data(), data.size()); - } - } - }); - } catch (const exception& e) { - log(ERROR, "[ProxyServer/%08" PRIX64 "] Failed to process command from client: %s", - this->id, e.what()); - this->disconnect(); - } + for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(), + [&](uint16_t command, uint32_t flag, string& data) { + print_received_command(command, flag, data.data(), data.size(), + this->version, this->client_name.c_str()); + process_proxy_command( + this->server->state, + *this, + false, // from_server + command, + flag, + data); + }); } void ProxyServer::LinkedSession::on_server_input() { - string name = string_printf("ProxySession:%08" PRIX64 ":server", this->id); - - try { - for_each_received_command(this->server_bev.get(), this->version, this->server_input_crypt.get(), - [&](uint16_t command, uint32_t flag, string& data) { - print_received_command(command, flag, data.data(), data.size(), - this->version, name.c_str()); - - // In the case of server init commands, the client output crypt cannot - // be set until after we forwarwd the command to the client, hence this - // variable. - shared_ptr new_client_output_crypt; - bool should_forward = true; - switch (command) { - case 0x02: - case 0x17: { - if (this->version == GameVersion::PATCH && command == 0x17) { - throw invalid_argument("patch server sent 17 server init"); - } - if (this->version == GameVersion::BB) { - throw invalid_argument("console server init received on BB"); - } - - // Most servers don't include after_message or have a shorter - // after_message than newserv does, so don't require it - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_GC_02_17, after_message), - sizeof(S_ServerInit_DC_PC_GC_02_17)); - - if (!this->license) { - log(INFO, "[ProxyServer/%08" PRIX64 "] No license in linked session", - this->id); - - if ((this->version == GameVersion::PC) || (this->version == GameVersion::PATCH)) { - this->server_input_crypt.reset(new PSOPCEncryption(cmd.server_key)); - this->server_output_crypt.reset(new PSOPCEncryption(cmd.client_key)); - this->client_input_crypt.reset(new PSOPCEncryption(cmd.client_key)); - new_client_output_crypt.reset(new PSOPCEncryption(cmd.server_key)); - } else if (this->version == GameVersion::GC) { - this->server_input_crypt.reset(new PSOGCEncryption(cmd.server_key)); - this->server_output_crypt.reset(new PSOGCEncryption(cmd.client_key)); - this->client_input_crypt.reset(new PSOGCEncryption(cmd.client_key)); - new_client_output_crypt.reset(new PSOGCEncryption(cmd.server_key)); - } else { - throw invalid_argument("unsupported version"); - } - break; - - } else { - log(INFO, "[ProxyServer/%08" PRIX64 "] Existing license in linked session", - this->id); - - // This doesn't get forwarded to the client, so don't recreate the - // client's crypts - if (this->version == GameVersion::PATCH) { - throw logic_error("patch session is indirect"); - } else if (this->version == GameVersion::PC) { - this->server_input_crypt.reset(new PSOPCEncryption(cmd.server_key)); - this->server_output_crypt.reset(new PSOPCEncryption(cmd.client_key)); - } else if (this->version == GameVersion::GC) { - this->server_input_crypt.reset(new PSOGCEncryption(cmd.server_key)); - this->server_output_crypt.reset(new PSOGCEncryption(cmd.client_key)); - } else { - throw invalid_argument("unsupported version"); - } - - should_forward = false; - - // Respond with an appropriate login command. We don't let the - // client do this because it believes it already did (when it was - // in an unlinked session). - if (this->version == GameVersion::PC) { - C_Login_PC_9D cmd; - if (this->remote_guild_card_number == 0) { - cmd.player_tag = 0xFFFF0000; - cmd.guild_card_number = 0xFFFFFFFF; - } else { - cmd.player_tag = 0x00010000; - cmd.guild_card_number = this->remote_guild_card_number; - } - cmd.unused = 0xFFFFFFFFFFFF0000; - cmd.sub_version = this->sub_version; - cmd.unused2.data()[1] = 1; - cmd.serial_number = string_printf("%08" PRIX32 "", - this->license->serial_number); - cmd.access_key = this->license->access_key; - cmd.serial_number2 = cmd.serial_number; - cmd.access_key2 = cmd.access_key; - cmd.name = this->character_name; - send_command(this->server_bev.get(), this->version, - this->server_output_crypt.get(), 0x9D, 0, &cmd, sizeof(cmd), - name.c_str()); - break; - - } else if (this->version == GameVersion::GC) { - if (command == 0x17) { - C_VerifyLicense_GC_DB cmd; - cmd.serial_number = string_printf("%08" PRIX32 "", - this->license->serial_number); - cmd.access_key = this->license->access_key; - cmd.sub_version = this->sub_version; - cmd.serial_number2 = cmd.serial_number; - cmd.access_key2 = cmd.access_key; - cmd.password = this->license->gc_password; - send_command(this->server_bev.get(), this->version, - this->server_output_crypt.get(), 0xDB, 0, &cmd, sizeof(cmd), - name.c_str()); - break; - } - } else { - throw logic_error("invalid game version in server init handler"); - } - } - - // If we get here, then: - // - The session has a license - // - The session's version is GC - // - The received command is 02, not 17 - // The command should be handled like 9A at this point (we should - // send a 9E in response). GCC can't seem to understand this - // fallthrough label unless it's right here, so we can't put it in a - // more intuitive place (e.g. in an `else` above), unfortunately. - [[fallthrough]]; - } - - case 0x9A: { - if (!this->license) { - break; - } - if (this->version != GameVersion::GC) { - throw runtime_error("9A received in non-GC session"); - } - should_forward = false; - C_LoginWithUnusedSpace_GC_9E cmd; - if (this->remote_guild_card_number == 0) { - cmd.player_tag = 0xFFFF0000; - cmd.guild_card_number = 0xFFFFFFFF; - } else { - cmd.player_tag = 0x00010000; - cmd.guild_card_number = this->remote_guild_card_number; - } - cmd.sub_version = this->sub_version; - cmd.unused2.data()[1] = 1; - cmd.serial_number = string_printf("%08" PRIX32 "", - this->license->serial_number); - cmd.access_key = this->license->access_key; - cmd.serial_number2 = cmd.serial_number; - cmd.access_key2 = cmd.access_key; - cmd.name = this->character_name; - cmd.client_config.data = this->remote_client_config_data; - - // If there's a guild card number, a shorter 9E is sent that ends - // right after the client config data - send_command( - this->server_bev.get(), - this->version, - this->server_output_crypt.get(), - 0x9E, - 0x01, - &cmd, - sizeof(C_LoginWithUnusedSpace_GC_9E) - (this->remote_guild_card_number ? sizeof(cmd.unused_space) : 0), - name.c_str()); - break; - } - - case 0x04: { - // Some servers send a short 04 command if they don't use all of the - // 0x20 bytes available. We should be prepared to handle that. - auto& cmd = check_size_t(data, - offsetof(S_UpdateClientConfig_DC_PC_GC_04, cfg), - sizeof(S_UpdateClientConfig_DC_PC_GC_04)); - - // If this is a licensed session, hide the guild card number - // assigned by the remote server so the client doesn't see it - // change. If this is an unlicensed session, then the client never - // received a guild card number from newserv anyway, so it's safe to - // let the client see the number from the remote server. - bool had_guild_card_number = (this->remote_guild_card_number != 0); - this->remote_guild_card_number = cmd.guild_card_number; - log(INFO, "[ProxyServer/%08" PRIX64 "] Remote guild card number set to %" PRIX32, - this->id, this->remote_guild_card_number); - if (this->license) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.guild_card_number = this->license->serial_number; - } - - // It seems the client ignores the length of the 04 command, and - // always copies 0x20 bytes to its config data. So if the server - // sends a short 04 command, part of the previous command ends up in - // the security data (usually part of the copyright string from the - // server init command). We simulate that bug here. - // If there was previously a guild card number, assume we got the - // lobby server init text instead of the port map init text. - memcpy(this->remote_client_config_data.data(), - had_guild_card_number - ? "t Lobby Server. Copyright SEGA E" - : "t Port Map. Copyright SEGA Enter", - this->remote_client_config_data.bytes()); - memcpy(this->remote_client_config_data.data(), &cmd.cfg, - min(data.size() - sizeof(S_UpdateClientConfig_DC_PC_GC_04), - this->remote_client_config_data.bytes())); - - // If the guild card number was not set, pretend (to the server) - // that this is the first 04 command the client has received. The - // client responds with a 96 (checksum) in that case. - if (!had_guild_card_number) { - // We don't actually have a client checksum, of course... - // hopefully just random data will do (probably no private servers - // check this at all) - le_uint64_t checksum = random_object() & 0x0000FFFFFFFFFFFF; - send_command(this->server_bev.get(), this->version, - this->server_output_crypt.get(), 0x96, 0x00, &checksum, - sizeof(checksum), name.c_str()); - } - - break; - } - - case 0x06: { - if (this->license) { - auto& cmd = check_size_t(data, - sizeof(SC_TextHeader_01_06_11_B0), - 0xFFFF); - if (cmd.guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.guild_card_number = this->license->serial_number; - } - } - break; - } - case 0x41: { - if (this->license) { - if (this->version == GameVersion::PC) { - auto& cmd = check_size_t(data); - if (cmd.searcher_guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.searcher_guild_card_number = this->license->serial_number; - } - if (cmd.result_guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.result_guild_card_number = this->license->serial_number; - } - - } else if (this->version == GameVersion::GC) { - auto& cmd = check_size_t(data); - if (cmd.searcher_guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.searcher_guild_card_number = this->license->serial_number; - } - if (cmd.result_guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.result_guild_card_number = this->license->serial_number; - } - } - } - break; - } - - case 0x81: { - if (this->license) { - // TODO: Handle this on PC as well (the format isn't in - // CommandFormats yet) - if (this->version == GameVersion::GC) { - auto& cmd = check_size_t(data); - if (cmd.from_guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.from_guild_card_number = this->license->serial_number; - } - if (cmd.to_guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.to_guild_card_number = this->license->serial_number; - } - } - } - break; - } - - case 0x88: { - if (this->license) { - size_t expected_size = sizeof(S_ArrowUpdateEntry_88) * flag; - auto* entries = &check_size_t(data, - expected_size, expected_size); - for (size_t x = 0; x < flag; x++) { - if (entries[x].guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - entries[x].guild_card_number = this->license->serial_number; - } - } - } - break; - } - case 0xC4: { - if (this->license) { - // TODO: Implement this for PC too - if (this->version == GameVersion::GC) { - size_t expected_size = sizeof(S_ChoiceSearchResultEntry_GC_C4) * flag; - auto* entries = &check_size_t(data, - expected_size, expected_size); - for (size_t x = 0; x < flag; x++) { - if (entries[x].guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - entries[x].guild_card_number = this->license->serial_number; - } - } - } - } - break; - } - case 0xE4: { - if (this->license && this->version == GameVersion::GC) { - auto& cmd = check_size_t(data); - for (size_t x = 0; x < 4; x++) { - if (cmd.entries[x].guild_card_number == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.entries[x].guild_card_number = this->license->serial_number; - } - } - } - break; - } - - case 0x19: { - if (this->version == GameVersion::PATCH) { - break; - } - - auto& cmd = check_size_t(data); - memset(&this->next_destination, 0, sizeof(this->next_destination)); - struct sockaddr_in* sin = reinterpret_cast( - &this->next_destination); - sin->sin_family = AF_INET; - sin->sin_addr.s_addr = cmd.address.load_raw(); - sin->sin_port = htons(cmd.port); - - if (!this->client_bev.get()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Received reconnect command with no destination present", - this->id); - - } else { - // If the client is on a virtual connection (fd < 0), only change - // the port (so we'll know which version to treat the next - // connection as). It's better to leave the address as-is so we - // can circumvent the Plus/Ep3 same-network-server check. - int fd = bufferevent_getfd(this->client_bev.get()); - if (fd >= 0) { - struct sockaddr_storage sockname_ss; - socklen_t len = sizeof(sockname_ss); - getsockname(fd, reinterpret_cast(&sockname_ss), &len); - if (sockname_ss.ss_family != AF_INET) { - throw logic_error("existing connection is not ipv4"); - } - - struct sockaddr_in* sockname_sin = reinterpret_cast( - &sockname_ss); - cmd.address.store_raw(sockname_sin->sin_addr.s_addr); - cmd.port = ntohs(sockname_sin->sin_port); - - } else { - cmd.port = this->local_port; - } - } - break; - } - - case 0x1A: - case 0xD5: { - if (this->version != GameVersion::GC) { - break; - } - - // If the client has the no-close-confirmation flag set in its - // newserv client config, send a fake confirmation to the remote - // server immediately. - if (this->newserv_client_config.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) { - send_command(this->server_bev.get(), this->version, - this->server_output_crypt.get(), 0xD6, 0x00, "", 0, - name.c_str()); - } - break; - } - - case 0x60: - case 0x62: - case 0x6C: - case 0x6D: - case 0xC9: - case 0xCB: - check_implemented_subcommand(this->id, data); - break; - - case 0x44: - case 0xA6: { - if (this->version != GameVersion::GC) { - break; - } - if (!this->save_files) { - break; - } - - const auto& cmd = check_size_t(data); - bool is_download_quest = (command == 0xA6); - - string output_filename = string_printf("%s.%s.%" PRIu64, - cmd.filename.c_str(), - is_download_quest ? "download" : "online", now()); - for (size_t x = 0; x < output_filename.size(); x++) { - if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') { - output_filename[x] = '_'; - } - } - if (output_filename[0] == '.') { - output_filename[0] = '_'; - } - - SavingFile sf(cmd.filename, output_filename, cmd.file_size); - this->saving_files.emplace(cmd.filename, move(sf)); - log(INFO, "[ProxyServer/%08" PRIX64 "] Opened file %s", - this->id, output_filename.c_str()); - break; - } - - case 0x13: - case 0xA7: { - if (this->version != GameVersion::GC) { - break; - } - if (!this->save_files) { - break; - } - - const auto& cmd = check_size_t(data); - - SavingFile* sf = nullptr; - try { - sf = &this->saving_files.at(cmd.filename); - } catch (const out_of_range&) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Received data for non-open file %s", - this->id, cmd.filename.c_str()); - break; - } - - size_t bytes_to_write = cmd.data_size; - if (bytes_to_write > 0x400) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Chunk data size is invalid; truncating to 0x400", - this->id); - bytes_to_write = 0x400; - } - - log(INFO, "[ProxyServer/%08" PRIX64 "] Writing %zu bytes to %s", - this->id, bytes_to_write, - sf->output_filename.c_str()); - fwritex(sf->f.get(), cmd.data, bytes_to_write); - if (bytes_to_write > sf->remaining_bytes) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Chunk size extends beyond original file size; file may be truncated", - this->id); - sf->remaining_bytes = 0; - } else { - sf->remaining_bytes -= bytes_to_write; - } - - if (sf->remaining_bytes == 0) { - log(INFO, "[ProxyServer/%08" PRIX64 "] File %s is complete", - this->id, sf->output_filename.c_str()); - this->saving_files.erase(cmd.filename); - } - break; - } - - case 0xB8: { - if (this->version != GameVersion::GC) { - break; - } - if (!this->save_files) { - break; - } - if (data.size() < 4) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Card list data size is too small; skipping file", - this->id); - break; - } - - StringReader r(data); - size_t size = r.get_u32l(); - if (r.remaining() < size) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Card list data size extends beyond end of command; skipping file", - this->id); - break; - } - - string output_filename = string_printf("cardupdate.mnr.%" PRIu64, now()); - save_file(output_filename, r.read(size)); - - log(INFO, "[ProxyServer/%08" PRIX64 "] Wrote %zu bytes to %s", - this->id, size, output_filename.c_str()); - break; - } - - case 0x67: // join lobby - if (this->version != GameVersion::GC) { - break; - } - - this->lobby_players.clear(); - this->lobby_players.resize(12); - log(INFO, "[ProxyServer/%08" PRIX64 "] Cleared lobby players", - this->id); - - // This command can cause the client to no longer send D6 responses - // when 1A/D5 large message boxes are closed. newserv keeps track of - // this behavior in the client config, so if it happens during a - // proxy session, update the client config that we'll restore if the - // client uses the change ship or change block command. - if (this->newserv_client_config.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN) { - this->newserv_client_config.flags |= Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION; - } - - [[fallthrough]]; - - case 0x65: // other player joined game - case 0x68: { // other player joined lobby - if (this->version == GameVersion::PC) { - size_t expected_size = offsetof(S_JoinLobby_PC_65_67_68, entries) + sizeof(S_JoinLobby_GC_65_67_68::Entry) * flag; - auto& cmd = check_size_t(data, - expected_size, expected_size); - - this->lobby_client_id = cmd.client_id; - for (size_t x = 0; x < flag; x++) { - size_t index = cmd.entries[x].lobby_data.client_id; - if (index >= this->lobby_players.size()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Ignoring invalid player index %zu at position %zu", - this->id, index, x); - } else { - if (this->license && (cmd.entries[x].lobby_data.guild_card == this->remote_guild_card_number)) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.entries[x].lobby_data.guild_card = this->license->serial_number; - } - this->lobby_players[index].guild_card_number = cmd.entries[x].lobby_data.guild_card; - this->lobby_players[index].name = cmd.entries[x].disp.name; - log(INFO, "[ProxyServer/%08" PRIX64 "] Added lobby player: (%zu) %" PRIu32 " %s", - this->id, index, - this->lobby_players[index].guild_card_number, - this->lobby_players[index].name.c_str()); - } - } - - if (this->override_lobby_event >= 0) { - cmd.event = this->override_lobby_event; - } - if (this->override_lobby_number >= 0) { - cmd.lobby_number = this->override_lobby_number; - } - - } else if (this->version == GameVersion::GC) { - size_t expected_size = offsetof(S_JoinLobby_GC_65_67_68, entries) + sizeof(S_JoinLobby_GC_65_67_68::Entry) * flag; - auto& cmd = check_size_t(data, - expected_size, expected_size); - - this->lobby_client_id = cmd.client_id; - for (size_t x = 0; x < flag; x++) { - size_t index = cmd.entries[x].lobby_data.client_id; - if (index >= this->lobby_players.size()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Ignoring invalid player index %zu at position %zu", - this->id, index, x); - } else { - if (this->license && (cmd.entries[x].lobby_data.guild_card == this->remote_guild_card_number)) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.entries[x].lobby_data.guild_card = this->license->serial_number; - } - this->lobby_players[index].guild_card_number = cmd.entries[x].lobby_data.guild_card; - this->lobby_players[index].name = cmd.entries[x].disp.name; - log(INFO, "[ProxyServer/%08" PRIX64 "] Added lobby player: (%zu) %" PRIu32 " %s", - this->id, index, - this->lobby_players[index].guild_card_number, - this->lobby_players[index].name.c_str()); - } - } - - if (this->override_lobby_event >= 0) { - cmd.event = this->override_lobby_event; - } - if (this->override_lobby_number >= 0) { - cmd.lobby_number = this->override_lobby_number; - } - } - break; - } - - case 0x64: { // join game - // We don't need to clear lobby_players here because we always - // overwrite all 4 entries for this command - this->lobby_players.resize(4); - log(INFO, "[ProxyServer/%08" PRIX64 "] Cleared lobby players", - this->id); - - if (this->version == GameVersion::PC) { - auto& cmd = check_size_t(data); - - this->lobby_client_id = cmd.client_id; - for (size_t x = 0; x < flag; x++) { - if (cmd.lobby_data[x].guild_card == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.lobby_data[x].guild_card = this->license->serial_number; - } - this->lobby_players[x].guild_card_number = cmd.lobby_data[x].guild_card; - this->lobby_players[x].name.clear(); - log(INFO, "[ProxyServer/%08" PRIX64 "] Added lobby player: (%zu) %" PRIu32, - this->id, x, - this->lobby_players[x].guild_card_number); - } - - if (this->override_section_id >= 0) { - cmd.section_id = this->override_section_id; - } - if (this->override_lobby_event >= 0) { - cmd.event = this->override_lobby_event; - } - - } else if (this->version == GameVersion::GC) { - const size_t expected_size = this->sub_version >= 0x40 - ? sizeof(S_JoinGame_GC_64) - : offsetof(S_JoinGame_GC_64, players_ep3); - auto& cmd = check_size_t(data, - expected_size, expected_size); - - this->lobby_client_id = cmd.client_id; - for (size_t x = 0; x < flag; x++) { - if (cmd.lobby_data[x].guild_card == this->remote_guild_card_number) { - log(INFO, "[ProxyServer/%08" PRIX64 "] Overriding remote guild card number", - this->id); - cmd.lobby_data[x].guild_card = this->license->serial_number; - } - this->lobby_players[x].guild_card_number = cmd.lobby_data[x].guild_card; - if (data.size() == sizeof(S_JoinGame_GC_64)) { - this->lobby_players[x].name = cmd.players_ep3[x].disp.name; - } else { - this->lobby_players[x].name.clear(); - } - log(INFO, "[ProxyServer/%08" PRIX64 "] Added lobby player: (%zu) %" PRIu32 " %s", - this->id, x, - this->lobby_players[x].guild_card_number, - this->lobby_players[x].name.c_str()); - } - - if (this->override_section_id >= 0) { - cmd.section_id = this->override_section_id; - } - if (this->override_lobby_event >= 0) { - cmd.event = this->override_lobby_event; - } - } - break; - } - - case 0x66: - case 0x69: { - if (this->version != GameVersion::GC) { - break; - } - - const auto& cmd = check_size_t(data); - size_t index = cmd.client_id; - if (index >= this->lobby_players.size()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Lobby leave command references missing position", - this->id); - } else { - this->lobby_players[index].guild_card_number = 0; - this->lobby_players[index].name.clear(); - log(INFO, "[ProxyServer/%08" PRIX64 "] Removed lobby player (%zu)", - this->id, index); - } - break; - } - } - - if (should_forward) { - if (!this->client_bev.get()) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] No client is present; dropping command", - this->id); - } else { - // Note: we intentionally don't pass name_str here because we already - // printed the command above - send_command(this->client_bev.get(), this->version, - this->client_output_crypt.get(), command, flag, - data.data(), data.size()); - } - } - - if (new_client_output_crypt.get()) { - this->client_output_crypt = new_client_output_crypt; - } - }); - - } catch (const exception& e) { - log(ERROR, "[ProxyServer/%08" PRIX64 "] Failed to process command from server: %s", - this->id, e.what()); - this->disconnect(); - } + for_each_received_command(this->server_bev.get(), this->version, this->server_input_crypt.get(), + [&](uint16_t command, uint32_t flag, string& data) { + print_received_command(command, flag, data.data(), data.size(), + this->version, this->server_name.c_str()); + process_proxy_command( + this->server->state, + *this, + true, // from_server + command, + flag, + data); + }); } void ProxyServer::LinkedSession::send_to_end( - const void* data, size_t size, bool to_server) { - size_t header_size = PSOCommandHeader::header_size(this->version); - if (size < header_size) { - throw runtime_error("command is too small for header"); - } + bool to_server, + uint16_t command, + uint32_t flag, + const void* data, + size_t size) { if (size & 3) { throw runtime_error("command size is not a multiple of 4"); } - const auto* header = reinterpret_cast(data); - - string name = string_printf("ProxySession:%08" PRIX64 ":shell:%s", + string name = string_printf("LinkedSession:%08" PRIX64 ":synthetic:%s", this->id, to_server ? "server" : "client"); send_command( to_server ? this->server_bev.get() : this->client_bev.get(), this->version, to_server ? this->server_output_crypt.get() : this->client_output_crypt.get(), - header->command(this->version), - header->flag(this->version), - reinterpret_cast(data) + header_size, - size - header_size, + command, + flag, + data, + size, name.c_str()); } -void ProxyServer::LinkedSession::send_to_end(const string& data, bool to_server) { - this->send_to_end(data.data(), data.size(), to_server); +void ProxyServer::LinkedSession::send_to_end( + bool to_server, uint16_t command, uint32_t flag, const string& data) { + this->send_to_end(to_server, command, flag, data.data(), data.size()); +} + +void ProxyServer::LinkedSession::send_to_end_with_header( + bool to_server, const void* data, size_t size) { + size_t header_size = PSOCommandHeader::header_size(this->version); + if (size < header_size) { + throw runtime_error("command is too small for header"); + } + const auto* header = reinterpret_cast(data); + this->send_to_end( + to_server, + header->command(this->version), + header->flag(this->version), + reinterpret_cast(data) + header_size, + size - header_size); +} + +void ProxyServer::LinkedSession::send_to_end_with_header( + bool to_server, const string& data) { + this->send_to_end_with_header(to_server, data.data(), data.size()); } shared_ptr ProxyServer::get_session() { @@ -1532,13 +665,13 @@ std::shared_ptr ProxyServer::create_licensed_session if (!emplace_ret.second) { throw runtime_error("session already exists for this license"); } - log(INFO, "[ProxyServer/%08" PRIX64 "] Opening licensed session", session->id); + session->log(INFO, "Opening licensed session"); return emplace_ret.first->second; } void ProxyServer::delete_session(uint64_t id) { if (this->id_to_session.erase(id)) { - log(WARNING, "[ProxyServer/%08" PRIX64 "] Closed session", id); + log(INFO, "Closed LinkedSession:%08" PRIX64, id); } } diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 1594dc66..72b33399 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -35,6 +35,9 @@ public: struct LinkedSession { ProxyServer* server; uint64_t id; + std::string client_name; + std::string server_name; + PrefixedLogger log; std::unique_ptr timeout_event; @@ -126,8 +129,13 @@ public: void on_stream_error(short events, bool is_server_stream); void on_timeout(); - void send_to_end(const void* data, size_t size, bool to_server); - void send_to_end(const std::string& data, bool to_server); + void send_to_end(bool to_server, uint16_t command, uint32_t flag, + const void* data = nullptr, size_t size = 0); + void send_to_end(bool to_server, uint16_t command, uint32_t flag, + const std::string& data); + void send_to_end_with_header( + bool to_server, const void* data, size_t size); + void send_to_end_with_header(bool to_server, const std::string& data); void disconnect(); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 73195e60..f8a4791a 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -283,16 +283,13 @@ Proxy commands (these will only work when exactly one client is connected):\n\ if (data.size() == 0) { throw invalid_argument("no data given"); } - uint16_t* size_field = reinterpret_cast(data.data() + 2); - *size_field = data.size(); - session->send_to_end(data, to_server); + session->send_to_end_with_header(to_server, data); } else if ((command_name == "chat") || (command_name == "dchat")) { auto session = this->get_proxy_session(); - string data(12, '\0'); - data[0] = 0x06; + string data(8, '\0'); data.push_back('\x09'); data.push_back('E'); if (command_name == "dchat") { @@ -302,36 +299,28 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } data.push_back('\0'); data.resize((data.size() + 3) & (~3)); - uint16_t* size_field = reinterpret_cast(data.data() + 2); - *size_field = data.size(); - session->send_to_end(data, true); + session->send_to_end(true, 0x06, 0x00, data); } else if (command_name == "marker") { auto session = this->get_proxy_session(); - - string data("\x89\x00\x04\x00", 4); - data[1] = stod(command_args); - - session->send_to_end(data, true); + session->send_to_end(true, 0x89, stoul(command_args)); } else if (command_name == "warp") { auto session = this->get_proxy_session(); - PSOSubcommand cmds[3]; - cmds[0].dword = 0x000C0060; // header (60 00 0C 00) - cmds[1].word[0] = 0x0294; - cmds[1].word[1] = session->lobby_client_id; - cmds[2].dword = stoul(command_args); + PSOSubcommand cmds[2]; + cmds[0].word[0] = 0x0294; + cmds[0].word[1] = session->lobby_client_id; + cmds[1].dword = stoul(command_args); - session->send_to_end(&cmds, sizeof(cmds), false); - session->send_to_end(&cmds, sizeof(cmds), true); + session->send_to_end(false, 0x60, 0x00, &cmds, sizeof(cmds)); + session->send_to_end(true, 0x60, 0x00, &cmds, sizeof(cmds)); } else if ((command_name == "info-board") || (command_name == "info-board-data")) { auto session = this->get_proxy_session(); - string data(4, '\0'); - data[0] = 0xD9; + string data; if (command_name == "info-board-data") { data += parse_data_string(command_args); } else { @@ -339,10 +328,8 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } data.push_back('\0'); data.resize((data.size() + 3) & (~3)); - uint16_t* size_field = reinterpret_cast(data.data() + 2); - *size_field = data.size(); - session->send_to_end(data, true); + session->send_to_end(true, 0xD9, 0x00, data); } else if (command_name == "set-override-section-id") { auto session = this->get_proxy_session(); @@ -358,9 +345,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ session->override_lobby_event = -1; } else { session->override_lobby_event = event_for_name(command_args); - string data("\xDA\x00\x04\x00", 4); - data[1] = session->override_lobby_event; - session->send_to_end(data, false); + session->send_to_end(false, 0xDA, session->override_lobby_event); } } else if (command_name == "set-override-lobby-number") {