//===========================================================================// // // Purpose: Implementation of the rcon server. // //===========================================================================// #include "pch.h" #include "concommand.h" #include "cvar.h" #include "convar.h" #include "NetAdr2.h" #include "socketcreator.h" #include "rcon_shared.h" #include "sv_rcon.h" #include "sv_rcon.pb.h" #include "cl_rcon.pb.h" #include "sha256.h" #include "gameutils.h" #include "igameserverdata.h" //----------------------------------------------------------------------------- // Purpose: NETCON systems init //----------------------------------------------------------------------------- void CRConServer::Init(void) { if (std::strlen(CVar_rcon_password->GetString()) < 8) { if (std::strlen(CVar_rcon_password->GetString()) > 0) { spdlog::info("Remote server access requires a password of at least 8 characters"); } this->Shutdown(); return; } static ConVar* hostport = g_pCVar->FindVar("hostport"); m_pAdr2 = new CNetAdr2(CVar_rcon_address->GetString(), hostport->GetString()); m_pSocket->CreateListenSocket(*m_pAdr2, false); m_svPasswordHash = sha256(CVar_rcon_password->GetString()); spdlog::info("Remote server access initialized"); m_bInitialized = true; } //----------------------------------------------------------------------------- // Purpose: NETCON systems shutdown //----------------------------------------------------------------------------- void CRConServer::Shutdown(void) { if (m_pSocket->IsListening()) { m_pSocket->CloseListenSocket(); } m_bInitialized = false; } //----------------------------------------------------------------------------- // Purpose: run tasks for the RCON server //----------------------------------------------------------------------------- void CRConServer::Think(void) { int nCount = m_pSocket->GetAcceptedSocketCount(); // Close redundant sockets if there are too many except for whitelisted and authenticated. if (nCount >= CVar_sv_rcon_maxsockets->GetInt()) { for (m_nConnIndex = nCount - 1; m_nConnIndex >= 0; m_nConnIndex--) { CNetAdr2 netAdr2 = m_pSocket->GetAcceptedSocketAddress(m_nConnIndex); if (std::strcmp(netAdr2.GetIP(true).c_str(), CVar_sv_rcon_whitelist_address->GetString()) != 0) { CConnectedNetConsoleData* pData = m_pSocket->GetAcceptedSocketData(m_nConnIndex); if (!pData->m_bAuthorized) { this->CloseConnection(); } } } } // Create a new listen socket if authenticated connection is closed. if (nCount == 0) { if (!m_pSocket->IsListening()) { m_pSocket->CreateListenSocket(*m_pAdr2, false); } } } //----------------------------------------------------------------------------- // Purpose: server RCON main loop (run this every frame) //----------------------------------------------------------------------------- void CRConServer::RunFrame(void) { if (m_bInitialized) { m_pSocket->RunFrame(); this->Think(); this->Recv(); } } //----------------------------------------------------------------------------- // Purpose: send message // Input : *svMessage - //----------------------------------------------------------------------------- void CRConServer::Send(const std::string& svMessage) const { int nCount = m_pSocket->GetAcceptedSocketCount(); for (int i = nCount - 1; i >= 0; i--) { CConnectedNetConsoleData* pData = m_pSocket->GetAcceptedSocketData(i); if (pData->m_bAuthorized) { std::string svFinal = this->Serialize(svMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_CONSOLE_LOG); ::send(pData->m_hSocket, svFinal.c_str(), static_cast(svFinal.size()), MSG_NOSIGNAL); } } } //----------------------------------------------------------------------------- // Purpose: receive message //----------------------------------------------------------------------------- void CRConServer::Recv(void) { int nCount = m_pSocket->GetAcceptedSocketCount(); static char szRecvBuf[MAX_NETCONSOLE_INPUT_LEN]{}; for (m_nConnIndex = nCount - 1; m_nConnIndex >= 0; m_nConnIndex--) { CConnectedNetConsoleData* pData = m_pSocket->GetAcceptedSocketData(m_nConnIndex); { ////////////////////////////////////////////// if (this->CheckForBan(pData)) { std::string svNoAuth = this->Serialize(s_pszBannedMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH); ::send(pData->m_hSocket, svNoAuth.c_str(), static_cast(svNoAuth.size()), MSG_NOSIGNAL); this->CloseConnection(); continue; } int nPendingLen = ::recv(pData->m_hSocket, szRecvBuf, sizeof(szRecvBuf), MSG_PEEK); if (nPendingLen == SOCKET_ERROR && m_pSocket->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) { memset(szRecvBuf, '\0', sizeof(szRecvBuf)); 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_pSocket->IsSocketBlocking()) { break; } nReadLen -= nRecvLen; // Process what we've got. this->ProcessBuffer(szRecvBuf, nRecvLen, pData); } } } //----------------------------------------------------------------------------- // Purpose: serializes input // Input : *svRspBuf - // *svRspVal - // response_t - // Output : serialized results as string //----------------------------------------------------------------------------- std::string CRConServer::Serialize(const std::string& svRspBuf, const std::string& svRspVal, sv_rcon::response_t response_t) const { sv_rcon::response sv_response; sv_response.set_responseid(-1); // TODO sv_response.set_responsetype(response_t); switch (response_t) { case sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH: { sv_response.set_responsebuf(svRspBuf); break; } case sv_rcon::response_t::SERVERDATA_RESPONSE_CONSOLE_LOG: { sv_response.set_responsebuf(svRspBuf); sv_response.set_responseval(""); break; } default: { break; } } return sv_response.SerializeAsString().append("\r"); } //----------------------------------------------------------------------------- // 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.c_str(), static_cast(svBuf.size())); return cl_request; } //----------------------------------------------------------------------------- // Purpose: authenticate new connections // Input : *cl_request - // *pData - // Todo : implement logic for key exchange instead so we never network our // password in plain text over the wire. create a cvar for this so user could // also opt out and use legacy authentication instead for older RCON clients //----------------------------------------------------------------------------- void CRConServer::Authenticate(const cl_rcon::request& cl_request, CConnectedNetConsoleData* pData) { if (pData->m_bAuthorized) { return; } else if (strcmp(cl_request.requestbuf().c_str(), "PASS") == 0) { if (this->Comparator(cl_request.requestval())) { pData->m_bAuthorized = true; m_pSocket->CloseListenSocket(); this->CloseNonAuthConnection(); std::string svAuth = this->Serialize(s_pszAuthMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH); ::send(pData->m_hSocket, svAuth.c_str(), static_cast(svAuth.size()), MSG_NOSIGNAL); } else // Bad password. { CNetAdr2 netAdr2 = m_pSocket->GetAcceptedSocketAddress(m_nConnIndex); if (CVar_sv_rcon_debug->GetBool()) { spdlog::info("Bad RCON password attempt from '{}'", netAdr2.GetIPAndPort().c_str()); } std::string svWrongPass = this->Serialize(s_pszWrongPwMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH); ::send(pData->m_hSocket, svWrongPass.c_str(), static_cast(svWrongPass.size()), MSG_NOSIGNAL); pData->m_bAuthorized = 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 (CVar_sv_rcon_debug->GetBool()) { spdlog::info("+---------------------------------------------------------------------------+"); spdlog::info("] Server: '{}'[", m_svPasswordHash.c_str()); spdlog::info("] Client: '{}'[", svPassword.c_str()); spdlog::info("+---------------------------------------------------------------------------+"); } if (memcmp(svPassword.c_str(), m_svPasswordHash.c_str(), SHA256::DIGEST_SIZE) == 0) { return true; } return false; } //----------------------------------------------------------------------------- // Purpose: handles input command buffer // Input : *pszIn - // nRecvLen - // *pData - //----------------------------------------------------------------------------- void CRConServer::ProcessBuffer(const char* pszIn, int nRecvLen, CConnectedNetConsoleData* pData) { while (nRecvLen) { switch (*pszIn) { case '\r': { if (pData->m_nCharsInCommandBuffer) { cl_rcon::request cl_request = this->Deserialize(pData->m_pszInputCommandBuffer); this->ProcessMessage(cl_request); } pData->m_nCharsInCommandBuffer = 0; break; } default: { if (pData->m_nCharsInCommandBuffer < MAX_NETCONSOLE_INPUT_LEN - 1) { pData->m_pszInputCommandBuffer[pData->m_nCharsInCommandBuffer++] = *pszIn; } break; } } pszIn++; nRecvLen--; } } //----------------------------------------------------------------------------- // Purpose: processes received message // Input : *cl_request - //----------------------------------------------------------------------------- void CRConServer::ProcessMessage(const cl_rcon::request& cl_request) { CConnectedNetConsoleData* pData = m_pSocket->GetAcceptedSocketData(m_nConnIndex); if (!pData->m_bAuthorized && cl_request.requesttype() != cl_rcon::request_t::SERVERDATA_REQUEST_AUTH) { // Notify net console that authentication is required. std::string svMessage = this->Serialize(s_pszNoAuthMessage, "", sv_rcon::response_t::SERVERDATA_RESPONSE_AUTH); ::send(pData->m_hSocket, svMessage.c_str(), static_cast(svMessage.size()), MSG_NOSIGNAL); 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: case cl_rcon::request_t::SERVERDATA_REQUEST_SETVALUE: { // Only execute if auth was succesfull. if (pData->m_bAuthorized) { this->Execute(cl_request); } break; } case cl_rcon::request_t::SERVERDATA_REQUEST_SEND_CONSOLE_LOG: { if (pData->m_bAuthorized) { CVar_sv_rcon_sendlogs->SetValue(1); } break; } default: { break; } } } //----------------------------------------------------------------------------- // Purpose: execute commands issued from net console // Input : *cl_request - //----------------------------------------------------------------------------- void CRConServer::Execute(const cl_rcon::request& cl_request) const { ConVar* pConVar = g_pCVar->FindVar(cl_request.requestbuf().c_str()); if (pConVar) { pConVar->SetValue(cl_request.requestval().c_str()); } else // Execute command with "". { std::string svExec = cl_request.requestbuf() + " \"" + cl_request.requestval() + "\""; Cbuf_AddText(Cbuf_GetCurrentPlayer(), svExec.c_str(), cmd_source_t::kCommandSrcCode); Cbuf_Execute(); } } //----------------------------------------------------------------------------- // Purpose: checks for amount of failed attempts and bans net console accordingly // Input : *pData - //----------------------------------------------------------------------------- bool CRConServer::CheckForBan(CConnectedNetConsoleData* pData) { CNetAdr2 netAdr2 = m_pSocket->GetAcceptedSocketAddress(m_nConnIndex); // Check if IP is in the ban vector. if (std::find(m_vBannedAddress.begin(), m_vBannedAddress.end(), netAdr2.GetIP(true)) != m_vBannedAddress.end()) { return true; } // Check if net console has reached maximum number of attempts and add to ban vector. if (pData->m_nFailedAttempts >= CVar_sv_rcon_maxfailures->GetInt() || pData->m_nIgnoredMessage >= CVar_sv_rcon_maxignores->GetInt()) { // Don't add whitelisted address to ban vector. if (std::strcmp(netAdr2.GetIP(true).c_str(), CVar_sv_rcon_whitelist_address->GetString()) == 0) { pData->m_nFailedAttempts = 0; pData->m_nIgnoredMessage = 0; return false; } spdlog::info("Banned '{}' for RCON hacking attempts", netAdr2.GetIPAndPort().c_str()); m_vBannedAddress.push_back(netAdr2.GetIP(true)); return true; } return false; } //----------------------------------------------------------------------------- // Purpose: close specific connection //----------------------------------------------------------------------------- void CRConServer::CloseConnection(void) // NETMGR { CConnectedNetConsoleData* pData = m_pSocket->GetAcceptedSocketData(m_nConnIndex); if (pData->m_bAuthorized) { // Inform server owner when authenticated connection has been closed. CNetAdr2 netAdr2 = m_pSocket->GetAcceptedSocketAddress(m_nConnIndex); spdlog::info("Net console '{}' closed RCON connection", netAdr2.GetIPAndPort().c_str()); } m_pSocket->CloseAcceptedSocket(m_nConnIndex); } //----------------------------------------------------------------------------- // Purpose: close all connections except for authenticated //----------------------------------------------------------------------------- void CRConServer::CloseNonAuthConnection(void) { int nCount = m_pSocket->GetAcceptedSocketCount(); for (int i = nCount - 1; i >= 0; i--) { CConnectedNetConsoleData* pData = m_pSocket->GetAcceptedSocketData(i); if (!pData->m_bAuthorized) { m_pSocket->CloseAcceptedSocket(i); } } } /////////////////////////////////////////////////////////////////////////////// CRConServer* g_pRConServer = new CRConServer();