From bc421842c6a052cbc74d3cc28d5edb263780350f Mon Sep 17 00:00:00 2001
From: "madmaxoft@gmail.com"
 <madmaxoft@gmail.com@0a769ca7-a7f5-676a-18bf-c427514a06d6>
Date: Thu, 27 Jun 2013 15:14:20 +0000
Subject: [PATCH] Added a basic RCON protocol

git-svn-id: http://mc-server.googlecode.com/svn/trunk@1628 0a769ca7-a7f5-676a-18bf-c427514a06d6
---
 VC2008/MCServer.vcproj            |   8 +
 source/OSSupport/ListenThread.cpp |  25 +--
 source/OSSupport/ListenThread.h   |   7 +-
 source/RCONServer.cpp             | 289 ++++++++++++++++++++++++++++++
 source/RCONServer.h               | 106 +++++++++++
 source/Server.cpp                 |   7 +-
 source/Server.h                   |  23 +--
 7 files changed, 436 insertions(+), 29 deletions(-)
 create mode 100644 source/RCONServer.cpp
 create mode 100644 source/RCONServer.h

diff --git a/VC2008/MCServer.vcproj b/VC2008/MCServer.vcproj
index 904c0bd3b..d1e1a5e14 100644
--- a/VC2008/MCServer.vcproj
+++ b/VC2008/MCServer.vcproj
@@ -610,6 +610,14 @@
 				RelativePath="..\source\ProbabDistrib.h"
 				>
 			</File>
+			<File
+				RelativePath="..\source\RCONServer.cpp"
+				>
+			</File>
+			<File
+				RelativePath="..\source\RCONServer.h"
+				>
+			</File>
 			<File
 				RelativePath="..\source\ReferenceManager.cpp"
 				>
diff --git a/source/OSSupport/ListenThread.cpp b/source/OSSupport/ListenThread.cpp
index bc14aa0ba..70c1159a4 100644
--- a/source/OSSupport/ListenThread.cpp
+++ b/source/OSSupport/ListenThread.cpp
@@ -10,10 +10,11 @@
 
 
 
-cListenThread::cListenThread(cCallback & a_Callback, cSocket::eFamily a_Family) :
-	super("ListenThread"),
+cListenThread::cListenThread(cCallback & a_Callback, cSocket::eFamily a_Family, const AString & a_ServiceName) :
+	super(Printf("ListenThread %s", a_ServiceName.c_str())),
 	m_Callback(a_Callback),
-	m_Family(a_Family)
+	m_Family(a_Family),
+	m_ServiceName(a_ServiceName)
 {
 }
 
@@ -105,11 +106,11 @@ bool cListenThread::CreateSockets(const AString & a_PortsString)
 		return false;
 	}
 	
-	const char * FamilyStr = "";
+	AString FamilyStr = m_ServiceName;
 	switch (m_Family)
 	{
-		case cSocket::IPv4: FamilyStr = "IPv4"; break;
-		case cSocket::IPv6: FamilyStr = "IPv6"; break;
+		case cSocket::IPv4: FamilyStr.append(" IPv4"); break;
+		case cSocket::IPv6: FamilyStr.append(" IPv6"); break;
 		default:
 		{
 			ASSERT(!"Unknown address family");
@@ -122,13 +123,13 @@ bool cListenThread::CreateSockets(const AString & a_PortsString)
 		int Port = atoi(itr->c_str());
 		if ((Port <= 0) || (Port > 65535))
 		{
-			LOGWARNING("%s: Invalid port specified: \"%s\".", FamilyStr, itr->c_str());
+			LOGWARNING("%s: Invalid port specified: \"%s\".", FamilyStr.c_str(), itr->c_str());
 			continue;
 		}
 		m_Sockets.push_back(cSocket::CreateSocket(m_Family));
 		if (!m_Sockets.back().IsValid())
 		{
-			LOGWARNING("%s: Cannot create listening socket for port %d: \"%s\"", FamilyStr, Port, cSocket::GetLastErrorString().c_str());
+			LOGWARNING("%s: Cannot create listening socket for port %d: \"%s\"", FamilyStr.c_str(), Port, cSocket::GetLastErrorString().c_str());
 			m_Sockets.pop_back();
 			continue;
 		}
@@ -137,7 +138,7 @@ bool cListenThread::CreateSockets(const AString & a_PortsString)
 		{
 			if (!m_Sockets.back().SetReuseAddress())
 			{
-				LOG("%s: Port %d cannot reuse addr, syscall failed: \"%s\".", FamilyStr, Port, cSocket::GetLastErrorString().c_str());
+				LOG("%s: Port %d cannot reuse addr, syscall failed: \"%s\".", FamilyStr.c_str(), Port, cSocket::GetLastErrorString().c_str());
 			}
 		}
 		
@@ -155,19 +156,19 @@ bool cListenThread::CreateSockets(const AString & a_PortsString)
 		}
 		if (!res)
 		{
-			LOGWARNING("%s: Cannot bind port %d: \"%s\".", FamilyStr, Port, cSocket::GetLastErrorString().c_str());
+			LOGWARNING("%s: Cannot bind port %d: \"%s\".", FamilyStr.c_str(), Port, cSocket::GetLastErrorString().c_str());
 			m_Sockets.pop_back();
 			continue;
 		}
 		
 		if (!m_Sockets.back().Listen())
 		{
-			LOGWARNING("%s: Cannot listen on port %d: \"%s\".", FamilyStr, Port, cSocket::GetLastErrorString().c_str());
+			LOGWARNING("%s: Cannot listen on port %d: \"%s\".", FamilyStr.c_str(), Port, cSocket::GetLastErrorString().c_str());
 			m_Sockets.pop_back();
 			continue;
 		}
 		
-		LOGINFO("%s: Port %d is open for connections", FamilyStr, Port);
+		LOGINFO("%s: Port %d is open for connections", FamilyStr.c_str(), Port);
 	}  // for itr - Ports[]
 	
 	return !(m_Sockets.empty());
