[Warden] Support of several client builds
This commit is contained in:
parent
f2329704f5
commit
505ef87c12
@ -97,7 +97,7 @@ bool WorldSessionFilter::Process(WorldPacket* packet)
|
|||||||
/// WorldSession constructor
|
/// WorldSession constructor
|
||||||
WorldSession::WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, time_t mute_time, LocaleConstant locale) :
|
WorldSession::WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, time_t mute_time, LocaleConstant locale) :
|
||||||
m_muteTime(mute_time),
|
m_muteTime(mute_time),
|
||||||
_player(NULL), m_Socket(sock), _security(sec), _accountId(id), _warden(NULL), _logoutTime(0),
|
_player(NULL), m_Socket(sock), _security(sec), _accountId(id), _warden(NULL), _build(0), _logoutTime(0),
|
||||||
m_inQueue(false), m_playerLoading(false), m_playerLogout(false), m_playerRecentlyLogout(false), m_playerSave(false),
|
m_inQueue(false), m_playerLoading(false), m_playerLogout(false), m_playerRecentlyLogout(false), m_playerSave(false),
|
||||||
m_sessionDbcLocale(sWorld.GetAvailableDbcLocale(locale)), m_sessionDbLocaleIndex(sObjectMgr.GetIndexForLocale(locale)),
|
m_sessionDbcLocale(sWorld.GetAvailableDbcLocale(locale)), m_sessionDbLocaleIndex(sObjectMgr.GetIndexForLocale(locale)),
|
||||||
m_latency(0), m_tutorialState(TUTORIALDATA_UNCHANGED)
|
m_latency(0), m_tutorialState(TUTORIALDATA_UNCHANGED)
|
||||||
@ -823,8 +823,10 @@ void WorldSession::SendPlaySpellVisual(ObjectGuid guid, uint32 spellArtKit)
|
|||||||
SendPacket(&data);
|
SendPacket(&data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldSession::InitWarden(BigNumber* k, std::string const& os)
|
void WorldSession::InitWarden(uint16 build, BigNumber* k, std::string const& os)
|
||||||
{
|
{
|
||||||
|
_build = build;
|
||||||
|
|
||||||
if (os == "Win" && sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED))
|
if (os == "Win" && sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED))
|
||||||
{
|
{
|
||||||
_warden = new WardenWin();
|
_warden = new WardenWin();
|
||||||
|
@ -194,7 +194,7 @@ class WorldSession
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Warden
|
// Warden
|
||||||
void InitWarden(BigNumber* k, std::string const& os);
|
void InitWarden(uint16 build, BigNumber* k, std::string const& os);
|
||||||
|
|
||||||
/// Session in auth.queue currently
|
/// Session in auth.queue currently
|
||||||
void SetInQueue(bool state)
|
void SetInQueue(bool state)
|
||||||
@ -737,6 +737,9 @@ class WorldSession
|
|||||||
void HandleBotPackets();
|
void HandleBotPackets();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// for Warden
|
||||||
|
uint16 GetClientBuild() const { return _build; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// private trade methods
|
// private trade methods
|
||||||
void moveItems(Item* myItems[], Item* hisItems[]);
|
void moveItems(Item* myItems[], Item* hisItems[]);
|
||||||
@ -759,6 +762,7 @@ class WorldSession
|
|||||||
|
|
||||||
// Warden
|
// Warden
|
||||||
Warden* _warden; // Remains NULL if Warden system is not enabled by config
|
Warden* _warden; // Remains NULL if Warden system is not enabled by config
|
||||||
|
uint16 _build; // connected client build
|
||||||
|
|
||||||
time_t _logoutTime;
|
time_t _logoutTime;
|
||||||
bool m_inQueue; // session wait in auth.queue
|
bool m_inQueue; // session wait in auth.queue
|
||||||
|
@ -864,7 +864,7 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket)
|
|||||||
|
|
||||||
// Initialize Warden system only if it is enabled by config
|
// Initialize Warden system only if it is enabled by config
|
||||||
if (wardenActive)
|
if (wardenActive)
|
||||||
m_Session->InitWarden(&K, os);
|
m_Session->InitWarden(uint16(BuiltNumberClient), &K, os);
|
||||||
|
|
||||||
sWorld.AddSession(m_Session);
|
sWorld.AddSession(m_Session);
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ std::string Warden::Penalty(WardenCheck* check /*= NULL*/)
|
|||||||
banReason << "Warden Anticheat Violation";
|
banReason << "Warden Anticheat Violation";
|
||||||
// Check can be NULL, for example if the client sent a wrong signature in the warden packet (CHECKSUM FAIL)
|
// Check can be NULL, for example if the client sent a wrong signature in the warden packet (CHECKSUM FAIL)
|
||||||
if (check)
|
if (check)
|
||||||
banReason << ": " << check->Comment << " (CheckId: " << check->CheckId << ")";
|
banReason << ": " << (check->Comment.empty() ? std::string("Undocumented Check") : check->Comment) << " (CheckId: " << check->CheckId << ")";
|
||||||
|
|
||||||
sWorld.BanAccount(BAN_ACCOUNT, accountName, sWorld.getConfig(CONFIG_UINT32_WARDEN_CLIENT_BAN_DURATION), banReason.str(), "Warden");
|
sWorld.BanAccount(BAN_ACCOUNT, accountName, sWorld.getConfig(CONFIG_UINT32_WARDEN_CLIENT_BAN_DURATION), banReason.str(), "Warden");
|
||||||
|
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
#include "WardenCheckMgr.h"
|
#include "WardenCheckMgr.h"
|
||||||
#include "Database/DatabaseEnv.h"
|
#include "Database/DatabaseEnv.h"
|
||||||
|
|
||||||
|
// the default client version with info in warden_checks; for other version checks, see warden_build_specific
|
||||||
|
#define DEFAULT_CLIENT_BUILD 5875
|
||||||
|
|
||||||
enum WardenOpcodes
|
enum WardenOpcodes
|
||||||
{
|
{
|
||||||
// Client->Server
|
// Client->Server
|
||||||
|
@ -66,6 +66,7 @@ void WardenCheckMgr::LoadWardenChecks()
|
|||||||
|
|
||||||
CheckStore.resize(maxCheckId + 1);
|
CheckStore.resize(maxCheckId + 1);
|
||||||
|
|
||||||
|
delete result;
|
||||||
// 0 1 2 3 4 5 6 7
|
// 0 1 2 3 4 5 6 7
|
||||||
result = WorldDatabase.Query("SELECT id, type, data, result, address, length, str, comment FROM warden_checks ORDER BY id ASC");
|
result = WorldDatabase.Query("SELECT id, type, data, result, address, length, str, comment FROM warden_checks ORDER BY id ASC");
|
||||||
|
|
||||||
@ -147,7 +148,7 @@ void WardenCheckMgr::LoadWardenChecks()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (comment.empty())
|
if (comment.empty())
|
||||||
wardenCheck->Comment = "Undocumented Check";
|
wardenCheck->Comment = "";
|
||||||
else
|
else
|
||||||
wardenCheck->Comment = comment;
|
wardenCheck->Comment = comment;
|
||||||
|
|
||||||
@ -155,6 +156,81 @@ void WardenCheckMgr::LoadWardenChecks()
|
|||||||
} while (result->NextRow());
|
} while (result->NextRow());
|
||||||
|
|
||||||
sLog.outWarden(">> Loaded %u warden checks.", count);
|
sLog.outWarden(">> Loaded %u warden checks.", count);
|
||||||
|
|
||||||
|
delete result;
|
||||||
|
// 0 1 2 3 4 5 6
|
||||||
|
result = WorldDatabase.Query("SELECT id, build, data, result, address, length, str FROM warden_build_specific ORDER BY id ASC");
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
sLog.outString("[Warden]: >> Loaded 0 warden client build-specific data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
fields = result->Fetch();
|
||||||
|
|
||||||
|
uint16 id = fields[0].GetUInt16();
|
||||||
|
if (id >= CheckStore.size())
|
||||||
|
{
|
||||||
|
sLog.outWarden("ERROR: Build-specific, check is missing in warden_checks, skipping id %u.", id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16 build = fields[1].GetUInt16();
|
||||||
|
if (build == DEFAULT_CLIENT_BUILD)
|
||||||
|
{
|
||||||
|
sLog.outWarden("ERROR: Build-specific table may not contain checks for default %u build, skipping id %u.", DEFAULT_CLIENT_BUILD, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//std::string data = fields[2].GetString(); //unused for now
|
||||||
|
std::string checkResult = fields[3].GetString();
|
||||||
|
uint32 address = fields[4].GetUInt32();
|
||||||
|
uint8 length = fields[5].GetUInt8();
|
||||||
|
std::string str = fields[6].GetString();
|
||||||
|
|
||||||
|
WardenCheck* wardenCheck = new WardenCheck();
|
||||||
|
wardenCheck->CheckId = id;
|
||||||
|
wardenCheck->Type = CheckStore[id]->Type;
|
||||||
|
|
||||||
|
WardenCheckResult* wr = new WardenCheckResult();
|
||||||
|
switch (wardenCheck->Type)
|
||||||
|
{
|
||||||
|
case MEM_CHECK:
|
||||||
|
wardenCheck->Address = address;
|
||||||
|
wardenCheck->Length = length;
|
||||||
|
wardenCheck->Str = str;
|
||||||
|
wr->Result.SetHexStr(checkResult.c_str());
|
||||||
|
{
|
||||||
|
int len = checkResult.size() / 2;
|
||||||
|
if (wr->Result.GetNumBytes() < len)
|
||||||
|
{
|
||||||
|
uint8 *temp = new uint8[len];
|
||||||
|
memset(temp, 0, len);
|
||||||
|
memcpy(temp, wr->Result.AsByteArray(), wr->Result.GetNumBytes());
|
||||||
|
std::reverse(temp, temp + len);
|
||||||
|
wr->Result.SetBinary((uint8*)temp, len);
|
||||||
|
delete[] temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sLog.outWarden("The check type %u is considered as build-independent, skipping id %u.", wardenCheck->Type, id);
|
||||||
|
delete wr;
|
||||||
|
delete wardenCheck;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MCheckStore[ComposeMultiCheckKey(build, id)] = wardenCheck;
|
||||||
|
MCheckResultStore[ComposeMultiCheckKey(build, id)] = wr;
|
||||||
|
++count;
|
||||||
|
|
||||||
|
} while (result->NextRow());
|
||||||
|
|
||||||
|
sLog.outString(">> Loaded %u warden client build-specific check overrides.", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WardenCheckMgr::LoadWardenOverrides()
|
void WardenCheckMgr::LoadWardenOverrides()
|
||||||
@ -203,18 +279,34 @@ void WardenCheckMgr::LoadWardenOverrides()
|
|||||||
sLog.outWarden(">> Loaded %u warden action overrides.", count);
|
sLog.outWarden(">> Loaded %u warden action overrides.", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
WardenCheck* WardenCheckMgr::GetWardenDataById(uint16 Id)
|
WardenCheck* WardenCheckMgr::GetWardenDataById(uint16 build, uint16 Id)
|
||||||
{
|
{
|
||||||
if (Id < CheckStore.size())
|
if (Id < CheckStore.size())
|
||||||
|
{
|
||||||
|
if (build != DEFAULT_CLIENT_BUILD)
|
||||||
|
{
|
||||||
|
MultiCheckContainer::const_iterator it = MCheckStore.find(ComposeMultiCheckKey(build, Id));
|
||||||
|
if (it != MCheckStore.end())
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
return CheckStore[Id];
|
return CheckStore[Id];
|
||||||
|
}
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
WardenCheckResult* WardenCheckMgr::GetWardenResultById(uint16 Id)
|
WardenCheckResult* WardenCheckMgr::GetWardenResultById(uint16 build, uint16 Id)
|
||||||
{
|
{
|
||||||
|
if (build != DEFAULT_CLIENT_BUILD)
|
||||||
|
{
|
||||||
|
MultiResultContainer::const_iterator it = MCheckResultStore.find(ComposeMultiCheckKey(build, Id));
|
||||||
|
if (it != MCheckResultStore.end())
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
CheckResultContainer::const_iterator itr = CheckResultStore.find(Id);
|
CheckResultContainer::const_iterator itr = CheckResultStore.find(Id);
|
||||||
if (itr != CheckResultStore.end())
|
if (itr != CheckResultStore.end())
|
||||||
return itr->second;
|
return itr->second;
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -66,12 +66,8 @@ class WardenCheckMgr
|
|||||||
return &instance;
|
return &instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a linear key without any gaps, so we use vector for fast access
|
WardenCheck* GetWardenDataById(uint16 build, uint16 Id);
|
||||||
typedef std::vector<WardenCheck*> CheckContainer;
|
WardenCheckResult* GetWardenResultById(uint16 build, uint16 Id);
|
||||||
typedef std::map<uint32, WardenCheckResult*> CheckResultContainer;
|
|
||||||
|
|
||||||
WardenCheck* GetWardenDataById(uint16 Id);
|
|
||||||
WardenCheckResult* GetWardenResultById(uint16 Id);
|
|
||||||
|
|
||||||
std::vector<uint16> MemChecksIdPool;
|
std::vector<uint16> MemChecksIdPool;
|
||||||
std::vector<uint16> OtherChecksIdPool;
|
std::vector<uint16> OtherChecksIdPool;
|
||||||
@ -82,8 +78,21 @@ class WardenCheckMgr
|
|||||||
ACE_RW_Mutex _checkStoreLock;
|
ACE_RW_Mutex _checkStoreLock;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
uint32 inline ComposeMultiCheckKey(uint16 clientBuild, uint16 checkID) { return (uint32(checkID) << 16) | clientBuild; }
|
||||||
|
|
||||||
|
// We have a linear key without any gaps, so we use vector for fast access
|
||||||
|
typedef std::vector<WardenCheck*> CheckContainer;
|
||||||
|
typedef std::map<uint32, WardenCheckResult*> CheckResultContainer;
|
||||||
|
|
||||||
CheckContainer CheckStore;
|
CheckContainer CheckStore;
|
||||||
CheckResultContainer CheckResultStore;
|
CheckResultContainer CheckResultStore;
|
||||||
|
|
||||||
|
// here we have just few checks, vector is not appropriate; key is from ComposeMultiCheckKey
|
||||||
|
typedef std::map<uint32 /*MultiCheckKey*/, WardenCheck*> MultiCheckContainer;
|
||||||
|
typedef std::map<uint32 /*MultiCheckKey*/, WardenCheckResult*> MultiResultContainer;
|
||||||
|
|
||||||
|
MultiCheckContainer MCheckStore;
|
||||||
|
MultiResultContainer MCheckResultStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define sWardenCheckMgr WardenCheckMgr::instance()
|
#define sWardenCheckMgr WardenCheckMgr::instance()
|
||||||
|
@ -61,7 +61,7 @@ void WardenMac::Init(WorldSession* pClient, BigNumber* K)
|
|||||||
|
|
||||||
_inputCrypto.Init(_inputKey);
|
_inputCrypto.Init(_inputKey);
|
||||||
_outputCrypto.Init(_outputKey);
|
_outputCrypto.Init(_outputKey);
|
||||||
sLog.outWarden("Server side warden for client %u initializing...", pClient->GetAccountId());
|
sLog.outWarden("Server side Mac warden for client %u (build %u) initializing...", pClient->GetAccountId(), _session->GetClientBuild());
|
||||||
sLog.outWarden("C->S Key: %s", ByteArrayToHexStr(_inputKey, 16).c_str());
|
sLog.outWarden("C->S Key: %s", ByteArrayToHexStr(_inputKey, 16).c_str());
|
||||||
sLog.outWarden("S->C Key: %s", ByteArrayToHexStr(_outputKey, 16).c_str());
|
sLog.outWarden("S->C Key: %s", ByteArrayToHexStr(_outputKey, 16).c_str());
|
||||||
sLog.outWarden(" Seed: %s", ByteArrayToHexStr(_seed, 16).c_str());
|
sLog.outWarden(" Seed: %s", ByteArrayToHexStr(_seed, 16).c_str());
|
||||||
|
@ -56,7 +56,7 @@ void WardenWin::Init(WorldSession* session, BigNumber* k)
|
|||||||
|
|
||||||
_inputCrypto.Init(_inputKey);
|
_inputCrypto.Init(_inputKey);
|
||||||
_outputCrypto.Init(_outputKey);
|
_outputCrypto.Init(_outputKey);
|
||||||
sLog.outWarden("Server side warden for client %u initializing...", session->GetAccountId());
|
sLog.outWarden("Server side warden for client %u (build %u) initializing...", session->GetAccountId(), _session->GetClientBuild());
|
||||||
sLog.outWarden("C->S Key: %s", ByteArrayToHexStr(_inputKey, 16).c_str());
|
sLog.outWarden("C->S Key: %s", ByteArrayToHexStr(_inputKey, 16).c_str());
|
||||||
sLog.outWarden("S->C Key: %s", ByteArrayToHexStr(_outputKey, 16).c_str());
|
sLog.outWarden("S->C Key: %s", ByteArrayToHexStr(_outputKey, 16).c_str());
|
||||||
sLog.outWarden(" Seed: %s", ByteArrayToHexStr(_seed, 16).c_str());
|
sLog.outWarden(" Seed: %s", ByteArrayToHexStr(_seed, 16).c_str());
|
||||||
@ -210,7 +210,7 @@ void WardenWin::RequestData()
|
|||||||
// Add the id to the list sent in this cycle
|
// Add the id to the list sent in this cycle
|
||||||
_currentChecks.push_back(id);
|
_currentChecks.push_back(id);
|
||||||
|
|
||||||
wd = sWardenCheckMgr->GetWardenDataById(id);
|
wd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), id);
|
||||||
|
|
||||||
switch (wd->Type)
|
switch (wd->Type)
|
||||||
{
|
{
|
||||||
@ -235,7 +235,7 @@ void WardenWin::RequestData()
|
|||||||
|
|
||||||
for (std::list<uint16>::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr)
|
for (std::list<uint16>::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr)
|
||||||
{
|
{
|
||||||
wd = sWardenCheckMgr->GetWardenDataById(*itr);
|
wd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr);
|
||||||
|
|
||||||
type = wd->Type;
|
type = wd->Type;
|
||||||
buff << uint8(type ^ xorByte);
|
buff << uint8(type ^ xorByte);
|
||||||
@ -360,8 +360,8 @@ void WardenWin::HandleData(ByteBuffer &buff)
|
|||||||
|
|
||||||
for (std::list<uint16>::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr)
|
for (std::list<uint16>::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr)
|
||||||
{
|
{
|
||||||
rd = sWardenCheckMgr->GetWardenDataById(*itr);
|
rd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr);
|
||||||
rs = sWardenCheckMgr->GetWardenResultById(*itr);
|
rs = sWardenCheckMgr->GetWardenResultById(_session->GetClientBuild(), *itr);
|
||||||
|
|
||||||
type = rd->Type;
|
type = rd->Type;
|
||||||
switch (type)
|
switch (type)
|
||||||
@ -475,7 +475,7 @@ void WardenWin::HandleData(ByteBuffer &buff)
|
|||||||
|
|
||||||
if (checkFailed > 0)
|
if (checkFailed > 0)
|
||||||
{
|
{
|
||||||
WardenCheck* check = sWardenCheckMgr->GetWardenDataById(checkFailed); //note it IS NOT NULL here
|
WardenCheck* check = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), checkFailed); //note it IS NOT NULL here
|
||||||
sLog.outWarden("%s failed Warden check %u. Action: %s", _session->GetPlayerName(), checkFailed, Penalty(check).c_str());
|
sLog.outWarden("%s failed Warden check %u. Action: %s", _session->GetPlayerName(), checkFailed, Penalty(check).c_str());
|
||||||
LogPositiveToDB(check);
|
LogPositiveToDB(check);
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ struct WardenInitModuleRequest
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
class WorldSession;
|
class WorldSession;
|
||||||
class Warden;
|
//class Warden;
|
||||||
|
|
||||||
class WardenWin : public Warden
|
class WardenWin : public Warden
|
||||||
{
|
{
|
||||||
|
@ -570,7 +570,7 @@ void Master::clearOnlineAccounts()
|
|||||||
{
|
{
|
||||||
// Cleanup online status for characters hosted at current realm
|
// Cleanup online status for characters hosted at current realm
|
||||||
/// \todo Only accounts with characters logged on *this* realm should have online status reset. Move the online column from 'account' to 'realmcharacters'?
|
/// \todo Only accounts with characters logged on *this* realm should have online status reset. Move the online column from 'account' to 'realmcharacters'?
|
||||||
LoginDatabase.PExecute("UPDATE account SET active_realm_id = 0 WHERE active_realm_id = '%u'", realmID);
|
LoginDatabase.PExecute("UPDATE account SET active_realm_id = 0, os = '' WHERE active_realm_id = '%u'", realmID);
|
||||||
|
|
||||||
CharacterDatabase.Execute("UPDATE characters SET online = 0 WHERE online<>0");
|
CharacterDatabase.Execute("UPDATE characters SET online = 0 WHERE online<>0");
|
||||||
|
|
||||||
|
@ -26,6 +26,6 @@
|
|||||||
#define MANGOS_H_REVISION
|
#define MANGOS_H_REVISION
|
||||||
#define REVISION_NR "21000"
|
#define REVISION_NR "21000"
|
||||||
#define REVISION_DB_CHARACTERS "required_21000_01_warden_action"
|
#define REVISION_DB_CHARACTERS "required_21000_01_warden_action"
|
||||||
#define REVISION_DB_MANGOS "required_21000_08_warden_checks"
|
#define REVISION_DB_MANGOS "required_21000_10_warden_multiversion"
|
||||||
#define REVISION_DB_REALMD "required_20150420_warden_db_log"
|
#define REVISION_DB_REALMD "required_20150420_warden_db_log"
|
||||||
#endif // __REVISION_H__
|
#endif // __REVISION_H__
|
||||||
|
Loading…
x
Reference in New Issue
Block a user