diff --git a/src/client/component/console/console.cpp b/src/client/component/console/console.cpp index 66fc0378..63977380 100644 --- a/src/client/component/console/console.cpp +++ b/src/client/component/console/console.cpp @@ -3,6 +3,8 @@ #include "game/game.hpp" +#include "component/rcon.hpp" + #include #include "wincon.hpp" @@ -105,6 +107,11 @@ namespace console void dispatch_message(const int type, const std::string& message) { + if (rcon::message_redirect(message)) + { + return; + } + if (console::is_enabled()) { if (wincon::is_enabled()) diff --git a/src/client/component/console/terminal.cpp b/src/client/component/console/terminal.cpp index 35add923..df64bc2f 100644 --- a/src/client/component/console/terminal.cpp +++ b/src/client/component/console/terminal.cpp @@ -6,7 +6,7 @@ #include "game/game.hpp" -#include "component/command.hpp" +#include "component/rcon.hpp" #include "version.h" @@ -311,6 +311,11 @@ namespace terminal int dispatch_message(const int type, const std::string& message) { + if (rcon::message_redirect(message)) + { + return 0; + } + std::lock_guard _0(print_mutex); clear_output(); @@ -386,7 +391,7 @@ namespace terminal if (msg.message == WM_QUIT) { - command::execute("quit", false); + game::Cbuf_AddCall(0, game::Com_Quit_f); break; } diff --git a/src/client/component/dvar_cheats.cpp b/src/client/component/dvar_cheats.cpp new file mode 100644 index 00000000..2fa74332 --- /dev/null +++ b/src/client/component/dvar_cheats.cpp @@ -0,0 +1,141 @@ +#include +#include "loader/component_loader.hpp" + +#include "console/console.hpp" +#include "scheduler.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include + +namespace dvar_cheats +{ + void apply_sv_cheats(const game::dvar_t* dvar, const game::DvarSetSource source, game::DvarValue* value) + { + static const auto sv_cheats_checksum = game::Dvar_GenerateChecksum("sv_cheats"); + + if (dvar && dvar->checksum == sv_cheats_checksum) + { + // if dedi, do not allow internal to change value so servers can allow cheats if they want to + if (game::environment::is_dedi() && source == game::DvarSetSource::DVAR_SOURCE_INTERNAL) + { + value->enabled = dvar->current.enabled; + } + + // if sv_cheats was enabled and it changes to disabled, we need to reset all cheat dvars + else if (dvar->current.enabled && !value->enabled) + { + for (auto i = 0; i < *game::dvarCount; ++i) + { + const auto var = game::dvarPool[i]; + if (var && (var->flags & game::DvarFlags::DVAR_FLAG_CHEAT)) + { + game::Dvar_Reset(var, game::DvarSetSource::DVAR_SOURCE_INTERNAL); + } + } + } + } + } + + bool dvar_flag_checks(const game::dvar_t* dvar, const game::DvarSetSource source) + { + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_WRITE)) + { +#ifdef DEBUG + console::error("%s is write protected\n", dvars::dvar_get_name(dvar).data()); +#endif + return false; + } + + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_READ)) + { +#ifdef DEBUG + console::error("%s is read only\n", dvars::dvar_get_name(dvar).data()); +#endif + return false; + } + + // only check cheat/replicated values when the source is external + if (source == game::DvarSetSource::DVAR_SOURCE_EXTERNAL) + { + const auto cl_ingame = game::CL_IsGameClientActive(0); + const auto sv_running = game::Dvar_FindVar("sv_running"); + + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_REPLICATED) && (cl_ingame) && (sv_running && !sv_running->current.enabled)) + { + console::error("%s can only be changed by the server\n", dvars::dvar_get_name(dvar).data()); + return false; + } + + const auto sv_cheats = game::Dvar_FindVar("sv_cheats"); + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_CHEAT) && (sv_cheats && !sv_cheats->current.enabled)) + { +#ifdef DEBUG + console::error("%s is cheat protected\n", dvars::dvar_get_name(dvar).data()); +#endif + return false; + } + } + + // pass all the flag checks, allow dvar to be changed + return true; + } + + void* get_dvar_flag_checks_stub() + { + return utils::hook::assemble([](utils::hook::assembler& a) + { + const auto can_set_value = a.newLabel(); + const auto zero_source = a.newLabel(); + + a.pushad64(); + a.mov(r8, rdi); + a.mov(edx, esi); + a.mov(rcx, rbx); + a.call_aligned(apply_sv_cheats); // check if we are setting sv_cheats + a.popad64(); + a.cmp(esi, 0); + a.jz(zero_source); // if the SetSource is 0 (INTERNAL) ignore flag checks + + a.pushad64(); + a.mov(edx, esi); // source + a.mov(rcx, rbx); // dvar + a.call_aligned(dvar_flag_checks); // protect read/write/cheat/replicated dvars + a.cmp(al, 1); + a.jz(can_set_value); + + // if we get here, we are non-zero source and CANNOT set values + a.popad64(); // if I do this before the jz it won't work. for some reason the popad64 is affecting the ZR flag + a.jmp(0xCEDBDF_b); + + // if we get here, we are non-zero source and CAN set values + a.bind(can_set_value); + a.popad64(); // if I do this before the jz it won't work. for some reason the popad64 is affecting the ZR flag + a.cmp(esi, 1); + a.jmp(0xCED8EE_b); + + // if we get here, we are zero source and ignore flags + a.bind(zero_source); + a.jmp(0xCED97A_b); + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::nop(0xCED8D4_b, 8); // let our stub handle zero-source sets + utils::hook::jump(0xCED8DF_b, get_dvar_flag_checks_stub(), true); // check extra dvar flags when setting values + + scheduler::once([] + { + game::Dvar_RegisterBool("sv_cheats", false, game::DvarFlags::DVAR_FLAG_REPLICATED, "Allow cheat commands and dvars on this server"); + }, scheduler::pipeline::main); + } + }; +} + +REGISTER_COMPONENT(dvar_cheats::component) \ No newline at end of file diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index f9aa1b4a..7cf7205c 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -37,9 +37,7 @@ namespace network const std::string_view data(message->data + offset, message->cursize - offset); -#ifdef DEBUG - console::info("[Network] Handling command %s\n", cmd_string.data()); -#endif + //console::debug("[Network] Handling command %s\n", cmd_string.data()); handler->second(*address, data); return true; diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 409a36f6..f9e3e0ab 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -283,9 +283,10 @@ namespace party int get_client_count() { auto count = 0; + const auto* svs_clients = *game::svs_clients; for (unsigned int i = 0; i < *game::svs_numclients; ++i) { - if (game::svs_clients[i].header.state >= 1) + if (svs_clients[i].header.state >= 1) { ++count; } @@ -297,9 +298,10 @@ namespace party int get_bot_count() { auto count = 0; + const auto* svs_clients = *game::svs_clients; for (unsigned int i = 0; i < *game::svs_numclients; ++i) { - if (game::svs_clients[i].header.state >= 1 && + if (svs_clients[i].header.state >= 1 && game::SV_BotIsBot(i)) { ++count; diff --git a/src/client/component/rcon.cpp b/src/client/component/rcon.cpp new file mode 100644 index 00000000..d44e33d6 --- /dev/null +++ b/src/client/component/rcon.cpp @@ -0,0 +1,225 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "command.hpp" +#include "console/console.hpp" +#include "network.hpp" +#include "scheduler.hpp" +#include "rcon.hpp" + +#include +#include + +namespace rcon +{ + namespace + { + std::atomic_bool is_redirecting_{ false }; + std::atomic_bool has_redirected_{ false }; + game::netadr_s redirect_target_ = {}; + std::recursive_mutex redirect_lock; + + void setup_redirect(const game::netadr_s& target) + { + std::lock_guard $(redirect_lock); + + has_redirected_ = false; + is_redirecting_ = true; + redirect_target_ = target; + } + + void clear_redirect() + { + std::lock_guard $(redirect_lock); + + has_redirected_ = false; + is_redirecting_ = false; + redirect_target_ = {}; + } + + void send_rcon_command(const std::string& password, const std::string& data) + { + // If you are the server, don't bother with rcon and just execute the command + if (game::Dvar_FindVar("sv_running")->current.enabled) + { + game::Cbuf_AddText(0, data.data()); + return; + } + + if (password.empty()) + { + console::info("You must login first to use RCON\n"); + return; + } + + if (*game::cl_con_data && game::clientUIActives[0].connectionState >= game::CA_CONNECTED) + { + const auto target = (*game::cl_con_data)->address; + const auto buffer = password + " " + data; + network::send(target, "rcon", buffer); + } + else + { + console::warn("You need to be connected to a server!\n"); + } + } + + std::string build_status_buffer() + { + const auto mapname = game::Dvar_FindVar("mapname"); + + std::string buffer{}; + buffer.append(utils::string::va("map: %s\n", mapname->current.string)); + buffer.append( + "num score bot ping guid name address qport\n"); + buffer.append( + "--- ----- --- ---- -------------------------------- ---------------- --------------------- -----\n"); + + const auto svs_clients = *game::svs_clients; + if (svs_clients == nullptr) + { + return buffer; + } + + for (auto i = 0u; i < *game::svs_numclients; i++) + { + const auto client = &svs_clients[i]; + + if (client->header.state >= 1 && client->gentity && client->gentity->client) + { + char clean_name[32] = { 0 }; + strncpy_s(clean_name, client->gentity->client->name, sizeof(clean_name)); + game::I_CleanStr(clean_name); + + buffer.append(utils::string::va("%3i %5i %3s %s %32s %16s %21s %5i\n", + i, + game::G_MainMP_GetClientScore(i), + game::SV_BotIsBot(i) ? "Yes" : "No", + (client->header.state == game::CS_RECONNECTING) + ? "CNCT" + : (client->header.state == game::CS_ZOMBIE) + ? "ZMBI" + : utils::string::va("%4i", game::SV_ClientMP_GetClientPing(i)), + game::SV_GameMP_GetGuid(i), + clean_name, + network::net_adr_to_string(client->remoteAddress), + client->remoteAddress.port) + ); + } + } + + return buffer; + } + } + + bool message_redirect(const std::string& message) + { + std::lock_guard $(redirect_lock); + + if (is_redirecting_) + { + has_redirected_ = true; + network::send(redirect_target_, "print", message, '\n'); + return true; + } + return false; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + scheduler::once([]() + { + game::Dvar_RegisterString("rcon_password", "", game::DvarFlags::DVAR_FLAG_NONE, "The password for remote console"); + }, scheduler::pipeline::main); + + command::add("status", []() + { + const auto sv_running = game::Dvar_FindVar("sv_running"); + if (game::Com_FrontEnd_IsInFrontEnd() || !sv_running || !sv_running->current.enabled) + { + console::error("Server is not running\n"); + return; + } + + auto status_buffer = build_status_buffer(); + console::info(status_buffer.data()); + }); + + if (!game::environment::is_dedi()) + { + command::add("rcon", [&](const command::params& params) + { + static std::string rcon_password{}; + + if (params.size() < 2) return; + + const auto operation = params.get(1); + if (operation == "login"s) + { + if (params.size() < 3) return; + + rcon_password = params.get(2); + } + else if (operation == "logout"s) + { + rcon_password.clear(); + } + else + { + send_rcon_command(rcon_password, params.join(1)); + } + }); + } + else + { + network::on("rcon", [](const game::netadr_s& addr, const std::string_view& data) + { + const auto message = std::string{ data }; + const auto pos = message.find_first_of(" "); + if (pos == std::string::npos) + { + network::send(addr, "print", "Invalid RCon request", '\n'); + console::info("Invalid RCon request from %s\n", network::net_adr_to_string(addr)); + return; + } + + const auto password = message.substr(0, pos); + const auto command = message.substr(pos + 1); + const auto rcon_password = game::Dvar_FindVar("rcon_password"); + if (command.empty() || !rcon_password || !rcon_password->current.string || !strlen( + rcon_password->current.string)) + { + return; + } + + setup_redirect(addr); + + if (password != rcon_password->current.string) + { + network::send(redirect_target_, "print", "Invalid rcon password", '\n'); + console::error("Invalid rcon password\n"); + } + else + { + command::execute(command, true); + } + + if (!has_redirected_) + { + network::send(redirect_target_, "print", "", '\n'); + } + + clear_redirect(); + }); + } + } + }; +} + +REGISTER_COMPONENT(rcon::component) \ No newline at end of file diff --git a/src/client/component/rcon.hpp b/src/client/component/rcon.hpp new file mode 100644 index 00000000..f70debab --- /dev/null +++ b/src/client/component/rcon.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace rcon +{ + bool message_redirect(const std::string& message); +} \ No newline at end of file diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 3c02407d..f3e04e81 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -508,6 +508,13 @@ namespace game } using namespace ddl; + // made up + struct connection_data + { + char __pad0[131112]; + netadr_s address; + }; + namespace entity { enum connstate_t : std::uint32_t @@ -572,24 +579,40 @@ namespace game static_assert(offsetof(gentity_s, client) == 368); static_assert(offsetof(gentity_s, flags) == 456); + enum SvClientConnectionState + { + CS_FREE = 0x0, + CS_ZOMBIE = 0x1, + CS_RECONNECTING = 0x2, + CS_CONNECTED = 0x3, + CS_CLIENTLOADING = 0x4, + CS_ACTIVE = 0x5, + }; + struct clientHeader_t { - char __pad0[8]; + void* unk; // 0 int state; // 8 }; // sizeof = ? struct client_t { clientHeader_t header; // 0 - char __pad0[124]; + char __pad0[120]; gentity_s* gentity; // 136 - char __pad1[1044]; + char __pad1[20]; + char userinfo[1024]; char name[32]; // 1188 - char __pad2[714196]; + char __pad2[648396]; + netadr_s remoteAddress; // 649616 + char __pad3[65780]; }; static_assert(sizeof(client_t) == 715416); - + + static_assert(offsetof(client_t, header.state) == 8); static_assert(offsetof(client_t, gentity) == 136); + static_assert(offsetof(client_t, userinfo) == 164); static_assert(offsetof(client_t, name) == 1188); + static_assert(offsetof(client_t, remoteAddress) == 649616); } using namespace entity; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 1f1bdbed..a5414c48 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -95,6 +95,7 @@ namespace game WEAK symbol Dvar_DisplayableLatchedValue{ 0xCEA1D0 }; WEAK symbol Dvar_GetCombinedString{ 0xBB1F30 }; WEAK symbol Dvar_ValueToString{ 0xCEED00 }; + WEAK symbol Dvar_Reset{ 0xCEC490 }; WEAK symbol Dvar_GenerateChecksum{ 0xCEA520 }; WEAK symbol Dvar_SetInt{ 0xCED3D0 }; @@ -102,6 +103,8 @@ namespace game WEAK symbol FS_FreeFile{ 0xCDE1F0 }; WEAK symbol FS_Printf{ 0xCDD1C0 }; + WEAK symbol G_MainMP_GetClientScore{ 0xB20550 }; + WEAK symbol I_CleanStr{ 0xCFACC0 }; WEAK symbol Key_KeynumToString{ 0x9A95E0 }; @@ -165,7 +168,11 @@ namespace game WEAK symbol SV_CmdsMP_CheckLoadGame{ 0xC4C9E0 }; WEAK symbol SV_CmdsSP_MapRestart_f{ 0xC12B30 }; WEAK symbol SV_CmdsSP_FastRestart_f{ 0xC12AF0 }; + WEAK symbol SV_ClientMP_GetClientPing{ 0xC507D0 }; + WEAK symbol SV_GameMP_GetGuid{ 0XC12410 }; + WEAK symbol SV_MainMP_KillLocalServer{ 0xC58DF0 }; WEAK symbol SV_GameSendServerCommand{ 0xC54780 }; + WEAK symbol SV_DropClient{ 0xC4FBA0 }; WEAK symbol SV_Loaded{ 0xC114C0 }; WEAK symbol SV_MapExists{ 0xCDB620 }; WEAK symbol SV_BotIsBot{ 0xC3BC90 }; @@ -193,10 +200,12 @@ namespace game WEAK symbol g_entities{ 0x3D22610 }; WEAK symbol svs_numclients{ 0x6B229E0 }; - WEAK symbol svs_clients{ 0x6B22950 }; + WEAK symbol svs_clients{ 0x6B22950 }; WEAK symbol clientUIActives{ 0x2246C30 }; + WEAK symbol cl_con_data{ 0x1FE58B8 }; + WEAK symbol sv_map_restart{ 0x6B2C9D4 }; WEAK symbol sv_loadScripts{ 0x6B2C9D8 }; WEAK symbol sv_migrate{ 0x6B2C9DC };