diff --git a/source/OSSupport/ListenThread.h b/source/OSSupport/ListenThread.h
index 952cc8a3f..c29140ed3 100644
--- a/source/OSSupport/ListenThread.h
+++ b/source/OSSupport/ListenThread.h
@@ -37,7 +37,7 @@ public:
 		virtual void OnConnectionAccepted(cSocket & a_Socket) = 0;
 	} ;
 	
-	cListenThread(cCallback & a_Callback, cSocket::eFamily a_Family);
+	cListenThread(cCallback & a_Callback, cSocket::eFamily a_Family, const AString & a_ServiceName = "");
 	~cListenThread();
 	
 	/// Creates all the sockets, returns trus if successful, false if not.
@@ -62,8 +62,13 @@ protected:
 	/// Sockets that are being monitored
 	cSockets m_Sockets;
 	
+	/// If set to true, the SO_REUSEADDR socket option is set to true
 	bool m_ShouldReuseAddr;
 	
+	/// Name of the service that's listening on the ports; for logging purposes only
+	AString m_ServiceName;
+	
+	
 	/** Fills in m_Sockets with individual sockets, each for one port specified in a_PortsString.
 	Returns true if successful and at least one socket has been created
 	*/
diff --git a/source/RCONServer.cpp b/source/RCONServer.cpp
new file mode 100644
index 000000000..9558512eb
--- /dev/null
+++ b/source/RCONServer.cpp
@@ -0,0 +1,289 @@
+
+// RCONServer.cpp
+
+// Implements the cRCONServer class representing the RCON server
+
+#include "Globals.h"
+#include "../iniFile/iniFile.h"
+#include "RCONServer.h"
+#include "Server.h"
+#include "Root.h"
+
+
+
+
+
+// Disable MSVC warnings:
+#if defined(_MSC_VER)
+	#pragma warning(push)
+	#pragma warning(disable:4355)  // 'this' : used in base member initializer list
+#endif
+
+
+
+
+
+enum
+{
+	// Client -> Server:
+	RCON_PACKET_COMMAND = 2,
+	RCON_PACKET_LOGIN   = 3,
+	
+	// Server -> Client:
+	RCON_PACKET_RESPONSE = 2,
+} ;
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cRCONServer:
+
+cRCONServer::cRCONServer(cServer & a_Server) :
+	m_Server(a_Server),
+	m_ListenThread4(*this, cSocket::IPv4, "RCON"),
+	m_ListenThread6(*this, cSocket::IPv6, "RCON")
+{
+}
+
+
+
+
+
+cRCONServer::~cRCONServer()
+{
+	m_ListenThread4.Stop();
+	m_ListenThread6.Stop();
+}
+
+
+
+
+
+void cRCONServer::Initialize(cIniFile & a_IniFile)
+{
+	if (!a_IniFile.GetValueSetB("RCON", "Enabled", false))
+	{
+		return;
+	}
+
+	// Read the password, don't allow an empty one:
+	m_Password = a_IniFile.GetValueSet("RCON", "Password", "");
+	if (m_Password.empty())
+	{
+		LOGWARNING("RCON is requested, but the password is not set. RCON is now disabled.");
+		return;
+	}
+	
+	// Read and initialize both IPv4 and IPv6 ports for RCON
+	bool HasAnyPorts = false;
+	AString Ports4 = a_IniFile.GetValueSet("RCON", "PortsIPv4", "25575");
+	if (m_ListenThread4.Initialize(Ports4))
+	{
+		HasAnyPorts = true;
+		m_ListenThread4.Start();
+	}
+	AString Ports6 = a_IniFile.GetValueSet("RCON", "PortsIPv6", "25575");
+	if (m_ListenThread6.Initialize(Ports6))
+	{
+		HasAnyPorts = true;
+		m_ListenThread6.Start();
+	}
+	if (!HasAnyPorts)
+	{
+		LOGWARNING("RCON is requested, but no ports are specified. Specify at least one port in PortsIPv4 or PortsIPv6. RCON is now disabled.");
+		return;
+	}
+}
+
+
+
+
+
+void cRCONServer::OnConnectionAccepted(cSocket & a_Socket)
+{
+	if (!a_Socket.IsValid())
+	{
+		return;
+	}
+
+	LOG("RCON Client \"%s\" connected!", a_Socket.GetIPString().c_str());
+	
+	// Create a new cConnection object, it will be deleted when the connection is closed
+	m_SocketThreads.AddClient(a_Socket, new cConnection(*this, a_Socket));
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cRCONServer::cConnection:
+
+cRCONServer::cConnection::cConnection(cRCONServer & a_RCONServer, cSocket & a_Socket) :
+	m_IsAuthenticated(false),
+	m_RCONServer(a_RCONServer),
+	m_Socket(a_Socket),
+	m_IPAddress(a_Socket.GetIPString())
+{
+}
+
+
+
+
+
+void cRCONServer::cConnection::DataReceived(const char * a_Data, int a_Size)
+{
+	// Append data to the buffer:
+	m_Buffer.append(a_Data, a_Size);
+	
+	// Process the packets in the buffer:
+	while (m_Buffer.size() >= 14)
+	{
+		int Length = IntFromBuffer(m_Buffer.data());
+		if (Length > 1500)
+		{
+			// Too long, drop the connection
+			LOGWARNING("Received an invalid RCON packet length (%d), dropping RCON connection to %s.",
+				Length, m_IPAddress.c_str()
+			);
+			m_RCONServer.m_SocketThreads.RemoveClient(this);
+			m_Socket.CloseSocket();
+			delete this;
+			return;
+		}
+		if (Length > (int)(m_Buffer.size() + 4))
+		{
+			// Incomplete packet yet, wait for more data to come
+			return;
+		}
+		
+		int RequestID  = IntFromBuffer(m_Buffer.data() + 4);
+		int PacketType = IntFromBuffer(m_Buffer.data() + 8);
+		if (!ProcessPacket(RequestID, PacketType, Length - 10, m_Buffer.data() + 12))
+		{
+			m_RCONServer.m_SocketThreads.RemoveClient(this);
+			m_Socket.CloseSocket();
+			delete this;
+			return;
+		}
+		m_Buffer.erase(0, Length + 4);
+	}  // while (m_Buffer.size() >= 14)
+}
+
+
+
+
+
+void cRCONServer::cConnection::GetOutgoingData(AString & a_Data)
+{
+	a_Data.assign(m_Outgoing);
+	m_Outgoing.clear();
+}
+
+
+
+
+
+void cRCONServer::cConnection::SocketClosed(void)
+{
+	m_RCONServer.m_SocketThreads.RemoveClient(this);
+	delete this;
+}
+
+
+
+
+
+bool cRCONServer::cConnection::ProcessPacket(int a_RequestID, int a_PacketType, int a_PayloadLength, const char * a_Payload)
+{
+	switch (a_PacketType)
+	{
+		case RCON_PACKET_LOGIN:
+		{
+			if (strncmp(a_Payload, m_RCONServer.m_Password.c_str(), a_PayloadLength) != 0)
+			{
+				LOGINFO("RCON: Invalid password from client %s, dropping connection.", m_IPAddress.c_str());
+				return false;
+			}
+			m_IsAuthenticated = true;
+			
+			LOGD("RCON: Client at %s has successfully authenticated", m_IPAddress.c_str());
+			
+			// Send OK response:
+			SendResponse(a_RequestID, RCON_PACKET_RESPONSE, 0, NULL);
+			return true;
+		}
+		
+		case RCON_PACKET_COMMAND:
+		{
+			AString cmd(a_Payload, a_PayloadLength);
+			LOGD("RCON command from %s: \"%s\"", m_IPAddress.c_str(), cmd.c_str());
+			cRoot::Get()->ExecuteConsoleCommand(cmd);
+			
+			// Send an empty response:
+			SendResponse(a_RequestID, RCON_PACKET_RESPONSE, 0, NULL);
+			return true;
+		}
+	}
+	
+	// Unknown packet type, drop the connection:
+	LOGWARNING("RCON: Client at %s has sent an unknown packet type %d, dropping connection.",
+		m_IPAddress.c_str(), a_PacketType
+	);
+	return false;
+}
+
+
+
+
+
+/// Reads 4 bytes from a_Buffer and returns the int they represent
+int cRCONServer::cConnection::IntFromBuffer(const char * a_Buffer)
+{
+	return ((unsigned char)a_Buffer[3] << 24) | ((unsigned char)a_Buffer[2] << 16) | ((unsigned char)a_Buffer[1] << 8) | (unsigned char)a_Buffer[0];
+}
+
+
+
+
+
+/// Puts 4 bytes representing the int into the buffer
+void cRCONServer::cConnection::IntToBuffer(int a_Value, char * a_Buffer)
+{
+	a_Buffer[0] = a_Value & 0xff;
+	a_Buffer[1] = (a_Value >> 8)  & 0xff;
+	a_Buffer[2] = (a_Value >> 16) & 0xff;
+	a_Buffer[3] = (a_Value >> 24) & 0xff;
+}
+
+
+
+
+
+/// Sends a RCON packet back to the client
+void cRCONServer::cConnection::SendResponse(int a_RequestID, int a_PacketType, int a_PayloadLength, const char * a_Payload)
+{
+	ASSERT((a_PayloadLength == 0) || (a_Payload != NULL));  // Either zero data to send, or a valid payload ptr
+	
+	char Buffer[4];
+	int Length = a_PayloadLength + 10;
+	IntToBuffer(Length, Buffer);
+	m_Outgoing.append(Buffer, 4);
+	IntToBuffer(a_RequestID, Buffer);
+	m_Outgoing.append(Buffer, 4);
+	IntToBuffer(a_PacketType, Buffer);
+	m_Outgoing.append(Buffer, 4);
+	if (a_PayloadLength > 0)
+	{
+		m_Outgoing.append(a_Payload, a_PayloadLength);
+	}
+	m_Outgoing.push_back(0);
+	m_Outgoing.push_back(0);
+	m_RCONServer.m_SocketThreads.NotifyWrite(this);
+}
+
+
+
+
diff --git a/source/RCONServer.h b/source/RCONServer.h
new file mode 100644
index 000000000..95122ba5f
--- /dev/null
+++ b/source/RCONServer.h
@@ -0,0 +1,106 @@
+
+// RCONServer.h
+
+// Declares the cRCONServer class representing the RCON server
+
+
+
+
+
+#pragma once
+
+#include "OSSupport/SocketThreads.h"
+#include "OSSupport/ListenThread.h"
+
+
+
+
+
+// fwd:
+class cServer;
+class cIniFile;
+
+
+
+
+
+class cRCONServer :
+	public cListenThread::cCallback
+{
+public:
+	cRCONServer(cServer & a_Server);
+	~cRCONServer();
+	
+	void Initialize(cIniFile & a_IniFile);
+	
+protected:
+	class cConnection :
+		public cSocketThreads::cCallback
+	{
+	public:
+		cConnection(cRCONServer & a_RCONServer, cSocket & a_Socket);
+		
+	protected:
+	
+		/// Set to true if the client has successfully authenticated
+		bool m_IsAuthenticated;
+		
+		/// Buffer for the incoming data
+		AString m_Buffer;
+		
+		/// Buffer for the outgoing data
+		AString m_Outgoing;
+		
+		/// Server that owns this connection and processes requests
+		cRCONServer & m_RCONServer;
+		
+		/// The socket belonging to the client
+		cSocket & m_Socket;
+		
+		/// Address of the client
+		AString m_IPAddress;
+		
+
+		// cSocketThreads::cCallback overrides:
+		virtual void DataReceived(const char * a_Data, int a_Size) override;
+		virtual void GetOutgoingData(AString & a_Data) override;
+		virtual void SocketClosed(void) override;
+		
+		/// Processes the given packet and sends the response; returns true if successful, false if the connection is to be dropped
+		bool ProcessPacket(int a_RequestID, int a_PacketType, int a_PayloadLength, const char * a_Payload);
+		
+		/// Reads 4 bytes from a_Buffer and returns the int they represent
+		int IntFromBuffer(const char * a_Buffer);
+		
+		/// Puts 4 bytes representing the int into the buffer
+		void IntToBuffer(int a_Value, char * a_Buffer);
+		
+		/// Sends a RCON packet back to the client
+		void SendResponse(int a_RequestID, int a_PacketType, int a_PayloadLength, const char * a_Payload);
+	} ;
+	
+	
+	/// The server object that will process the commands received
+	cServer & m_Server;
+	
+	/// The thread(s) that take care of all the traffic on the RCON ports
+	cSocketThreads m_SocketThreads;
+	
+	/// The thread for accepting IPv4 RCON connections
+	cListenThread  m_ListenThread4;
+	
+	/// The thread for accepting IPv6 RCON connections
+	cListenThread  m_ListenThread6;
+	
+	/// Password for authentication
+	AString m_Password;
+
+
+	// cListenThread::cCallback overrides:
+	virtual void OnConnectionAccepted(cSocket & a_Socket) override;
+} ;
+
+
+
+
+
diff --git a/source/Server.cpp b/source/Server.cpp
index 21fbb97db..7dac3ea18 100644
--- a/source/Server.cpp
+++ b/source/Server.cpp
@@ -174,6 +174,8 @@ bool cServer::InitServer(cIniFile & a_SettingsIni)
 		return false;
 	}
 
