Files
WoWee/tests/test_realm_list.cpp
Kelsi 64ece68aa4 feat(auth): fall back to the other vanilla auth protocol on mismatch
Vanilla 1.12 servers disagree on the auth protocol byte — vmangos-derived
realms speak 8, stock mangos/cmangos speak 3 — and a profile can only name
one, so pointing the classic profile at the other kind hard-failed at login.
Classic/Turtle now retry once on the alternate byte when the failure looks
protocol-shaped (unparseable handshake, version rejection, drop during
challenge) and never when credentials were rejected, so a wrong password
can't trigger a second lockout-provoking attempt.

Also fixes the realm-list recovery heuristic from #100: it only fired when
the shifted parse left a non-empty address, so vanilla realms sending flags=0
(both name and address parse empty) silently produced garbage fields instead.
Keyed on the empty name alone, and covered by new realm-list parser tests.
2026-07-14 01:29:37 -07:00

149 lines
5.8 KiB
C++

// REALM_LIST response parsing across the vanilla / TBC-WotLK layout split.
//
// Vanilla-family servers disagree on the auth protocol byte (vmangos-derived
// 1.12 realms speak protocol 8, stock mangos/cmangos speak 3) while still
// sending the vanilla realm-entry shape, so the realm-list layout is selected
// by expansion rather than by protocol version — and the parser additionally
// recovers in-place when a modern-layout parse visibly shifts the fields.
#include <catch_amalgamated.hpp>
#include "auth/auth_packets.hpp"
#include "network/packet.hpp"
#include <cstring>
using namespace wowee::auth;
namespace network = wowee::network;
using Bytes = std::vector<uint8_t>;
namespace {
void putU8(Bytes& b, uint8_t v) { b.push_back(v); }
void putU16(Bytes& b, uint16_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); }
void putU32(Bytes& b, uint32_t v) {
b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF);
b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF);
}
void putF(Bytes& b, float v) { uint32_t u; std::memcpy(&u, &v, 4); putU32(b, u); }
void putStr(Bytes& b, const char* s) {
while (*s) b.push_back(static_cast<uint8_t>(*s++));
b.push_back(0);
}
// Header shared by both layouts: size(2) + unknown(4), then the realm count,
// whose width is what differs (uint8 vanilla, uint16 TBC/WotLK).
void putHeader(Bytes& b, uint16_t realmCount, bool legacyVanilla) {
putU16(b, 0); // packet size — parser reads and ignores it
putU32(b, 0); // unknown
if (legacyVanilla) putU8(b, static_cast<uint8_t>(realmCount));
else putU16(b, realmCount);
}
// Vanilla realm entry: uint32 icon, NO lock byte.
void putVanillaRealm(Bytes& b, const char* name, const char* addr, uint8_t id) {
putU32(b, 1); // icon (uint32 in vanilla)
putU8(b, 0x00); // flags — no version info
putStr(b, name);
putStr(b, addr);
putF(b, 0.5f); // population
putU8(b, 3); // characters
putU8(b, 1); // timezone
putU8(b, id);
}
// TBC/WotLK realm entry: uint8 icon + lock byte, optional version block.
void putModernRealm(Bytes& b, const char* name, const char* addr, uint8_t id, bool withVersion) {
putU8(b, 1); // icon
putU8(b, 0); // lock
putU8(b, withVersion ? 0x04 : 0x00); // flags (0x04 = version info follows)
putStr(b, name);
putStr(b, addr);
putF(b, 0.5f);
putU8(b, 3);
putU8(b, 1);
putU8(b, id);
if (withVersion) {
putU8(b, 3); putU8(b, 3); putU8(b, 5); // 3.3.5
putU16(b, 12340);
}
}
} // namespace
TEST_CASE("Realm list: vanilla layout parses with legacy flag", "[realm_list]") {
Bytes b;
putHeader(b, 2, /*legacyVanilla=*/true);
putVanillaRealm(b, "Vanilla Realm", "127.0.0.1:8085", 1);
putVanillaRealm(b, "Second Realm", "127.0.0.1:8086", 2);
network::Packet pkt(0, b);
RealmListResponse resp;
REQUIRE(RealmListResponseParser::parse(pkt, resp, /*legacyVanillaLayout=*/true));
REQUIRE(resp.realms.size() == 2);
CHECK(resp.realms[0].name == "Vanilla Realm");
CHECK(resp.realms[0].address == "127.0.0.1:8085");
CHECK(resp.realms[0].id == 1);
CHECK(resp.realms[1].name == "Second Realm");
CHECK(resp.realms[1].id == 2);
}
TEST_CASE("Realm list: TBC/WotLK layout parses without legacy flag", "[realm_list]") {
Bytes b;
putHeader(b, 1, /*legacyVanilla=*/false);
putModernRealm(b, "WotLK Realm", "127.0.0.1:8085", 1, /*withVersion=*/false);
network::Packet pkt(0, b);
RealmListResponse resp;
REQUIRE(RealmListResponseParser::parse(pkt, resp, /*legacyVanillaLayout=*/false));
REQUIRE(resp.realms.size() == 1);
CHECK(resp.realms[0].name == "WotLK Realm");
CHECK(resp.realms[0].address == "127.0.0.1:8085");
CHECK(resp.realms[0].id == 1);
}
TEST_CASE("Realm list: WotLK version block consumed, not leaked into next realm", "[realm_list]") {
Bytes b;
putHeader(b, 2, /*legacyVanilla=*/false);
putModernRealm(b, "Realm One", "127.0.0.1:8085", 1, /*withVersion=*/true);
putModernRealm(b, "Realm Two", "127.0.0.1:8086", 2, /*withVersion=*/false);
network::Packet pkt(0, b);
RealmListResponse resp;
REQUIRE(RealmListResponseParser::parse(pkt, resp, /*legacyVanillaLayout=*/false));
REQUIRE(resp.realms.size() == 2);
CHECK(resp.realms[0].name == "Realm One");
CHECK(resp.realms[0].hasVersionInfo());
CHECK(resp.realms[0].build == 12340);
// The second realm only parses correctly if the version block above was
// fully consumed — a stray byte shifts every field from here on.
CHECK(resp.realms[1].name == "Realm Two");
CHECK(resp.realms[1].address == "127.0.0.1:8086");
CHECK(resp.realms[1].id == 2);
}
TEST_CASE("Realm list: vanilla body under modern flag recovers in-place", "[realm_list]") {
// vmangos: auth protocol 8 (so the caller may not set the legacy flag) but
// a vanilla-shaped realm body. The modern parse mis-slices this — the extra
// icon bytes swallow the name — and the parser must detect and re-read it.
Bytes b;
putHeader(b, 1, /*legacyVanilla=*/false);
putVanillaRealm(b, "VMangos Realm", "127.0.0.1:8085", 1);
network::Packet pkt(0, b);
RealmListResponse resp;
REQUIRE(RealmListResponseParser::parse(pkt, resp, /*legacyVanillaLayout=*/false));
REQUIRE(resp.realms.size() == 1);
CHECK(resp.realms[0].name == "VMangos Realm");
CHECK(resp.realms[0].address == "127.0.0.1:8085");
CHECK(resp.realms[0].id == 1);
}
TEST_CASE("Realm list: empty realm list is not an error", "[realm_list]") {
Bytes b;
putHeader(b, 0, /*legacyVanilla=*/true);
network::Packet pkt(0, b);
RealmListResponse resp;
REQUIRE(RealmListResponseParser::parse(pkt, resp, /*legacyVanillaLayout=*/true));
CHECK(resp.realms.empty());
}