From 821e4a9e768ec87de6c64de1fd12606dece21576 Mon Sep 17 00:00:00 2001 From: jakub-cfx <101104461+jakub-cfx@users.noreply.github.com> Date: Wed, 25 Oct 2023 01:02:43 +0200 Subject: [PATCH] tweak(server): scriptless startup notices --- .../citizen-server-impl/component.lua | 2 +- .../include/NoticeLogicProcessor.h | 60 ++++++ .../src/NoticeLogicProcessor.cpp | 158 +++++++++++++++ .../citizen-server-impl/src/ServerNucleus.cpp | 182 ++---------------- .../http-client/include/HttpClient.h | 1 + .../components/http-client/src/HttpClient.cpp | 7 + 6 files changed, 239 insertions(+), 171 deletions(-) create mode 100644 code/components/citizen-server-impl/include/NoticeLogicProcessor.h create mode 100644 code/components/citizen-server-impl/src/NoticeLogicProcessor.cpp diff --git a/code/components/citizen-server-impl/component.lua b/code/components/citizen-server-impl/component.lua index 4bd9c9a2e6..a357ada061 100644 --- a/code/components/citizen-server-impl/component.lua +++ b/code/components/citizen-server-impl/component.lua @@ -11,7 +11,7 @@ end return function() filter {} - add_dependencies { 'vendor:folly', 'vendor:lua' } + add_dependencies { 'vendor:folly' } removefiles { 'components/citizen-server-impl/src/state/**' diff --git a/code/components/citizen-server-impl/include/NoticeLogicProcessor.h b/code/components/citizen-server-impl/include/NoticeLogicProcessor.h new file mode 100644 index 0000000000..7b1a50b711 --- /dev/null +++ b/code/components/citizen-server-impl/include/NoticeLogicProcessor.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#include + +namespace fx +{ +class NoticeLogicProcessor +{ +private: + enum RuleOp + { + And, + Or, + Not, + NullOrEmpty, + Contains, + Equals + }; + + enum RuleType + { + ConVar, + StartedResourceList + }; + +public: + NoticeLogicProcessor(fx::ServerInstanceBase* server); + bool ProcessNoticeRule(const nlohmann::json& ruleRef, uint32_t nestingLevel) const; + + static void BeginProcessingNotices(fx::ServerInstanceBase* server, const nlohmann::json& noticesBlob); + +private: + ConsoleVariableManager* m_cvManager; + fx::ResourceManager* m_resManager; + + // Macro'd map entries to prevent accidental typos or bad mapping +#define StrEnumMapEntry(EnumName, ValueName) \ + { \ + #ValueName, EnumName::ValueName \ + } + + const std::map m_ruleOpStringToEnum{ + StrEnumMapEntry(RuleOp, And), + StrEnumMapEntry(RuleOp, Or), + StrEnumMapEntry(RuleOp, Not), + StrEnumMapEntry(RuleOp, NullOrEmpty), + StrEnumMapEntry(RuleOp, Contains), + StrEnumMapEntry(RuleOp, Equals) + }; + + const std::map m_ruleTypeStringToEnum{ + StrEnumMapEntry(RuleType, ConVar), + StrEnumMapEntry(RuleType, StartedResourceList), + }; +}; +} diff --git a/code/components/citizen-server-impl/src/NoticeLogicProcessor.cpp b/code/components/citizen-server-impl/src/NoticeLogicProcessor.cpp new file mode 100644 index 0000000000..a88bef21cb --- /dev/null +++ b/code/components/citizen-server-impl/src/NoticeLogicProcessor.cpp @@ -0,0 +1,158 @@ +#include +#include "NoticeLogicProcessor.h" + +fx::NoticeLogicProcessor::NoticeLogicProcessor(fx::ServerInstanceBase* server) +{ + m_cvManager = server->GetComponent()->GetVariableManager(); + m_resManager = server->GetComponent()->GetCurrent(); +} + +bool fx::NoticeLogicProcessor::ProcessNoticeRule(const nlohmann::json& ruleRef, uint32_t nestingLevel) const +{ + if (nestingLevel >= 10) + { + throw std::invalid_argument("Maximum nesting level for notice rules was exceeded"); + } + if (!ruleRef.is_object()) + { + return false; + } + auto ruleObject = ruleRef.get(); + + auto& opObject = ruleObject["op"]; + if (!opObject.is_string()) + { + return false; + } + + auto mappedOp = m_ruleOpStringToEnum.find(opObject.get()); + if (mappedOp == m_ruleOpStringToEnum.end()) + { + return false; + } + + auto opNum = mappedOp->second; + switch (opNum) + { + case RuleOp::And: + case RuleOp::Or: + { + auto& nestedRules = ruleObject["rules"]; + if (!nestedRules.is_array()) + { + return false; + } + + for (auto& subRule : nestedRules) + { + auto isSubRuleTrue = ProcessNoticeRule(subRule, nestingLevel + 1); + if (opNum == RuleOp::And) + { + if (!isSubRuleTrue) + { + return false; + } + } + else // ::Or + { + if (isSubRuleTrue) + { + return true; + } + } + } + + // Final catch when all ::Ands were true or all ::Ors were false + return opNum == RuleOp::And ? true : false; + } + case RuleOp::Not: + { + auto& ruleToInvert = ruleObject["rule"]; + return ruleToInvert.is_object() ? !ProcessNoticeRule(ruleToInvert, nestingLevel + 1) : false; + } + case RuleOp::NullOrEmpty: + case RuleOp::Contains: + case RuleOp::Equals: + { + auto& typeObject = ruleObject["type"]; + if (!typeObject.is_string()) + { + return false; + } + + auto mappedType = m_ruleTypeStringToEnum.find(typeObject.get()); + if (mappedType == m_ruleTypeStringToEnum.end()) + { + return false; + } + + auto typeNum = mappedType->second; + if (typeNum == RuleType::ConVar) + { + auto& keyObject = ruleObject["key"]; + if (!keyObject.is_string()) + { + return false; + } + + auto cvEntry = m_cvManager->FindEntryRaw(keyObject.get()); + + if (opNum == RuleOp::NullOrEmpty) + return !cvEntry || cvEntry->GetValue() == ""; + + auto& dataObject = ruleObject["data"]; + if (!dataObject.is_string()) + { + return false; + } + + switch (opNum) + { + case RuleOp::Contains: + return cvEntry->GetValue().find(dataObject.get()) != std::string::npos; + case RuleOp::Equals: + return cvEntry->GetValue() == dataObject.get(); + } + } + else if (typeNum == RuleType::StartedResourceList && opNum == RuleOp::Contains) + { + auto& dataObject = ruleObject["data"]; + if (!dataObject.is_string()) + { + return false; + } + + auto resource = m_resManager->GetResource(dataObject.get(), false); + return (resource.GetRef() && resource->GetState() == fx::ResourceState::Started); + } + return false; + } + } + return false; +} + +void fx::NoticeLogicProcessor::BeginProcessingNotices(fx::ServerInstanceBase* server, const nlohmann::json& noticesBlob) +{ + auto nlp = fx::NoticeLogicProcessor::NoticeLogicProcessor(server); + + for (auto& [noticeType, data] : noticesBlob.get()) + { + auto& enabled = data["enabled"]; + if (!enabled.get()) + continue; + + auto& rootRule = data["rule"]; + auto ruleIsTrue = nlp.ProcessNoticeRule(rootRule, 0); + + if (ruleIsTrue) + { + auto& lines = data["notice_lines"]; + if (lines.is_array()) + { + trace("^1-- [server notice: %s]^7\n", noticeType); + for (auto& noticeLine : lines) + trace("%s\n", noticeLine.get()); + } + } + } +} diff --git a/code/components/citizen-server-impl/src/ServerNucleus.cpp b/code/components/citizen-server-impl/src/ServerNucleus.cpp index 7e688b8988..6501af1078 100644 --- a/code/components/citizen-server-impl/src/ServerNucleus.cpp +++ b/code/components/citizen-server-impl/src/ServerNucleus.cpp @@ -18,182 +18,24 @@ #include -#include -#include -#include +#include -using json = nlohmann::json; +// 100KiB cap, conditional notices should fit more than comfortably under this limit +#define MAX_NOTICE_FILESIZE 102400 -struct Lua +static void DownloadAndProcessNotices(fx::ServerInstanceBase* server, HttpClient* httpClient) { - Lua() - : L(nullptr) + HttpRequestOptions options; + options.maxFilesize = MAX_NOTICE_FILESIZE; + httpClient->DoGetRequest("https://runtime.fivem.net/promotions_targeting.json", options, [server, httpClient](bool success, const char* data, size_t length) { - static const luaL_Reg lualibs[] = { - { "_G", luaopen_base }, - { LUA_TABLIBNAME, luaopen_table }, - { LUA_STRLIBNAME, luaopen_string }, - { LUA_MATHLIBNAME, luaopen_math }, - { LUA_UTF8LIBNAME, luaopen_utf8 }, - { "json", luaopen_rapidjson }, - { NULL, NULL } - }; - - L = luaL_newstate(); - - const luaL_Reg* lib = lualibs; - for (; lib->func; lib++) + // Double checking received size because CURL will let bigger files through if the server doesn't specify Content-Length outright + if (success && length <= MAX_NOTICE_FILESIZE) { - luaL_requiref(L, lib->name, lib->func, 1); - lua_pop(L, 1); - } - } - - ~Lua() - { - if (L) - { - lua_close(L); - L = nullptr; - } - } - - std::optional EvaluateExpression(const std::string& expr, const json& context) - { - if (luaL_loadbufferx(L, expr.c_str(), expr.size(), "@expr", "t") != LUA_OK) - { - return {}; - } - - // stack: [expr chunk] - - lua_getglobal(L, "json"); - lua_getfield(L, -1, "decode"); - lua_remove(L, -2); - - // stack: [expr chunk], [json.decode] - - auto j = context.dump(-1, ' ', false, nlohmann::detail::error_handler_t::ignore); - lua_pushlstring(L, j.c_str(), j.size()); - if (lua_pcall(L, 1, 1, 0) != LUA_OK) - { - return {}; - } - - // stack: [expr chunk], [json table] - - // store _G in new env - lua_pushglobaltable(L); - lua_setfield(L, -2, "_G"); - - // set _ENV to the JSON chunk - lua_setupvalue(L, -2, 1); - - // stack: [expr chunk, with json table as env] - if (lua_pcall(L, 0, 1, 0) != LUA_OK) - { - const char* e = lua_tostring(L, -1); - return {}; - } - - // stack: retval - auto rv = lua_toboolean(L, -1); - lua_pop(L, 1); - - return rv; - } - -private: - lua_State* L; -}; - -static std::optional EvaluateLua(const std::string& in, const json& context) -{ - Lua l; - return l.EvaluateExpression(in, context); -} - -static void DisplayNotices(fx::ServerInstanceBase* server, HttpClient* httpClient) -{ - httpClient->DoGetRequest("https://runtime.fivem.net/promotions_targeting.json", [server](bool success, const char* data, size_t length) - { - if (success) - { - json convarList = json::object(); - json resourceList = json::array(); - - auto conCtx = server->GetComponent(); - conCtx->GetVariableManager()->ForAllVariables([&convarList](const std::string& name, int flags, const std::shared_ptr& variable) - { - convarList[name] = variable->GetValue(); - }); - - auto resman = server->GetComponent(); - resman->ForAllResources([&resourceList](const fwRefContainer& resource) - { - if (resource->GetState() == fx::ResourceState::Started) - { - resourceList.push_back(json::object({ { "name", resource->GetName() } })); - } - }); - - json contextBlob = json::object(); - contextBlob["convar"] = convarList; - contextBlob["resource"] = resourceList; - try { - json noticeBlob = json::parse(data, data + length); - - for (auto& [ noticeType, data ] : noticeBlob.get()) - { - auto& conditions = data["conditions_lua"]; - auto& actions = data["actions"]; - - if (conditions.is_array()) - { - for (auto& condition : conditions) - { - auto cond = condition.get(); - if (cond.find("--[[]]") != 0) - { - cond = "return " + cond; - } - - auto rv = EvaluateLua(cond, contextBlob); - - if (rv && *rv) - { - // evaluate actions - if (actions.is_array()) - { - auto noticeTypeStr = noticeType; - - gscomms_execute_callback_on_main_thread([actions, conCtx, noticeTypeStr]() - { - trace("^1-- [server notice: %s]^7\n", noticeTypeStr); - - se::ScopedPrincipal principalScope(se::Principal{ "system.console" }); - - try - { - for (auto& action : actions) - { - conCtx->ExecuteSingleCommand(action.get()); - } - } - catch (std::exception& e) - { - - } - - trace("\n"); - }); - } - } - } - } - } + auto noticesBlob = nlohmann::json::parse(data, data + length); + fx::NoticeLogicProcessor::BeginProcessingNotices(server, noticesBlob); } catch (std::exception& e) { @@ -286,7 +128,7 @@ static InitFunction initFunction([]() setNucleusSuccess = true; } - DisplayNotices(instance, httpClient); + DownloadAndProcessNotices(instance, httpClient); }); } diff --git a/code/components/http-client/include/HttpClient.h b/code/components/http-client/include/HttpClient.h index d3142c3370..0223e156fd 100644 --- a/code/components/http-client/include/HttpClient.h +++ b/code/components/http-client/include/HttpClient.h @@ -55,6 +55,7 @@ struct HttpRequestOptions std::function streamingCallback; std::chrono::milliseconds timeoutNoResponse{ 0 }; int weight = 16; + uint64_t maxFilesize = 0; bool ipv4 = false; bool addErrorBody = false; diff --git a/code/components/http-client/src/HttpClient.cpp b/code/components/http-client/src/HttpClient.cpp index ef43d98779..4043fa83ea 100644 --- a/code/components/http-client/src/HttpClient.cpp +++ b/code/components/http-client/src/HttpClient.cpp @@ -83,6 +83,7 @@ class CurlData final std::shared_ptr responseCode; std::chrono::milliseconds timeoutNoResponse; std::chrono::high_resolution_clock::duration reqStart; + uint64_t maxFilesize; std::stringstream errorBody; std::stringstream rawBody; @@ -526,6 +527,7 @@ static std::shared_ptr SetupCURLHandle(const std::unique_ptrtimeoutNoResponse = options.timeoutNoResponse; curlData->addErrorBody = options.addErrorBody; curlData->addRawBody = options.addRawBody; + curlData->maxFilesize = options.maxFilesize; auto scb = options.streamingCallback; @@ -577,6 +579,11 @@ static std::shared_ptr SetupCURLHandle(const std::unique_ptr 0) + { + curl_easy_setopt(curlHandle, CURLOPT_MAXFILESIZE_LARGE, options.maxFilesize); + } + impl->client->OnSetupCurlHandle(curlHandle, url); return curlData;