diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 05f214f6..a7615df3 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -124,7 +124,7 @@ static void server_command_lobby_info(shared_ptr, shared_ptr static void proxy_command_lobby_info(shared_ptr, ProxyServer::LinkedSession& session, const std::u16string&) { - string msg = string_printf("$C7GC: $C6%" PRIu32 "\n$C7Client ID: $C6%zu%s", + string msg = string_printf("$C7GC: $C6%" PRId64 "\n$C7Client ID: $C6%zu%s", session.remote_guild_card_number, session.lobby_client_id, (session.leader_client_id == session.lobby_client_id) ? " (L)" : ""); diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index b484de4e..d0dfc017 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -259,7 +259,17 @@ struct S_StartFileDownloads_Patch_11 { // size of this command is 0x2004 bytes, including the header. // 14 (S->C): Reconnect -// Same format and usage as command 19 on the game server (described below). +// Same format and usage as command 19 on the game server (described below), +// except the port field is big-endian for some reason. + +template +struct S_Reconnect { + be_uint32_t address; + PortT port; + le_uint16_t unused; +}; + +struct S_Reconnect_Patch_14 : S_Reconnect { }; // 15 (S->C): Login failure // No arguments @@ -618,11 +628,7 @@ struct C_WriteFileConfirmation_V3_BB_13_A7 { // Note: PSO XB seems to ignore the address field, which makes sense given its // networking architecture. -struct S_Reconnect_19 { - be_uint32_t address; - le_uint16_t port; - le_uint16_t unused; -}; +struct S_Reconnect_19 : S_Reconnect { }; // Because PSO PC and some versions of PSO DC/GC use the same port but different // protocols, we use a specially-crafted 19 command to send them to two diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 596d40fb..c0aadf0a 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -132,7 +132,7 @@ static HandlerResult process_server_gc_9A(shared_ptr, } C_LoginExtended_GC_9E cmd; - if (session.remote_guild_card_number == 0) { + if (session.remote_guild_card_number < 0) { cmd.player_tag = 0xFFFF0000; cmd.guild_card_number = 0xFFFFFFFF; } else { @@ -141,7 +141,7 @@ static HandlerResult process_server_gc_9A(shared_ptr, } cmd.unused = 0; cmd.sub_version = session.sub_version; - cmd.is_extended = session.remote_guild_card_number ? 0 : 1; + cmd.is_extended = (session.remote_guild_card_number < 0) ? 0 : 1; cmd.language = session.language; cmd.serial_number = string_printf("%08" PRIX32 "", session.license->serial_number); cmd.access_key = session.license->access_key; @@ -227,47 +227,82 @@ static HandlerResult process_server_dc_pc_v3_patch_02_17( } else if ((session.version == GameVersion::DC) || (session.version == GameVersion::PC)) { if (session.newserv_client_config.cfg.flags & Client::Flag::DCV1) { - C_LoginV1_DC_93 cmd; - if (session.remote_guild_card_number == 0) { - cmd.player_tag = 0xFFFF0000; - cmd.guild_card_number = 0xFFFFFFFF; + if (command == 0x17) { + C_LoginV1_DC_PC_V3_90 cmd; + cmd.serial_number = string_printf("%08" PRIX32 "", + session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.access_key.clear_after(8); + session.server_channel.send(0x90, 0x00, &cmd, sizeof(cmd)); + return HandlerResult::Type::SUPPRESS; } else { - cmd.player_tag = 0x00010000; - cmd.guild_card_number = session.remote_guild_card_number; + C_LoginV1_DC_93 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.unknown_a1 = 0; + cmd.unknown_a2 = 0; + cmd.sub_version = session.sub_version; + cmd.is_extended = 0; + cmd.language = session.language; + cmd.serial_number = string_printf("%08" PRIX32 "", + session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.access_key.clear_after(8); + cmd.hardware_id = session.hardware_id; + cmd.name = session.character_name; + session.server_channel.send(0x93, 0x00, &cmd, sizeof(cmd)); + return HandlerResult::Type::SUPPRESS; } - cmd.unknown_a1 = 0; - cmd.unknown_a2 = 0; - cmd.sub_version = session.sub_version; - cmd.is_extended = 0; - cmd.language = session.language; - cmd.serial_number = string_printf("%08" PRIX32 "", - session.license->serial_number); - cmd.access_key = session.license->access_key; - cmd.hardware_id = session.hardware_id; - cmd.name = session.character_name; - session.server_channel.send(0x93, 0x00, &cmd, sizeof(cmd)); - return HandlerResult::Type::SUPPRESS; - } else { - C_Login_DC_PC_GC_9D cmd; - if (session.remote_guild_card_number == 0) { - cmd.player_tag = 0xFFFF0000; - cmd.guild_card_number = 0xFFFFFFFF; + } else { // DCv2 or PC + if (command == 0x17) { + C_Login_DC_PC_V3_9A 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.serial_number = string_printf("%08" PRIX32 "", + session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.access_key.clear_after(8); + cmd.serial_number2 = cmd.serial_number; + cmd.access_key2 = cmd.access_key; + // TODO: We probably should set email_address, but we currently don't + // keep that value anywhere in the session object, nor is it saved in + // the License object. + session.server_channel.send(0x9A, 0x00, &cmd, sizeof(cmd)); + return HandlerResult::Type::SUPPRESS; } else { - cmd.player_tag = 0x00010000; - cmd.guild_card_number = session.remote_guild_card_number; + C_Login_DC_PC_GC_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 = 0; + cmd.sub_version = session.sub_version; + cmd.is_extended = 0; + cmd.language = session.language; + cmd.serial_number = string_printf("%08" PRIX32 "", + session.license->serial_number); + cmd.access_key = session.license->access_key; + cmd.access_key.clear_after(8); + cmd.serial_number2 = cmd.serial_number; + cmd.access_key2 = cmd.access_key; + cmd.name = session.character_name; + session.server_channel.send(0x9D, 0x00, &cmd, sizeof(cmd)); + return HandlerResult::Type::SUPPRESS; } - cmd.unused = 0xFFFFFFFFFFFF0000; - cmd.sub_version = session.sub_version; - cmd.is_extended = 0; - cmd.language = session.language; - 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.server_channel.send(0x9D, 0x00, &cmd, sizeof(cmd)); - return HandlerResult::Type::SUPPRESS; } } else if (session.version == GameVersion::GC) { @@ -365,13 +400,13 @@ static HandlerResult process_server_dc_pc_v3_04(shared_ptr, // 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); + bool had_guild_card_number = (session.remote_guild_card_number >= 0); if (session.remote_guild_card_number != cmd.guild_card_number) { session.remote_guild_card_number = cmd.guild_card_number; - session.log.info("Remote guild card number set to %" PRIu32, + session.log.info("Remote guild card number set to %" PRId64, session.remote_guild_card_number); send_text_message_to_client(session, 0x11, string_printf( - "The remote server\nhas assigned your\nGuild Card number as\n\tC6%" PRIu32, + "The remote server\nhas assigned your\nGuild Card number:\n\tC6%" PRId64, session.remote_guild_card_number)); } if (session.license) { @@ -636,7 +671,7 @@ static HandlerResult process_server_bb_22(shared_ptr, } static HandlerResult process_server_game_19_patch_14(shared_ptr, - ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) { + ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { // If the command is shorter than 6 bytes, use the previous server command to // fill it in. This simulates a behavior used by some private servers where a // longer previous command is used to fill part of the client's receive buffer @@ -657,52 +692,47 @@ static HandlerResult process_server_game_19_patch_14(shared_ptr, session.remote_ip_crc = crc32(data.data(), 4); } - // 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); + // Set the destination netloc appropriately 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.version == GameVersion::PATCH) { + auto& cmd = check_size_t(data); + sin->sin_addr.s_addr = cmd.address.load_raw(); // Already big-endian + sin->sin_port = htons(cmd.port); + } else { + // 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), 0xFFFF); + sin->sin_addr.s_addr = cmd.address.load_raw(); // Already big-endian + sin->sin_port = htons(cmd.port); + } if (!session.client_channel.connected()) { session.log.warning("Received reconnect command with no destination present"); return HandlerResult::Type::SUPPRESS; - } else if (command == 0x14) { - // On the patch server, hide redirects from the client completely. The new - // destination server will presumably send a new 02 command to start - // encryption; it appears that PSOBB doesn't fail if this happens, and - // simply re-initializes its encryption appropriately. + } else if (session.version != GameVersion::BB) { + // Hide redirects from the client completely. The new destination server + // will presumably send a new encryption init command, which the handlers + // will appropriately respond to. session.server_channel.crypt_in.reset(); session.server_channel.crypt_out.reset(); - struct sockaddr_in* dest_sin = reinterpret_cast( - &session.next_destination); - dest_sin->sin_family = AF_INET; - dest_sin->sin_addr.s_addr = cmd.address.load_raw(); - dest_sin->sin_port = cmd.port; + // We already modified next_destination, so start the connection process session.connect(); return HandlerResult::Type::SUPPRESS; } 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. - if (session.client_channel.is_virtual_connection) { - cmd.port = session.local_port; - } else { - const struct sockaddr_in* sin = reinterpret_cast( - &session.client_channel.local_addr); - if (sin->sin_family != AF_INET) { - throw logic_error("existing connection is not ipv4"); - } - cmd.address.store_raw(sin->sin_addr.s_addr); - cmd.port = ntohs(sin->sin_port); + const struct sockaddr_in* sin = reinterpret_cast( + &session.client_channel.local_addr); + if (sin->sin_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); } + auto& cmd = check_size_t(data, sizeof(S_Reconnect_19), 0xFFFF); + cmd.address.store_raw(sin->sin_addr.s_addr); + cmd.port = ntohs(sin->sin_port); return HandlerResult::Type::MODIFIED; } } @@ -1135,7 +1165,7 @@ HandlerResult process_client_60_62_6C_6D_C9_CB(shared_ptr, return HandlerResult::Type::FORWARD; } -static HandlerResult process_client_dc_pc_v3_A0_A1(shared_ptr s, +static HandlerResult process_client_dc_pc_v3_A0_A1(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) { if (!session.license) { return HandlerResult::Type::FORWARD; @@ -1143,58 +1173,7 @@ static HandlerResult process_client_dc_pc_v3_A0_A1(shared_ptr s, // 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_Ep3_E9 cmd = {leaving_id, leader_id, 0}; - session.client_channel.send(0x69, leaving_id, &cmd, sizeof(cmd)); - } - - string encoded_name = encode_sjis(s->name); - send_text_message_to_client(session, 0x11, string_printf( - "You\'ve returned to\n\tC6%s", encoded_name.c_str())); - - // Restore newserv_client_config, so the login server gets the client flags - S_UpdateClientConfig_DC_PC_V3_04 update_client_config_cmd; - update_client_config_cmd.player_tag = 0x00010000; - update_client_config_cmd.guild_card_number = session.license->serial_number; - update_client_config_cmd.cfg = session.newserv_client_config.cfg; - session.client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); - - static const vector version_to_port_name({ - "console-login", "pc-login", "bb-patch", "console-login", "console-login", "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). - if (session.client_channel.is_virtual_connection) { - 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 { - const struct sockaddr_in* sin = reinterpret_cast( - &session.client_channel.local_addr); - if (sin->sin_family != AF_INET) { - throw logic_error("existing connection is not ipv4"); - } - reconnect_cmd.address.store_raw(sin->sin_addr.s_addr); - } - - session.client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd)); - + session.send_to_game_server(); return HandlerResult::Type::SUPPRESS; } @@ -1486,6 +1465,12 @@ void process_proxy_command( } } catch (const exception& e) { session.log.error("Failed to process command: %s", e.what()); - session.disconnect(); + if (from_server) { + string error_str = "Error: "; + error_str += e.what(); + session.send_to_game_server(error_str.c_str()); + } else { + session.disconnect(); + } } } diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index c4d3bfb6..f39416cb 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -477,7 +477,7 @@ ProxyServer::LinkedSession::LinkedSession( version(version), sub_version(0), // This is set during resume() language(1), // Default = English. This is also set during resume() - remote_guild_card_number(0), + remote_guild_card_number(-1), enable_chat_filter(true), switch_assist(false), infinite_hp(false), @@ -655,10 +655,69 @@ void ProxyServer::LinkedSession::on_error(Channel& ch, short events) { if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { session->log.info("%s has disconnected", is_server_stream ? "Server" : "Client"); + // If the server disconnected, send the client back to the game server so + // they're not disconnected completely. + if (is_server_stream) { + session->send_to_game_server("The server has\ndisconnected."); + } session->disconnect(); } } +void ProxyServer::LinkedSession::send_to_game_server(const char* error_message) { + // 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_Ep3_E9 cmd = {leaving_id, leader_id, 0}; + this->client_channel.send(0x69, leaving_id, &cmd, sizeof(cmd)); + } + + string encoded_name = encode_sjis(this->server->state->name); + send_ship_info(this->client_channel, decode_sjis(string_printf( + "You\'ve returned to\n\tC6%s$C7\n\n%s", encoded_name.c_str(), + error_message ? error_message : ""))); + + // Restore newserv_client_config, so the login server gets the client flags + S_UpdateClientConfig_DC_PC_V3_04 update_client_config_cmd; + update_client_config_cmd.player_tag = 0x00010000; + update_client_config_cmd.guild_card_number = this->license->serial_number; + update_client_config_cmd.cfg = this->newserv_client_config.cfg; + this->client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); + + static const vector version_to_port_name({ + "console-login", "pc-login", "bb-patch", "console-login", "console-login", "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). + if (this->client_channel.is_virtual_connection) { + 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 { + const struct sockaddr_in* sin = reinterpret_cast( + &this->client_channel.local_addr); + if (sin->sin_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); + } + reconnect_cmd.address.store_raw(sin->sin_addr.s_addr); + } + + this->client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd)); +} + void ProxyServer::LinkedSession::disconnect() { // Forward the disconnection to the other end this->client_channel.disconnect(); diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 4c5d4d36..2ea0f082 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -57,7 +57,7 @@ public: std::string hardware_id; // Only used for DC sessions std::string login_command_bb; - uint32_t remote_guild_card_number; + int64_t remote_guild_card_number; parray remote_client_config_data; ClientConfigBB newserv_client_config; bool enable_chat_filter; @@ -145,6 +145,7 @@ public: static void on_error(Channel& ch, short events); void on_timeout(); + void send_to_game_server(const char* error_message = nullptr); void disconnect(); bool is_connected() const; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 668b0a19..d3c55996 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -568,6 +568,10 @@ void send_ship_info(shared_ptr c, const u16string& text) { send_header_text(c->channel, 0x11, 0, text, true); } +void send_ship_info(Channel& ch, const u16string& text) { + send_header_text(ch, 0x11, 0, text, true); +} + void send_text_message(Channel& ch, const std::u16string& text) { send_header_text(ch, 0xB0, 0, text, true); } diff --git a/src/SendCommands.hh b/src/SendCommands.hh index b1f6aedd..85018d55 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -149,6 +149,7 @@ void send_quest_info(std::shared_ptr c, const std::u16string& text, bool is_download_quest); void send_lobby_message_box(std::shared_ptr c, const std::u16string& text); void send_ship_info(std::shared_ptr c, const std::u16string& text); +void send_ship_info(Channel& ch, const std::u16string& text); void send_text_message(Channel& ch, const std::u16string& text); void send_text_message(std::shared_ptr c, const std::u16string& text); void send_text_message(std::shared_ptr l, const std::u16string& text);