From faf5fe1093221547cfd25bb97fee61e2e36c1380 Mon Sep 17 00:00:00 2001 From: Knightly Date: Sun, 19 Apr 2026 08:48:14 -1000 Subject: [PATCH] Update PlayerLoginReply struct for TOB - TOB Struct is different from the old one - Moved specifics into their respective structs - Added variant for choosing the struct to use - Added setters to abstract the client operations and reduce duplicate code - Pass at cleanup on GenerateRandomLoginKey to make it easier to modify --- common/emu_versions.cpp | 4 +- loginserver/client.cpp | 181 ++++++++++----------------------- loginserver/client.h | 2 +- loginserver/client_manager.cpp | 4 +- loginserver/login_types.h | 91 ++++++++++++++--- 5 files changed, 133 insertions(+), 149 deletions(-) diff --git a/common/emu_versions.cpp b/common/emu_versions.cpp index 82f2dbe68..64e86f7a2 100644 --- a/common/emu_versions.cpp +++ b/common/emu_versions.cpp @@ -219,7 +219,7 @@ const char* EQ::versions::MobVersionName(MobVersion mob_version) case MobVersion::OfflineRoF2: return "Offline RoF2"; case MobVersion::OfflineTOB: - return "Offline Steam Latest"; + return "Offline TOB"; default: return "Invalid Version"; }; @@ -505,7 +505,7 @@ uint32 EQ::expansions::ConvertExpansionToExpansionBit(Expansion expansion) return bitLS; case Expansion::TOB: return bitTOB; - + default: return bitEverQuest; } diff --git a/loginserver/client.cpp b/loginserver/client.cpp index 9b315da98..8e863e78a 100644 --- a/loginserver/client.cpp +++ b/loginserver/client.cpp @@ -302,20 +302,13 @@ void Client::SendPlayResponse(EQApplicationPacket *outapp) void Client::GenerateRandomLoginKey() { - m_key.clear(); - int count = 0; - while (count < 10) { - static const char key_selection[] = - { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', '0', '1', '2', '3', '4', '5', - '6', '7', '8', '9' - }; + static constexpr std::string_view key_selection = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + constexpr size_t key_length = 10; - m_key.append((const char *) &key_selection[m_random.Int(0, 35)], 1); - count++; + m_key.clear(); + m_key.reserve(key_length); + for (size_t i = 0; i < key_length; ++i) { + m_key += key_selection[m_random.Int(0, key_selection.size() - 1)]; } } @@ -362,55 +355,30 @@ void Client::SendFailedLogin() m_stored_username.clear(); m_stored_password.clear(); - if (m_client_version == cv_tob) { - // unencrypted - LoginBaseMessage h{}; - h.sequence = m_login_base_message.sequence; // login (3) - h.encrypt_type = m_login_base_message.encrypt_type; + // unencrypted + LoginBaseMessage h{}; + h.sequence = m_login_base_message.sequence; + h.encrypt_type = m_login_base_message.encrypt_type; + h.unk3 = m_login_base_message.unk3; - // encrypted - PlayerLoginReplyTOB r{}; - r.base_reply.success = false; - r.base_reply.error_str_id = 105; // Error - The username and/or password were not valid + PlayerLoginReply r = m_client_version == cv_tob ? PlayerLoginReply(PlayerLoginReplyTOB{}) : PlayerLoginReply(PlayerLoginReplyOld{}); + r.set_error_code(LS::ErrStr::ERROR_INVALID_CREDS); + // We don't care what key we send, just that it exists and is 10 characters so that we do not shift + r.set_key("InvalidKey"); - char encrypted_buffer[80] = { 0 }; - auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1); - if (rc == nullptr) { - LogDebug("Failed to encrypt eqcrypt block for failed login"); - } - - constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer); - EQApplicationPacket outapp(OP_LoginAccepted, outsize); - outapp.WriteData(&h, sizeof(h)); - outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer)); - - m_connection->QueuePacket(&outapp); + char encrypted_buffer[80] = { 0 }; + auto rc = eqcrypt_block(r.data(), r.size(), encrypted_buffer, 1); + if (rc == nullptr) { + LogDebug("Failed to encrypt eqcrypt block for failed login"); } - else { - // unencrypted - LoginBaseMessage h{}; - h.sequence = m_login_base_message.sequence; // login (3) - h.encrypt_type = m_login_base_message.encrypt_type; - // encrypted - PlayerLoginReply r{}; - r.base_reply.success = false; - r.base_reply.error_str_id = 105; // Error - The username and/or password were not valid + constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer); + EQApplicationPacket outapp(OP_LoginAccepted, outsize); + outapp.WriteData(&h, sizeof(h)); + outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer)); - char encrypted_buffer[80] = { 0 }; - auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1); - if (rc == nullptr) { - LogDebug("Failed to encrypt eqcrypt block for failed login"); - } + m_connection->QueuePacket(&outapp); - constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer); - EQApplicationPacket outapp(OP_LoginAccepted, outsize); - outapp.WriteData(&h, sizeof(h)); - outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer)); - - m_connection->QueuePacket(&outapp); - } - m_client_status = cs_failed_to_login; } @@ -496,91 +464,44 @@ void Client::DoSuccessfulLogin(LoginAccountsRepository::LoginAccounts &a) m_account_name = a.account_name; m_loginserver_name = a.source_loginserver; - if (m_client_version == cv_tob) { - // unencrypted - LoginBaseMessage h{}; - h.sequence = m_login_base_message.sequence; - h.compressed = false; - h.encrypt_type = m_login_base_message.encrypt_type; - h.unk3 = m_login_base_message.unk3; + // unencrypted + LoginBaseMessage h{}; + h.sequence = m_login_base_message.sequence; + h.compressed = false; + h.encrypt_type = m_login_base_message.encrypt_type; + h.unk3 = m_login_base_message.unk3; - // not serializing any of the variable length strings so just use struct directly - PlayerLoginReplyTOB r{}; - r.base_reply.success = true; - r.base_reply.error_str_id = 101; // No Error - r.unk1 = 0; - r.unk2 = 0; - r.lsid = a.id; - r.failed_attempts = 0; - r.show_player_count = server.options.IsShowPlayerCountEnabled(); - r.unk3 = 0; - r.unk4 = 0; - memcpy(r.key, m_key.c_str(), m_key.size()); + // not serializing any of the variable length strings so just use struct directly + PlayerLoginReply r = m_client_version == cv_tob ? PlayerLoginReply(PlayerLoginReplyTOB{}) : PlayerLoginReply(PlayerLoginReplyOld{}); - //todo: needs to be fixed - //SendExpansionPacketData(r); + r.set_success(true); + r.set_error_code(LS::ErrStr::ERROR_NONE); + r.set_lsid(a.id); + r.set_show_player_count(server.options.IsShowPlayerCountEnabled()); + r.set_key(m_key); - char encrypted_buffer[80] = { 0 }; - - auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1); - if (rc == nullptr) { - LogDebug("Failed to encrypt eqcrypt block"); - } - - constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer); - auto outapp = std::make_unique(OP_LoginAccepted, outsize); - outapp->WriteData(&h, sizeof(h)); - outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer)); - - m_connection->QueuePacket(outapp.get()); + if (m_client_version != cv_tob) { + SendExpansionPacketData(r.old()); } - else { - // unencrypted - LoginBaseMessage h{}; - h.sequence = m_login_base_message.sequence; - h.compressed = false; - h.encrypt_type = m_login_base_message.encrypt_type; - h.unk3 = m_login_base_message.unk3; - // not serializing any of the variable length strings so just use struct directly - PlayerLoginReply r{}; - r.base_reply.success = true; - r.base_reply.error_str_id = 101; // No Error - r.unk1 = 0; - r.unk2 = 0; - r.lsid = a.id; - r.failed_attempts = 0; - r.show_player_count = server.options.IsShowPlayerCountEnabled(); - r.offer_min_days = 99; - r.offer_min_views = -1; - r.offer_cooldown_minutes = 0; - r.web_offer_number = 0; - r.web_offer_min_days = 99; - r.web_offer_min_views = -1; - r.web_offer_cooldown_minutes = 0; - memcpy(r.key, m_key.c_str(), m_key.size()); + char encrypted_buffer[80] = { 0 }; - SendExpansionPacketData(r); - - char encrypted_buffer[80] = { 0 }; - - auto rc = eqcrypt_block((const char*)&r, sizeof(r), encrypted_buffer, 1); - if (rc == nullptr) { - LogDebug("Failed to encrypt eqcrypt block"); - } - - constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer); - auto outapp = std::make_unique(OP_LoginAccepted, outsize); - outapp->WriteData(&h, sizeof(h)); - outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer)); - - m_connection->QueuePacket(outapp.get()); + auto rc = eqcrypt_block(r.data(), r.size(), encrypted_buffer, 1); + if (rc == nullptr) { + LogDebug("Failed to encrypt eqcrypt block"); } + constexpr int outsize = sizeof(LoginBaseMessage) + sizeof(encrypted_buffer); + auto outapp = std::make_unique(OP_LoginAccepted, outsize); + outapp->WriteData(&h, sizeof(h)); + outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer)); + + m_connection->QueuePacket(outapp.get()); + m_client_status = cs_logged_in; } -void Client::SendExpansionPacketData(PlayerLoginReply &plrs) +void Client::SendExpansionPacketData(PlayerLoginReplyOld &plrs) { SerializeBuffer buf; //from eqlsstr_us.txt id of each expansion, excluding 'Everquest' diff --git a/loginserver/client.h b/loginserver/client.h index e1767bece..7decbadab 100644 --- a/loginserver/client.h +++ b/loginserver/client.h @@ -38,7 +38,7 @@ public: // Titanium uses the encrypted data block to contact the expansion (You own xxx:) and the max expansions (of yyy) // Rof uses a separate data packet specifically for the expansion data // Live, as of July 2021 uses a similar but slightly different seperate data packet - void SendExpansionPacketData(PlayerLoginReply &plrs); + void SendExpansionPacketData(PlayerLoginReplyOld &plrs); void SendPlayToWorld(const char *data); void SendServerListPacket(uint32 seq); void SendPlayResponse(EQApplicationPacket *outapp); diff --git a/loginserver/client_manager.cpp b/loginserver/client_manager.cpp index 56b1d0a9f..4e5d1e187 100644 --- a/loginserver/client_manager.cpp +++ b/loginserver/client_manager.cpp @@ -194,7 +194,7 @@ ClientManager::ClientManager() if (!m_tob_ops->LoadOpcodes(opcodes_path.c_str())) { LogError( - "ClientManager fatal error: couldn't load opcodes for Steam Latest file [{}]", + "ClientManager fatal error: couldn't load opcodes for TOB file [{}]", server.config.GetVariableString("client_configuration", "tob_opcodes", "login_opcodes.conf") ); @@ -204,7 +204,7 @@ ClientManager::ClientManager() m_tob_stream->OnNewConnection( [this](std::shared_ptr stream) { LogInfo( - "New Steam Latest client connection from [{}:{}]", + "New TOB client connection from [{}:{}]", long2ip(stream->GetRemoteIP()), stream->GetRemotePort() ); diff --git a/loginserver/login_types.h b/loginserver/login_types.h index 12a20baf0..aca9de12a 100644 --- a/loginserver/login_types.h +++ b/loginserver/login_types.h @@ -20,6 +20,7 @@ #include "common/types.h" #include +#include #pragma pack(push) #pragma pack(1) @@ -45,7 +46,7 @@ struct LoginHandShakeReply { }; // variable length, can use directly if not serializing strings -struct PlayerLoginReply { +struct PlayerLoginReplyOld { // base header excluded to make struct data easier to encrypt //LoginBaseMessage base_header; LoginBaseReplyMessage base_reply; @@ -65,21 +66,80 @@ struct PlayerLoginReply { int32_t web_offer_cooldown_minutes; // minimum minutes between offers (based on last offer time in client eqls ini) (default: 0) char username[1]; // variable length, if not empty client attempts to re-login to server select when quitting from char select and sends this in a struct char unknown[1]; // variable length, password unlikely? client doesn't send this on re-login from char select -}; -struct PlayerLoginReplyTOB -{ + void set_success(bool v) { base_reply.success = v; } + void set_error_code(int32_t v) { base_reply.error_str_id = v; } + void set_lsid(int32_t v) { lsid = v; } + void set_show_player_count(bool v) { show_player_count = v; } + void set_hardcoded_success_values() { + offer_min_days = 99; + offer_min_views = -1; + web_offer_min_days = 99; + web_offer_min_views = -1; + } +}; +static_assert(sizeof(PlayerLoginReplyOld) == 58, "PlayerLoginReplyOld struct size does not match expected size"); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_standard_layout_v); + +struct PlayerLoginReplyTOB { LoginBaseReplyMessage base_reply; - int8_t unk1; // (default: 0) - int8_t unk2; // (default: 0) - int32_t lsid; // (default: -1) - char key[11]; // client reads until null (variable length) - int32_t failed_attempts; - bool show_player_count; // admin flag, enables admin button and shows server player counts (default: false) - int32_t unk3; // guess, needs more investigation (default: 0) - int32_t unk4; // guess, needs more investigation (default: 0) - char username[1]; // variable length, if not empty client attempts to re-login to server select when quitting from char select and sends this in a struct - char unknown[1]; // variable length, password unlikely? client doesn't send this on re-login from char select + + int8_t unk1 = 0; + int8_t unk2 = 0; + int32_t lsid = -1; + char key[11] = {}; + int32_t failed_attempts = 0; + int32_t display_error_str_id = 0; + int32_t unk3 = 0; + bool show_player_count = false; + char username[1] = {}; + char unk4[1] = {}; + + void set_success(bool v) { base_reply.success = v; } + void set_error_code(int32_t v) { display_error_str_id = v; base_reply.error_str_id = v; } + void set_lsid(int32_t v) { lsid = v; } + void set_show_player_count(bool v) { show_player_count = v; } + void set_hardcoded_success_values() {} +}; +static_assert(sizeof(PlayerLoginReplyTOB) == 38, "PlayerLoginReplyTOB struct size does not match expected size"); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_standard_layout_v); + +class PlayerLoginReply { + std::variant v_; +public: + PlayerLoginReply(PlayerLoginReplyOld s) : v_(s) {} + PlayerLoginReply(PlayerLoginReplyTOB s) : v_(s) {} + + void set_success(bool val) { + std::visit([val](auto& s) { s.set_success(val); }, v_); + } + void set_error_code(int32_t val) { + std::visit([val](auto& s) { s.set_error_code(val); }, v_); + } + void set_lsid(int32_t val) { + std::visit([val](auto& s) { s.set_lsid(val); }, v_); + } + void set_show_player_count(bool val) { + std::visit([val](auto& s) { s.set_show_player_count(val); }, v_); + } + void set_key(std::string_view s) { + std::visit([&](auto& st) { + const size_t n = s.copy(st.key, sizeof(st.key) - 1); + st.key[n] = '\0'; + }, v_); + } + + PlayerLoginReplyOld& old() { return std::get(v_); } + const PlayerLoginReplyOld& old() const { return std::get(v_); } + + char* data() noexcept { + return std::visit([](auto& s) { return reinterpret_cast(&s); }, v_); + } + size_t size() const noexcept { + return std::visit([](auto const& s) { return sizeof(s); }, v_); + } }; // variable length, for reference @@ -195,11 +255,14 @@ namespace LS { namespace ErrStr { constexpr static int ERROR_NONE = 101; // No Error constexpr static int ERROR_UNKNOWN = 102; // Error - Unknown Error Occurred + constexpr static int ERROR_INVALID_CREDS = 105; // Error - Invalid Account Name or Password constexpr static int ERROR_ACTIVE_CHARACTER = 111; // Error 1018: You currently have an active character on that EverQuest Server, please allow a minute for synchronization and try again. + constexpr static int ERROR_PASSWORD_RESET = 112; // Require password reset constexpr static int ERROR_SERVER_UNAVAILABLE = 326; // That server is currently unavailable. Please check the EverQuest webpage for current server status and try again later. constexpr static int ERROR_ACCOUNT_SUSPENDED = 337; // This account is currently suspended. Please contact customer service for more information. constexpr static int ERROR_ACCOUNT_BANNED = 338; // This account is currently banned. Please contact customer service for more information. constexpr static int ERROR_WORLD_MAX_CAPACITY = 339; // The world server is currently at maximum capacity and not allowing further logins until the number of players online decreases. Please try again later. + constexpr static int ERROR_REQUIRE_2FA = 342; // This account requires two-factor authentication. }; }