Fix world server authentication for AzerothCore

- Use 6-byte CMSG header (uint16 size + uint32 opcode) for client-to-server
- Enable RC4 encryption immediately after sending AUTH_SESSION
- Encrypt all 6 header bytes to match AzerothCore expectations
- Add 8-byte addon info (addonCount + clientTime) for proper parsing
- Fix CharCreateResult enum to use correct WoW 3.3.5a response codes
- Add CHAR_NAME_* error codes for name validation messages
- Allow character list refresh from CHAR_LIST_RECEIVED state
This commit is contained in:
Kelsi 2026-02-05 21:03:11 -08:00
parent b2a1fbc42c
commit 82a44659b4
4 changed files with 272 additions and 76 deletions

View file

@ -1115,16 +1115,17 @@ void GameHandler::sendAuthSession() {
LOG_DEBUG("CMSG_AUTH_SESSION packet size: ", packet.getSize(), " bytes");
// Send packet (NOT encrypted yet)
// Send packet (unencrypted - this is the last unencrypted packet)
socket->send(packet);
// CRITICAL: Initialize encryption AFTER sending AUTH_SESSION
// but BEFORE receiving AUTH_RESPONSE
LOG_INFO("Initializing RC4 header encryption...");
// Enable encryption IMMEDIATELY after sending AUTH_SESSION
// AzerothCore enables encryption before sending AUTH_RESPONSE,
// so we need to be ready to decrypt the response
LOG_INFO("Enabling encryption immediately after AUTH_SESSION");
socket->initEncryption(sessionKey);
setState(WorldState::AUTH_SENT);
LOG_INFO("CMSG_AUTH_SESSION sent, encryption initialized, waiting for response...");
LOG_INFO("CMSG_AUTH_SESSION sent, encryption enabled, waiting for AUTH_RESPONSE...");
}
void GameHandler::handleAuthResponse(network::Packet& packet) {
@ -1143,7 +1144,9 @@ void GameHandler::handleAuthResponse(network::Packet& packet) {
return;
}
// Authentication successful!
// Encryption was already enabled after sending AUTH_SESSION
LOG_INFO("AUTH_RESPONSE OK - world authentication successful");
setState(WorldState::AUTHENTICATED);
LOG_INFO("========================================");
@ -1154,6 +1157,9 @@ void GameHandler::handleAuthResponse(network::Packet& packet) {
setState(WorldState::READY);
// Request character list automatically
requestCharacterList();
// Call success callback
if (onSuccess) {
onSuccess();
@ -1166,7 +1172,8 @@ void GameHandler::requestCharacterList() {
setState(WorldState::CHAR_LIST_RECEIVED);
return;
}
if (state != WorldState::READY && state != WorldState::AUTHENTICATED) {
if (state != WorldState::READY && state != WorldState::AUTHENTICATED &&
state != WorldState::CHAR_LIST_RECEIVED) {
LOG_WARNING("Cannot request character list in state: ", (int)state);
return;
}
@ -1335,13 +1342,37 @@ void GameHandler::handleCharCreateResponse(network::Packet& packet) {
} else {
std::string msg;
switch (data.result) {
case CharCreateResult::ERROR: msg = "Server error"; break;
case CharCreateResult::FAILED: msg = "Creation failed"; break;
case CharCreateResult::NAME_IN_USE: msg = "Name already in use"; break;
case CharCreateResult::DISABLED: msg = "Character creation disabled"; break;
case CharCreateResult::PVP_TEAMS_VIOLATION: msg = "PvP faction violation"; break;
case CharCreateResult::SERVER_LIMIT: msg = "Server character limit reached"; break;
case CharCreateResult::ACCOUNT_LIMIT: msg = "Account character limit reached"; break;
default: msg = "Character creation failed"; break;
case CharCreateResult::SERVER_QUEUE: msg = "Server is queued"; break;
case CharCreateResult::ONLY_EXISTING: msg = "Only existing characters allowed"; break;
case CharCreateResult::EXPANSION: msg = "Expansion required"; break;
case CharCreateResult::EXPANSION_CLASS: msg = "Expansion required for this class"; break;
case CharCreateResult::LEVEL_REQUIREMENT: msg = "Level requirement not met"; break;
case CharCreateResult::UNIQUE_CLASS_LIMIT: msg = "Unique class limit reached"; break;
case CharCreateResult::RESTRICTED_RACECLASS: msg = "Race/class combination not allowed"; break;
// Name validation errors
case CharCreateResult::NAME_FAILURE: msg = "Invalid name"; break;
case CharCreateResult::NAME_NO_NAME: msg = "Please enter a name"; break;
case CharCreateResult::NAME_TOO_SHORT: msg = "Name is too short"; break;
case CharCreateResult::NAME_TOO_LONG: msg = "Name is too long"; break;
case CharCreateResult::NAME_INVALID_CHARACTER: msg = "Name contains invalid characters"; break;
case CharCreateResult::NAME_MIXED_LANGUAGES: msg = "Name mixes languages"; break;
case CharCreateResult::NAME_PROFANE: msg = "Name contains profanity"; break;
case CharCreateResult::NAME_RESERVED: msg = "Name is reserved"; break;
case CharCreateResult::NAME_INVALID_APOSTROPHE: msg = "Invalid apostrophe in name"; break;
case CharCreateResult::NAME_MULTIPLE_APOSTROPHES: msg = "Name has multiple apostrophes"; break;
case CharCreateResult::NAME_THREE_CONSECUTIVE: msg = "Name has 3+ consecutive same letters"; break;
case CharCreateResult::NAME_INVALID_SPACE: msg = "Invalid space in name"; break;
case CharCreateResult::NAME_CONSECUTIVE_SPACES: msg = "Name has consecutive spaces"; break;
default: msg = "Unknown error (code " + std::to_string(static_cast<int>(data.result)) + ")"; break;
}
LOG_WARNING("Character creation failed: ", msg);
LOG_WARNING("Character creation failed: ", msg, " (code=", static_cast<int>(data.result), ")");
if (charCreateCallback_) {
charCreateCallback_(false, msg);
}

View file

@ -6,6 +6,7 @@
#include <algorithm>
#include <cctype>
#include <cstring>
#include <zlib.h>
namespace wowee {
namespace game {
@ -14,7 +15,8 @@ network::Packet AuthSessionPacket::build(uint32_t build,
const std::string& accountName,
uint32_t clientSeed,
const std::vector<uint8_t>& sessionKey,
uint32_t serverSeed) {
uint32_t serverSeed,
uint32_t realmId) {
if (sessionKey.size() != 40) {
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
}
@ -37,34 +39,80 @@ network::Packet AuthSessionPacket::build(uint32_t build,
// Create packet (opcode will be added by WorldSocket)
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTH_SESSION));
// AzerothCore 3.3.5a expects this exact field order:
// Build, LoginServerID, Account, LoginServerType, LocalChallenge,
// RegionID, BattlegroupID, RealmID, DosResponse, Digest, AddonInfo
// Build number (uint32, little-endian)
packet.writeUInt32(build);
// Unknown uint32 (always 0)
// Login server ID (uint32, always 0)
packet.writeUInt32(0);
// Account name (null-terminated string)
packet.writeString(upperAccount);
// Unknown uint32 (always 0)
// Login server type (uint32, always 0)
packet.writeUInt32(0);
// Client seed (uint32, little-endian)
// LocalChallenge / Client seed (uint32, little-endian)
packet.writeUInt32(clientSeed);
// Unknown fields (5x uint32, all zeros)
for (int i = 0; i < 5; ++i) {
// Region ID (uint32)
packet.writeUInt32(0);
// Battlegroup ID (uint32)
packet.writeUInt32(0);
// Realm ID (uint32)
packet.writeUInt32(realmId);
LOG_DEBUG(" Realm ID: ", realmId);
// DOS response (uint64) - required for 3.x
packet.writeUInt32(0);
packet.writeUInt32(0);
// Authentication hash/digest (20 bytes)
packet.writeBytes(authHash.data(), authHash.size());
// Addon info - compressed block with 0 addons
// AzerothCore format: uint32 decompressedSize + zlib compressed data
// Decompressed format: uint32 addonCount + [addons...] + uint32 clientTime
uint8_t addonData[8] = {
0, 0, 0, 0, // addon count = 0
0, 0, 0, 0 // client time = 0
};
uint32_t decompressedSize = 8;
// Compress with zlib
uLongf compressedSize = compressBound(decompressedSize);
std::vector<uint8_t> compressed(compressedSize);
int ret = compress(compressed.data(), &compressedSize, addonData, decompressedSize);
if (ret == Z_OK) {
compressed.resize(compressedSize);
// Write decompressedSize, then compressed bytes
packet.writeUInt32(decompressedSize);
packet.writeBytes(compressed.data(), compressed.size());
LOG_DEBUG("Addon info: decompressedSize=", decompressedSize,
" compressedSize=", compressedSize);
} else {
LOG_ERROR("zlib compress failed with code: ", ret);
packet.writeUInt32(0);
}
// Authentication hash (20 bytes)
packet.writeBytes(authHash.data(), authHash.size());
// Addon CRC (uint32, can be 0)
packet.writeUInt32(0);
LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes");
// Dump full packet for protocol debugging
const auto& data = packet.getData();
std::string hexDump;
for (size_t i = 0; i < data.size(); ++i) {
char buf[4];
snprintf(buf, sizeof(buf), "%02x ", data[i]);
hexDump += buf;
if ((i + 1) % 16 == 0) hexDump += "\n";
}
LOG_DEBUG("CMSG_AUTH_SESSION full dump:\n", hexDump);
return packet;
}
@ -220,7 +268,25 @@ network::Packet CharCreatePacket::build(const CharCreateData& data) {
packet.writeUInt8(data.facialHair);
packet.writeUInt8(0); // outfitId, always 0
LOG_DEBUG("Built CMSG_CHAR_CREATE: name=", data.name);
LOG_DEBUG("Built CMSG_CHAR_CREATE: name=", data.name,
" race=", static_cast<int>(data.race),
" class=", static_cast<int>(data.characterClass),
" gender=", static_cast<int>(data.gender),
" skin=", static_cast<int>(data.skin),
" face=", static_cast<int>(data.face),
" hair=", static_cast<int>(data.hairStyle),
" hairColor=", static_cast<int>(data.hairColor),
" facial=", static_cast<int>(data.facialHair));
// Dump full packet for protocol debugging
const auto& pktData = packet.getData();
std::string hexDump;
for (size_t i = 0; i < pktData.size(); ++i) {
char buf[4];
snprintf(buf, sizeof(buf), "%02x ", pktData[i]);
hexDump += buf;
}
LOG_DEBUG("CMSG_CHAR_CREATE full dump: ", hexDump);
return packet;
}