//===========================================================================// // // Purpose: Implementation of the rcon server. // //===========================================================================// #include "core/stdafx.h" #include "tier1/cmd.h" #include "tier1/cvar.h" #include "tier1/NetAdr.h" #include "tier2/socketcreator.h" #include "engine/net.h" #include "engine/server/sv_rcon.h" #include "protoc/sv_rcon.pb.h" #include "protoc/cl_rcon.pb.h" #include "mathlib/sha256.h" #include "common/igameserverdata.h" //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CRConServer::CRConServer(void) : m_bInitialized(false) , m_nConnIndex(0) { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CRConServer::~CRConServer(void) { } //----------------------------------------------------------------------------- // Purpose: NETCON systems init //----------------------------------------------------------------------------- void CRConServer::Init(void) { if (!m_bInitialized) { if (!this->SetPassword(rcon_password->GetString())) { return; } } m_Address.SetFromString(Format("[%s]:%i", NET_IPV6_UNSPEC, hostport->GetInt()).c_str(), true); m_Socket.CreateListenSocket(m_Address); DevMsg(eDLL_T::SERVER, "Remote server access initialized ('%s')\n", m_Address.ToString()); m_bInitialized = true; } //----------------------------------------------------------------------------- // Purpose: NETCON systems shutdown //----------------------------------------------------------------------------- void CRConServer::Shutdown(void) { m_Socket.CloseAllAcceptedSockets(); if (m_Socket.IsListening()) { m_Socket.CloseListenSocket(); } m_bInitialized = false; } //----------------------------------------------------------------------------- // Purpose: run tasks for the RCON server //----------------------------------------------------------------------------- void CRConServer::Think(void) { const int nCount = m_Socket.GetAcceptedSocketCount(); // Close redundant sockets if there are too many except for whitelisted and authenticated. if (nCount >= sv_rcon_maxsockets->GetInt()) { for (m_nConnIndex = nCount - 1; m_nConnIndex >= 0; m_nConnIndex--) { const netadr_t& netAdr = m_Socket.GetAcceptedSocketAddress(m_nConnIndex); if (!m_WhiteListAddress.CompareAdr(netAdr)) { const CConnectedNetConsoleData* pData = m_Socket.GetAcceptedSocketData(m_nConnIndex); if (!pData->m_bAuthorized) { this->CloseConnection(); } } } } // Create a new listen socket if authenticated connection is closed. if (nCount == 0) { if (!m_Socket.IsListening()) { m_Socket.CreateListenSocket(m_Address); } } } //----------------------------------------------------------------------------- // Purpose: changes the password // Input : *pszPassword - // Output : true on success, false otherwise //----------------------------------------------------------------------------- bool CRConServer::SetPassword(const char* pszPassword) { m_bInitialized = false; m_Socket.CloseAllAcceptedSockets(); const size_t nLen = std::strlen(pszPassword); if (nLen < RCON_MIN_PASSWORD_LEN) { if (nLen > NULL) { Warning(eDLL_T::SERVER, "Remote server access requires a password of at least %i characters\n", RCON_MIN_PASSWORD_LEN); } this->Shutdown(); return false; } m_svPasswordHash = sha256(pszPassword); DevMsg(eDLL_T::SERVER, "Password hash ('%s')\n", m_svPasswordHash.c_str()); m_bInitialized = true; return true; } //----------------------------------------------------------------------------- // Purpose: sets the white list address // Input : *pszAddress - // Output : true on success, false otherwise //----------------------------------------------------------------------------- bool CRConServer::SetWhiteListAddress(const char* pszAddress) { return m_WhiteListAddress.SetFromString(pszAddress); } //----------------------------------------------------------------------------- // Purpose: server RCON main loop (run this every frame) //----------------------------------------------------------------------------- void CRConServer::RunFrame(void) { if (m_bInitialized) { m_Socket.RunFrame(); this->Think(); this->Recv(); } } //----------------------------------------------------------------------------- // Purpose: send message to all connected sockets // Input : *svMessage - //----------------------------------------------------------------------------- void CRConServer::Send(const std::string& svMessage) const { std::ostringstream ssSendBuf; const u_long nLen = htonl(static_cast(svMessage.size())); ssSendBuf.write(reinterpret_cast(&nLen), sizeof(u_long)); ssSendBuf.write(svMessage.data(), svMessage.size()); const int nCount = m_Socket.GetAcceptedSocketCount(); for (int i = nCount - 1; i >= 0; i--) { CConnectedNetConsoleData* pData = m_Socket.GetAcceptedSocketData(i); if (pData->m_bAuthorized) { ::send(pData->m_hSocket, ssSendBuf.str().data(), static_cast(ssSendBuf.str().size()), MSG_NOSIGNAL); } } } //----------------------------------------------------------------------------- // Purpose: send message to specific connected socket // Input : hSocket - // *svMessage - //----------------------------------------------------------------------------- void CRConServer::Send(const SocketHandle_t hSocket, const std::string& svMessage) const { std::ostringstream ssSendBuf; const u_long nLen = htonl(static_cast(svMessage.size())); ssSendBuf.write(reinterpret_cast(&nLen), sizeof(u_long)); ssSendBuf.write(svMessage.data(), svMessage.size()); ::send(hSocket, ssSendBuf.str().data(), static_cast(ssSendBuf.str().size()), MSG_NOSIGNAL); } //----------------------------------------------------------------------------- // Purpose: send serialized message to all connected sockets // Input : *responseMsg - // *responseVal - // responseType - // nMessageId - // nMessageType - //----------------------------------------------------------------------------- void CRConServer::Send(const std::string& responseMsg, const std::string& responseVal, const sv_rcon::response_t responseType, const int nMessageId, const int nMessageType) { if (this->ShouldSend(responseType)) { this->Send(this->Serialize(responseMsg, responseVal, responseType, nMessageId, nMessageType)); } } //----------------------------------------------------------------------------- // Purpose: send serialized message to specific connected socket // Input : hSocket - // *responseMsg - // *responseVal - // responseType - // nMessageId - // nMessageType - //----------------------------------------------------------------------------- void CRConServer::Send(const SocketHandle_t hSocket, const std::string& responseMsg, const std::string& responseVal, const sv_rcon::response_t responseType, const int nMessageId, const int nMessageType) { if (this->ShouldSend(responseType)) { this->Send(hSocket, this->Serialize(responseMsg, responseVal, responseType, nMessageId, nMessageType)); } } //----------------------------------------------------------------------------- // Purpose: receive message //----------------------------------------------------------------------------- void CRConServer::Recv(void) { const int nCount = m_Socket.GetAcceptedSocketCount(); static char szRecvBuf[1024]; for (m_nConnIndex = nCount - 1; m_nConnIndex >= 0; m_nConnIndex--) { CConnectedNetConsoleData* pData = m_Socket.GetAcceptedSocketData(m_nConnIndex); {////////////////////////////////////////////// if (this->CheckForBan(pData)) { this->Send(pData->m_hSocket, this->Serialize(s_pszBannedMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH, static_cast(eDLL_T::NETCON))); this->CloseConnection(); continue; } const int nPendingLen = ::recv(pData->m_hSocket, szRecvBuf, sizeof(char), MSG_PEEK); if (nPendingLen == SOCKET_ERROR && m_Socket.IsSocketBlocking()) { continue; } if (nPendingLen <= 0) // EOF or error. { this->CloseConnection(); continue; } }////////////////////////////////////////////// u_long nReadLen; // Find out how much we have to read. ::ioctlsocket(pData->m_hSocket, FIONREAD, &nReadLen); while (nReadLen > 0) { const int nRecvLen = ::recv(pData->m_hSocket, szRecvBuf, MIN(sizeof(szRecvBuf), nReadLen), MSG_NOSIGNAL); if (nRecvLen == 0) // Socket was closed. { this->CloseConnection(); break; } if (nRecvLen < 0 && !m_Socket.IsSocketBlocking()) { Error(eDLL_T::SERVER, NO_ERROR, "RCON Cmd: recv error (%s)\n", NET_ErrorString(WSAGetLastError())); break; } nReadLen -= nRecvLen; // Process what we've got. this->ProcessBuffer(szRecvBuf, nRecvLen, pData); } } } //----------------------------------------------------------------------------- // Purpose: serializes input // Input : *responseMsg - // *responseVal - // responseType - // nMessageId - // nMessageType - // Output : serialized results as string //----------------------------------------------------------------------------- std::string CRConServer::Serialize(const std::string& responseMsg, const std::string& responseVal, const sv_rcon::response_t responseType, const int nMessageId, const int nMessageType) const { sv_rcon::response sv_response; sv_response.set_messageid(nMessageId); sv_response.set_messagetype(nMessageType); sv_response.set_responsetype(responseType); switch (responseType) { case sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH: case sv_rcon::response_t::SERVERDATA_RESPONSE_CONSOLE_LOG: { sv_response.set_responsemsg(responseMsg); sv_response.set_responseval(responseVal); break; } default: { break; } } return sv_response.SerializeAsString(); } //----------------------------------------------------------------------------- // Purpose: de-serializes input // Input : *svBuf - // Output : de-serialized object //----------------------------------------------------------------------------- cl_rcon::request CRConServer::Deserialize(const std::string& svBuf) const { cl_rcon::request cl_request; cl_request.ParseFromArray(svBuf.data(), static_cast(svBuf.size())); return cl_request; } //----------------------------------------------------------------------------- // Purpose: authenticate new connections // Input : *cl_request - // *pData - //----------------------------------------------------------------------------- void CRConServer::Authenticate(const cl_rcon::request& cl_request, CConnectedNetConsoleData* pData) { if (pData->m_bAuthorized) { return; } else // Authorize. { if (this->Comparator(cl_request.requestmsg())) { pData->m_bAuthorized = true; m_Socket.CloseListenSocket(); this->CloseNonAuthConnection(); this->Send(pData->m_hSocket, this->Serialize(s_pszAuthMessage, sv_rcon_sendlogs->GetString(), sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH, static_cast(eDLL_T::NETCON))); } else // Bad password. { const netadr_t netAdr = m_Socket.GetAcceptedSocketAddress(m_nConnIndex); if (sv_rcon_debug->GetBool()) { DevMsg(eDLL_T::SERVER, "Bad RCON password attempt from '%s'\n", netAdr.ToString()); } this->Send(pData->m_hSocket, this->Serialize(s_pszWrongPwMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH, static_cast(eDLL_T::NETCON))); pData->m_bAuthorized = false; pData->m_bValidated = false; pData->m_nFailedAttempts++; } } } //----------------------------------------------------------------------------- // Purpose: sha256 hashed password comparison // Input : svCompare - // Output : true if matches, false otherwise //----------------------------------------------------------------------------- bool CRConServer::Comparator(std::string svPassword) const { svPassword = sha256(svPassword); if (sv_rcon_debug->GetBool()) { DevMsg(eDLL_T::SERVER, "+---------------------------------------------------------------------------+\n"); DevMsg(eDLL_T::SERVER, "[ Server: '%s']\n", m_svPasswordHash.c_str()); DevMsg(eDLL_T::SERVER, "[ Client: '%s']\n", svPassword.c_str()); DevMsg(eDLL_T::SERVER, "+---------------------------------------------------------------------------+\n"); } if (std::memcmp(svPassword.data(), m_svPasswordHash.data(), SHA256::DIGEST_SIZE) == 0) { return true; } return false; } //----------------------------------------------------------------------------- // Purpose: parses input response buffer using length-prefix framing // Input : *pRecvBuf - // nRecvLen - // *pData - //----------------------------------------------------------------------------- void CRConServer::ProcessBuffer(const char* pRecvBuf, int nRecvLen, CConnectedNetConsoleData* pData) { while (nRecvLen > 0) { if (pData->m_nPayloadLen) { if (pData->m_nPayloadRead < pData->m_nPayloadLen) { pData->m_RecvBuffer[pData->m_nPayloadRead++] = *pRecvBuf; pRecvBuf++; nRecvLen--; } if (pData->m_nPayloadRead == pData->m_nPayloadLen) { this->ProcessMessage(this->Deserialize(std::string( reinterpret_cast(pData->m_RecvBuffer.data()), pData->m_nPayloadLen))); pData->m_nPayloadLen = 0; pData->m_nPayloadRead = 0; } } else if (pData->m_nPayloadRead+1 <= sizeof(int)) // Read size field. { pData->m_RecvBuffer[pData->m_nPayloadRead++] = *pRecvBuf; pRecvBuf++; nRecvLen--; } else // Build prefix. { pData->m_nPayloadLen = int(ntohl(*reinterpret_cast(&pData->m_RecvBuffer[0]))); pData->m_nPayloadRead = 0; if (!pData->m_bAuthorized) { if (pData->m_nPayloadLen > MAX_NETCONSOLE_INPUT_LEN) { this->CloseConnection(); // Sending large messages while not authenticated. break; } } if (pData->m_nPayloadLen < 0 || pData->m_nPayloadLen > pData->m_RecvBuffer.max_size()) { Error(eDLL_T::SERVER, NO_ERROR, "RCON Cmd: sync error (%d)\n", pData->m_nPayloadLen); this->CloseConnection(); // Out of sync (irrecoverable). break; } else { pData->m_RecvBuffer.resize(pData->m_nPayloadLen); } } } } //----------------------------------------------------------------------------- // Purpose: processes received message // Input : *cl_request - //----------------------------------------------------------------------------- void CRConServer::ProcessMessage(const cl_rcon::request& cl_request) { CConnectedNetConsoleData* pData = m_Socket.GetAcceptedSocketData(m_nConnIndex); if (!pData->m_bAuthorized && cl_request.requesttype() != cl_rcon::request_t::SERVERDATA_REQUEST_AUTH) { // Notify net console that authentication is required. this->Send(pData->m_hSocket, this->Serialize(s_pszNoAuthMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH, static_cast(eDLL_T::NETCON))); pData->m_bValidated = false; pData->m_nIgnoredMessage++; return; } switch (cl_request.requesttype()) { case cl_rcon::request_t::SERVERDATA_REQUEST_AUTH: { this->Authenticate(cl_request, pData); break; } case cl_rcon::request_t::SERVERDATA_REQUEST_EXECCOMMAND: { if (pData->m_bAuthorized) // Only execute if auth was successful. { this->Execute(cl_request, false); } break; } case cl_rcon::request_t::SERVERDATA_REQUEST_SETVALUE: { if (pData->m_bAuthorized) { this->Execute(cl_request, true); } break; } case cl_rcon::request_t::SERVERDATA_REQUEST_SEND_CONSOLE_LOG: { if (pData->m_bAuthorized) { sv_rcon_sendlogs->SetValue(cl_request.requestval().c_str()); } break; } default: { break; } } } //----------------------------------------------------------------------------- // Purpose: execute commands issued from net console // Input : *cl_request - // bConVar - //----------------------------------------------------------------------------- void CRConServer::Execute(const cl_rcon::request& cl_request, const bool bConVar) const { if (bConVar) { ConVar* pConVar = g_pCVar->FindVar(cl_request.requestmsg().c_str()); if (pConVar) // Only run if this is a ConVar. { pConVar->SetValue(cl_request.requestval().c_str()); } } else // Execute command with "". { Cbuf_AddText(Cbuf_GetCurrentPlayer(), cl_request.requestmsg().c_str(), cmd_source_t::kCommandSrcCode); } } //----------------------------------------------------------------------------- // Purpose: checks for amount of failed attempts and bans net console accordingly // Input : *pData - //----------------------------------------------------------------------------- bool CRConServer::CheckForBan(CConnectedNetConsoleData* pData) { if (pData->m_bValidated) { return false; } const netadr_t netAdr = m_Socket.GetAcceptedSocketAddress(m_nConnIndex); const char* szNetAdr = netAdr.ToString(true); if (m_BannedList.size() >= RCON_MAX_BANNEDLIST_SIZE) { const char* pszWhiteListAddress = sv_rcon_whitelist_address->GetString(); if (!pszWhiteListAddress[0]) { DevMsg(eDLL_T::SERVER, "Banned list overflowed; please use a whitelist address. RCON shutting down...\n"); this->Shutdown(); return true; } // Only allow whitelisted at this point. if (!m_WhiteListAddress.CompareAdr(netAdr)) { if (sv_rcon_debug->GetBool()) { DevMsg(eDLL_T::SERVER, "Banned list is full; dropping '%s'\n", szNetAdr); } return true; } } pData->m_bValidated = true; // Check if IP is in the banned list. if (m_BannedList.find(szNetAdr) != m_BannedList.end()) { return true; } // Check if net console has reached maximum number of attempts > add to banned list. if (pData->m_nFailedAttempts >= sv_rcon_maxfailures->GetInt() || pData->m_nIgnoredMessage >= sv_rcon_maxignores->GetInt()) { // Don't add white listed address to banned list. if (szNetAdr == sv_rcon_whitelist_address->GetString()) { pData->m_nFailedAttempts = 0; pData->m_nIgnoredMessage = 0; return false; } DevMsg(eDLL_T::SERVER, "Banned '%s' for RCON hacking attempts\n", szNetAdr); m_BannedList.insert(szNetAdr); return true; } return false; } //----------------------------------------------------------------------------- // Purpose: close specific connection //----------------------------------------------------------------------------- void CRConServer::CloseConnection(void) // NETMGR { CConnectedNetConsoleData* pData = m_Socket.GetAcceptedSocketData(m_nConnIndex); if (pData->m_bAuthorized) { // Inform server owner when authenticated connection has been closed. netadr_t netAdr = m_Socket.GetAcceptedSocketAddress(m_nConnIndex); DevMsg(eDLL_T::SERVER, "Net console '%s' closed RCON connection\n", netAdr.ToString()); } m_Socket.CloseAcceptedSocket(m_nConnIndex); } //----------------------------------------------------------------------------- // Purpose: close all connections except for authenticated //----------------------------------------------------------------------------- void CRConServer::CloseNonAuthConnection(void) { int nCount = m_Socket.GetAcceptedSocketCount(); for (int i = nCount - 1; i >= 0; i--) { CConnectedNetConsoleData* pData = m_Socket.GetAcceptedSocketData(i); if (!pData->m_bAuthorized) { m_Socket.CloseAcceptedSocket(i); } } } //----------------------------------------------------------------------------- // Purpose: checks if this message should be send or not // Output : true if it should send, false otherwise //----------------------------------------------------------------------------- bool CRConServer::ShouldSend(const sv_rcon::response_t responseType) const { if (!this->IsInitialized() || !m_Socket.GetAcceptedSocketCount()) { // Not initialized or no sockets... return false; } if (responseType == sv_rcon::response_t::SERVERDATA_RESPONSE_CONSOLE_LOG) { if (!sv_rcon_sendlogs->GetBool() || !m_Socket.GetAuthorizedSocketCount()) { // Disabled or no authorized clients to send to... return false; } } return true; } //----------------------------------------------------------------------------- // Purpose: checks if server rcon is initialized // Output : true if initialized, false otherwise //----------------------------------------------------------------------------- bool CRConServer::IsInitialized(void) const { return m_bInitialized; } /////////////////////////////////////////////////////////////////////////////// CRConServer g_RCONServer; CRConServer* RCONServer() // Singleton RCON Server. { return &g_RCONServer; }