From fa8482dc7a7721e8cfee7cff5ec9bf6cd7d83eec Mon Sep 17 00:00:00 2001 From: Jaskowicz1 Date: Mon, 1 Jan 2024 19:31:57 +0000 Subject: [PATCH] feat: added server support --- README.md | 2 +- include/rconpp/server.h | 122 ++++++++++++++++++- include/rconpp/utilities.h | 9 ++ src/rconpp/client.cpp | 6 +- src/rconpp/server.cpp | 236 ++++++++++++++++++++++++++++++++++++- src/rconpp/utilities.cpp | 4 + unittest/test.cpp | 47 ++++++-- 7 files changed, 409 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 65df556..63d50aa 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Rcon++ is a modern Source RCON library for C++, allowing people to easily use RC - Support for Valve and non-Valve games. - Callbacks, allowing non-blocking calls. +- Support for hosting an RCON server. #### To-do -- Support for hosting an RCON server. - Support for multiple response packets. #### Library Usage diff --git a/include/rconpp/server.h b/include/rconpp/server.h index 7b9637e..c20a1cc 100644 --- a/include/rconpp/server.h +++ b/include/rconpp/server.h @@ -1 +1,121 @@ -#pragma once \ No newline at end of file +#pragma once + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "Ws2_32.lib") +#else +#include +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "utilities.h" + +namespace rconpp { + +struct connected_client { + struct sockaddr_in sock_info{}; + int socket{0}; + bool connected{false}; + + bool authenticated{false}; +}; + +struct server_info { + std::string address{}; + int port{0}; + std::string password{}; +}; + +struct client_command { + connected_client client; + std::string command{}; +}; + +class RCONPP_EXPORT rcon_server { + server_info serv_info{}; + +#ifdef _WIN32 + SOCKET sock{INVALID_SOCKET}; +#else + int sock{0}; +#endif + + std::thread accept_connections_runner; + +public: + bool online{false}; + + std::function on_command; + + /** + * @brief A map of connected clients. The key is their socket to talk to. + */ + std::unordered_map connected_clients{}; + + std::unordered_map request_handlers{}; + + /** + * @brief rcon_server constuctor. Initiates a connection to an RCON server with the parameters given. + * + * @param addr The IP Address (NOT domain) to connect to. + * @param _port The port to connect to. + * @param pass The password for the RCON server you are connecting to. + * + * @note This is a blocking call (done on purpose). It needs to wait to connect to the RCON server before anything else happens. + * It will timeout after 4 seconds if it can't connect. + */ + rcon_server(const std::string_view addr, const int port, const std::string_view pass); + + ~rcon_server(); + + /** + * @brief Disconnect a client from the server. + * + * @param client_socket The socket of the client to disconnect. + */ + void disconnect_client(const int client_socket); + +private: + + /** + * @brief Connects to RCON using `address`, `port`, and `password`. + * Those values are pre-filled when constructing this class. + * + * @warning This should only ever be called by the constructor. + * The constructor calls this function once it has filled in the required data and proceeds to login. + */ + bool startup_server(); + + /** + * @brief Ask to receive information from the server for a specified ID. + * + * @param id The ID that we should except the server to return, alongside information. + * @param type The type of packet that we should expect. + * + * @return Data given by the server. + */ + response receive_information(int32_t id, data_type type); + + /** + * @brief Gathers all the packet's content (based on the length returned by `read_packet_length`) + */ + void read_packet(rconpp::connected_client client); + + /** + * @brief Reads the first 4 bytes of a packet to get the packet size (not to be mistaken with length). + * + * @return The size (not length) of the packet. + */ + int read_packet_size(const rconpp::connected_client client); +}; + +} // namespace rconpp \ No newline at end of file diff --git a/include/rconpp/utilities.h b/include/rconpp/utilities.h index b581992..c15db97 100644 --- a/include/rconpp/utilities.h +++ b/include/rconpp/utilities.h @@ -77,6 +77,15 @@ RCONPP_EXPORT packet form_packet(const std::string_view data, int32_t id, int32_ */ RCONPP_EXPORT int bit32_to_int(const std::vector& buffer); +/** + * @brief Turn the second lot of 4 bytes (bytes 4-7) of a buffer (which ideally a 32 bit int) into an integer. + * + * @param buffer The bytes to turn into an integer. + * + * @return The value of the 4 bytes. + */ +RCONPP_EXPORT int type_to_int(const std::vector& buffer); + /** * @brief Reports the recent socket error. */ diff --git a/src/rconpp/client.cpp b/src/rconpp/client.cpp index d961a2c..b3cb7d6 100644 --- a/src/rconpp/client.cpp +++ b/src/rconpp/client.cpp @@ -11,7 +11,7 @@ rconpp::rcon_client::rcon_client(const std::string_view addr, const int _port, c std::cout << "Attempting connection to RCON server..." << "\n"; if (!connect_to_server()) { - std::cout << "RCON is aborting as it failed to initiate." << "\n"; + std::cout << "RCON is aborting as it failed to initiate client." << "\n"; return; } @@ -56,7 +56,7 @@ rconpp::rcon_client::~rcon_client() { #ifdef _WIN32 closesocket(sock); - WSACleanup(); + WSACleanup(); #else close(sock); #endif @@ -129,7 +129,7 @@ bool rconpp::rcon_client::connect_to_server() { #ifdef _WIN32 int corrected_timeout = DEFAULT_TIMEOUT * 1000; - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&corrected_timeout, sizeof(corrected_timeout)); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&corrected_timeout, sizeof(corrected_timeout)); #else // Set a timeout of 4 seconds. struct timeval tv {}; diff --git a/src/rconpp/server.cpp b/src/rconpp/server.cpp index e9ea187..b2e1226 100644 --- a/src/rconpp/server.cpp +++ b/src/rconpp/server.cpp @@ -1 +1,235 @@ -#include "server.h" \ No newline at end of file +#include "server.h" + +#include "utilities.h" + +rconpp::rcon_server::rcon_server(const std::string_view addr, const int port, const std::string_view pass) { + + if(port > 65535) { + std::cout << "Invalid port! The port can't exceed 65535!" << "\n"; + return; + } + + serv_info.address = addr; + serv_info.port = port; + serv_info.password = pass; + + std::cout << "Attempting to startup an RCON server..." << "\n"; + + if (!startup_server()) { + std::cout << "RCON is aborting as it failed to initiate server." << "\n"; + return; + } + + online = true; + + std::cout << "Server is now listening, initiating runners..." << "\n"; + + accept_connections_runner = std::thread([this]() { + while (online) { + connected_client client{}; + struct sockaddr_in client_info{}; + + socklen_t client_len = sizeof(client_info); + int client_socket = accept(sock, reinterpret_cast(&client_info), &client_len); + + if(client_socket == -1) { + std::cout << "client with socket: \"" << client_socket << "\" failed to connect." << "\n"; + continue; + } + + std::cout << "Client [" << inet_ntoa(client_info.sin_addr) << ":" << ntohs(client_info.sin_port) << "] has connected to the server." << "\n"; + + client.sock_info = client_info; + client.socket = client_socket; + client.connected = true; + + std::thread client_thread([this, client]{ + read_packet(client); + }); + + request_handlers.insert({ client_socket, std::move(client_thread) }); + + request_handlers.at(client_socket).detach(); + + connected_clients.insert({}); + } + }); + + accept_connections_runner.detach(); + + std::cout << "Server is now ready!" << "\n"; +} + +rconpp::rcon_server::~rcon_server() { + // Set connected to false, meaning no requests can be attempted during shutdown. + online = false; + + // Safely disconnect all clients from server. + for(const auto& client : connected_clients) { + disconnect_client(client.first); + } + +#ifdef _WIN32 + closesocket(sock); + WSACleanup(); +#else + close(sock); +#endif + if (accept_connections_runner.joinable()) { + accept_connections_runner.join(); + } +} + +bool rconpp::rcon_server::startup_server() { +#ifdef _WIN32 + // Initialize Winsock + WSADATA wsa_data; + int result = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (result != 0) { + std::cout << "WSAStartup failed. Error: " << result << std::endl; + return false; + } +#endif + + // Create new TCP socket. + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + +#ifdef _WIN32 + if (sock == INVALID_SOCKET) { +#else + if (sock == -1) { +#endif + std::cout << "Failed to open socket." << "\n"; + report_error(); + return false; + } + + struct sockaddr_in server{}; + + // Setup port, address, and family. + server.sin_family = AF_INET; +#ifdef _WIN32 + #ifdef UNICODE + InetPton(AF_INET, std::wstring(address.begin(), address.end()).c_str(), &server.sin_addr.s_addr); + #else + InetPton(AF_INET, address.c_str(), &server.sin_addr.s_addr); + #endif +#else + server.sin_addr.s_addr = INADDR_ANY; +#endif + server.sin_port = htons(serv_info.port); + + bool allow = true; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &allow, sizeof(allow)); + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &allow, sizeof(allow)); + + + // Connect to the socket and set the status of the connection. + int status = bind(sock, reinterpret_cast(&server), sizeof(server)); + + if (status == -1) { + report_error(); + return false; + } + + status = listen(sock, SOMAXCONN); + + if(status == -1) { + report_error(); + return false; + } + + return true; +} + +void rconpp::rcon_server::disconnect_client(const int client_socket) { + close(client_socket); + + connected_clients.at(client_socket).connected = false; + + if(request_handlers.at(client_socket).joinable()) { + request_handlers.at(client_socket).join(); + } + + connected_clients.erase(client_socket); +}; + +void rconpp::rcon_server::read_packet(rconpp::connected_client client) { + while(client.connected) { + size_t packet_size = read_packet_size(client); + + if(packet_size <= 10) { + continue; + } + + std::vector buffer{}; + buffer.resize(packet_size); + + if (recv(client.socket, buffer.data(), packet_size, 0) == -1) { + std::cout << "Failed to get a packet from client." << "\n"; + report_error(); + } + + std::string packet_data(&buffer[8], &buffer[buffer.size()-2]); + int id = bit32_to_int(buffer); + int type = type_to_int(buffer); + + rconpp::packet packet_to_send{}; + + if(!client.authenticated) { + if(packet_data == serv_info.password) { + packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_AUTH_RESPONSE); + client.authenticated = true; + } else { + packet_to_send = form_packet("", -1, rconpp::data_type::SERVERDATA_AUTH_RESPONSE); + } + } else { + if(type != rconpp::data_type::SERVERDATA_EXECCOMMAND) { + packet_to_send = form_packet("Invalid packet type (" + std::to_string(type) + "). Double check your packets.", id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE); + std::cout << "Invalid packet type (" + std::to_string(type) + ") sent by [" + inet_ntoa(client.sock_info.sin_addr) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]. Double check your packets." << "\n"; + } else { + std::cout << "Client [" << inet_ntoa(client.sock_info.sin_addr) << ":" << ntohs(client.sock_info.sin_port) << "] has asked to execute the command: \"" << packet_data << "\"" << "\n"; + if(!on_command) { + /* + * Whilst sending information about the server not responding would be nice, + * we would end up with the possibility of clients thinking that is the response. + * It's better to just send no information and let clients assume that meant + * the server didn't like the command. + */ + packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE); + } else { + client_command command{}; + command.command = packet_data; + command.client = client; + + std::string text_to_send = on_command(command); + + packet_to_send = form_packet(text_to_send, id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE); + } + } + } + + if (send(client.socket, packet_to_send.data.data(), packet_to_send.length, 0) < 0) { + std::cout << "Sending failed!" << "\n"; + report_error(); + continue; + } + } +} + +int rconpp::rcon_server::read_packet_size(const rconpp::connected_client client) { + std::vector buffer{}; + buffer.resize(4); + + /* + * RCON gives the packet SIZE in the first four (4) bytes of each packet. + * We simply just want to read that and then return it. + */ + if (recv(client.socket, buffer.data(), 4, 0) == -1) { + std::cout << "Did not receive a packet in time. Did the server send a response?" << "\n"; + report_error(); + return -1; + } + + return bit32_to_int(buffer); +} diff --git a/src/rconpp/utilities.cpp b/src/rconpp/utilities.cpp index 92b5c2e..5504409 100644 --- a/src/rconpp/utilities.cpp +++ b/src/rconpp/utilities.cpp @@ -34,6 +34,10 @@ int rconpp::bit32_to_int(const std::vector& buffer) { return static_cast(buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24); } +int rconpp::type_to_int(const std::vector& buffer) { + return static_cast(buffer[4] | buffer[5] << 8 | buffer[6] << 16 | buffer[7] << 24); +} + void rconpp::report_error() { #ifdef _WIN32 std::cout << "Error code: " << WSAGetLastError() << "\n"; diff --git a/unittest/test.cpp b/unittest/test.cpp index 5e4a3b5..7831f34 100644 --- a/unittest/test.cpp +++ b/unittest/test.cpp @@ -1,21 +1,46 @@ #include "../include/rconpp/rcon.h" int main() { - if(!std::getenv("RCON_TESTING_IP") || !std::getenv("RCON_TESTING_PORT") || !std::getenv("RCON_TESTING_PASSWORD")) { - throw std::invalid_argument("Environment variables not set."); - } + try { + if (!std::getenv("RCON_TESTING_IP") || !std::getenv("RCON_TESTING_PORT") || + !std::getenv("RCON_TESTING_PASSWORD")) { + throw std::invalid_argument("Environment variables not set."); + } - rconpp::rcon_client client(std::getenv("RCON_TESTING_IP"), std::stoi(std::getenv("RCON_TESTING_PORT")), std::getenv("RCON_TESTING_PASSWORD")); + rconpp::rcon_client client(std::getenv("RCON_TESTING_IP"), std::stoi(std::getenv("RCON_TESTING_PORT")), + std::getenv("RCON_TESTING_PASSWORD")); - if(client.connected) { - rconpp::response res = client.send_data_sync("testing", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND); + if (client.connected) { + rconpp::response res = client.send_data_sync("testing", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND); - if(res.server_responded) { - std::cout << "Server responded!" << "\n"; + if (res.server_responded) { + std::cout << "Server responded!" << "\n"; + std::cout << "Client test passed!" << "\n"; + } else { + std::cout << "No server response." << "\n"; + } } else { - std::cout << "No server response." << "\n"; + std::cout << "No connection!" << "\n"; } - } else { - std::cout << "No connection!" << "\n"; + } catch(std::exception& e) { + std::cout << "Client test failed. Reason: " << e.what() << "\n"; + } + + try { + rconpp::rcon_server server("0.0.0.0", 27015, "testing"); + + server.on_command = [](const rconpp::client_command& command) { + if (command.command == "test") { + return "This is a test!"; + } else if (command.command == "/players") { + return "Players: none lol"; + } else { + return "Hello!"; + } + }; + + std::cout << "Server test passed!" << "\n"; + } catch(std::exception& e) { + std::cout << "Server test failed. Reason: " << e.what() << "\n"; } } \ No newline at end of file