From 2597da95bc10d50e62bcc13b49325c4b19f82246 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 27 Mar 2022 18:14:23 -0700 Subject: [PATCH] rewrite proxy server to support multiple clients and integration with game server --- CMakeLists.txt | 1 - src/Client.cc | 30 +- src/Client.hh | 16 +- src/IPStackSimulator.cc | 42 +- src/IPStackSimulator.hh | 7 - src/License.cc | 31 +- src/License.hh | 16 +- src/Main.cc | 132 ++--- src/Menu.hh | 18 + src/PSOProtocol.cc | 126 ++++- src/PSOProtocol.hh | 16 +- src/ProxyServer.cc | 1112 +++++++++++++++++++++++---------------- src/ProxyServer.hh | 160 ++++-- src/ProxyShell.cc | 146 ----- src/ProxyShell.hh | 28 - src/ReceiveCommands.cc | 207 ++++---- src/ReceiveCommands.hh | 33 ++ src/SendCommands.cc | 55 +- src/SendCommands.hh | 23 +- src/ServerShell.cc | 132 ++++- src/ServerShell.hh | 6 +- src/ServerState.cc | 9 - src/ServerState.hh | 9 + system/config.json | 7 + 24 files changed, 1370 insertions(+), 992 deletions(-) delete mode 100644 src/ProxyShell.cc delete mode 100644 src/ProxyShell.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a0b10de..27b294f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,6 @@ add_executable(newserv src/ProxyServer.cc src/Shell.cc src/ServerShell.cc - src/ProxyShell.cc src/IPFrameInfo.cc src/IPStackSimulator.cc src/Main.cc diff --git a/src/Client.cc b/src/Client.cc index 44338db8..a3e0d2b5 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -15,7 +15,7 @@ using namespace std; -static const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839; +const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839; @@ -28,6 +28,8 @@ Client::Client( bev(bev), server_behavior(server_behavior), should_disconnect(false), + proxy_destination_address(0), + proxy_destination_port(0), play_time_begin(now()), last_recv_time(this->play_time_begin), last_send_time(0), @@ -52,32 +54,16 @@ Client::Client( memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr)); } -bool Client::send(string&& data) { - if (!this->bev) { - return false; - } - - if (this->crypt_out.get()) { - this->crypt_out->encrypt(data.data(), data.size()); - } - - struct evbuffer* buf = bufferevent_get_output(this->bev); - evbuffer_add(buf, data.data(), data.size()); - return true; -} - ClientConfig Client::export_config() const { ClientConfig cc; cc.magic = CLIENT_CONFIG_MAGIC; cc.bb_game_state = this->bb_game_state; cc.bb_player_index = this->bb_player_index; cc.flags = this->flags; - for (size_t x = 0; x < 5; x++) { - cc.unused[x] = 0xFFFFFFFF; - } - for (size_t x = 0; x < 2; x++) { - cc.unused_bb_only[x] = 0xFFFFFFFF; - } + cc.proxy_destination_address = this->proxy_destination_address; + cc.proxy_destination_port = this->proxy_destination_port; + memset(cc.unused, 0xFF, sizeof(cc.unused)); + memset(cc.unused_bb_only, 0xFF, sizeof(cc.unused_bb_only)); return cc; } @@ -88,4 +74,6 @@ void Client::import_config(const ClientConfig& cc) { this->bb_game_state = cc.bb_game_state; this->bb_player_index = cc.bb_player_index; this->flags = cc.flags; + this->proxy_destination_address = cc.proxy_destination_address; + this->proxy_destination_port = cc.proxy_destination_port; } diff --git a/src/Client.hh b/src/Client.hh index fdc72a18..0ac60da0 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -10,12 +10,17 @@ +extern const uint64_t CLIENT_CONFIG_MAGIC; + + + enum class ServerBehavior { SPLIT_RECONNECT = 0, LOGIN_SERVER, LOBBY_SERVER, DATA_SERVER_BB, PATCH_SERVER, + PROXY_SERVER, }; struct ClientConfig { @@ -23,8 +28,10 @@ struct ClientConfig { uint8_t bb_game_state; uint8_t bb_player_index; uint16_t flags; - uint32_t unused[5]; - uint32_t unused_bb_only[2]; + uint32_t proxy_destination_address; + uint16_t proxy_destination_port; + uint8_t unused[0x0E]; + uint8_t unused_bb_only[0x08]; } __attribute__((packed)); struct Client { @@ -51,6 +58,8 @@ struct Client { ServerBehavior server_behavior; bool is_virtual_connection; bool should_disconnect; + uint32_t proxy_destination_address; + uint16_t proxy_destination_port; // timing & menus uint64_t play_time_begin; // time of connection (used for incrementing play time on BB) @@ -75,9 +84,6 @@ struct Client { Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior); - // adds data to the client's output buffer, encrypting it first - bool send(std::string&& data); - ClientConfig export_config() const; void import_config(const ClientConfig& cc); }; diff --git a/src/IPStackSimulator.cc b/src/IPStackSimulator.cc index 2b21a2fa..11616aad 100644 --- a/src/IPStackSimulator.cc +++ b/src/IPStackSimulator.cc @@ -77,13 +77,10 @@ string IPStackSimulator::str_for_tcp_connection(shared_ptr c, IPStackSimulator::IPStackSimulator( std::shared_ptr base, std::shared_ptr game_server, - std::shared_ptr proxy_server, std::shared_ptr state) : base(base), game_server(game_server), - proxy_server(proxy_server), state(state), - proxy_destination_address(0), pcap_text_log_file(state->ip_stack_debug ? fopen("IPStackSimulator-Log.txt", "wt") : nullptr) { memset(this->host_mac_address_bytes, 0x90, 6); memset(this->broadcast_mac_address_bytes, 0xFF, 6); @@ -421,9 +418,7 @@ void IPStackSimulator::on_client_udp_frame( // r_udp.size filled in later // r_udp.checksum filled in later - uint32_t resolved_address = this->proxy_destination_address - ? this->proxy_destination_address - : this->connect_address_for_remote_address(c->ipv4_addr); + uint32_t resolved_address = this->connect_address_for_remote_address(c->ipv4_addr); string r_data = DNSServer::response_for_query( fi.payload, fi.payload_size, resolved_address); @@ -758,25 +753,34 @@ void IPStackSimulator::open_server_connection( // Link the client to the server - the server sees this as a normal TCP // connection and treats it as if the client connected to one of its listening // sockets - string conn_str = this->str_for_tcp_connection(c, conn); - if (this->game_server.get()) { - const PortConfiguration* port_config; - try { - port_config = &this->state->numbered_port_configuration.at(conn.server_port); - } catch (const out_of_range&) { - bufferevent_free(bevs[1]); - throw logic_error("client connected to port missing from configuration"); - } + const PortConfiguration* port_config; + try { + port_config = &this->state->numbered_port_configuration.at(conn.server_port); + } catch (const out_of_range&) { + bufferevent_free(bevs[1]); + throw logic_error("client connected to port missing from configuration"); + } + string conn_str = this->str_for_tcp_connection(c, conn); + if (port_config->behavior == ServerBehavior::PROXY_SERVER) { + if (!this->state->proxy_server.get()) { + log(ERROR, "[IPStackSimulator] TCP connection %s is to non-running proxy server", + conn_str.c_str()); + flush_and_free_bufferevent(bevs[1]); + } else { + this->state->proxy_server->connect_client(bevs[1], conn.server_port); + log(INFO, "[IPStackSimulator] Connected TCP connection %s to proxy server", + conn_str.c_str()); + } + } else if (this->game_server.get()) { this->game_server->connect_client(bevs[1], c->ipv4_addr, conn.client_port, port_config->version, port_config->behavior); log(INFO, "[IPStackSimulator] Connected TCP connection %s to game server", conn_str.c_str()); - - } else if (this->proxy_server.get()) { - this->proxy_server->connect_client(bevs[1], conn.server_addr, conn.server_port); - log(INFO, "[IPStackSimulator] Connected TCP connection %s to proxy server", + } else { + log(ERROR, "[IPStackSimulator] No server available for TCP connection %s", conn_str.c_str()); + flush_and_free_bufferevent(bevs[1]); } } diff --git a/src/IPStackSimulator.hh b/src/IPStackSimulator.hh index 24911d61..bbda1456 100644 --- a/src/IPStackSimulator.hh +++ b/src/IPStackSimulator.hh @@ -18,7 +18,6 @@ public: IPStackSimulator( std::shared_ptr base, std::shared_ptr game_server, - std::shared_ptr proxy_server, std::shared_ptr state); ~IPStackSimulator(); @@ -27,18 +26,12 @@ public: void listen(int port); void add_socket(int fd); - inline void set_proxy_destination_address(uint32_t addr) { - this->proxy_destination_address = addr; - } - static uint32_t connect_address_for_remote_address(uint32_t remote_addr); private: std::shared_ptr base; std::shared_ptr game_server; - std::shared_ptr proxy_server; std::shared_ptr state; - uint32_t proxy_destination_address; using unique_listener = std::unique_ptr; using unique_bufferevent = std::unique_ptr; diff --git a/src/License.cc b/src/License.cc index c2881db5..1a71c233 100644 --- a/src/License.cc +++ b/src/License.cc @@ -53,6 +53,12 @@ LicenseManager::LicenseManager(const string& filename) : filename(filename) { auto licenses = load_vector_file(this->filename); for (const auto& read_license : licenses) { shared_ptr license(new License(read_license)); + + // Before the temporary flag existed, licenses with root privileges would + // have the temporary flag set. To migrate these, explicitly unset the + // flag for all licenses loaded from the license file. + license->privileges &= ~Privilege::TEMPORARY; + uint32_t serial_number = license->serial_number; this->bb_username_to_license.emplace(license->username, license); this->serial_number_to_license.emplace(serial_number, license); @@ -67,6 +73,9 @@ LicenseManager::LicenseManager(const string& filename) : filename(filename) { void LicenseManager::save() const { auto f = fopen_unique(this->filename, "wb"); for (const auto& it : this->serial_number_to_license) { + if (it.second->privileges & Privilege::TEMPORARY) { + continue; + } fwritex(f.get(), it.second.get(), sizeof(License)); } } @@ -154,33 +163,43 @@ vector LicenseManager::snapshot() const { } -shared_ptr LicenseManager::create_license_pc( - uint32_t serial_number,const char* access_key, const char* password) { + +shared_ptr LicenseManager::create_license_pc( + uint32_t serial_number,const char* access_key, const char* password, bool temporary) { shared_ptr l(new License()); l->serial_number = serial_number; strncpy(l->access_key, access_key, 8); if (password) { strncpy(l->gc_password, password, 8); } + if (temporary) { + l->privileges |= Privilege::TEMPORARY; + } return l; } -shared_ptr LicenseManager::create_license_gc( - uint32_t serial_number, const char* access_key, const char* password) { +shared_ptr LicenseManager::create_license_gc( + uint32_t serial_number, const char* access_key, const char* password, bool temporary) { shared_ptr l(new License()); l->serial_number = serial_number; strncpy(l->access_key, access_key, 12); if (password) { strncpy(l->gc_password, password, 8); } + if (temporary) { + l->privileges |= Privilege::TEMPORARY; + } return l; } -shared_ptr LicenseManager::create_license_bb( - uint32_t serial_number, const char* username, const char* password) { +shared_ptr LicenseManager::create_license_bb( + uint32_t serial_number, const char* username, const char* password, bool temporary) { shared_ptr l(new License()); l->serial_number = serial_number; strncpy(l->username, username, 19); strncpy(l->bb_password, password, 19); + if (temporary) { + l->privileges |= Privilege::TEMPORARY; + } return l; } diff --git a/src/License.hh b/src/License.hh index 7344a6a8..e3408aca 100644 --- a/src/License.hh +++ b/src/License.hh @@ -17,7 +17,9 @@ enum Privilege { MODERATOR = 0x00000007, ADMINISTRATOR = 0x0000003F, - ROOT = 0xFFFFFFFF, + ROOT = 0x7FFFFFFF, + + TEMPORARY = 0x80000000, }; enum LicenseVerifyAction { @@ -59,12 +61,12 @@ public: void remove(uint32_t serial_number); std::vector snapshot() const; - static std::shared_ptr create_license_pc( - uint32_t serial_number, const char* access_key, const char* password); - static std::shared_ptr create_license_gc( - uint32_t serial_number, const char* access_key, const char* password); - static std::shared_ptr create_license_bb( - uint32_t serial_number, const char* username, const char* password); + static std::shared_ptr create_license_pc( + uint32_t serial_number, const char* access_key, const char* password, bool temporary); + static std::shared_ptr create_license_gc( + uint32_t serial_number, const char* access_key, const char* password, bool temporary); + static std::shared_ptr create_license_bb( + uint32_t serial_number, const char* username, const char* password, bool temporary); protected: void save() const; diff --git a/src/Main.cc b/src/Main.cc index 201856fd..61aaa10f 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -19,7 +19,6 @@ #include "FileContentsCache.hh" #include "Text.hh" #include "ServerShell.hh" -#include "ProxyShell.hh" #include "IPStackSimulator.hh" using namespace std; @@ -51,6 +50,9 @@ static const unordered_map default_port_to_behavior({ {"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}}, }); @@ -100,18 +102,45 @@ void populate_state_from_config(shared_ptr s, information_menu->emplace_back(INFORMATION_MENU_GO_BACK, u"Go back", u"Return to the\nmain menu", 0); - - uint32_t item_id = 0; - for (const auto& item : d.at("InformationMenuContents")->as_list()) { - auto& v = item->as_list(); - information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), - decode_sjis(v.at(1)->as_string()), MenuItemFlag::REQUIRES_MESSAGE_BOXES); - information_contents->emplace_back(decode_sjis(v.at(2)->as_string())); - item_id++; + { + uint32_t item_id = 0; + for (const auto& item : d.at("InformationMenuContents")->as_list()) { + auto& v = item->as_list(); + information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), + decode_sjis(v.at(1)->as_string()), MenuItemFlag::REQUIRES_MESSAGE_BOXES); + information_contents->emplace_back(decode_sjis(v.at(2)->as_string())); + item_id++; + } } s->information_menu = information_menu; s->information_contents = information_contents; + s->proxy_destinations_menu.emplace_back(PROXY_DESTINATIONS_MENU_GO_BACK, + u"Go back", u"Return to the\nmain menu", 0); + { + uint32_t item_id = 0; + for (const auto& item : d.at("ProxyDestinations")->as_dict()) { + const string& netloc_str = item.second->as_string(); + s->proxy_destinations_menu.emplace_back(item_id, decode_sjis(item.first), + decode_sjis(netloc_str), 0); + s->proxy_destinations.emplace_back(parse_netloc(netloc_str)); + item_id++; + } + } + + s->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby", + u"Join the lobby", 0); + s->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information", + u"View server information", MenuItemFlag::REQUIRES_MESSAGE_BOXES); + if (!s->proxy_destinations.empty()) { + s->main_menu.emplace_back(MAIN_MENU_PROXY_DESTINATIONS, u"Proxy server", + u"Connect to another\nserver", MenuItemFlag::REQUIRES_MESSAGE_BOXES); + } + s->main_menu.emplace_back(MAIN_MENU_DOWNLOAD_QUESTS, u"Download quests", + u"Download quests", 0); + s->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect", + u"Disconnect", 0); + try { s->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string()); } catch (const out_of_range&) { } @@ -214,24 +243,7 @@ void drop_privileges(const string& username) { -int main(int argc, char* argv[]) { - string proxy_hostname; - int proxy_port = 0; - GameVersion proxy_version = GameVersion::GC; - for (int x = 1; x < argc; x++) { - if (!strncmp(argv[x], "--proxy-destination=", 20)) { - auto netloc = parse_netloc(&argv[x][20], 9100); - proxy_hostname = netloc.first; - proxy_port = netloc.second; - - } else if (!strncmp(argv[x], "--proxy-version=", 16)) { - proxy_version = version_for_name(&argv[x][16]); - - } else { - throw invalid_argument(string_printf("unknown option: %s", argv[x])); - } - } - +int main(int, char**) { signal(SIGPIPE, SIG_IGN); shared_ptr state(new ServerState()); @@ -249,6 +261,18 @@ int main(int argc, char* argv[]) { auto config_json = JSONObject::parse(load_file("system/config.json")); populate_state_from_config(state, config_json); + log(INFO, "Loading license list"); + state->license_manager.reset(new LicenseManager("system/licenses.nsi")); + + log(INFO, "Loading battle parameters"); + state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); + + log(INFO, "Loading level table"); + state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + + log(INFO, "Collecting quest metadata"); + state->quest_index.reset(new QuestIndex("system/quests")); + shared_ptr dns_server; if (state->dns_server_port) { log(INFO, "Starting DNS server"); @@ -259,50 +283,31 @@ int main(int argc, char* argv[]) { log(INFO, "DNS server is disabled"); } - shared_ptr proxy_server; shared_ptr game_server; - uint32_t proxy_destination_address = 0; - if (proxy_port) { + if (!state->proxy_destinations.empty()) { log(INFO, "Starting proxy server"); - sockaddr_storage proxy_destination_ss = make_sockaddr_storage( - proxy_hostname, proxy_port).first; - if (proxy_destination_ss.ss_family != AF_INET) { - throw runtime_error("proxy destination address is not IPv4"); - } - proxy_destination_address = ntohl( - reinterpret_cast(&proxy_destination_ss)->sin_addr.s_addr); - proxy_server.reset(new ProxyServer(base, proxy_destination_ss, proxy_version)); - proxy_server->listen(proxy_port); - if (proxy_version == GameVersion::BB) { - proxy_server->listen(proxy_port + 1); - } + state->proxy_server.reset(new ProxyServer(base, state)); + } - } else { - log(INFO, "Starting game server"); - game_server.reset(new Server(base, state)); - for (const auto& it : state->named_port_configuration) { + log(INFO, "Starting game server"); + game_server.reset(new Server(base, state)); + + log(INFO, "Opening sockets"); + for (const auto& it : state->named_port_configuration) { + if (it.second.behavior == ServerBehavior::PROXY_SERVER) { + if (state->proxy_server.get()) { + state->proxy_server->listen(it.second.port, it.second.version); + } + } else { game_server->listen("", it.second.port, it.second.version, it.second.behavior); } - - log(INFO, "Loading license list"); - state->license_manager.reset(new LicenseManager("system/licenses.nsi")); - - log(INFO, "Loading battle parameters"); - state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); - - log(INFO, "Loading level table"); - state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); - - log(INFO, "Collecting quest metadata"); - state->quest_index.reset(new QuestIndex("system/quests")); } shared_ptr ip_stack_simulator; if (!state->ip_stack_addresses.empty()) { log(INFO, "Starting IP stack simulator"); ip_stack_simulator.reset(new IPStackSimulator( - base, game_server, proxy_server, state)); - ip_stack_simulator->set_proxy_destination_address(proxy_destination_address); + base, game_server, state)); for (const auto& it : state->ip_stack_addresses) { auto netloc = parse_netloc(it); ip_stack_simulator->listen(netloc.first, netloc.second); @@ -321,16 +326,13 @@ int main(int argc, char* argv[]) { shared_ptr shell; if (should_run_shell) { - if (proxy_port) { - shell.reset(new ProxyShell(base, state, proxy_server)); - } else { - shell.reset(new ServerShell(base, state)); - } + shell.reset(new ServerShell(base, state)); } log(INFO, "Ready"); event_base_dispatch(base.get()); log(INFO, "Normal shutdown"); + state->proxy_server.reset(); // Break reference cycle return 0; } diff --git a/src/Menu.hh b/src/Menu.hh index c3ac00e4..cbff8efe 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -6,6 +6,24 @@ +#define MAIN_MENU_ID 0x11000011 +#define INFORMATION_MENU_ID 0x22000022 +#define LOBBY_MENU_ID 0x33000033 +#define GAME_MENU_ID 0x44000044 +#define QUEST_MENU_ID 0x55000055 +#define QUEST_FILTER_MENU_ID 0x66000066 +#define PROXY_DESTINATIONS_MENU_ID 0x77000077 + +#define MAIN_MENU_GO_TO_LOBBY 0x11AAAA11 +#define MAIN_MENU_INFORMATION 0x11BBBB11 +#define MAIN_MENU_DOWNLOAD_QUESTS 0x11CCCC11 +#define MAIN_MENU_PROXY_DESTINATIONS 0x11DDDD11 +#define MAIN_MENU_DISCONNECT 0x11EEEE11 +#define INFORMATION_MENU_GO_BACK 0x22FFFF22 +#define PROXY_DESTINATIONS_MENU_GO_BACK 0x77FFFF77 + + + enum MenuItemFlag { INVISIBLE_ON_DC = 0x01, INVISIBLE_ON_PC = 0x02, diff --git a/src/PSOProtocol.cc b/src/PSOProtocol.cc index 38ae3433..3ad12c26 100644 --- a/src/PSOProtocol.cc +++ b/src/PSOProtocol.cc @@ -3,6 +3,9 @@ #include #include +#include + +#include "Text.hh" using namespace std; @@ -17,43 +20,109 @@ PSOCommandHeader::PSOCommandHeader() { uint16_t PSOCommandHeader::command(GameVersion version) const { switch (version) { case GameVersion::DC: + return this->dc.command; case GameVersion::GC: - return reinterpret_cast(this)->command; + return this->gc.command; case GameVersion::PC: case GameVersion::PATCH: - return reinterpret_cast(this)->command; + return this->pc.command; case GameVersion::BB: - return reinterpret_cast(this)->command; + return this->bb.command; + default: + throw logic_error("unknown game version"); + } +} + +void PSOCommandHeader::set_command(GameVersion version, uint16_t command) { + switch (version) { + case GameVersion::DC: + this->dc.command = command; + break; + case GameVersion::GC: + this->gc.command = command; + break; + case GameVersion::PC: + case GameVersion::PATCH: + this->pc.command = command; + break; + case GameVersion::BB: + this->bb.command = command; + break; + default: + throw logic_error("unknown game version"); } - throw logic_error("unknown game version"); } uint16_t PSOCommandHeader::size(GameVersion version) const { switch (version) { case GameVersion::DC: + return this->dc.size; case GameVersion::GC: - return reinterpret_cast(this)->size; + return this->gc.size; case GameVersion::PC: case GameVersion::PATCH: - return reinterpret_cast(this)->size; + return this->pc.size; case GameVersion::BB: - return reinterpret_cast(this)->size; + return this->bb.size; + default: + throw logic_error("unknown game version"); + } +} + +void PSOCommandHeader::set_size(GameVersion version, uint32_t size) { + switch (version) { + case GameVersion::DC: + this->dc.size = size; + break; + case GameVersion::GC: + this->gc.size = size; + break; + case GameVersion::PC: + case GameVersion::PATCH: + this->pc.size = size; + break; + case GameVersion::BB: + this->bb.size = size; + break; + default: + throw logic_error("unknown game version"); } - throw logic_error("unknown game version"); } uint32_t PSOCommandHeader::flag(GameVersion version) const { switch (version) { case GameVersion::DC: + return this->dc.flag; case GameVersion::GC: - return reinterpret_cast(this)->flag; + return this->gc.flag; case GameVersion::PC: case GameVersion::PATCH: - return reinterpret_cast(this)->flag; + return this->pc.flag; case GameVersion::BB: - return reinterpret_cast(this)->flag; + return this->bb.flag; + default: + throw logic_error("unknown game version"); + } +} + +void PSOCommandHeader::set_flag(GameVersion version, uint32_t flag) { + switch (version) { + case GameVersion::DC: + this->dc.flag = flag; + break; + case GameVersion::GC: + this->gc.flag = flag; + break; + case GameVersion::PC: + case GameVersion::PATCH: + this->pc.flag = flag; + break; + case GameVersion::BB: + this->bb.flag = flag; + break; + default: + throw logic_error("unknown game version"); } - throw logic_error("unknown game version"); } @@ -62,7 +131,7 @@ void for_each_received_command( struct bufferevent* bev, GameVersion version, PSOEncryption* crypt, - function fn) { + function fn) { struct evbuffer* buf = bufferevent_get_input(bev); size_t header_size = version == GameVersion::BB ? 8 : 4; @@ -108,4 +177,33 @@ void for_each_received_command( fn(header.command(version), header.flag(version), command_data); } -} \ No newline at end of file +} + +void print_received_command( + uint16_t command, + uint32_t flag, + const void* data, + size_t size, + GameVersion version, + const char* name) { + string name_token; + if (name && name[0]) { + name_token = string(" from ") + name; + } + log(INFO, "Received%s (version=%d command=%04hX flag=%08X)", + name_token.c_str(), static_cast(version), command, flag); + + PSOCommandHeader header; + size_t header_size = header.header_size(version); + header.set_command(version, command); + header.set_flag(version, flag); + header.set_size(version, size + header_size); + + // TODO: This is unnecessarily slow. It'd be nice to have a print_data_v() so + // we don't have to copy data around here. + StringWriter w; + w.write(&header, header_size); + w.write(data, size); + + print_data(stderr, w.str()); +} diff --git a/src/PSOProtocol.hh b/src/PSOProtocol.hh index 78ccab81..8f5d8c17 100644 --- a/src/PSOProtocol.hh +++ b/src/PSOProtocol.hh @@ -33,8 +33,14 @@ union PSOCommandHeader { PSOCommandHeaderBB bb; uint16_t command(GameVersion version) const; + void set_command(GameVersion version, uint16_t command); uint16_t size(GameVersion version) const; + void set_size(GameVersion version, uint32_t size); uint32_t flag(GameVersion version) const; + void set_flag(GameVersion version, uint32_t flag); + static inline size_t header_size(GameVersion version) { + return (version == GameVersion::BB) ? 8 : 4; + } PSOCommandHeader(); } __attribute__((packed)); @@ -49,4 +55,12 @@ void for_each_received_command( struct bufferevent* bev, GameVersion version, PSOEncryption* crypt, - std::function fn); + std::function fn); + +void print_received_command( + uint16_t command, + uint32_t flag, + const void* data, + size_t size, + GameVersion version, + const char* name = nullptr); diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index f5fb7428..8f3429c8 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -18,10 +18,12 @@ #include #include #include +#include #include #include #include "PSOProtocol.hh" +#include "SendCommands.hh" #include "ReceiveCommands.hh" #include "ReceiveSubcommands.hh" @@ -38,36 +40,320 @@ static void flush_and_free_bufferevent(struct bufferevent* bev) { ProxyServer::ProxyServer( shared_ptr base, - const struct sockaddr_storage& initial_destination, - GameVersion version) + shared_ptr state) : base(base), + state(state) { } + +void ProxyServer::listen(uint16_t port, GameVersion version) { + int fd = ::listen("", port, SOMAXCONN); + if (fd < 0) { + throw runtime_error("cannot listen on port"); + } + auto* listener = evconnlistener_new(this->base.get(), + &ProxyServer::ListeningSocket::dispatch_on_listen_accept, this, + LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 0, fd); + if (!listener) { + close(fd); + throw runtime_error("cannot create listener"); + } + evconnlistener_set_error_cb( + listener, &ProxyServer::ListeningSocket::dispatch_on_listen_error); + + unique_ptr evlistener( + listener, evconnlistener_free); + shared_ptr socket_obj(new ListeningSocket({ + .server = this, + .fd = fd, + .port = port, + .listener = move(evlistener), + .version = version, + })); + this->listeners.emplace(port, socket_obj); + + log(INFO, "[ProxyServer] Listening on TCP port %hu (%s) on fd %d", + port, name_for_version(version), fd); +} + +void ProxyServer::ListeningSocket::dispatch_on_listen_accept( + struct evconnlistener*, evutil_socket_t, struct sockaddr* address, int socklen, void* ctx) { + reinterpret_cast(ctx)->on_listen_accept(address, socklen); +} + +void ProxyServer::ListeningSocket::dispatch_on_listen_error( + struct evconnlistener*, void* ctx) { + reinterpret_cast(ctx)->on_listen_error(); +} + +void ProxyServer::ListeningSocket::on_listen_accept(struct sockaddr*, int) { + log(INFO, "[ProxyServer] Client connected on fd %d", fd); + auto* bev = bufferevent_socket_new(this->server->base.get(), fd, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + this->server->on_client_connect(bev, this->port, this->version); +} + +void ProxyServer::ListeningSocket::on_listen_error() { + int err = EVUTIL_SOCKET_ERROR(); + log(ERROR, "[ProxyServer] Failure on listening socket %d: %d (%s)", + evconnlistener_get_fd(this->listener.get()), + err, evutil_socket_error_to_string(err)); + event_base_loopexit(this->server->base.get(), nullptr); +} + + + +void ProxyServer::connect_client(struct bufferevent* bev, uint16_t server_port) { + // Look up the listening socket for the given port, and use that game version + GameVersion version; + try { + version = this->listeners.at(server_port)->version; + } catch (const out_of_range&) { + log(INFO, "[ProxyServer] Virtual connection received on unregistered port %hu; closing it", + server_port); + flush_and_free_bufferevent(bev); + return; + } + + log(INFO, "[ProxyServer] Client connected on virtual connection %p", bev); + this->on_client_connect(bev, server_port, version); +} + + + +void ProxyServer::on_client_connect( + struct bufferevent* bev, uint16_t listen_port, GameVersion version) { + auto emplace_ret = this->bev_to_unlinked_session.emplace(bev, new UnlinkedSession( + this, bev, listen_port, version)); + if (!emplace_ret.second) { + throw logic_error("stale unlinked session exists"); + } + auto session = emplace_ret.first->second; + + switch (version) { + case GameVersion::GC: { + uint32_t server_key = random_object(); + uint32_t client_key = random_object(); + string data = prepare_server_init_contents_dc_pc_gc(false, server_key, client_key); + send_command(session->bev.get(), session->version, session->crypt_out.get(), 0x02, + 0, data.data(), data.size(), "unlinked proxy client"); + session->crypt_out.reset(new PSOGCEncryption(server_key)); + session->crypt_in.reset(new PSOGCEncryption(client_key)); + break; + } + default: + throw logic_error("unsupported game version on proxy server"); + } +} + + + +ProxyServer::UnlinkedSession::UnlinkedSession( + ProxyServer* server, struct bufferevent* bev, uint16_t local_port, GameVersion version) + : server(server), + bev(bev, flush_and_free_bufferevent), + local_port(local_port), + version(version) { + bufferevent_setcb(this->bev.get(), + &UnlinkedSession::dispatch_on_client_input, nullptr, + &UnlinkedSession::dispatch_on_client_error, this); + bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE); +} + +void ProxyServer::UnlinkedSession::dispatch_on_client_input( + struct bufferevent*, void* ctx) { + reinterpret_cast(ctx)->on_client_input(); +} + +void ProxyServer::UnlinkedSession::dispatch_on_client_error( + struct bufferevent*, short events, void* ctx) { + reinterpret_cast(ctx)->on_client_error(events); +} + +void ProxyServer::UnlinkedSession::on_client_input() { + bool should_close_unlinked_session = false; + shared_ptr license; + uint32_t sub_version = 0; + string character_name; + ClientConfig client_config; + + for_each_received_command(this->bev.get(), this->version, this->crypt_in.get(), + [&](uint16_t command, uint32_t flag, const string& data) { + print_received_command(command, flag, data.data(), data.size(), + this->version, "unlinked proxy client"); + + if (this->version == GameVersion::GC) { + // We should really only get a 9E while the session is unlinked; if we + // get anything else, disconnect + if (command != 0x9E) { + log(ERROR, "[ProxyServer] Received unexpected command %02hX", command); + should_close_unlinked_session = true; + } else if (data.size() < sizeof(LoginCommand_GC_9E) - 0x64) { + log(ERROR, "[ProxyServer] Login command is too small"); + should_close_unlinked_session = true; + } else { + const auto* cmd = reinterpret_cast(data.data()); + uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); + try { + license = this->server->state->license_manager->verify_gc( + serial_number, cmd->access_key, nullptr); + sub_version = cmd->sub_version; + character_name = cmd->name; + memcpy(&client_config, &cmd->cfg, 0x20); + } catch (const exception& e) { + log(ERROR, "[ProxyServer] Unlinked client has no valid license"); + should_close_unlinked_session = true; + } + } + } + }); + + struct bufferevent* session_key = this->bev.get(); + + // If license is non-null, then the client has a password and can be connected + // to the remote lobby server. + if (license.get()) { + // At this point, we will always close the unlinked session, even if it + // doesn't get converted/merged to a linked session + should_close_unlinked_session = true; + + // Look up the linked session for this license (if any) + shared_ptr session; + try { + session = this->server->serial_number_to_session.at(license->serial_number); + + } catch (const out_of_range&) { + // 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) { + log(ERROR, "[ProxyServer/%08X] Client configuration is invalid", + license->serial_number); + } else { + session.reset(new LinkedSession( + this->server, + this->local_port, + this->version, + license, + client_config.proxy_destination_address, + client_config.proxy_destination_port)); + this->server->serial_number_to_session.emplace( + license->serial_number, session); + } + } + + if (session.get() && (session->version != this->version)) { + log(ERROR, "[ProxyServer/%08X] Linked session has different game version", + session->license->serial_number); + } else { + // Resume the linked session using the unlinked session + try { + session->resume(move(this->bev), this->crypt_in, this->crypt_out, sub_version, character_name); + this->crypt_in.reset(); + this->crypt_out.reset(); + } catch (const exception& e) { + log(ERROR, "[ProxyServer/%08X] Failed to resume linked session: %s", + session->license->serial_number, e.what()); + } + } + } + + if (should_close_unlinked_session) { + this->server->bev_to_unlinked_session.erase(session_key); + // At this point, (*this) is destroyed! We must be careful not to touch it. + } +} + +void ProxyServer::UnlinkedSession::on_client_error(short events) { + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + log(WARNING, "[ProxyServer] Error %d (%s) in unlinked client stream", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) { + log(WARNING, "[ProxyServer] Unlinked client has disconnected"); + this->server->bev_to_unlinked_session.erase(this->bev.get()); + } +} + + + +ProxyServer::LinkedSession::LinkedSession( + ProxyServer* server, + uint16_t local_port, + GameVersion version, + std::shared_ptr license, + uint32_t dest_addr, + uint16_t dest_port) + : server(server), + license(license), client_bev(nullptr, flush_and_free_bufferevent), server_bev(nullptr, flush_and_free_bufferevent), - next_destination(initial_destination), - default_destination(initial_destination), + local_port(local_port), version(version), - header_size((version == GameVersion::BB) ? 8 : 4), + sub_version(0), // This is set during resume() save_quests(false) { - memset(&this->client_input_header, 0, sizeof(this->client_input_header)); - memset(&this->server_input_header, 0, sizeof(this->server_input_header)); + 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(dest_port); + dest_sin->sin_addr.s_addr = htonl(dest_addr); } -void ProxyServer::listen(int port) { - unique_ptr listener( - evconnlistener_new(this->base.get(), - &ProxyServer::dispatch_on_listen_accept, this, - LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 0, - ::listen("", port, SOMAXCONN)), evconnlistener_free); - this->listeners.emplace(port, move(listener)); -} +void ProxyServer::LinkedSession::resume( + std::unique_ptr&& client_bev, + std::shared_ptr client_input_crypt, + std::shared_ptr client_output_crypt, + uint32_t sub_version, + const string& character_name) { + if (this->client_bev.get()) { + throw runtime_error("client connection is already open for this session"); + } -void ProxyServer::set_save_quests(bool save_quests) { - this->save_quests = save_quests; + this->client_bev = move(client_bev); + bufferevent_setcb(this->client_bev.get(), + &ProxyServer::LinkedSession::dispatch_on_client_input, nullptr, + &ProxyServer::LinkedSession::dispatch_on_client_error, this); + bufferevent_enable(this->client_bev.get(), EV_READ | EV_WRITE); + + this->client_input_crypt = client_input_crypt; + this->client_output_crypt = client_output_crypt; + this->sub_version = sub_version; + this->character_name = character_name; + this->server_input_crypt.reset(); + this->server_output_crypt.reset(); + + this->saving_quest_files.clear(); + + // Connect to the remote server. The command handlers will do the login steps + // and set up forwarding + this->server_bev.reset(bufferevent_socket_new(this->server->base.get(), -1, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); + + struct sockaddr_storage local_ss; + struct sockaddr_in* local_sin = reinterpret_cast(&local_ss); + memset(local_sin, 0, sizeof(*local_sin)); + local_sin->sin_family = AF_INET; + const struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); + if (dest_sin->sin_family != AF_INET) { + throw logic_error("ss not AF_INET"); + } + local_sin->sin_port = dest_sin->sin_port; + local_sin->sin_addr.s_addr = dest_sin->sin_addr.s_addr; + + string netloc_str = render_sockaddr_storage(local_ss); + log(INFO, "[ProxyServer/%08X] Connecting to %s", this->license->serial_number, 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())); + } + bufferevent_setcb(this->server_bev.get(), + &ProxyServer::LinkedSession::dispatch_on_server_input, nullptr, + &ProxyServer::LinkedSession::dispatch_on_server_error, this); + bufferevent_enable(this->server_bev.get(), EV_READ | EV_WRITE); } -ProxyServer::SavingQuestFile::SavingQuestFile( +ProxyServer::LinkedSession::SavingQuestFile::SavingQuestFile( const std::string& basename, const std::string& output_filename, uint32_t remaining_bytes) @@ -78,471 +364,375 @@ ProxyServer::SavingQuestFile::SavingQuestFile( -void ProxyServer::send_to_client(const std::string& data) { - this->send_to_end(data, false); +void ProxyServer::LinkedSession::dispatch_on_client_input( + struct bufferevent*, void* ctx) { + reinterpret_cast(ctx)->on_client_input(); } -void ProxyServer::send_to_server(const std::string& data) { - this->send_to_end(data, true); +void ProxyServer::LinkedSession::dispatch_on_client_error( + struct bufferevent*, short events, void* ctx) { + reinterpret_cast(ctx)->on_stream_error(events, false); } -void ProxyServer::send_to_end(const std::string& data, bool to_server) { - struct bufferevent* bev = to_server ? this->server_bev.get() : this->client_bev.get(); - if (!bev) { - throw runtime_error("connection not open"); - } +void ProxyServer::LinkedSession::dispatch_on_server_input( + struct bufferevent*, void* ctx) { + reinterpret_cast(ctx)->on_server_input(); +} - struct evbuffer* buf = bufferevent_get_output(bev); - - PSOEncryption* crypt = to_server ? this->server_output_crypt.get() : this->client_output_crypt.get(); - if (crypt) { - string crypted_data = data; - crypt->encrypt(crypted_data.data(), crypted_data.size()); - evbuffer_add(buf, crypted_data.data(), crypted_data.size()); - } else { - evbuffer_add(buf, data.data(), data.size()); - } +void ProxyServer::LinkedSession::dispatch_on_server_error( + struct bufferevent*, short events, void* ctx) { + reinterpret_cast(ctx)->on_stream_error(events, true); } -void ProxyServer::dispatch_on_listen_accept( - struct evconnlistener* listener, evutil_socket_t fd, - struct sockaddr* address, int socklen, void* ctx) { - reinterpret_cast(ctx)->on_listen_accept(listener, fd, address, - socklen); -} - -void ProxyServer::dispatch_on_listen_error(struct evconnlistener* listener, - void* ctx) { - reinterpret_cast(ctx)->on_listen_error(listener); -} - -void ProxyServer::dispatch_on_client_input(struct bufferevent* bev, void* ctx) { - reinterpret_cast(ctx)->on_client_input(bev); -} - -void ProxyServer::dispatch_on_client_error(struct bufferevent* bev, short events, - void* ctx) { - reinterpret_cast(ctx)->on_client_error(bev, events); -} - -void ProxyServer::dispatch_on_server_input(struct bufferevent* bev, void* ctx) { - reinterpret_cast(ctx)->on_server_input(bev); -} - -void ProxyServer::dispatch_on_server_error(struct bufferevent* bev, short events, - void* ctx) { - reinterpret_cast(ctx)->on_server_error(bev, events); -} - - - -void ProxyServer::on_listen_accept(struct evconnlistener*, evutil_socket_t fd, - struct sockaddr*, int) { - if (this->client_bev.get()) { - log(WARNING, "[ProxyServer] Ignoring client connection because client already exists"); - close(fd); - return; - } - - log(INFO, "[ProxyServer] Client connected on fd %d", fd); - this->on_client_connect(bufferevent_socket_new(this->base.get(), fd, - BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); -} - -void ProxyServer::connect_client( - struct bufferevent* bev, uint32_t server_ipv4_addr, uint16_t server_port) { - if (this->client_bev.get()) { - log(WARNING, "[ProxyServer] Ignoring client virtual connection because client already exists"); - bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); - return; - } - - log(INFO, "[ProxyServer] Client connected on virtual connection %p", bev); - - this->on_client_connect(bev, server_ipv4_addr, server_port); -} - -void ProxyServer::on_client_connect(struct bufferevent* bev, - uint32_t server_ipv4_addr, uint16_t server_port) { - this->client_bev.reset(bev); - - bufferevent_setcb(this->client_bev.get(), - &ProxyServer::dispatch_on_client_input, nullptr, - &ProxyServer::dispatch_on_client_error, this); - bufferevent_enable(this->client_bev.get(), EV_READ | EV_WRITE); - - // Connect to the server, disconnecting first if needed - this->server_bev.reset(bufferevent_socket_new(this->base.get(), -1, - BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); - - struct sockaddr_storage local_ss; - struct sockaddr_in* local_sin = reinterpret_cast(&local_ss); - memset(local_sin, 0, sizeof(*local_sin)); - local_sin->sin_family = AF_INET; - if (server_ipv4_addr && server_port) { - local_sin->sin_port = htons(server_port); - local_sin->sin_addr.s_addr = htonl(server_ipv4_addr); - } else { - const struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); - if (dest_sin->sin_family != AF_INET) { - throw logic_error("ss not AF_INET"); - } - local_sin->sin_port = dest_sin->sin_port; - local_sin->sin_addr.s_addr = dest_sin->sin_addr.s_addr; - } - - string netloc_str = render_sockaddr_storage(local_ss); - log(INFO, "[ProxyServer] 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())); - } - bufferevent_setcb(this->server_bev.get(), - &ProxyServer::dispatch_on_server_input, nullptr, - &ProxyServer::dispatch_on_server_error, this); - bufferevent_enable(this->server_bev.get(), EV_READ | EV_WRITE); -} - -void ProxyServer::on_listen_error(struct evconnlistener* listener) { - int err = EVUTIL_SOCKET_ERROR(); - log(ERROR, "[ProxyServer] Failure on listening socket %d: %d (%s)", - evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err)); - event_base_loopexit(this->base.get(), nullptr); -} - -void ProxyServer::on_client_input(struct bufferevent*) { - this->receive_and_process_commands(false); -} - -void ProxyServer::on_server_input(struct bufferevent*) { - this->receive_and_process_commands(true); -} - -void ProxyServer::on_client_error(struct bufferevent*, short events) { +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] Error %d (%s) in client stream", err, - evutil_socket_error_to_string(err)); + log(WARNING, "[ProxyServer/%08X] Error %d (%s) in %s stream", + this->license->serial_number, err, evutil_socket_error_to_string(err), + is_server_stream ? "server" : "client"); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - log(INFO, "[ProxyServer] Client has disconnected"); - this->client_bev.reset(); - // "forward" the disconnection to the server - this->server_bev.reset(); - - // disable encryption - this->server_input_crypt.reset(); - this->server_output_crypt.reset(); - this->client_input_crypt.reset(); - this->client_output_crypt.reset(); + log(INFO, "[ProxyServer/%08X] %s has disconnected", + this->license->serial_number, is_server_stream ? "Server" : "Client"); + this->disconnect(); } } -void ProxyServer::on_server_error(struct bufferevent*, short events) { - if (events & BEV_EVENT_ERROR) { - int err = EVUTIL_SOCKET_ERROR(); - log(WARNING, "[ProxyServer] Error %d (%s) in server stream", err, - evutil_socket_error_to_string(err)); - } - if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - log(INFO, "[ProxyServer] Server has disconnected"); - this->server_bev.reset(); - // "forward" the disconnection to the client - this->client_bev.reset(); +void ProxyServer::LinkedSession::disconnect() { + // Forward the disconnection to the other end + this->server_bev.reset(); + this->client_bev.reset(); - // disable encryption - this->server_input_crypt.reset(); - this->server_output_crypt.reset(); - this->client_input_crypt.reset(); - this->client_output_crypt.reset(); - } + // Disable encryption for the next connection + this->server_input_crypt.reset(); + this->server_output_crypt.reset(); + this->client_input_crypt.reset(); + this->client_output_crypt.reset(); } -size_t ProxyServer::get_size_field(const PSOCommandHeader* header) { - if (this->version == GameVersion::DC) { - return header->dc.size; - } else if (this->version == GameVersion::PC) { - return header->pc.size; - } else if (this->version == GameVersion::GC) { - return header->gc.size; - } else if (this->version == GameVersion::BB) { - return header->bb.size; - } else { - throw logic_error("version not supported in proxy mode"); - } -} -size_t ProxyServer::get_command_field(const PSOCommandHeader* header) { - if (this->version == GameVersion::DC) { - return header->dc.command; - } else if (this->version == GameVersion::PC) { - return header->pc.command; - } else if (this->version == GameVersion::GC) { - return header->gc.command; - } else if (this->version == GameVersion::BB) { - return header->bb.command; - } else { - throw logic_error("version not supported in proxy mode"); - } -} -void ProxyServer::receive_and_process_commands(bool from_server) { - struct bufferevent* source_bev = from_server ? this->server_bev.get() : this->client_bev.get(); - struct bufferevent* dest_bev = from_server ? this->client_bev.get() : this->server_bev.get(); - - struct evbuffer* source_buf = bufferevent_get_input(source_bev); - struct evbuffer* dest_buf = dest_bev ? bufferevent_get_output(dest_bev) : nullptr; - - PSOEncryption* source_crypt = from_server ? this->server_input_crypt.get() : this->client_input_crypt.get(); - PSOEncryption* dest_crypt = from_server ? this->client_output_crypt.get() : this->server_output_crypt.get(); - - PSOCommandHeader* input_header = from_server ? &this->server_input_header : &this->client_input_header; - - for (;;) { - if (this->get_size_field(input_header) == 0) { - ssize_t bytes = evbuffer_copyout(source_buf, input_header, - this->header_size); - if (bytes < static_cast(this->header_size)) { - break; - } - - if (source_crypt) { - source_crypt->decrypt(input_header, this->header_size); - } - } - - size_t command_size = this->get_size_field(input_header); - if (evbuffer_get_length(source_buf) < command_size) { - break; - } - - string command(command_size, '\0'); - ssize_t bytes = evbuffer_remove(source_buf, command.data(), command_size); - if (bytes < static_cast(command_size)) { - throw logic_error("enough bytes available, but could not remove them"); - } - memcpy(command.data(), input_header, this->header_size); - - if (source_crypt) { - source_crypt->decrypt(command.data() + this->header_size, - command_size - this->header_size); - } - - log(INFO, "[ProxyServer] %s:", from_server ? "server" : "client"); - print_data(stderr, command); - - // Preprocess the command if needed - - // Preprocessing for bidirectional commands... - switch (this->get_command_field(input_header)) { - case 0x60: - case 0x62: - case 0x6C: - case 0x6D: - case 0xC9: - case 0xCB: { // broadcast/target commands - if (command.size() <= this->header_size) { - log(WARNING, "[ProxyServer] Received broadcast/target command with no contents"); - } else { - uint8_t which = *reinterpret_cast(command.data() + this->header_size); - if (!subcommand_is_implemented(which)) { - log(WARNING, "[ProxyServer] Received broadcast/target subcommand %02hhX which is not implemented on the server", - which); - } - } - break; - } - } - - // Preprocessing for server->client commands... - if (from_server) { - switch (this->get_command_field(input_header)) { - case 0x02: // init encryption - case 0x17: { // init encryption - if (this->version == GameVersion::BB) { - throw invalid_argument("console server init received on BB"); - } - - struct InitEncryptionCommand { - PSOCommandHeaderDCGC header; - char copyright[0x40]; - uint32_t server_key; - uint32_t client_key; - }; - if (command.size() < sizeof(InitEncryptionCommand)) { - throw std::runtime_error("init encryption command is too small"); - } - - const InitEncryptionCommand* cmd = reinterpret_cast( - command.data()); - 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)); - this->client_input_crypt.reset(new PSOPCEncryption(cmd->client_key)); - this->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)); - this->client_output_crypt.reset(new PSOGCEncryption(cmd->server_key)); - - } else { - throw invalid_argument("unsupported version"); - } - break; - } - - case 0x19: { // reconnect - struct ReconnectCommandArgs { - uint32_t address; - uint16_t port; - uint16_t unused; - }; - - if (command.size() < sizeof(ReconnectCommandArgs) + this->header_size) { - throw std::runtime_error("reconnect command is too small"); - } - - ReconnectCommandArgs* args = reinterpret_cast( - command.data() + this->header_size); - 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_port = htons(args->port); - sin->sin_addr.s_addr = args->address; // already network byte order - - if (!dest_bev) { - log(WARNING, "[ProxyServer] Received reconnect command with no destination present"); - } else { - // If the client is on a virtual connection (fd < 0), we don't need - // to do anything - we assume the client will connect again via a - // virtual connection, and we can just use the destination - // address/port from their connection request. - int fd = bufferevent_getfd(dest_bev); - 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); - args->address = sockname_sin->sin_addr.s_addr; // Already network byte order - args->port = ntohs(sockname_sin->sin_port); // Client expects this little-endian for some reason - } - } - break; - } - - case 0x44: - case 0xA6: { // open quest file - if (!this->save_quests) { - break; - } - - bool is_download_quest = this->get_command_field(input_header) == 0xA6; - - struct OpenFileCommand { - char name[0x20]; - uint16_t unused; - uint16_t flags; - char filename[0x10]; - uint32_t file_size; - }; - if (command.size() < sizeof(OpenFileCommand)) { - log(WARNING, "[ProxyServer] Open file command is too small"); - break; - } - const auto* cmd = reinterpret_cast(command.data() + this->header_size); - - string output_filename = string_printf("%s.%s.%" PRIu64, - cmd->filename, 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] = '_'; - } - - SavingQuestFile sqf(cmd->filename, output_filename, cmd->file_size); - this->saving_quest_files.emplace(cmd->filename, move(sqf)); - log(INFO, "[ProxyServer] Opened quest file %s", output_filename.c_str()); - break; - } - case 0x13: - case 0xA7: { // quest data segment - if (!this->save_quests) { - break; - } - - struct WriteFileCommand { - char filename[0x10]; - uint8_t data[0x400]; - uint32_t data_size; - }; - if (command.size() < sizeof(WriteFileCommand)) { - log(WARNING, "[ProxyServer] Write file command is too small"); - break; - } - const auto* cmd = reinterpret_cast(command.data() + this->header_size); - - SavingQuestFile* sqf = nullptr; - try { - sqf = &this->saving_quest_files.at(cmd->filename); - } catch (const out_of_range&) { - log(WARNING, "[ProxyServer] Can\'t find saving quest file %s", - cmd->filename); - break; - } - - size_t bytes_to_write = cmd->data_size; - if (bytes_to_write > 0x400) { - log(WARNING, "[ProxyServer] Chunk data size is invalid; truncating to 0x400"); - bytes_to_write = 0x400; - } - - log(INFO, "[ProxyServer] Writing %zu bytes to %s", bytes_to_write, - sqf->output_filename.c_str()); - fwritex(sqf->f.get(), cmd->data, bytes_to_write); - if (bytes_to_write > sqf->remaining_bytes) { - log(WARNING, "[ProxyServer] Chunk size extends beyond original file size; file may be truncated"); - sqf->remaining_bytes = 0; - } else { - sqf->remaining_bytes -= bytes_to_write; - } - - if (sqf->remaining_bytes == 0) { - log(INFO, "[ProxyServer] File %s is complete", sqf->output_filename.c_str()); - this->saving_quest_files.erase(cmd->filename); - } - - break; - } - } - } - - // reencrypt and forward the command - if (dest_buf) { - if (dest_crypt) { - dest_crypt->encrypt(command.data(), command.size()); - } - //log(INFO, "[ProxyServer-debug] Sending encrypted command"); - //print_data(stderr, command); - - evbuffer_add(dest_buf, command.data(), command.size()); +static void check_implemented_subcommand( + uint32_t serial_number, uint16_t command, const string& data) { + if (command == 0x60 || command == 0x6C || command == 0xC9 || + command == 0x62 || command == 0x6D || command == 0xCB) { + if (data.size() < 4) { + log(WARNING, "[ProxyServer/%08X] Received broadcast/target command with no contents", serial_number); } else { - log(WARNING, "[ProxyServer] No destination present; dropping command"); + if (!subcommand_is_implemented(data[0])) { + log(WARNING, "[ProxyServer/%08X] Received subcommand %02hhX which is not implemented on the server", + serial_number, data[0]); + } } - - // clear the input header so we can read the next command - memset(input_header, 0, this->header_size); } } + +void ProxyServer::LinkedSession::on_client_input() { + string name = string_printf("ProxySession:%08X:client", this->license->serial_number); + for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(), + [&](uint16_t command, uint32_t flag, const string& data) { + print_received_command(command, flag, data.data(), data.size(), + this->version, name.c_str()); + check_implemented_subcommand(this->license->serial_number, command, data); + + if (!this->client_bev.get()) { + log(WARNING, "[ProxyServer/%08X] No server is present; dropping command", + this->license->serial_number); + } 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()); + } + }); +} + +void ProxyServer::LinkedSession::on_server_input() { + string name = string_printf("ProxySession:%08X:server", this->license->serial_number); + + 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()); + check_implemented_subcommand(this->license->serial_number, command, data); + + bool should_forward = true; + + switch (command) { + case 0x02: + case 0x17: { + 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 + if (data.size() < offsetof(ServerInitCommand_GC_02_17, after_message)) { + throw std::runtime_error("init encryption command is too small"); + } + + const auto* cmd = reinterpret_cast( + data.data()); + + // This doesn't get forwarded to the client, so don't recreate the + // client's crypts + 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; + + // If this is a 17, respond with a DB; otherwise respond with a 9E. + // We don't let the client do this because it believes it already + // did (when it was in an unlinked session). + if (command == 0x17) { + VerifyLicenseCommand_GC_DB cmd; + memset(&cmd, 0, sizeof(cmd)); + snprintf(cmd.serial_number, sizeof(cmd.serial_number), "%08X", + this->license->serial_number); + strncpy(cmd.access_key, this->license->access_key, sizeof(cmd.access_key)); + cmd.sub_version = this->sub_version; + snprintf(cmd.serial_number2, sizeof(cmd.serial_number2), "%08X", + this->license->serial_number); + strncpy(cmd.access_key2, this->license->access_key, sizeof(cmd.access_key2)); + strncpy(cmd.password, this->license->gc_password, sizeof(cmd.password)); + send_command(this->server_bev.get(), this->version, + this->server_output_crypt.get(), 0xDB, 0, &cmd, sizeof(cmd), + name.c_str()); + break; + } + // For command 02, intentional fallthrough to 9A case + } + + case 0x9A: { + should_forward = false; + LoginCommand_GC_9E cmd; + memset(&cmd, 0, sizeof(cmd)); + memset(&cmd.unused[2], 0xFF, 6); + cmd.sub_version = this->sub_version; + cmd.unused2[1] = 1; + snprintf(cmd.serial_number, sizeof(cmd.serial_number), "%08X", + this->license->serial_number); + strncpy(cmd.access_key, this->license->access_key, sizeof(cmd.access_key)); + snprintf(cmd.serial_number2, sizeof(cmd.serial_number2), "%08X", + this->license->serial_number); + strncpy(cmd.access_key2, this->license->access_key, sizeof(cmd.access_key2)); + strncpy(cmd.name, this->character_name.c_str(), sizeof(cmd.name)); + send_command(this->server_bev.get(), this->version, + this->server_output_crypt.get(), 0x9E, 0, &cmd, sizeof(cmd), + name.c_str()); + break; + } + + case 0x19: { + struct ReconnectCommand { + be_uint32_t address; + uint16_t port; + uint16_t unused; + }; + + if (data.size() < sizeof(ReconnectCommand)) { + throw std::runtime_error("reconnect command is too small"); + } + + auto* args = reinterpret_cast(data.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 = args->address.load_raw(); + sin->sin_port = htons(args->port); + + if (!this->client_bev.get()) { + log(WARNING, "[ProxyServer/%08X] Received reconnect command with no destination present", + this->license->serial_number); + + } 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); + args->address.store_raw(sockname_sin->sin_addr.s_addr); + args->port = ntohs(sockname_sin->sin_port); + + } else { + args->port = this->local_port; + } + } + break; + } + + case 0x44: + case 0xA6: { + if (!this->save_quests) { + break; + } + + bool is_download_quest = (command == 0xA6); + + struct OpenFileCommand { + char name[0x20]; + uint16_t unused; + uint16_t flags; + char filename[0x10]; + uint32_t file_size; + }; + if (data.size() < sizeof(OpenFileCommand)) { + log(WARNING, "[ProxyServer/%08X] Open file command is too small; skipping file", + this->license->serial_number); + break; + } + const auto* cmd = reinterpret_cast(data.data()); + + string output_filename = string_printf("%s.%s.%" PRIu64, + cmd->filename, 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] = '_'; + } + + SavingQuestFile sqf(cmd->filename, output_filename, cmd->file_size); + this->saving_quest_files.emplace(cmd->filename, move(sqf)); + log(INFO, "[ProxyServer/%08X] Opened quest file %s", + this->license->serial_number, output_filename.c_str()); + break; + } + + case 0x13: + case 0xA7: { + if (!this->save_quests) { + break; + } + + struct WriteFileCommand { + char filename[0x10]; + uint8_t data[0x400]; + uint32_t data_size; + }; + if (data.size() < sizeof(WriteFileCommand)) { + log(WARNING, "[ProxyServer/%08X] Write file command is too small", + this->license->serial_number); + break; + } + const auto* cmd = reinterpret_cast(data.data()); + + SavingQuestFile* sqf = nullptr; + try { + sqf = &this->saving_quest_files.at(cmd->filename); + } catch (const out_of_range&) { + log(WARNING, "[ProxyServer/%08X] Can\'t find saving quest file %s", + this->license->serial_number, cmd->filename); + break; + } + + size_t bytes_to_write = cmd->data_size; + if (bytes_to_write > 0x400) { + log(WARNING, "[ProxyServer/%08X] Chunk data size is invalid; truncating to 0x400", + this->license->serial_number); + bytes_to_write = 0x400; + } + + log(INFO, "[ProxyServer/%08X] Writing %zu bytes to %s", + this->license->serial_number, bytes_to_write, + sqf->output_filename.c_str()); + fwritex(sqf->f.get(), cmd->data, bytes_to_write); + if (bytes_to_write > sqf->remaining_bytes) { + log(WARNING, "[ProxyServer/%08X] Chunk size extends beyond original file size; file may be truncated", + this->license->serial_number); + sqf->remaining_bytes = 0; + } else { + sqf->remaining_bytes -= bytes_to_write; + } + + if (sqf->remaining_bytes == 0) { + log(INFO, "[ProxyServer/%08X] File %s is complete", + this->license->serial_number, sqf->output_filename.c_str()); + this->saving_quest_files.erase(cmd->filename); + } + break; + } + } + + if (should_forward) { + if (!this->client_bev.get()) { + log(WARNING, "[ProxyServer/%08X] No client is present; dropping command", + this->license->serial_number); + } 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()); + } + } + }); + + } catch (const exception& e) { + log(ERROR, "[ProxyServer/%08X] Failed to process server command: %s", + this->license->serial_number, e.what()); + this->disconnect(); + } +} + +void ProxyServer::LinkedSession::send_to_end(const string& data, bool to_server) { + string name = string_printf("ProxySession:%08X:shell:%s", + this->license->serial_number, to_server ? "server" : "client"); + + size_t header_size = PSOCommandHeader::header_size(this->version); + if (data.size() < header_size) { + throw runtime_error("command is too small for header"); + } + if (data.size() & 3) { + throw runtime_error("command size is not a multiple of 4"); + } + const auto* header = reinterpret_cast(data.data()); + + 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), + data.data() + header_size, + data.size() + header_size, + name.c_str()); +} + +shared_ptr ProxyServer::get_session() { + if (this->serial_number_to_session.empty()) { + throw runtime_error("no sessions exist"); + } + if (this->serial_number_to_session.size() > 1) { + throw runtime_error("multiple sessions exist"); + } + return this->serial_number_to_session.begin()->second; +} + +void ProxyServer::delete_session(uint32_t serial_number) { + this->serial_number_to_session.erase(serial_number); +} diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 69398311..593f4a86 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -12,6 +12,7 @@ #include "PSOEncryption.hh" #include "PSOProtocol.hh" +#include "ServerState.hh" @@ -22,77 +23,122 @@ public: ProxyServer(ProxyServer&&) = delete; ProxyServer( std::shared_ptr base, - const struct sockaddr_storage& initial_destination, - GameVersion version); + std::shared_ptr state); virtual ~ProxyServer() = default; - void listen(int port); + void listen(uint16_t port, GameVersion version); - void connect_client(struct bufferevent* bev, uint32_t server_ipv4_addr, - uint16_t server_port); + void connect_client(struct bufferevent* bev, uint16_t server_port); - void send_to_client(const std::string& data); - void send_to_server(const std::string& data); + struct LinkedSession { + ProxyServer* server; - void set_save_quests(bool save_quests); + std::shared_ptr license; + + std::unique_ptr client_bev; + std::unique_ptr server_bev; + uint16_t local_port; + struct sockaddr_storage next_destination; + GameVersion version; + uint32_t sub_version; + std::string character_name; + + std::shared_ptr client_input_crypt; + std::shared_ptr client_output_crypt; + std::shared_ptr server_input_crypt; + std::shared_ptr server_output_crypt; + + struct SavingQuestFile { + std::string basename; + std::string output_filename; + uint32_t remaining_bytes; + std::unique_ptr> f; + + SavingQuestFile( + const std::string& basename, + const std::string& output_filename, + uint32_t remaining_bytes); + }; + bool save_quests; + std::unordered_map saving_quest_files; + + LinkedSession( + ProxyServer* server, + uint16_t local_port, + GameVersion version, + std::shared_ptr license, + uint32_t dest_addr, + uint16_t dest_port); + + void resume( + std::unique_ptr&& client_bev, + std::shared_ptr client_input_crypt, + std::shared_ptr client_output_crypt, + uint32_t sub_version, + const std::string& character_name); + + static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx); + static void dispatch_on_server_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_server_error(struct bufferevent* bev, short events, + void* ctx); + void on_client_input(); + void on_server_input(); + void on_stream_error(short events, bool is_server_stream); + + void send_to_end(const std::string& data, bool to_server); + + void disconnect(); + + bool is_open() const; + }; + + std::shared_ptr get_session(); + void delete_session(uint32_t serial_number); private: - std::shared_ptr base; - std::map> listeners; - std::unique_ptr client_bev; - std::unique_ptr server_bev; - struct sockaddr_storage next_destination; - struct sockaddr_storage default_destination; - int listen_port; - GameVersion version; + struct ListeningSocket { + ProxyServer* server; - size_t header_size; - PSOCommandHeader client_input_header; - PSOCommandHeader server_input_header; - std::shared_ptr client_input_crypt; - std::shared_ptr client_output_crypt; - std::shared_ptr server_input_crypt; - std::shared_ptr server_output_crypt; + int fd; + uint16_t port; + std::unique_ptr listener; + GameVersion version; - struct SavingQuestFile { - std::string basename; - std::string output_filename; - uint32_t remaining_bytes; - std::unique_ptr> f; - - SavingQuestFile( - const std::string& basename, - const std::string& output_filename, - uint32_t remaining_bytes); + static void dispatch_on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); + static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); + void on_listen_accept(struct sockaddr *address, int socklen); + void on_listen_error(); }; - bool save_quests; - std::unordered_map saving_quest_files; - void send_to_end(const std::string& data, bool to_server); + struct UnlinkedSession { + ProxyServer* server; - static void dispatch_on_listen_accept(struct evconnlistener* listener, - evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); - static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); - static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); - static void dispatch_on_client_error(struct bufferevent* bev, short events, - void* ctx); - static void dispatch_on_server_input(struct bufferevent* bev, void* ctx); - static void dispatch_on_server_error(struct bufferevent* bev, short events, - void* ctx); + std::unique_ptr bev; + uint16_t local_port; + GameVersion version; - void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, - struct sockaddr *address, int socklen); - void on_listen_error(struct evconnlistener* listener); - void on_client_input(struct bufferevent* bev); - void on_client_error(struct bufferevent* bev, short events); - void on_server_input(struct bufferevent* bev); - void on_server_error(struct bufferevent* bev, short events); + std::shared_ptr crypt_out; + std::shared_ptr crypt_in; - void on_client_connect(struct bufferevent* bev, - uint32_t server_ipv4_addr = 0, uint16_t server_port = 0); + UnlinkedSession(ProxyServer* server, struct bufferevent* bev, uint16_t port, GameVersion version); - size_t get_size_field(const PSOCommandHeader* header); - size_t get_command_field(const PSOCommandHeader* header); + void receive_and_process_commands(); - void receive_and_process_commands(bool from_server); + static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); + static void dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx); + void on_client_input(); + void on_client_error(short events); + }; + + std::shared_ptr base; + std::shared_ptr state; + std::map> listeners; + std::unordered_map> bev_to_unlinked_session; + std::unordered_map> serial_number_to_session; + + void on_client_connect(struct bufferevent* bev, uint16_t port, GameVersion version); }; diff --git a/src/ProxyShell.cc b/src/ProxyShell.cc deleted file mode 100644 index 873516c5..00000000 --- a/src/ProxyShell.cc +++ /dev/null @@ -1,146 +0,0 @@ -#include "ProxyShell.hh" - -#include -#include - -#include - -using namespace std; - - - -ProxyShell::ProxyShell(std::shared_ptr base, - std::shared_ptr state, - std::shared_ptr proxy_server) : Shell(base, state), - proxy_server(proxy_server) { } - -void ProxyShell::execute_command(const string& command) { - // find the entry in the command table and run the command - size_t command_end = skip_non_whitespace(command, 0); - size_t args_begin = skip_whitespace(command, command_end); - string command_name = command.substr(0, command_end); - string command_args = command.substr(args_begin); - - if (command_name == "exit") { - throw exit_shell(); - - } else if (command_name == "help") { - fprintf(stderr, "\ -Commands:\n\ - help\n\ - You\'re reading it now.\n\ - exit (or ctrl+d)\n\ - Shut down the proxy.\n\ - sc \n\ - Send a command to the client.\n\ - ss \n\ - Send a command to the server.\n\ - chat \n\ - Send a chat message to the server.\n\ - dchat \n\ - Send a chat message to the server with arbitrary data in it.\n\ - info-board \n\ - Set your info board contents.\n\ - info-board-data \n\ - Set your info board contents with arbitrary data.\n\ - marker \n\ - Send a lobby marker message to the server.\n\ - event \n\ - Send a lobby event update to yourself.\n\ - ship\n\ - Request the ship select menu from the server.\n\ -"); - - } else if ((command_name == "sc") || (command_name == "ss")) { - bool to_client = (command_name[1] == 'c'); - string data = parse_data_string(command_args); - if (data.size() & 3) { - throw invalid_argument("data size is not a multiple of 4"); - } - if (data.size() == 0) { - throw invalid_argument("no data given"); - } - uint16_t* size_field = reinterpret_cast(data.data() + 2); - *size_field = data.size(); - - log(INFO, "%s (from proxy):", to_client ? "server" : "client"); - print_data(stderr, data); - - if (to_client) { - this->proxy_server->send_to_client(data); - } else { - this->proxy_server->send_to_server(data); - } - - } else if ((command_name == "chat") || (command_name == "dchat")) { - string data(12, '\0'); - data[0] = 0x06; - data.push_back('\x09'); - data.push_back('E'); - if (command_name == "dchat") { - data += parse_data_string(command_args); - } else { - data += command_args; - } - data.push_back('\0'); - data.resize((data.size() + 3) & (~3)); - uint16_t* size_field = reinterpret_cast(data.data() + 2); - *size_field = data.size(); - - log(INFO, "Client (from proxy):"); - print_data(stderr, data); - this->proxy_server->send_to_server(data); - - } else if (command_name == "marker") { - string data("\x89\x00\x04\x00", 4); - data[1] = stod(command_args); - - log(INFO, "Client (from proxy):"); - print_data(stderr, data); - this->proxy_server->send_to_server(data); - - } else if (command_name == "event") { - string data("\xDA\x00\x04\x00", 4); - data[1] = stod(command_args); - - log(INFO, "Server (from proxy):"); - print_data(stderr, data); - this->proxy_server->send_to_client(data); - - } else if (command_name == "ship") { - static const string data("\xA0\x00\x04\x00", 4); - - log(INFO, "Server (from proxy):"); - print_data(stderr, data); - this->proxy_server->send_to_server(data); - - } else if ((command_name == "info-board") || (command_name == "info-board-data")) { - string data(4, '\0'); - data[0] = 0xD9; - if (command_name == "info-board-data") { - data += parse_data_string(command_args); - } else { - data += command_args; - } - data.push_back('\0'); - data.resize((data.size() + 3) & (~3)); - uint16_t* size_field = reinterpret_cast(data.data() + 2); - *size_field = data.size(); - - log(INFO, "Client (from proxy):"); - print_data(stderr, data); - this->proxy_server->send_to_server(data); - - } else if (command_name == "set-save-quests") { - if (command_args == "on") { - this->proxy_server->set_save_quests(true); - } else if (command_args == "off") { - this->proxy_server->set_save_quests(false); - } else { - throw invalid_argument("argument must be \"on\" or \"off\""); - } - - } else { - throw invalid_argument("unknown command; try \'help\'"); - } -} diff --git a/src/ProxyShell.hh b/src/ProxyShell.hh deleted file mode 100644 index 97168baa..00000000 --- a/src/ProxyShell.hh +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -#include - -#include "Shell.hh" -#include "ProxyServer.hh" - - - -class ProxyShell : public Shell { -public: - ProxyShell(std::shared_ptr base, - std::shared_ptr state, - std::shared_ptr proxy_server); - virtual ~ProxyShell() = default; - ProxyShell(const ProxyShell&) = delete; - ProxyShell(ProxyShell&&) = delete; - ProxyShell& operator=(const ProxyShell&) = delete; - ProxyShell& operator=(ProxyShell&&) = delete; - -protected: - std::shared_ptr proxy_server; - - virtual void execute_command(const std::string& command); -}; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index d43a0b21..df5d0cae 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1,10 +1,11 @@ -#include "SendCommands.hh" +#include "ReceiveCommands.hh" #include #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include "SendCommands.hh" #include "ReceiveSubcommands.hh" #include "ChatCommands.hh" +#include "ProxyServer.hh" using namespace std; @@ -196,21 +198,10 @@ void process_disconnect(shared_ptr s, shared_ptr c) { void process_verify_license_gc(shared_ptr s, shared_ptr c, uint16_t, uint32_t, uint16_t size, const void* data) { // DB - struct Cmd { - char unused[0x20]; - char serial_number[0x10]; - char access_key[0x10]; - char unused2[0x08]; - uint32_t sub_version; - char unused3[0x60]; - char password[0x10]; - char unused4[0x20]; - }; - check_size(size, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + check_size(size, sizeof(VerifyLicenseCommand_GC_DB)); + const auto* cmd = reinterpret_cast(data); - uint32_t serial_number = 0; - sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); try { c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, cmd->password); @@ -221,8 +212,10 @@ void process_verify_license_gc(shared_ptr s, shared_ptr c, c->should_disconnect = true; return; } else { - c->license = LicenseManager::create_license_pc(serial_number, - cmd->access_key, cmd->password); + auto l = LicenseManager::create_license_gc(serial_number, + cmd->access_key, cmd->password, true); + s->license_manager->add(l); + c->license = l; } } @@ -240,8 +233,7 @@ void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, check_size(size, sizeof(Cmd)); const auto* cmd = reinterpret_cast(data); - uint32_t serial_number = 0; - sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); try { if (c->version == GameVersion::GC) { c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, @@ -251,20 +243,14 @@ void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, nullptr); } } catch (const exception& e) { - if (!s->allow_unregistered_users) { - u16string message = u"Login failed: " + decode_sjis(e.what()); - send_message_box(c, message.c_str()); - c->should_disconnect = true; - return; - } else { - if (c->version == GameVersion::GC) { - c->license = LicenseManager::create_license_gc(serial_number, - cmd->access_key, nullptr); - } else { - c->license = LicenseManager::create_license_pc(serial_number, - cmd->access_key, nullptr); - } - } + // The client should have sent a different command containing the password + // already, which should have created and added a temporary license. If no + // license exists, disconnect the client even if unregistered clients are + // allowed. + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; } send_command(c, 0x9C, 0x01); @@ -285,8 +271,7 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, c->flags |= flags_for_version(c->version, cmd->sub_version); - uint32_t serial_number = 0; - sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); try { if (c->version == GameVersion::GC) { c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, @@ -302,13 +287,16 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, c->should_disconnect = true; return; } else { + shared_ptr l; if (c->version == GameVersion::GC) { - c->license = LicenseManager::create_license_gc(serial_number, - cmd->access_key, cmd->password); + l = LicenseManager::create_license_gc(serial_number, + cmd->access_key, cmd->password, true); } else { - c->license = LicenseManager::create_license_pc(serial_number, - cmd->access_key, cmd->password); + l = LicenseManager::create_license_pc(serial_number, + cmd->access_key, cmd->password, true); } + s->license_manager->add(l); + c->license = l; } } @@ -317,28 +305,13 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, uint16_t, uint32_t, uint16_t size, const void* data) { // 9D 9E - struct Cmd { - char unused[0x10]; - uint8_t sub_version; - uint8_t unused2[0x27]; - char serial_number[0x10]; - char access_key[0x10]; - char unused3[0x60]; - char name[0x10]; - // Note: there are 8 bytes at the end of cfg that are technically not - // included in the client config on GC, but the field after it is - // sufficiently large and unused anyway - ClientConfig cfg; - uint8_t unused4[0x5C]; - }; // sometimes the unused bytes aren't sent? - check_size(size, sizeof(Cmd) - 0x64, sizeof(Cmd)); - const auto* cmd = reinterpret_cast(data); + check_size(size, sizeof(LoginCommand_GC_9E) - 0x64, sizeof(LoginCommand_GC_9E)); + const auto* cmd = reinterpret_cast(data); c->flags |= flags_for_version(c->version, cmd->sub_version); - uint32_t serial_number = 0; - sscanf(cmd->serial_number, "%8" PRIX32, &serial_number); + uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16); try { if (c->version == GameVersion::GC) { c->license = s->license_manager->verify_gc(serial_number, cmd->access_key, @@ -348,20 +321,12 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, nullptr); } } catch (const exception& e) { - if (!s->allow_unregistered_users) { - u16string message = u"Login failed: " + decode_sjis(e.what()); - send_message_box(c, message.c_str()); - c->should_disconnect = true; - return; - } else { - if (c->version == GameVersion::GC) { - c->license = LicenseManager::create_license_gc(serial_number, - cmd->access_key, nullptr); - } else { - c->license = LicenseManager::create_license_pc(serial_number, - cmd->access_key, nullptr); - } - } + // See comment in 9A handler about why we do this even if unregistered users + // are allowed on the server + u16string message = u"Login failed: " + decode_sjis(e.what()); + send_message_box(c, message.c_str()); + c->should_disconnect = true; + return; } try { @@ -612,7 +577,13 @@ void process_menu_item_info_request(shared_ptr s, shared_ptr s, shared_ptritem_id == PROXY_DESTINATIONS_MENU_GO_BACK) { + send_ship_info(c, u"Return to the\nmain menu."); + } else { + try { + // we use item_id + 1 here because "go back" is the first item + send_ship_info(c, s->proxy_destinations_menu.at(cmd->item_id + 1).description.c_str()); + } catch (const out_of_range&) { + send_ship_info(c, u"$C6No such information exists."); + } + } + break; + case QUEST_MENU_ID: { if (!s->quest_index) { send_quest_info(c, u"$C6Quests are not available."); @@ -690,6 +674,11 @@ void process_menu_selection(shared_ptr s, shared_ptr c, c->flags |= ClientFlag::IN_INFORMATION_MENU; break; + case MAIN_MENU_PROXY_DESTINATIONS: + send_menu(c, u"Proxy server", PROXY_DESTINATIONS_MENU_ID, + s->proxy_destinations_menu, false); + break; + case MAIN_MENU_DOWNLOAD_QUESTS: send_quest_menu(c, QUEST_FILTER_MENU_ID, quest_download_menu, true); break; @@ -720,6 +709,41 @@ void process_menu_selection(shared_ptr s, shared_ptr c, break; } + case PROXY_DESTINATIONS_MENU_ID: { + if (cmd->item_id == PROXY_DESTINATIONS_MENU_GO_BACK) { + send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false); + + } else { + pair* dest = nullptr; + try { + dest = &s->proxy_destinations.at(cmd->item_id); + } catch (const out_of_range&) { } + + if (!dest) { + send_message_box(c, u"$C6No such destination exists."); + c->should_disconnect = true; + } else { + // TODO: We can probably avoid using client config and reconnecting the + // client here; it's likely we could build a way to just directly link + // the client to the proxy server instead (would have to provide + // license/char name/etc. for remote auth) + + c->proxy_destination_address = resolve_ipv4(dest->first); + c->proxy_destination_port = dest->second; + send_update_client_config(c); + + s->proxy_server->delete_session(c->license->serial_number); + + static const vector version_to_port_name({ + "dc-proxy", "pc-proxy", "", "gc-proxy", "bb-proxy"}); + const auto& port_name = version_to_port_name.at(static_cast(c->version)); + send_reconnect(c, s->connect_address_for_client(c), + s->named_port_configuration.at(port_name).port); + } + } + break; + } + case GAME_MENU_ID: { auto game = s->find_lobby(cmd->item_id); if (!game) { @@ -2219,43 +2243,8 @@ static process_command_t* handlers[6] = { void process_command(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, uint16_t size, const void* data) { - // TODO: this is slow; make it better somehow - { - string name_token; - if (c->player.disp.name[0]) { - name_token = " from " + remove_language_marker(encode_sjis(c->player.disp.name)); - } - log(INFO, "Received%s version=%d size=%04hX command=%04hX flag=%08X", - name_token.c_str(), static_cast(c->version), size, command, flag); - - string data_to_print; - if (c->version == GameVersion::BB) { - data_to_print.resize(size + 8); - PSOCommandHeaderBB* header = reinterpret_cast( - data_to_print.data()); - header->command = command; - header->flag = flag; - header->size = size + 8; - memcpy(data_to_print.data() + 8, data, size); - } else if (c->version == GameVersion::PC) { - data_to_print.resize(size + 4); - PSOCommandHeaderPC* header = reinterpret_cast( - data_to_print.data()); - header->command = command; - header->flag = flag; - header->size = size + 4; - memcpy(data_to_print.data() + 4, data, size); - } else { // DC/GC - data_to_print.resize(size + 4); - PSOCommandHeaderDCGC* header = reinterpret_cast( - data_to_print.data()); - header->command = command; - header->flag = flag; - header->size = size + 4; - memcpy(data_to_print.data() + 4, data, size); - } - print_data(stderr, data_to_print); - } + string encoded_name = remove_language_marker(encode_sjis(c->player.disp.name)); + print_received_command(command, flag, data, size, c->version, encoded_name.c_str()); auto fn = handlers[static_cast(c->version)][command & 0xFF]; if (fn) { diff --git a/src/ReceiveCommands.hh b/src/ReceiveCommands.hh index c2443ecd..1d6cfbad 100644 --- a/src/ReceiveCommands.hh +++ b/src/ReceiveCommands.hh @@ -5,6 +5,39 @@ #include "ServerState.hh" + +// These commands' structures are defined here because they're used by both the +// game server and proxy server + +struct VerifyLicenseCommand_GC_DB { + char unused[0x20]; + char serial_number[0x10]; + char access_key[0x10]; + char unused2[0x08]; + uint32_t sub_version; + char serial_number2[0x30]; + char access_key2[0x30]; + char password[0x30]; +} __attribute__((packed)); + +struct LoginCommand_GC_9E { + char unused[0x10]; // 00 00 FF FF FF FF FF FF 00 00 00 00 00 00 00 00 + uint32_t sub_version; + uint8_t unused2[0x24]; // 00 01 00 00 ... (rest is 00) + char serial_number[0x10]; + char access_key[0x10]; + char serial_number2[0x30]; + char access_key2[0x30]; + char name[0x10]; + // Note: there are 8 bytes at the end of cfg that are technically not + // included in the client config on GC, but the field after it is + // sufficiently large and unused anyway + ClientConfig cfg; + uint8_t unused4[0x5C]; +} __attribute__((packed)); + + + void process_connect(std::shared_ptr s, std::shared_ptr c); void process_disconnect(std::shared_ptr s, std::shared_ptr c); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 20a49b3e..76a75a48 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -24,11 +25,18 @@ static FileContentsCache file_cache; -void send_command(shared_ptr c, uint16_t command, uint32_t flag, - const void* data, size_t size) { +void send_command( + struct bufferevent* bev, + GameVersion version, + PSOEncryption* crypt, + uint16_t command, + uint32_t flag, + const void* data, + size_t size, + const char* name_str) { string send_data; - switch (c->version) { + switch (version) { case GameVersion::GC: case GameVersion::DC: { PSOCommandHeaderDCGC header; @@ -74,15 +82,32 @@ void send_command(shared_ptr c, uint16_t command, uint32_t flag, throw logic_error("unimplemented game version in send_command"); } - string name_token; - if (c->player.disp.name[0]) { - name_token = " to " + remove_language_marker(encode_sjis(c->player.disp.name)); + if (name_str) { + string name_token; + if (name_str[0]) { + name_token = string(" to ") + name_str; + } + log(INFO, "Sending%s (version=%d command=%04hX flag=%08X)", + name_token.c_str(), static_cast(version), command, flag); + print_data(stderr, send_data.data(), send_data.size()); } - log(INFO, "Sending%s version=%d size=%04zX command=%04hX flag=%08X", - name_token.c_str(), static_cast(c->version), size, command, flag); - print_data(stderr, send_data.data(), send_data.size()); - c->send(move(send_data)); + if (crypt) { + crypt->encrypt(send_data.data(), send_data.size()); + } + + struct evbuffer* buf = bufferevent_get_output(bev); + evbuffer_add(buf, send_data.data(), send_data.size()); +} + +void send_command(shared_ptr c, uint16_t command, uint32_t flag, + const void* data, size_t size) { + if (!c->bev) { + return; + } + string encoded_name = remove_language_marker(encode_sjis(c->player.disp.name)); + send_command(c->bev, c->version, c->crypt_out.get(), command, flag, data, + size, encoded_name.c_str()); } void send_command_excluding_client(shared_ptr l, shared_ptr c, @@ -132,14 +157,8 @@ string prepare_server_init_contents_dc_pc_gc( bool initial_connection, uint32_t server_key, uint32_t client_key) { - struct Command { - char copyright[0x40]; - uint32_t server_key; - uint32_t client_key; - char after_message[200]; - }; - string ret(sizeof(Command), '\0'); - auto* cmd = reinterpret_cast(ret.data()); + string ret(sizeof(ServerInitCommand_GC_02_17), '\0'); + auto* cmd = reinterpret_cast(ret.data()); strcpy(cmd->copyright, initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright); cmd->server_key = server_key; diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 7db17831..319ed5f4 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -16,20 +16,9 @@ -#define MAIN_MENU_ID 0x60000000 -#define INFORMATION_MENU_ID 0x60000030 -#define LOBBY_MENU_ID 0x60000060 -#define GAME_MENU_ID 0x60000090 -#define QUEST_MENU_ID 0x600000C0 -#define QUEST_FILTER_MENU_ID 0x600000F0 - -#define MAIN_MENU_GO_TO_LOBBY 0x00000001 -#define MAIN_MENU_INFORMATION 0x00000002 -#define MAIN_MENU_DOWNLOAD_QUESTS 0x00000003 -#define MAIN_MENU_DISCONNECT 0x00000004 -#define INFORMATION_MENU_GO_BACK 0xFFFFFFFF - - +void send_command(struct bufferevent* bev, GameVersion version, + PSOEncryption* crypt, uint16_t command, uint32_t flag = 0, + const void* data = nullptr, size_t size = 0, const char* name_str = nullptr); void send_command(std::shared_ptr c, uint16_t command, uint32_t flag = 0, const void* data = nullptr, size_t size = 0); @@ -73,6 +62,12 @@ void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, +struct ServerInitCommand_GC_02_17 { + char copyright[0x40]; + uint32_t server_key; + uint32_t client_key; + char after_message[200]; +} __attribute__((packed)); std::string prepare_server_init_contents_dc_pc_gc( bool initial_connection, uint32_t server_key, uint32_t client_key); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index a3e2ee92..a6e2238e 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -14,14 +14,23 @@ using namespace std; -ServerShell::ServerShell(std::shared_ptr base, - std::shared_ptr state) : Shell(base, state) { } +ServerShell::ServerShell( + shared_ptr base, + shared_ptr state) + : Shell(base, state) { } void ServerShell::print_prompt() { fwrite("newserv> ", 9, 1, stdout); fflush(stdout); } +shared_ptr ServerShell::get_proxy_session() { + if (!this->state->proxy_server.get()) { + throw runtime_error("the proxy server is disabled"); + } + return this->state->proxy_server->get_session(); +} + void ServerShell::execute_command(const string& command) { // find the entry in the command table and run the command size_t command_end = skip_non_whitespace(command, 0); @@ -34,9 +43,11 @@ void ServerShell::execute_command(const string& command) { } else if (command_name == "help") { fprintf(stderr, "\ -Commands:\n\ +General commands:\n\ help\n\ You\'re reading it now.\n\ +\n\ +Server commands:\n\ exit (or ctrl+d)\n\ Shut down the server.\n\ reload ...\n\ @@ -66,8 +77,32 @@ Commands:\n\ Song IDs are 0 through 51; the default song is -1.\n\ announce \n\ Send an announcement message to all players.\n\ +\n\ +Proxy commands (these will only work when exactly one client is connected):\n\ + sc \n\ + Send a command to the client.\n\ + ss \n\ + Send a command to the server.\n\ + chat \n\ + Send a chat message to the server.\n\ + dchat \n\ + Send a chat message to the server with arbitrary data in it.\n\ + info-board \n\ + Set your info board contents.\n\ + info-board-data \n\ + Set your info board contents with arbitrary data.\n\ + marker \n\ + Send a lobby marker message to the server.\n\ + event \n\ + Send a lobby event update to yourself.\n\ + ship\n\ + Request the ship select menu from the server.\n\ "); + + + // SERVER COMMANDS + } else if (command_name == "reload") { auto types = split(command_args, ' '); if (types.empty()) { @@ -189,6 +224,97 @@ Commands:\n\ u16string message16 = decode_sjis(command_args); send_text_message(this->state, message16.c_str()); + + + // PROXY COMMANDS + + } else if ((command_name == "sc") || (command_name == "ss")) { + auto session = this->get_proxy_session(); + + bool to_server = (command_name[1] == 's'); + string data = parse_data_string(command_args); + if (data.size() & 3) { + throw invalid_argument("data size is not a multiple of 4"); + } + 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); + + } else if ((command_name == "chat") || (command_name == "dchat")) { + auto session = this->get_proxy_session(); + + string data(12, '\0'); + data[0] = 0x06; + data.push_back('\x09'); + data.push_back('E'); + if (command_name == "dchat") { + data += parse_data_string(command_args); + } else { + data += command_args; + } + 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); + + } 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); + + } else if (command_name == "event") { + auto session = this->get_proxy_session(); + + string data("\xDA\x00\x04\x00", 4); + data[1] = stod(command_args); + + session->send_to_end(data, false); + + } else if (command_name == "ship") { + auto session = this->get_proxy_session(); + + static const string data("\xA0\x00\x04\x00", 4); + session->send_to_end(data, true); + + } else if ((command_name == "info-board") || (command_name == "info-board-data")) { + auto session = this->get_proxy_session(); + + string data(4, '\0'); + data[0] = 0xD9; + if (command_name == "info-board-data") { + data += parse_data_string(command_args); + } else { + data += command_args; + } + 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); + + } else if (command_name == "set-save-quests") { + auto session = this->get_proxy_session(); + + if (command_args == "on") { + session->save_quests = true; + } else if (command_args == "off") { + session->save_quests = false; + } else { + throw invalid_argument("argument must be \"on\" or \"off\""); + } + + + } else { throw invalid_argument("unknown command; try \'help\'"); } diff --git a/src/ServerShell.hh b/src/ServerShell.hh index c88b41c3..1b4f66d5 100644 --- a/src/ServerShell.hh +++ b/src/ServerShell.hh @@ -6,12 +6,14 @@ #include #include "Shell.hh" +#include "ProxyServer.hh" class ServerShell : public Shell { public: - ServerShell(std::shared_ptr base, + ServerShell( + std::shared_ptr base, std::shared_ptr state); virtual ~ServerShell() = default; ServerShell(const ServerShell&) = delete; @@ -20,6 +22,8 @@ public: ServerShell& operator=(ServerShell&&) = delete; protected: + std::shared_ptr get_proxy_session(); + virtual void print_prompt(); virtual void execute_command(const std::string& command); }; diff --git a/src/ServerState.cc b/src/ServerState.cc index 3f2c725d..341897b4 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -22,15 +22,6 @@ ServerState::ServerState() ep3_menu_song(-1) { memset(&this->default_key_file, 0, sizeof(this->default_key_file)); - this->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby", - u"Join the lobby.", 0); - this->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information", - u"View server information.", MenuItemFlag::REQUIRES_MESSAGE_BOXES); - this->main_menu.emplace_back(MAIN_MENU_DOWNLOAD_QUESTS, u"Download quests", - u"Download quests.", 0); - this->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect", - u"Disconnect.", 0); - for (size_t x = 0; x < 20; x++) { auto lobby_name = decode_sjis(string_printf("LOBBY%zu", x + 1)); shared_ptr l(new Lobby()); diff --git a/src/ServerState.hh b/src/ServerState.hh index ecee228a..ce878d82 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -18,6 +18,9 @@ +// Forwawrd declaration due to reference cycle +class ProxyServer; + struct PortConfiguration { uint16_t port; GameVersion version; @@ -51,6 +54,8 @@ struct ServerState { std::vector main_menu; std::shared_ptr> information_menu; std::shared_ptr> information_contents; + std::vector proxy_destinations_menu; + std::vector> proxy_destinations; std::u16string welcome_message; std::map> id_to_lobby; @@ -62,6 +67,10 @@ struct ServerState { uint32_t local_address; uint32_t external_address; + // TODO: This is only here because the menu selection handler has to call + // delete_session on it. Find a cleaner way to do this. + std::shared_ptr proxy_server; + ServerState(); void add_client_to_available_lobby(std::shared_ptr c); diff --git a/system/config.json b/system/config.json index 1700f391..898a84d6 100755 --- a/system/config.json +++ b/system/config.json @@ -30,6 +30,13 @@ // simulator. This is generally only useful for finding bugs in the interface. // "IPStackDebug": true, + // Other servers to support proxying to. If this is empty, the proxy server is + // disabled. + "ProxyDestinations": { + // "name (appears in menu)": "netloc" + "Schtserv": "149.56.167.128:9103", + }, + // 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. // "RunInteractiveShell": false,