diff options
Diffstat (limited to 'NorthstarDedicatedTest/sv_rcon.cpp')
-rw-r--r-- | NorthstarDedicatedTest/sv_rcon.cpp | 462 |
1 files changed, 462 insertions, 0 deletions
diff --git a/NorthstarDedicatedTest/sv_rcon.cpp b/NorthstarDedicatedTest/sv_rcon.cpp new file mode 100644 index 00000000..936a7811 --- /dev/null +++ b/NorthstarDedicatedTest/sv_rcon.cpp @@ -0,0 +1,462 @@ +//===========================================================================// +// +// 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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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 "<val>". + { + 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(); |