/* * Copyright (C) 2005-2008 MaNGOS * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** \file \ingroup u2w */ #include "Common.h" #include "Log.h" #include "Opcodes.h" #include "Database/DatabaseEnv.h" #include "Auth/Sha1.h" #include "WorldPacket.h" #include "WorldSocket.h" #include "WorldSession.h" #include "World.h" #include "WorldSocketMgr.h" #include "Policies/SingletonImp.h" #include "WorldLog.h" #include "AddonHandler.h" #include "sockets/Utility.h" #include "Util.h" // 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 platform #if defined( __GNUC__ ) #pragma pack(1) #else #pragma pack(push,1) #endif /// Client Packet Header struct ClientPktHeader { uint16 size; uint32 cmd; }; /// Server Packet Header struct ServerPktHeader { uint16 size; uint16 cmd; }; // GCC have alternative #pragma pack() syntax and old gcc version not support pack(pop), also any gcc version not support it at some platform #if defined( __GNUC__ ) #pragma pack() #else #pragma pack(pop) #endif #define SOCKET_CHECK_PACKET_SIZE(P,S) if((P).size() < (S)) return SizeError((P),(S)); /// WorldSocket construction and initialization. WorldSocket::WorldSocket(ISocketHandler &sh): TcpSocket(sh), _cmd(0), _remaining(0), _session(NULL) { _seed = static_cast(rand32()); m_LastPingMSTime = 0; // first time it will counted as overspeed maybe, but this is not important m_OverSpeedPings = 0; if (sWorld.getConfig(CONFIG_TCP_NO_DELAY)) SetTcpNodelay(true); } /// WorldSocket destructor WorldSocket::~WorldSocket() { if(_session) _session->SetSocket(0); WorldPacket *packet; ///- Go through the to-be-sent queue and delete remaining packets while(!_sendQueue.empty()) { packet = _sendQueue.next(); delete packet; } } /// Copy the packet to the to-be-sent queue void WorldSocket::SendPacket(WorldPacket const* packet) { WorldPacket *pck = new WorldPacket(*packet); ASSERT(pck); _sendQueue.add(pck); } /// On client connection void WorldSocket::OnAccept() { ///- Add the current socket to the list of sockets to be managed (WorldSocketMgr) sWorldSocketMgr.AddSocket(this); Utility::ResolveLocal(); ///- Send a AUTH_CHALLENGE packet WorldPacket packet( SMSG_AUTH_CHALLENGE, 4 ); packet << _seed; SendPacket(&packet); } /// Read the client transmitted data void WorldSocket::OnRead() { TcpSocket::OnRead(); while(1) { ///- Read the packet header and decipher it (if needed) if (!_remaining) { if (ibuf.GetLength() < 6) break; ClientPktHeader hdr; ibuf.Read((char *)&hdr, 6); _crypt.DecryptRecv((uint8 *)&hdr, 6); _remaining = ntohs(hdr.size) - 4; _cmd = hdr.cmd; } if (ibuf.GetLength() < _remaining) break; ///- Read the remaining of the packet WorldPacket packet((uint16)_cmd, _remaining); packet.resize(_remaining); if(_remaining) ibuf.Read((char*)packet.contents(), _remaining); _remaining = 0; ///- If log of world packets is enable, log the incoming packet if( sWorldLog.LogWorld() ) { sWorldLog.Log("CLIENT:\nSOCKET: %u\nLENGTH: %u\nOPCODE: %s (0x%.4X)\nDATA:\n", (uint32)GetSocket(), packet.size(), LookupOpcodeName(packet.GetOpcode()), packet.GetOpcode()); uint32 p = 0; while (p < packet.size()) { for (uint32 j = 0; j < 16 && p < packet.size(); j++) sWorldLog.Log("%.2X ", packet[p++]); sWorldLog.Log("\n"); } sWorldLog.Log("\n\n"); } ///- If the packet is PING, KEEP_ALIVE or AUTH_SESSION, handle immediately switch (_cmd) { case CMSG_KEEP_ALIVE: break; // just ignore, network connectivity timeout preventing case CMSG_PING: { _HandlePing(packet); break; } case CMSG_AUTH_SESSION: { _HandleAuthSession(packet); break; } default: { ///- Else, put it in the world session queue for this user (need to be already authenticated) if (_session) _session->QueuePacket(packet); else sLog.outDetail("Received out of place packet with cmdid 0x%.4X", _cmd); break; } } } } /// On socket closing void WorldSocket::CloseSocket() { ///- Set CloseAndDelete flag for TcpSocket class SetCloseAndDelete(true); ///- Set _session to NULL. Prevent crashes _session = NULL; } /// On socket deleting void WorldSocket::OnDelete() { ///- Stop sending remaining data through this socket if (_session) { _session->SetSocket(NULL); // Session deleted from World session list at socket==0, This is only back reference from socket to session. _session = NULL; } ///- Remove the socket from the WorldSocketMgr list sWorldSocketMgr.RemoveSocket(this); ///- Removes socket from player queue sWorld.RemoveQueuedPlayer(this); } /// Handle the client authentication packet void WorldSocket::_HandleAuthSession(WorldPacket& recvPacket) { uint8 digest[20]; uint32 clientSeed; uint32 unk2; uint32 BuiltNumberClient; uint32 id, security; bool tbc = false; std::string account; Sha1Hash sha1; BigNumber v, s, g, N, x, I; WorldPacket packet, SendAddonPacked; BigNumber K; SOCKET_CHECK_PACKET_SIZE(recvPacket,4+4+1+4+20); ///- Read the content of the packet recvPacket >> BuiltNumberClient; // for now no use recvPacket >> unk2; recvPacket >> account; // recheck size SOCKET_CHECK_PACKET_SIZE(recvPacket,4+4+(account.size()+1)+4+20); recvPacket >> clientSeed; recvPacket.read(digest, 20); sLog.outDebug("Auth: client %u, unk2 %u, account %s, clientseed %u", BuiltNumberClient, unk2, account.c_str(), clientSeed); ///- Normalize account name //utf8ToUpperOnlyLatin(account); -- client already send account in expected form ///- Get the account information from the realmd database std::string safe_account = account; // Duplicate, else will screw the SHA hash verification below loginDatabase.escape_string(safe_account); //No SQL injection, username escaped. // 0 1 2 3 4 5 6 7 8 9 10 QueryResult *result = loginDatabase.PQuery("SELECT id, gmlevel, sessionkey, last_ip, locked, sha_pass_hash, v, s, tbc, mutetime, locale FROM account WHERE username = '%s'", safe_account.c_str()); ///- Stop if the account is not found if ( !result ) { packet.Initialize( SMSG_AUTH_RESPONSE, 1 ); packet << uint8( AUTH_UNKNOWN_ACCOUNT ); SendPacket( &packet ); sLog.outDetail( "SOCKET: Sent Auth Response (unknown account)." ); return; } Field* fields = result->Fetch(); tbc = fields[8].GetUInt8() && sWorld.getConfig(CONFIG_EXPANSION) > 0; N.SetHexStr("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7"); g.SetDword(7); I.SetHexStr(fields[5].GetString()); //In case of leading zeros in the I hash, restore them uint8 mDigest[SHA_DIGEST_LENGTH]; memset(mDigest,0,SHA_DIGEST_LENGTH); if (I.GetNumBytes() <= SHA_DIGEST_LENGTH) memcpy(mDigest,I.AsByteArray(),I.GetNumBytes()); std::reverse(mDigest,mDigest+SHA_DIGEST_LENGTH); s.SetHexStr(fields[7].GetString()); sha1.UpdateData(s.AsByteArray(), s.GetNumBytes()); sha1.UpdateData(mDigest, SHA_DIGEST_LENGTH); sha1.Finalize(); x.SetBinary(sha1.GetDigest(), sha1.GetLength()); v = g.ModExp(x, N); const char* sStr = s.AsHexStr(); //Must be freed by OPENSSL_free() const char* vStr = v.AsHexStr(); //Must be freed by OPENSSL_free() const char* vold = fields[6].GetString(); sLog.outDebug("SOCKET: (s,v) check s: %s v_old: %s v_new: %s", sStr, vold, vStr ); loginDatabase.PExecute("UPDATE account SET v = '0', s = '0' WHERE username = '%s'", safe_account.c_str()); if ( !vold || strcmp( vStr, vold ) ) { packet.Initialize( SMSG_AUTH_RESPONSE, 1 ); packet << uint8( AUTH_UNKNOWN_ACCOUNT ); SendPacket( &packet ); sLog.outDetail( "SOCKET: User not logged."); delete result; OPENSSL_free((void*)sStr); OPENSSL_free((void*)vStr); return; } OPENSSL_free((void*)sStr); OPENSSL_free((void*)vStr); ///- Re-check ip locking (same check as in realmd). if(fields[4].GetUInt8() == 1) // if ip is locked { if ( strcmp(fields[3].GetString(),GetRemoteAddress().c_str()) ) { packet.Initialize( SMSG_AUTH_RESPONSE, 1 ); packet << uint8( AUTH_FAILED ); SendPacket( &packet ); sLog.outDetail( "SOCKET: Sent Auth Response (Account IP differs)." ); delete result; return; } } id = fields[0].GetUInt32(); security = fields[1].GetUInt16(); K.SetHexStr(fields[2].GetString()); time_t mutetime = time_t(fields[9].GetUInt64()); LocaleConstant locale = LocaleConstant(fields[10].GetUInt8()); if (locale>=MAX_LOCALE) locale=LOCALE_enUS; delete result; ///- Re-check account ban (same check as in realmd) /// TO DO: why on earth do 2 checks for same thing? QueryResult *banresult = loginDatabase.PQuery("SELECT bandate,unbandate FROM account_banned WHERE id = '%u' AND active = 1", id); if(banresult) // if account banned { packet.Initialize( SMSG_AUTH_RESPONSE, 1 ); packet << uint8( AUTH_BANNED ); SendPacket( &packet ); sLog.outDetail( "SOCKET: Sent Auth Response (Account banned)." ); delete banresult; return; } ///- Check locked state for server AccountTypes allowedAccountType = sWorld.GetPlayerSecurityLimit(); if( allowedAccountType > SEC_PLAYER && security < allowedAccountType) { WorldPacket Packet(SMSG_AUTH_RESPONSE, 1); Packet << uint8(AUTH_UNAVAILABLE); SendPacket(&Packet); return; } ///- kick already loaded player with same account (if any) and remove session ///- if player is in loading and want to load again, return if(!sWorld.RemoveSession(id)) { return; } ///- Check that Key and account name are the same on client and server Sha1Hash sha; uint32 t = 0; uint32 seed = _seed; sha.UpdateData(account); sha.UpdateData((uint8 *)&t, 4); sha.UpdateData((uint8 *)&clientSeed, 4); sha.UpdateData((uint8 *)&seed, 4); sha.UpdateBigNumbers(&K, NULL); sha.Finalize(); if (memcmp(sha.GetDigest(), digest, 20)) { packet.Initialize( SMSG_AUTH_RESPONSE, 1 ); packet << uint8( AUTH_FAILED ); SendPacket( &packet ); sLog.outDetail( "SOCKET: Sent Auth Response (authentication failed)." ); return; } ///- Initialize the encryption with the Key _crypt.SetKey(&K); _crypt.Init(); ///- Send 'Auth is ok' packet.Initialize( SMSG_AUTH_RESPONSE, 1+4+1+4+1 ); packet << uint8( AUTH_OK ); packet << uint32(0); // unknown random value... packet << uint8(0); // can be 0 and 2 packet << uint32(0); // const 0 packet << uint8(tbc ? 1 : 0); // 0 - normal, 1 - TBC, must be set in database manually for each account SendPacket(&packet); ///- Create a new WorldSession for the player and add it to the World _session = new WorldSession(id, this,security,tbc,mutetime,locale); sWorld.AddSession(_session); if(sLog.IsOutDebug()) // optimize disabled debug output { sLog.outDebug( "SOCKET: Client '%s' authenticated successfully.", account.c_str() ); sLog.outDebug( "Account: '%s' Logged in from IP %s.", account.c_str(), GetRemoteAddress().c_str()); } ///- Update the last_ip in the database //No SQL injection, username escaped. std::string address = GetRemoteAddress(); loginDatabase.escape_string(address); loginDatabase.PExecute("UPDATE account SET last_ip = '%s' WHERE username = '%s'",address.c_str(), safe_account.c_str()); // do small delay (10ms) at accepting successful authed connection to prevent dropping packets by client // don't must harm anyone (let login ~100 accounts in 1 sec ;) ) #ifdef WIN32 Sleep(10); #else ZThread::Thread::sleep(10); #endif ///- Check that we do not exceed the maximum number of online players in the realm uint32 Sessions = sWorld.GetActiveAndQueuedSessionCount(); uint32 pLimit = sWorld.GetPlayerAmountLimit(); uint32 QueueSize = sWorld.GetQueueSize(); //number of players in the queue bool inQueue = false; --Sessions; //so we don't count the user trying to login as a session and queue the socket that we are using if( pLimit > 0 && Sessions >= pLimit && security == SEC_PLAYER ) { sWorld.AddQueuedPlayer(this); SendAuthWaitQue(sWorld.GetQueuePos(this)); sWorld.UpdateMaxSessionCounters(); sLog.outDetail( "PlayerQueue: %s is in Queue Position (%u).",safe_account.c_str(),++QueueSize); inQueue = true; } ///- Create and send the Addon packet if(sAddOnHandler.BuildAddonPacket(&recvPacket, &SendAddonPacked)) SendPacket(&SendAddonPacked); if(inQueue) return; sWorld.UpdateMaxSessionCounters(); // Updates the population if (pLimit > 0) { float popu = sWorld.GetActiveSessionCount(); //updated number of users on the server popu /= pLimit; popu *= 2; loginDatabase.PExecute("UPDATE realmlist SET population = '%f' WHERE id = '%d'",popu,realmID); sLog.outDetail( "Server Population (%f).",popu); } return; } /// Handle the Ping packet void WorldSocket::_HandlePing(WorldPacket& recvPacket) { uint32 ping; uint32 latency; CHECK_PACKET_SIZE(recvPacket,8); ///- Get the ping packet content recvPacket >> ping; recvPacket >> latency; if (_session ) _session->SetLatency(latency); ///- check ping speed for players if(_session && _session->GetSecurity() == SEC_PLAYER) { uint32 cur_mstime = getMSTime(); // can overflow and start from 0 uint32 diff_mstime = getMSTimeDiff(m_LastPingMSTime,cur_mstime); m_LastPingMSTime = cur_mstime; if(diff_mstime < 27000) // should be 30000 (=30 secs), add little tolerance { ++m_OverSpeedPings; uint32 max_count = sWorld.getConfig(CONFIG_MAX_OVERSPEED_PINGS); if(max_count && m_OverSpeedPings > max_count) { sLog.outBasic("Player %s from account id %u kicked for overspeed ping packets from client (non-playable connection lags or cheating) ",_session->GetPlayerName(),_session->GetAccountId()); _session->KickPlayer(); return; } } else m_OverSpeedPings = 0; } ///- And put the pong answer in the to-be-sent queue WorldPacket packet( SMSG_PONG, 4 ); packet << ping; SendPacket(&packet); return; } /// Handle the update order for the socket void WorldSocket::SendSinglePacket() { WorldPacket *packet; ServerPktHeader hdr; ///- If we have packet to send if (!_sendQueue.empty()) { packet = _sendQueue.next(); hdr.size = ntohs((uint16)packet->size() + 2); hdr.cmd = packet->GetOpcode(); if( sWorldLog.LogWorld() ) { sWorldLog.Log("SERVER:\nSOCKET: %u\nLENGTH: %u\nOPCODE: %s (0x%.4X)\nDATA:\n", (uint32)GetSocket(), packet->size(), LookupOpcodeName(packet->GetOpcode()), packet->GetOpcode()); uint32 p = 0; while (p < packet->size()) { for (uint32 j = 0; j < 16 && p < packet->size(); j++) sWorldLog.Log("%.2X ", (*packet)[p++]); sWorldLog.Log("\n"); } sWorldLog.Log("\n\n"); } ///- Encrypt (if needed) the header _crypt.EncryptSend((uint8*)&hdr, 4); ///- Send the header and body to the client TcpSocket::SendBuf((char*)&hdr, 4); if(!packet->empty()) TcpSocket::SendBuf((char*)packet->contents(), packet->size()); delete packet; } } void WorldSocket::Update(time_t diff) { const uint32 SEND_PACKETS_MAX = 100; const uint32 SEND_BUFFER_SIZE = 1024; uint8 sendBuffer[SEND_BUFFER_SIZE]; while (!_sendQueue.empty()) { bool haveBigPacket = false; uint32 bufferSize = 0; ///- While we have packets to send for (uint32 packetCount = 0; (packetCount < SEND_PACKETS_MAX) && !_sendQueue.empty(); packetCount++) { ServerPktHeader *hdr = (ServerPktHeader*)&sendBuffer[bufferSize]; // check merge possibility. WorldPacket *front = _sendQueue.front(); uint32 packetSize = front->size(); if ((sizeof(*hdr) + packetSize) > SEND_BUFFER_SIZE) { haveBigPacket = true; break; } if ((bufferSize + sizeof(*hdr) + packetSize) > sizeof(sendBuffer)) break; // can be merged WorldPacket *packet = _sendQueue.next(); hdr->size = ntohs((uint16)packetSize + 2); hdr->cmd = packet->GetOpcode(); if( sWorldLog.LogWorld() ) { sWorldLog.Log("SERVER:\nSOCKET: %u\nLENGTH: %u\nOPCODE: %s (0x%.4X)\nDATA:\n", (uint32)GetSocket(), packetSize, LookupOpcodeName(packet->GetOpcode()), packet->GetOpcode()); uint32 p = 0; while (p < packetSize) { for (uint32 j = 0; j < 16 && p < packetSize; j++) sWorldLog.Log("%.2X ", (*packet)[p++]); sWorldLog.Log("\n"); } sWorldLog.Log("\n\n"); } ///- Encrypt (if needed) the header _crypt.EncryptSend((uint8*)hdr, sizeof(*hdr)); bufferSize += sizeof(*hdr); if (packetSize) { memcpy(&sendBuffer[bufferSize], packet->contents(), packetSize); bufferSize += packetSize; } ///- Send the header and body to the client delete packet; } // send merged packets if (bufferSize) TcpSocket::SendBuf((char*)sendBuffer, bufferSize); // send too big non-merged packet if (haveBigPacket) SendSinglePacket(); } } /// Handle the authentication waiting queue (to be completed) void WorldSocket::SendAuthWaitQue(uint32 position) { if(position == 0) { WorldPacket packet( SMSG_AUTH_RESPONSE, 1 ); packet << uint8( AUTH_OK ); SendPacket(&packet); } else { WorldPacket packet( SMSG_AUTH_RESPONSE, 5 ); packet << uint8( AUTH_WAIT_QUEUE ); packet << uint32 (position); //amount of players in queue SendPacket(&packet); } } void WorldSocket::SizeError(WorldPacket const& packet, uint32 size) const { sLog.outError("Client send packet %s (%u) with size %u but expected %u (attempt crash server?), skipped", LookupOpcodeName(packet.GetOpcode()),packet.GetOpcode(),packet.size(),size); }