+	m_RCONServer.Initialize(a_SettingsIni);
+
 	m_bIsConnected = true;
 
 	m_pState->ServerID = "-";
@@ -215,12 +217,13 @@ bool cServer::InitServer(cIniFile & a_SettingsIni)
 
 cServer::cServer(void)
 	: m_pState(new sServerState)
-	, m_ListenThreadIPv4(*this, cSocket::IPv4)
-	, m_ListenThreadIPv6(*this, cSocket::IPv6)
+	, m_ListenThreadIPv4(*this, cSocket::IPv4, "Client")
+	, m_ListenThreadIPv6(*this, cSocket::IPv6, "Client")
 	, m_Millisecondsf(0)
 	, m_Milliseconds(0)
 	, m_bIsConnected(false)
 	, m_bRestarting(false)
+	, m_RCONServer(*this)
 {
 }
 
diff --git a/source/Server.h b/source/Server.h
index dfda56c62..3f7a24699 100644
--- a/source/Server.h
+++ b/source/Server.h
@@ -8,13 +8,12 @@
 
 
 #pragma once
-#ifndef CSERVER_H_INCLUDED
-#define CSERVER_H_INCLUDED
 
 #include "OSSupport/SocketThreads.h"
 #include "OSSupport/ListenThread.h"
 #include "CryptoPP/rsa.h"
 #include "CryptoPP/randpool.h"
