diff --git a/include/rcon.h b/include/rcon.h index a8a93a0..eac447d 100644 --- a/include/rcon.h +++ b/include/rcon.h @@ -1,10 +1,27 @@ #pragma once +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +#include + #include #include +#include +#include +#include +#include + +namespace rconpp { -#define DEFAULT_TIMEOUT 4 -#define HEADER_SIZE 14 +constexpr int DEFAULT_TIMEOUT = 4; +constexpr int HEADER_SIZE = 14; enum data_type { /** @@ -24,49 +41,105 @@ enum data_type { SERVERDATA_AUTH = 3, }; -struct rcon_packet { +struct packet { unsigned int bytes; - unsigned char* data; + std::vector data; + //unsigned char* data; bool server_responded; - ~rcon_packet() { - delete[] data; - } + //~packet() { + // delete[] data; + //} }; -struct rcon_response { +struct response { std::string data; bool server_responded; }; -struct rcon_queued_request { +struct queued_request { std::string data; int32_t id; data_type type; - std::function callback; - - rcon_queued_request(const std::string& _data, const int32_t _id, const data_type _type, std::function _callback) : data(_data), id(_id), type(_type), callback(_callback) {} + std::function callback; }; class rcon { const std::string address; const unsigned int port; const std::string password; - int sock{0}; +#ifdef _WIN32 + SOCKET sock{INVALID_SOCKET}; +#else + int sock{ 0 }; +#endif bool connected{false}; - - std::vector requests_queued{}; - -public: - + + std::vector requests_queued{}; + + public: + /** * @brief rcon constuctor. Initiates a connection to an RCON server with the parameters given. * * @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(const std::string& addr, const unsigned int _port, const std::string& pass); - + rcon(const std::string& addr, const unsigned int _port, const std::string& pass) : address(addr), port(_port), password(pass) { + + std::cout << "Attempting connection to RCON server..." << "\n"; + + if (!connect()) { + std::cout << "RCON is aborting as it failed to initiate." << "\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; + + std::thread queue_runner([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(); + }; + + ~rcon() { +#ifdef _WIN32 + closesocket(sock); + WSACleanup(); +#else + close(sock); +#endif + + + } + /** * @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. * @@ -76,8 +149,10 @@ class rcon { * * @warning If you are expecting no response from the server, do NOT use the callback. You will halt the RCON process until the next received message (which will chain). */ - void send_data(const std::string& data, const int32_t id, data_type type, std::function callback = {}); - + void send_data(const std::string& data, const int32_t id, data_type type, std::function callback = {}) { + requests_queued.emplace_back(queued_request{data, id, type, std::move(callback)}); + } + /** * @brief Send data to the connected RCON server. * @@ -90,10 +165,31 @@ class rcon { * * @returns Data given by the server from the request. */ - const rcon_response send_data_sync(const std::string data, const int32_t id, data_type type, bool feedback = true); - -private: - + response send_data_sync(const std::string data, const int32_t id, data_type type, bool feedback = true) { + if (!connected && type != data_type::SERVERDATA_AUTH) { + std::cout << "Cannot send data when not connected." << "\n"; + return { "", false }; + } + + unsigned long long packet_len = data.length() + HEADER_SIZE; + std::vector formed_packet = form_packet(data, id, type); + + if (send(sock, formed_packet.data(), packet_len, 0) < 0) { + std::cout << "Sending failed!" << "\n"; + return { "", false }; + } + + if (!feedback) { + // Because we do not want any feedback, we just send no data and say the server didn't respond. + return { "", false }; + } + + // Server will send a SERVERDATA_RESPONSE_VALUE packet. + return receive_information(id, type); + } + + private: + /** * @brief Connects to RCON using `address`, `port`, and `password`. * Those values are pre-filled when constructing this class. @@ -101,8 +197,64 @@ class rcon { * @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 connect(); - + bool connect() { + // 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"; + return false; + } + + // Setup port, address, and family. + struct sockaddr_in server{}; + server.sin_family = AF_INET; + server.sin_addr.s_addr = inet_addr(address.c_str()); + server.sin_port = htons(port); + + // Make it non-blocking. +#ifdef _WIN32 + u_long ul{ 1 }; + ioctlsocket(sock, FIONBIO, &ul); +#else + fcntl(sock, F_SETFL, O_NONBLOCK); +#endif + + // Set a timeout of 4 seconds. + struct timeval tv {}; + tv.tv_sec = DEFAULT_TIMEOUT; + tv.tv_usec = 0; + +#ifdef _WIN32 + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*) &DEFAULT_TIMEOUT, sizeof(4)); +#else + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); +#endif + + // Connect to the socket and set the status to our temp status. + if (::connect(sock, (struct sockaddr*)&server, sizeof(server)) == -1) { + if (errno != EINPROGRESS) { + return false; + } + } + + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + // Create temp status + int status = select(sock + 1, nullptr, &fds, nullptr, &tv); + + //fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) & ~O_NONBLOCK); + + // If status wasn't zero, we successfully connected. + return status != 0; + } + /** * @brief Form a valid RCON packet. * @@ -111,8 +263,41 @@ class rcon { * @param id The ID of the request. * @param type The type of packet. */ - void form_packet(unsigned char packet[], const std::string& data, int32_t id, int32_t type); - + std::vector form_packet(const std::string& data, int32_t id, int32_t type) { + const char nullbytes[] = { '\x00', '\x00' }; + const int32_t min_size = sizeof(id) + sizeof(type) + sizeof(nullbytes); // 10 bytes. + const int32_t data_size = static_cast(data.size()) + min_size; + + if (data_size > 4096) { + std::cout << "This packet is too big to send. Please generate a smaller packet." << "\n"; + return {}; + } + + std::vector temp_packet; + + temp_packet.resize(data_size + sizeof(data_size) + sizeof(id)); /* make sure the vector is big enough to hold all the data */ + + std::memcpy(temp_packet.data() + 0, &data_size, sizeof(data_size)); /* copy size into it */ + std::memcpy(temp_packet.data() + sizeof(data_size), &id, sizeof(id)); /* copy id into it */ + std::memcpy(temp_packet.data() + sizeof(data_size), &type, sizeof(type)); /* copy type into it */ + std::memcpy(temp_packet.data() + sizeof(data_size), data.data(), data.size()); /* copy data into it */ + + //(memset(packet, '\0', data_size), (void)0); + + // Each part is 4 bytes + //packet[0] = data_size; + //packet[4] = id; + //packet[8] = type; + + //const char* data_chars = data.c_str(); + + //for (int i = 0; i < data_size; i++) { + // packet[12 + i] = data_chars[i]; + //} + + return temp_packet; + } + /** * @brief Ask to receive information from the server for a specified ID. * @@ -120,16 +305,97 @@ class rcon { * * @return Data given by the server. */ - rcon_response receive_information(int32_t id, data_type type); - + response receive_information(int32_t id, data_type type) { + // Whilst this loop is better than a while loop, + // it should really just keep going for a certain amount of seconds. + for (int i = 0; i < 500; i++) { + packet packet = read_packet(); + + if (packet.bytes == 0) { + if (type != SERVERDATA_AUTH) + return { "", packet.server_responded }; + else + continue; + } + + //unsigned char* buffer = packet.data; + + if (type == SERVERDATA_AUTH) { + if (byte32_to_int(packet.data) == -1) { + return { "", false }; + } + else { + if (byte32_to_int(packet.data) == id) { + return { "", true }; + } + } + } + + int offset = packet.bytes - HEADER_SIZE + 3; + + if (offset == -1) + continue; + + std::string part(&packet.data[8], &packet.data[8] + offset); + + if (byte32_to_int(packet.data) == id) { + return { part, packet.server_responded }; + } + } + return { "", false }; + } + /** * @brief Gathers all the packet's content (based on the length returned by `read_packet_length`) */ - rcon_packet read_packet(); - - const size_t read_packet_length(); + packet read_packet() { + size_t packet_length = read_packet_length(); + + /* + * If the packet length is -1, the server didn't respond. + * If the packet length is 0, the server did respond but said nothing. + */ + if (packet_length == -1) { + return { 0, {}, false }; + } + else if (packet_length == 0) { + return { 0, {}, true }; + } - inline const int byte32_to_int(unsigned char* buffer) { + //auto* buffer = new unsigned char[packet_length] {0}; + std::vector buffer; + buffer.resize(packet_length); + //std::string buffer; + unsigned int bytes = 0; + + do { + size_t recv_bytes = recv(sock, buffer.data(), packet_length - bytes, 0); + if (recv_bytes == -1) { + std::cout << "Did not receive a packet in time. Did the server send a response?" << "\n"; + return { 0, {}, false }; + } + + bytes += recv_bytes; + + } while (bytes < packet_length); + + return { bytes, buffer }; + } + + int read_packet_length() { + std::vector buffer; + buffer.resize(4); + size_t recv_bytes = recv(sock, buffer.data(), 4, 0); + if (recv_bytes == -1) { + std::cout << "Did not receive a packet in time. Did the server send a response?" << "\n"; + return -1; + } + return byte32_to_int(buffer); + } + + inline int byte32_to_int(std::vector& buffer) { return static_cast(buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24); } }; + +} // namespace rconpp diff --git a/src/main.cpp b/src/main.cpp index aac4212..bbdbb71 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -66,7 +66,7 @@ int main() { std::cout << "Configuration loaded. Starting FDR." << "\n"; - rcon rcon_client{ip, port, pass}; + rconpp::rcon rcon_client{ip, port, pass}; dpp::cluster bot(bot_token, dpp::i_default_intents | dpp::i_message_content | dpp::i_guild_members, 0, 0, 1, true, dpp::cache_policy::cpol_none); @@ -83,32 +83,32 @@ int main() { if (event.msg.channel_id == FDR::config.msg_channel) { if(FDR::config.allow_achievements) { // ID here doesn't matter really, we're not wanting a response. - rcon_client.send_data(event.msg.content, 999, data_type::SERVERDATA_EXECCOMMAND); + rcon_client.send_data(event.msg.content, 999, rconpp::data_type::SERVERDATA_EXECCOMMAND); } else { - rcon_client.send_data("/silent-command game.print(\"[Discord] " + event.msg.author.username + " » " + event.msg.content + "\")", 999, data_type::SERVERDATA_EXECCOMMAND); + rcon_client.send_data("/silent-command game.print(\"[Discord] " + event.msg.author.username + " » " + event.msg.content + "\")", 999, rconpp::data_type::SERVERDATA_EXECCOMMAND); } } }); bot.on_slashcommand([&bot, &rcon_client](const dpp::slashcommand_t& event) { if (event.command.get_command_name() == "evolution") { - rcon_client.send_data("/evolution", 3, data_type::SERVERDATA_EXECCOMMAND, [event](const rcon_response& response) { + rcon_client.send_data("/evolution", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [event](const rconpp::response& response) { event.reply(dpp::message(response.data).set_flags(dpp::m_ephemeral)); }); } else if (event.command.get_command_name() == "time") { - rcon_client.send_data("/time", 3, data_type::SERVERDATA_EXECCOMMAND, [event](const rcon_response& response) { + rcon_client.send_data("/time", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [event](const rconpp::response& response) { event.reply(dpp::message("Server uptime: " + response.data).set_flags(dpp::m_ephemeral)); }); } else if (event.command.get_command_name() == "version") { - rcon_client.send_data("/version", 3, data_type::SERVERDATA_EXECCOMMAND, [event](const rcon_response& response) { + rcon_client.send_data("/version", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [event](const rconpp::response& response) { event.reply(dpp::message("Factorio version: " + response.data).set_flags(dpp::m_ephemeral)); }); } else if (event.command.get_command_name() == "players") { - rcon_client.send_data("/players online", 3, data_type::SERVERDATA_EXECCOMMAND, [event](const rcon_response& response) { + rcon_client.send_data("/players online", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [event](const rconpp::response& response) { event.reply(dpp::message(response.data).set_flags(dpp::m_ephemeral)); }); } else if (event.command.get_command_name() == "seed") { - rcon_client.send_data("/seed", 3, data_type::SERVERDATA_EXECCOMMAND, [event](const rcon_response& response) { + rcon_client.send_data("/seed", 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [event](const rconpp::response& response) { event.reply(dpp::message(response.data).set_flags(dpp::m_ephemeral)); }); } else if (event.command.get_command_name() == "command") { @@ -124,7 +124,7 @@ int main() { auto command_to_run = std::get(event.get_parameter("cmd")); - rcon_client.send_data("/command " + command_to_run, 3, data_type::SERVERDATA_EXECCOMMAND, [event](const rcon_response& response) { + rcon_client.send_data("/command " + command_to_run, 3, rconpp::data_type::SERVERDATA_EXECCOMMAND, [event](const rconpp::response& response) { if(response.data.empty()) { return; } @@ -176,8 +176,8 @@ int main() { info_command }); } - rcon_client.send_data("/players online count", 2, data_type::SERVERDATA_EXECCOMMAND, - [&bot](const rcon_response& response) { + rcon_client.send_data("/players online count", 2, rconpp::data_type::SERVERDATA_EXECCOMMAND, + [&bot](const rconpp::response& response) { std::string players = response.data; std::replace(players.begin(), players.end(), ':', ' '); std::replace(players.begin(), players.end(), '(', ' '); @@ -187,7 +187,7 @@ int main() { /* Create a timer that runs every 120 seconds, that sets the status */ bot.start_timer([&bot, &rcon_client](const dpp::timer& timer) { - rcon_client.send_data("/players online count", 2, data_type::SERVERDATA_EXECCOMMAND, [&bot](const rcon_response& response) { + rcon_client.send_data("/players online count", 2, rconpp::data_type::SERVERDATA_EXECCOMMAND, [&bot](const rconpp::response& response) { std::string players = response.data; std::replace(players.begin(), players.end(), ':', ' '); std::replace(players.begin(), players.end(), '(', ' '); @@ -205,9 +205,9 @@ int main() { bot.message_create(dpp::message(FDR::config.msg_channel, "Factorio-Discord-Relay (FDR) has loaded!"), [&rcon_client](const dpp::confirmation_callback_t& callback) { if(FDR::config.allow_achievements) { rcon_client.send_data("Factorio-Discord-Relay (FDR) has loaded!", 999, - data_type::SERVERDATA_EXECCOMMAND); + rconpp::data_type::SERVERDATA_EXECCOMMAND); } else { - rcon_client.send_data("/silent-command game.print(\"Factorio-Discord-Relay (FDR) has loaded!\")", 999, data_type::SERVERDATA_EXECCOMMAND); + rcon_client.send_data("/silent-command game.print(\"Factorio-Discord-Relay (FDR) has loaded!\")", 999, rconpp::data_type::SERVERDATA_EXECCOMMAND); } }); }); diff --git a/src/rcon.cpp b/src/rcon.cpp deleted file mode 100644 index 33dd6a2..0000000 --- a/src/rcon.cpp +++ /dev/null @@ -1,231 +0,0 @@ -#include "../include/rcon.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -rcon::rcon(const std::string& addr, const unsigned int _port, const std::string& pass) : address(addr), port(_port), password(pass) { - - std::cout << "Attempting connection to RCON server..." << "\n"; - - if(!connect()) { - std::cout << "RCON is aborting as it failed to initiate." << "\n"; - close(sock); - 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! - rcon_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"; - close(sock); - return; - } - - std::cout << "Sent login data." << "\n"; - - connected = true; - - std::thread queue_runner([this]() { - while(connected) { - if(requests_queued.empty()) { - continue; - } - - for(rcon_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(); -}; - -void rcon::send_data(const std::string& data, const int32_t id, data_type type, std::function callback) { - requests_queued.emplace_back(data, id, type, callback); -} - -const rcon_response rcon::send_data_sync(const std::string data, const int32_t id, data_type type, bool feedback) { - if(!connected && type != data_type::SERVERDATA_AUTH) { - std::cout << "Cannot send data when not connected." << "\n"; - return {"", false}; - } - - unsigned long long packet_len = data.length() + HEADER_SIZE; - unsigned char packet[packet_len]; - form_packet(packet, data, id, type); - - if(::send(sock, packet, packet_len, 0) < 0) { - std::cout << "Sending failed!" << "\n"; - return {"", false}; - } - - if(!feedback) { - // Because we do not want any feedback, we just send no data and say the server didn't respond. - return {"", false}; - } - - // Server will send a SERVERDATA_RESPONSE_VALUE packet. - return receive_information(id, type); -} - -bool rcon::connect() { - // Create new TCP socket. - sock = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - - if(sock == -1) { - std::cout << "Failed to open socket." << "\n"; - return false; - } - - // Setup port, address, and family. - struct sockaddr_in server; - server.sin_family = AF_INET; - server.sin_addr.s_addr = inet_addr(address.c_str()); - server.sin_port = htons(port); - - // Make it non-blocking. - fcntl(sock, F_SETFL, O_NONBLOCK); - - // Set a timeout of 4 seconds. - struct timeval tv{}; - tv.tv_sec = DEFAULT_TIMEOUT; - tv.tv_usec = 0; - fd_set fds; - FD_ZERO(&fds); - FD_SET(sock, &fds); - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - - // Connect to the socket and set the status to our temp status. - if(::connect(sock, (struct sockaddr *)&server, sizeof(server)) == -1) { - if(errno != EINPROGRESS) { - return false; - } - } - - // Create temp status - int status = select(sock +1, nullptr, &fds, nullptr, &tv); - fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) & ~O_NONBLOCK); - - // If status wasn't zero, we successfully connected. - return status != 0; -} - -void rcon::form_packet(unsigned char packet[], const std::string& data, int32_t id, int32_t type) { - const char nullbytes[] = {'\x00', '\x00'}; - const int32_t min_size = sizeof(id) + sizeof(type) + sizeof(nullbytes); // 10 bytes. - const int32_t data_size = static_cast(data.size()) + min_size; - - if(data_size > 4096) { - std::cout << "This packet is too big to send. Please generate a smaller packet." << "\n"; - return; - } - - (memset(packet, '\0', data_size), (void) 0); - - // Each part is 4 bytes - packet[0] = data_size; - packet[4] = id; - packet[8] = type; - - const char* data_chars = data.c_str(); - - for(int i = 0; i < data_size; i++) { - packet[12 + i] = data_chars[i]; - } -} - -rcon_response rcon::receive_information(int32_t id, data_type type) { - // Whilst this loop is better than a while loop, - // it should really just keep going for a certain amount of seconds. - for(int i=0; i < 500; i++) { - rcon_packet packet = read_packet(); - - if(packet.bytes == 0) { - if(type != SERVERDATA_AUTH) - return {"", packet.server_responded}; - else - continue; - } - - unsigned char* buffer = packet.data; - - if(type == SERVERDATA_AUTH) { - if(byte32_to_int(buffer) == -1) { - return {"", false}; - } else { - if(byte32_to_int(packet.data) == id) { - return {"", true}; - } - } - } - - int offset = packet.bytes - HEADER_SIZE + 3; - - if(offset == -1) - continue; - - std::string part(&buffer[8], &buffer[8] + offset); - - if(byte32_to_int(packet.data) == id) { - return {part, packet.server_responded}; - } - } - return {"", false}; -} - -rcon_packet rcon::read_packet() { - size_t packet_length = read_packet_length(); - - /* - * If the packet length is -1, the server didn't respond. - * If the packet length is 0, the server did respond but said nothing. - */ - if(packet_length == -1) { - return {0, nullptr, false}; - } else if(packet_length == 0) { - return {0, nullptr, true}; - } - - auto* buffer = new unsigned char[packet_length]{0}; - unsigned int bytes = 0; - - do { - size_t recv_bytes = ::recv(sock, buffer, packet_length - bytes, 0); - if(recv_bytes == -1) { - std::cout << "Did not receive a packet in time. Did the server send a response?" << "\n"; - return {0, nullptr, false}; - } - - bytes += recv_bytes; - - } while(bytes < packet_length); - - return {bytes, buffer}; -} - -const size_t rcon::read_packet_length() { - auto* buffer = new unsigned char[4]{0}; - size_t recv_bytes = ::recv(sock, buffer, 4, 0); - if(recv_bytes == -1) { - std::cout << "Did not receive a packet in time. Did the server send a response?" << "\n"; - return -1; - } - const size_t len = byte32_to_int(buffer); - delete[] buffer; - return len; -}