Add identity-safe player moderation runtimes for T5 and T6
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
#include <stdinc.hpp>
|
||||
#include "loader/component_loader.hpp"
|
||||
#include "gsc.hpp"
|
||||
#include "scripting.hpp"
|
||||
#include "scheduler.hpp"
|
||||
#include "command.hpp"
|
||||
#include "game/game.hpp"
|
||||
#include "game/scripting/array.hpp"
|
||||
#include <utils/moderation.hpp>
|
||||
#include <utils/http.hpp>
|
||||
#include <utils/cryptography.hpp>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
namespace moderation
|
||||
{
|
||||
namespace
|
||||
{
|
||||
using runtime = utils::moderation::runtime;
|
||||
struct transfer
|
||||
{
|
||||
std::string session, response;
|
||||
std::chrono::steady_clock::time_point started = std::chrono::steady_clock::now();
|
||||
std::atomic<bool> done{false};
|
||||
bool success = false;
|
||||
};
|
||||
std::shared_ptr<runtime> current;
|
||||
std::shared_ptr<transfer> pending;
|
||||
std::string endpoint, credential, configured_server;
|
||||
int configured_generation = 0;
|
||||
std::chrono::steady_clock::time_point next_poll{}, next_log{};
|
||||
bool fault = false;
|
||||
long long now() { return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count(); }
|
||||
void failure()
|
||||
{
|
||||
if (std::chrono::steady_clock::now() >= next_log)
|
||||
{
|
||||
printf("[tws_moderation] Runtime unavailable: verify credentials, policy, clock and persistent storage.\n");
|
||||
next_log = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
}
|
||||
}
|
||||
void disconnected(unsigned int slot) { if (current) current->disconnect(static_cast<int>(slot)); }
|
||||
std::string execute(const std::string& kind, int slot, const std::string& reason)
|
||||
{
|
||||
auto active = current;
|
||||
if (!active) return "failed";
|
||||
// This function is called only by the server-thread GSC pump. No queue/yield
|
||||
// separates runtime target validation from synchronous engine dispatch.
|
||||
if (kind == "announce" || kind == "warn" || kind == "message")
|
||||
{
|
||||
const auto text = std::string("j \"") + reason + "\"";
|
||||
game::SV_GameSendServerCommand(slot, 0, text.c_str());
|
||||
return "confirmed";
|
||||
}
|
||||
if (slot < 0 || slot >= 64) return "failed";
|
||||
if (kind == "mute" || kind == "unmute")
|
||||
{
|
||||
command::execute((kind == "mute" ? "muteclient " : "unmuteclient ") + std::to_string(slot), true);
|
||||
return "unknown"; // Engine has no mute-state readback in this adapter.
|
||||
}
|
||||
if (kind != "kick") return "failed";
|
||||
const auto connection = active->players.contains(slot) ? active->players.at(slot).connection : std::string();
|
||||
command::execute("clientkick " + std::to_string(slot) + " \"" + reason + "\"", true);
|
||||
return !active->players.contains(slot) || active->players.at(slot).connection != connection ? "confirmed" : "unknown";
|
||||
}
|
||||
void pump()
|
||||
{
|
||||
if (!current || fault) return;
|
||||
try
|
||||
{
|
||||
if (pending && pending->done.load())
|
||||
{
|
||||
auto completed = pending; pending.reset();
|
||||
if (completed->session == current->session)
|
||||
{
|
||||
if (completed->success)
|
||||
{
|
||||
const auto age = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - completed->started).count();
|
||||
current->accept(completed->response, now(), age);
|
||||
}
|
||||
else failure();
|
||||
}
|
||||
}
|
||||
auto active = current;
|
||||
active->enforce(now(), [active](const auto& kind, int slot, const auto& reason)
|
||||
{
|
||||
return current == active ? execute(kind, slot, reason) : std::string("failed");
|
||||
});
|
||||
if (current != active) return;
|
||||
if (!pending && std::chrono::steady_clock::now() >= next_poll)
|
||||
{
|
||||
auto request = current->request().dump(); auto target = endpoint; auto token = credential;
|
||||
pending = std::make_shared<transfer>(); pending->session = current->session; auto work = pending;
|
||||
next_poll = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||
scheduler::thread_pool.push([work, request, target, token]
|
||||
{
|
||||
try
|
||||
{
|
||||
utils::http::timeouts limits; limits.connect_timeout_ms = 3000; limits.timeout_ms = 5000;
|
||||
limits.max_response_bytes = 1024 * 1024;
|
||||
const auto response = utils::http::get_data(target, request,
|
||||
{{"Authorization", "Bearer " + token}, {"Content-Type", "application/json"}}, "POST", limits);
|
||||
if (response && response->code == CURLE_OK && response->status == 200)
|
||||
{ work->response = response->buffer; work->success = true; }
|
||||
}
|
||||
catch (...) { work->success = false; }
|
||||
work->done.store(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (...) { failure(); }
|
||||
}
|
||||
}
|
||||
class component final : public component_interface
|
||||
{
|
||||
public:
|
||||
void on_startup(plugin::plugin* plugin) override
|
||||
{
|
||||
plugin->get_interface()->callbacks()->on_player_connect(disconnected);
|
||||
plugin->get_interface()->callbacks()->on_player_disconnect(disconnected);
|
||||
scripting::on_shutdown([]
|
||||
{
|
||||
// In-flight requests own only native data. Results from the old VM are abandoned.
|
||||
current.reset(); pending.reset(); credential.clear(); endpoint.clear(); fault = false;
|
||||
});
|
||||
gsc::function::add("moderationversion", [] { return std::string("1.0.0"); });
|
||||
gsc::function::add("moderationconfigure", [](const std::string& server, const std::string& url, const std::string& token, int generation)
|
||||
{
|
||||
if (current && !fault && endpoint == url && credential == token && configured_server == server && configured_generation == generation) return true;
|
||||
// Credentials cannot leave the explicitly configured HTTPS origin. Curl does not follow redirects.
|
||||
if (url.size() > 2048 || url.rfind("https://", 0) != 0 || url.find_first_of("\r\n?#@") != std::string::npos ||
|
||||
url.size() < 8 || url.find("/api/moderation/runtime") != url.size() - 23 ||
|
||||
token.rfind("ec_moderation_", 0) != 0 || token.size() < 40 || token.size() > 256 ||
|
||||
token.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-") != std::string::npos) return false;
|
||||
try
|
||||
{
|
||||
auto configured = std::make_shared<runtime>("tws-moderation/t6", server, url, generation,
|
||||
utils::cryptography::sha256::compute(token, true));
|
||||
current = std::move(configured); pending.reset(); configured_server = server; configured_generation = generation;
|
||||
endpoint = url; credential = token; next_poll = {}; fault = false; return true;
|
||||
}
|
||||
catch (...) { fault = true; failure(); return false; }
|
||||
});
|
||||
gsc::method::add("moderationobserve", [](const scripting::entity& entity, const std::string& guid, const std::string& name)
|
||||
{
|
||||
if (!current || fault) return false;
|
||||
const auto ref = entity.get_entity_reference();
|
||||
if (ref.classnum != 0 || ref.entnum >= 64) return false;
|
||||
try { current->observe(ref.entnum, guid, name); return true; }
|
||||
catch (...) { failure(); return false; }
|
||||
});
|
||||
gsc::function::add("moderationstop", []
|
||||
{
|
||||
current.reset(); pending.reset(); credential.clear(); endpoint.clear(); fault = false;
|
||||
});
|
||||
gsc::function::add("moderationpump", pump);
|
||||
}
|
||||
};
|
||||
}
|
||||
REGISTER_COMPONENT(moderation::component)
|
||||
Reference in New Issue
Block a user