add session replay functions

This commit is contained in:
Martin Michelsen
2022-07-01 00:26:48 -07:00
parent 38b0539124
commit a7e3d4853a
25 changed files with 686 additions and 125 deletions
+4 -12
View File
@@ -213,12 +213,8 @@ Channel::Message Channel::recv(bool print_contents) {
print_color_escape(stderr, this->terminal_recv_color, TerminalFormat::BOLD, TerminalFormat::END);
}
string name_token;
if (!this->name.empty()) {
name_token = " from " + this->name;
}
command_data_log.info("Received%s (version=%s command=%04hX flag=%08X)",
name_token.c_str(),
command_data_log.info("Received from %s (version=%s command=%04hX flag=%08X)",
this->name.c_str(),
name_for_version(this->version),
header.command(this->version),
header.flag(this->version));
@@ -320,15 +316,11 @@ void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size,
}
if (print_contents && (this->terminal_send_color != TerminalFormat::END)) {
string name_token;
if (!this->name.empty()) {
name_token = " to " + this->name;
}
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END);
}
command_data_log.info("Sending%s (version=%s command=%04hX flag=%08X)",
name_token.c_str(), name_for_version(version), cmd, flag);
command_data_log.info("Sending to %s (version=%s command=%04hX flag=%08X)",
this->name.c_str(), name_for_version(version), cmd, flag);
print_data(stderr, send_data.data(), logical_size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
+3 -1
View File
@@ -38,14 +38,16 @@ struct Channel {
on_error_t on_error;
void* context_obj;
// Creates an unconnected channel
Channel(
GameVersion version,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const std::string& name = "",
const std::string& name,
TerminalFormat terminal_send_color = TerminalFormat::END,
TerminalFormat terminal_recv_color = TerminalFormat::END);
// Creates a connected channel
Channel(
struct bufferevent* bev,
GameVersion version,
+6 -2
View File
@@ -6,6 +6,7 @@
#include <string.h>
#include <unistd.h>
#include <atomic>
#include <phosg/Network.hh>
#include <phosg/Time.hh>
@@ -18,17 +19,20 @@ using namespace std;
const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839;
static atomic<uint64_t> next_id(1);
Client::Client(
struct bufferevent* bev,
GameVersion version,
ServerBehavior server_behavior)
: log("", client_log.min_level),
: id(next_id++),
log("", client_log.min_level),
version(version),
bb_game_state(0),
flags(flags_for_version(this->version, 0)),
channel(bev, this->version, nullptr, nullptr, this, "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
channel(bev, this->version, nullptr, nullptr, this, string_printf("C-%" PRIX64, this->id), TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
server_behavior(server_behavior),
should_disconnect(false),
should_send_to_lobby_server(false),
+1
View File
@@ -57,6 +57,7 @@ struct Client {
DEFAULT_V4_BB = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | NO_MESSAGE_BOX_CLOSE_CONFIRMATION | SAVE_ENABLED,
};
uint64_t id;
PrefixedLogger log;
// License & account
+2 -2
View File
@@ -269,7 +269,7 @@ struct SC_TextHeader_01_06_11_B0_EE {
// The copyright field in the below structure must contain the following text:
// "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999"
struct S_ServerInit_DC_PC_GC_02_17_92_9B {
struct S_ServerInit_DC_PC_GC_02_17_91_9B {
ptext<char, 0x40> copyright;
le_uint32_t server_key; // Key for data sent by server
le_uint32_t client_key; // Key for data sent by client
@@ -305,7 +305,7 @@ struct C_LegacyLogin_PC_GC_03 {
// The copyright field in the below structure must contain the following text:
// "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM."
struct S_ServerInit_BB_03 {
struct S_ServerInit_BB_03_9B {
ptext<char, 0x60> copyright;
parray<uint8_t, 0x30> server_key;
parray<uint8_t, 0x30> client_key;
+2 -1
View File
@@ -776,7 +776,8 @@ void IPStackSimulator::open_server_connection(
}
} else if (this->state->game_server.get()) {
this->state->game_server->connect_client(bevs[1], c->ipv4_addr,
conn.client_port, port_config->version, port_config->behavior);
conn.client_port, conn.server_port, port_config->version,
port_config->behavior);
ip_stack_simulator_log.info("Connected TCP connection %s to game server",
conn_str.c_str());
} else {
+3 -1
View File
@@ -9,7 +9,7 @@ using namespace std;
PrefixedLogger ax_messages_log ("[$ax message] " , LogLevel::USE_DEFAULT);
PrefixedLogger channel_exceptions_log("[Channel] " , LogLevel::USE_DEFAULT);
PrefixedLogger client_log ("" , LogLevel::USE_DEFAULT);
PrefixedLogger command_data_log ("" , LogLevel::USE_DEFAULT);
PrefixedLogger command_data_log ("[Commands] " , LogLevel::USE_DEFAULT);
PrefixedLogger config_log ("[Config] " , LogLevel::USE_DEFAULT);
PrefixedLogger dns_server_log ("[DNSServer] " , LogLevel::USE_DEFAULT);
PrefixedLogger function_compiler_log ("[FunctionCompiler] ", LogLevel::USE_DEFAULT);
@@ -18,6 +18,7 @@ PrefixedLogger license_log ("[LicenseManager] " , LogLevel::USE_DEFAU
PrefixedLogger lobby_log ("" , LogLevel::USE_DEFAULT);
PrefixedLogger player_data_log ("" , LogLevel::USE_DEFAULT);
PrefixedLogger proxy_server_log ("[ProxyServer] " , LogLevel::USE_DEFAULT);
PrefixedLogger replay_log ("[ReplaySession] " , LogLevel::USE_DEFAULT);
PrefixedLogger server_log ("[Server] " , LogLevel::USE_DEFAULT);
PrefixedLogger static_game_data_log ("[StaticGameData] " , LogLevel::USE_DEFAULT);
@@ -52,6 +53,7 @@ void set_log_levels_from_json(shared_ptr<JSONObject> json) {
set_log_level_from_json(lobby_log , json, "Lobbies");
set_log_level_from_json(player_data_log , json, "PlayerData");
set_log_level_from_json(proxy_server_log , json, "ProxyServer");
set_log_level_from_json(replay_log , json, "Replay");
set_log_level_from_json(server_log , json, "GameServer");
set_log_level_from_json(static_game_data_log , json, "StaticGameData");
}
+1
View File
@@ -17,6 +17,7 @@ extern PrefixedLogger license_log;
extern PrefixedLogger lobby_log;
extern PrefixedLogger player_data_log;
extern PrefixedLogger proxy_server_log;
extern PrefixedLogger replay_log;
extern PrefixedLogger server_log;
extern PrefixedLogger static_game_data_log;
+87 -59
View File
@@ -1,26 +1,27 @@
#include <signal.h>
#include <pwd.h>
#include <event2/event.h>
#include <pwd.h>
#include <signal.h>
#include <string.h>
#include <unordered_map>
#include <phosg/Filesystem.hh>
#include <phosg/JSON.hh>
#include <phosg/Network.hh>
#include <phosg/Strings.hh>
#include <phosg/Filesystem.hh>
#include <set>
#include <unordered_map>
#include "DNSServer.hh"
#include "FileContentsCache.hh"
#include "IPStackSimulator.hh"
#include "Loggers.hh"
#include "NetworkAddresses.hh"
#include "SendCommands.hh"
#include "DNSServer.hh"
#include "ProxyServer.hh"
#include "ServerState.hh"
#include "ReplaySession.hh"
#include "SendCommands.hh"
#include "Server.hh"
#include "FileContentsCache.hh"
#include "Text.hh"
#include "ServerShell.hh"
#include "IPStackSimulator.hh"
#include "ServerState.hh"
#include "Text.hh"
using namespace std;
@@ -198,6 +199,7 @@ enum class Behavior {
ENCRYPT_DATA,
DECODE_QUEST_FILE,
DECODE_SJIS,
REPLAY_LOG,
};
enum class EncryptionType {
@@ -220,6 +222,7 @@ int main(int argc, char** argv) {
string seed;
string key_file_name;
bool parse_data = false;
const char* replay_log_filename = nullptr;
for (int x = 1; x < argc; x++) {
if (!strcmp(argv[x], "--decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
@@ -251,6 +254,9 @@ int main(int argc, char** argv) {
key_file_name = &argv[x][6];
} else if (!strcmp(argv[x], "--parse-data")) {
parse_data = true;
} else if (!strncmp(argv[x], "--replay-log=", 13)) {
behavior = Behavior::REPLAY_LOG;
replay_log_filename = &argv[x][13];
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
}
@@ -373,54 +379,71 @@ int main(int argc, char** argv) {
config_log.info("DNS server is disabled");
}
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 {
if (!state->game_server.get()) {
config_log.info("Starting game server");
state->game_server.reset(new Server(base, state));
}
string name = string_printf("%s (%s, %s) on port %hu",
pc->name.c_str(), name_for_version(pc->version),
name_for_server_behavior(pc->behavior), pc->port);
state->game_server->listen(name, "", pc->port, pc->version, pc->behavior);
}
}
shared_ptr<Shell> shell;
shared_ptr<ReplaySession> replay_session;
shared_ptr<IPStackSimulator> ip_stack_simulator;
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);
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 {
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);
}
}
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");
}
if (!state->username.empty()) {
@@ -428,12 +451,17 @@ int main(int argc, char** argv) {
drop_privileges(state->username);
}
bool should_run_shell = (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS);
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();
}
shared_ptr<Shell> shell;
if (should_run_shell) {
shell.reset(new ServerShell(base, state));
}
+55
View File
@@ -146,3 +146,58 @@ void check_size_v(size_t size, size_t min_size, size_t max_size) {
max_size, size));
}
}
std::string prepend_command_header(
GameVersion version,
bool encryption_enabled,
uint16_t cmd,
uint32_t flag,
const std::string& data) {
StringWriter ret;
switch (version) {
case GameVersion::GC:
case GameVersion::DC: {
PSOCommandHeaderDCGC header;
if (encryption_enabled) {
header.size = (sizeof(header) + data.size() + 3) & ~3;
} else {
header.size = (sizeof(header) + data.size());
}
header.command = cmd;
header.flag = flag;
ret.put(header);
break;
}
case GameVersion::PC:
case GameVersion::PATCH: {
PSOCommandHeaderPC header;
if (encryption_enabled) {
header.size = (sizeof(header) + data.size() + 3) & ~3;
} else {
header.size = (sizeof(header) + data.size());
}
header.command = cmd;
header.flag = flag;
ret.put(header);
break;
}
case GameVersion::BB: {
PSOCommandHeaderBB header;
if (encryption_enabled) {
header.size = (sizeof(header) + data.size() + 7) & ~7;
} else {
header.size = (sizeof(header) + data.size());
}
header.command = cmd;
header.flag = flag;
ret.put(header);
break;
}
default:
throw logic_error("unimplemented game version in prepend_command_header");
}
ret.write(data);
return move(ret.str());
}
+7
View File
@@ -90,3 +90,10 @@ T& check_size_t(
}
void check_size_v(size_t size, size_t min_size, size_t max_size = 0);
std::string prepend_command_header(
GameVersion version,
bool encryption_enabled,
uint16_t cmd,
uint32_t flag,
const std::string& data);
+4 -4
View File
@@ -155,8 +155,8 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
// Most servers don't include after_message or have a shorter
// after_message than newserv does, so don't require it
const auto& cmd = check_size_t<S_ServerInit_DC_PC_GC_02_17_92_9B>(data,
offsetof(S_ServerInit_DC_PC_GC_02_17_92_9B, after_message), 0xFFFF);
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 (!session.license) {
session.log.info("No license in linked session");
@@ -250,8 +250,8 @@ static HandlerResult process_server_bb_03(shared_ptr<ServerState> s,
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
// Most servers don't include after_message or have a shorter after_message
// than newserv does, so don't require it
const auto& cmd = check_size_t<S_ServerInit_BB_03>(data,
offsetof(S_ServerInit_BB_03, after_message), 0xFFFF);
const auto& cmd = check_size_t<S_ServerInit_BB_03_9B>(data,
offsetof(S_ServerInit_BB_03_9B, after_message), 0xFFFF);
// If the session has a detector crypt, then it was resumed from an unlinked
// session, during which we already sent an 03 command.
+3 -1
View File
@@ -1388,7 +1388,9 @@ void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
auto player = c->game_data.player(false);
if (player) {
c->channel.name = remove_language_marker(encode_sjis(player->disp.name));
string name_str = remove_language_marker(encode_sjis(player->disp.name));
c->channel.name = string_printf("C-%" PRIX64 " (%s)",
c->id, name_str.c_str());
}
// 98 should only be sent when leaving a game, and we should leave the client
+358
View File
@@ -0,0 +1,358 @@
#include "ReplaySession.hh"
#include <phosg/Filesystem.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#include "Loggers.hh"
#include "Shell.hh"
#include "Server.hh"
using namespace std;
ReplaySession::Event::Event(Type type, uint64_t client_id)
: type(type), client_id(client_id), complete(false) { }
ReplaySession::Client::Client(
ReplaySession* session, uint64_t id, uint16_t port, GameVersion version)
: id(id),
port(port),
version(version),
channel(
this->version,
&ReplaySession::dispatch_on_command_received,
&ReplaySession::dispatch_on_error,
session,
string_printf("R-%" PRIX64, this->id)) { }
shared_ptr<ReplaySession::Event> ReplaySession::create_event(
Event::Type type, shared_ptr<Client> c) {
shared_ptr<Event> event(new Event(type, c->id));
if (!this->last_event.get()) {
this->first_event = event;
} else {
this->last_event->next_event = event;
}
this->last_event = event;
if (type == Event::Type::RECEIVE) {
c->receive_events.emplace_back(event);
}
return event;
}
ReplaySession::ReplaySession(
shared_ptr<struct event_base> base,
FILE* input_log,
shared_ptr<ServerState> state)
: state(state),
base(base),
commands_sent(0),
bytes_sent(0),
commands_received(0),
bytes_received(0) {
shared_ptr<Event> parsing_command = nullptr;
while (!feof(input_log)) {
string line = fgets(input_log);
if (starts_with(line, Shell::PROMPT)) {
line = line.substr(Shell::PROMPT.size());
}
if (ends_with(line, "\n")) {
line.resize(line.size() - 1);
}
if (line.empty()) {
continue;
}
if (parsing_command.get()) {
string expected_start = string_printf("%016zX |", parsing_command->data.size());
if (starts_with(line, expected_start)) {
// Parse out the hex part of the hex/ASCII dump
string mask_bytes;
string data_bytes = parse_data_string(
line.substr(expected_start.size(), 16 * 3 + 1), &mask_bytes);
parsing_command->data += data_bytes;
parsing_command->mask += mask_bytes;
continue;
} else {
parsing_command = nullptr;
}
}
if (starts_with(line, "I ")) {
// I <pid/ts> - [Server] Client connected: C-%X on fd %d via %d (T-%hu-%s-%s-%s)
// I <pid/ts> - [Server] Client connected: C-%X on virtual connection %p via T-%hu-VI
size_t offset = line.find(" - [Server] Client connected: C-");
if (offset != string::npos) {
auto tokens = split(line, ' ');
if (tokens.size() != 15) {
throw runtime_error("client connection message has incorrect token count");
}
if (!starts_with(tokens[8], "C-")) {
throw runtime_error("client connection message missing client ID token");
}
auto listen_tokens = split(tokens[14], '-');
if (listen_tokens.size() < 4) {
throw runtime_error("client connection message listening socket token format is incorrect");
}
shared_ptr<Client> c(new Client(
this,
stoull(tokens[8].substr(2), nullptr, 16),
stoul(listen_tokens[1], nullptr, 10),
version_for_name(listen_tokens[2].c_str())));
if (!this->clients.emplace(c->id, c).second) {
throw runtime_error("duplicate client ID in input log");
}
this->create_event(Event::Type::CONNECT, c);
continue;
}
// I <pid/ts> - [Server] Disconnecting C-%X on fd %d
offset = line.find(" - [Server] Client disconnected: C-");
if (offset != string::npos) {
auto tokens = split(line, ' ');
if (tokens.size() < 9) {
throw runtime_error("client disconnection message has incorrect token count");
}
if (!starts_with(tokens[8], "C-")) {
throw runtime_error("client disconnection message missing client ID token");
}
uint64_t client_id = stoul(tokens[8].substr(2), nullptr, 16);
try {
auto& c = this->clients.at(client_id);
if (c->disconnect_event.get()) {
throw runtime_error("client has multiple disconnect events");
}
c->disconnect_event = this->create_event(Event::Type::DISCONNECT, c);
} catch (const out_of_range&) {
throw runtime_error("unknown disconnecting client ID in input log");
}
continue;
}
// I <pid/ts> - [Commands] Sending to C-%X (...)
// I <pid/ts> - [Commands] Received from C-%X (...)
offset = line.find(" - [Commands] Sending to C-");
if (offset == string::npos) {
offset = line.find(" - [Commands] Received from C-");
}
if (offset != string::npos) {
auto tokens = split(line, ' ');
if (tokens.size() < 10) {
throw runtime_error("command header line too short");
}
bool from_client = (tokens[6] == "Received");
uint64_t client_id = stoull(tokens[8].substr(2), nullptr, 16);
try {
parsing_command = this->create_event(
from_client ? Event::Type::SEND : Event::Type::RECEIVE,
this->clients.at(client_id));
} catch (const out_of_range&) {
throw runtime_error("input log contains command for missing client");
}
continue;
}
}
}
}
void ReplaySession::start() {
this->update_timeout_event();
this->execute_pending_events();
}
void ReplaySession::update_timeout_event() {
if (!this->timeout_ev.get()) {
this->timeout_ev.reset(
event_new(this->base.get(), -1, EV_TIMEOUT, this->dispatch_on_timeout, this),
event_free);
}
struct timeval tv = usecs_to_timeval(3000000);
event_add(this->timeout_ev.get(), &tv);
}
void ReplaySession::dispatch_on_timeout(evutil_socket_t, short, void*) {
throw runtime_error("timeout waiting for next event");
}
void ReplaySession::execute_pending_events() {
while (this->first_event) {
if (!this->first_event->complete) {
auto& c = this->clients.at(this->first_event->client_id);
switch (this->first_event->type) {
case Event::Type::CONNECT: {
if (c->channel.connected()) {
throw runtime_error("connect event on already-connected client");
}
struct bufferevent* bevs[2];
bufferevent_pair_new(this->base.get(), 0, bevs);
c->channel.set_bufferevent(bevs[0]);
this->channel_to_client.emplace(&c->channel, c);
shared_ptr<const PortConfiguration> port_config;
try {
port_config = this->state->number_to_port_config.at(c->port);
} catch (const out_of_range&) {
bufferevent_free(bevs[1]);
throw runtime_error("client connected to port missing from configuration");
}
if (port_config->behavior == ServerBehavior::PROXY_SERVER) {
// TODO: We should support this at some point in the future
throw runtime_error("client connected to proxy server");
} else if (this->state->game_server.get()) {
this->state->game_server->connect_client(bevs[1], 0x20202020,
1025, c->port, port_config->version, port_config->behavior);
} else {
throw runtime_error("no server available for connection");
bufferevent_free(bevs[1]);
}
break;
}
case Event::Type::DISCONNECT:
this->channel_to_client.erase(&c->channel);
c->channel.disconnect();
break;
case Event::Type::SEND:
if (!c->channel.connected()) {
throw runtime_error("send event attempted on unconnected client");
}
c->channel.send(this->first_event->data);
this->commands_sent++;
this->bytes_sent += this->first_event->data.size();
break;
case Event::Type::RECEIVE:
// Receive events cannot be executed here, since we have to wait for
// an incoming command. The existing handlers will take care of it:
// on_command_received will be called sometime (hopefully) soon.
return;
default:
throw logic_error("unhandled event type");
}
this->first_event->complete = true;
}
this->first_event = this->first_event->next_event;
if (!this->first_event.get()) {
this->last_event = nullptr;
}
}
// If we get here, then there are no more events to run: we're done.
// TODO: We should flush any pending sends on the remaining client here, even
// though there are no pending receives (to make sure the last sent commands
// don't crash newserv)
replay_log.info("Replay complete: %zu commands sent (%zu bytes), %zu commands received (%zu bytes)",
this->commands_sent, this->bytes_sent, this->commands_received, this->bytes_received);
event_base_loopexit(this->base.get(), nullptr);
}
void ReplaySession::dispatch_on_command_received(
Channel& ch, uint16_t command, uint32_t flag, string& data) {
ReplaySession* session = reinterpret_cast<ReplaySession*>(ch.context_obj);
session->on_command_received(
session->channel_to_client.at(&ch), command, flag, data);
}
void ReplaySession::dispatch_on_error(Channel& ch, short events) {
ReplaySession* session = reinterpret_cast<ReplaySession*>(ch.context_obj);
session->on_error(session->channel_to_client.at(&ch), events);
}
void ReplaySession::on_command_received(
shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) {
string full_command = prepend_command_header(
c->version, c->channel.crypt_in.get(), command, flag, data);
this->commands_received++;
this->bytes_received += full_command.size();
if (c->receive_events.empty()) {
print_data(stderr, full_command);
throw runtime_error("received unexpected command for client");
}
auto& ev = c->receive_events.front();
if (full_command.size() != ev->data.size()) {
replay_log.error("Expected command:");
print_data(stderr, ev->data);
replay_log.error("Received command:");
print_data(stderr, full_command);
throw runtime_error("received command sizes do not match");
}
for (size_t x = 0; x < full_command.size(); x++) {
if ((full_command[x] & ev->mask[x]) != (ev->data[x] & ev->mask[x])) {
replay_log.error("Expected command:");
print_data(stderr, ev->data);
replay_log.error("Received command:");
print_data(stderr, full_command, 0, ev->data.data());
throw runtime_error("received command data does not match expected data");
}
}
ev->complete = true;
c->receive_events.pop_front();
// If the command is an encryption init, set up encryption on the channel
switch (c->version) {
case GameVersion::DC:
throw runtime_error("DC encryption is not supported during replays");
case GameVersion::PC:
case GameVersion::GC:
if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) {
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 (c->version == GameVersion::GC) {
c->channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key));
c->channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key));
} else {
c->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key));
c->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key));
}
}
break;
case GameVersion::BB:
if (command == 0x03 || command == 0x9B) {
auto& cmd = check_size_t<S_ServerInit_BB_03_9B>(data,
sizeof(S_ServerInit_BB_03_9B), 0xFFFF);
// TODO: At some point it may matter which BB private key file we use.
// Don't just blindly use the first one here.
c->channel.crypt_in.reset(new PSOBBEncryption(
*this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size()));
c->channel.crypt_out.reset(new PSOBBEncryption(
*this->state->bb_private_keys[0], cmd.client_key.data(), cmd.client_key.size()));
}
break;
default:
throw logic_error("unsupported encryption version");
}
this->update_timeout_event();
this->execute_pending_events();
}
void ReplaySession::on_error(shared_ptr<Client> c, short events) {
if (events & BEV_EVENT_ERROR) {
throw runtime_error(string_printf("C-%" PRIX64 " caused stream error", c->id));
}
if (events & BEV_EVENT_EOF) {
if (!c->disconnect_event.get()) {
throw runtime_error(string_printf(
"C-%" PRIX64 " disconnected, but has no disconnect event", c->id));
}
if (!c->receive_events.empty()) {
throw runtime_error(string_printf(
"C-%" PRIX64 " disconnected, but has pending receive events", c->id));
}
c->disconnect_event->complete = true;
this->channel_to_client.erase(&c->channel);
c->channel.disconnect();
}
}
+90
View File
@@ -0,0 +1,90 @@
#pragma once
#include <event2/event.h>
#include <stdint.h>
#include <stdio.h>
#include <deque>
#include <memory>
#include <string>
#include "Channel.hh"
#include "Version.hh"
#include "ServerState.hh"
class ReplaySession {
public:
ReplaySession(
std::shared_ptr<struct event_base> base,
FILE* input_log,
std::shared_ptr<ServerState> state);
ReplaySession(const ReplaySession&) = delete;
ReplaySession(ReplaySession&&) = delete;
ReplaySession& operator=(const ReplaySession&) = delete;
ReplaySession& operator=(ReplaySession&&) = delete;
~ReplaySession() = default;
void start();
private:
struct Event {
enum class Type {
CONNECT = 0,
DISCONNECT,
SEND,
RECEIVE,
};
Type type;
uint64_t client_id;
std::string data; // Only used for SEND and RECEIVE
std::string mask; // Only used for RECEIVE
bool complete;
std::shared_ptr<Event> next_event;
Event(Type type, uint64_t client_id);
};
struct Client {
uint64_t id;
uint16_t port;
GameVersion version;
Channel channel;
std::deque<std::shared_ptr<Event>> receive_events;
std::shared_ptr<Event> disconnect_event;
Client(ReplaySession* session, uint64_t id, uint16_t port, GameVersion version);
};
std::shared_ptr<ServerState> state;
std::unordered_map<uint64_t, std::shared_ptr<Client>> clients;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
std::shared_ptr<Event> first_event;
std::shared_ptr<Event> last_event;
std::shared_ptr<struct event_base> base;
std::shared_ptr<struct event> timeout_ev;
size_t commands_sent;
size_t bytes_sent;
size_t commands_received;
size_t bytes_received;
std::shared_ptr<ReplaySession::Event> create_event(
Event::Type type, std::shared_ptr<Client> c);
void update_timeout_event();
static void dispatch_on_timeout(evutil_socket_t fd, short events, void* ctx);
static void dispatch_on_command_received(
Channel& ch, uint16_t command, uint32_t flag, std::string& data);
static void dispatch_on_error(Channel& ch, short events);
void on_command_received(
std::shared_ptr<Client> c, uint16_t command, uint32_t flag, std::string& data);
void on_error(std::shared_ptr<Client> c, short events);
void execute_pending_events();
};
+4 -4
View File
@@ -100,11 +100,11 @@ static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst G
static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.";
static const char* patch_server_copyright = "Patch Server. Copyright SonicTeam, LTD. 2001";
S_ServerInit_DC_PC_GC_02_17_92_9B prepare_server_init_contents_dc_pc_gc(
S_ServerInit_DC_PC_GC_02_17_91_9B prepare_server_init_contents_dc_pc_gc(
bool initial_connection,
uint32_t server_key,
uint32_t client_key) {
S_ServerInit_DC_PC_GC_02_17_92_9B cmd;
S_ServerInit_DC_PC_GC_02_17_91_9B cmd;
cmd.copyright = initial_connection
? dc_port_map_copyright : dc_lobby_server_copyright;
cmd.server_key = server_key;
@@ -138,11 +138,11 @@ void send_server_init_dc_pc_gc(shared_ptr<Client> c,
}
}
S_ServerInit_BB_03 prepare_server_init_contents_bb(
S_ServerInit_BB_03_9B prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key,
bool use_secondary_message) {
S_ServerInit_BB_03 cmd;
S_ServerInit_BB_03_9B cmd;
cmd.copyright = use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright;
cmd.server_key = server_key;
cmd.client_key = client_key;
+2 -2
View File
@@ -96,11 +96,11 @@ void send_command_with_header(std::shared_ptr<Client> c, const void* data,
S_ServerInit_DC_PC_GC_02_17_92_9B prepare_server_init_contents_dc_pc_gc(
S_ServerInit_DC_PC_GC_02_17_91_9B prepare_server_init_contents_dc_pc_gc(
bool initial_connection,
uint32_t server_key,
uint32_t client_key);
S_ServerInit_BB_03 prepare_server_init_contents_bb(
S_ServerInit_BB_03_9B prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key,
bool use_secondary_message);
+34 -29
View File
@@ -31,11 +31,11 @@ using namespace std::placeholders;
void Server::disconnect_client(shared_ptr<Client> c) {
if (c->channel.is_virtual_connection) {
server_log.info("Disconnecting client on virtual connection %p",
c->channel.bev.get());
server_log.info("Client disconnected: C-%" PRIX64 " on virtual connection %p",
c->id, c->channel.bev.get());
} else {
server_log.info("Disconnecting client on fd %d",
bufferevent_getfd(c->channel.bev.get()));
server_log.info("Client disconnected: C-%" PRIX64 " on fd %d",
c->id, bufferevent_getfd(c->channel.bev.get()));
}
this->channel_to_client.erase(&c->channel);
@@ -76,9 +76,6 @@ void Server::on_listen_accept(struct evconnlistener* listener,
return;
}
server_log.info("Client fd %d connected via fd %d (%s)",
fd, listen_fd, listening_socket->name.c_str());
struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd,
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
shared_ptr<Client> c(new Client(
@@ -88,6 +85,9 @@ void Server::on_listen_accept(struct evconnlistener* listener,
c->channel.context_obj = this;
this->channel_to_client.emplace(&c->channel, c);
server_log.info("Client connected: C-%" PRIX64 " on fd %d via %d (%s)",
c->id, fd, listen_fd, listening_socket->addr_str.c_str());
try {
process_connect(this->state, c);
} catch (const exception& e) {
@@ -97,23 +97,28 @@ void Server::on_listen_accept(struct evconnlistener* listener,
}
void Server::connect_client(
struct bufferevent* bev, uint32_t address, uint16_t port,
GameVersion version, ServerBehavior initial_state) {
server_log.info("Client connected on virtual connection %p", bev);
struct bufferevent* bev, uint32_t address, uint16_t client_port,
uint16_t server_port, GameVersion version, ServerBehavior initial_state) {
shared_ptr<Client> c(new Client(bev, version, initial_state));
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
server_log.info("Client connected: C-%" PRIX64 " on virtual connection %p via T-%hu-%s-%s-VI",
c->id,
bev,
server_port,
name_for_version(version),
name_for_server_behavior(initial_state));
this->channel_to_client.emplace(&c->channel, c);
// Manually set the remote address, since the bufferevent has no fd and the
// Channel constructor can't figure out the virtual remote address
auto* sin = reinterpret_cast<sockaddr_in*>(&c->channel.remote_addr);
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = htonl(address);
sin->sin_port = htons(port);
auto* remote_sin = reinterpret_cast<sockaddr_in*>(&c->channel.remote_addr);
remote_sin->sin_family = AF_INET;
remote_sin->sin_addr.s_addr = htonl(address);
remote_sin->sin_port = htons(client_port);
try {
process_connect(this->state, c);
@@ -169,36 +174,36 @@ Server::Server(
: base(base), state(state) { }
void Server::listen(
const std::string& name,
const std::string& addr_str,
const string& socket_path,
GameVersion version,
ServerBehavior behavior) {
int fd = ::listen(socket_path, 0, SOMAXCONN);
server_log.info("Listening on Unix socket %s (%s) on fd %d (name: %s)",
socket_path.c_str(), name_for_version(version), fd, name.c_str());
this->add_socket(name, fd, version, behavior);
server_log.info("Listening on Unix socket %s on fd %d as %s",
socket_path.c_str(), fd, addr_str.c_str());
this->add_socket(addr_str, fd, version, behavior);
}
void Server::listen(
const std::string& name,
const std::string& addr_str,
const string& addr,
int port,
GameVersion version,
ServerBehavior behavior) {
int fd = ::listen(addr, port, SOMAXCONN);
string netloc_str = render_netloc(addr, port);
server_log.info("Listening on TCP interface %s (%s) on fd %d (name: %s)",
netloc_str.c_str(), name_for_version(version), fd, name.c_str());
this->add_socket(name, fd, version, behavior);
server_log.info("Listening on TCP interface %s on fd %d as %s",
netloc_str.c_str(), fd, addr_str.c_str());
this->add_socket(addr_str, fd, version, behavior);
}
void Server::listen(const std::string& name, int port, GameVersion version, ServerBehavior behavior) {
this->listen(name, "", port, version, behavior);
void Server::listen(const std::string& addr_str, int port, GameVersion version, ServerBehavior behavior) {
this->listen(addr_str, "", port, version, behavior);
}
Server::ListeningSocket::ListeningSocket(Server* s, const std::string& name,
Server::ListeningSocket::ListeningSocket(Server* s, const std::string& addr_str,
int fd, GameVersion version, ServerBehavior behavior) :
name(name), fd(fd), version(version), behavior(behavior), listener(
addr_str(addr_str), fd(fd), version(version), behavior(behavior), listener(
evconnlistener_new(s->base.get(), Server::dispatch_on_listen_accept, s,
LEV_OPT_REUSEABLE, 0, this->fd), evconnlistener_free) {
evconnlistener_set_error_cb(this->listener.get(),
@@ -206,12 +211,12 @@ Server::ListeningSocket::ListeningSocket(Server* s, const std::string& name,
}
void Server::add_socket(
const std::string& name,
const std::string& addr_str,
int fd,
GameVersion version,
ServerBehavior behavior) {
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd),
forward_as_tuple(this, name, fd, version, behavior));
forward_as_tuple(this, addr_str, fd, version, behavior));
}
shared_ptr<Client> Server::get_client() const {
+7 -6
View File
@@ -21,12 +21,13 @@ public:
std::shared_ptr<ServerState> state);
virtual ~Server() = default;
void listen(const std::string& name, const std::string& socket_path, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& name, const std::string& addr, int port, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& name, int port, GameVersion version, ServerBehavior initial_state);
void add_socket(const std::string& name, int fd, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& addr_str, const std::string& socket_path, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& addr_str, const std::string& addr, int port, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& addr_str, int port, GameVersion version, ServerBehavior initial_state);
void add_socket(const std::string& addr_str, int fd, GameVersion version, ServerBehavior initial_state);
void connect_client(struct bufferevent* bev, uint32_t address, uint16_t port,
void connect_client(struct bufferevent* bev, uint32_t address,
uint16_t client_port, uint16_t server_port,
GameVersion version, ServerBehavior initial_state);
std::shared_ptr<Client> get_client() const;
@@ -35,7 +36,7 @@ private:
std::shared_ptr<struct event_base> base;
struct ListeningSocket {
std::string name;
std::string addr_str;
int fd;
GameVersion version;
ServerBehavior behavior;
+1 -1
View File
@@ -20,7 +20,7 @@ ServerShell::ServerShell(
: Shell(base, state) { }
void ServerShell::print_prompt() {
fwrite("newserv> ", 9, 1, stdout);
fwritex(stdout, Shell::PROMPT);
fflush(stdout);
}
+2
View File
@@ -8,6 +8,8 @@
#include "Shell.hh"
#include "ProxyServer.hh"
#define SHELL_PROMPT "newserv> "
class ServerShell : public Shell {
+4
View File
@@ -10,6 +10,10 @@ using namespace std;
const std::string Shell::PROMPT("newserv> ");
Shell::exit_shell::exit_shell() : runtime_error("shell exited") { }
+2
View File
@@ -21,6 +21,8 @@ public:
Shell& operator=(const Shell&) = delete;
Shell& operator=(Shell&&) = delete;
static const std::string PROMPT;
protected:
std::shared_ptr<struct event_base> base;
std::shared_ptr<ServerState> state;