#include "ProxyServer.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "PSOProtocol.hh" #include "SendCommands.hh" #include "ReceiveCommands.hh" #include "ReceiveSubcommands.hh" #include "ProxyCommands.hh" using namespace std; static const uint32_t SESSION_TIMEOUT_USECS = 10000000; // 10 seconds static void flush_and_free_bufferevent(struct bufferevent* bev) { bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED); bufferevent_free(bev); } ProxyServer::ProxyServer( shared_ptr base, shared_ptr state) : log("[ProxyServer] "), base(base), state(state), next_unlicensed_session_id(0xFF00000000000001) { } void ProxyServer::listen(uint16_t port, GameVersion version, const struct sockaddr_storage* default_destination) { shared_ptr socket_obj(new ListeningSocket( this, port, version, default_destination)); auto l = this->listeners.emplace(port, socket_obj).first->second; } ProxyServer::ListeningSocket::ListeningSocket( ProxyServer* server, uint16_t port, GameVersion version, const struct sockaddr_storage* default_destination) : server(server), log(string_printf("[ProxyServer:ListeningSocket:%hu] ", port)), port(port), fd(::listen("", port, SOMAXCONN)), listener(nullptr, evconnlistener_free), version(version) { if (!this->fd.is_open()) { throw runtime_error("cannot listen on port"); } this->listener.reset(evconnlistener_new( this->server->base.get(), &ProxyServer::ListeningSocket::dispatch_on_listen_accept, this, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 0, this->fd)); if (!listener) { throw runtime_error("cannot create listener"); } evconnlistener_set_error_cb( this->listener.get(), &ProxyServer::ListeningSocket::dispatch_on_listen_error); if (default_destination) { this->default_destination = *default_destination; } else { this->default_destination.ss_family = 0; } this->log(INFO, "Listening on TCP port %hu (%s) on fd %d", this->port, name_for_version(this->version), static_cast(this->fd)); } void ProxyServer::ListeningSocket::dispatch_on_listen_accept( struct evconnlistener*, evutil_socket_t fd, struct sockaddr*, int, void* ctx) { reinterpret_cast(ctx)->on_listen_accept(fd); } void ProxyServer::ListeningSocket::dispatch_on_listen_error( struct evconnlistener*, void* ctx) { reinterpret_cast(ctx)->on_listen_error(); } void ProxyServer::ListeningSocket::on_listen_accept(int fd) { this->log(INFO, "Client connected on fd %d (port %hu, version %s)", fd, this->port, name_for_version(this->version)); 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, (this->default_destination.ss_family == AF_INET) ? &this->default_destination : nullptr); } void ProxyServer::ListeningSocket::on_listen_error() { int err = EVUTIL_SOCKET_ERROR(); this->log(ERROR, "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. // We don't support default-destination proxying for virtual connections (yet) GameVersion version; try { version = this->listeners.at(server_port)->version; } catch (const out_of_range&) { this->log(INFO, "Virtual connection received on unregistered port %hu; closing it", server_port); flush_and_free_bufferevent(bev); return; } this->log(INFO, "Client connected on virtual connection %p (port %hu)", bev, server_port); this->on_client_connect(bev, server_port, version, nullptr); } void ProxyServer::on_client_connect( struct bufferevent* bev, uint16_t listen_port, GameVersion version, const struct sockaddr_storage* default_destination) { // If a default destination exists for this client and the client is a patch // client, create a linked session immediately and connect to the remote // server. This creates a direct session. if (default_destination && (version == GameVersion::PATCH)) { uint64_t session_id = this->next_unlicensed_session_id++; if (this->next_unlicensed_session_id == 0) { this->next_unlicensed_session_id = 0xFF00000000000001; } auto emplace_ret = this->id_to_session.emplace(session_id, new LinkedSession( this, session_id, listen_port, version, *default_destination)); if (!emplace_ret.second) { throw logic_error("linked session already exists for unlicensed client"); } auto session = emplace_ret.first->second; session->log(INFO, "Opened linked session"); session->resume(bev); // If no default destination exists, or the client is not a patch client, // create an unlinked session - we'll have to get the destination from the // client's config, which we'll get via a 9E command soon. } else { 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; this->log(INFO, "Opened unlinked session"); // Note that this should only be set when the linked session is created, not // when it is resumed! if (default_destination) { session->next_destination = *default_destination; } switch (version) { case GameVersion::PATCH: throw logic_error("cannot create unlinked patch session"); case GameVersion::PC: case GameVersion::GC: { uint32_t server_key = random_object(); uint32_t client_key = random_object(); auto cmd = 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, &cmd, sizeof(cmd), "unlinked proxy client"); bufferevent_flush(session->bev.get(), EV_READ | EV_WRITE, BEV_FLUSH); if (version == GameVersion::PC) { session->crypt_out.reset(new PSOPCEncryption(server_key)); session->crypt_in.reset(new PSOPCEncryption(client_key)); } else { session->crypt_out.reset(new PSOGCEncryption(server_key)); session->crypt_in.reset(new PSOGCEncryption(client_key)); } break; } case GameVersion::BB: { parray server_key; parray client_key; random_data(server_key.data(), server_key.bytes()); random_data(client_key.data(), client_key.bytes()); auto cmd = prepare_server_init_contents_bb(server_key, client_key); send_command( session->bev.get(), session->version, session->crypt_out.get(), 0x03, 0, &cmd, sizeof(cmd), "unlinked proxy client"); bufferevent_flush(session->bev.get(), EV_READ | EV_WRITE, BEV_FLUSH); static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); session->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption( this->state->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key))); session->crypt_in = session->detector_crypt; session->crypt_out.reset(new PSOBBMultiKeyImitatorEncryption( session->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true)); 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), log(string_printf("[ProxyServer:UnlinkedSession:%p] ", bev)), bev(bev, flush_and_free_bufferevent), local_port(local_port), version(version) { memset(&this->next_destination, 0, sizeof(this->next_destination)); 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; ClientConfigBB client_config; string login_command_bb; try { 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::PC) { // We should only get a 9D while the session is unlinked; if we get // anything else, disconnect if (command != 0x9D) { throw runtime_error("command is not 9D"); } const auto& cmd = check_size_t(data, sizeof(C_Login_PC_9D), sizeof(C_LoginWithUnusedSpace_PC_9D)); license = this->server->state->license_manager->verify_pc( stoul(cmd.serial_number, nullptr, 16), cmd.access_key); sub_version = cmd.sub_version; character_name = cmd.name; } else if (this->version == GameVersion::GC) { // We should only get a 9E while the session is unlinked; if we get // anything else, disconnect if (command != 0x9E) { throw runtime_error("command is not 9E"); } const auto& cmd = check_size_t(data, sizeof(C_Login_GC_9E), sizeof(C_LoginWithUnusedSpace_GC_9E)); license = this->server->state->license_manager->verify_gc( stoul(cmd.serial_number, nullptr, 16), cmd.access_key); sub_version = cmd.sub_version; character_name = cmd.name; client_config.cfg = cmd.client_config.cfg; } else if (this->version == GameVersion::BB) { // We should only get a 93 while the session is unlinked; if we get // anything else, disconnect if (command != 0x93) { throw runtime_error("command is not 93"); } const auto& cmd = check_size_t(data); license = this->server->state->license_manager->verify_bb( cmd.username, cmd.password); login_command_bb = data; } else { throw logic_error("unsupported unlinked session version"); } }); } catch (const exception& e) { this->log(ERROR, "Failed to process command from unlinked client: %s", e.what()); 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->id_to_session.at(license->serial_number); session->log(INFO, "Resuming linked session from unlinked session"); } catch (const out_of_range&) { // If there's no open session for this license, then there must be a valid // destination somewhere - either in the client config or in the unlinked // session if (client_config.cfg.magic == CLIENT_CONFIG_MAGIC) { session.reset(new LinkedSession( this->server, this->local_port, this->version, license, client_config)); session->log(INFO, "Opened licensed session for unlinked session based on client config"); } else if (this->next_destination.ss_family == AF_INET) { session.reset(new LinkedSession( this->server, this->local_port, this->version, license, this->next_destination)); session->log(INFO, "Opened licensed session for unlinked session based on unlinked default destination"); } else { this->log(ERROR, "Cannot open linked session: no valid destination in client config or unlinked session"); } } if (session.get()) { this->server->id_to_session.emplace(license->serial_number, session); if (session->version != this->version) { session->log(ERROR, "Linked session has different game version"); } else { // Resume the linked session using the unlinked session try { if (this->version == GameVersion::BB) { session->resume(move(this->bev), this->crypt_in, this->crypt_out, this->detector_crypt, move(login_command_bb)); } else { session->resume(move(this->bev), this->crypt_in, this->crypt_out, this->detector_crypt, sub_version, character_name); } this->crypt_in.reset(); this->crypt_out.reset(); } catch (const exception& e) { session->log(ERROR, "Failed to resume linked session: %s", e.what()); } } } } if (should_close_unlinked_session) { this->log(INFO, "Closing 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(); this->log(WARNING, "Error %d (%s) in unlinked client stream", err, evutil_socket_error_to_string(err)); } if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) { this->log(WARNING, "Unlinked client has disconnected"); this->server->bev_to_unlinked_session.erase(this->bev.get()); } } ProxyServer::LinkedSession::LinkedSession( ProxyServer* server, uint64_t id, uint16_t local_port, GameVersion version) : server(server), id(id), client_name(string_printf("LinkedSession:%08" PRIX64 ":client", this->id)), server_name(string_printf("LinkedSession:%08" PRIX64 ":server", this->id)), log(string_printf("[ProxyServer:LinkedSession:%08" PRIX64 "] ", this->id)), timeout_event(event_new(this->server->base.get(), -1, EV_TIMEOUT, &LinkedSession::dispatch_on_timeout, this), event_free), license(nullptr), client_bev(nullptr, flush_and_free_bufferevent), server_bev(nullptr, flush_and_free_bufferevent), local_port(local_port), version(version), sub_version(0), // This is set during resume() remote_guild_card_number(0), suppress_newserv_commands(true), enable_chat_filter(true), enable_switch_assist(false), save_files(false), function_call_return_value(-1), override_section_id(-1), override_lobby_event(-1), override_lobby_number(-1), lobby_players(12), lobby_client_id(0) { this->last_switch_enabled_command.subcommand = 0; memset(this->prev_server_command_bytes, 0, sizeof(this->prev_server_command_bytes)); } ProxyServer::LinkedSession::LinkedSession( ProxyServer* server, uint16_t local_port, GameVersion version, shared_ptr license, const ClientConfigBB& newserv_client_config) : LinkedSession(server, license->serial_number, local_port, version) { this->license = license; this->newserv_client_config = newserv_client_config; memset(&this->next_destination, 0, sizeof(this->next_destination)); struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); dest_sin->sin_family = AF_INET; dest_sin->sin_port = htons(this->newserv_client_config.cfg.proxy_destination_port); dest_sin->sin_addr.s_addr = htonl(this->newserv_client_config.cfg.proxy_destination_address); } ProxyServer::LinkedSession::LinkedSession( ProxyServer* server, uint16_t local_port, GameVersion version, std::shared_ptr license, const struct sockaddr_storage& next_destination) : LinkedSession(server, license->serial_number, local_port, version) { this->license = license; this->next_destination = next_destination; } ProxyServer::LinkedSession::LinkedSession( ProxyServer* server, uint64_t id, uint16_t local_port, GameVersion version, const struct sockaddr_storage& destination) : LinkedSession(server, id, local_port, version) { this->next_destination = destination; } void ProxyServer::LinkedSession::resume( unique_ptr&& client_bev, shared_ptr client_input_crypt, shared_ptr client_output_crypt, shared_ptr detector_crypt, uint32_t sub_version, const string& character_name) { this->sub_version = sub_version; this->character_name = character_name; this->resume_inner(move(client_bev), client_input_crypt, client_output_crypt, detector_crypt); } void ProxyServer::LinkedSession::resume( unique_ptr&& client_bev, shared_ptr client_input_crypt, shared_ptr client_output_crypt, shared_ptr detector_crypt, string&& login_command_bb) { this->login_command_bb = move(login_command_bb); this->resume_inner(move(client_bev), client_input_crypt, client_output_crypt, detector_crypt); } void ProxyServer::LinkedSession::resume(struct bufferevent* client_bev) { unique_ptr bev_unique( client_bev, flush_and_free_bufferevent); this->sub_version = 0; this->character_name.clear(); this->resume_inner(move(bev_unique), nullptr, nullptr, nullptr); } void ProxyServer::LinkedSession::resume_inner( unique_ptr&& client_bev, shared_ptr client_input_crypt, shared_ptr client_output_crypt, shared_ptr detector_crypt) { if (this->client_bev.get()) { throw runtime_error("client connection is already open for this session"); } if (this->next_destination.ss_family != AF_INET) { throw logic_error("attempted to resume an unlicensed linked session without destination set"); } 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->detector_crypt = detector_crypt; this->client_input_crypt = client_input_crypt; this->client_output_crypt = client_output_crypt; this->server_input_crypt.reset(); this->server_output_crypt.reset(); this->saving_files.clear(); this->connect(); } void ProxyServer::LinkedSession::connect() { // 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); this->log(INFO, "Connecting to %s", netloc_str.c_str()); if (bufferevent_socket_connect(this->server_bev.get(), reinterpret_cast(local_sin), sizeof(*local_sin)) != 0) { throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); } 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); // Cancel the session delete timeout event_del(this->timeout_event.get()); } ProxyServer::LinkedSession::SavingFile::SavingFile( const string& basename, const string& output_filename, uint32_t remaining_bytes) : basename(basename), output_filename(output_filename), remaining_bytes(remaining_bytes), f(fopen_unique(this->output_filename, "wb")) { } void ProxyServer::LinkedSession::dispatch_on_client_input( struct bufferevent*, void* ctx) { reinterpret_cast(ctx)->on_client_input(); } void ProxyServer::LinkedSession::dispatch_on_client_error( struct bufferevent*, short events, void* ctx) { reinterpret_cast(ctx)->on_stream_error(events, false); } void ProxyServer::LinkedSession::dispatch_on_server_input( struct bufferevent*, void* ctx) { reinterpret_cast(ctx)->on_server_input(); } void ProxyServer::LinkedSession::dispatch_on_server_error( struct bufferevent*, short events, void* ctx) { reinterpret_cast(ctx)->on_stream_error(events, true); } void ProxyServer::LinkedSession::dispatch_on_timeout( evutil_socket_t, short, void* ctx) { reinterpret_cast(ctx)->on_timeout(); } void ProxyServer::LinkedSession::on_timeout() { this->log(INFO, "Session timed out"); this->server->delete_session(this->id); } void ProxyServer::LinkedSession::on_stream_error( short events, bool is_server_stream) { if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); this->log(WARNING, "Error %d (%s) in %s stream", err, evutil_socket_error_to_string(err), is_server_stream ? "server" : "client"); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { this->log(INFO, "%s has disconnected", is_server_stream ? "Server" : "Client"); this->disconnect(); } } void ProxyServer::LinkedSession::disconnect() { // Forward the disconnection to the other end this->server_bev.reset(); this->client_bev.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(); // Set a timeout to delete the session entirely (in case the client doesn't // reconnect) struct timeval tv = usecs_to_timeval(SESSION_TIMEOUT_USECS); event_add(this->timeout_event.get(), &tv); } void ProxyServer::LinkedSession::on_client_input() { try { for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(), [&](uint16_t command, uint32_t flag, string& data) { print_received_command(command, flag, data.data(), data.size(), this->version, this->client_name.c_str()); process_proxy_command( this->server->state, *this, false, // from_server command, flag, data); }); } catch (const exception& e) { this->log(ERROR, "Failed to process command from client: %s", e.what()); this->disconnect(); } } void ProxyServer::LinkedSession::on_server_input() { 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, this->server_name.c_str(), TerminalFormat::FG_RED); size_t bytes_to_save = min(data.size(), sizeof(this->prev_server_command_bytes)); memcpy(this->prev_server_command_bytes, data.data(), bytes_to_save); process_proxy_command( this->server->state, *this, true, // from_server command, flag, data); }); } catch (const exception& e) { this->log(ERROR, "Failed to process command from server: %s", e.what()); this->disconnect(); } } void ProxyServer::LinkedSession::send_to_end( bool to_server, uint16_t command, uint32_t flag, const void* data, size_t size) { if (size & 3) { throw runtime_error("command size is not a multiple of 4"); } string name = string_printf("LinkedSession:%08" PRIX64 ":synthetic:%s", this->id, to_server ? "server" : "client"); auto* bev = to_server ? this->server_bev.get() : this->client_bev.get(); if (!bev) { throw runtime_error("session endpoint is not connected"); } send_command( bev, this->version, to_server ? this->server_output_crypt.get() : this->client_output_crypt.get(), command, flag, data, size, name.c_str()); } void ProxyServer::LinkedSession::send_to_end( bool to_server, uint16_t command, uint32_t flag, const string& data) { this->send_to_end(to_server, command, flag, data.data(), data.size()); } void ProxyServer::LinkedSession::send_to_end_with_header( bool to_server, const void* data, size_t size) { size_t header_size = PSOCommandHeader::header_size(this->version); if (size < header_size) { throw runtime_error("command is too small for header"); } const auto* header = reinterpret_cast(data); this->send_to_end( to_server, header->command(this->version), header->flag(this->version), reinterpret_cast(data) + header_size, size - header_size); } void ProxyServer::LinkedSession::send_to_end_with_header( bool to_server, const string& data) { this->send_to_end_with_header(to_server, data.data(), data.size()); } shared_ptr ProxyServer::get_session() { if (this->id_to_session.empty()) { throw runtime_error("no sessions exist"); } if (this->id_to_session.size() > 1) { throw runtime_error("multiple sessions exist"); } return this->id_to_session.begin()->second; } shared_ptr ProxyServer::create_licensed_session( shared_ptr l, uint16_t local_port, GameVersion version, const ClientConfigBB& newserv_client_config) { shared_ptr session(new LinkedSession( this, local_port, version, l, newserv_client_config)); auto emplace_ret = this->id_to_session.emplace(session->id, session); if (!emplace_ret.second) { throw runtime_error("session already exists for this license"); } session->log(INFO, "Opening licensed session"); return emplace_ret.first->second; } void ProxyServer::delete_session(uint64_t id) { if (this->id_to_session.erase(id)) { this->log(INFO, "Closed LinkedSession:%08" PRIX64, id); } }