diff --git a/README.md b/README.md index 63d50aa..b5c8533 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Rcon++ is a modern Source RCON library for C++, allowing people to easily use RC This library is used in: - [Factorio-Discord-Relay Revamped](https://github.com/Jaskowicz1/fdr-remake) +- RCON-UE If you're using this library, feel free to message me and show me, you might just get your project shown here! @@ -31,18 +32,26 @@ We do not test support for MinGW, nor do we want to actively try and support it. # Getting Started -rcon++ can be installed from the .deb file in the recent actions (soon to be released!). +rcon++ can be installed from the releases section! We're aiming to start rolling out to package managers soon! # Quick Example +### Client ```c++ #include #include int main() { rconpp::rcon_client client("127.0.0.1", 27015, "changeme"); + + client.on_log = [](const std::string_view& log) { + std::cout << log << "\n"; + }; + + client.start(true); + client.send_data("Hello!", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [](const rconpp::response& response) { std::cout << "response: " << response.data << "\n"; }); @@ -51,6 +60,32 @@ int main() { } ``` +### Server +```c++ +#include +#include + +int main() { + rconpp::rcon_server server("0.0.0.0", 27015, "testing"); + + server.on_log = [](const std::string_view log) { + std::cout << log << "\n"; + }; + + server.on_command = [](const rconpp::client_command& command) { + if (command.command == "/test") { + return "This is a test!"; + } else { + return "Hello!"; + } + }; + + server.start(false); + + return 0; +} +``` + # Contributing If you want to help out, simply make a fork and submit your PR! diff --git a/include/rconpp/client.h b/include/rconpp/client.h index f1661f2..0ce648f 100644 --- a/include/rconpp/client.h +++ b/include/rconpp/client.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "utilities.h" namespace rconpp { @@ -45,8 +46,12 @@ class RCONPP_EXPORT rcon_client { public: bool connected{false}; + std::function on_log; + + std::condition_variable terminating; + /** - * @brief rcon constuctor. Initiates a connection to an RCON server with the parameters given. + * @brief rcon_client constuctor. * * @param addr The IP Address (NOT domain) to connect to. * @param _port The port to connect to. @@ -59,6 +64,8 @@ class RCONPP_EXPORT rcon_client { ~rcon_client(); + void start(bool return_after); + /** * @brief Send data to the connected RCON server. Requests from this function are added to a queue (`requests_queued`) and are handled by a different thread. * diff --git a/include/rconpp/rcon.h b/include/rconpp/rcon.h index aff9138..b6e12d6 100644 --- a/include/rconpp/rcon.h +++ b/include/rconpp/rcon.h @@ -1,7 +1,7 @@ #pragma once #ifdef _WIN32 -#pragma warning( disable : 4251 ); // 4251 warns when we export classes or structures with stl member variables +#pragma warning( disable : 4251 ) // 4251 warns when we export classes or structures with stl member variables #endif #include "export.h" diff --git a/include/rconpp/server.h b/include/rconpp/server.h index c20a1cc..171a10c 100644 --- a/include/rconpp/server.h +++ b/include/rconpp/server.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "utilities.h" namespace rconpp { @@ -29,19 +30,15 @@ struct connected_client { 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{}; + std::string address{}; + int port{0}; + std::string password{}; #ifdef _WIN32 SOCKET sock{INVALID_SOCKET}; @@ -56,6 +53,10 @@ class RCONPP_EXPORT rcon_server { std::function on_command; + std::function on_log = {}; + + std::condition_variable terminating; + /** * @brief A map of connected clients. The key is their socket to talk to. */ @@ -73,10 +74,12 @@ class RCONPP_EXPORT rcon_server { * @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(const std::string_view addr, const int _port, const std::string_view pass); ~rcon_server(); + void start(bool return_after); + /** * @brief Disconnect a client from the server. * diff --git a/src/rconpp/client.cpp b/src/rconpp/client.cpp index b3cb7d6..d897da0 100644 --- a/src/rconpp/client.cpp +++ b/src/rconpp/client.cpp @@ -1,59 +1,16 @@ +#include #include "client.h" #include "utilities.h" rconpp::rcon_client::rcon_client(const std::string_view addr, const int _port, const std::string_view pass) : address(addr), port(_port), password(pass) { - - if(_port > 65535) { - std::cout << "Invalid port! The port can't exceed 65535!" << "\n"; - return; - } - - std::cout << "Attempting connection to RCON server..." << "\n"; - - if (!connect_to_server()) { - std::cout << "RCON is aborting as it failed to initiate client." << "\n"; - return; - } - - std::cout << "Connected successfully! Sending login data..." << "\n"; - - // The server will send SERVERDATA_AUTH_RESPONSE once it's happy. If it's not -1, the server will have accepted us! - response response = send_data_sync(pass, 1, data_type::SERVERDATA_AUTH, true); - - if (!response.server_responded) { - std::cout << "Login data was incorrect. RCON will now abort." << "\n"; - return; - } - - std::cout << "Sent login data." << "\n"; - - connected = true; - - queue_runner = std::thread([this]() { - while (connected) { - if (requests_queued.empty()) { - continue; - } - - for (const queued_request& request : requests_queued) { - // Send data to callback if it's been set. - if (request.callback) - request.callback(send_data_sync(request.data, request.id, request.type)); - else - send_data_sync(request.data, request.id, request.type, false); - } - - requests_queued.clear(); - } - }); - - queue_runner.detach(); } rconpp::rcon_client::~rcon_client() { // Set connected to false, meaning no requests can be attempted during shutdown. connected = false; + terminating.notify_all(); + #ifdef _WIN32 closesocket(sock); WSACleanup(); @@ -68,14 +25,14 @@ rconpp::rcon_client::~rcon_client() { rconpp::response rconpp::rcon_client::send_data_sync(const std::string_view data, const int32_t id, rconpp::data_type type, bool feedback) { if (!connected && type != data_type::SERVERDATA_AUTH) { - std::cout << "Cannot send data when not connected." << "\n"; + on_log("Cannot send data when not connected."); return { "", false }; } packet formed_packet = form_packet(data, id, type); if (send(sock, formed_packet.data.data(), formed_packet.length, 0) < 0) { - std::cout << "Sending failed!" << "\n"; + on_log("Sending failed!"); report_error(); return { "", false }; } @@ -95,7 +52,7 @@ bool rconpp::rcon_client::connect_to_server() { WSADATA wsa_data; int result = WSAStartup(MAKEWORD(2, 2), &wsa_data); if (result != 0) { - std::cout << "WSAStartup failed. Error: " << result << std::endl; + on_log("WSAStartup failed. Error: " + std::to_string(result)); return false; } #endif @@ -108,7 +65,7 @@ bool rconpp::rcon_client::connect_to_server() { #else if (sock == -1) { #endif - std::cout << "Failed to open socket." << "\n"; + on_log("Failed to open socket."); report_error(); return false; } @@ -235,10 +192,73 @@ int rconpp::rcon_client::read_packet_size() { * We simply just want to read that and then return it. */ if (recv(sock, buffer.data(), 4, 0) == -1) { - std::cout << "Did not receive a packet in time. Did the server send a response?" << "\n"; + on_log("Did not receive a packet in time. Did the server send a response?"); report_error(); return -1; } return bit32_to_int(buffer); +} + +void rconpp::rcon_client::start(bool return_after) { + + auto block_calling_thread = [this]() { + std::mutex thread_mutex; + std::unique_lock thread_lock(thread_mutex); + this->terminating.wait(thread_lock); + }; + + if(port > 65535) { + on_log("Invalid port! The port can't exceed 65535!"); + return; + } + + on_log("Attempting connection to RCON server..."); + + if (!connect_to_server()) { + on_log("RCON is aborting as it failed to initiate client."); + return; + } + + on_log("Connected successfully! Sending login data..."); + + // The server will send SERVERDATA_AUTH_RESPONSE once it's happy. If it's not -1, the server will have accepted us! + response response = send_data_sync(password, 1, data_type::SERVERDATA_AUTH, true); + + if (!response.server_responded) { + on_log("Login data was incorrect. RCON will now abort."); + return; + } + + on_log("Sent login data."); + + connected = true; + + queue_runner = std::thread([this]() { + while (connected) { + if (requests_queued.empty()) { + continue; + } + + for (const queued_request& request : requests_queued) { + // If we're closing the connection down, we need to back out. + if(!connected) + return; + + // Send data to callback if it's been set. + if (request.callback) + request.callback(send_data_sync(request.data, request.id, request.type)); + else + send_data_sync(request.data, request.id, request.type, false); + } + + requests_queued.clear(); + } + }); + + queue_runner.detach(); + + if(!return_after) { + block_calling_thread(); + } }; diff --git a/src/rconpp/server.cpp b/src/rconpp/server.cpp index 552d93d..7b4e58f 100644 --- a/src/rconpp/server.cpp +++ b/src/rconpp/server.cpp @@ -1,69 +1,17 @@ +#include #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(const std::string_view addr, const int _port, const std::string_view pass) : address(addr), port(_port), password(pass) { } rconpp::rcon_server::~rcon_server() { // Set connected to false, meaning no requests can be attempted during shutdown. online = false; + terminating.notify_all(); + // Safely disconnect all clients from server. for(const auto& client : connected_clients) { disconnect_client(client.first); @@ -86,7 +34,7 @@ bool rconpp::rcon_server::startup_server() { WSADATA wsa_data; int result = WSAStartup(MAKEWORD(2, 2), &wsa_data); if (result != 0) { - std::cout << "WSAStartup failed. Error: " << result << std::endl; + on_log("WSAStartup failed. Error: " + std::to_string(result)); return false; } #endif @@ -99,7 +47,7 @@ bool rconpp::rcon_server::startup_server() { #else if (sock == -1) { #endif - std::cout << "Failed to open socket." << "\n"; + on_log("Failed to open socket."); report_error(); return false; } @@ -109,7 +57,7 @@ bool rconpp::rcon_server::startup_server() { // Setup port, address, and family. server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; - server.sin_port = htons(serv_info.port); + server.sin_port = htons(port); int allow = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&allow), sizeof(allow)); @@ -161,7 +109,7 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) { buffer.resize(packet_size); if (recv(client.socket, buffer.data(), packet_size, 0) == -1) { - std::cout << "Failed to get a packet from client." << "\n"; + on_log("Failed to get a packet from client."); report_error(); } @@ -172,7 +120,7 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) { rconpp::packet packet_to_send{}; if(!client.authenticated) { - if(packet_data == serv_info.password) { + if(packet_data == password) { packet_to_send = form_packet("", id, rconpp::data_type::SERVERDATA_AUTH_RESPONSE); client.authenticated = true; } else { @@ -181,10 +129,12 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) { } 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"; + on_log("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."); } 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"; + on_log("Client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "] has asked to execute the command: \"" + packet_data + "\""); if(!on_command) { + on_log("You have not set any response for on_command! The server will default to a blank response."); + /* * 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. @@ -199,13 +149,17 @@ void rconpp::rcon_server::read_packet(rconpp::connected_client client) { std::string text_to_send = on_command(command); + on_log("Sending reply \"" + text_to_send + "\" to client [" + std::string(inet_ntoa(client.sock_info.sin_addr)) + ":" + std::to_string(ntohs(client.sock_info.sin_port)) + "]."); + packet_to_send = form_packet(text_to_send, id, rconpp::data_type::SERVERDATA_RESPONSE_VALUE); } } } + on_log("Sending..."); + if (send(client.socket, packet_to_send.data.data(), packet_to_send.length, 0) < 0) { - std::cout << "Sending failed!" << "\n"; + on_log("Sending failed!"); report_error(); continue; } @@ -221,10 +175,73 @@ int rconpp::rcon_server::read_packet_size(const rconpp::connected_client client) * 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"; + on_log("Did not receive a packet in time. Did the server send a response?"); report_error(); return -1; } return bit32_to_int(buffer); } + +void rconpp::rcon_server::start(bool return_after) { + auto block_calling_thread = [this]() { + std::mutex thread_mutex; + std::unique_lock thread_lock(thread_mutex); + this->terminating.wait(thread_lock); + }; + + if(port > 65535) { + on_log("Invalid port! The port can't exceed 65535!"); + return; + } + + on_log("Attempting to startup an RCON server..."); + + if (!startup_server()) { + on_log("RCON is aborting as it failed to initiate server."); + return; + } + + online = true; + + on_log("Server is now listening, initiating runners..."); + + 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) { + on_log("client with socket: \"" + std::to_string(client_socket) + "\" failed to connect."); + continue; + } + + on_log("Client [" + std::string(inet_ntoa(client_info.sin_addr)) + ":" + std::to_string(ntohs(client_info.sin_port)) + "] has connected to the server."); + + 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(); + + on_log("Server is now ready!"); + + if(!return_after) { + block_calling_thread(); + } +} diff --git a/src/rconpp/utilities.cpp b/src/rconpp/utilities.cpp index 5504409..38801b8 100644 --- a/src/rconpp/utilities.cpp +++ b/src/rconpp/utilities.cpp @@ -13,14 +13,12 @@ rconpp::packet rconpp::form_packet(const std::string_view data, int32_t id, int3 return {}; } - std::vector temp_data{}; + std::vector temp_data(data_size + 4); /* Create a vector that exactly the size of the packet length. */ - temp_data.resize(data_size + 4); /* make sure the vector is big enough to hold all the data */ - - std::memcpy(temp_data.data() + 0, &data_size, sizeof(data_size)); /* copy size into it */ - std::memcpy(temp_data.data() + sizeof(data_size), &id, sizeof(id)); /* copy id into it */ - std::memcpy(temp_data.data() + sizeof(data_size) + sizeof(id), &type, sizeof(type)); /* copy type into it */ - std::memcpy(temp_data.data() + sizeof(data_size) + sizeof(id) + sizeof(type), data.data(), data.size()); /* copy data into it */ + std::memcpy(temp_data.data() + 0, &data_size, sizeof(data_size)); /* Copy size into it */ + std::memcpy(temp_data.data() + sizeof(data_size), &id, sizeof(id)); /* Copy id into it */ + std::memcpy(temp_data.data() + sizeof(data_size) + sizeof(id), &type, sizeof(type)); /* Copy type into it */ + std::memcpy(temp_data.data() + sizeof(data_size) + sizeof(id) + sizeof(type), data.data(), data.size()); /* Copy data into it */ packet temp_packet; temp_packet.length = data_size + 4; diff --git a/unittest/test.cpp b/unittest/test.cpp index 7831f34..80c0577 100644 --- a/unittest/test.cpp +++ b/unittest/test.cpp @@ -10,6 +10,12 @@ int main() { rconpp::rcon_client client(std::getenv("RCON_TESTING_IP"), std::stoi(std::getenv("RCON_TESTING_PORT")), std::getenv("RCON_TESTING_PASSWORD")); + client.on_log = [](const std::string_view log) { + std::cout << log << "\n"; + }; + + client.start(true); + if (client.connected) { rconpp::response res = client.send_data_sync("testing", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND); @@ -22,6 +28,7 @@ int main() { } else { std::cout << "No connection!" << "\n"; } + } catch(std::exception& e) { std::cout << "Client test failed. Reason: " << e.what() << "\n"; } @@ -29,6 +36,10 @@ int main() { try { rconpp::rcon_server server("0.0.0.0", 27015, "testing"); + server.on_log = [](const std::string_view log) { + std::cout << log << "\n"; + }; + server.on_command = [](const rconpp::client_command& command) { if (command.command == "test") { return "This is a test!"; @@ -39,6 +50,8 @@ int main() { } }; + server.start(true); + std::cout << "Server test passed!" << "\n"; } catch(std::exception& e) { std::cout << "Server test failed. Reason: " << e.what() << "\n";