improve BB proxy functionality
This commit is contained in:
+13
@@ -663,6 +663,19 @@ Action a_generate_pc_v2_registry(
|
||||
write_output_data(args, output_data.data(), output_data.size(), "reg");
|
||||
});
|
||||
|
||||
Action a_encrypt_challenge_time(
|
||||
"encrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
|
||||
uint16_t time = args.get<uint16_t>(1);
|
||||
uint32_t ret = encrypt_challenge_time(time);
|
||||
phosg::fwrite_fmt(stderr, "{} => {:08X}\n", phosg::format_duration(time * 1000000), ret);
|
||||
});
|
||||
Action a_decrypt_challenge_time(
|
||||
"decrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
|
||||
uint32_t time = args.get<uint32_t>(1, phosg::Arguments::IntFormat::HEX);
|
||||
uint16_t ret = decrypt_challenge_time(time);
|
||||
phosg::fwrite_fmt(stderr, "{:08X} => {}\n", time, phosg::format_duration(ret * 1000000));
|
||||
});
|
||||
|
||||
Action a_encrypt_challenge_data(
|
||||
"encrypt-challenge-data", nullptr, +[](phosg::Arguments& args) {
|
||||
string data = read_input_data(args);
|
||||
|
||||
+21
-20
@@ -706,27 +706,28 @@ void PSOBBMultiKeyDetectorEncryption::encrypt(void* data, size_t size) {
|
||||
}
|
||||
|
||||
void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size) {
|
||||
if (!this->active_crypt.get()) {
|
||||
if (size != 8) {
|
||||
throw logic_error("initial decryption size does not match expected first data size");
|
||||
}
|
||||
|
||||
for (const auto& key : this->possible_keys) {
|
||||
this->active_key = key;
|
||||
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
|
||||
string test_data(reinterpret_cast<const char*>(data), size);
|
||||
this->active_crypt->decrypt(test_data.data(), test_data.size());
|
||||
if (this->expected_first_data.count(test_data)) {
|
||||
break;
|
||||
}
|
||||
this->active_key.reset();
|
||||
this->active_crypt.reset();
|
||||
}
|
||||
if (!this->active_crypt.get()) {
|
||||
throw runtime_error("none of the registered private keys are valid for this client");
|
||||
}
|
||||
if (this->active_crypt.get()) {
|
||||
this->active_crypt->decrypt(data, size);
|
||||
return;
|
||||
}
|
||||
this->active_crypt->decrypt(data, size);
|
||||
|
||||
if (size != 8) {
|
||||
throw logic_error("initial decryption size does not match expected first data size");
|
||||
}
|
||||
|
||||
for (const auto& key : this->possible_keys) {
|
||||
this->active_key = key;
|
||||
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
|
||||
string test_data(reinterpret_cast<const char*>(data), size);
|
||||
this->active_crypt->decrypt(test_data.data(), test_data.size());
|
||||
if (this->expected_first_data.count(test_data)) {
|
||||
memcpy(data, test_data.data(), size);
|
||||
return;
|
||||
}
|
||||
this->active_key.reset();
|
||||
this->active_crypt.reset();
|
||||
}
|
||||
throw runtime_error("none of the registered private keys are valid for this client");
|
||||
}
|
||||
|
||||
PSOEncryption::Type PSOBBMultiKeyDetectorEncryption::type() const {
|
||||
|
||||
+15
-21
@@ -356,11 +356,11 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
|
||||
resp.character_slot = c->bb_character_index;
|
||||
resp.connection_phase = c->bb_connection_phase;
|
||||
resp.client_code = c->bb_client_code;
|
||||
resp.security_token = c->proxy_session->remote_bb_security_token;
|
||||
resp.security_token = c->bb_security_token;
|
||||
resp.username.encode(c->username, c->language());
|
||||
resp.password.encode(c->password, c->language());
|
||||
resp.hardware_id = c->hardware_id;
|
||||
resp.client_config = c->proxy_session->remote_client_config_data;
|
||||
resp.client_config = c->bb_client_config;
|
||||
if (c->proxy_session->enable_remote_ip_crc_patch) {
|
||||
*reinterpret_cast<le_uint32_t*>(resp.client_config.data() + 0x10) =
|
||||
c->proxy_session->remote_ip_crc ^ (1309539928UL + 1248334810UL);
|
||||
@@ -373,27 +373,17 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
|
||||
static asio::awaitable<HandlerResult> S_B_E6(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
const auto& cmd = msg.check_size_t<S_ClientInit_BB_00E6>(0xFFFF);
|
||||
c->proxy_session->remote_guild_card_number = cmd.guild_card_number;
|
||||
c->proxy_session->remote_bb_security_token = cmd.security_token;
|
||||
c->proxy_session->remote_client_config_data = cmd.client_config;
|
||||
c->bb_security_token = cmd.security_token;
|
||||
c->bb_client_config = cmd.client_config;
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto& pc = s->proxy_persistent_configs[c->login->account->account_id];
|
||||
pc.account_id = c->login->account->account_id;
|
||||
pc.remote_guild_card_number = c->proxy_session->remote_guild_card_number;
|
||||
pc.remote_bb_security_token = c->proxy_session->remote_bb_security_token;
|
||||
pc.remote_client_config_data = c->proxy_session->remote_client_config_data;
|
||||
pc.enable_remote_ip_crc_patch = c->proxy_session->enable_remote_ip_crc_patch;
|
||||
c->log.info_f("Updated persistent config for proxy session");
|
||||
|
||||
if ((c->bb_connection_phase == 0) && c->proxy_session->received_reconnect) {
|
||||
c->proxy_session->server_channel->send(0x00E0); // Request system file
|
||||
}
|
||||
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
static asio::awaitable<HandlerResult> C_B_E0(shared_ptr<Client>, Channel::Message&) {
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
co_return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static asio::awaitable<HandlerResult> S_V123_04(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
@@ -748,18 +738,18 @@ static asio::awaitable<HandlerResult> S_G_E4(shared_ptr<Client> c, Channel::Mess
|
||||
static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
// We use this command (which is sent before the init encryption command) to
|
||||
// detect a particular server behavior that we'll have to work around later.
|
||||
// It looks like this command's existence is another anti-proxy measure, since
|
||||
// It looks like this command's existence is an anti-proxy measure, since
|
||||
// this command is 0x34 bytes in total, and the logic that adds padding bytes
|
||||
// when the command size isn't a multiple of 8 is only active when encryption
|
||||
// is enabled. Presumably some simpler proxies would get this wrong.
|
||||
// Editor's note: There's an unsavory message in this command's data field,
|
||||
// hence the hash here instead of a direct string comparison. I'd love to hear
|
||||
// the story behind why they put that string there.
|
||||
// hence the hash here instead of a direct string comparison. I'd love to
|
||||
// hear the story behind why they put that string there.
|
||||
if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) {
|
||||
c->log.info_f("Enabling remote IP CRC patch");
|
||||
c->proxy_session->enable_remote_ip_crc_patch = true;
|
||||
}
|
||||
co_return HandlerResult::FORWARD;
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
@@ -2249,7 +2239,7 @@ static on_message_t handlers[0x100][NUM_VERSIONS][2] = {
|
||||
/* DE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
|
||||
/* DF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_12_2000 C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, C_B_E0}},
|
||||
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
|
||||
/* E1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
|
||||
/* E2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_B_E2, nullptr}},
|
||||
/* E3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
|
||||
@@ -2317,7 +2307,8 @@ asio::awaitable<void> on_proxy_command(shared_ptr<Client> c, bool from_server, u
|
||||
}
|
||||
}
|
||||
|
||||
asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
|
||||
asio::awaitable<void> handle_proxy_server_commands(
|
||||
shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
|
||||
std::string error_str;
|
||||
// server_channel can be changed by receiving a 19 command, hence the
|
||||
// exception handler is inside the loop here
|
||||
@@ -2326,6 +2317,9 @@ asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_
|
||||
try {
|
||||
msg = make_unique<Channel::Message>(co_await channel->recv());
|
||||
if (c->proxy_session == ses) {
|
||||
for (size_t z = 0; z < std::min<size_t>(c->proxy_session->prev_server_command_bytes.size(), msg->data.size()); z++) {
|
||||
c->proxy_session->prev_server_command_bytes[z] = msg->data[z];
|
||||
}
|
||||
asio::co_spawn(co_await asio::this_coro::executor, on_proxy_command(c, true, std::move(msg)), asio::detached);
|
||||
}
|
||||
} catch (const std::system_error& e) {
|
||||
|
||||
@@ -10,8 +10,6 @@ ProxySession::ProxySession(shared_ptr<Channel> server_channel, const PersistentC
|
||||
: server_channel(server_channel) {
|
||||
if (pc) {
|
||||
this->remote_guild_card_number = pc->remote_guild_card_number;
|
||||
this->remote_bb_security_token = pc->remote_bb_security_token;
|
||||
this->remote_client_config_data = pc->remote_client_config_data;
|
||||
this->enable_remote_ip_crc_patch = pc->enable_remote_ip_crc_patch;
|
||||
} else if (is_v4(this->server_channel->version)) {
|
||||
this->remote_guild_card_number = 0;
|
||||
|
||||
@@ -44,7 +44,6 @@ struct ProxySession {
|
||||
uint64_t server_ping_start_time = 0;
|
||||
|
||||
int64_t remote_guild_card_number = -1;
|
||||
uint32_t remote_bb_security_token = 0;
|
||||
parray<uint8_t, 0x28> remote_client_config_data;
|
||||
|
||||
enum class DropMode {
|
||||
@@ -64,8 +63,6 @@ struct ProxySession {
|
||||
struct PersistentConfig {
|
||||
uint32_t account_id;
|
||||
uint32_t remote_guild_card_number;
|
||||
uint32_t remote_bb_security_token;
|
||||
parray<uint8_t, 0x28> remote_client_config_data;
|
||||
bool enable_remote_ip_crc_patch;
|
||||
std::unique_ptr<asio::steady_timer> expire_timer;
|
||||
};
|
||||
|
||||
+29
-19
@@ -454,7 +454,7 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
|
||||
send_proxy_destinations_menu(c);
|
||||
}
|
||||
} else {
|
||||
// Get persistent config if client is BB
|
||||
// Get persistent config if available
|
||||
ProxySession::PersistentConfig* pc = nullptr;
|
||||
if (use_persistent_config) {
|
||||
try {
|
||||
@@ -477,8 +477,8 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
|
||||
std::format("C-{} proxy remote server at {}", c->id, netloc_str),
|
||||
phosg::TerminalFormat::FG_YELLOW,
|
||||
phosg::TerminalFormat::FG_RED);
|
||||
|
||||
c->proxy_session = make_shared<ProxySession>(channel, pc);
|
||||
|
||||
c->log.info_f("Server channel connected");
|
||||
asio::co_spawn(*s->io_context, handle_proxy_server_commands(c, c->proxy_session, channel), asio::detached);
|
||||
}
|
||||
@@ -507,15 +507,6 @@ asio::awaitable<void> end_proxy_session(shared_ptr<Client> c, const std::string&
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
bool is_in_game = c->proxy_session->is_in_game;
|
||||
c->proxy_session->server_channel->disconnect();
|
||||
c->proxy_session.reset();
|
||||
|
||||
if (is_v4(c->version())) {
|
||||
c->channel->disconnect();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Delete all the other players
|
||||
for (size_t x = 0; x < c->proxy_session->lobby_players.size(); x++) {
|
||||
if (c->proxy_session->lobby_players[x].guild_card_number == 0) {
|
||||
@@ -527,6 +518,15 @@ asio::awaitable<void> end_proxy_session(shared_ptr<Client> c, const std::string&
|
||||
c->channel->send(c->proxy_session->is_in_game ? 0x66 : 0x69, leaving_id, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
bool is_in_game = c->proxy_session->is_in_game;
|
||||
c->proxy_session->server_channel->disconnect();
|
||||
c->proxy_session.reset();
|
||||
|
||||
if (is_v4(c->version())) {
|
||||
c->channel->disconnect();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (is_in_game) {
|
||||
string msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}",
|
||||
s->name, error_message);
|
||||
@@ -1480,24 +1480,34 @@ static asio::awaitable<void> on_93_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
c->preferred_lobby_id = base_cmd.preferred_lobby_id;
|
||||
}
|
||||
|
||||
send_client_init_bb(c, 0);
|
||||
|
||||
if (!c->bb_client_config.is_filled_with(0xFF)) {
|
||||
if (base_cmd.guild_card_number == 0) {
|
||||
// There is a (bug? feature?) in the BB client such that it has to receive
|
||||
// a reconnect command during the data server phase, or else it won't know
|
||||
// where to connect to during character selection. It's not clear why they
|
||||
// didn't just make it use the initial connection address by default...
|
||||
send_client_init_bb(c, 0);
|
||||
send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port);
|
||||
co_return;
|
||||
|
||||
} else if (s->proxy_destination_bb.has_value()) {
|
||||
// Start a proxy session if there's a destination configured Ignore the
|
||||
// persistent config if this is the first data server connection, to
|
||||
// prevent quick reconnects from incorrectly reusing the old session's
|
||||
// state.
|
||||
// Start a proxy session immediately if there's a destination set. Two
|
||||
// things to watch out for:
|
||||
// - Ignore the persistent config if this is the first data server
|
||||
// connection, to prevent quick reconnects from incorrectly reusing the
|
||||
// old session's state.
|
||||
// - We don't send 00E6 (send_client_init_bb) in this case. This is because
|
||||
// the login command is resent to the remote server, and we forward its
|
||||
// response back to the client directly.
|
||||
const auto& [host, port] = *s->proxy_destination_bb;
|
||||
co_await start_proxy_session(c, host, port, c->bb_connection_phase != 0);
|
||||
c->proxy_session->remote_client_config_data = c->bb_client_config;
|
||||
co_return;
|
||||
|
||||
} else if (c->bb_connection_phase >= 0x04) {
|
||||
} else {
|
||||
send_client_init_bb(c, 0);
|
||||
}
|
||||
|
||||
if (c->bb_connection_phase >= 0x04) {
|
||||
// This means the client is done with the data server phase and is in the
|
||||
// game server phase; we should send the ship select menu or a lobby join
|
||||
// command.
|
||||
|
||||
@@ -237,8 +237,6 @@ void send_server_init_bb(shared_ptr<Client> c, uint8_t flags) {
|
||||
auto cmd = prepare_server_init_contents_bb(server_key, client_key, flags);
|
||||
send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd);
|
||||
|
||||
static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
|
||||
static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8);
|
||||
c->bb_detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
|
||||
c->require_server_state()->bb_private_keys,
|
||||
bb_crypt_initial_client_commands,
|
||||
|
||||
@@ -908,7 +908,11 @@ asio::awaitable<deque<string>> f_sc_ss(ShellCommand::Args& args) {
|
||||
|
||||
auto c = args.get_client();
|
||||
if (args.command[1] == 's') {
|
||||
co_await on_command_with_header(c, data);
|
||||
if (c->proxy_session) {
|
||||
send_command_with_header(c->proxy_session->server_channel, data.data(), data.size());
|
||||
} else {
|
||||
co_await on_command_with_header(c, data);
|
||||
}
|
||||
} else {
|
||||
send_command_with_header(c->channel, data.data(), data.size());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user