add --cat-client behavior

This commit is contained in:
Martin Michelsen
2022-07-20 23:05:47 -07:00
parent 5f836711c7
commit 4163f2affa
11 changed files with 499 additions and 252 deletions
+130
View File
@@ -0,0 +1,130 @@
#include "CatSession.hh"
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <algorithm>
#include <iostream>
#include <phosg/Encoding.hh>
#include <phosg/Filesystem.hh>
#include <phosg/Network.hh>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#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<struct event_base> 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<const sockaddr*>(&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<CatSession*>(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<S_ServerInit_DC_PC_GC_02_17_91_9B>(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<CatSession*>(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());
}
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <event2/event.h>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <vector>
#include <string>
#include <memory>
#include <phosg/Filesystem.hh>
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "ServerState.hh"
#include "Shell.hh"
class CatSession : public Shell {
public:
CatSession(
std::shared_ptr<struct event_base> 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);
};
+306 -220
View File
@@ -10,6 +10,7 @@
#include <set>
#include <unordered_map>
#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<PSOEncryption> 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<PSOBBEncryption::KeyFile>(
"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<ServerState> state(new ServerState());
shared_ptr<struct event_base> 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<DNSServer> 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> shell;
shared_ptr<ReplaySession> replay_session;
shared_ptr<IPStackSimulator> 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<PSOEncryption> 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<PSOBBEncryption::KeyFile>(
"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<struct event_base> 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<ServerState> state(new ServerState());
shared_ptr<struct event_base> 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<DNSServer> 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> shell;
shared_ptr<ReplaySession> replay_session;
shared_ptr<IPStackSimulator> 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;
}
+5 -10
View File
@@ -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<struct sockaddr_in*>(&local_ss);
memset(local_sin, 0, sizeof(*local_sin));
local_sin->sin_family = AF_INET;
const struct sockaddr_in* dest_sin = reinterpret_cast<const sockaddr_in*>(&this->next_destination);
const struct sockaddr_in* dest_sin = reinterpret_cast<const sockaddr_in*>(
&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<const sockaddr*>(local_sin), sizeof(*local_sin)) != 0) {
reinterpret_cast<const sockaddr*>(dest_sin), sizeof(*dest_sin)) != 0) {
throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
}
+7 -9
View File
@@ -53,25 +53,23 @@ void send_command(shared_ptr<ServerState> s, uint16_t command, uint32_t flag,
}
template <typename HeaderT>
void send_command_with_header_t(shared_ptr<Client> 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<const HeaderT*>(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<Client> 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<PSOCommandHeaderDCGC>(c, data, size);
send_command_with_header_t<PSOCommandHeaderDCGC>(ch, data, size);
break;
case GameVersion::PC:
case GameVersion::PATCH:
send_command_with_header_t<PSOCommandHeaderPC>(c, data, size);
send_command_with_header_t<PSOCommandHeaderPC>(ch, data, size);
break;
case GameVersion::BB:
send_command_with_header_t<PSOCommandHeaderBB>(c, data, size);
send_command_with_header_t<PSOCommandHeaderBB>(ch, data, size);
break;
default:
throw logic_error("unimplemented game version in send_command_with_header");
+1 -5
View File
@@ -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<Client> 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<TargetT> c, uint16_t command,
send_command(c, command, flag, all_data.data(), all_data.size());
}
void send_command_with_header(std::shared_ptr<Client> c, const void* data,
size_t size);
void send_command_with_header(Channel& c, const void* data, size_t size);
+2 -2
View File
@@ -17,7 +17,7 @@ using namespace std;
ServerShell::ServerShell(
shared_ptr<struct event_base> base,
shared_ptr<ServerState> 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")) {
+2
View File
@@ -24,6 +24,8 @@ public:
ServerShell& operator=(ServerShell&&) = delete;
protected:
std::shared_ptr<ServerState> state;
std::shared_ptr<ProxyServer::LinkedSession> get_proxy_session();
virtual void print_prompt();
+3 -3
View File
@@ -18,15 +18,15 @@ Shell::exit_shell::exit_shell() : runtime_error("shell exited") { }
Shell::Shell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state) : base(base), state(state),
Shell::Shell(std::shared_ptr<struct event_base> 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};
+1 -3
View File
@@ -13,8 +13,7 @@
class Shell {
public:
Shell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state);
Shell(std::shared_ptr<struct event_base> base);
virtual ~Shell() = default;
Shell(const Shell&) = delete;
Shell(Shell&&) = delete;
@@ -25,7 +24,6 @@ public:
protected:
std::shared_ptr<struct event_base> base;
std::shared_ptr<ServerState> state;
std::unique_ptr<struct event, void (*)(struct event*)> read_event;
std::unique_ptr<struct event, void (*)(struct event*)> prompt_event;
Poll poll;