diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index a17b13e6..7629b40d 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -130,7 +130,7 @@ struct C_Login_Patch_04 { parray unused; ptext username; ptext password; - ptext email; // Note: this field is not present on BB + ptext email; // Note: this field is blank on BB }; // 05: Disconnect @@ -684,7 +684,11 @@ struct C_Login_BB_93 { // will be something like "Ver. 1.24.3". Note also that some old versions // (before 1.23.8?) omit the unknown field before the client config, so the // client config starts 8 bytes earlier on those versions. - ClientConfigBB client_config; + union ClientConfigFields { + ClientConfigBB cfg; + ptext version_string; + ClientConfigFields() : version_string() { } + } client_config; }; // 94: Invalid command diff --git a/src/Main.cc b/src/Main.cc index 04097869..4d239bfd 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -31,29 +31,31 @@ bool use_terminal_colors = false; static const vector default_port_to_behavior({ - {"gc-jp10", 9000, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-jp11", 9001, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-jp3", 9003, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-us10", 9100, GameVersion::PC, ServerBehavior::SPLIT_RECONNECT}, - {"gc-us3", 9103, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-eu10", 9200, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-eu11", 9201, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-eu3", 9203, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"pc-login", 9300, GameVersion::PC, ServerBehavior::LOGIN_SERVER}, - {"pc-patch", 10000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, - {"bb-patch", 11000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, - {"bb-data", 12000, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, + {"gc-jp10", 9000, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"gc-jp11", 9001, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"gc-jp3", 9003, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"gc-us10", 9100, GameVersion::PC, ServerBehavior::SPLIT_RECONNECT}, + {"gc-us3", 9103, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"gc-eu10", 9200, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"gc-eu11", 9201, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"gc-eu3", 9203, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, + {"pc-login", 9300, GameVersion::PC, ServerBehavior::LOGIN_SERVER}, + {"pc-patch", 10000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, + {"bb-patch", 11000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, + {"bb-init", 12000, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, + {"bb-patch2", 13000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, + {"bb-init2", 14000, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - // these aren't hardcoded in any games; user can override them - {"bb-data1", 12004, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - {"bb-data2", 12005, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - {"bb-login", 12008, GameVersion::BB, ServerBehavior::LOGIN_SERVER}, - {"pc-lobby", 9420, GameVersion::PC, ServerBehavior::LOBBY_SERVER}, - {"gc-lobby", 9421, GameVersion::GC, ServerBehavior::LOBBY_SERVER}, - {"bb-lobby", 9422, GameVersion::BB, ServerBehavior::LOBBY_SERVER}, - {"pc-proxy", 9520, GameVersion::PC, ServerBehavior::PROXY_SERVER}, - {"gc-proxy", 9521, GameVersion::GC, ServerBehavior::PROXY_SERVER}, - {"bb-proxy", 9522, GameVersion::BB, ServerBehavior::PROXY_SERVER}, + // These aren't hardcoded in clients; user should be allowed to override them + {"bb-data1", 12004, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, + {"bb-data2", 12005, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, + {"bb-login", 12008, GameVersion::BB, ServerBehavior::LOGIN_SERVER}, + {"pc-lobby", 9420, GameVersion::PC, ServerBehavior::LOBBY_SERVER}, + {"gc-lobby", 9421, GameVersion::GC, ServerBehavior::LOBBY_SERVER}, + {"bb-lobby", 9422, GameVersion::BB, ServerBehavior::LOBBY_SERVER}, + {"pc-proxy", 9520, GameVersion::PC, ServerBehavior::PROXY_SERVER}, + {"gc-proxy", 9521, GameVersion::GC, ServerBehavior::PROXY_SERVER}, + {"bb-proxy", 9522, GameVersion::BB, ServerBehavior::PROXY_SERVER}, }); @@ -157,6 +159,19 @@ void populate_state_from_config(shared_ptr s, s->proxy_destination_patch.first = ""; s->proxy_destination_patch.second = 0; } + try { + const string& netloc_str = d.at("ProxyDestination-BB")->as_string(); + s->proxy_destination_bb = parse_netloc(netloc_str); + log(INFO, "BB proxy is enabled with destination %s", netloc_str.c_str()); + for (auto& it : s->name_to_port_config) { + if (it.second->version == GameVersion::BB) { + it.second->behavior = ServerBehavior::PROXY_SERVER; + } + } + } catch (const out_of_range&) { + s->proxy_destination_bb.first = ""; + s->proxy_destination_bb.second = 0; + } s->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby", u"Join the lobby", 0); @@ -322,30 +337,39 @@ int main(int, char**) { } shared_ptr game_server; - if (!state->proxy_destinations_pc.empty() || - !state->proxy_destinations_gc.empty() || - (state->proxy_destination_patch.second != 0)) { - log(INFO, "Starting proxy server"); - state->proxy_server.reset(new ProxyServer(base, state)); - } - - log(INFO, "Starting game server"); - game_server.reset(new Server(base, state)); log(INFO, "Opening sockets"); for (const auto& it : state->name_to_port_config) { if (it.second->behavior == ServerBehavior::PROXY_SERVER) { + if (!state->proxy_server.get()) { + log(INFO, "Starting proxy server"); + state->proxy_server.reset(new ProxyServer(base, state)); + } if (state->proxy_server.get()) { + // For PC and GC, proxy sessions are dynamically created when a client + // picks a destination from the menu. For patch and BB clients, there's + // no way to ask the client which destination they want, so only one + // destination is supported, and we have to manually specify the + // destination netloc here. if (it.second->version == GameVersion::PATCH) { struct sockaddr_storage ss = make_sockaddr_storage( state->proxy_destination_patch.first, state->proxy_destination_patch.second).first; state->proxy_server->listen(it.second->port, it.second->version, &ss); + } if (it.second->version == GameVersion::BB) { + struct sockaddr_storage ss = make_sockaddr_storage( + state->proxy_destination_bb.first, + state->proxy_destination_bb.second).first; + state->proxy_server->listen(it.second->port, it.second->version, &ss); } else { state->proxy_server->listen(it.second->port, it.second->version); } } } else { + if (!game_server.get()) { + log(INFO, "Starting game server"); + game_server.reset(new Server(base, state)); + } game_server->listen("", it.second->port, it.second->version, it.second->behavior); } } diff --git a/src/PSOProtocol.cc b/src/PSOProtocol.cc index 9e6b6c93..f0a685ba 100644 --- a/src/PSOProtocol.cc +++ b/src/PSOProtocol.cc @@ -155,7 +155,7 @@ void for_each_received_command( // BB pads commands to 8-byte boundaries, and this is not reflected in the // size field size_t command_physical_size = (version == GameVersion::BB) - ? (command_logical_size + header_size - 1) & ~(header_size - 1) + ? ((command_logical_size + header_size - 1) & ~(header_size - 1)) : command_logical_size; if (evbuffer_get_length(buf) < command_physical_size) { break; @@ -165,19 +165,17 @@ void for_each_received_command( evbuffer_drain(buf, header_size); - string command_data(command_logical_size - header_size, '\0'); + string command_data(command_physical_size - header_size, '\0'); if (evbuffer_remove(buf, command_data.data(), command_data.size()) < static_cast(command_data.size())) { throw logic_error("enough bytes available, but could not remove them"); } - if (command_logical_size != command_physical_size) { - evbuffer_drain(buf, command_physical_size - command_logical_size); - } if (crypt) { crypt->skip(header_size); crypt->decrypt(command_data.data(), command_data.size()); } + command_data.resize(command_logical_size - header_size); fn(header.command(version), header.flag(version), command_data); } diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index c39e7fa6..f65339e8 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -115,8 +115,7 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr s, // 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)); + offsetof(S_ServerInit_DC_PC_GC_02_17, after_message), 0xFFFF); if (!session.license) { session.log(INFO, "No license in linked session"); @@ -202,6 +201,38 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr s, } } +static bool process_server_bb_03(shared_ptr s, + ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) { + // 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_BB_03, after_message), 0xFFFF); + + // Unlike on PC/GC, BB linked sessions never have licenses. + // TODO: Is there any way we can support this in the future? Probably not, due + // to BB's login and character select flow, right? + if (session.license) { + throw runtime_error("BB linked session has 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); + + // BB encryption is stateless after it's initialized, unlike previous + // versions, so we can get away with only two instances instead of four here. + session.server_input_crypt.reset(new PSOBBEncryption( + s->default_key_file, cmd.server_key.data(), sizeof(cmd.server_key))); + session.server_output_crypt.reset(new PSOBBEncryption( + s->default_key_file, cmd.client_key.data(), sizeof(cmd.client_key))); + session.client_input_crypt = session.server_output_crypt; + session.client_output_crypt = session.server_input_crypt; + + return false; +} + 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 @@ -401,7 +432,7 @@ static bool process_server_gc_1A_D5(shared_ptr, // 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) { + if (session.newserv_client_config.cfg.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) { session.send_to_end(true, 0xD6, 0x00, "", 0); } return true; @@ -514,8 +545,8 @@ static bool process_server_65_67_68(shared_ptr, // 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; + if (session.newserv_client_config.cfg.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN) { + session.newserv_client_config.cfg.flags |= Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION; } } @@ -714,7 +745,7 @@ static bool process_client_dc_pc_gc_A0_A1(shared_ptr s, S_UpdateClientConfig_DC_PC_GC_04 update_client_config_cmd = { 0x00010000, session.license->serial_number, - session.newserv_client_config, + session.newserv_client_config.cfg, }; session.send_to_end(false, 0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); @@ -824,7 +855,7 @@ static process_command_t gc_server_handlers[0x100] = { /* 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, + /* 00 */ defh, defh, defh, process_server_bb_03, 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, diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index fdf5b187..b2a5d82b 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -240,7 +240,7 @@ void ProxyServer::UnlinkedSession::on_client_input() { shared_ptr license; uint32_t sub_version = 0; string character_name; - ClientConfig client_config; + ClientConfigBB client_config; try { for_each_received_command(this->bev.get(), this->version, this->crypt_in.get(), @@ -271,7 +271,7 @@ void ProxyServer::UnlinkedSession::on_client_input() { stoul(cmd.serial_number, nullptr, 16), cmd.access_key); sub_version = cmd.sub_version; character_name = cmd.name; - client_config = cmd.client_config.cfg; + client_config.cfg = cmd.client_config.cfg; } else { throw logic_error("unsupported unlinked session version"); @@ -302,7 +302,7 @@ void ProxyServer::UnlinkedSession::on_client_input() { // If there's no open session for this license, then there must be a valid // destination in the client config. If there is, open a new linked // session and set its initial destination - if (client_config.magic != CLIENT_CONFIG_MAGIC) { + if (client_config.cfg.magic != CLIENT_CONFIG_MAGIC) { this->log(ERROR, "Client configuration is invalid; cannot open session"); } else { session.reset(new LinkedSession( @@ -388,15 +388,15 @@ ProxyServer::LinkedSession::LinkedSession( uint16_t local_port, GameVersion version, std::shared_ptr license, - const ClientConfig& newserv_client_config) + const ClientConfigBB& newserv_client_config) : LinkedSession(server, license->serial_number, local_port, version) { this->license = license; this->newserv_client_config = newserv_client_config; memset(&this->next_destination, 0, sizeof(this->next_destination)); struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); dest_sin->sin_family = AF_INET; - dest_sin->sin_port = htons(this->newserv_client_config.proxy_destination_port); - dest_sin->sin_addr.s_addr = htonl(this->newserv_client_config.proxy_destination_address); + dest_sin->sin_port = htons(this->newserv_client_config.cfg.proxy_destination_port); + dest_sin->sin_addr.s_addr = htonl(this->newserv_client_config.cfg.proxy_destination_address); } ProxyServer::LinkedSession::LinkedSession( @@ -668,7 +668,7 @@ shared_ptr ProxyServer::get_session() { std::shared_ptr ProxyServer::create_licensed_session( std::shared_ptr l, uint16_t local_port, GameVersion version, - const ClientConfig& newserv_client_config) { + const ClientConfigBB& newserv_client_config) { shared_ptr session(new LinkedSession( this, local_port, version, l, newserv_client_config)); auto emplace_ret = this->id_to_session.emplace(session->id, session); diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 36712da8..0c98aff8 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -53,7 +53,7 @@ public: uint32_t remote_guild_card_number; parray remote_client_config_data; - ClientConfig newserv_client_config; + ClientConfigBB newserv_client_config; bool suppress_newserv_commands; bool enable_chat_filter; bool enable_switch_assist; @@ -101,7 +101,7 @@ public: uint16_t local_port, GameVersion version, std::shared_ptr license, - const ClientConfig& newserv_client_config); + const ClientConfigBB& newserv_client_config); LinkedSession( ProxyServer* server, uint64_t id, @@ -148,7 +148,7 @@ public: std::shared_ptr l, uint16_t local_port, GameVersion version, - const ClientConfig& newserv_client_config); + const ClientConfigBB& newserv_client_config); void delete_session(uint64_t id); private: diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 18b8c414..11b808d2 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -367,7 +367,7 @@ void process_login_bb(shared_ptr s, shared_ptr c, } try { - c->import_config(cmd.client_config.cfg); + c->import_config(cmd.client_config.cfg.cfg); c->bb_game_state++; } catch (const invalid_argument&) { c->bb_game_state = 0; @@ -749,7 +749,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, s->proxy_server->delete_session(c->license->serial_number); s->proxy_server->create_licensed_session( - c->license, local_port, c->version, c->export_config()); + c->license, local_port, c->version, c->export_config_bb()); send_reconnect(c, s->connect_address_for_client(c), local_port); } @@ -1751,13 +1751,10 @@ void process_encryption_ok_patch(shared_ptr, shared_ptr c, void process_login_patch(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { - const auto& cmd = check_size_t(data, - offsetof(C_Login_Patch_04, email), sizeof(C_Login_Patch_04)); + const auto& cmd = check_size_t(data); - if (data.size() == offsetof(C_Login_Patch_04, email)) { + if (cmd.email.len() == 0) { c->flags |= Client::Flag::BB_PATCH; - } else if (data.size() != sizeof(C_Login_Patch_04)) { - throw runtime_error("unknown patch server login format"); } // On BB we can use colors and newlines should be \n; on PC we can't use diff --git a/src/ServerShell.cc b/src/ServerShell.cc index ebfdf4d7..e7191bbc 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -66,7 +66,7 @@ Server commands:\n\ license is deleted by reloading, they will not be disconnected immediately.\n\ add-license \n\ Add a license to the server. is some subset of the following:\n\ - username= (BB username)\n\ + bb-username= (BB username)\n\ bb-password= (BB password)\n\ gc-password= (GC password)\n\ access-key= (GC/PC access key)\n\ @@ -179,11 +179,11 @@ Proxy commands (these will only work when exactly one client is connected):\n\ shared_ptr l(new License()); for (const string& token : split(command_args, ' ')) { - if (starts_with(token, "username=")) { - if (token.size() >= 29) { + if (starts_with(token, "bb-username=")) { + if (token.size() >= 32) { throw invalid_argument("username too long"); } - l->username = token.substr(9); + l->username = token.substr(12); } else if (starts_with(token, "bb-password=")) { if (token.size() >= 32) { diff --git a/src/ServerState.hh b/src/ServerState.hh index 808fcb53..ce9b919e 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -61,6 +61,7 @@ struct ServerState { std::vector> proxy_destinations_pc; std::vector> proxy_destinations_gc; std::pair proxy_destination_patch; + std::pair proxy_destination_bb; std::u16string welcome_message; std::map> id_to_lobby; diff --git a/system/config.example.json b/system/config.example.json index 76410746..94bed224 100755 --- a/system/config.example.json +++ b/system/config.example.json @@ -34,6 +34,9 @@ // patch server (for PC and BB) is bypassed, and any client that connects to // the patch server is instead proxied to this destination. // "ProxyDestination-Patch": "", + // Proxy destination for BB clients. If this is given, all BB clients that + // connect to newserv will be proxied to this destination. + // "ProxyDestination-BB": "", // By default, the interactive shell runs if stdin is a terminal, and doesn't // run if it's not. This option, if present, overrides that behavior.