diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b05eb2..347611d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.0) project(jupiter_bot) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") include(build/CMakeLists.txt) diff --git a/Configs/RenX.FuckCronus.ini b/Configs/RenX.FuckCronus.ini new file mode 100644 index 0000000..b885b90 --- /dev/null +++ b/Configs/RenX.FuckCronus.ini @@ -0,0 +1,22 @@ +; File: RenX.FuckCronus.ini +; +; Function: +; Sanitizes logs going to the Renegade X devbot (server list), and sanitizes commands coming from the Renegade X devbot. +; +; Rationale: +; Certain server owners report great unease about a specific Totem Arts sysadmin who is entirely unrestrained, who has +; entirely unrestrained access to player personal information such as IP addresses, Hardware IDs (MAC addresses), +; SteamIDs, and usernames, particularly since it's known that this sysadmin logs all of that information every time any +; player joins any Renegade X server. It's also known that this data is stored alongside C&C Renegade player information, +; meaning that C&C Renegade players and Renegade X players can be tracked across each game. +; By sanitizing personal information going to the DevBot, players can be feel safe knowing that their data is not being +; tracked by a sysadmin who is extremely well known to hoard, steal, and misuse data. +; +; Settings: +; SanitizeNames=Bool (Default: true; sanitizes all player names from messages) +; SanitizeIPs=Bool (Default: true; sanitizes all player IPs from all messages) +; SanitizeHWIDs=Bool (Default: true; sanitizes all player HWIDs from all messages) +; SanitizeSteamIDs=Bool (Default: true; sanitizes all player SteamIDs from all messages) +; SanitizeUnknownCmds=Bool (Default: true; sanitizes all unknown commands coming from devbot) +; SanitizeBlacklistedCmds=Bool (Default: true; sanitizes all non-informational commands coming from devbot) +; diff --git a/MakeRelease.bat b/MakeRelease.bat index 0fa0c73..8d90903 100644 --- a/MakeRelease.bat +++ b/MakeRelease.bat @@ -4,7 +4,7 @@ ECHO. SET Platform=Win32 SET NoArgs=False -SET BinDir=cmake-build-release---%Platform%\bin\\ +SET BinDir=cmake-build-release\bin\\ if "%1" == "" SET NoArgs=True diff --git a/src/Jupiter b/src/Jupiter index b409190..fd01d3a 160000 --- a/src/Jupiter +++ b/src/Jupiter @@ -1 +1 @@ -Subproject commit b4091905511e37933613cabc32e5b1752455d7eb +Subproject commit fd01d3a73509a545608f850dbf924d30e7e0d858 diff --git a/src/Plugins/RenX/CMakeLists.txt b/src/Plugins/RenX/CMakeLists.txt index 97111a8..14476eb 100644 --- a/src/Plugins/RenX/CMakeLists.txt +++ b/src/Plugins/RenX/CMakeLists.txt @@ -18,6 +18,7 @@ add_subdirectory(RenX.ChatLogging) add_subdirectory(RenX.CommandLogging) add_subdirectory(RenX.ExcessiveHeadshots) add_subdirectory(RenX.ExtraLogging) +add_subdirectory(RenX.FuckCronus) add_subdirectory(RenX.Greetings) add_subdirectory(RenX.HybridUUID) add_subdirectory(RenX.IRCJoin) diff --git a/src/Plugins/RenX/RenX.FuckCronus/CMakeLists.txt b/src/Plugins/RenX/RenX.FuckCronus/CMakeLists.txt new file mode 100644 index 0000000..a71adcd --- /dev/null +++ b/src/Plugins/RenX/RenX.FuckCronus/CMakeLists.txt @@ -0,0 +1,3 @@ +add_renx_plugin(RenX.FuckCronus + FuckCronus.cpp + FuckCronus.h) \ No newline at end of file diff --git a/src/Plugins/RenX/RenX.FuckCronus/FuckCronus.cpp b/src/Plugins/RenX/RenX.FuckCronus/FuckCronus.cpp new file mode 100644 index 0000000..dd37912 --- /dev/null +++ b/src/Plugins/RenX/RenX.FuckCronus/FuckCronus.cpp @@ -0,0 +1,651 @@ +/** + * Copyright (C) 2021 Jessica James. All rights reserved. + * Written by Jessica James + */ + +#include "FuckCronus.h" +#include +#include +#include +#include +#include +#include "Jupiter/IRC.h" +#include "RenX_Functions.h" +#include "RenX_Server.h" +#include "RenX_PlayerInfo.h" + +using namespace Jupiter::literals; +using namespace std::literals; +constexpr const char* g_devbot_hostname = "devbot.ren-x.com"; +constexpr uint16_t g_devbot_port = 21337; +constexpr const char g_blank_steamid[] = "0x0000000000000000"; +constexpr std::chrono::steady_clock::duration g_reconnect_delay = std::chrono::seconds{15 }; +constexpr std::chrono::steady_clock::duration g_activity_timeout = std::chrono::seconds{ 60 }; + +int RenX_FuckCronusPlugin::think() { + for (auto& server_pair : m_server_info_map) { + auto server = server_pair.first; + auto& server_info = server_pair.second; + auto& devbot_socket = server_info.m_socket; + + if (!devbot_socket) { + // This should never happen + return 0; + } + + if (!server_info.m_devbot_connected) { + // Not connected; attempt retry if needed + if (std::chrono::steady_clock::now() >= server_info.m_last_connect_attempt + g_reconnect_delay) { + if (devbot_socket->connect(g_devbot_hostname, g_devbot_port)) { + // There's some handshake garbage that needs to go on here so the devbot accepts us + devbot_connected(*server, server_info); + server->sendLogChan(IRCCOLOR "03[RenX]" IRCCOLOR " Socket successfully reconnected to DevBot; game server now listed."); + } + else { + server->sendLogChan(IRCCOLOR "04[Error]" IRCCOLOR " Failed to reconnect to DevBot; game server not listed."); + } + + // Update our timings + server_info.m_last_connect_attempt = std::chrono::steady_clock::now(); + server_info.m_last_activity = server_info.m_last_connect_attempt; + } + } + else if (std::chrono::steady_clock::now() - server_info.m_last_activity >= g_activity_timeout) // ping timeout + { + // Ping timeout; disconnect immediately + server->sendLogChan(STRING_LITERAL_AS_REFERENCE(IRCCOLOR "04[Error]" IRCCOLOR " Disconnected from DevBot (ping timeout); game server is no longer listed.")); + devbot_disconnected(*server, server_info); + } + else { + // Connected and fine + if (devbot_socket->recv() > 0) // Data received + { + Jupiter::ReadableString::TokenizeResult result = Jupiter::ReferenceString::tokenize(devbot_socket->getBuffer(), '\n'); + if (result.token_count != 0) + { + server_info.m_last_activity = std::chrono::steady_clock::now(); + server_info.m_last_line.concat(result.tokens[0]); + if (result.token_count != 1) + { + // Process devbot message received + process_devbot_message(server, server_info.m_last_line); + server_info.m_last_line = result.tokens[result.token_count - 1]; + + for (size_t index = 1; index != result.token_count - 1; ++index) + process_devbot_message(server, result.tokens[index]); + } + } + } + else if (Jupiter::Socket::getLastError() == JUPITER_SOCK_EWOULDBLOCK) // Operation would block (no new data) + { + if (std::chrono::steady_clock::now() - server_info.m_last_activity >= g_activity_timeout) { + devbot_disconnected(*server, server_info); + } + } + else // This is a serious error + { + devbot_disconnected(*server, server_info); + + server->sendLogChan(IRCCOLOR "07[Warning]" IRCCOLOR " Connection to DevBot lost. Reconnection attempt in progress."); + if (devbot_socket->connect(g_devbot_hostname, g_devbot_port)) { + devbot_connected(*server, server_info); + server->sendLogChan(IRCCOLOR "06[Progress]" IRCCOLOR " Connection to DevBot reestablished. Initializing Renegade-X RCON protocol..."); + } + else { + server->sendLogChan(IRCCOLOR "04[Error]" IRCCOLOR " Connection to DevBot lost. Reconnection attempt failed."); + } + + // Update our timings + server_info.m_last_connect_attempt = std::chrono::steady_clock::now(); + server_info.m_last_activity = server_info.m_last_connect_attempt; + + return 0; + } + } + } + + return 0; +} + +bool RenX_FuckCronusPlugin::initialize() { + m_init_time = std::chrono::steady_clock::now(); + m_sanitize_names = config.get("SanitizeNames"_jrs, true); + m_sanitize_ips = config.get("SanitizeIPs"_jrs, true); + m_sanitize_hwids = config.get("SanitizeHWIDs"_jrs, true); + m_sanitize_steam_ids = config.get("SanitizeSteamIDs"_jrs, true); + m_sanitize_unknown_commands = config.get("SanitizeUnknownCmds"_jrs, true); + m_sanitize_blacklisted_commands = config.get("SanitizeBlacklistedCmds"_jrs, true); + + return RenX::Plugin::initialize(); +} + +void RenX_FuckCronusPlugin::RenX_OnServerCreate(RenX::Server &server) { + auto& server_info = m_server_info_map[&server]; + + server_info.m_socket = std::unique_ptr(new Jupiter::TCPSocket()); + if (server_info.m_socket->connect(g_devbot_hostname, g_devbot_port)) { + devbot_connected(server, server_info); + } +} + +void RenX_FuckCronusPlugin::RenX_OnServerDisconnect(RenX::Server &server, RenX::DisconnectReason reason) { + auto pair_itr = m_server_info_map.find(&server); + if (pair_itr != m_server_info_map.end()) { + auto& socket_ptr = pair_itr->second.m_socket; + if (socket_ptr) { + socket_ptr->close(); + } + } + + m_server_info_map.erase(&server); +} + +// There's not truly any way to know for certain that a token is a player token without message-specific positional context, +// but the format is just specific enough that there shouldn't be many false positives. For false positives that do occur, +// we likely don't really care anyways, since this is just getting forwarded to the devbot +bool is_player_token(const char* begin, const char* end) { + std::regex player_token_regex{ "[A-Za-z]*,b?[0-9]+,.+" }; + std::cmatch match_result; + return std::regex_match(begin, end, match_result, player_token_regex); +} + +/** Copied from Rx_TCPLink so that the same formatting bug is included */ + +const char hexadecimal_rep_table_upper[][3] = { + "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F", + "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B", "1C", "1D", "1E", "1F", + "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F", + "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C", "3D", "3E", "3F", + "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4A", "4B", "4C", "4D", "4E", "4F", + "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D", "5E", "5F", + "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6A", "6B", "6C", "6D", "6E", "6F", + "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E", "7F", + "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8A", "8B", "8C", "8D", "8E", "8F", + "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F", + "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF", + "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF", + "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF", + "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF", + "E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF", + "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF" +}; + +struct HWID { + union { + uint64_t hwid; + struct { + uint32_t left; + uint32_t right; + }; + }; +}; + +template +std::string to_hex(T in_integer) { + std::string result; + uint8_t* begin = reinterpret_cast(&in_integer); + uint8_t* itr = begin + sizeof(T); + + result.reserve(sizeof(in_integer) * 2); + while (itr != begin) { + --itr; + result += hexadecimal_rep_table_upper[*itr]; + } + + 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_FuckCronusPlugin::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); + bool required_sanitization = false; + + // Ensure valid message received + if (tokens.token_count == 0) { + return; + } + + // 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; + } + socket = pair_itr->second.m_socket.get(); + if (!socket) { + return; + } + } + + auto findPlayerByIP = [&server](const Jupiter::ReadableString& in_ip) -> const RenX::PlayerInfo* { + // Parse into integer so we're doing int comparisons instead of strings + auto ip32 = Jupiter::Socket::pton4(static_cast(in_ip).c_str()); + + // Search players + for (const auto& player : server.players) { + if (player.ip32 == ip32) { + return &player; + } + } + + return nullptr; + }; + + auto findPlayerByHWID = [&server](const Jupiter::ReadableString& in_hwid) -> const RenX::PlayerInfo* { + for (const auto& player : server.players) { + if (player.hwid == in_hwid) { + return &player; + } + } + + return nullptr; + }; + + auto findPlayerBySteamID = [&server](const Jupiter::ReadableString& in_steamid) -> const RenX::PlayerInfo* { + uint64_t steamid = in_steamid.asUnsignedLongLong(); + for (const auto& player : server.players) { + if (player.steamid == steamid) { + return &player; + } + } + + return nullptr; + }; + + if (m_sanitize_names) { + for (size_t index = 0; index != tokens.token_count; ++index) { + auto& token = tokens.tokens[index]; + if (is_player_token(token.ptr(), token.ptr() + token.size())) { + // Get token pieces + Jupiter::ReferenceString teamToken = Jupiter::ReferenceString::getToken(token, 0, ','); + Jupiter::ReferenceString idToken = Jupiter::ReferenceString::getToken(token, 1, ','); + + Jupiter::StringS replacement_player = teamToken; + replacement_player += ','; + replacement_player += idToken; + replacement_player += ','; + if (idToken.isNotEmpty() && idToken.get(0) == 'b') { + idToken.shiftRight(1); + } + + // Name (sanitized) + replacement_player += "Player"; + replacement_player += idToken; + + token = replacement_player; + required_sanitization = true; + } + } + } + + if (m_sanitize_ips || m_sanitize_hwids) { + // It's way too much of a pain to check for command usages, and it's not full-proof either (an alias or similar could be added). + // So instead, we'll just search all messages for any tokens containing any player's IP or HWID. + // This isn't terribly efficient, but there's only up to 64 players, so not a huge concern + for (size_t index = 0; index != tokens.token_count; ++index) { + auto& token = tokens.tokens[index]; + const RenX::PlayerInfo* player; + if (m_sanitize_ips) { + player = findPlayerByIP(token); + if (player != nullptr) { + // Initialize the engine here using the init time, so that player fake IPs will be consistent + // Also include player ID so we get different IPs between players and for each match + std::mt19937 randgen(m_init_time.time_since_epoch().count() + (player->id * 2)); + std::uniform_int_distribution dist(10, 200); + + // Replace real IP with fake + token.format("%u.%u.%u.%u", + static_cast(dist(randgen)), + static_cast(dist(randgen)), + static_cast(dist(randgen)), + static_cast(dist(randgen))); + + required_sanitization = true; + continue; + } + } + + if (m_sanitize_hwids) { + player = findPlayerByHWID(token); + if (player != nullptr) { + // Initialize the engine here using the init time, so that player fake HWIDs will be consistent + // Also include player ID so we get different HWIDs between players and for each match + std::mt19937 randgen(m_init_time.time_since_epoch().count() + (player->id * 2 + 69)); + std::uniform_int_distribution dist(0, 0x0000FFFFFFFFFFFFULL); + + HWID hwid{}; + hwid.hwid = dist(randgen); + + token = "m"; + token += to_hex(hwid.left) + to_hex(hwid.right); + + required_sanitization = true; + continue; + } + } + + if (m_sanitize_steam_ids) { + player = findPlayerBySteamID(token); + if (player != nullptr) { + token = g_blank_steamid; + continue; + } + } + + // More sanitization checks here... + } + } + + Jupiter::StringS line_sanitized; + if (required_sanitization) { + // Construct line to send and send it + line_sanitized = tokens.tokens[0]; + for (size_t index = 1; index != tokens.token_count; ++index) { + line_sanitized += RenX::DelimC; + line_sanitized += tokens.tokens[index]; + } + } + else { + // Forward line without modification + line_sanitized = line; + } + + line_sanitized += '\n'; + socket->send(line_sanitized); +} + +void RenX_FuckCronusPlugin::devbot_connected(RenX::Server& in_server, ext_server_info& in_server_info) { + in_server_info.m_devbot_connected = true; + in_server_info.m_socket->setBlocking(false); + + // New format: 004 | Game Version Number | Game Version + auto& version_str = in_server.getGameVersion(); + std::string version_message = "v004"; + version_message += RenX::DelimC; + version_message += std::to_string(in_server.getGameVersionNumber()); + version_message += RenX::DelimC; + version_message.append(version_str.ptr(), version_str.size()); + version_message += '\n'; + in_server_info.m_socket->send(version_message.c_str(), version_message.size()); +} + +void RenX_FuckCronusPlugin::devbot_disconnected(RenX::Server&, ext_server_info& in_server_info) { + in_server_info.m_devbot_connected = false; + + if (in_server_info.m_socket) { + in_server_info.m_socket->close(); + } +} + +void RenX_FuckCronusPlugin::process_devbot_message(RenX::Server* in_server, const Jupiter::ReadableString& in_line) { + if (in_line.isEmpty()) { + return; + } + + // Sanitize unknown & blacklisted commands + if (in_line[0] == 'c' && in_line.size() > 1) { + 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() }; + + if (m_sanitize_unknown_commands + && g_known_commands.find(command_view) == g_known_commands.end()) { + // Command not in known commands list; ignore it + return; + } + + if (m_sanitize_blacklisted_commands + && g_blacklist_commands.find(command_view) != g_blacklist_commands.end()) { + // Command is blacklisted; ignore it + return; + } + } + } + + // Send line to game server + Jupiter::StringS sanitized_message = in_line; + sanitized_message += '\n'; + in_server->sendData(in_line); +} + +// Plugin instantiation and entry point. +RenX_FuckCronusPlugin pluginInstance; + +extern "C" JUPITER_EXPORT Jupiter::Plugin *getPlugin() +{ + return &pluginInstance; +} diff --git a/src/Plugins/RenX/RenX.FuckCronus/FuckCronus.h b/src/Plugins/RenX/RenX.FuckCronus/FuckCronus.h new file mode 100644 index 0000000..b862d28 --- /dev/null +++ b/src/Plugins/RenX/RenX.FuckCronus/FuckCronus.h @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2021 Jessica James. All rights reserved. + * Written by Jessica James + */ + +#if !defined _EXAMPLE_H_HEADER +#define _EXAMPLE_H_HEADER + +#include "Jupiter/Plugin.h" +#include "Jupiter/Reference_String.h" +#include "Jupiter/TCPSocket.h" +#include "RenX_Plugin.h" + +class RenX_FuckCronusPlugin : public RenX::Plugin +{ +public: // Jupiter::Thinker + int think() override; + +public: // Jupiter::Plugin + bool initialize() override; + +public: // RenX::Plugin + void RenX_OnServerCreate(RenX::Server &server) override; + void RenX_OnServerDisconnect(RenX::Server &server, RenX::DisconnectReason reason) override; + void RenX_OnRaw(RenX::Server &server, const Jupiter::ReadableString &raw) override; + +private: + 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; + }; + + void devbot_connected(RenX::Server& in_server, ext_server_info& in_server_info); + void devbot_disconnected(RenX::Server& in_server, ext_server_info& in_server_info); + void process_devbot_message(RenX::Server* in_server, const Jupiter::ReadableString& in_line); + + std::unordered_map m_server_info_map; + std::chrono::steady_clock::time_point m_init_time{}; + bool m_sanitize_names{}; + bool m_sanitize_ips{}; + bool m_sanitize_hwids{}; + bool m_sanitize_steam_ids{}; + bool m_sanitize_unknown_commands{}; + bool m_sanitize_blacklisted_commands{}; +}; + +#endif // _EXAMPLE_H_HEADER \ No newline at end of file diff --git a/src/Plugins/RenX/RenX.FuckCronus/README.md b/src/Plugins/RenX/RenX.FuckCronus/README.md new file mode 100644 index 0000000..4e46591 --- /dev/null +++ b/src/Plugins/RenX/RenX.FuckCronus/README.md @@ -0,0 +1,22 @@ +== Function == + +Sanitizes logs going to the Renegade X devbot (server list), and sanitizes commands coming from the Renegade X devbot. + +When all sanitizations are disabled, this can also be used to change the listed IP address for a Renegade X server, +without relying on Cronus. This is done by hosting Jupiter on the IP address you wish to have listed, and routing +traffic from there. + +== Rationale == + +*Short*: Thieves shouldn't be trusted with personally identifiable information. This prevents that. + +*Long* (copied from RenX.FuckCronus.ini): + +Certain server owners report great unease about a specific Totem Arts sysadmin who is entirely unrestrained, who has +entirely unrestrained access to player personal information such as IP addresses, Hardware IDs (MAC addresses), +SteamIDs, and usernames, particularly since it's known that this sysadmin logs all of that information every time any +player joins any Renegade X server. It's also known that this data is stored alongside C&C Renegade player information, +meaning that C&C Renegade players and Renegade X players can be tracked across each game. + +By sanitizing personal information going to the DevBot, players can be feel safe knowing that their data is not being +tracked by a sysadmin who is extremely well known to hoard, steal, and misuse data. \ No newline at end of file