mirror of
https://github.com/EQEmu/Server.git
synced 2026-03-07 23:52:24 +00:00
[Loginserver] Identify unknown login client packet fields (#1680)
* Add player login reply struct * Use player login reply struct for failed logins * Use base message struct for login requests * Refactor server list reply serialization Use BaseMessage and BaseReplyMessage structs for server list and add flags for server type and status * Use reply message struct for login handshake Remove client version checks, the packets are the same for titanium and rof2 * Use base headers for join server requests * Log correct server list ip * Add compressed flag to base message header Document encrypt type flag more
This commit is contained in:
parent
099759c477
commit
cbea7045fa
@ -120,36 +120,20 @@ void Client::Handle_SessionReady(const char *data, unsigned int size)
|
|||||||
m_client_status = cs_waiting_for_login;
|
m_client_status = cs_waiting_for_login;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The packets are mostly the same but slightly different between the two versions
|
* The packets are identical between the two versions
|
||||||
*/
|
*/
|
||||||
if (m_client_version == cv_sod) {
|
auto *outapp = new EQApplicationPacket(OP_ChatMessage, sizeof(LoginHandShakeReply_Struct));
|
||||||
auto *outapp = new EQApplicationPacket(OP_ChatMessage, 17);
|
auto buf = reinterpret_cast<LoginHandShakeReply_Struct*>(outapp->pBuffer);
|
||||||
outapp->pBuffer[0] = 0x02;
|
buf->base_header.sequence = 0x02;
|
||||||
outapp->pBuffer[10] = 0x01;
|
buf->base_reply.success = true;
|
||||||
outapp->pBuffer[11] = 0x65;
|
buf->base_reply.error_str_id = 0x65; // 101 "No Error"
|
||||||
|
|
||||||
if (server.options.IsDumpOutPacketsOn()) {
|
if (server.options.IsDumpOutPacketsOn()) {
|
||||||
DumpPacket(outapp);
|
DumpPacket(outapp);
|
||||||
}
|
|
||||||
|
|
||||||
m_connection->QueuePacket(outapp);
|
|
||||||
delete outapp;
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
const char *msg = "ChatMessage";
|
|
||||||
auto *outapp = new EQApplicationPacket(OP_ChatMessage, 16 + strlen(msg));
|
|
||||||
outapp->pBuffer[0] = 0x02;
|
|
||||||
outapp->pBuffer[10] = 0x01;
|
|
||||||
outapp->pBuffer[11] = 0x65;
|
|
||||||
strcpy((char *) (outapp->pBuffer + 15), msg);
|
|
||||||
|
|
||||||
if (server.options.IsDumpOutPacketsOn()) {
|
m_connection->QueuePacket(outapp);
|
||||||
DumpPacket(outapp);
|
delete outapp;
|
||||||
}
|
|
||||||
|
|
||||||
m_connection->QueuePacket(outapp);
|
|
||||||
delete outapp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,14 +149,18 @@ void Client::Handle_Login(const char *data, unsigned int size)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((size - 12) % 8 != 0) {
|
// login user/pass are variable length after unencrypted opcode and base message header (size includes opcode)
|
||||||
LogError("Login received packet of size: {0}, this would cause a block corruption, discarding", size);
|
constexpr int header_size = sizeof(uint16_t) + sizeof(LoginBaseMessage_Struct);
|
||||||
|
int data_size = size - header_size;
|
||||||
|
|
||||||
|
if (size <= header_size) {
|
||||||
|
LogError("Login received packet of size: {0}, this would cause a buffer overflow, discarding", size);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size < sizeof(LoginLoginRequest_Struct)) {
|
if (data_size % 8 != 0) {
|
||||||
LogError("Login received packet of size: {0}, this would cause a buffer overflow, discarding", size);
|
LogError("Login received packet of size: {0}, this would cause a block corruption, discarding", size);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -189,13 +177,14 @@ void Client::Handle_Login(const char *data, unsigned int size)
|
|||||||
std::string db_account_password_hash;
|
std::string db_account_password_hash;
|
||||||
|
|
||||||
std::string outbuffer;
|
std::string outbuffer;
|
||||||
outbuffer.resize(size - 12);
|
outbuffer.resize(data_size);
|
||||||
if (outbuffer.length() == 0) {
|
if (outbuffer.length() == 0) {
|
||||||
LogError("Corrupt buffer sent to server, no length");
|
LogError("Corrupt buffer sent to server, no length");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto r = eqcrypt_block(data + 10, size - 12, &outbuffer[0], 0);
|
// data starts at base message header (opcode not included)
|
||||||
|
auto r = eqcrypt_block(data + sizeof(LoginBaseMessage_Struct), data_size, &outbuffer[0], 0);
|
||||||
if (r == nullptr) {
|
if (r == nullptr) {
|
||||||
LogError("Failed to decrypt eqcrypt block");
|
LogError("Failed to decrypt eqcrypt block");
|
||||||
return;
|
return;
|
||||||
@ -209,7 +198,8 @@ void Client::Handle_Login(const char *data, unsigned int size)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(&m_llrs, data, sizeof(LoginLoginRequest_Struct));
|
// only need to copy the base header for reply options, ignore login info
|
||||||
|
memcpy(&m_llrs, data, sizeof(LoginBaseMessage_Struct));
|
||||||
|
|
||||||
bool result = false;
|
bool result = false;
|
||||||
if (outbuffer[0] == 0 && outbuffer[1] == 0) {
|
if (outbuffer[0] == 0 && outbuffer[1] == 0) {
|
||||||
@ -297,8 +287,8 @@ void Client::Handle_Play(const char *data)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto *play = (const PlayEverquestRequest_Struct *) data;
|
const auto *play = (const PlayEverquestRequest_Struct *) data;
|
||||||
auto server_id_in = (unsigned int) play->ServerNumber;
|
auto server_id_in = (unsigned int) play->server_number;
|
||||||
auto sequence_in = (unsigned int) play->Sequence;
|
auto sequence_in = (unsigned int) play->base_header.sequence;
|
||||||
|
|
||||||
if (server.options.IsTraceOn()) {
|
if (server.options.IsTraceOn()) {
|
||||||
LogInfo(
|
LogInfo(
|
||||||
@ -309,7 +299,7 @@ void Client::Handle_Play(const char *data)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_play_server_id = (unsigned int) play->ServerNumber;
|
m_play_server_id = (unsigned int) play->server_number;
|
||||||
m_play_sequence_id = sequence_in;
|
m_play_sequence_id = sequence_in;
|
||||||
m_play_server_id = server_id_in;
|
m_play_server_id = server_id_in;
|
||||||
server.server_manager->SendUserToWorldRequest(server_id_in, m_account_id, m_loginserver_name);
|
server.server_manager->SendUserToWorldRequest(server_id_in, m_account_id, m_loginserver_name);
|
||||||
@ -320,14 +310,13 @@ void Client::Handle_Play(const char *data)
|
|||||||
*/
|
*/
|
||||||
void Client::SendServerListPacket(uint32 seq)
|
void Client::SendServerListPacket(uint32 seq)
|
||||||
{
|
{
|
||||||
EQApplicationPacket *outapp = server.server_manager->CreateServerListPacket(this, seq);
|
auto outapp = server.server_manager->CreateServerListPacket(this, seq);
|
||||||
|
|
||||||
if (server.options.IsDumpOutPacketsOn()) {
|
if (server.options.IsDumpOutPacketsOn()) {
|
||||||
DumpPacket(outapp);
|
DumpPacket(outapp.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
m_connection->QueuePacket(outapp);
|
m_connection->QueuePacket(outapp.get());
|
||||||
delete outapp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::SendPlayResponse(EQApplicationPacket *outapp)
|
void Client::SendPlayResponse(EQApplicationPacket *outapp)
|
||||||
@ -412,16 +401,26 @@ void Client::DoFailedLogin()
|
|||||||
m_stored_user.clear();
|
m_stored_user.clear();
|
||||||
m_stored_pass.clear();
|
m_stored_pass.clear();
|
||||||
|
|
||||||
EQApplicationPacket outapp(OP_LoginAccepted, sizeof(LoginLoginFailed_Struct));
|
// unencrypted
|
||||||
auto *login_failed = (LoginLoginFailed_Struct *) outapp.pBuffer;
|
LoginBaseMessage_Struct base_header{};
|
||||||
|
base_header.sequence = m_llrs.sequence; // login (3)
|
||||||
|
base_header.encrypt_type = m_llrs.encrypt_type;
|
||||||
|
|
||||||
login_failed->unknown1 = m_llrs.unknown1;
|
// encrypted
|
||||||
login_failed->unknown2 = m_llrs.unknown2;
|
PlayerLoginReply_Struct login_reply{};
|
||||||
login_failed->unknown3 = m_llrs.unknown3;
|
login_reply.base_reply.success = false;
|
||||||
login_failed->unknown4 = m_llrs.unknown4;
|
login_reply.base_reply.error_str_id = 105; // Error - The username and/or password were not valid
|
||||||
login_failed->unknown5 = m_llrs.unknown5;
|
|
||||||
|
|
||||||
memcpy(login_failed->unknown6, FailedLoginResponseData, sizeof(FailedLoginResponseData));
|
char encrypted_buffer[80] = {0};
|
||||||
|
auto rc = eqcrypt_block((const char*)&login_reply, sizeof(login_reply), encrypted_buffer, 1);
|
||||||
|
if (rc == nullptr) {
|
||||||
|
LogDebug("Failed to encrypt eqcrypt block for failed login");
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int outsize = sizeof(LoginBaseMessage_Struct) + sizeof(encrypted_buffer);
|
||||||
|
EQApplicationPacket outapp(OP_LoginAccepted, outsize);
|
||||||
|
outapp.WriteData(&base_header, sizeof(base_header));
|
||||||
|
outapp.WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
|
||||||
|
|
||||||
if (server.options.IsDumpOutPacketsOn()) {
|
if (server.options.IsDumpOutPacketsOn()) {
|
||||||
DumpPacket(&outapp);
|
DumpPacket(&outapp);
|
||||||
@ -539,51 +538,47 @@ void Client::DoSuccessfulLogin(
|
|||||||
m_account_name = in_account_name;
|
m_account_name = in_account_name;
|
||||||
m_loginserver_name = db_loginserver;
|
m_loginserver_name = db_loginserver;
|
||||||
|
|
||||||
auto *outapp = new EQApplicationPacket(OP_LoginAccepted, 10 + 80);
|
// unencrypted
|
||||||
auto *login_accepted = (LoginAccepted_Struct *) outapp->pBuffer;
|
LoginBaseMessage_Struct base_header{};
|
||||||
login_accepted->unknown1 = m_llrs.unknown1;
|
base_header.sequence = m_llrs.sequence;
|
||||||
login_accepted->unknown2 = m_llrs.unknown2;
|
base_header.compressed = false;
|
||||||
login_accepted->unknown3 = m_llrs.unknown3;
|
base_header.encrypt_type = m_llrs.encrypt_type;
|
||||||
login_accepted->unknown4 = m_llrs.unknown4;
|
base_header.unk3 = m_llrs.unk3;
|
||||||
login_accepted->unknown5 = m_llrs.unknown5;
|
|
||||||
|
|
||||||
auto *login_failed_attempts = new LoginFailedAttempts_Struct;
|
// not serializing any of the variable length strings so just use struct directly
|
||||||
memset(login_failed_attempts, 0, sizeof(LoginFailedAttempts_Struct));
|
PlayerLoginReply_Struct login_reply{};
|
||||||
|
login_reply.base_reply.success = true;
|
||||||
login_failed_attempts->failed_attempts = 0;
|
login_reply.base_reply.error_str_id = 101; // No Error
|
||||||
login_failed_attempts->message = 0x01;
|
login_reply.unk1 = 0;
|
||||||
login_failed_attempts->lsid = db_account_id;
|
login_reply.unk2 = 0;
|
||||||
login_failed_attempts->unknown3[3] = 0x03;
|
login_reply.lsid = db_account_id;
|
||||||
login_failed_attempts->unknown4[3] = 0x02;
|
login_reply.failed_attempts = 0;
|
||||||
login_failed_attempts->unknown5[0] = 0xe7;
|
login_reply.show_player_count = false; // todo: config option
|
||||||
login_failed_attempts->unknown5[1] = 0x03;
|
login_reply.offer_min_days = 99;
|
||||||
login_failed_attempts->unknown6[0] = 0xff;
|
login_reply.offer_min_views = -1;
|
||||||
login_failed_attempts->unknown6[1] = 0xff;
|
login_reply.offer_cooldown_minutes = 0;
|
||||||
login_failed_attempts->unknown6[2] = 0xff;
|
login_reply.web_offer_number = 0;
|
||||||
login_failed_attempts->unknown6[3] = 0xff;
|
login_reply.web_offer_min_days = 99;
|
||||||
login_failed_attempts->unknown7[0] = 0xa0;
|
login_reply.web_offer_min_views = -1;
|
||||||
login_failed_attempts->unknown7[1] = 0x05;
|
login_reply.web_offer_cooldown_minutes = 0;
|
||||||
login_failed_attempts->unknown8[3] = 0x02;
|
memcpy(login_reply.key, m_key.c_str(), m_key.size());
|
||||||
login_failed_attempts->unknown9[0] = 0xff;
|
|
||||||
login_failed_attempts->unknown9[1] = 0x03;
|
|
||||||
login_failed_attempts->unknown11[0] = 0x63;
|
|
||||||
login_failed_attempts->unknown12[0] = 0x01;
|
|
||||||
memcpy(login_failed_attempts->key, m_key.c_str(), m_key.size());
|
|
||||||
|
|
||||||
char encrypted_buffer[80] = {0};
|
char encrypted_buffer[80] = {0};
|
||||||
auto rc = eqcrypt_block((const char *) login_failed_attempts, 75, encrypted_buffer, 1);
|
auto rc = eqcrypt_block((const char*)&login_reply, sizeof(login_reply), encrypted_buffer, 1);
|
||||||
if (rc == nullptr) {
|
if (rc == nullptr) {
|
||||||
LogDebug("Failed to encrypt eqcrypt block");
|
LogDebug("Failed to encrypt eqcrypt block");
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(login_accepted->encrypt, encrypted_buffer, 80);
|
constexpr int outsize = sizeof(LoginBaseMessage_Struct) + sizeof(encrypted_buffer);
|
||||||
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_LoginAccepted, outsize);
|
||||||
|
outapp->WriteData(&base_header, sizeof(base_header));
|
||||||
|
outapp->WriteData(&encrypted_buffer, sizeof(encrypted_buffer));
|
||||||
|
|
||||||
if (server.options.IsDumpOutPacketsOn()) {
|
if (server.options.IsDumpOutPacketsOn()) {
|
||||||
DumpPacket(outapp);
|
DumpPacket(outapp.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
m_connection->QueuePacket(outapp);
|
m_connection->QueuePacket(outapp.get());
|
||||||
delete outapp;
|
|
||||||
|
|
||||||
m_client_status = cs_logged_in;
|
m_client_status = cs_logged_in;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,28 @@ enum LSClientStatus {
|
|||||||
cs_logged_in
|
cs_logged_in
|
||||||
};
|
};
|
||||||
|
|
||||||
|
namespace LS {
|
||||||
|
namespace ServerStatusFlags {
|
||||||
|
enum eServerStatusFlags {
|
||||||
|
Up = 0, // default
|
||||||
|
Down = 1,
|
||||||
|
Unused = 2,
|
||||||
|
Locked = 4 // can be combined with Down to show "Locked (Down)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ServerTypeFlags {
|
||||||
|
enum eServerTypeFlags {
|
||||||
|
None = 0,
|
||||||
|
Standard = 1,
|
||||||
|
Unknown2 = 2,
|
||||||
|
Unknown4 = 4,
|
||||||
|
Preferred = 8,
|
||||||
|
Legends = 16 // can be combined with Preferred flag to override color in Legends section with Preferred color (green)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client class, controls a single client and it's connection to the login server
|
* Client class, controls a single client and it's connection to the login server
|
||||||
*/
|
*/
|
||||||
@ -189,7 +211,7 @@ private:
|
|||||||
|
|
||||||
std::unique_ptr<EQ::Net::DaybreakConnectionManager> m_login_connection_manager;
|
std::unique_ptr<EQ::Net::DaybreakConnectionManager> m_login_connection_manager;
|
||||||
std::shared_ptr<EQ::Net::DaybreakConnection> m_login_connection;
|
std::shared_ptr<EQ::Net::DaybreakConnection> m_login_connection;
|
||||||
LoginLoginRequest_Struct m_llrs;
|
LoginBaseMessage_Struct m_llrs;
|
||||||
|
|
||||||
std::string m_stored_user;
|
std::string m_stored_user;
|
||||||
std::string m_stored_pass;
|
std::string m_stored_pass;
|
||||||
|
|||||||
@ -3,91 +3,94 @@
|
|||||||
|
|
||||||
#pragma pack(1)
|
#pragma pack(1)
|
||||||
|
|
||||||
struct LoginChatMessage_Struct {
|
// unencrypted base message header in all packets
|
||||||
short Unknown0;
|
struct LoginBaseMessage_Struct
|
||||||
uint32 Unknown1;
|
{
|
||||||
uint32 Unknown2;
|
int32_t sequence; // request type/login sequence (2: handshake, 3: login, 4: serverlist, ...)
|
||||||
uint32 Unknown3;
|
bool compressed; // true: deflated
|
||||||
uint8 Unknown4;
|
int8_t encrypt_type; // 1: invert (unused) 2: des (2 for encrypted player logins and order expansions) (client uses what it sent, ignores in reply)
|
||||||
char ChatMessage[1];
|
int32_t unk3; // unused?
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LoginLoginRequest_Struct {
|
struct LoginBaseReplyMessage_Struct
|
||||||
short unknown1;
|
{
|
||||||
short unknown2;
|
bool success; // 0: failure (shows error string) 1: success
|
||||||
short unknown3;
|
int32_t error_str_id; // last error eqlsstr id, default: 101 (no error)
|
||||||
short unknown4;
|
char str[1]; // variable length, unknown (may be unused, this struct is a common pattern elsewhere)
|
||||||
short unknown5;
|
|
||||||
char unknown6[16];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LoginAccepted_Struct {
|
struct LoginHandShakeReply_Struct
|
||||||
short unknown1;
|
{
|
||||||
short unknown2;
|
LoginBaseMessage_Struct base_header;
|
||||||
short unknown3;
|
LoginBaseReplyMessage_Struct base_reply;
|
||||||
short unknown4;
|
char unknown[1]; // variable length string
|
||||||
short unknown5;
|
|
||||||
char encrypt[80];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LoginFailedAttempts_Struct {
|
// for reference, login buffer is variable (minimum size 8 due to encryption)
|
||||||
char message; //0x01
|
struct PlayerLogin_Struct
|
||||||
char unknown2[7]; //0x00
|
{
|
||||||
uint32 lsid;
|
LoginBaseMessage_Struct base_header;
|
||||||
char key[11]; //10 char + null term;
|
char username[1];
|
||||||
uint32 failed_attempts;
|
char password[1];
|
||||||
char unknown3[4]; //0x00, 0x00, 0x00, 0x03
|
|
||||||
char unknown4[4]; //0x00, 0x00, 0x00, 0x02
|
|
||||||
char unknown5[4]; //0xe7, 0x03, 0x00, 0x00
|
|
||||||
char unknown6[4]; //0xff, 0xff, 0xff, 0xff
|
|
||||||
char unknown7[4]; //0xa0, 0x05, 0x00, 0x00
|
|
||||||
char unknown8[4]; //0x00, 0x00, 0x00, 0x02
|
|
||||||
char unknown9[4]; //0xff, 0x03, 0x00, 0x00
|
|
||||||
char unknown10[4]; //0x00, 0x00, 0x00, 0x00
|
|
||||||
char unknown11[4]; //0x63, 0x00, 0x00, 0x00
|
|
||||||
char unknown12[4]; //0x01, 0x00, 0x00, 0x00
|
|
||||||
char unknown13[4]; //0x00, 0x00, 0x00, 0x00
|
|
||||||
char unknown14[4]; //0x00, 0x00, 0x00, 0x00
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LoginLoginFailed_Struct {
|
// variable length, can use directly if not serializing strings
|
||||||
short unknown1;
|
struct PlayerLoginReply_Struct
|
||||||
short unknown2;
|
{
|
||||||
short unknown3;
|
// base header excluded to make struct data easier to encrypt
|
||||||
short unknown4;
|
//LoginBaseMessage_Struct base_header;
|
||||||
short unknown5;
|
LoginBaseReplyMessage_Struct base_reply;
|
||||||
char unknown6[74];
|
|
||||||
|
int8_t unk1; // (default: 0)
|
||||||
|
int8_t unk2; // (default: 0)
|
||||||
|
int32_t lsid; // (default: -1)
|
||||||
|
char key[11]; // client reads until null (variable length)
|
||||||
|
int32_t failed_attempts;
|
||||||
|
bool show_player_count; // admin flag, enables admin button and shows server player counts (default: false)
|
||||||
|
int32_t offer_min_days; // guess, needs more investigation, maybe expansion offers (default: 99)
|
||||||
|
int32_t offer_min_views; // guess (default: -1)
|
||||||
|
int32_t offer_cooldown_minutes; // guess (default: 0)
|
||||||
|
int32_t web_offer_number; // web order view number, 0 nothing (default: 0)
|
||||||
|
int32_t web_offer_min_days; // number of days to show offer (based on first offer time in client eqls ini) (default: 99)
|
||||||
|
int32_t web_offer_min_views; // mininum views, -1 for no minimum, 0 for never shows (based on client eqls ini) (default: -1)
|
||||||
|
int32_t web_offer_cooldown_minutes; // minimum minutes between offers (based on last offer time in client eqls ini) (default: 0)
|
||||||
|
char username[1]; // variable length, if not empty client attempts to re-login to server select when quitting from char select and sends this in a struct
|
||||||
|
char unknown[1]; // variable length, password unlikely? client doesn't send this on re-login from char select
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ServerListHeader_Struct {
|
// variable length, for reference
|
||||||
|
struct LoginClientServerData_Struct
|
||||||
|
{
|
||||||
|
char ip[1];
|
||||||
|
int32_t server_type; // legends, preferred, standard
|
||||||
|
int32_t server_id;
|
||||||
|
char server_name[1];
|
||||||
|
char country_code[1]; // if doesn't match client locale then server is colored dark grey in list and joining is prevented (to block for "us" use one of "kr", "tw", "jp", "de", "fr", or "cn") (ISO 3166-1 alpha-2)
|
||||||
|
char language_code[1];
|
||||||
|
int32_t server_status; // see ServerStatusFlags
|
||||||
|
int32_t player_count;
|
||||||
|
};
|
||||||
|
|
||||||
uint32 Unknown1;
|
// variable length, for reference
|
||||||
uint32 Unknown2;
|
struct ServerListReply_Struct
|
||||||
uint32 Unknown3;
|
{
|
||||||
uint32 Unknown4;
|
LoginBaseMessage_Struct base_header;
|
||||||
uint32 NumberOfServers;
|
LoginBaseReplyMessage_Struct base_reply;
|
||||||
|
|
||||||
|
int32_t server_count;
|
||||||
|
LoginClientServerData_Struct servers[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PlayEverquestRequest_Struct {
|
struct PlayEverquestRequest_Struct {
|
||||||
uint16 Sequence;
|
LoginBaseMessage_Struct base_header;
|
||||||
uint32 Unknown1;
|
uint32 server_number;
|
||||||
uint32 Unknown2;
|
|
||||||
uint32 ServerNumber;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SCJoinServerReply
|
||||||
struct PlayEverquestResponse_Struct {
|
struct PlayEverquestResponse_Struct {
|
||||||
uint8 Sequence;
|
LoginBaseMessage_Struct base_header;
|
||||||
uint8 Unknown1[9];
|
LoginBaseReplyMessage_Struct base_reply;
|
||||||
uint8 Allowed;
|
uint32 server_number;
|
||||||
uint16 Message;
|
|
||||||
uint8 Unknown2[3];
|
|
||||||
uint32 ServerNumber;
|
|
||||||
};
|
|
||||||
|
|
||||||
static const unsigned char FailedLoginResponseData[] = {
|
|
||||||
0xf6, 0x85, 0x9c, 0x23, 0x57, 0x7e, 0x3e, 0x55, 0xb3, 0x4c, 0xf8, 0xc8, 0xcb, 0x77, 0xd5, 0x16,
|
|
||||||
0x09, 0x7a, 0x63, 0xdc, 0x57, 0x7e, 0x3e, 0x55, 0xb3, 0x4c, 0xf8, 0xc8, 0xcb, 0x77, 0xd5, 0x16,
|
|
||||||
0x09, 0x7a, 0x63, 0xdc, 0x57, 0x7e, 0x3e, 0x55, 0xb3
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -97,9 +97,8 @@ WorldServer *ServerManager::GetServerByAddress(const std::string &ip_address, in
|
|||||||
* @param sequence
|
* @param sequence
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
EQApplicationPacket *ServerManager::CreateServerListPacket(Client *client, uint32 sequence)
|
std::unique_ptr<EQApplicationPacket> ServerManager::CreateServerListPacket(Client *client, uint32 sequence)
|
||||||
{
|
{
|
||||||
unsigned int packet_size = sizeof(ServerListHeader_Struct);
|
|
||||||
unsigned int server_count = 0;
|
unsigned int server_count = 0;
|
||||||
in_addr in{};
|
in_addr in{};
|
||||||
in.s_addr = client->GetConnection()->GetRemoteIP();
|
in.s_addr = client->GetConnection()->GetRemoteIP();
|
||||||
@ -107,144 +106,60 @@ EQApplicationPacket *ServerManager::CreateServerListPacket(Client *client, uint3
|
|||||||
|
|
||||||
LogDebug("ServerManager::CreateServerListPacket via client address [{0}]", client_ip);
|
LogDebug("ServerManager::CreateServerListPacket via client address [{0}]", client_ip);
|
||||||
|
|
||||||
auto iter = m_world_servers.begin();
|
for (const auto& world_server : m_world_servers)
|
||||||
while (iter != m_world_servers.end()) {
|
{
|
||||||
if (!(*iter)->IsAuthorized()) {
|
if (world_server->IsAuthorized()) {
|
||||||
|
++server_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializeBuffer buf;
|
||||||
|
|
||||||
|
// LoginBaseMessage_Struct header
|
||||||
|
buf.WriteInt32(sequence);
|
||||||
|
buf.WriteInt8(0);
|
||||||
|
buf.WriteInt8(0);
|
||||||
|
buf.WriteInt32(0);
|
||||||
|
|
||||||
|
// LoginBaseReplyMessage_Struct
|
||||||
|
buf.WriteInt8(true); // success (no error)
|
||||||
|
buf.WriteInt32(0x65); // 101 "No Error" eqlsstr
|
||||||
|
buf.WriteString("");
|
||||||
|
|
||||||
|
// ServerListReply_Struct
|
||||||
|
buf.WriteInt32(server_count);
|
||||||
|
|
||||||
|
for (const auto& world_server : m_world_servers)
|
||||||
|
{
|
||||||
|
if (!world_server->IsAuthorized()) {
|
||||||
LogDebug(
|
LogDebug(
|
||||||
"ServerManager::CreateServerListPacket | Server [{0}] via IP [{1}] is not authorized to be listed",
|
"ServerManager::CreateServerListPacket | Server [{}] via IP [{}] is not authorized to be listed",
|
||||||
(*iter)->GetServerLongName(),
|
world_server->GetServerLongName(),
|
||||||
(*iter)->GetConnection()->Handle()->RemoteIP()
|
world_server->GetConnection()->Handle()->RemoteIP()
|
||||||
);
|
);
|
||||||
++iter;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string world_ip = (*iter)->GetConnection()->Handle()->RemoteIP();
|
bool use_local_ip = false;
|
||||||
if (world_ip == client_ip) {
|
|
||||||
packet_size += (*iter)->GetServerLongName().size() + (*iter)->GetLocalIP().size() + 24;
|
|
||||||
|
|
||||||
LogDebug(
|
std::string world_ip = world_server->GetConnection()->Handle()->RemoteIP();
|
||||||
"CreateServerListPacket | Building list entry | Client [{0}] IP [{1}] Server Long Name [{2}] Server IP [{3}] (Local)",
|
if (world_ip == client_ip || IpUtil::IsIpInPrivateRfc1918(client_ip)) {
|
||||||
client->GetAccountName(),
|
use_local_ip = true;
|
||||||
client_ip,
|
|
||||||
(*iter)->GetServerLongName(),
|
|
||||||
(*iter)->GetLocalIP()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (IpUtil::IsIpInPrivateRfc1918(client_ip)) {
|
|
||||||
packet_size += (*iter)->GetServerLongName().size() + (*iter)->GetLocalIP().size() + 24;
|
|
||||||
|
|
||||||
LogDebug(
|
|
||||||
"CreateServerListPacket | Building list entry | Client [{0}] IP [{1}] Server Long Name [{2}] Server IP [{3}] (Local)",
|
|
||||||
client->GetAccountName(),
|
|
||||||
client_ip,
|
|
||||||
(*iter)->GetServerLongName(),
|
|
||||||
(*iter)->GetLocalIP()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
packet_size += (*iter)->GetServerLongName().size() + (*iter)->GetRemoteIP().size() + 24;
|
|
||||||
|
|
||||||
LogDebug(
|
|
||||||
"CreateServerListPacket | Building list entry | Client [{0}] IP [{1}] Server Long Name [{2}] Server IP [{3}] (Remote)",
|
|
||||||
client->GetAccountName(),
|
|
||||||
client_ip,
|
|
||||||
(*iter)->GetServerLongName(),
|
|
||||||
(*iter)->GetRemoteIP()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server_count++;
|
LogDebug(
|
||||||
++iter;
|
"CreateServerListPacket | Building list entry | Client [{}] IP [{}] Server Long Name [{}] Server IP [{}] ({})",
|
||||||
|
client->GetAccountName(),
|
||||||
|
client_ip,
|
||||||
|
world_server->GetServerLongName(),
|
||||||
|
use_local_ip ? world_server->GetLocalIP() : world_server->GetRemoteIP(),
|
||||||
|
use_local_ip ? "Local" : "Remote"
|
||||||
|
);
|
||||||
|
|
||||||
|
world_server->SerializeForClientServerList(buf, use_local_ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *outapp = new EQApplicationPacket(OP_ServerListResponse, packet_size);
|
return std::make_unique<EQApplicationPacket>(OP_ServerListResponse, buf);
|
||||||
auto *server_list = (ServerListHeader_Struct *) outapp->pBuffer;
|
|
||||||
|
|
||||||
server_list->Unknown1 = sequence;
|
|
||||||
server_list->Unknown2 = 0x00000000;
|
|
||||||
server_list->Unknown3 = 0x01650000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not sure what this is but it should be noted setting it to
|
|
||||||
* 0xFFFFFFFF crashes the client so: don't do that.
|
|
||||||
*/
|
|
||||||
server_list->Unknown4 = 0x00000000;
|
|
||||||
server_list->NumberOfServers = server_count;
|
|
||||||
|
|
||||||
unsigned char *data_pointer = outapp->pBuffer;
|
|
||||||
data_pointer += sizeof(ServerListHeader_Struct);
|
|
||||||
|
|
||||||
iter = m_world_servers.begin();
|
|
||||||
while (iter != m_world_servers.end()) {
|
|
||||||
if (!(*iter)->IsAuthorized()) {
|
|
||||||
++iter;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string world_ip = (*iter)->GetConnection()->Handle()->RemoteIP();
|
|
||||||
if (world_ip == client_ip) {
|
|
||||||
memcpy(data_pointer, (*iter)->GetLocalIP().c_str(), (*iter)->GetLocalIP().size());
|
|
||||||
data_pointer += ((*iter)->GetLocalIP().size() + 1);
|
|
||||||
}
|
|
||||||
else if (IpUtil::IsIpInPrivateRfc1918(client_ip)) {
|
|
||||||
memcpy(data_pointer, (*iter)->GetLocalIP().c_str(), (*iter)->GetLocalIP().size());
|
|
||||||
data_pointer += ((*iter)->GetLocalIP().size() + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
memcpy(data_pointer, (*iter)->GetRemoteIP().c_str(), (*iter)->GetRemoteIP().size());
|
|
||||||
data_pointer += ((*iter)->GetRemoteIP().size() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ((*iter)->GetServerListID()) {
|
|
||||||
case 1: {
|
|
||||||
*(unsigned int *) data_pointer = 0x00000030;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
*(unsigned int *) data_pointer = 0x00000009;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
*(unsigned int *) data_pointer = 0x00000001;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data_pointer += 4;
|
|
||||||
|
|
||||||
*(unsigned int *) data_pointer = (*iter)->GetServerId();
|
|
||||||
data_pointer += 4;
|
|
||||||
|
|
||||||
memcpy(data_pointer, (*iter)->GetServerLongName().c_str(), (*iter)->GetServerLongName().size());
|
|
||||||
data_pointer += ((*iter)->GetServerLongName().size() + 1);
|
|
||||||
|
|
||||||
memcpy(data_pointer, "EN", 2);
|
|
||||||
data_pointer += 3;
|
|
||||||
|
|
||||||
memcpy(data_pointer, "US", 2);
|
|
||||||
data_pointer += 3;
|
|
||||||
|
|
||||||
// 0 = Up, 1 = Down, 2 = Up, 3 = down, 4 = locked, 5 = locked(down)
|
|
||||||
if ((*iter)->GetStatus() < 0) {
|
|
||||||
if ((*iter)->GetZonesBooted() == 0) {
|
|
||||||
*(uint32 *) data_pointer = 0x01;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
*(uint32 *) data_pointer = 0x04;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
*(uint32 *) data_pointer = 0x02;
|
|
||||||
}
|
|
||||||
data_pointer += 4;
|
|
||||||
|
|
||||||
*(uint32 *) data_pointer = (*iter)->GetPlayersOnline();
|
|
||||||
data_pointer += 4;
|
|
||||||
|
|
||||||
++iter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return outapp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -45,7 +45,7 @@ public:
|
|||||||
* @param sequence
|
* @param sequence
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
EQApplicationPacket *CreateServerListPacket(Client *client, uint32 sequence);
|
std::unique_ptr<EQApplicationPacket> CreateServerListPacket(Client *client, uint32 sequence);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks to see if there is a server exists with this name, ignoring option
|
* Checks to see if there is a server exists with this name, ignoring option
|
||||||
|
|||||||
@ -216,11 +216,11 @@ void WorldServer::ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Ne
|
|||||||
);
|
);
|
||||||
|
|
||||||
auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer;
|
auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer;
|
||||||
per->Sequence = client->GetPlaySequence();
|
per->base_header.sequence = client->GetPlaySequence();
|
||||||
per->ServerNumber = client->GetPlayServerID();
|
per->server_number = client->GetPlayServerID();
|
||||||
|
|
||||||
if (user_to_world_response->response > 0) {
|
if (user_to_world_response->response > 0) {
|
||||||
per->Allowed = 1;
|
per->base_reply.success = true;
|
||||||
SendClientAuth(
|
SendClientAuth(
|
||||||
client->GetConnection()->GetRemoteAddr(),
|
client->GetConnection()->GetRemoteAddr(),
|
||||||
client->GetAccountName(),
|
client->GetAccountName(),
|
||||||
@ -232,34 +232,34 @@ void WorldServer::ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Ne
|
|||||||
|
|
||||||
switch (user_to_world_response->response) {
|
switch (user_to_world_response->response) {
|
||||||
case UserToWorldStatusSuccess:
|
case UserToWorldStatusSuccess:
|
||||||
per->Message = 101;
|
per->base_reply.error_str_id = 101;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusWorldUnavail:
|
case UserToWorldStatusWorldUnavail:
|
||||||
per->Message = 326;
|
per->base_reply.error_str_id = 326;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusSuspended:
|
case UserToWorldStatusSuspended:
|
||||||
per->Message = 337;
|
per->base_reply.error_str_id = 337;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusBanned:
|
case UserToWorldStatusBanned:
|
||||||
per->Message = 338;
|
per->base_reply.error_str_id = 338;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusWorldAtCapacity:
|
case UserToWorldStatusWorldAtCapacity:
|
||||||
per->Message = 339;
|
per->base_reply.error_str_id = 339;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusAlreadyOnline:
|
case UserToWorldStatusAlreadyOnline:
|
||||||
per->Message = 111;
|
per->base_reply.error_str_id = 111;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
per->Message = 102;
|
per->base_reply.error_str_id = 102;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.options.IsWorldTraceOn()) {
|
if (server.options.IsWorldTraceOn()) {
|
||||||
LogDebug(
|
LogDebug(
|
||||||
"Sending play response: allowed [{0}] sequence [{1}] server number [{2}] message [{3}]",
|
"Sending play response: allowed [{0}] sequence [{1}] server number [{2}] message [{3}]",
|
||||||
per->Allowed,
|
per->base_reply.success,
|
||||||
per->Sequence,
|
per->base_header.sequence,
|
||||||
per->ServerNumber,
|
per->server_number,
|
||||||
per->Message
|
per->base_reply.error_str_id
|
||||||
);
|
);
|
||||||
|
|
||||||
LogDebug("[Size: [{0}]] {1}", outapp->size, DumpPacketToString(outapp));
|
LogDebug("[Size: [{0}]] {1}", outapp->size, DumpPacketToString(outapp));
|
||||||
@ -334,8 +334,8 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac
|
|||||||
);
|
);
|
||||||
|
|
||||||
auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer;
|
auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer;
|
||||||
per->Sequence = client->GetPlaySequence();
|
per->base_header.sequence = client->GetPlaySequence();
|
||||||
per->ServerNumber = client->GetPlayServerID();
|
per->server_number = client->GetPlayServerID();
|
||||||
|
|
||||||
LogDebug(
|
LogDebug(
|
||||||
"Found sequence and play of [{0}] [{1}]",
|
"Found sequence and play of [{0}] [{1}]",
|
||||||
@ -346,7 +346,7 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac
|
|||||||
LogDebug("[Size: [{0}]] {1}", outapp->size, DumpPacketToString(outapp));
|
LogDebug("[Size: [{0}]] {1}", outapp->size, DumpPacketToString(outapp));
|
||||||
|
|
||||||
if (user_to_world_response->response > 0) {
|
if (user_to_world_response->response > 0) {
|
||||||
per->Allowed = 1;
|
per->base_reply.success = true;
|
||||||
SendClientAuth(
|
SendClientAuth(
|
||||||
client->GetConnection()->GetRemoteAddr(),
|
client->GetConnection()->GetRemoteAddr(),
|
||||||
client->GetAccountName(),
|
client->GetAccountName(),
|
||||||
@ -358,34 +358,34 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac
|
|||||||
|
|
||||||
switch (user_to_world_response->response) {
|
switch (user_to_world_response->response) {
|
||||||
case UserToWorldStatusSuccess:
|
case UserToWorldStatusSuccess:
|
||||||
per->Message = 101;
|
per->base_reply.error_str_id = 101;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusWorldUnavail:
|
case UserToWorldStatusWorldUnavail:
|
||||||
per->Message = 326;
|
per->base_reply.error_str_id = 326;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusSuspended:
|
case UserToWorldStatusSuspended:
|
||||||
per->Message = 337;
|
per->base_reply.error_str_id = 337;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusBanned:
|
case UserToWorldStatusBanned:
|
||||||
per->Message = 338;
|
per->base_reply.error_str_id = 338;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusWorldAtCapacity:
|
case UserToWorldStatusWorldAtCapacity:
|
||||||
per->Message = 339;
|
per->base_reply.error_str_id = 339;
|
||||||
break;
|
break;
|
||||||
case UserToWorldStatusAlreadyOnline:
|
case UserToWorldStatusAlreadyOnline:
|
||||||
per->Message = 111;
|
per->base_reply.error_str_id = 111;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
per->Message = 102;
|
per->base_reply.error_str_id = 102;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.options.IsTraceOn()) {
|
if (server.options.IsTraceOn()) {
|
||||||
LogDebug(
|
LogDebug(
|
||||||
"Sending play response with following data, allowed [{0}], sequence {1}, server number {2}, message {3}",
|
"Sending play response with following data, allowed [{0}], sequence {1}, server number {2}, message {3}",
|
||||||
per->Allowed,
|
per->base_reply.success,
|
||||||
per->Sequence,
|
per->base_header.sequence,
|
||||||
per->ServerNumber,
|
per->server_number,
|
||||||
per->Message
|
per->base_reply.error_str_id
|
||||||
);
|
);
|
||||||
LogDebug("[Size: [{0}]] {1}", outapp->size, DumpPacketToString(outapp));
|
LogDebug("[Size: [{0}]] {1}", outapp->size, DumpPacketToString(outapp));
|
||||||
}
|
}
|
||||||
@ -1025,6 +1025,49 @@ bool WorldServer::ValidateWorldServerAdminLogin(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WorldServer::SerializeForClientServerList(SerializeBuffer& out, bool use_local_ip) const
|
||||||
|
{
|
||||||
|
// see LoginClientServerData_Struct
|
||||||
|
if (use_local_ip) {
|
||||||
|
out.WriteString(GetLocalIP());
|
||||||
|
} else {
|
||||||
|
out.WriteString(GetRemoteIP());
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (GetServerListID())
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
out.WriteInt32(LS::ServerTypeFlags::Legends);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
out.WriteInt32(LS::ServerTypeFlags::Preferred);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
out.WriteInt32(LS::ServerTypeFlags::Standard);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteUInt32(GetServerId());
|
||||||
|
out.WriteString(GetServerLongName());
|
||||||
|
out.WriteString("us"); // country code
|
||||||
|
out.WriteString("en"); // language code
|
||||||
|
|
||||||
|
// 0 = Up, 1 = Down, 2 = Up, 3 = down, 4 = locked, 5 = locked(down)
|
||||||
|
if (GetStatus() < 0) {
|
||||||
|
if (GetZonesBooted() == 0) {
|
||||||
|
out.WriteInt32(LS::ServerStatusFlags::Down);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
out.WriteInt32(LS::ServerStatusFlags::Locked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
out.WriteInt32(LS::ServerStatusFlags::Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteUInt32(GetPlayersOnline());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param in_server_list_id
|
* @param in_server_list_id
|
||||||
* @return
|
* @return
|
||||||
|
|||||||
@ -150,6 +150,8 @@ public:
|
|||||||
bool HandleNewLoginserverRegisteredOnly(Database::DbWorldRegistration &world_registration);
|
bool HandleNewLoginserverRegisteredOnly(Database::DbWorldRegistration &world_registration);
|
||||||
bool HandleNewLoginserverInfoUnregisteredAllowed(Database::DbWorldRegistration &world_registration);
|
bool HandleNewLoginserverInfoUnregisteredAllowed(Database::DbWorldRegistration &world_registration);
|
||||||
|
|
||||||
|
void SerializeForClientServerList(class SerializeBuffer& out, bool use_local_ip) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user