diff --git a/CMakeLists.txt b/CMakeLists.txt index 00209d70..1bcb968d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ endif() # Executable definition add_executable(newserv + src/CatSession.cc src/Channel.cc src/ChatCommands.cc src/Client.cc diff --git a/src/CatSession.cc b/src/CatSession.cc new file mode 100644 index 00000000..eef5181d --- /dev/null +++ b/src/CatSession.cc @@ -0,0 +1,130 @@ +#include "CatSession.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Loggers.hh" +#include "PSOProtocol.hh" +#include "SendCommands.hh" +#include "ReceiveCommands.hh" +#include "ReceiveSubcommands.hh" +#include "ProxyCommands.hh" + +using namespace std; + + + +CatSession::CatSession( + shared_ptr base, + const struct sockaddr_storage& remote, + GameVersion version) + : Shell(base), + log("[CatSession] ", proxy_server_log.min_level), + channel( + version, + CatSession::dispatch_on_channel_input, + CatSession::dispatch_on_channel_error, + this, + "CatSession") { + if (remote.ss_family != AF_INET) { + throw runtime_error("remote is not AF_INET"); + } + + string netloc_str = render_sockaddr_storage(remote); + this->log.info("Connecting to %s", netloc_str.c_str()); + + struct bufferevent* bev = bufferevent_socket_new( + this->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + if (!bev) { + throw runtime_error(string_printf("failed to open socket (%d)", EVUTIL_SOCKET_ERROR())); + } + this->channel.set_bufferevent(bev); + + if (bufferevent_socket_connect(this->channel.bev.get(), + reinterpret_cast(&remote), sizeof(struct sockaddr_in)) != 0) { + throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); + } +} + +void CatSession::dispatch_on_channel_input( + Channel& ch, uint16_t command, uint32_t flag, std::string& data) { + auto* session = reinterpret_cast(ch.context_obj); + session->on_channel_input(command, flag, data); +} + +void CatSession::on_channel_input( + uint16_t command, uint32_t flag, std::string& data) { + if (this->channel.version != GameVersion::BB) { + if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { + const auto& cmd = check_size_t(data, + offsetof(S_ServerInit_DC_PC_GC_02_17_91_9B, after_message), 0xFFFF); + if (this->channel.version == GameVersion::GC) { + this->channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key)); + this->channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key)); + this->log.info("Enabled GC encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", + cmd.server_key.load(), cmd.client_key.load()); + } else { // PC, DC, or patch server + this->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); + this->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); + this->log.info("Enabled PC encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", + cmd.server_key.load(), cmd.client_key.load()); + } + } + } else { // BB + // TODO: This can easily be done; I'm just lazy. We need to have the user + // pass in a key name, then get that key file from this->state, then create + // the crypts. + // TODO: We really should just move encryption handling into the Channel + // abstraction instead of the above, but this is a bit harder because then + // Channels would have to know about BB key files. + throw runtime_error("CatSession does not implement BB encryption yet"); + } + + string full_cmd = prepend_command_header( + this->channel.version, this->channel.crypt_in.get(), command, flag, data); + print_data(stdout, full_cmd); +} + +void CatSession::dispatch_on_channel_error(Channel& ch, short events) { + auto* session = reinterpret_cast(ch.context_obj); + session->on_channel_error(events); +} + +void CatSession::on_channel_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.info("Session endpoint has disconnected"); + this->channel.disconnect(); + event_base_loopexit(this->base.get(), nullptr); + } +} + +void CatSession::print_prompt() { } + +void CatSession::execute_command(const std::string& command) { + string full_cmd = parse_data_string(command); + send_command_with_header(this->channel, full_cmd.data(), full_cmd.size()); +} diff --git a/src/CatSession.hh b/src/CatSession.hh new file mode 100644 index 00000000..e69df6b0 --- /dev/null +++ b/src/CatSession.hh @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PSOEncryption.hh" +#include "PSOProtocol.hh" +#include "ServerState.hh" +#include "Shell.hh" + + + +class CatSession : public Shell { +public: + CatSession( + std::shared_ptr base, + const struct sockaddr_storage& remote, + GameVersion version); + virtual ~CatSession() = default; + +protected: + PrefixedLogger log; + Channel channel; + + virtual void print_prompt(); + virtual void execute_command(const std::string& command); + + static void dispatch_on_channel_input( + Channel& ch, uint16_t command, uint32_t flag, std::string& msg); + static void dispatch_on_channel_error(Channel& ch, short events); + void on_channel_input(uint16_t command, uint32_t flag, std::string& msg); + void on_channel_error(short events); +}; diff --git a/src/Main.cc b/src/Main.cc index 1381f835..2170b8cc 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -10,6 +10,7 @@ #include #include +#include "CatSession.hh" #include "DNSServer.hh" #include "IPStackSimulator.hh" #include "Loggers.hh" @@ -207,6 +208,59 @@ void drop_privileges(const string& username) { +void print_usage() { + fputs("\ +newserv - a Phantasy Star Online Swiss Army knife\n\ +\n\ +Usage:\n\ + newserv [options]\n\ +\n\ +With no options, newserv runs in server mode. PSO clients can connect normally,\n\ +join lobbies, play games, and use the proxy server. See README.md and\n\ +system/config.json for more information.\n\ +\n\ +When options are given, newserv will do things other than running the server.\n\ +Specifically:\n\ + --decrypt-data\n\ + --encrypt-data\n\ + If either of these options is given, newserv will read from stdin and\n\ + write the encrypted or decrypted result to stdout. By default, PSO GC\n\ + encryption is used, but this can be overridden with --pc or --bb. The\n\ + --seed option specifies the ecryption seed (4 hex bytes for PC or GC, or\n\ + 48 hex bytes for BB). For BB, the --key option is requires as well, and\n\ + should refer to a .nsk file in system/blueburst/keys (without the\n\ + directory or .nsk extension).\n\ + --decode-sjis\n\ + If this option is given, newserv applies its text decoding algorithm to\n\ + the data on stding, producing little-endian UTF-16 data on stdout.\n\ + --decode-gci=FILENAME\n\ + --decode-dlq=FILENAME\n\ + --decode-qst=FILENAME\n\ + If any of these options are given, newserv will decode the given quest\n\ + file into a compressed, unencrypted .bin or .dat file (or in the case of)\n\ + --decode-qst, both a .bin and a .dat file).\n\ + --cat-client=ADDR:PORT\n\ + If this option is given, newserv will behave as if it's a PSO client, and\n\ + will connect to the given server. It will then print all the received\n\ + commands to stdout, and forward any commands typed into stdin to the\n\ + remote server. It is assumed that the input and output are terminals, so\n\ + all commands are hex-encoded. The --patch, --dc, --pc, --gc, and --bb\n\ + options can be used to select the command format end encryption.\n\ + --replay-log=FILENAME\n\ + This option makes newserv replay terminal log as if it were a client\n\ + session. This is used for regression testing, to make sure client\n\ + sessions are repeatable and code changes don\'t affect existing (working)\n\ + functionality.\n\ +\n\ +A few options apply to multiple modes described above:\n\ + --parse-data\n\ + For modes that take input on stdin, parse the input as a hex string\n\ + before encrypting/decoding/etc.\n\ + --config=FILENAME\n\ + Use this file instead of system/config.json.\n\ +", stderr); +} + enum class Behavior { RUN_SERVER = 0, DECRYPT_DATA, @@ -214,12 +268,7 @@ enum class Behavior { DECODE_QUEST_FILE, DECODE_SJIS, REPLAY_LOG, -}; - -enum class EncryptionType { - PC = 0, - GC, - BB, + CAT_CLIENT, }; enum class QuestFileFormat { @@ -230,7 +279,7 @@ enum class QuestFileFormat { int main(int argc, char** argv) { Behavior behavior = Behavior::RUN_SERVER; - EncryptionType crypt_type = EncryptionType::PC; + GameVersion cli_version = GameVersion::GC; QuestFileFormat quest_file_type = QuestFileFormat::GCI; string quest_filename; string seed; @@ -238,8 +287,12 @@ int main(int argc, char** argv) { const char* config_filename = "system/config.json"; bool parse_data = false; const char* replay_log_filename = nullptr; + struct sockaddr_storage cat_client_remote; for (int x = 1; x < argc; x++) { - if (!strcmp(argv[x], "--decrypt-data")) { + if (!strcmp(argv[x], "--help")) { + print_usage(); + return 0; + } else if (!strcmp(argv[x], "--decrypt-data")) { behavior = Behavior::DECRYPT_DATA; } else if (!strcmp(argv[x], "--encrypt-data")) { behavior = Behavior::ENCRYPT_DATA; @@ -257,12 +310,19 @@ int main(int argc, char** argv) { behavior = Behavior::DECODE_QUEST_FILE; quest_file_type = QuestFileFormat::QST; quest_filename = &argv[x][13]; + } else if (!strncmp(argv[x], "--cat-client=", 13)) { + behavior = Behavior::CAT_CLIENT; + cat_client_remote = make_sockaddr_storage(parse_netloc(&argv[x][13])).first; + } else if (!strcmp(argv[x], "--patch")) { + cli_version = GameVersion::PATCH; + } else if (!strcmp(argv[x], "--dc")) { + cli_version = GameVersion::DC; } else if (!strcmp(argv[x], "--pc")) { - crypt_type = EncryptionType::PC; + cli_version = GameVersion::PC; } else if (!strcmp(argv[x], "--gc")) { - crypt_type = EncryptionType::GC; + cli_version = GameVersion::GC; } else if (!strcmp(argv[x], "--bb")) { - crypt_type = EncryptionType::BB; + cli_version = GameVersion::BB; } else if (!strncmp(argv[x], "--seed=", 7)) { seed = &argv[x][7]; } else if (!strncmp(argv[x], "--key=", 6)) { @@ -279,224 +339,250 @@ int main(int argc, char** argv) { } } - if (behavior == Behavior::DECRYPT_DATA || behavior == Behavior::ENCRYPT_DATA) { - shared_ptr crypt; - if (crypt_type == EncryptionType::PC) { - crypt.reset(new PSOPCEncryption(stoul(seed, nullptr, 16))); - } else if (crypt_type == EncryptionType::GC) { - crypt.reset(new PSOGCEncryption(stoul(seed, nullptr, 16))); - } else if (crypt_type == EncryptionType::BB) { - seed = parse_data_string(seed); - auto key = load_object_file( - "system/blueburst/keys/" + key_file_name + ".nsk"); - crypt.reset(new PSOBBEncryption(key, seed.data(), seed.size())); - } else { - throw logic_error("invalid encryption type"); - } - - string data = read_all(stdin); - if (parse_data) { - data = parse_data_string(data); - } - - if (behavior == Behavior::DECRYPT_DATA) { - crypt->decrypt(data.data(), data.size()); - } else if (behavior == Behavior::ENCRYPT_DATA) { - crypt->encrypt(data.data(), data.size()); - } else { - throw logic_error("invalid behavior"); - } - - if (isatty(fileno(stdout))) { - print_data(stdout, data); - } else { - fwritex(stdout, data); - } - fflush(stdout); - - return 0; - - } else if (behavior == Behavior::DECODE_QUEST_FILE) { - if (quest_file_type == QuestFileFormat::GCI) { - save_file(quest_filename + ".dec", Quest::decode_gci(quest_filename)); - } else if (quest_file_type == QuestFileFormat::DLQ) { - save_file(quest_filename + ".dec", Quest::decode_dlq(quest_filename)); - } else if (quest_file_type == QuestFileFormat::QST) { - auto data = Quest::decode_qst(quest_filename); - save_file(quest_filename + ".bin", data.first); - save_file(quest_filename + ".dat", data.second); - } else { - throw logic_error("invalid quest file format"); - } - - return 0; - - } else if (behavior == Behavior::DECODE_SJIS) { - string data = read_all(stdin); - if (parse_data) { - data = parse_data_string(data); - } - auto decoded = decode_sjis(data); - print_data(stderr, decoded.data(), decoded.size() * sizeof(decoded[0])); - return 0; - } - - signal(SIGPIPE, SIG_IGN); - - if (isatty(fileno(stderr))) { - use_terminal_colors = true; - } - - shared_ptr state(new ServerState()); - - shared_ptr base(event_base_new(), event_base_free); - - config_log.info("Reading network addresses"); - state->all_addresses = get_local_addresses(); - for (const auto& it : state->all_addresses) { - string addr_str = string_for_address(it.second); - config_log.info("Found interface: %s = %s", it.first.c_str(), addr_str.c_str()); - } - - config_log.info("Loading configuration"); - auto config_json = JSONObject::parse(load_file(config_filename)); - populate_state_from_config(state, config_json); - - config_log.info("Loading license list"); - state->license_manager.reset(new LicenseManager("system/licenses.nsi")); - - config_log.info("Loading battle parameters"); - state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); - - config_log.info("Loading level table"); - state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); - - config_log.info("Collecting Episode 3 data"); - state->ep3_data_index.reset(new Ep3DataIndex("system/ep3")); - - config_log.info("Collecting quest metadata"); - state->quest_index.reset(new QuestIndex("system/quests")); - - if (!replay_log_filename) { - config_log.info("Compiling client functions"); - state->function_code_index.reset(new FunctionCodeIndex("system/ppc")); - config_log.info("Loading DOL files"); - state->dol_file_index.reset(new DOLFileIndex("system/dol")); - } else { - state->function_code_index.reset(new FunctionCodeIndex()); - state->dol_file_index.reset(new DOLFileIndex()); - } - - config_log.info("Creating menus"); - state->create_menus(config_json); - - if (replay_log_filename) { - state->allow_saving = false; - state->license_manager->set_autosave(false); - config_log.info("Saving disabled because this is a replay session"); - } - - shared_ptr dns_server; - if (state->dns_server_port && !replay_log_filename) { - config_log.info("Starting DNS server"); - dns_server.reset(new DNSServer(base, state->local_address, - state->external_address)); - dns_server->listen("", state->dns_server_port); - } else { - config_log.info("DNS server is disabled"); - } - - shared_ptr shell; - shared_ptr replay_session; - shared_ptr ip_stack_simulator; - if (behavior == Behavior::REPLAY_LOG) { - config_log.info("Starting proxy server"); - state->proxy_server.reset(new ProxyServer(base, state)); - config_log.info("Starting game server"); - state->game_server.reset(new Server(base, state)); - - auto f = fopen_unique(replay_log_filename, "rt"); - replay_session.reset(new ReplaySession(base, f.get(), state)); - replay_session->start(); - - } else if (behavior == Behavior::RUN_SERVER) { - config_log.info("Opening sockets"); - for (const auto& it : state->name_to_port_config) { - const auto& pc = it.second; - if (pc->behavior == ServerBehavior::PROXY_SERVER) { - if (!state->proxy_server.get()) { - config_log.info("Starting proxy server"); - state->proxy_server.reset(new ProxyServer(base, state)); + switch (behavior) { + case Behavior::DECRYPT_DATA: + case Behavior::ENCRYPT_DATA: { + shared_ptr crypt; + switch (cli_version) { + case GameVersion::PATCH: + case GameVersion::DC: + case GameVersion::PC: + crypt.reset(new PSOPCEncryption(stoul(seed, nullptr, 16))); + break; + case GameVersion::GC: + crypt.reset(new PSOGCEncryption(stoul(seed, nullptr, 16))); + break; + case GameVersion::BB: { + seed = parse_data_string(seed); + auto key = load_object_file( + "system/blueburst/keys/" + key_file_name + ".nsk"); + crypt.reset(new PSOBBEncryption(key, seed.data(), seed.size())); + break; } - if (state->proxy_server.get()) { - // For PC and GC, proxy sessions are dynamically created when a client - // picks a destination from the menu. For patch and BB clients, there's - // no way to ask the client which destination they want, so only one - // destination is supported, and we have to manually specify the - // destination netloc here. - if (pc->version == GameVersion::PATCH) { - struct sockaddr_storage ss = make_sockaddr_storage( - state->proxy_destination_patch.first, - state->proxy_destination_patch.second).first; - state->proxy_server->listen(pc->port, pc->version, &ss); - } else if (pc->version == GameVersion::BB) { - struct sockaddr_storage ss = make_sockaddr_storage( - state->proxy_destination_bb.first, - state->proxy_destination_bb.second).first; - state->proxy_server->listen(pc->port, pc->version, &ss); + default: + throw logic_error("invalid game version"); + } + + string data = read_all(stdin); + if (parse_data) { + data = parse_data_string(data); + } + + if (behavior == Behavior::DECRYPT_DATA) { + crypt->decrypt(data.data(), data.size()); + } else if (behavior == Behavior::ENCRYPT_DATA) { + crypt->encrypt(data.data(), data.size()); + } else { + throw logic_error("invalid behavior"); + } + + if (isatty(fileno(stdout))) { + print_data(stdout, data); + } else { + fwritex(stdout, data); + } + fflush(stdout); + + break; + } + + case Behavior::DECODE_QUEST_FILE: + if (quest_file_type == QuestFileFormat::GCI) { + save_file(quest_filename + ".dec", Quest::decode_gci(quest_filename)); + } else if (quest_file_type == QuestFileFormat::DLQ) { + save_file(quest_filename + ".dec", Quest::decode_dlq(quest_filename)); + } else if (quest_file_type == QuestFileFormat::QST) { + auto data = Quest::decode_qst(quest_filename); + save_file(quest_filename + ".bin", data.first); + save_file(quest_filename + ".dat", data.second); + } else { + throw logic_error("invalid quest file format"); + } + + break; + + case Behavior::DECODE_SJIS: { + string data = read_all(stdin); + if (parse_data) { + data = parse_data_string(data); + } + auto decoded = decode_sjis(data); + print_data(stderr, decoded.data(), decoded.size() * sizeof(decoded[0])); + break; + } + + case Behavior::CAT_CLIENT: { + shared_ptr base(event_base_new(), event_base_free); + CatSession session(base, cat_client_remote, cli_version); + event_base_dispatch(base.get()); + break; + } + + case Behavior::REPLAY_LOG: + case Behavior::RUN_SERVER: { + signal(SIGPIPE, SIG_IGN); + + if (isatty(fileno(stderr))) { + use_terminal_colors = true; + } + + shared_ptr state(new ServerState()); + + shared_ptr base(event_base_new(), event_base_free); + + config_log.info("Reading network addresses"); + state->all_addresses = get_local_addresses(); + for (const auto& it : state->all_addresses) { + string addr_str = string_for_address(it.second); + config_log.info("Found interface: %s = %s", it.first.c_str(), addr_str.c_str()); + } + + config_log.info("Loading configuration"); + auto config_json = JSONObject::parse(load_file(config_filename)); + populate_state_from_config(state, config_json); + + config_log.info("Loading license list"); + state->license_manager.reset(new LicenseManager("system/licenses.nsi")); + + config_log.info("Loading battle parameters"); + state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry")); + + config_log.info("Loading level table"); + state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + + config_log.info("Collecting Episode 3 data"); + state->ep3_data_index.reset(new Ep3DataIndex("system/ep3")); + + config_log.info("Collecting quest metadata"); + state->quest_index.reset(new QuestIndex("system/quests")); + + if (!replay_log_filename) { + config_log.info("Compiling client functions"); + state->function_code_index.reset(new FunctionCodeIndex("system/ppc")); + config_log.info("Loading DOL files"); + state->dol_file_index.reset(new DOLFileIndex("system/dol")); + } else { + state->function_code_index.reset(new FunctionCodeIndex()); + state->dol_file_index.reset(new DOLFileIndex()); + } + + config_log.info("Creating menus"); + state->create_menus(config_json); + + if (replay_log_filename) { + state->allow_saving = false; + state->license_manager->set_autosave(false); + config_log.info("Saving disabled because this is a replay session"); + } + + shared_ptr dns_server; + if (state->dns_server_port && !replay_log_filename) { + config_log.info("Starting DNS server"); + dns_server.reset(new DNSServer(base, state->local_address, + state->external_address)); + dns_server->listen("", state->dns_server_port); + } else { + config_log.info("DNS server is disabled"); + } + + shared_ptr shell; + shared_ptr replay_session; + shared_ptr ip_stack_simulator; + if (behavior == Behavior::REPLAY_LOG) { + config_log.info("Starting proxy server"); + state->proxy_server.reset(new ProxyServer(base, state)); + config_log.info("Starting game server"); + state->game_server.reset(new Server(base, state)); + + auto f = fopen_unique(replay_log_filename, "rt"); + replay_session.reset(new ReplaySession(base, f.get(), state)); + replay_session->start(); + + } else if (behavior == Behavior::RUN_SERVER) { + config_log.info("Opening sockets"); + for (const auto& it : state->name_to_port_config) { + const auto& pc = it.second; + if (pc->behavior == ServerBehavior::PROXY_SERVER) { + if (!state->proxy_server.get()) { + config_log.info("Starting proxy server"); + state->proxy_server.reset(new ProxyServer(base, state)); + } + if (state->proxy_server.get()) { + // For PC and GC, proxy sessions are dynamically created when a client + // picks a destination from the menu. For patch and BB clients, there's + // no way to ask the client which destination they want, so only one + // destination is supported, and we have to manually specify the + // destination netloc here. + if (pc->version == GameVersion::PATCH) { + struct sockaddr_storage ss = make_sockaddr_storage( + state->proxy_destination_patch.first, + state->proxy_destination_patch.second).first; + state->proxy_server->listen(pc->port, pc->version, &ss); + } else if (pc->version == GameVersion::BB) { + struct sockaddr_storage ss = make_sockaddr_storage( + state->proxy_destination_bb.first, + state->proxy_destination_bb.second).first; + state->proxy_server->listen(pc->port, pc->version, &ss); + } else { + state->proxy_server->listen(pc->port, pc->version); + } + } } else { - state->proxy_server->listen(pc->port, pc->version); + if (!state->game_server.get()) { + config_log.info("Starting game server"); + state->game_server.reset(new Server(base, state)); + } + string spec = string_printf("T-%hu-%s-%s-%s", + pc->port, name_for_version(pc->version), pc->name.c_str(), + name_for_server_behavior(pc->behavior)); + state->game_server->listen(spec, "", pc->port, pc->version, pc->behavior); } } - } else { - if (!state->game_server.get()) { - config_log.info("Starting game server"); - state->game_server.reset(new Server(base, state)); + + if (!state->ip_stack_addresses.empty()) { + config_log.info("Starting IP stack simulator"); + ip_stack_simulator.reset(new IPStackSimulator(base, state)); + for (const auto& it : state->ip_stack_addresses) { + auto netloc = parse_netloc(it); + ip_stack_simulator->listen(netloc.first, netloc.second); + } } - string spec = string_printf("T-%hu-%s-%s-%s", - pc->port, name_for_version(pc->version), pc->name.c_str(), - name_for_server_behavior(pc->behavior)); - state->game_server->listen(spec, "", pc->port, pc->version, pc->behavior); + + } else { + throw logic_error("invalid behavior"); } + + if (!state->username.empty()) { + config_log.info("Switching to user %s", state->username.c_str()); + drop_privileges(state->username); + } + + bool should_run_shell; + if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) { + should_run_shell = isatty(fileno(stdin)); + } else if (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS) { + should_run_shell = true; + } else { + should_run_shell = false; + } + if (should_run_shell) { + should_run_shell = !replay_session.get(); + } + if (should_run_shell) { + shell.reset(new ServerShell(base, state)); + } + + config_log.info("Ready"); + event_base_dispatch(base.get()); + + config_log.info("Normal shutdown"); + state->proxy_server.reset(); // Break reference cycle + break; } - if (!state->ip_stack_addresses.empty()) { - config_log.info("Starting IP stack simulator"); - ip_stack_simulator.reset(new IPStackSimulator(base, state)); - for (const auto& it : state->ip_stack_addresses) { - auto netloc = parse_netloc(it); - ip_stack_simulator->listen(netloc.first, netloc.second); - } - } - - } else { - throw logic_error("invalid behavior"); + default: + throw logic_error("invalid behavior"); } - if (!state->username.empty()) { - config_log.info("Switching to user %s", state->username.c_str()); - drop_privileges(state->username); - } - - bool should_run_shell; - if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) { - should_run_shell = isatty(fileno(stdin)); - } else if (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS) { - should_run_shell = true; - } else { - should_run_shell = false; - } - if (should_run_shell) { - should_run_shell = !replay_session.get(); - } - if (should_run_shell) { - shell.reset(new ServerShell(base, state)); - } - - config_log.info("Ready"); - event_base_dispatch(base.get()); - - config_log.info("Normal shutdown"); - state->proxy_server.reset(); // Break reference cycle return 0; } diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 3c99845e..6c6e53cc 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -538,24 +538,19 @@ void ProxyServer::LinkedSession::resume_inner( void ProxyServer::LinkedSession::connect() { // Connect to the remote server. The command handlers will do the login steps // and set up forwarding - 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); + 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"); + throw runtime_error("destination is 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); + string netloc_str = render_sockaddr_storage(this->next_destination); this->log.info("Connecting to %s", netloc_str.c_str()); this->server_channel.set_bufferevent(bufferevent_socket_new( this->server->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); if (bufferevent_socket_connect(this->server_channel.bev.get(), - reinterpret_cast(local_sin), sizeof(*local_sin)) != 0) { + reinterpret_cast(dest_sin), sizeof(*dest_sin)) != 0) { throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index e8544454..2f8ced46 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -53,25 +53,23 @@ void send_command(shared_ptr s, uint16_t command, uint32_t flag, } template -void send_command_with_header_t(shared_ptr c, const void* data, - size_t size) { +void send_command_with_header_t(Channel& ch, const void* data, size_t size) { const HeaderT* header = reinterpret_cast(data); - send_command(c, header->command, header->flag, header + 1, size - sizeof(HeaderT)); + ch.send(header->command, header->flag, header + 1, size - sizeof(HeaderT)); } -void send_command_with_header(shared_ptr c, const void* data, - size_t size) { - switch (c->version) { +void send_command_with_header(Channel& ch, const void* data, size_t size) { + switch (ch.version) { case GameVersion::GC: case GameVersion::DC: - send_command_with_header_t(c, data, size); + send_command_with_header_t(ch, data, size); break; case GameVersion::PC: case GameVersion::PATCH: - send_command_with_header_t(c, data, size); + send_command_with_header_t(ch, data, size); break; case GameVersion::BB: - send_command_with_header_t(c, data, size); + send_command_with_header_t(ch, data, size); break; default: throw logic_error("unimplemented game version in send_command_with_header"); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 1498ab49..e8acb555 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -29,9 +29,6 @@ // pointer is given but size is accidentally not given (e.g. if the type of // data in the calling function is changed from string to void*). -void send_command(Channel& ch, uint16_t command, uint32_t flag, - const void* data, size_t size); - void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, const void* data, size_t size); @@ -91,8 +88,7 @@ void send_command_t_vt(std::shared_ptr c, uint16_t command, send_command(c, command, flag, all_data.data(), all_data.size()); } -void send_command_with_header(std::shared_ptr c, const void* data, - size_t size); +void send_command_with_header(Channel& c, const void* data, size_t size); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index c43e6dc0..4416b1c2 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -17,7 +17,7 @@ using namespace std; ServerShell::ServerShell( shared_ptr base, shared_ptr state) - : Shell(base, state) { } + : Shell(base), state(state) { } void ServerShell::print_prompt() { fwritex(stdout, Shell::PROMPT); @@ -305,7 +305,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } auto c = this->state->game_server->get_client(); - send_command_with_header(c, data.data(), data.size()); + send_command_with_header(c->channel, data.data(), data.size()); } } else if ((command_name == "chat") || (command_name == "dchat")) { diff --git a/src/ServerShell.hh b/src/ServerShell.hh index e179d4c3..51ec05da 100644 --- a/src/ServerShell.hh +++ b/src/ServerShell.hh @@ -24,6 +24,8 @@ public: ServerShell& operator=(ServerShell&&) = delete; protected: + std::shared_ptr state; + std::shared_ptr get_proxy_session(); virtual void print_prompt(); diff --git a/src/Shell.cc b/src/Shell.cc index 3b51efa1..8f9d78e1 100644 --- a/src/Shell.cc +++ b/src/Shell.cc @@ -18,15 +18,15 @@ Shell::exit_shell::exit_shell() : runtime_error("shell exited") { } -Shell::Shell(std::shared_ptr base, - std::shared_ptr state) : base(base), state(state), +Shell::Shell(std::shared_ptr base) + : base(base), read_event(event_new(this->base.get(), 0, EV_READ | EV_PERSIST, &Shell::dispatch_read_stdin, this), event_free), prompt_event(event_new(this->base.get(), 0, EV_TIMEOUT, &Shell::dispatch_print_prompt, this), event_free) { event_add(this->read_event.get(), nullptr); - // schedule an event to print the prompt as soon as the event loop starts + // Schedule an event to print the prompt as soon as the event loop starts // running. we do this so the prompt appears after any initialization // messages that come after starting the shell struct timeval tv = {0, 0}; diff --git a/src/Shell.hh b/src/Shell.hh index 177f68ce..a21e9f6a 100644 --- a/src/Shell.hh +++ b/src/Shell.hh @@ -13,8 +13,7 @@ class Shell { public: - Shell(std::shared_ptr base, - std::shared_ptr state); + Shell(std::shared_ptr base); virtual ~Shell() = default; Shell(const Shell&) = delete; Shell(Shell&&) = delete; @@ -25,7 +24,6 @@ public: protected: std::shared_ptr base; - std::shared_ptr state; std::unique_ptr read_event; std::unique_ptr prompt_event; Poll poll;