+#include "RCONServer.h"
 
 
 
@@ -105,11 +104,11 @@ private:
 	
 	cNotifyWriteThread m_NotifyWriteThread;
 	
-	cListenThread m_ListenThreadIPv4;  // IPv4
-	cListenThread m_ListenThreadIPv6;  // IPv6
+	cListenThread m_ListenThreadIPv4;
+	cListenThread m_ListenThreadIPv6;
 	
 	cCriticalSection  m_CSClients;  // Locks client list
-	cClientHandleList m_Clients;         // Clients that are connected to the server
+	cClientHandleList m_Clients;    // Clients that are connected to the server
 	
 	cSocketThreads m_SocketThreads;
 	
@@ -120,12 +119,14 @@ private:
 	unsigned int m_Milliseconds;
 
 	bool m_bIsConnected; // true - connected false - not connected
-	int m_iServerPort;
 
 	bool m_bRestarting;
 	
-	CryptoPP::RSA::PrivateKey      m_PrivateKey;
-	CryptoPP::RSA::PublicKey       m_PublicKey;
+	CryptoPP::RSA::PrivateKey m_PrivateKey;
+	CryptoPP::RSA::PublicKey  m_PublicKey;
+	
+	cRCONServer m_RCONServer;
+	
 
 	cServer(void);
 	~cServer();
@@ -140,9 +141,3 @@ private:
 
 
 
-
-#endif  // CSERVER_H_INCLUDED
-
-
-
-