source-engine/engine/sv_rcon.cpp
FluorescentCIAAfricanAmerican 3bf9df6b27 1
2020-04-22 12:56:21 -04:00

637 lines
19 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: implementation of the rcon server
//
//===========================================================================//
#if defined(_WIN32)
#if !defined(_X360)
#include <winsock.h>
#endif
#undef SetPort // winsock screws with the SetPort string... *sigh*
#define socklen_t int
#define MSG_NOSIGNAL 0
#elif POSIX
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <sys/ioctl.h>
#define closesocket close
#define WSAGetLastError() errno
#define ioctlsocket ioctl
#ifdef OSX
#define MSG_NOSIGNAL 0
#endif
#endif
#include <tier0/dbg.h>
#include "utlbuffer.h"
#include "server.h"
#include "sv_rcon.h"
#include "proto_oob.h" // PORT_RCON define
#include "sv_remoteaccess.h"
#include "cl_rcon.h"
#include "sv_filter.h"
#if defined( _X360 )
#include "xbox/xbox_win32stubs.h"
#endif
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#ifdef ENABLE_RPT
class CRPTServer : public CRConServer
{
typedef CRConServer BaseClass;
public:
virtual void OnSocketAccepted( SocketHandle_t hSocket, const netadr_t & netAdr, void** ppData )
{
BaseClass::OnSocketAccepted( hSocket, netAdr, ppData );
// Enable cheats on this client only
Cmd_SetRptActive( true );
}
virtual void OnSocketClosed( SocketHandle_t hSocket, const netadr_t & netAdr, void* pData )
{
Cmd_SetRptActive( false );
BaseClass::OnSocketClosed( hSocket, netAdr, pData );
}
};
static CRPTServer g_RPTServer;
CRConServer & RPTServer()
{
return g_RPTServer;
}
#endif // ENABLE_RPT
static CRConServer g_RCONServer;
CRConServer & RCONServer()
{
return g_RCONServer;
}
static void RconPasswordChanged_f( IConVar *pConVar, const char *pOldString, float flOldValue )
{
ConVarRef var( pConVar );
const char *pPassword = var.GetString();
#ifndef SWDS
RCONClient().SetPassword( pPassword );
#endif
RCONServer().SetPassword( pPassword );
}
ConVar rcon_password ( "rcon_password", "", FCVAR_SERVER_CANNOT_QUERY|FCVAR_DONTRECORD, "remote console password.", RconPasswordChanged_f );
ConVar sv_rcon_banpenalty( "sv_rcon_banpenalty", "0", 0, "Number of minutes to ban users who fail rcon authentication", true, 0, false, 0 );
ConVar sv_rcon_maxfailures( "sv_rcon_maxfailures", "10", 0, "Max number of times a user can fail rcon authentication before being banned", true, 1, true, 20 );
ConVar sv_rcon_minfailures( "sv_rcon_minfailures", "5", 0, "Number of times a user can fail rcon authentication in sv_rcon_minfailuretime before being banned", true, 1, true, 20 );
ConVar sv_rcon_minfailuretime( "sv_rcon_minfailuretime", "30", 0, "Number of seconds to track failed rcon authentications", true, 1, false, 0 );
ConVar sv_rcon_whitelist_address( "sv_rcon_whitelist_address", "", 0, "When set, rcon failed authentications will never ban this address, e.g. '127.0.0.1'" );
ConVar sv_rcon_maxpacketsize( "sv_rcon_maxpacketsize", "1024", 0, "The maximum number of bytes to allow in a command packet", true, 0, false, 0 );
ConVar sv_rcon_maxpacketbans( "sv_rcon_maxpacketbans", "1", 0, "Ban IPs for sending RCON packets exceeding the value specified in sv_rcon_maxpacketsize", true, 0, true, 1 );
//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
#pragma warning ( disable : 4355 )
CRConServer::CRConServer() : m_Socket( this )
{
}
CRConServer::CRConServer( const char *pNetAddress ) : m_Socket( this )
{
SetAddress( pNetAddress );
}
#pragma warning ( default : 4355 )
//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CRConServer::~CRConServer()
{
}
//-----------------------------------------------------------------------------
// Allows a server to request a listening client to connect to it
//-----------------------------------------------------------------------------
bool CRConServer::ConnectToListeningClient( const netadr_t &adr, bool bSingleSocket )
{
if ( m_Socket.ConnectSocket( adr, bSingleSocket ) < 0 )
{
ConWarning( "Unable to connect to remote client (%s)\n", adr.ToString() );
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: returns true if the listening socket is created and listening
//-----------------------------------------------------------------------------
bool CRConServer::IsConnected()
{
return m_Socket.IsListening();
}
void CRConServer::SetPassword( const char *pPassword )
{
m_Socket.CloseAllAcceptedSockets();
m_Password = pPassword;
}
bool CRConServer::HasPassword() const
{
return !m_Password.IsEmpty();
}
bool CRConServer::IsPassword( const char *pPassword ) const
{
// Must have a password set to allow any rconning.
if ( !HasPassword() )
return false;
// If the pw does not match, then not authed
return ( Q_strcmp( pPassword, m_Password.Get() ) == 0 );
}
//-----------------------------------------------------------------------------
// Purpose: Set the address to bind to
//-----------------------------------------------------------------------------
void CRConServer::SetAddress( const char *pNetAddress )
{
NET_StringToAdr( pNetAddress, &m_Address );
if ( m_Address.GetPort() == 0 )
{
m_Address.SetPort( PORT_RCON );
}
}
bool CRConServer::CreateSocket()
{
return m_Socket.CreateListenSocket( m_Address );
}
//-----------------------------------------------------------------------------
// Inherited from ISocketCreatorListener
//-----------------------------------------------------------------------------
bool CRConServer::ShouldAcceptSocket( SocketHandle_t hSocket, const netadr_t & netAdr )
{
return !Filter_ShouldDiscard( netAdr );
}
void CRConServer::OnSocketAccepted( SocketHandle_t hSocket, const netadr_t &netAdr, void** ppData )
{
ConnectedRConSocket_t *pNewSocket = new ConnectedRConSocket_t;
pNewSocket->lastRequestID = 0;
pNewSocket->authed = false;
pNewSocket->listenerID = g_ServerRemoteAccess.GetNextListenerID( true, &netAdr );
*ppData = pNewSocket;
}
void CRConServer::OnSocketClosed( SocketHandle_t hSocket, const netadr_t &netAdr, void* pData )
{
m_bSocketDeleted = true;
ConnectedRConSocket_t *pOldSocket = (ConnectedRConSocket_t*)( pData );
delete pOldSocket;
}
//-----------------------------------------------------------------------------
// Purpose: accept new connections and walk open sockets and handle any incoming data
//-----------------------------------------------------------------------------
void CRConServer::RunFrame()
{
m_Socket.RunFrame();
m_bSocketDeleted = false;
// handle incoming data
// NOTE: Have to iterate in reverse since we may be killing sockets
int nCount = m_Socket.GetAcceptedSocketCount();
for ( int i = nCount - 1; i >= 0; --i )
{
// process any outgoing data for this socket
ConnectedRConSocket_t *pData = GetSocketData( i );
SocketHandle_t hSocket = m_Socket.GetAcceptedSocketHandle( i );
const netadr_t& socketAdr = m_Socket.GetAcceptedSocketAddress( i );
while ( pData->m_OutstandingSends.Count() > 0 )
{
CUtlBuffer &packet = pData->m_OutstandingSends[ pData->m_OutstandingSends.Head()];
bool bSent = SendRCONResponse( i, packet.PeekGet(), packet.TellPut() - packet.TellGet(), true );
if ( bSent ) // all this packet was sent, remove it
{
pData->m_OutstandingSends.Remove( pData->m_OutstandingSends.Head() ); // delete this entry no matter what, SendRCONResponse() will re-queue if needed
}
else // must have blocked part way through, SendRCONResponse
// fixed up the queued entry
{
break;
}
}
int sendLen = g_ServerRemoteAccess.GetDataResponseSize( pData->listenerID );
if ( sendLen > 0 )
{
char sendBuf[4096];
char *pBuf = sendBuf;
bool bAllocate = ( sendLen + sizeof(int) > sizeof(sendBuf) );
if ( bAllocate )
{
pBuf = new char[sendLen + sizeof(int)];
}
memcpy( pBuf, &sendLen, sizeof(sendLen) ); // copy the size of the packet in
g_ServerRemoteAccess.ReadDataResponse( pData->listenerID, pBuf + sizeof(int), sendLen );
SendRCONResponse( i, pBuf, sendLen + sizeof(int) );
if ( bAllocate )
{
delete [] pBuf;
}
}
// check for incoming data
int pendingLen = 0;
unsigned long readLen = 0;
char ch;
pendingLen = recv( hSocket, &ch, sizeof(ch), MSG_PEEK );
if ( pendingLen == -1 && SocketWouldBlock() )
continue;
if ( pendingLen == 0 )
{
m_Socket.CloseAcceptedSocket( i );
continue;
}
if ( pendingLen < 0 )
{
//DevMsg( "RCON Cmd: peek error %s\n", NET_ErrorString(WSAGetLastError()));
m_Socket.CloseAcceptedSocket( i );
continue;
}
// find out how much we have to read
ioctlsocket( hSocket, FIONREAD, &readLen );
if ( readLen > sizeof(int) ) // we have a command to process
{
CUtlBuffer & response = pData->packetbuffer;
response.EnsureCapacity( response.TellPut() + readLen );
char *recvBuf = (char *)_alloca( min( 1024ul, readLen ) ); // a buffer used for recv()
unsigned int len = 0;
while ( len < readLen )
{
int recvLen = recv( hSocket, recvBuf , min(1024ul, readLen - len) , 0 );
if ( recvLen == 0 ) // socket was closed
{
m_Socket.CloseAcceptedSocket( i );
break;
}
if ( recvLen < 0 && !SocketWouldBlock() )
{
Warning( "RCON Cmd: recv error (%s)\n", NET_ErrorString( WSAGetLastError() ) );
break;
}
response.Put( recvBuf, recvLen );
len += recvLen;
}
response.SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
int size = response.GetInt();
if ( sv_rcon_maxpacketsize.GetInt() > 0 && size > sv_rcon_maxpacketsize.GetInt() )
{
if ( sv_rcon_maxpacketbans.GetBool() )
{
HandleFailedRconAuth( socketAdr );
}
m_Socket.CloseAcceptedSocket( i );
continue;
}
while ( size > 0 && size <= response.TellPut() - response.TellGet() )
{
SV_RedirectStart( RD_SOCKET, &socketAdr );
g_ServerRemoteAccess.WriteDataRequest( this, pData->listenerID, response.PeekGet(), size );
SV_RedirectEnd();
if ( m_bSocketDeleted )
return;
response.SeekGet( CUtlBuffer::SEEK_CURRENT, size ); // eat up the buffer we just sent
if ( response.TellPut() - response.TellGet() >= sizeof(int) )
{
size = response.GetInt(); // read how much is in this packet
}
else
{
size = 0; // finished the packet
}
}
// Check and see if socket was closed as a result of processing - this can happen if the user has entered too many passwords
int nNewCount = m_Socket.GetAcceptedSocketCount();
if ( 0 == nNewCount || i > nNewCount || pData != GetSocketData( i ) )
{
response.Purge();
break;
}
if ( size > 0 || (response.TellPut() - response.TellGet() > 0))
{
// trim the bytes that were just processed
CUtlBuffer tmpBuf;
if ( response.TellPut() - response.TellGet() > 0 )
{
tmpBuf.Put( response.PeekGet(), response.TellPut() - response.TellGet() );
}
response.Purge();
if ( size > 0 )
{
response.Put( &size, sizeof(size));
}
if ( tmpBuf.TellPut() > 0 )
{
response.Put( tmpBuf.Base(), tmpBuf.TellPut() );
}
}
else
{
response.Purge();
}
}
} // for each socket
}
//-----------------------------------------------------------------------------
// Purpose: flush the response of a network command back to a user
//-----------------------------------------------------------------------------
void CRConServer::FinishRedirect( const char *msg, const netadr_t &adr )
{
// NOTE: Has to iterate in reverse; SendRCONResponse can close sockets
int nCount = m_Socket.GetAcceptedSocketCount();
for ( int i = nCount - 1; i >= 0; --i )
{
const netadr_t& socketAdr = m_Socket.GetAcceptedSocketAddress( i );
if ( !adr.CompareAdr( socketAdr ) )
continue;
CUtlBuffer response;
// build the response
ConnectedRConSocket_t *pSocketData = GetSocketData( i );
response.PutInt(0); // the size, this gets set once we make the packet
response.PutInt(pSocketData->lastRequestID);
response.PutInt(SERVERDATA_RESPONSE_VALUE);
response.PutString(msg);
response.PutString("");
int size = response.TellPut() - sizeof(int);
response.SeekPut( CUtlBuffer::SEEK_HEAD, 0 );
response.PutInt(size); // the size
response.SeekPut( CUtlBuffer::SEEK_CURRENT, size );
// OutputDebugString( va("RCON: String is %i long\n", Q_strlen(msg)) ); // can't use DevMsg(), we are potentially inside the RedirectFlush() function
// printf("RCON: String is %i long, packet size %i\n", Q_strlen(msg), size );
SendRCONResponse( i, response.Base(), response.TellPut() );
}
}
//-----------------------------------------------------------------------------
// Purpose: set the current outstanding request ID for this connection, used by the redirect flush above
//-----------------------------------------------------------------------------
void CRConServer::SetRequestID( ra_listener_id listener, int iRequestID )
{
int nCount = m_Socket.GetAcceptedSocketCount();
for ( int i = 0; i < nCount; i++ )
{
ConnectedRConSocket_t *pSocketData = GetSocketData( i );
if ( pSocketData->listenerID == listener)
{
pSocketData->lastRequestID = iRequestID;
}
}
}
//-----------------------------------------------------------------------------
// Purpose: send a buffer to a particular connection
//-----------------------------------------------------------------------------
bool CRConServer::SendRCONResponse( int nIndex, const void *data, int len, bool fromQueue )
{
SocketHandle_t hSocket = m_Socket.GetAcceptedSocketHandle( nIndex );
if ( hSocket < 0 )
return false;
ConnectedRConSocket_t *pSocketData = GetSocketData( nIndex );
// if we already have queued data pending then just add this to the end
// of the queue
if ( !fromQueue && pSocketData->m_OutstandingSends.Count() > 0 )
{
if ( pSocketData->m_OutstandingSends.Count() > RCON_MAX_OUTSTANDING_SENDS )
{
m_Socket.CloseAcceptedSocket( nIndex );
return false;
}
int index = pSocketData->m_OutstandingSends.AddToTail();
pSocketData->m_OutstandingSends[index].Put( data, len );
return true;
}
Assert( !( fromQueue && data != (pSocketData->m_OutstandingSends[pSocketData->m_OutstandingSends.Head()].Base())));
int sendLen = 0;
while ( sendLen < len )
{
int ret = send( hSocket, (const char *)data + sendLen, len - sendLen, MSG_NOSIGNAL );
if ( ret == -1 )
{
// can't finish sending this right now, push it back
// on the TOP of the queue to be sent next time around
if ( !SocketWouldBlock() )
{
m_Socket.CloseAcceptedSocket( nIndex );
return false;
}
if ( !fromQueue ) // we don't have an entry for this
// yet, add a new one
{
int index = pSocketData->m_OutstandingSends.AddToHead();
pSocketData->m_OutstandingSends[index].Put( (void *)((char *)data + sendLen), len - sendLen );
}
else // update the existing queued item to show we
// sent some of it (we only ever send the head of the list)
{
pSocketData->m_OutstandingSends[pSocketData->m_OutstandingSends.Head()].SeekGet( CUtlBuffer::SEEK_CURRENT, sendLen );
}
return false;
}
else if ( ret > 0 )
{
sendLen += ret;
}
}
// printf("RCON: Sending packet %i in len\n", len);
// OutputDebugString( va("RCON: Sending packet %i in len\n", len) ); // can't use DevMsg(), we are potentially inside the RedirectFlush() function
return true;
}
//-----------------------------------------------------------------------------
// Purpose: compares failed rcons based on most recent failure time
//-----------------------------------------------------------------------------
bool CRConServer::FailedRCon_t::operator<(const struct CRConServer::FailedRCon_t &rhs) const
{
int myTime = 0;
int rhsTime = 0;
if ( badPasswordTimes.Count() )
myTime = badPasswordTimes[ badPasswordTimes.Count() - 1 ];
if ( rhs.badPasswordTimes.Count() )
rhsTime = rhs.badPasswordTimes[ rhs.badPasswordTimes.Count() - 1 ];
return myTime < rhsTime;
}
//-----------------------------------------------------------------------------
// Purpose: tracks failed rcon attempts and bans repeat offenders
//-----------------------------------------------------------------------------
bool CRConServer::HandleFailedRconAuth( const netadr_t & adr )
{
if ( sv_rcon_whitelist_address.GetString()[0] )
{
if ( !V_strcmp( adr.ToString( true ), sv_rcon_whitelist_address.GetString() ) )
{
ConMsg( "Rcon auth failed from rcon whitelist address %s\n", adr.ToString() );
return false;
}
}
int i;
FailedRCon_t *failedRcon = NULL;
int nCount = m_failedRcons.Count();
for ( i=0; i < nCount; ++i )
{
if ( adr.CompareAdr( m_failedRcons[i].adr, true ) )
{
failedRcon = &m_failedRcons[i];
break;
}
}
if ( !failedRcon )
{
// remove an old rcon if necessary
if ( nCount >= 32 )
{
// look for the one with the oldest failure
int indexToRemove = -1;
for ( i=0; i < nCount; ++i )
{
if ( indexToRemove < 0 || m_failedRcons[i] < m_failedRcons[indexToRemove] )
{
indexToRemove = i;
}
}
if ( indexToRemove >= 0 )
{
m_failedRcons.Remove( indexToRemove );
}
}
// add the new rcon
int index = m_failedRcons.AddToTail();
failedRcon = &m_failedRcons[index];
failedRcon->adr = adr;
failedRcon->badPasswordCount = 0;
failedRcon->badPasswordTimes.RemoveAll();
}
// update this failed rcon
++failedRcon->badPasswordCount;
failedRcon->badPasswordTimes.AddToTail( sv.GetTime() );
// remove old failure times (sv_rcon_maxfailures is limited to 20, so we won't be hurting anything by pruning)
while ( failedRcon->badPasswordTimes.Count() > 20 )
{
failedRcon->badPasswordTimes.Remove( 0 );
}
// sanity-check the rcon banning cvars
if ( sv_rcon_maxfailures.GetInt() < sv_rcon_minfailures.GetInt() )
{
int temp = sv_rcon_maxfailures.GetInt();
sv_rcon_maxfailures.SetValue( sv_rcon_minfailures.GetInt() );
sv_rcon_minfailures.SetValue( temp );
}
// ConMsg( "%d of %d bad password times tracked\n", failedRcon->badPasswordTimes.Count(), failedRcon->badPasswordCount );
// ConMsg( "min=%d, max=%d, time=%.2f\n", sv_rcon_minfailures.GetInt(), sv_rcon_maxfailures.GetInt(), sv_rcon_minfailuretime.GetFloat() );
// check if the user should be banned based on total failed attempts
if ( failedRcon->badPasswordCount > sv_rcon_maxfailures.GetInt() )
{
ConMsg( "Banning %s for rcon hacking attempts\n", failedRcon->adr.ToString( true ) );
Cbuf_AddText( va( "addip %i %s\n", sv_rcon_banpenalty.GetInt(), failedRcon->adr.ToString( true ) ) );
Cbuf_Execute();
return true;
}
// check if the user should be banned based on recent failed attempts
int recentFailures = 0;
for ( i=failedRcon->badPasswordTimes.Count()-1; i>=0; --i )
{
if ( failedRcon->badPasswordTimes[i] + sv_rcon_minfailuretime.GetFloat() >= sv.GetTime() )
{
++recentFailures;
}
}
if ( recentFailures > sv_rcon_minfailures.GetInt() )
{
ConMsg( "Banning %s for rcon hacking attempts\n", failedRcon->adr.ToString( true ) );
Cbuf_AddText( va( "addip %i %s\n", sv_rcon_banpenalty.GetInt(), failedRcon->adr.ToString( true ) ) );
Cbuf_Execute();
return true;
}
return false;
}
bool CRConServer::BCloseAcceptedSocket( ra_listener_id listener )
{
int nCount = m_Socket.GetAcceptedSocketCount();
for ( int i = 0; i < nCount; i++ )
{
ConnectedRConSocket_t *pSocketData = GetSocketData( i );
if ( pSocketData->listenerID == listener )
{
m_Socket.CloseAcceptedSocket( i );
return true;
}
}
return false;
}