From 0aac75bc88c11e51a8ea97495dd95d7473ce720d Mon Sep 17 00:00:00 2001 From: Jessica James Date: Wed, 3 Nov 2021 16:36:03 -0500 Subject: [PATCH] Improved command faking --- CMakeLists.txt | 2 +- src/Plugins/RenX/RenX.Relay/RenX_Relay.cpp | 477 +++++++++------------ src/Plugins/RenX/RenX.Relay/RenX_Relay.h | 16 + 3 files changed, 229 insertions(+), 266 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f028dc..4105777 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.0) project(jupiter_bot) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") include(build/CMakeLists.txt) diff --git a/src/Plugins/RenX/RenX.Relay/RenX_Relay.cpp b/src/Plugins/RenX/RenX.Relay/RenX_Relay.cpp index 2b57195..003559b 100644 --- a/src/Plugins/RenX/RenX.Relay/RenX_Relay.cpp +++ b/src/Plugins/RenX/RenX.Relay/RenX_Relay.cpp @@ -14,6 +14,9 @@ #include "RenX_Server.h" #include "RenX_PlayerInfo.h" +// String literal redefinition of RenX::DelimC +#define RX_DELIM "\x02" + using namespace Jupiter::literals; using namespace std::literals; constexpr const char g_blank_steamid[] = "0x0000000000000000"; @@ -106,6 +109,62 @@ int RenX_RelayPlugin::think() { return 0; } +bool noop_handler(std::string_view, RenX::Server&, std::vector&) { + return true; +} + +bool handle_ping(std::string_view in_command_line, RenX::Server&, std::vector& out_response) { + std::string pong_message; + pong_message.reserve(in_command_line.size() + 1); + pong_message = "PONG"sv; + pong_message += RenX::DelimC; + if (in_command_line.size() > pong_message.size()) { + pong_message += in_command_line.substr(5); + } + out_response.push_back(std::move(pong_message)); + return true; +} + +static const std::unordered_set g_known_commands { + "addmap"sv, "amsg"sv, "botlist"sv, "botvarlist"sv, "buildinginfo"sv, "binfo"sv, "buildinglist"sv, "blist"sv, + "cancelvote"sv, "votestop"sv, "changemap"sv, "setmap"sv, "changename"sv, "changeplayername"sv, "clientlist"sv, + "clientvarlist"sv, "disarm"sv, "disarmbeacon"sv, "disarmb"sv, "disarmc4"sv, "dumpkilllog"sv, "dumpkills"sv, + "endmap"sv, "gameover"sv, "endgame"sv, "fkick"sv, "forcekick"sv, "forcenonseamless"sv, "forceseamless"sv, + "gameinfo"sv, "ginfo"sv, "hascommand"sv, "help"sv, "hostprivatesay"sv, "page"sv, "hostsay"sv, "say"sv, "kick"sv, + "kickban"sv, "kill"sv, "listmutators"sv, "listmutator"sv, "mutatorlist"sv, "mutatorslist"sv, "loadmutator"sv, + "mutatorload"sv, "lockbuildings"sv, "lockhealth"sv, "lockb"sv, "lockh"sv, "lb"sv, "makeadmin"sv, "map"sv, "getmap"sv, + "mineban"sv, "mban"sv, "minelimit"sv, "mlimit"sv, "mineunban"sv, "unmineban"sv, "munban"sv, "unmban"sv, + "mutateasnone"sv, "mutateasplayer"sv, "normalmode"sv, "nmode"sv, "ping"sv, "playerinfo"sv, "pamsg"sv, "recorddemo"sv, + "demorecord"sv, "demorec"sv, "removemap"sv, "rotation"sv, "serverinfo"sv, "sinfo"sv, "setcommander"sv, + "spectatemode"sv, "smode"sv, "swapteams"sv, "teamswap"sv, "team"sv, "changeteam"sv, "team2"sv, "changeteam2"sv, + "teaminfo"sv, "tinfo"sv, "textmute"sv, "mute"sv, "textunmute"sv, "unmute"sv, "togglebotvoice"sv, "mutebot"sv, + "mutebots"sv, "unmutebot"sv, "unmutebots"sv, "cheatbots"sv, "togglecheatbots"sv, "switchcheatbots"sv, "forcebots"sv, + "toggleforcebots"sv, "switchforcebots"sv, "togglesuspect"sv, "travel"sv, "removemutator"sv, "mutatorremove"sv, + "unloadmutator"sv, "mutatorunload"sv, "vehiclelimit"sv, "vlimit"sv, "warn"sv +}; + +static const std::unordered_set g_whitelist_commands { + "map"sv, "help"sv, "playerinfo"sv, "sinfo"sv, "teaminfo"sv, "hascommand"sv, "buildinglist"sv, "blist"sv, + "clientvarlist"sv, "getmap"sv, "buildinginfo"sv, "botlist"sv, "vlimit"sv, "serverinfo"sv, "ginfo"sv, "rotation"sv, + "binfo"sv, "vehiclelimit"sv, "tinfo"sv, "botvarlist"sv, "minelimit"sv, "gameinfo"sv, "clientlist"sv, "mlimit"sv, + "ping"sv, +}; + +static const std::unordered_set g_blacklist_commands { + "addmap"sv, "admin"sv, "amsg"sv, "cancelvote"sv, "votestop"sv, "changemap"sv, "setmap"sv, "changename"sv, + "changeplayername"sv, "disarm"sv, "disarmbeacon"sv, "disarmb"sv, "disarmc4"sv, "dumpkilllog"sv, "dumpkills"sv, + "endmap"sv, "gameover"sv, "endgame"sv, "fkick"sv, "forcekick"sv, "forcenonseamless"sv, "forceseamless"sv, + "hostprivatesay"sv, "page"sv, "hostsay"sv, "say"sv, "kick"sv, "kickban"sv, "kill"sv, "listmutators"sv, + "listmutator"sv, "mutatorlist"sv, "mutatorslist"sv, "loadmutator"sv, "mutatorload"sv, "lockbuildings"sv, + "lockhealth"sv, "lockb"sv, "lockh"sv, "lb"sv, "makeadmin"sv, "mineban"sv, "mban"sv, "mineunban"sv, "unmineban"sv, + "munban"sv, "unmban"sv, "mutateasnone"sv, "mutateasplayer"sv, "normalmode"sv, "nmode"sv, "pamsg"sv, "recorddemo"sv, + "demorecord"sv, "demorec"sv, "removemap"sv, "setcommander"sv, "spectatemode"sv, "smode"sv, "swapteams"sv, + "teamswap"sv, "team"sv, "changeteam"sv, "team2"sv, "changeteam2"sv, "textmute"sv, "mute"sv, "textunmute"sv, + "unmute"sv, "togglebotvoice"sv, "mutebot"sv, "mutebots"sv, "unmutebot"sv, "unmutebots"sv, "cheatbots"sv, + "togglecheatbots"sv, "switchcheatbots"sv, "forcebots"sv, "toggleforcebots"sv, "switchforcebots"sv, "togglesuspect"sv, + "travel"sv, "removemutator"sv, "mutatorremove"sv, "unloadmutator"sv, "mutatorunload"sv, "warn"sv +}; + bool RenX_RelayPlugin::initialize() { m_init_time = std::chrono::steady_clock::now(); // TODO: add BindHost, BindPort @@ -116,7 +175,8 @@ bool RenX_RelayPlugin::initialize() { // * notify game server to set failover m_upstream_hostname = config.get("UpstreamHost"_jrs, "devbot.ren-x.com"); m_upstream_port = config.get("UpstreamPort"_jrs, 21337); - m_fake_pings = config.get("FakePings"_jrs, false); + m_fake_pings = config.get("FakePings"_jrs, true); + m_fake_ignored_commands = config.get("FakeIgnoredCommands"_jrs, false); // change to true if anything breaks m_sanitize_names = config.get("SanitizeNames"_jrs, true); m_sanitize_ips = config.get("SanitizeIPs"_jrs, true); m_sanitize_hwids = config.get("SanitizeHWIDs"_jrs, true); @@ -125,6 +185,22 @@ bool RenX_RelayPlugin::initialize() { m_sanitize_blacklisted_commands = config.get("SanitizeBlacklistedCmds"_jrs, true); m_suppress_chat_logs = config.get("SuppressChatLogs"_jrs, true); + // Populate fake command handlers + if (m_fake_pings) { + m_fake_command_table.emplace("ping", &handle_ping); + } + + if (m_fake_ignored_commands) { + if (m_sanitize_blacklisted_commands) { + for(auto& command : g_blacklist_commands) { + m_fake_command_table.emplace(command, &noop_handler); + } + + // Disable dropping in favor of faking + m_sanitize_blacklisted_commands = false; + } + } + return RenX::Plugin::initialize(); } @@ -206,238 +282,6 @@ std::string to_hex(T in_integer) { return result; } -static const std::unordered_set g_known_commands { - "addmap"sv, - "amsg"sv, - "botlist"sv, - "botvarlist"sv, - "buildinginfo"sv, - "binfo"sv, - "buildinglist"sv, - "blist"sv, - "cancelvote"sv, - "votestop"sv, - "changemap"sv, - "setmap"sv, - "changename"sv, - "changeplayername"sv, - "clientlist"sv, - "clientvarlist"sv, - "disarm"sv, - "disarmbeacon"sv, - "disarmb"sv, - "disarmc4"sv, - "dumpkilllog"sv, - "dumpkills"sv, - "endmap"sv, - "gameover"sv, - "endgame"sv, - "fkick"sv, - "forcekick"sv, - "forcenonseamless"sv, - "forceseamless"sv, - "gameinfo"sv, - "ginfo"sv, - "hascommand"sv, - "help"sv, - "hostprivatesay"sv, - "page"sv, - "hostsay"sv, - "say"sv, - "kick"sv, - "kickban"sv, - "kill"sv, - "listmutators"sv, - "listmutator"sv, - "mutatorlist"sv, - "mutatorslist"sv, - "loadmutator"sv, - "mutatorload"sv, - "lockbuildings"sv, - "lockhealth"sv, - "lockb"sv, - "lockh"sv, - "lb"sv, - "makeadmin"sv, - "map"sv, - "getmap"sv, - "mineban"sv, - "mban"sv, - "minelimit"sv, - "mlimit"sv, - "mineunban"sv, - "unmineban"sv, - "munban"sv, - "unmban"sv, - "mutateasnone"sv, - "mutateasplayer"sv, - "normalmode"sv, - "nmode"sv, - "ping"sv, - "playerinfo"sv, - "pamsg"sv, - "recorddemo"sv, - "demorecord"sv, - "demorec"sv, - "removemap"sv, - "rotation"sv, - "serverinfo"sv, - "sinfo"sv, - "setcommander"sv, - "spectatemode"sv, - "smode"sv, - "swapteams"sv, - "teamswap"sv, - "team"sv, - "changeteam"sv, - "team2"sv, - "changeteam2"sv, - "teaminfo"sv, - "tinfo"sv, - "textmute"sv, - "mute"sv, - "textunmute"sv, - "unmute"sv, - "togglebotvoice"sv, - "mutebot"sv, - "mutebots"sv, - "unmutebot"sv, - "unmutebots"sv, - "cheatbots"sv, - "togglecheatbots"sv, - "switchcheatbots"sv, - "forcebots"sv, - "toggleforcebots"sv, - "switchforcebots"sv, - "togglesuspect"sv, - "travel"sv, - "removemutator"sv, - "mutatorremove"sv, - "unloadmutator"sv, - "mutatorunload"sv, - "vehiclelimit"sv, - "vlimit"sv, - "warn"sv -}; - -static const std::unordered_set g_whitelist_commands { - "map"sv, - "help"sv, - "playerinfo"sv, - "sinfo"sv, - "teaminfo"sv, - "hascommand"sv, - "buildinglist"sv, - "blist"sv, - "clientvarlist"sv, - "getmap"sv, - "buildinginfo"sv, - "botlist"sv, - "vlimit"sv, - "serverinfo"sv, - "ginfo"sv, - "rotation"sv, - "binfo"sv, - "vehiclelimit"sv, - "tinfo"sv, - "botvarlist"sv, - "minelimit"sv, - "gameinfo"sv, - "clientlist"sv, - "mlimit"sv, - "ping"sv, -}; - -static const std::unordered_set g_blacklist_commands { - "addmap"sv, - "admin"sv, // Console command - "amsg"sv, - "cancelvote"sv, - "votestop"sv, - "changemap"sv, - "setmap"sv, - "changename"sv, - "changeplayername"sv, - "disarm"sv, - "disarmbeacon"sv, - "disarmb"sv, - "disarmc4"sv, - "dumpkilllog"sv, - "dumpkills"sv, - "endmap"sv, - "gameover"sv, - "endgame"sv, - "fkick"sv, - "forcekick"sv, - "forcenonseamless"sv, - "forceseamless"sv, - "hostprivatesay"sv, - "page"sv, - "hostsay"sv, - "say"sv, - "kick"sv, - "kickban"sv, - "kill"sv, - "listmutators"sv, - "listmutator"sv, - "mutatorlist"sv, - "mutatorslist"sv, - "loadmutator"sv, - "mutatorload"sv, - "lockbuildings"sv, - "lockhealth"sv, - "lockb"sv, - "lockh"sv, - "lb"sv, - "makeadmin"sv, - "mineban"sv, - "mban"sv, - "mineunban"sv, - "unmineban"sv, - "munban"sv, - "unmban"sv, - "mutateasnone"sv, - "mutateasplayer"sv, - "normalmode"sv, - "nmode"sv, - "pamsg"sv, - "recorddemo"sv, - "demorecord"sv, - "demorec"sv, - "removemap"sv, - "setcommander"sv, - "spectatemode"sv, - "smode"sv, - "swapteams"sv, - "teamswap"sv, - "team"sv, - "changeteam"sv, - "team2"sv, - "changeteam2"sv, - "textmute"sv, - "mute"sv, - "textunmute"sv, - "unmute"sv, - "togglebotvoice"sv, - "mutebot"sv, - "mutebots"sv, - "unmutebot"sv, - "unmutebots"sv, - "cheatbots"sv, - "togglecheatbots"sv, - "switchcheatbots"sv, - "forcebots"sv, - "toggleforcebots"sv, - "switchforcebots"sv, - "togglesuspect"sv, - "travel"sv, - "removemutator"sv, - "mutatorremove"sv, - "unloadmutator"sv, - "mutatorunload"sv, - "warn"sv -}; - void RenX_RelayPlugin::RenX_OnRaw(RenX::Server &server, const Jupiter::ReadableString &line) { // Not parsing any escape sequences, so data gets sent to devbot exactly as it's received here. Copy tokens where needed to process escape sequences. Jupiter::ReadableString::TokenizeResult tokens = Jupiter::StringS::tokenize(line, RenX::DelimC); @@ -449,20 +293,48 @@ void RenX_RelayPlugin::RenX_OnRaw(RenX::Server &server, const Jupiter::ReadableS } // Check that we already have a session for this server - Jupiter::TCPSocket* socket{}; - { - auto pair_itr = m_server_info_map.find(&server); - if (pair_itr == m_server_info_map.end()) { - return; + auto server_info_map_itr = m_server_info_map.find(&server); + if (server_info_map_itr == m_server_info_map.end()) { + // early out: server not yet registered (i.e: finished auth) + return; + } + + ext_server_info& server_info = server_info_map_itr->second; + Jupiter::TCPSocket* socket = server_info.m_socket.get(); + if (!socket) { + // early out: no upstream RCON session + return; + } + + if (m_suppress_chat_logs + && tokens.getToken(0) == "lCHAT") { + return; + } + + // Suppress unassociated command execution logs from going upstream + if (tokens.token_count >= 5 + && !server_info.m_response_queue.empty() + && tokens.tokens[0] == "lRCON" + && tokens.tokens[1] == "Command;" + && tokens.tokens[2] == server.getRCONUsername() + && tokens.tokens[3] == "executed:" + && tokens.tokens[4].isNotEmpty()) { + // if m_processing_command is already true, there's an unhandled protocol error here, and something is likely to eventually go wrong + if (tokens.tokens[4] == server_info.m_response_queue.front().m_command) { + // This is the next command we're been waiting on; mark processing command and let this go through + server_info.m_processing_command = true; } - socket = pair_itr->second.m_socket.get(); - if (!socket) { + else { + // This command response wasn't requested upstream; suppress it return; } } - if (m_suppress_chat_logs - && tokens.getToken(0) == "lCHAT") { + // Suppress unassociated command responses from going upstream + if (tokens.tokens[0].isNotEmpty() + && tokens.tokens[0] == 'r' + && !server_info.m_processing_command) { + // This command response wasn't requested upstream; suppress it return; } @@ -612,6 +484,26 @@ void RenX_RelayPlugin::RenX_OnRaw(RenX::Server &server, const Jupiter::ReadableS line_sanitized += '\n'; socket->send(line_sanitized); + + if (line_sanitized[0] == 'c' + && server_info.m_processing_command) { + auto& queue = server_info.m_response_queue; + server_info.m_processing_command = false; + + if (queue.empty()) { + std::cerr << "COMMAND FINISHED PROCESSING ON EMPTY QUEUE" << std::endl; + return; + } + + // We've finished executing a command; pop it and go through any pending fakes + queue.pop_front(); + std::string response; + while (!queue.empty() && queue.front().m_is_fake) { + response = queue.front().to_rcon(server.getRCONUsername()); + server_info.m_socket->send(response.c_str(), response.size()); + queue.pop_front(); + } + } } void RenX_RelayPlugin::devbot_connected(RenX::Server& in_server, ext_server_info& in_server_info) { @@ -647,7 +539,6 @@ void RenX_RelayPlugin::devbot_disconnected(RenX::Server&, ext_server_info& in_se } void RenX_RelayPlugin::process_devbot_message(RenX::Server* in_server, const Jupiter::ReadableString& in_line, ext_server_info& in_server_info) { - if (in_line.isEmpty()) { return; } @@ -662,35 +553,36 @@ void RenX_RelayPlugin::process_devbot_message(RenX::Server* in_server, const Jup return; } - // Faking the pings is dangerous, because if this triggers in the middle of another command's response, the - // devbot is not going to be able to resume processing that command - if (m_fake_pings - && in_line == "cping"_jrs) { - // First: echo command - Jupiter::StringS pong_message = "lRCON"_jrs + RenX::DelimC + "Command;" + RenX::DelimC + in_server->getRCONUsername() - + RenX::DelimC + "executed:" + RenX::DelimC + "ping\n"; - - // Second: command response - pong_message += "rPONG"_jrs + RenX::DelimC + '\n'; - - // Third: command complete - pong_message += "c\n"; - - // Send it - in_server_info.m_socket->send(pong_message); - return; - } - // Sanitize unknown & blacklisted commands if (in_line[0] == 'c' && in_line.size() > 1) { + // Sanitize unknown & blacklisted commands if (m_sanitize_unknown_commands || m_sanitize_blacklisted_commands) { - Jupiter::ReferenceString command = Jupiter::ReferenceString::getToken(in_line, 0, ' '); - command.shiftRight(1); - std::string_view command_view{ command.ptr(), command.size() }; + Jupiter::ReferenceString command_str = Jupiter::ReferenceString::getToken(in_line, 0, ' '); + command_str.shiftRight(1); + std::string_view command_view{ command_str.ptr(), command_str.size() }; if (m_sanitize_unknown_commands && g_known_commands.find(command_view) == g_known_commands.end()) { // Command not in known commands list; ignore it + if (m_fake_ignored_commands) { + // Queue a fake response if necessary + UpstreamCommand command; + command.m_command = command_view; + command.m_is_fake = true; + command.m_response.push_back("Non-existent RconCommand - executed as ConsoleCommand"s); + + // Push upstream or to queue + if (in_server_info.m_response_queue.empty()) { + // No commands are in the queue; go ahead and shove back the response + auto response = command.to_rcon(in_server->getRCONUsername()); + in_server_info.m_socket->send(response.c_str(), response.size()); + return; + } + + // Other commands are waiting in the queue; tack this to the end to ensure command order + in_server_info.m_response_queue.push_back(std::move(command)); + return; + } return; } @@ -700,6 +592,37 @@ void RenX_RelayPlugin::process_devbot_message(RenX::Server* in_server, const Jup return; } } + + std::string_view command_line{ in_line.ptr() + 1, in_line.size() - 1 }; + std::string_view command_word = command_line.substr(0, std::min(command_line.find(' '), command_line.size())); + std::string command_word_lower; + command_word_lower.reserve(command_word.size()); + std::transform(command_word.begin(), command_word.end(), std::back_inserter(command_word_lower), + static_cast(std::tolower)); + + // Populate any fake responses (i.e: pings) + UpstreamCommand command; + command.m_command = command_line; + auto handler = m_fake_command_table.find(command_word_lower); + if (handler != m_fake_command_table.end()) { + // Execute fake command + command.m_is_fake = handler->second(command_line, *in_server, command.m_response); + if (command.m_is_fake) { + if (in_server_info.m_response_queue.empty()) { + // No commands are in the queue; go ahead and shove back the response + auto response = command.to_rcon(in_server->getRCONUsername()); + in_server_info.m_socket->send(response.c_str(), response.size()); + return; + } + + // Other commands are waiting in the queue; tack this to the end to ensure command order + in_server_info.m_response_queue.push_back(std::move(command)); + return; + } + } + + // This is not a fake command; queue it and send it + in_server_info.m_response_queue.push_back(std::move(command)); } // Send line to game server @@ -708,6 +631,30 @@ void RenX_RelayPlugin::process_devbot_message(RenX::Server* in_server, const Jup in_server->sendData(sanitized_message); } +std::string RenX_RelayPlugin::UpstreamCommand::to_rcon(const std::string_view& rcon_username) const { + std::string result; + result.reserve(m_command.size() + m_response.size() + 64); + + // First: echo command + result = std::string_view { "lRCON" RX_DELIM "Command;" RX_DELIM }; + result += rcon_username; + result += std::string_view { RX_DELIM "executed:" RX_DELIM }; + result += m_command; + result += '\n'; + + // Second: command response + for (auto& response_line : m_response) { + result += 'r'; + result += response_line; + result += '\n'; + } + + // Third: command complete + result += "c\n"sv; + + return result; +} + // Plugin instantiation and entry point. RenX_RelayPlugin pluginInstance; diff --git a/src/Plugins/RenX/RenX.Relay/RenX_Relay.h b/src/Plugins/RenX/RenX.Relay/RenX_Relay.h index fb39c56..06b7bd2 100644 --- a/src/Plugins/RenX/RenX.Relay/RenX_Relay.h +++ b/src/Plugins/RenX/RenX.Relay/RenX_Relay.h @@ -6,6 +6,9 @@ #if !defined _RELAY_H_HEADER #define _RELAY_H_HEADER +#include +#include +#include #include "Jupiter/Plugin.h" #include "Jupiter/Reference_String.h" #include "Jupiter/TCPSocket.h" @@ -25,12 +28,22 @@ public: // RenX::Plugin void RenX_OnRaw(RenX::Server &server, const Jupiter::ReadableString &raw) override; private: + struct UpstreamCommand { + std::string m_command; // including parameters + std::vector m_response; + bool m_is_fake{}; + + std::string to_rcon(const std::string_view& rcon_username) const; + }; + struct ext_server_info { std::unique_ptr m_socket; bool m_devbot_connected{}; std::chrono::steady_clock::time_point m_last_connect_attempt{}; std::chrono::steady_clock::time_point m_last_activity{}; Jupiter::StringL m_last_line; + std::deque m_response_queue; // also contains real commands + bool m_processing_command{}; }; void devbot_connected(RenX::Server& in_server, ext_server_info& in_server_info); @@ -42,6 +55,7 @@ private: std::string m_upstream_hostname; uint16_t m_upstream_port; bool m_fake_pings{}; + bool m_fake_ignored_commands{}; bool m_sanitize_names{}; bool m_sanitize_ips{}; bool m_sanitize_hwids{}; @@ -49,6 +63,8 @@ private: bool m_sanitize_unknown_commands{}; bool m_sanitize_blacklisted_commands{}; bool m_suppress_chat_logs{}; + using fake_command_handler = std::function& out_response)>; + std::unordered_map m_fake_command_table; }; #endif // _RELAY_H_HEADER \ No newline at end of file