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
+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;
}