/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*/
#include "RealmList.h"
#include "BattlenetRpcErrorCodes.h"
#include "CryptoRandom.h"
#include "DatabaseEnv.h"
#include "DeadlineTimer.h"
#include "Log.h"
#include "MapUtils.h"
#include "ProtobufJSON.h"
#include "Resolver.h"
#include "Util.h"
#include "game_utilities_service.pb.h"
#include "RealmList.pb.h"
#include "advstd.h"
#include
#include
namespace
{
bool CompressJson(std::string const& json, std::vector* compressed)
{
uLong uncompressedLength = uLong(json.length() + 1);
uLong compressedLength = compressBound(uLong(json.length()));
compressed->resize(compressedLength + 4);
memcpy(compressed->data(), &uncompressedLength, sizeof(uncompressedLength));
if (compress(compressed->data() + 4, &compressedLength, reinterpret_cast(json.data()), uncompressedLength) != Z_OK)
{
compressed->clear();
return false;
}
compressed->resize(compressedLength + 4); // trim excess bytes
return true;
}
}
RealmList::RealmList() : _updateInterval(0)
{
}
RealmList::~RealmList() = default;
RealmList* RealmList::Instance()
{
static RealmList instance;
return &instance;
}
// Load the realm list from the database
void RealmList::Initialize(Trinity::Asio::IoContext& ioContext, uint32 updateInterval)
{
_updateInterval = updateInterval;
_updateTimer = std::make_unique(ioContext);
_resolver = std::make_unique(ioContext);
ClientBuild::LoadBuildInfo();
// Get the content of the realmlist table in the database
UpdateRealms();
}
void RealmList::Close()
{
_updateTimer->cancel();
}
void RealmList::UpdateRealm(Realm& realm, Battlenet::RealmHandle const& id, uint32 build, std::string const& name,
std::vector&& addresses,
uint16 port, uint8 icon, RealmFlags flag, uint8 timezone, AccountTypes allowedSecurityLevel,
RealmPopulationState population)
{
realm.Id = id;
realm.Build = build;
if (realm.Name != name)
realm.SetName(name);
realm.Type = icon;
realm.Flags = flag;
realm.Timezone = timezone;
realm.AllowedSecurityLevel = allowedSecurityLevel;
realm.PopulationLevel = population;
realm.Addresses = std::move(addresses);
realm.Port = port;
}
void RealmList::UpdateRealms()
{
TC_LOG_DEBUG("realmlist", "Updating Realm List...");
LoginDatabasePreparedStatement *stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_REALMLIST);
PreparedQueryResult result = LoginDatabase.Query(stmt);
std::map existingRealms;
for (auto const& p : _realms)
existingRealms[p.first] = p.second->Name;
std::unordered_set newSubRegions;
RealmMap newRealms;
// Circle through results and add them to the realm map
if (result)
{
do
{
Field* fields = result->Fetch();
uint32 realmId = fields[0].GetUInt32();
std::string name = fields[1].GetString();
std::vector addresses;
for (std::size_t i = 0; i < 4; ++i)
{
if (Optional addressStr = fields[2 + i].GetStringViewOrNull())
{
for (boost::asio::ip::tcp::endpoint const& endpoint : _resolver->ResolveAll(*addressStr, ""))
{
boost::asio::ip::address address = endpoint.address();
if (advstd::ranges::contains(addresses, address))
continue;
addresses.push_back(std::move(address));
}
}
}
if (addresses.empty())
{
TC_LOG_ERROR("realmlist", "Could not resolve any address for realm \"{}\" id {}", name, realmId);
continue;
}
uint16 port = fields[6].GetUInt16();
uint8 icon = fields[7].GetUInt8();
if (icon == REALM_TYPE_FFA_PVP)
icon = REALM_TYPE_PVP;
if (icon >= MAX_CLIENT_REALM_TYPE)
icon = REALM_TYPE_NORMAL;
RealmFlags flag = ConvertLegacyRealmFlags(Trinity::Legacy::RealmFlags(fields[8].GetUInt8()));
uint8 timezone = fields[9].GetUInt8();
uint8 allowedSecurityLevel = fields[10].GetUInt8();
RealmPopulationState pop = ConvertLegacyPopulationState(Trinity::Legacy::RealmFlags(fields[8].GetUInt8()), fields[11].GetFloat());
uint32 build = fields[12].GetUInt32();
uint8 region = fields[13].GetUInt8();
uint8 battlegroup = fields[14].GetUInt8();
Battlenet::RealmHandle id{ region, battlegroup, realmId };
UpdateRealm(*newRealms.try_emplace(id, std::make_shared()).first->second, id, build, name, std::move(addresses), port, icon,
flag, timezone, (allowedSecurityLevel <= SEC_ADMINISTRATOR ? AccountTypes(allowedSecurityLevel) : SEC_ADMINISTRATOR), pop);
newSubRegions.insert(Battlenet::RealmHandle{ region, battlegroup, 0 }.GetAddressString());
auto buildAddressesLogText = [&]
{
std::string text;
for (boost::asio::ip::address const& address : newRealms[id]->Addresses)
{
text += address.to_string();
text += ' ';
}
return text;
};
if (!existingRealms.erase(id))
TC_LOG_INFO("realmlist", "Added realm \"{}\" at {}(port {}).", name, buildAddressesLogText(), port);
else
TC_LOG_DEBUG("realmlist", "Updating realm \"{}\" at {}(port {}).", name, buildAddressesLogText(), port);
}
while (result->NextRow());
}
for (auto itr = existingRealms.begin(); itr != existingRealms.end(); ++itr)
TC_LOG_INFO("realmlist", "Removed realm \"{}\".", itr->second);
{
std::unique_lock lock(_realmsMutex);
_subRegions.swap(newSubRegions);
_realms.swap(newRealms);
_removedRealms.swap(existingRealms);
if (_currentRealmId)
if (std::shared_ptr realm = Trinity::Containers::MapGetValuePtr(_realms, *_currentRealmId))
_currentRealmId = realm->Id; // fill other fields of realm id
}
if (_updateInterval)
{
_updateTimer->expires_after(std::chrono::seconds(_updateInterval));
_updateTimer->async_wait([this](boost::system::error_code const& error)
{
if (error)
return;
UpdateRealms();
});
}
}
std::shared_ptr RealmList::GetRealm(Battlenet::RealmHandle const& id) const
{
std::shared_lock lock(_realmsMutex);
return Trinity::Containers::MapGetValuePtr(_realms, id);
}
Battlenet::RealmHandle RealmList::GetCurrentRealmId() const
{
return _currentRealmId ? *_currentRealmId : Battlenet::RealmHandle();
}
void RealmList::SetCurrentRealmId(Battlenet::RealmHandle const& id)
{
_currentRealmId = id;
}
std::shared_ptr RealmList::GetCurrentRealm() const
{
if (_currentRealmId)
return GetRealm(*_currentRealmId);
return nullptr;
}
void RealmList::WriteSubRegions(bgs::protocol::game_utilities::v1::GetAllValuesForAttributeResponse* response) const
{
std::shared_lock lock(_realmsMutex);
for (std::string const& subRegion : _subRegions)
response->add_attribute_value()->set_string_value(subRegion);
}
void RealmList::FillRealmEntry(Realm const& realm, uint32 clientBuild, AccountTypes accountSecurityLevel, JSON::RealmList::RealmEntry* realmEntry) const
{
realmEntry->set_wowrealmaddress(realm.Id.GetAddress());
realmEntry->set_cfgtimezonesid(1);
if (accountSecurityLevel >= realm.AllowedSecurityLevel || realm.PopulationLevel == RealmPopulationState::Offline)
realmEntry->set_populationstate(AsUnderlyingType(realm.PopulationLevel));
else
realmEntry->set_populationstate(AsUnderlyingType(RealmPopulationState::Locked));
realmEntry->set_cfgcategoriesid(realm.Timezone);
JSON::RealmList::ClientVersion* version = realmEntry->mutable_version();
if (ClientBuild::Info const* buildInfo = ClientBuild::GetBuildInfo(realm.Build))
{
version->set_versionmajor(buildInfo->MajorVersion);
version->set_versionminor(buildInfo->MinorVersion);
version->set_versionrevision(buildInfo->BugfixVersion);
version->set_versionbuild(buildInfo->Build);
}
else
{
version->set_versionmajor(6);
version->set_versionminor(2);
version->set_versionrevision(4);
version->set_versionbuild(realm.Build);
}
RealmFlags flag = realm.Flags;
if (realm.Build != clientBuild)
flag |= RealmFlags::VersionMismatch;
realmEntry->set_cfgrealmsid(realm.Id.Realm);
realmEntry->set_flags(AsUnderlyingType(flag));
realmEntry->set_name(realm.Name);
realmEntry->set_cfgconfigsid(realm.GetConfigId());
realmEntry->set_cfglanguagesid(1);
}
std::vector RealmList::GetRealmEntryJSON(Battlenet::RealmHandle const& id, uint32 build, AccountTypes accountSecurityLevel) const
{
std::vector compressed;
if (std::shared_ptr realm = GetRealm(id))
{
if (realm->PopulationLevel != RealmPopulationState::Offline && realm->Build == build && accountSecurityLevel >= realm->AllowedSecurityLevel)
{
JSON::RealmList::RealmEntry realmEntry;
FillRealmEntry(*realm, build, accountSecurityLevel, &realmEntry);
std::string json = "JamJSONRealmEntry:" + JSON::Serialize(realmEntry);
CompressJson(json, &compressed);
}
}
return compressed;
}
std::vector RealmList::GetRealmList(uint32 build, AccountTypes accountSecurityLevel, std::string const& subRegion) const
{
JSON::RealmList::RealmListUpdates realmList;
{
std::shared_lock lock(_realmsMutex);
for (auto const& [_, realm] : _realms)
{
if (realm->Id.GetSubRegionAddress() != subRegion)
continue;
JSON::RealmList::RealmListUpdatePart* state = realmList.add_updates();
FillRealmEntry(*realm, build, accountSecurityLevel, state->mutable_update());
state->set_deleting(false);
}
for (auto const& [id, _] : _removedRealms)
{
if (id.GetSubRegionAddress() != subRegion)
continue;
JSON::RealmList::RealmListUpdatePart* state = realmList.add_updates();
state->set_wowrealmaddress(id.GetAddress());
state->set_deleting(true);
}
}
std::string json = "JSONRealmListUpdates:" + JSON::Serialize(realmList);
std::vector compressed;
CompressJson(json, &compressed);
return compressed;
}
uint32 RealmList::JoinRealm(uint32 realmAddress, uint32 build, ClientBuild::VariantId const& buildVariant, boost::asio::ip::address const& clientAddress,
std::array const& clientSecret, LocaleConstant locale, std::string const& os, Minutes timezoneOffset, std::string const& accountName,
AccountTypes accountSecurityLevel, bgs::protocol::game_utilities::v1::ClientResponse* response) const
{
if (std::shared_ptr realm = GetRealm(realmAddress))
{
if (realm->PopulationLevel == RealmPopulationState::Offline || realm->Build != build || accountSecurityLevel < realm->AllowedSecurityLevel)
return ERROR_USER_SERVER_NOT_PERMITTED_ON_REALM;
boost::asio::ip::address addressForClient = realm->GetAddressForClient(clientAddress);
JSON::RealmList::RealmListServerIPAddresses serverAddresses;
JSON::RealmList::RealmIPAddressFamily* addressFamily = serverAddresses.add_families();
addressFamily->set_family(addressForClient.is_v6() ? 2 : 1);
JSON::RealmList::IPAddress* address = addressFamily->add_addresses();
address->set_ip(addressForClient.to_string());
address->set_port(realm->Port);
std::string json = "JSONRealmListServerIPAddresses:" + JSON::Serialize(serverAddresses);
std::vector compressed;
if (!CompressJson(json, &compressed))
return ERROR_UTIL_SERVER_FAILED_TO_SERIALIZE_RESPONSE;
std::array serverSecret = Trinity::Crypto::GetRandomBytes<32>();
std::array keyData;
auto keyDestItr = keyData.begin();
keyDestItr = std::ranges::copy(clientSecret, keyDestItr).out;
keyDestItr = std::ranges::copy(serverSecret, keyDestItr).out;
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_GAME_ACCOUNT_LOGIN_INFO);
stmt->setBinary(0, keyData);
stmt->setString(1, clientAddress.to_string());
stmt->setUInt32(2, build);
stmt->setUInt8(3, locale);
stmt->setString(4, os);
stmt->setInt16(5, timezoneOffset.count());
stmt->setString(6, accountName);
LoginDatabase.DirectExecute(stmt);
JSON::RealmList::RealmJoinTicket joinTicket;
joinTicket.set_gameaccount(accountName);
joinTicket.set_platform(buildVariant.Platform);
joinTicket.set_clientarch(buildVariant.Arch);
joinTicket.set_type(buildVariant.Type);
bgs::protocol::Attribute* attribute = response->add_attribute();
attribute->set_name("Param_RealmJoinTicket");
attribute->mutable_value()->set_blob_value(JSON::Serialize(joinTicket));
attribute = response->add_attribute();
attribute->set_name("Param_ServerAddresses");
attribute->mutable_value()->set_blob_value(compressed.data(), compressed.size());
attribute = response->add_attribute();
attribute->set_name("Param_JoinSecret");
attribute->mutable_value()->set_blob_value(serverSecret.data(), serverSecret.size());
return ERROR_OK;
}
return ERROR_UTIL_SERVER_UNKNOWN_REALM;
}