Make adding/removing realm builds easier for use in multirealm environments - original patch provided by Kudlaty
--HG-- branch : trunk
This commit is contained in:
@@ -73,6 +73,11 @@ enum LoginResult
|
||||
//3.1.3 build 9947
|
||||
//3.1.3 build 10146 Chinese build
|
||||
|
||||
#define EXPECTED_TRINITY_CLIENT_BUILD {10146, 9947, 8606, 5875, 6005, 0}
|
||||
int accepted_versions[3][3]
|
||||
{
|
||||
{5875, 6005, 0}
|
||||
{8606, 0, 0}
|
||||
{10146, 9947, 0}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -63,7 +63,7 @@ enum eStatus
|
||||
};
|
||||
|
||||
// GCC have alternative #pragma pack(N) syntax and old gcc version not support pack(push,N), also any gcc version not support it at some paltform
|
||||
#if defined( __GNUC__ )
|
||||
#if defined(__GNUC__)
|
||||
#pragma pack(1)
|
||||
#else
|
||||
#pragma pack(push,1)
|
||||
@@ -176,7 +176,7 @@ typedef struct AuthHandler
|
||||
}AuthHandler;
|
||||
|
||||
// GCC have alternative #pragma pack() syntax and old gcc version not support pack(pop), also any gcc version not support it at some paltform
|
||||
#if defined( __GNUC__ )
|
||||
#if defined(__GNUC__)
|
||||
#pragma pack()
|
||||
#else
|
||||
#pragma pack(pop)
|
||||
@@ -248,7 +248,7 @@ AuthSocket::~AuthSocket()
|
||||
{
|
||||
ACE_Guard<ACE_Thread_Mutex> g(patcherLock);
|
||||
|
||||
if(pPatch)
|
||||
if (pPatch)
|
||||
fclose(pPatch);
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ void AuthSocket::_SetVSFields(const std::string& rI)
|
||||
const char *v_hex, *s_hex;
|
||||
v_hex = v.AsHexStr();
|
||||
s_hex = s.AsHexStr();
|
||||
loginDatabase.PExecute("UPDATE account SET v = '%s', s = '%s' WHERE username = '%s'", v_hex, s_hex, _safelogin.c_str() );
|
||||
loginDatabase.PExecute("UPDATE account SET v = '%s', s = '%s' WHERE username = '%s'", v_hex, s_hex, _safelogin.c_str());
|
||||
OPENSSL_free((void*)v_hex);
|
||||
OPENSSL_free((void*)s_hex);
|
||||
}
|
||||
@@ -397,8 +397,8 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
|
||||
std::string address = GetRemoteAddress();
|
||||
loginDatabase.escape_string(address);
|
||||
QueryResult *result = loginDatabase.PQuery( "SELECT * FROM ip_banned WHERE ip = '%s'",address.c_str());
|
||||
if(result)
|
||||
QueryResult *result = loginDatabase.PQuery("SELECT * FROM ip_banned WHERE ip = '%s'",address.c_str());
|
||||
if (result)
|
||||
{
|
||||
pkt << (uint8)REALM_AUTH_ACCOUNT_BANNED;
|
||||
sLog.outBasic("[AuthChallenge] Banned ip %s tries to login!",GetRemoteAddress().c_str ());
|
||||
@@ -410,15 +410,15 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
// No SQL injection (escaped user name)
|
||||
|
||||
result = loginDatabase.PQuery("SELECT sha_pass_hash,id,locked,last_ip,gmlevel,v,s FROM account WHERE username = '%s'",_safelogin.c_str ());
|
||||
if( result )
|
||||
if (result)
|
||||
{
|
||||
///- If the IP is 'locked', check that the player comes indeed from the correct IP address
|
||||
bool locked = false;
|
||||
if((*result)[2].GetUInt8() == 1) // if ip is locked
|
||||
if ((*result)[2].GetUInt8() == 1) // if ip is locked
|
||||
{
|
||||
DEBUG_LOG("[AuthChallenge] Account '%s' is locked to IP - '%s'", _login.c_str(), (*result)[3].GetString());
|
||||
DEBUG_LOG("[AuthChallenge] Player address is '%s'", GetRemoteAddress().c_str());
|
||||
if ( strcmp((*result)[3].GetString(),GetRemoteAddress().c_str()) )
|
||||
if (strcmp((*result)[3].GetString(),GetRemoteAddress().c_str()))
|
||||
{
|
||||
DEBUG_LOG("[AuthChallenge] Account IP differs");
|
||||
pkt << (uint8) REALM_AUTH_ACCOUNT_FREEZED;
|
||||
@@ -440,9 +440,9 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
loginDatabase.Execute("UPDATE account_banned SET active = 0 WHERE unbandate<=UNIX_TIMESTAMP() AND unbandate<>bandate");
|
||||
///- If the account is banned, reject the logon attempt
|
||||
QueryResult *banresult = loginDatabase.PQuery("SELECT bandate,unbandate FROM account_banned WHERE id = %u AND active = 1", (*result)[1].GetUInt32());
|
||||
if(banresult)
|
||||
if (banresult)
|
||||
{
|
||||
if((*banresult)[0].GetUInt64() == (*banresult)[1].GetUInt64())
|
||||
if ((*banresult)[0].GetUInt64() == (*banresult)[1].GetUInt64())
|
||||
{
|
||||
pkt << (uint8) REALM_AUTH_ACCOUNT_BANNED;
|
||||
sLog.outBasic("[AuthChallenge] Banned account %s tries to login!",_login.c_str ());
|
||||
@@ -467,7 +467,7 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
sLog.outDebug("database authentication values: v='%s' s='%s'", databaseV.c_str(), databaseS.c_str());
|
||||
|
||||
// multiply with 2, bytes are stored as hexstring
|
||||
if(databaseV.size() != s_BYTE_SIZE*2 || databaseS.size() != s_BYTE_SIZE*2)
|
||||
if (databaseV.size() != s_BYTE_SIZE*2 || databaseS.size() != s_BYTE_SIZE*2)
|
||||
_SetVSFields(rI);
|
||||
else
|
||||
{
|
||||
@@ -498,13 +498,13 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
uint8 securityFlags = 0;
|
||||
pkt << uint8(securityFlags); // security flags (0x0...0x04)
|
||||
|
||||
if(securityFlags & REALM_AUTH_FAILURE) // PIN input
|
||||
if (securityFlags & REALM_AUTH_FAILURE) // PIN input
|
||||
{
|
||||
pkt << uint32(0);
|
||||
pkt << uint64(0) << uint64(0); // 16 bytes hash?
|
||||
}
|
||||
|
||||
if(securityFlags & REALM_AUTH_UNKNOWN1) // Matrix input
|
||||
if (securityFlags & REALM_AUTH_UNKNOWN1) // Matrix input
|
||||
{
|
||||
pkt << uint8(0);
|
||||
pkt << uint8(0);
|
||||
@@ -513,7 +513,7 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
pkt << uint64(0);
|
||||
}
|
||||
|
||||
if(securityFlags & REALM_AUTH_NO_MATCH) // Security token input
|
||||
if (securityFlags & REALM_AUTH_NO_MATCH) // Security token input
|
||||
{
|
||||
pkt << uint8(1);
|
||||
}
|
||||
@@ -522,7 +522,7 @@ bool AuthSocket::_HandleLogonChallenge()
|
||||
_accountSecurityLevel = secLevel <= SEC_ADMINISTRATOR ? AccountTypes(secLevel) : SEC_ADMINISTRATOR;
|
||||
|
||||
_localizationName.resize(4);
|
||||
for(int i = 0; i < 4; ++i)
|
||||
for (int i = 0; i < 4; ++i)
|
||||
_localizationName[i] = ch->country[4-i-1];
|
||||
|
||||
sLog.outBasic("[AuthChallenge] account %s is using '%c%c%c%c' locale (%u)", _login.c_str (), ch->country[3], ch->country[2], ch->country[1], ch->country[0], GetLocaleByName(_localizationName));
|
||||
@@ -550,19 +550,37 @@ bool AuthSocket::_HandleLogonProof()
|
||||
ibuf.Read((char *)&lp, sizeof(sAuthLogonProof_C));
|
||||
|
||||
///- Check if the client has one of the expected version numbers
|
||||
bool valid_version = false;
|
||||
int accepted_versions[] = EXPECTED_TRINITY_CLIENT_BUILD;
|
||||
for(int i = 0; accepted_versions[i]; ++i)
|
||||
bool valid_pre_version = false;
|
||||
bool valid_tbc_version = false;
|
||||
bool valid_wlk_version = false;
|
||||
//int accepted_versions[] = EXPECTED_TRINITY_CLIENT_BUILD;
|
||||
for (int i = 0; accepted_versions[1][i]; ++i)
|
||||
{
|
||||
if(_build == accepted_versions[i])
|
||||
if (_build == accepted_versions[1][i])
|
||||
{
|
||||
valid_version = true;
|
||||
valid_pre_version = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; accepted_versions[2][i]; ++i)
|
||||
{
|
||||
if (_build == accepted_versions[2][i])
|
||||
{
|
||||
valid_tbc_version = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; accepted_versions[3][i]; ++i)
|
||||
{
|
||||
if (_build == accepted_versions[3][i])
|
||||
{
|
||||
valid_wlk_version = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <ul><li> If the client has no valid version
|
||||
if(!valid_version)
|
||||
if (!valid_pre_version && !valid_tbc_version && !valid_wlk_version)
|
||||
{
|
||||
///- Check if we have the apropriate patch on the disk
|
||||
|
||||
@@ -573,7 +591,7 @@ bool AuthSocket::_HandleLogonProof()
|
||||
// This will be closed at the destruction of the AuthSocket (client disconnection)
|
||||
FILE *pFile = fopen(tmp, "rb");
|
||||
|
||||
if(!pFile)
|
||||
if (!pFile)
|
||||
{
|
||||
ByteBuffer pkt;
|
||||
pkt << (uint8) AUTH_LOGON_CHALLENGE;
|
||||
@@ -590,7 +608,7 @@ bool AuthSocket::_HandleLogonProof()
|
||||
XFER_INIT xferh;
|
||||
|
||||
///- Get the MD5 hash of the patch file (get it from preloaded Patcher cache or calculate it)
|
||||
if(PatchesCache.GetHash(tmp, (uint8*)&xferh.md5))
|
||||
if (PatchesCache.GetHash(tmp, (uint8*)&xferh.md5))
|
||||
{
|
||||
DEBUG_LOG("\n[AuthChallenge] Found precached patch info for patch %s", tmp);
|
||||
}
|
||||
@@ -698,7 +716,7 @@ bool AuthSocket::_HandleLogonProof()
|
||||
///- Update the sessionkey, last_ip, last login time and reset number of failed logins in the account table for this account
|
||||
// No SQL injection (escaped user name) and IP address as received by socket
|
||||
const char* K_hex = K.AsHexStr();
|
||||
loginDatabase.PExecute("UPDATE account SET sessionkey = '%s', last_ip = '%s', last_login = NOW(), locale = '%u', failed_logins = 0 WHERE username = '%s'", K_hex, GetRemoteAddress().c_str(), GetLocaleByName(_localizationName), _safelogin.c_str() );
|
||||
loginDatabase.PExecute("UPDATE account SET sessionkey = '%s', last_ip = '%s', last_login = NOW(), locale = '%u', failed_logins = 0 WHERE username = '%s'", K_hex, GetRemoteAddress().c_str(), GetLocaleByName(_localizationName), _safelogin.c_str());
|
||||
OPENSSL_free((void*)K_hex);
|
||||
|
||||
///- Finish SRP6 and send the final result to the client
|
||||
@@ -706,7 +724,7 @@ bool AuthSocket::_HandleLogonProof()
|
||||
sha.UpdateBigNumbers(&A, &M, &K, NULL);
|
||||
sha.Finalize();
|
||||
|
||||
if(_build == 8606 || _build == 9947 || _build == 10146)//2.4.3 and 3.1.3 clients (10146 is Chinese build for 3.1.3)
|
||||
if (valid_tbc_version || valid_wlk_version)//2.4.3 and 3.1.3 cliens
|
||||
{
|
||||
sAuthLogonProof_S proof;
|
||||
memcpy(proof.M2, sha.GetDigest(), 20);
|
||||
@@ -716,7 +734,9 @@ bool AuthSocket::_HandleLogonProof()
|
||||
proof.unk2 = 0x00;
|
||||
proof.unk3 = 0x00;
|
||||
SendBuf((char *)&proof, sizeof(proof));
|
||||
}else{
|
||||
}
|
||||
else if (valid_pre_version)
|
||||
{
|
||||
sAuthLogonProof_S_Old proof;
|
||||
memcpy(proof.M2, sha.GetDigest(), 20);
|
||||
proof.cmd = AUTH_LOGON_PROOF;
|
||||
@@ -737,22 +757,22 @@ bool AuthSocket::_HandleLogonProof()
|
||||
sLog.outBasic("[AuthChallenge] account %s tried to login with wrong password!",_login.c_str ());
|
||||
|
||||
uint32 MaxWrongPassCount = sConfig.GetIntDefault("WrongPass.MaxCount", 0);
|
||||
if(MaxWrongPassCount > 0)
|
||||
if (MaxWrongPassCount > 0)
|
||||
{
|
||||
//Increment number of failed logins by one and if it reaches the limit temporarily ban that account or IP
|
||||
loginDatabase.PExecute("UPDATE account SET failed_logins = failed_logins + 1 WHERE username = '%s'",_safelogin.c_str());
|
||||
|
||||
if(QueryResult *loginfail = loginDatabase.PQuery("SELECT id, failed_logins FROM account WHERE username = '%s'", _safelogin.c_str()))
|
||||
if (QueryResult *loginfail = loginDatabase.PQuery("SELECT id, failed_logins FROM account WHERE username = '%s'", _safelogin.c_str()))
|
||||
{
|
||||
Field* fields = loginfail->Fetch();
|
||||
uint32 failed_logins = fields[1].GetUInt32();
|
||||
|
||||
if( failed_logins >= MaxWrongPassCount )
|
||||
if (failed_logins >= MaxWrongPassCount)
|
||||
{
|
||||
uint32 WrongPassBanTime = sConfig.GetIntDefault("WrongPass.BanTime", 600);
|
||||
bool WrongPassBanType = sConfig.GetBoolDefault("WrongPass.BanType", false);
|
||||
|
||||
if(WrongPassBanType)
|
||||
if (WrongPassBanType)
|
||||
{
|
||||
uint32 acc_id = fields[0].GetUInt32();
|
||||
loginDatabase.PExecute("INSERT INTO account_banned VALUES ('%u',UNIX_TIMESTAMP(),UNIX_TIMESTAMP()+'%u','Trinity realmd','Failed login autoban',1)",
|
||||
@@ -891,7 +911,7 @@ bool AuthSocket::_HandleRealmList()
|
||||
// No SQL injection (escaped user name)
|
||||
|
||||
QueryResult *result = loginDatabase.PQuery("SELECT id,sha_pass_hash FROM account WHERE username = '%s'",_safelogin.c_str());
|
||||
if(!result)
|
||||
if (!result)
|
||||
{
|
||||
sLog.outError("[ERROR] user %s tried to login and we cannot find him in the database.",_login.c_str());
|
||||
SetCloseAndDelete();
|
||||
@@ -907,17 +927,31 @@ bool AuthSocket::_HandleRealmList()
|
||||
|
||||
RealmList::RealmMap::const_iterator rlm;
|
||||
RealmList built_realmList;
|
||||
for( rlm = m_realmList.begin(); rlm != m_realmList.end(); ++rlm )
|
||||
for (rlm = m_realmList.begin(); rlm != m_realmList.end(); ++rlm)
|
||||
{
|
||||
if(_build == 8606 || _build == 9947)//2.4.3 and 3.1.3 cliens
|
||||
if (valid_pre_version)//1.12.1 and 1.12.2
|
||||
{
|
||||
if(rlm->second.gamebuild == _build)
|
||||
built_realmList.AddRealm(rlm->second);
|
||||
for (int i = 0; accepted_versions[1][i]; ++i)
|
||||
{
|
||||
if (rlm->second.gamebuild == accepted_versions[1][i])
|
||||
built_realmList.AddRealm(rlm->second);
|
||||
}
|
||||
}
|
||||
else if(_build == 5875 || _build == 6005)//1.12.1 and 1.12.2 clients are compatible with eachother
|
||||
else if (valid_tbc_version)//2.4.3
|
||||
{
|
||||
if(rlm->second.gamebuild == 5875 || rlm->second.gamebuild == 6005)
|
||||
built_realmList.AddRealm(rlm->second);
|
||||
for (int i = 0; accepted_versions[2][i]; ++i)
|
||||
{
|
||||
if (rlm->second.gamebuild == accepted_versions[2][i])
|
||||
built_realmList.AddRealm(rlm->second);
|
||||
}
|
||||
}
|
||||
else if (valid_wlk_version)//3.1.3
|
||||
{
|
||||
for (int i = 0; accepted_versions[3][i]; ++i)
|
||||
{
|
||||
if (rlm->second.gamebuild == accepted_versions[3][i])
|
||||
built_realmList.AddRealm(rlm->second);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -925,18 +959,18 @@ bool AuthSocket::_HandleRealmList()
|
||||
///- Circle through realms in the RealmList and construct the return packet (including # of user characters in each realm)
|
||||
ByteBuffer pkt;
|
||||
pkt << (uint32) 0;
|
||||
if(_build == 8606 || _build == 9947)//only 2.4.3 and 3.1.3 cliens
|
||||
if (valid_tbc_version || valid_wlk_version)//only 2.4.3 and 3.1.3 cliens
|
||||
pkt << (uint16) built_realmList.size();
|
||||
else
|
||||
else if (valid_pre_version)
|
||||
pkt << (uint32) built_realmList.size();
|
||||
RealmList::RealmMap::const_iterator i;
|
||||
for( i = built_realmList.begin(); i != built_realmList.end(); ++i )
|
||||
for (i = built_realmList.begin(); i != built_realmList.end(); ++i)
|
||||
{
|
||||
uint8 AmountOfCharacters;
|
||||
|
||||
// No SQL injection. id of realm is controlled by the database.
|
||||
result = loginDatabase.PQuery( "SELECT numchars FROM realmcharacters WHERE realmid = '%d' AND acctid='%u'",i->second.m_ID,id);
|
||||
if( result )
|
||||
result = loginDatabase.PQuery("SELECT numchars FROM realmcharacters WHERE realmid = '%d' AND acctid='%u'",i->second.m_ID,id);
|
||||
if (result)
|
||||
{
|
||||
Field *fields = result->Fetch();
|
||||
AmountOfCharacters = fields[0].GetUInt8();
|
||||
@@ -948,7 +982,7 @@ bool AuthSocket::_HandleRealmList()
|
||||
uint8 lock = (i->second.allowedSecurityLevel > _accountSecurityLevel) ? 1 : 0;
|
||||
|
||||
pkt << i->second.icon; // realm type
|
||||
if(i->second.gamebuild == 9947 || i->second.gamebuild == 8606)//only 2.4.3 and 3.1.3 cliens
|
||||
if (valid_tbc_version || valid_wlk_version)// only 2.4.3 and 3.1.3
|
||||
pkt << lock; // if 1, then realm locked
|
||||
pkt << i->second.color; // if 2, then realm is offline
|
||||
pkt << i->first;
|
||||
@@ -956,17 +990,19 @@ bool AuthSocket::_HandleRealmList()
|
||||
pkt << i->second.populationLevel;
|
||||
pkt << AmountOfCharacters;
|
||||
pkt << i->second.timezone; // realm category
|
||||
if(i->second.gamebuild == 9947 || i->second.gamebuild == 8606)//2.4.3 and 3.1.3 clients
|
||||
if (valid_tbc_version || valid_wlk_version)//2.4.3 and 3.1.3 clients
|
||||
pkt << (uint8) 0x2C; // unk, may be realm number/id?
|
||||
else
|
||||
else if (valid_tbc_version)
|
||||
pkt << (uint8) 0x0; //1.12.1 and 1.12.2 clients
|
||||
}
|
||||
|
||||
if(_build == 9947 || _build == 8606)//2.4.3 and 3.1.3 cliens
|
||||
if (valid_tbc_version || valid_wlk_version)//2.4.3 and 3.1.3 cliens
|
||||
{
|
||||
pkt << (uint8) 0x10;
|
||||
pkt << (uint8) 0x00;
|
||||
}else{//1.12.1 and 1.12.2 clients
|
||||
}
|
||||
else if (valid_tbc_version)//1.12.1 and 1.12.2 clients
|
||||
{
|
||||
pkt << (uint8) 0x00;
|
||||
pkt << (uint8) 0x02;
|
||||
}
|
||||
@@ -1077,7 +1113,7 @@ void Patcher::LoadPatchesInfo()
|
||||
//int errno;
|
||||
struct dirent * dp;
|
||||
dirp = opendir("./patches/");
|
||||
if(!dirp)
|
||||
if (!dirp)
|
||||
return;
|
||||
while (dirp)
|
||||
{
|
||||
@@ -1085,14 +1121,14 @@ void Patcher::LoadPatchesInfo()
|
||||
if ((dp = readdir(dirp)) != NULL)
|
||||
{
|
||||
int l = strlen(dp->d_name);
|
||||
if(l < 8)
|
||||
if (l < 8)
|
||||
continue;
|
||||
if(!memcmp(&dp->d_name[l-4],".mpq",4))
|
||||
if (!memcmp(&dp->d_name[l-4],".mpq",4))
|
||||
LoadPatchMD5(dp->d_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(errno != 0)
|
||||
if (errno != 0)
|
||||
{
|
||||
closedir(dirp);
|
||||
return;
|
||||
@@ -1101,7 +1137,7 @@ void Patcher::LoadPatchesInfo()
|
||||
}
|
||||
}
|
||||
|
||||
if(dirp)
|
||||
if (dirp)
|
||||
closedir(dirp);
|
||||
}
|
||||
|
||||
@@ -1110,7 +1146,7 @@ void Patcher::LoadPatchesInfo()
|
||||
{
|
||||
WIN32_FIND_DATA fil;
|
||||
HANDLE hFil=FindFirstFile("./patches/*.mpq", &fil);
|
||||
if(hFil == INVALID_HANDLE_VALUE)
|
||||
if (hFil == INVALID_HANDLE_VALUE)
|
||||
return; // no patches were found
|
||||
|
||||
do
|
||||
@@ -1129,7 +1165,7 @@ void Patcher::LoadPatchMD5(char * szFileName)
|
||||
path += szFileName;
|
||||
FILE *pPatch = fopen(path.c_str(), "rb");
|
||||
sLog.outDebug("Loading patch info from %s\n", path.c_str());
|
||||
if(!pPatch)
|
||||
if (!pPatch)
|
||||
{
|
||||
sLog.outError("Error loading patch %s\n", path.c_str());
|
||||
return;
|
||||
@@ -1156,8 +1192,8 @@ void Patcher::LoadPatchMD5(char * szFileName)
|
||||
/// Get cached MD5 hash for a given patch file
|
||||
bool Patcher::GetHash(char * pat, uint8 mymd5[16])
|
||||
{
|
||||
for( Patches::iterator i = _patches.begin(); i != _patches.end(); ++i )
|
||||
if(!stricmp(pat, i->first.c_str()))
|
||||
for (Patches::iterator i = _patches.begin(); i != _patches.end(); ++i)
|
||||
if (!stricmp(pat, i->first.c_str()))
|
||||
{
|
||||
memcpy(mymd5, i->second->md5, 16);
|
||||
return true;
|
||||
@@ -1175,6 +1211,6 @@ Patcher::Patcher()
|
||||
/// Empty and delete the patch map on termination
|
||||
Patcher::~Patcher()
|
||||
{
|
||||
for(Patches::iterator i = _patches.begin(); i != _patches.end(); ++i )
|
||||
for (Patches::iterator i = _patches.begin(); i != _patches.end(); ++i)
|
||||
delete i->second;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user