rewrite proxy server to support multiple clients and integration with game server

This commit is contained in:
Martin Michelsen
2022-03-27 18:14:23 -07:00
parent d977cf0608
commit 2597da95bc
24 changed files with 1370 additions and 992 deletions
-1
View File
@@ -59,7 +59,6 @@ add_executable(newserv
src/ProxyServer.cc
src/Shell.cc
src/ServerShell.cc
src/ProxyShell.cc
src/IPFrameInfo.cc
src/IPStackSimulator.cc
src/Main.cc
+9 -21
View File
@@ -15,7 +15,7 @@ using namespace std;
static const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839;
const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839;
@@ -28,6 +28,8 @@ Client::Client(
bev(bev),
server_behavior(server_behavior),
should_disconnect(false),
proxy_destination_address(0),
proxy_destination_port(0),
play_time_begin(now()),
last_recv_time(this->play_time_begin),
last_send_time(0),
@@ -52,32 +54,16 @@ Client::Client(
memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr));
}
bool Client::send(string&& data) {
if (!this->bev) {
return false;
}
if (this->crypt_out.get()) {
this->crypt_out->encrypt(data.data(), data.size());
}
struct evbuffer* buf = bufferevent_get_output(this->bev);
evbuffer_add(buf, data.data(), data.size());
return true;
}
ClientConfig Client::export_config() const {
ClientConfig cc;
cc.magic = CLIENT_CONFIG_MAGIC;
cc.bb_game_state = this->bb_game_state;
cc.bb_player_index = this->bb_player_index;
cc.flags = this->flags;
for (size_t x = 0; x < 5; x++) {
cc.unused[x] = 0xFFFFFFFF;
}
for (size_t x = 0; x < 2; x++) {
cc.unused_bb_only[x] = 0xFFFFFFFF;
}
cc.proxy_destination_address = this->proxy_destination_address;
cc.proxy_destination_port = this->proxy_destination_port;
memset(cc.unused, 0xFF, sizeof(cc.unused));
memset(cc.unused_bb_only, 0xFF, sizeof(cc.unused_bb_only));
return cc;
}
@@ -88,4 +74,6 @@ void Client::import_config(const ClientConfig& cc) {
this->bb_game_state = cc.bb_game_state;
this->bb_player_index = cc.bb_player_index;
this->flags = cc.flags;
this->proxy_destination_address = cc.proxy_destination_address;
this->proxy_destination_port = cc.proxy_destination_port;
}
+11 -5
View File
@@ -10,12 +10,17 @@
extern const uint64_t CLIENT_CONFIG_MAGIC;
enum class ServerBehavior {
SPLIT_RECONNECT = 0,
LOGIN_SERVER,
LOBBY_SERVER,
DATA_SERVER_BB,
PATCH_SERVER,
PROXY_SERVER,
};
struct ClientConfig {
@@ -23,8 +28,10 @@ struct ClientConfig {
uint8_t bb_game_state;
uint8_t bb_player_index;
uint16_t flags;
uint32_t unused[5];
uint32_t unused_bb_only[2];
uint32_t proxy_destination_address;
uint16_t proxy_destination_port;
uint8_t unused[0x0E];
uint8_t unused_bb_only[0x08];
} __attribute__((packed));
struct Client {
@@ -51,6 +58,8 @@ struct Client {
ServerBehavior server_behavior;
bool is_virtual_connection;
bool should_disconnect;
uint32_t proxy_destination_address;
uint16_t proxy_destination_port;
// timing & menus
uint64_t play_time_begin; // time of connection (used for incrementing play time on BB)
@@ -75,9 +84,6 @@ struct Client {
Client(struct bufferevent* bev, GameVersion version,
ServerBehavior server_behavior);
// adds data to the client's output buffer, encrypting it first
bool send(std::string&& data);
ClientConfig export_config() const;
void import_config(const ClientConfig& cc);
};
+23 -19
View File
@@ -77,13 +77,10 @@ string IPStackSimulator::str_for_tcp_connection(shared_ptr<const IPClient> c,
IPStackSimulator::IPStackSimulator(
std::shared_ptr<struct event_base> base,
std::shared_ptr<Server> game_server,
std::shared_ptr<ProxyServer> proxy_server,
std::shared_ptr<ServerState> state)
: base(base),
game_server(game_server),
proxy_server(proxy_server),
state(state),
proxy_destination_address(0),
pcap_text_log_file(state->ip_stack_debug ? fopen("IPStackSimulator-Log.txt", "wt") : nullptr) {
memset(this->host_mac_address_bytes, 0x90, 6);
memset(this->broadcast_mac_address_bytes, 0xFF, 6);
@@ -421,9 +418,7 @@ void IPStackSimulator::on_client_udp_frame(
// r_udp.size filled in later
// r_udp.checksum filled in later
uint32_t resolved_address = this->proxy_destination_address
? this->proxy_destination_address
: this->connect_address_for_remote_address(c->ipv4_addr);
uint32_t resolved_address = this->connect_address_for_remote_address(c->ipv4_addr);
string r_data = DNSServer::response_for_query(
fi.payload, fi.payload_size, resolved_address);
@@ -758,25 +753,34 @@ void IPStackSimulator::open_server_connection(
// Link the client to the server - the server sees this as a normal TCP
// connection and treats it as if the client connected to one of its listening
// sockets
string conn_str = this->str_for_tcp_connection(c, conn);
if (this->game_server.get()) {
const PortConfiguration* port_config;
try {
port_config = &this->state->numbered_port_configuration.at(conn.server_port);
} catch (const out_of_range&) {
bufferevent_free(bevs[1]);
throw logic_error("client connected to port missing from configuration");
}
const PortConfiguration* port_config;
try {
port_config = &this->state->numbered_port_configuration.at(conn.server_port);
} catch (const out_of_range&) {
bufferevent_free(bevs[1]);
throw logic_error("client connected to port missing from configuration");
}
string conn_str = this->str_for_tcp_connection(c, conn);
if (port_config->behavior == ServerBehavior::PROXY_SERVER) {
if (!this->state->proxy_server.get()) {
log(ERROR, "[IPStackSimulator] TCP connection %s is to non-running proxy server",
conn_str.c_str());
flush_and_free_bufferevent(bevs[1]);
} else {
this->state->proxy_server->connect_client(bevs[1], conn.server_port);
log(INFO, "[IPStackSimulator] Connected TCP connection %s to proxy server",
conn_str.c_str());
}
} else if (this->game_server.get()) {
this->game_server->connect_client(bevs[1], c->ipv4_addr, conn.client_port,
port_config->version, port_config->behavior);
log(INFO, "[IPStackSimulator] Connected TCP connection %s to game server",
conn_str.c_str());
} else if (this->proxy_server.get()) {
this->proxy_server->connect_client(bevs[1], conn.server_addr, conn.server_port);
log(INFO, "[IPStackSimulator] Connected TCP connection %s to proxy server",
} else {
log(ERROR, "[IPStackSimulator] No server available for TCP connection %s",
conn_str.c_str());
flush_and_free_bufferevent(bevs[1]);
}
}
-7
View File
@@ -18,7 +18,6 @@ public:
IPStackSimulator(
std::shared_ptr<struct event_base> base,
std::shared_ptr<Server> game_server,
std::shared_ptr<ProxyServer> proxy_server,
std::shared_ptr<ServerState> state);
~IPStackSimulator();
@@ -27,18 +26,12 @@ public:
void listen(int port);
void add_socket(int fd);
inline void set_proxy_destination_address(uint32_t addr) {
this->proxy_destination_address = addr;
}
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
private:
std::shared_ptr<struct event_base> base;
std::shared_ptr<Server> game_server;
std::shared_ptr<ProxyServer> proxy_server;
std::shared_ptr<ServerState> state;
uint32_t proxy_destination_address;
using unique_listener = std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>;
using unique_bufferevent = std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>;
+25 -6
View File
@@ -53,6 +53,12 @@ LicenseManager::LicenseManager(const string& filename) : filename(filename) {
auto licenses = load_vector_file<License>(this->filename);
for (const auto& read_license : licenses) {
shared_ptr<License> license(new License(read_license));
// Before the temporary flag existed, licenses with root privileges would
// have the temporary flag set. To migrate these, explicitly unset the
// flag for all licenses loaded from the license file.
license->privileges &= ~Privilege::TEMPORARY;
uint32_t serial_number = license->serial_number;
this->bb_username_to_license.emplace(license->username, license);
this->serial_number_to_license.emplace(serial_number, license);
@@ -67,6 +73,9 @@ LicenseManager::LicenseManager(const string& filename) : filename(filename) {
void LicenseManager::save() const {
auto f = fopen_unique(this->filename, "wb");
for (const auto& it : this->serial_number_to_license) {
if (it.second->privileges & Privilege::TEMPORARY) {
continue;
}
fwritex(f.get(), it.second.get(), sizeof(License));
}
}
@@ -154,33 +163,43 @@ vector<License> LicenseManager::snapshot() const {
}
shared_ptr<const License> LicenseManager::create_license_pc(
uint32_t serial_number,const char* access_key, const char* password) {
shared_ptr<License> LicenseManager::create_license_pc(
uint32_t serial_number,const char* access_key, const char* password, bool temporary) {
shared_ptr<License> l(new License());
l->serial_number = serial_number;
strncpy(l->access_key, access_key, 8);
if (password) {
strncpy(l->gc_password, password, 8);
}
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
shared_ptr<const License> LicenseManager::create_license_gc(
uint32_t serial_number, const char* access_key, const char* password) {
shared_ptr<License> LicenseManager::create_license_gc(
uint32_t serial_number, const char* access_key, const char* password, bool temporary) {
shared_ptr<License> l(new License());
l->serial_number = serial_number;
strncpy(l->access_key, access_key, 12);
if (password) {
strncpy(l->gc_password, password, 8);
}
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
shared_ptr<const License> LicenseManager::create_license_bb(
uint32_t serial_number, const char* username, const char* password) {
shared_ptr<License> LicenseManager::create_license_bb(
uint32_t serial_number, const char* username, const char* password, bool temporary) {
shared_ptr<License> l(new License());
l->serial_number = serial_number;
strncpy(l->username, username, 19);
strncpy(l->bb_password, password, 19);
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
+9 -7
View File
@@ -17,7 +17,9 @@ enum Privilege {
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x0000003F,
ROOT = 0xFFFFFFFF,
ROOT = 0x7FFFFFFF,
TEMPORARY = 0x80000000,
};
enum LicenseVerifyAction {
@@ -59,12 +61,12 @@ public:
void remove(uint32_t serial_number);
std::vector<License> snapshot() const;
static std::shared_ptr<const License> create_license_pc(
uint32_t serial_number, const char* access_key, const char* password);
static std::shared_ptr<const License> create_license_gc(
uint32_t serial_number, const char* access_key, const char* password);
static std::shared_ptr<const License> create_license_bb(
uint32_t serial_number, const char* username, const char* password);
static std::shared_ptr<License> create_license_pc(
uint32_t serial_number, const char* access_key, const char* password, bool temporary);
static std::shared_ptr<License> create_license_gc(
uint32_t serial_number, const char* access_key, const char* password, bool temporary);
static std::shared_ptr<License> create_license_bb(
uint32_t serial_number, const char* username, const char* password, bool temporary);
protected:
void save() const;
+67 -65
View File
@@ -19,7 +19,6 @@
#include "FileContentsCache.hh"
#include "Text.hh"
#include "ServerShell.hh"
#include "ProxyShell.hh"
#include "IPStackSimulator.hh"
using namespace std;
@@ -51,6 +50,9 @@ static const unordered_map<string, PortConfiguration> default_port_to_behavior({
{"pc-lobby", {9420, GameVersion::PC, ServerBehavior::LOBBY_SERVER}},
{"gc-lobby", {9421, GameVersion::GC, ServerBehavior::LOBBY_SERVER}},
{"bb-lobby", {9422, GameVersion::BB, ServerBehavior::LOBBY_SERVER}},
{"pc-proxy", {9520, GameVersion::PC, ServerBehavior::PROXY_SERVER}},
{"gc-proxy", {9521, GameVersion::GC, ServerBehavior::PROXY_SERVER}},
{"bb-proxy", {9522, GameVersion::BB, ServerBehavior::PROXY_SERVER}},
});
@@ -100,18 +102,45 @@ void populate_state_from_config(shared_ptr<ServerState> s,
information_menu->emplace_back(INFORMATION_MENU_GO_BACK, u"Go back",
u"Return to the\nmain menu", 0);
uint32_t item_id = 0;
for (const auto& item : d.at("InformationMenuContents")->as_list()) {
auto& v = item->as_list();
information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()),
decode_sjis(v.at(1)->as_string()), MenuItemFlag::REQUIRES_MESSAGE_BOXES);
information_contents->emplace_back(decode_sjis(v.at(2)->as_string()));
item_id++;
{
uint32_t item_id = 0;
for (const auto& item : d.at("InformationMenuContents")->as_list()) {
auto& v = item->as_list();
information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()),
decode_sjis(v.at(1)->as_string()), MenuItemFlag::REQUIRES_MESSAGE_BOXES);
information_contents->emplace_back(decode_sjis(v.at(2)->as_string()));
item_id++;
}
}
s->information_menu = information_menu;
s->information_contents = information_contents;
s->proxy_destinations_menu.emplace_back(PROXY_DESTINATIONS_MENU_GO_BACK,
u"Go back", u"Return to the\nmain menu", 0);
{
uint32_t item_id = 0;
for (const auto& item : d.at("ProxyDestinations")->as_dict()) {
const string& netloc_str = item.second->as_string();
s->proxy_destinations_menu.emplace_back(item_id, decode_sjis(item.first),
decode_sjis(netloc_str), 0);
s->proxy_destinations.emplace_back(parse_netloc(netloc_str));
item_id++;
}
}
s->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby",
u"Join the lobby", 0);
s->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information",
u"View server information", MenuItemFlag::REQUIRES_MESSAGE_BOXES);
if (!s->proxy_destinations.empty()) {
s->main_menu.emplace_back(MAIN_MENU_PROXY_DESTINATIONS, u"Proxy server",
u"Connect to another\nserver", MenuItemFlag::REQUIRES_MESSAGE_BOXES);
}
s->main_menu.emplace_back(MAIN_MENU_DOWNLOAD_QUESTS, u"Download quests",
u"Download quests", 0);
s->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect",
u"Disconnect", 0);
try {
s->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string());
} catch (const out_of_range&) { }
@@ -214,24 +243,7 @@ void drop_privileges(const string& username) {
int main(int argc, char* argv[]) {
string proxy_hostname;
int proxy_port = 0;
GameVersion proxy_version = GameVersion::GC;
for (int x = 1; x < argc; x++) {
if (!strncmp(argv[x], "--proxy-destination=", 20)) {
auto netloc = parse_netloc(&argv[x][20], 9100);
proxy_hostname = netloc.first;
proxy_port = netloc.second;
} else if (!strncmp(argv[x], "--proxy-version=", 16)) {
proxy_version = version_for_name(&argv[x][16]);
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
}
}
int main(int, char**) {
signal(SIGPIPE, SIG_IGN);
shared_ptr<ServerState> state(new ServerState());
@@ -249,6 +261,18 @@ int main(int argc, char* argv[]) {
auto config_json = JSONObject::parse(load_file("system/config.json"));
populate_state_from_config(state, config_json);
log(INFO, "Loading license list");
state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
log(INFO, "Loading battle parameters");
state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry"));
log(INFO, "Loading level table");
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
log(INFO, "Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests"));
shared_ptr<DNSServer> dns_server;
if (state->dns_server_port) {
log(INFO, "Starting DNS server");
@@ -259,50 +283,31 @@ int main(int argc, char* argv[]) {
log(INFO, "DNS server is disabled");
}
shared_ptr<ProxyServer> proxy_server;
shared_ptr<Server> game_server;
uint32_t proxy_destination_address = 0;
if (proxy_port) {
if (!state->proxy_destinations.empty()) {
log(INFO, "Starting proxy server");
sockaddr_storage proxy_destination_ss = make_sockaddr_storage(
proxy_hostname, proxy_port).first;
if (proxy_destination_ss.ss_family != AF_INET) {
throw runtime_error("proxy destination address is not IPv4");
}
proxy_destination_address = ntohl(
reinterpret_cast<struct sockaddr_in*>(&proxy_destination_ss)->sin_addr.s_addr);
proxy_server.reset(new ProxyServer(base, proxy_destination_ss, proxy_version));
proxy_server->listen(proxy_port);
if (proxy_version == GameVersion::BB) {
proxy_server->listen(proxy_port + 1);
}
state->proxy_server.reset(new ProxyServer(base, state));
}
} else {
log(INFO, "Starting game server");
game_server.reset(new Server(base, state));
for (const auto& it : state->named_port_configuration) {
log(INFO, "Starting game server");
game_server.reset(new Server(base, state));
log(INFO, "Opening sockets");
for (const auto& it : state->named_port_configuration) {
if (it.second.behavior == ServerBehavior::PROXY_SERVER) {
if (state->proxy_server.get()) {
state->proxy_server->listen(it.second.port, it.second.version);
}
} else {
game_server->listen("", it.second.port, it.second.version, it.second.behavior);
}
log(INFO, "Loading license list");
state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
log(INFO, "Loading battle parameters");
state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry"));
log(INFO, "Loading level table");
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
log(INFO, "Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests"));
}
shared_ptr<IPStackSimulator> ip_stack_simulator;
if (!state->ip_stack_addresses.empty()) {
log(INFO, "Starting IP stack simulator");
ip_stack_simulator.reset(new IPStackSimulator(
base, game_server, proxy_server, state));
ip_stack_simulator->set_proxy_destination_address(proxy_destination_address);
base, game_server, state));
for (const auto& it : state->ip_stack_addresses) {
auto netloc = parse_netloc(it);
ip_stack_simulator->listen(netloc.first, netloc.second);
@@ -321,16 +326,13 @@ int main(int argc, char* argv[]) {
shared_ptr<Shell> shell;
if (should_run_shell) {
if (proxy_port) {
shell.reset(new ProxyShell(base, state, proxy_server));
} else {
shell.reset(new ServerShell(base, state));
}
shell.reset(new ServerShell(base, state));
}
log(INFO, "Ready");
event_base_dispatch(base.get());
log(INFO, "Normal shutdown");
state->proxy_server.reset(); // Break reference cycle
return 0;
}
+18
View File
@@ -6,6 +6,24 @@
#define MAIN_MENU_ID 0x11000011
#define INFORMATION_MENU_ID 0x22000022
#define LOBBY_MENU_ID 0x33000033
#define GAME_MENU_ID 0x44000044
#define QUEST_MENU_ID 0x55000055
#define QUEST_FILTER_MENU_ID 0x66000066
#define PROXY_DESTINATIONS_MENU_ID 0x77000077
#define MAIN_MENU_GO_TO_LOBBY 0x11AAAA11
#define MAIN_MENU_INFORMATION 0x11BBBB11
#define MAIN_MENU_DOWNLOAD_QUESTS 0x11CCCC11
#define MAIN_MENU_PROXY_DESTINATIONS 0x11DDDD11
#define MAIN_MENU_DISCONNECT 0x11EEEE11
#define INFORMATION_MENU_GO_BACK 0x22FFFF22
#define PROXY_DESTINATIONS_MENU_GO_BACK 0x77FFFF77
enum MenuItemFlag {
INVISIBLE_ON_DC = 0x01,
INVISIBLE_ON_PC = 0x02,
+112 -14
View File
@@ -3,6 +3,9 @@
#include <event2/buffer.h>
#include <stdexcept>
#include <phosg/Strings.hh>
#include "Text.hh"
using namespace std;
@@ -17,43 +20,109 @@ PSOCommandHeader::PSOCommandHeader() {
uint16_t PSOCommandHeader::command(GameVersion version) const {
switch (version) {
case GameVersion::DC:
return this->dc.command;
case GameVersion::GC:
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->command;
return this->gc.command;
case GameVersion::PC:
case GameVersion::PATCH:
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->command;
return this->pc.command;
case GameVersion::BB:
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->command;
return this->bb.command;
default:
throw logic_error("unknown game version");
}
}
void PSOCommandHeader::set_command(GameVersion version, uint16_t command) {
switch (version) {
case GameVersion::DC:
this->dc.command = command;
break;
case GameVersion::GC:
this->gc.command = command;
break;
case GameVersion::PC:
case GameVersion::PATCH:
this->pc.command = command;
break;
case GameVersion::BB:
this->bb.command = command;
break;
default:
throw logic_error("unknown game version");
}
throw logic_error("unknown game version");
}
uint16_t PSOCommandHeader::size(GameVersion version) const {
switch (version) {
case GameVersion::DC:
return this->dc.size;
case GameVersion::GC:
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->size;
return this->gc.size;
case GameVersion::PC:
case GameVersion::PATCH:
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->size;
return this->pc.size;
case GameVersion::BB:
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->size;
return this->bb.size;
default:
throw logic_error("unknown game version");
}
}
void PSOCommandHeader::set_size(GameVersion version, uint32_t size) {
switch (version) {
case GameVersion::DC:
this->dc.size = size;
break;
case GameVersion::GC:
this->gc.size = size;
break;
case GameVersion::PC:
case GameVersion::PATCH:
this->pc.size = size;
break;
case GameVersion::BB:
this->bb.size = size;
break;
default:
throw logic_error("unknown game version");
}
throw logic_error("unknown game version");
}
uint32_t PSOCommandHeader::flag(GameVersion version) const {
switch (version) {
case GameVersion::DC:
return this->dc.flag;
case GameVersion::GC:
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->flag;
return this->gc.flag;
case GameVersion::PC:
case GameVersion::PATCH:
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->flag;
return this->pc.flag;
case GameVersion::BB:
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->flag;
return this->bb.flag;
default:
throw logic_error("unknown game version");
}
}
void PSOCommandHeader::set_flag(GameVersion version, uint32_t flag) {
switch (version) {
case GameVersion::DC:
this->dc.flag = flag;
break;
case GameVersion::GC:
this->gc.flag = flag;
break;
case GameVersion::PC:
case GameVersion::PATCH:
this->pc.flag = flag;
break;
case GameVersion::BB:
this->bb.flag = flag;
break;
default:
throw logic_error("unknown game version");
}
throw logic_error("unknown game version");
}
@@ -62,7 +131,7 @@ void for_each_received_command(
struct bufferevent* bev,
GameVersion version,
PSOEncryption* crypt,
function<void(uint16_t, uint16_t, const string&)> fn) {
function<void(uint16_t, uint16_t, string&)> fn) {
struct evbuffer* buf = bufferevent_get_input(bev);
size_t header_size = version == GameVersion::BB ? 8 : 4;
@@ -108,4 +177,33 @@ void for_each_received_command(
fn(header.command(version), header.flag(version), command_data);
}
}
}
void print_received_command(
uint16_t command,
uint32_t flag,
const void* data,
size_t size,
GameVersion version,
const char* name) {
string name_token;
if (name && name[0]) {
name_token = string(" from ") + name;
}
log(INFO, "Received%s (version=%d command=%04hX flag=%08X)",
name_token.c_str(), static_cast<int>(version), command, flag);
PSOCommandHeader header;
size_t header_size = header.header_size(version);
header.set_command(version, command);
header.set_flag(version, flag);
header.set_size(version, size + header_size);
// TODO: This is unnecessarily slow. It'd be nice to have a print_data_v() so
// we don't have to copy data around here.
StringWriter w;
w.write(&header, header_size);
w.write(data, size);
print_data(stderr, w.str());
}
+15 -1
View File
@@ -33,8 +33,14 @@ union PSOCommandHeader {
PSOCommandHeaderBB bb;
uint16_t command(GameVersion version) const;
void set_command(GameVersion version, uint16_t command);
uint16_t size(GameVersion version) const;
void set_size(GameVersion version, uint32_t size);
uint32_t flag(GameVersion version) const;
void set_flag(GameVersion version, uint32_t flag);
static inline size_t header_size(GameVersion version) {
return (version == GameVersion::BB) ? 8 : 4;
}
PSOCommandHeader();
} __attribute__((packed));
@@ -49,4 +55,12 @@ void for_each_received_command(
struct bufferevent* bev,
GameVersion version,
PSOEncryption* crypt,
std::function<void(uint16_t, uint16_t, const std::string&)> fn);
std::function<void(uint16_t, uint16_t, std::string&)> fn);
void print_received_command(
uint16_t command,
uint32_t flag,
const void* data,
size_t size,
GameVersion version,
const char* name = nullptr);
+651 -461
View File
File diff suppressed because it is too large Load Diff
+103 -57
View File
@@ -12,6 +12,7 @@
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "ServerState.hh"
@@ -22,77 +23,122 @@ public:
ProxyServer(ProxyServer&&) = delete;
ProxyServer(
std::shared_ptr<struct event_base> base,
const struct sockaddr_storage& initial_destination,
GameVersion version);
std::shared_ptr<ServerState> state);
virtual ~ProxyServer() = default;
void listen(int port);
void listen(uint16_t port, GameVersion version);
void connect_client(struct bufferevent* bev, uint32_t server_ipv4_addr,
uint16_t server_port);
void connect_client(struct bufferevent* bev, uint16_t server_port);
void send_to_client(const std::string& data);
void send_to_server(const std::string& data);
struct LinkedSession {
ProxyServer* server;
void set_save_quests(bool save_quests);
std::shared_ptr<const License> license;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> client_bev;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> server_bev;
uint16_t local_port;
struct sockaddr_storage next_destination;
GameVersion version;
uint32_t sub_version;
std::string character_name;
std::shared_ptr<PSOEncryption> client_input_crypt;
std::shared_ptr<PSOEncryption> client_output_crypt;
std::shared_ptr<PSOEncryption> server_input_crypt;
std::shared_ptr<PSOEncryption> server_output_crypt;
struct SavingQuestFile {
std::string basename;
std::string output_filename;
uint32_t remaining_bytes;
std::unique_ptr<FILE, std::function<void(FILE*)>> f;
SavingQuestFile(
const std::string& basename,
const std::string& output_filename,
uint32_t remaining_bytes);
};
bool save_quests;
std::unordered_map<std::string, SavingQuestFile> saving_quest_files;
LinkedSession(
ProxyServer* server,
uint16_t local_port,
GameVersion version,
std::shared_ptr<const License> license,
uint32_t dest_addr,
uint16_t dest_port);
void resume(
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
std::shared_ptr<PSOEncryption> client_input_crypt,
std::shared_ptr<PSOEncryption> client_output_crypt,
uint32_t sub_version,
const std::string& character_name);
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx);
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_server_error(struct bufferevent* bev, short events,
void* ctx);
void on_client_input();
void on_server_input();
void on_stream_error(short events, bool is_server_stream);
void send_to_end(const std::string& data, bool to_server);
void disconnect();
bool is_open() const;
};
std::shared_ptr<LinkedSession> get_session();
void delete_session(uint32_t serial_number);
private:
std::shared_ptr<struct event_base> base;
std::map<int, std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>> listeners;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> client_bev;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> server_bev;
struct sockaddr_storage next_destination;
struct sockaddr_storage default_destination;
int listen_port;
GameVersion version;
struct ListeningSocket {
ProxyServer* server;
size_t header_size;
PSOCommandHeader client_input_header;
PSOCommandHeader server_input_header;
std::shared_ptr<PSOEncryption> client_input_crypt;
std::shared_ptr<PSOEncryption> client_output_crypt;
std::shared_ptr<PSOEncryption> server_input_crypt;
std::shared_ptr<PSOEncryption> server_output_crypt;
int fd;
uint16_t port;
std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener;
GameVersion version;
struct SavingQuestFile {
std::string basename;
std::string output_filename;
uint32_t remaining_bytes;
std::unique_ptr<FILE, std::function<void(FILE*)>> f;
SavingQuestFile(
const std::string& basename,
const std::string& output_filename,
uint32_t remaining_bytes);
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
void on_listen_accept(struct sockaddr *address, int socklen);
void on_listen_error();
};
bool save_quests;
std::unordered_map<std::string, SavingQuestFile> saving_quest_files;
void send_to_end(const std::string& data, bool to_server);
struct UnlinkedSession {
ProxyServer* server;
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx);
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_server_error(struct bufferevent* bev, short events,
void* ctx);
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> bev;
uint16_t local_port;
GameVersion version;
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr *address, int socklen);
void on_listen_error(struct evconnlistener* listener);
void on_client_input(struct bufferevent* bev);
void on_client_error(struct bufferevent* bev, short events);
void on_server_input(struct bufferevent* bev);
void on_server_error(struct bufferevent* bev, short events);
std::shared_ptr<PSOEncryption> crypt_out;
std::shared_ptr<PSOEncryption> crypt_in;
void on_client_connect(struct bufferevent* bev,
uint32_t server_ipv4_addr = 0, uint16_t server_port = 0);
UnlinkedSession(ProxyServer* server, struct bufferevent* bev, uint16_t port, GameVersion version);
size_t get_size_field(const PSOCommandHeader* header);
size_t get_command_field(const PSOCommandHeader* header);
void receive_and_process_commands();
void receive_and_process_commands(bool from_server);
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx);
void on_client_input();
void on_client_error(short events);
};
std::shared_ptr<struct event_base> base;
std::shared_ptr<ServerState> state;
std::map<int, std::shared_ptr<ListeningSocket>> listeners;
std::unordered_map<struct bufferevent*, std::shared_ptr<UnlinkedSession>> bev_to_unlinked_session;
std::unordered_map<uint32_t, std::shared_ptr<LinkedSession>> serial_number_to_session;
void on_client_connect(struct bufferevent* bev, uint16_t port, GameVersion version);
};
-146
View File
@@ -1,146 +0,0 @@
#include "ProxyShell.hh"
#include <event2/event.h>
#include <stdio.h>
#include <phosg/Strings.hh>
using namespace std;
ProxyShell::ProxyShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state,
std::shared_ptr<ProxyServer> proxy_server) : Shell(base, state),
proxy_server(proxy_server) { }
void ProxyShell::execute_command(const string& command) {
// find the entry in the command table and run the command
size_t command_end = skip_non_whitespace(command, 0);
size_t args_begin = skip_whitespace(command, command_end);
string command_name = command.substr(0, command_end);
string command_args = command.substr(args_begin);
if (command_name == "exit") {
throw exit_shell();
} else if (command_name == "help") {
fprintf(stderr, "\
Commands:\n\
help\n\
You\'re reading it now.\n\
exit (or ctrl+d)\n\
Shut down the proxy.\n\
sc <data>\n\
Send a command to the client.\n\
ss <data>\n\
Send a command to the server.\n\
chat <text>\n\
Send a chat message to the server.\n\
dchat <data>\n\
Send a chat message to the server with arbitrary data in it.\n\
info-board <text>\n\
Set your info board contents.\n\
info-board-data <data>\n\
Set your info board contents with arbitrary data.\n\
marker <color-id>\n\
Send a lobby marker message to the server.\n\
event <event-id>\n\
Send a lobby event update to yourself.\n\
ship\n\
Request the ship select menu from the server.\n\
");
} else if ((command_name == "sc") || (command_name == "ss")) {
bool to_client = (command_name[1] == 'c');
string data = parse_data_string(command_args);
if (data.size() & 3) {
throw invalid_argument("data size is not a multiple of 4");
}
if (data.size() == 0) {
throw invalid_argument("no data given");
}
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
log(INFO, "%s (from proxy):", to_client ? "server" : "client");
print_data(stderr, data);
if (to_client) {
this->proxy_server->send_to_client(data);
} else {
this->proxy_server->send_to_server(data);
}
} else if ((command_name == "chat") || (command_name == "dchat")) {
string data(12, '\0');
data[0] = 0x06;
data.push_back('\x09');
data.push_back('E');
if (command_name == "dchat") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
log(INFO, "Client (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if (command_name == "marker") {
string data("\x89\x00\x04\x00", 4);
data[1] = stod(command_args);
log(INFO, "Client (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if (command_name == "event") {
string data("\xDA\x00\x04\x00", 4);
data[1] = stod(command_args);
log(INFO, "Server (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_client(data);
} else if (command_name == "ship") {
static const string data("\xA0\x00\x04\x00", 4);
log(INFO, "Server (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if ((command_name == "info-board") || (command_name == "info-board-data")) {
string data(4, '\0');
data[0] = 0xD9;
if (command_name == "info-board-data") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
log(INFO, "Client (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if (command_name == "set-save-quests") {
if (command_args == "on") {
this->proxy_server->set_save_quests(true);
} else if (command_args == "off") {
this->proxy_server->set_save_quests(false);
} else {
throw invalid_argument("argument must be \"on\" or \"off\"");
}
} else {
throw invalid_argument("unknown command; try \'help\'");
}
}
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <event2/event.h>
#include "Shell.hh"
#include "ProxyServer.hh"
class ProxyShell : public Shell {
public:
ProxyShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state,
std::shared_ptr<ProxyServer> proxy_server);
virtual ~ProxyShell() = default;
ProxyShell(const ProxyShell&) = delete;
ProxyShell(ProxyShell&&) = delete;
ProxyShell& operator=(const ProxyShell&) = delete;
ProxyShell& operator=(ProxyShell&&) = delete;
protected:
std::shared_ptr<ProxyServer> proxy_server;
virtual void execute_command(const std::string& command);
};
+98 -109
View File
@@ -1,10 +1,11 @@
#include "SendCommands.hh"
#include "ReceiveCommands.hh"
#include <inttypes.h>
#include <string.h>
#include <memory>
#include <phosg/Filesystem.hh>
#include <phosg/Network.hh>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
@@ -15,6 +16,7 @@
#include "SendCommands.hh"
#include "ReceiveSubcommands.hh"
#include "ChatCommands.hh"
#include "ProxyServer.hh"
using namespace std;
@@ -196,21 +198,10 @@ void process_disconnect(shared_ptr<ServerState> s, shared_ptr<Client> c) {
void process_verify_license_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, uint16_t size, const void* data) { // DB
struct Cmd {
char unused[0x20];
char serial_number[0x10];
char access_key[0x10];
char unused2[0x08];
uint32_t sub_version;
char unused3[0x60];
char password[0x10];
char unused4[0x20];
};
check_size(size, sizeof(Cmd));
const auto* cmd = reinterpret_cast<const Cmd*>(data);
check_size(size, sizeof(VerifyLicenseCommand_GC_DB));
const auto* cmd = reinterpret_cast<const VerifyLicenseCommand_GC_DB*>(data);
uint32_t serial_number = 0;
sscanf(cmd->serial_number, "%8" PRIX32, &serial_number);
uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16);
try {
c->license = s->license_manager->verify_gc(serial_number, cmd->access_key,
cmd->password);
@@ -221,8 +212,10 @@ void process_verify_license_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
c->should_disconnect = true;
return;
} else {
c->license = LicenseManager::create_license_pc(serial_number,
cmd->access_key, cmd->password);
auto l = LicenseManager::create_license_gc(serial_number,
cmd->access_key, cmd->password, true);
s->license_manager->add(l);
c->license = l;
}
}
@@ -240,8 +233,7 @@ void process_login_a_dc_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
check_size(size, sizeof(Cmd));
const auto* cmd = reinterpret_cast<const Cmd*>(data);
uint32_t serial_number = 0;
sscanf(cmd->serial_number, "%8" PRIX32, &serial_number);
uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16);
try {
if (c->version == GameVersion::GC) {
c->license = s->license_manager->verify_gc(serial_number, cmd->access_key,
@@ -251,20 +243,14 @@ void process_login_a_dc_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
nullptr);
}
} catch (const exception& e) {
if (!s->allow_unregistered_users) {
u16string message = u"Login failed: " + decode_sjis(e.what());
send_message_box(c, message.c_str());
c->should_disconnect = true;
return;
} else {
if (c->version == GameVersion::GC) {
c->license = LicenseManager::create_license_gc(serial_number,
cmd->access_key, nullptr);
} else {
c->license = LicenseManager::create_license_pc(serial_number,
cmd->access_key, nullptr);
}
}
// The client should have sent a different command containing the password
// already, which should have created and added a temporary license. If no
// license exists, disconnect the client even if unregistered clients are
// allowed.
u16string message = u"Login failed: " + decode_sjis(e.what());
send_message_box(c, message.c_str());
c->should_disconnect = true;
return;
}
send_command(c, 0x9C, 0x01);
@@ -285,8 +271,7 @@ void process_login_c_dc_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
c->flags |= flags_for_version(c->version, cmd->sub_version);
uint32_t serial_number = 0;
sscanf(cmd->serial_number, "%8" PRIX32, &serial_number);
uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16);
try {
if (c->version == GameVersion::GC) {
c->license = s->license_manager->verify_gc(serial_number, cmd->access_key,
@@ -302,13 +287,16 @@ void process_login_c_dc_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
c->should_disconnect = true;
return;
} else {
shared_ptr<License> l;
if (c->version == GameVersion::GC) {
c->license = LicenseManager::create_license_gc(serial_number,
cmd->access_key, cmd->password);
l = LicenseManager::create_license_gc(serial_number,
cmd->access_key, cmd->password, true);
} else {
c->license = LicenseManager::create_license_pc(serial_number,
cmd->access_key, cmd->password);
l = LicenseManager::create_license_pc(serial_number,
cmd->access_key, cmd->password, true);
}
s->license_manager->add(l);
c->license = l;
}
}
@@ -317,28 +305,13 @@ void process_login_c_dc_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
void process_login_d_e_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, uint16_t size, const void* data) { // 9D 9E
struct Cmd {
char unused[0x10];
uint8_t sub_version;
uint8_t unused2[0x27];
char serial_number[0x10];
char access_key[0x10];
char unused3[0x60];
char name[0x10];
// Note: there are 8 bytes at the end of cfg that are technically not
// included in the client config on GC, but the field after it is
// sufficiently large and unused anyway
ClientConfig cfg;
uint8_t unused4[0x5C];
};
// sometimes the unused bytes aren't sent?
check_size(size, sizeof(Cmd) - 0x64, sizeof(Cmd));
const auto* cmd = reinterpret_cast<const Cmd*>(data);
check_size(size, sizeof(LoginCommand_GC_9E) - 0x64, sizeof(LoginCommand_GC_9E));
const auto* cmd = reinterpret_cast<const LoginCommand_GC_9E*>(data);
c->flags |= flags_for_version(c->version, cmd->sub_version);
uint32_t serial_number = 0;
sscanf(cmd->serial_number, "%8" PRIX32, &serial_number);
uint32_t serial_number = strtoul(cmd->serial_number, nullptr, 16);
try {
if (c->version == GameVersion::GC) {
c->license = s->license_manager->verify_gc(serial_number, cmd->access_key,
@@ -348,20 +321,12 @@ void process_login_d_e_pc_gc(shared_ptr<ServerState> s, shared_ptr<Client> c,
nullptr);
}
} catch (const exception& e) {
if (!s->allow_unregistered_users) {
u16string message = u"Login failed: " + decode_sjis(e.what());
send_message_box(c, message.c_str());
c->should_disconnect = true;
return;
} else {
if (c->version == GameVersion::GC) {
c->license = LicenseManager::create_license_gc(serial_number,
cmd->access_key, nullptr);
} else {
c->license = LicenseManager::create_license_pc(serial_number,
cmd->access_key, nullptr);
}
}
// See comment in 9A handler about why we do this even if unregistered users
// are allowed on the server
u16string message = u"Login failed: " + decode_sjis(e.what());
send_message_box(c, message.c_str());
c->should_disconnect = true;
return;
}
try {
@@ -612,7 +577,13 @@ void process_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Client
send_ship_info(c, u"Go to the lobby.");
break;
case MAIN_MENU_INFORMATION:
send_ship_info(c, u"View server information.");
send_ship_info(c, u"View server\ninformation.");
break;
case MAIN_MENU_PROXY_DESTINATIONS:
send_ship_info(c, u"Connect to another\nserver.");
break;
case MAIN_MENU_DOWNLOAD_QUESTS:
send_ship_info(c, u"Download a quest.");
break;
case MAIN_MENU_DISCONNECT:
send_ship_info(c, u"End your session.");
@@ -636,6 +607,19 @@ void process_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Client
}
break;
case PROXY_DESTINATIONS_MENU_ID:
if (cmd->item_id == PROXY_DESTINATIONS_MENU_GO_BACK) {
send_ship_info(c, u"Return to the\nmain menu.");
} else {
try {
// we use item_id + 1 here because "go back" is the first item
send_ship_info(c, s->proxy_destinations_menu.at(cmd->item_id + 1).description.c_str());
} catch (const out_of_range&) {
send_ship_info(c, u"$C6No such information exists.");
}
}
break;
case QUEST_MENU_ID: {
if (!s->quest_index) {
send_quest_info(c, u"$C6Quests are not available.");
@@ -690,6 +674,11 @@ void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
c->flags |= ClientFlag::IN_INFORMATION_MENU;
break;
case MAIN_MENU_PROXY_DESTINATIONS:
send_menu(c, u"Proxy server", PROXY_DESTINATIONS_MENU_ID,
s->proxy_destinations_menu, false);
break;
case MAIN_MENU_DOWNLOAD_QUESTS:
send_quest_menu(c, QUEST_FILTER_MENU_ID, quest_download_menu, true);
break;
@@ -720,6 +709,41 @@ void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
break;
}
case PROXY_DESTINATIONS_MENU_ID: {
if (cmd->item_id == PROXY_DESTINATIONS_MENU_GO_BACK) {
send_menu(c, s->name.c_str(), MAIN_MENU_ID, s->main_menu, false);
} else {
pair<string, uint16_t>* dest = nullptr;
try {
dest = &s->proxy_destinations.at(cmd->item_id);
} catch (const out_of_range&) { }
if (!dest) {
send_message_box(c, u"$C6No such destination exists.");
c->should_disconnect = true;
} else {
// TODO: We can probably avoid using client config and reconnecting the
// client here; it's likely we could build a way to just directly link
// the client to the proxy server instead (would have to provide
// license/char name/etc. for remote auth)
c->proxy_destination_address = resolve_ipv4(dest->first);
c->proxy_destination_port = dest->second;
send_update_client_config(c);
s->proxy_server->delete_session(c->license->serial_number);
static const vector<string> version_to_port_name({
"dc-proxy", "pc-proxy", "", "gc-proxy", "bb-proxy"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(c->version));
send_reconnect(c, s->connect_address_for_client(c),
s->named_port_configuration.at(port_name).port);
}
}
break;
}
case GAME_MENU_ID: {
auto game = s->find_lobby(cmd->item_id);
if (!game) {
@@ -2219,43 +2243,8 @@ static process_command_t* handlers[6] = {
void process_command(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t command, uint32_t flag, uint16_t size, const void* data) {
// TODO: this is slow; make it better somehow
{
string name_token;
if (c->player.disp.name[0]) {
name_token = " from " + remove_language_marker(encode_sjis(c->player.disp.name));
}
log(INFO, "Received%s version=%d size=%04hX command=%04hX flag=%08X",
name_token.c_str(), static_cast<int>(c->version), size, command, flag);
string data_to_print;
if (c->version == GameVersion::BB) {
data_to_print.resize(size + 8);
PSOCommandHeaderBB* header = reinterpret_cast<PSOCommandHeaderBB*>(
data_to_print.data());
header->command = command;
header->flag = flag;
header->size = size + 8;
memcpy(data_to_print.data() + 8, data, size);
} else if (c->version == GameVersion::PC) {
data_to_print.resize(size + 4);
PSOCommandHeaderPC* header = reinterpret_cast<PSOCommandHeaderPC*>(
data_to_print.data());
header->command = command;
header->flag = flag;
header->size = size + 4;
memcpy(data_to_print.data() + 4, data, size);
} else { // DC/GC
data_to_print.resize(size + 4);
PSOCommandHeaderDCGC* header = reinterpret_cast<PSOCommandHeaderDCGC*>(
data_to_print.data());
header->command = command;
header->flag = flag;
header->size = size + 4;
memcpy(data_to_print.data() + 4, data, size);
}
print_data(stderr, data_to_print);
}
string encoded_name = remove_language_marker(encode_sjis(c->player.disp.name));
print_received_command(command, flag, data, size, c->version, encoded_name.c_str());
auto fn = handlers[static_cast<size_t>(c->version)][command & 0xFF];
if (fn) {
+33
View File
@@ -5,6 +5,39 @@
#include "ServerState.hh"
// These commands' structures are defined here because they're used by both the
// game server and proxy server
struct VerifyLicenseCommand_GC_DB {
char unused[0x20];
char serial_number[0x10];
char access_key[0x10];
char unused2[0x08];
uint32_t sub_version;
char serial_number2[0x30];
char access_key2[0x30];
char password[0x30];
} __attribute__((packed));
struct LoginCommand_GC_9E {
char unused[0x10]; // 00 00 FF FF FF FF FF FF 00 00 00 00 00 00 00 00
uint32_t sub_version;
uint8_t unused2[0x24]; // 00 01 00 00 ... (rest is 00)
char serial_number[0x10];
char access_key[0x10];
char serial_number2[0x30];
char access_key2[0x30];
char name[0x10];
// Note: there are 8 bytes at the end of cfg that are technically not
// included in the client config on GC, but the field after it is
// sufficiently large and unused anyway
ClientConfig cfg;
uint8_t unused4[0x5C];
} __attribute__((packed));
void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void process_disconnect(std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c);
+37 -18
View File
@@ -2,6 +2,7 @@
#include <inttypes.h>
#include <string.h>
#include <event2/buffer.h>
#include <memory>
#include <phosg/Encoding.hh>
@@ -24,11 +25,18 @@ static FileContentsCache file_cache;
void send_command(shared_ptr<Client> c, uint16_t command, uint32_t flag,
const void* data, size_t size) {
void send_command(
struct bufferevent* bev,
GameVersion version,
PSOEncryption* crypt,
uint16_t command,
uint32_t flag,
const void* data,
size_t size,
const char* name_str) {
string send_data;
switch (c->version) {
switch (version) {
case GameVersion::GC:
case GameVersion::DC: {
PSOCommandHeaderDCGC header;
@@ -74,15 +82,32 @@ void send_command(shared_ptr<Client> c, uint16_t command, uint32_t flag,
throw logic_error("unimplemented game version in send_command");
}
string name_token;
if (c->player.disp.name[0]) {
name_token = " to " + remove_language_marker(encode_sjis(c->player.disp.name));
if (name_str) {
string name_token;
if (name_str[0]) {
name_token = string(" to ") + name_str;
}
log(INFO, "Sending%s (version=%d command=%04hX flag=%08X)",
name_token.c_str(), static_cast<int>(version), command, flag);
print_data(stderr, send_data.data(), send_data.size());
}
log(INFO, "Sending%s version=%d size=%04zX command=%04hX flag=%08X",
name_token.c_str(), static_cast<int>(c->version), size, command, flag);
print_data(stderr, send_data.data(), send_data.size());
c->send(move(send_data));
if (crypt) {
crypt->encrypt(send_data.data(), send_data.size());
}
struct evbuffer* buf = bufferevent_get_output(bev);
evbuffer_add(buf, send_data.data(), send_data.size());
}
void send_command(shared_ptr<Client> c, uint16_t command, uint32_t flag,
const void* data, size_t size) {
if (!c->bev) {
return;
}
string encoded_name = remove_language_marker(encode_sjis(c->player.disp.name));
send_command(c->bev, c->version, c->crypt_out.get(), command, flag, data,
size, encoded_name.c_str());
}
void send_command_excluding_client(shared_ptr<Lobby> l, shared_ptr<Client> c,
@@ -132,14 +157,8 @@ string prepare_server_init_contents_dc_pc_gc(
bool initial_connection,
uint32_t server_key,
uint32_t client_key) {
struct Command {
char copyright[0x40];
uint32_t server_key;
uint32_t client_key;
char after_message[200];
};
string ret(sizeof(Command), '\0');
auto* cmd = reinterpret_cast<Command*>(ret.data());
string ret(sizeof(ServerInitCommand_GC_02_17), '\0');
auto* cmd = reinterpret_cast<ServerInitCommand_GC_02_17*>(ret.data());
strcpy(cmd->copyright, initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright);
cmd->server_key = server_key;
+9 -14
View File
@@ -16,20 +16,9 @@
#define MAIN_MENU_ID 0x60000000
#define INFORMATION_MENU_ID 0x60000030
#define LOBBY_MENU_ID 0x60000060
#define GAME_MENU_ID 0x60000090
#define QUEST_MENU_ID 0x600000C0
#define QUEST_FILTER_MENU_ID 0x600000F0
#define MAIN_MENU_GO_TO_LOBBY 0x00000001
#define MAIN_MENU_INFORMATION 0x00000002
#define MAIN_MENU_DOWNLOAD_QUESTS 0x00000003
#define MAIN_MENU_DISCONNECT 0x00000004
#define INFORMATION_MENU_GO_BACK 0xFFFFFFFF
void send_command(struct bufferevent* bev, GameVersion version,
PSOEncryption* crypt, uint16_t command, uint32_t flag = 0,
const void* data = nullptr, size_t size = 0, const char* name_str = nullptr);
void send_command(std::shared_ptr<Client> c, uint16_t command,
uint32_t flag = 0, const void* data = nullptr, size_t size = 0);
@@ -73,6 +62,12 @@ void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
struct ServerInitCommand_GC_02_17 {
char copyright[0x40];
uint32_t server_key;
uint32_t client_key;
char after_message[200];
} __attribute__((packed));
std::string prepare_server_init_contents_dc_pc_gc(
bool initial_connection, uint32_t server_key, uint32_t client_key);
+129 -3
View File
@@ -14,14 +14,23 @@ using namespace std;
ServerShell::ServerShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state) : Shell(base, state) { }
ServerShell::ServerShell(
shared_ptr<struct event_base> base,
shared_ptr<ServerState> state)
: Shell(base, state) { }
void ServerShell::print_prompt() {
fwrite("newserv> ", 9, 1, stdout);
fflush(stdout);
}
shared_ptr<ProxyServer::LinkedSession> ServerShell::get_proxy_session() {
if (!this->state->proxy_server.get()) {
throw runtime_error("the proxy server is disabled");
}
return this->state->proxy_server->get_session();
}
void ServerShell::execute_command(const string& command) {
// find the entry in the command table and run the command
size_t command_end = skip_non_whitespace(command, 0);
@@ -34,9 +43,11 @@ void ServerShell::execute_command(const string& command) {
} else if (command_name == "help") {
fprintf(stderr, "\
Commands:\n\
General commands:\n\
help\n\
You\'re reading it now.\n\
\n\
Server commands:\n\
exit (or ctrl+d)\n\
Shut down the server.\n\
reload <item> ...\n\
@@ -66,8 +77,32 @@ Commands:\n\
Song IDs are 0 through 51; the default song is -1.\n\
announce <message>\n\
Send an announcement message to all players.\n\
\n\
Proxy commands (these will only work when exactly one client is connected):\n\
sc <data>\n\
Send a command to the client.\n\
ss <data>\n\
Send a command to the server.\n\
chat <text>\n\
Send a chat message to the server.\n\
dchat <data>\n\
Send a chat message to the server with arbitrary data in it.\n\
info-board <text>\n\
Set your info board contents.\n\
info-board-data <data>\n\
Set your info board contents with arbitrary data.\n\
marker <color-id>\n\
Send a lobby marker message to the server.\n\
event <event-id>\n\
Send a lobby event update to yourself.\n\
ship\n\
Request the ship select menu from the server.\n\
");
// SERVER COMMANDS
} else if (command_name == "reload") {
auto types = split(command_args, ' ');
if (types.empty()) {
@@ -189,6 +224,97 @@ Commands:\n\
u16string message16 = decode_sjis(command_args);
send_text_message(this->state, message16.c_str());
// PROXY COMMANDS
} else if ((command_name == "sc") || (command_name == "ss")) {
auto session = this->get_proxy_session();
bool to_server = (command_name[1] == 's');
string data = parse_data_string(command_args);
if (data.size() & 3) {
throw invalid_argument("data size is not a multiple of 4");
}
if (data.size() == 0) {
throw invalid_argument("no data given");
}
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
session->send_to_end(data, to_server);
} else if ((command_name == "chat") || (command_name == "dchat")) {
auto session = this->get_proxy_session();
string data(12, '\0');
data[0] = 0x06;
data.push_back('\x09');
data.push_back('E');
if (command_name == "dchat") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
session->send_to_end(data, true);
} else if (command_name == "marker") {
auto session = this->get_proxy_session();
string data("\x89\x00\x04\x00", 4);
data[1] = stod(command_args);
session->send_to_end(data, true);
} else if (command_name == "event") {
auto session = this->get_proxy_session();
string data("\xDA\x00\x04\x00", 4);
data[1] = stod(command_args);
session->send_to_end(data, false);
} else if (command_name == "ship") {
auto session = this->get_proxy_session();
static const string data("\xA0\x00\x04\x00", 4);
session->send_to_end(data, true);
} else if ((command_name == "info-board") || (command_name == "info-board-data")) {
auto session = this->get_proxy_session();
string data(4, '\0');
data[0] = 0xD9;
if (command_name == "info-board-data") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
session->send_to_end(data, true);
} else if (command_name == "set-save-quests") {
auto session = this->get_proxy_session();
if (command_args == "on") {
session->save_quests = true;
} else if (command_args == "off") {
session->save_quests = false;
} else {
throw invalid_argument("argument must be \"on\" or \"off\"");
}
} else {
throw invalid_argument("unknown command; try \'help\'");
}
+5 -1
View File
@@ -6,12 +6,14 @@
#include <event2/event.h>
#include "Shell.hh"
#include "ProxyServer.hh"
class ServerShell : public Shell {
public:
ServerShell(std::shared_ptr<struct event_base> base,
ServerShell(
std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state);
virtual ~ServerShell() = default;
ServerShell(const ServerShell&) = delete;
@@ -20,6 +22,8 @@ public:
ServerShell& operator=(ServerShell&&) = delete;
protected:
std::shared_ptr<ProxyServer::LinkedSession> get_proxy_session();
virtual void print_prompt();
virtual void execute_command(const std::string& command);
};
-9
View File
@@ -22,15 +22,6 @@ ServerState::ServerState()
ep3_menu_song(-1) {
memset(&this->default_key_file, 0, sizeof(this->default_key_file));
this->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby",
u"Join the lobby.", 0);
this->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information",
u"View server information.", MenuItemFlag::REQUIRES_MESSAGE_BOXES);
this->main_menu.emplace_back(MAIN_MENU_DOWNLOAD_QUESTS, u"Download quests",
u"Download quests.", 0);
this->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect",
u"Disconnect.", 0);
for (size_t x = 0; x < 20; x++) {
auto lobby_name = decode_sjis(string_printf("LOBBY%zu", x + 1));
shared_ptr<Lobby> l(new Lobby());
+9
View File
@@ -18,6 +18,9 @@
// Forwawrd declaration due to reference cycle
class ProxyServer;
struct PortConfiguration {
uint16_t port;
GameVersion version;
@@ -51,6 +54,8 @@ struct ServerState {
std::vector<MenuItem> main_menu;
std::shared_ptr<std::vector<MenuItem>> information_menu;
std::shared_ptr<std::vector<std::u16string>> information_contents;
std::vector<MenuItem> proxy_destinations_menu;
std::vector<std::pair<std::string, uint16_t>> proxy_destinations;
std::u16string welcome_message;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
@@ -62,6 +67,10 @@ struct ServerState {
uint32_t local_address;
uint32_t external_address;
// TODO: This is only here because the menu selection handler has to call
// delete_session on it. Find a cleaner way to do this.
std::shared_ptr<ProxyServer> proxy_server;
ServerState();
void add_client_to_available_lobby(std::shared_ptr<Client> c);
+7
View File
@@ -30,6 +30,13 @@
// simulator. This is generally only useful for finding bugs in the interface.
// "IPStackDebug": true,
// Other servers to support proxying to. If this is empty, the proxy server is
// disabled.
"ProxyDestinations": {
// "name (appears in menu)": "netloc"
"Schtserv": "149.56.167.128:9103",
},
// By default, the interactive shell runs if stdin is a terminal, and doesn't
// run if it's not. This option, if present, overrides that behavior.
// "RunInteractiveShell": false,