diff --git a/CMakeLists.txt b/CMakeLists.txt index b8f371a8..62021927 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,8 +16,9 @@ find_package(Threads REQUIRED) find_package(Angelscript) find_package(jsoncpp REQUIRED) find_package(SocketW REQUIRED) - +find_package(CURL) cmake_dependent_option(RORSERVER_WITH_ANGELSCRIPT "Adds scripting support" ON "TARGET Angelscript::angelscript" OFF) +cmake_dependent_option(RORSERVER_WITH_CURL "Adds CURL request support (needs AngelScript)" ON "TARGET CURL:libcurl" OFF) # setup paths SET(RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/") diff --git a/conanfile.py b/conanfile.py index f3f18fd8..2865c957 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,4 +15,5 @@ def requirements(self): self.requires("angelscript/2.37.0") self.requires("jsoncpp/1.9.5") self.requires("openssl/3.3.2", override=True) - self.requires("socketw/3.11.0@anotherfoxguy/stable") \ No newline at end of file + self.requires("socketw/3.11.0@anotherfoxguy/stable") + self.requires("libcurl/8.10.1") \ No newline at end of file diff --git a/contrib/example-script.as b/contrib/example-script.as index 26d5a820..500375c0 100644 --- a/contrib/example-script.as +++ b/contrib/example-script.as @@ -16,6 +16,7 @@ have a single handler function (`setCallback()` replaces the previous). int streamAdded(int uid, StreamRegister@ reg) ~ executed when player spawns an actor. Returns `broadcastType` which determines how the message is treated. int playerChat(int uid, const string &in msg) ~ ONLY ONE AT A TIME ~ executed when player sends a chat message. Returns `broadcastType` which determines how the message is treated. void gameCmd(int uid, const string &in cmd) ~ ONLY ONE AT A TIME ~ invoked when a script running on client calls `game.sendGameCmd()` + void curlStatus(curlStatusType type, int n1, int n2, string displayname, string message) ~ Provides progress and result info, see `server.curlRequestAsync()`; for CURL_STATUS_PROGRESS, n1 = bytes downloaded, n2 = total bytes; otherwise n1 = CURL return code, n2 = HTTP result code. Constants and enumerations @@ -39,6 +40,15 @@ enum broadcastType // This is returned by the `playerChat()/streamAdded()` callb BROADCAST_BLOCK // no broadcast }; +enum curlStatusType // Used by `curlStatus()` callback. +{ + CURL_STATUS_INVALID, //!< Should never be reported. + CURL_STATUS_START, //!< New CURL request started, n1/n2 both 0. + CURL_STATUS_PROGRESS, //!< Download in progress, n1 = bytes downloaded, n2 = total bytes. + CURL_STATUS_SUCCESS, //!< CURL request finished, n1 = CURL return code, n2 = HTTP result code, message = received payload. + CURL_STATUS_FAILURE, //!< CURL request finished, n1 = CURL return code, n2 = HTTP result code, message = CURL error string. +}; + TO_ALL = -1 // constant for functions that receive an uid for sending something */ @@ -64,14 +74,20 @@ void main() server.Log("Example server script loaded!"); } +const float TIME_LOG_CHUNK = 5.f; float g_totalTime = 0.f; float g_timeSinceLastMsg = 0.f; +// For CURL progress, don't log each received byte, just +const float CURL_LOG_CHUNK = 0.1; // Log at least in 10% steps. +float g_prevCurlProgress = 0.f; +float g_lastCurlLoggedProgress = 0.f; + // Optional, executed periodically, the parameter is delta time (time since last execution) in milliseconds. void frameStep(float dt_millis) { // reset every 5 sec - if (g_timeSinceLastMsg >= 5.f) + if (g_timeSinceLastMsg >= TIME_LOG_CHUNK) { server.say("Example server script: frameStep(): total time is " + g_totalTime + " sec.", TO_ALL, FROM_SERVER); g_timeSinceLastMsg = 0.f; @@ -118,6 +134,38 @@ void gameCmd(int uid, const string &in cmd) { server.say("Example server script: gameCmd(): UID: " + uid + ", cmd: '" + cmd + "'.", TO_ALL, FROM_SERVER); } + +void curlStatus(curlStatusType type, int n1, int n2, string displayname, string message) +{ + switch (type) + { + case CURL_STATUS_START: + g_prevCurlProgress = 0.f; + g_lastCurlLoggedProgress = 0.f; + server.say("Example server script: curlStatus(): type: CURL_STATUS_START, displayname: '" + displayname + "'", TO_ALL, FROM_SERVER); + break; + + case CURL_STATUS_PROGRESS: + g_prevCurlProgress = float(n1)/float(n2); + if (g_prevCurlProgress - g_lastCurlLoggedProgress >= CURL_LOG_CHUNK) + { + server.say("Example server script: curlStatus(): type: CURL_STATUS_PROGRESS (" + (g_prevCurlProgress * 100) + "%)" + + ", n1(bytes dl): " + n1 + ", n2(bytes total): " + n2 + ", displayname: '" + displayname + "'", TO_ALL, FROM_SERVER); + g_lastCurlLoggedProgress = g_prevCurlProgress; + } + break; + + case CURL_STATUS_SUCCESS: + server.say("Example server script: curlStatus(): type: CURL_STATUS_SUCCESS" + + ", n1(curl result): " + n1 + ", n2(HTTP result): " + n2 + ", displayname: '" + displayname + "', message(payload): '" + message + "'", TO_ALL, FROM_SERVER); + break; + + case CURL_STATUS_FAILURE: + server.say("Example server script: curlStatus(): type: CURL_STATUS_FAILURE" + + ", n1(curl result): " + n1 + ", n2(HTTP result): " + n2 + ", displayname: '" + displayname + "', message(CURL error): '" + message + "'", TO_ALL, FROM_SERVER); + break; + } +} // ============================================================================ // Callback functions registered manually @@ -162,6 +210,10 @@ int myStreamRegisteredCallback(int uid, StreamRegister@ reg) int myChatMessageCallback(int uid, const string &in msg) { server.say("Example server script: myChatMessageCallback(): UID: " + uid + ", msg: '" + msg + "'.", TO_ALL, FROM_SERVER); + if (msg == "CURL test") + { + server.curlRequestAsync("https://www.rigsofrods.org", "rigsofrods.org"); + } return BROADCAST_NORMAL; } diff --git a/source/server/CMakeLists.txt b/source/server/CMakeLists.txt index 725487a4..f4e8348d 100644 --- a/source/server/CMakeLists.txt +++ b/source/server/CMakeLists.txt @@ -15,6 +15,11 @@ if (RORSERVER_WITH_ANGELSCRIPT) target_link_libraries(${PROJECT_NAME} PRIVATE Angelscript::angelscript angelscript_addons) endif () +if (RORSERVER_WITH_CURL) + target_compile_definitions(${PROJECT_NAME} PRIVATE WITH_CURL) + target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl) +endif () + target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads SocketW::SocketW jsoncpp_lib) IF (WIN32) diff --git a/source/server/CurlHelpers.cpp b/source/server/CurlHelpers.cpp new file mode 100644 index 00000000..10b0f4dd --- /dev/null +++ b/source/server/CurlHelpers.cpp @@ -0,0 +1,93 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2023 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#ifdef WITH_CURL + +#include "CurlHelpers.h" +#include "ScriptEngine.h" + +#include +#include + +#include + +static size_t CurlStringWriteFunc(void *ptr, size_t size, size_t nmemb, std::string* data) +{ + data->append((char*)ptr, size * nmemb); + return size * nmemb; +} + +static size_t CurlXferInfoFunc(void* ptr, curl_off_t filesize_B, curl_off_t downloaded_B, curl_off_t uploadsize_B, curl_off_t uploaded_B) +{ + CurlTaskContext* context = (CurlTaskContext*)ptr; + + context->ctc_script_engine->curlStatus( + CURL_STATUS_PROGRESS, (int)downloaded_B, (int)filesize_B, context->ctc_displayname, ""); + + // If you don't return 0, the transfer will be aborted - see the documentation + return 0; +} + +bool GetUrlAsString(const std::string& url, CURLcode& curl_result, long& response_code, std::string& response_payload) +{ + std::string response_header; + std::string user_agent = "Rigs of Rods Server"; + + CURL *curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlStringWriteFunc); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, CurlXferInfoFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (curl_result != CURLE_OK || response_code != 200) + { + return false; + } + + return true; +} + +bool CurlRequestThreadFunc(CurlTaskContext context) +{ + context.ctc_script_engine->curlStatus(CURL_STATUS_START, 0, 0, context.ctc_displayname, ""); + std::string data; + CURLcode curl_result = CURLE_OK; + long http_response = 0; + bool result = GetUrlAsString(context.ctc_url, /*out:*/curl_result, /*out:*/http_response, /*out:*/data); + CurlStatusType type = result ? CURL_STATUS_SUCCESS : CURL_STATUS_FAILURE; + context.ctc_script_engine->curlStatus(type, (int)curl_result, (int)http_response, context.ctc_displayname, curl_easy_strerror(curl_result)); + return result; +} + +#endif // WITH_CURL diff --git a/source/server/CurlHelpers.h b/source/server/CurlHelpers.h new file mode 100644 index 00000000..0a640975 --- /dev/null +++ b/source/server/CurlHelpers.h @@ -0,0 +1,57 @@ +/* +This file is part of "Rigs of Rods Server" (Relay mode) + +Copyright 2007 Pierre-Michel Ricordel +Copyright 2014+ Rigs of Rods Community + +"Rigs of Rods Server" is free software: you can redistribute it +and/or modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, either version 3 +of the License, or (at your option) any later version. + +"Rigs of Rods Server" is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; without even the implied +warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see . +*/ + + +#pragma once + +enum CurlStatusType +{ + CURL_STATUS_INVALID, //!< Should never be reported. + CURL_STATUS_START, //!< New CURL request started, n1/n2 both 0. + CURL_STATUS_PROGRESS, //!< Download in progress, n1 = bytes downloaded, n2 = total bytes. + CURL_STATUS_SUCCESS, //!< CURL request finished, n1 = CURL return code, n2 = HTTP result code, message = received payload. + CURL_STATUS_FAILURE, //!< CURL request finished, n1 = CURL return code, n2 = HTTP result code, message = CURL error string. +}; + +#ifdef WITH_CURL + +class ScriptEngine; + +#include +#include + +#include + + +struct CurlTaskContext +{ + std::string ctc_displayname; + std::string ctc_url; + ScriptEngine* ctc_script_engine; + // Status is reported via new server callback `curlStatus()` +}; + +bool GetUrlAsString(const std::string& url, CURLcode& curl_result, long& response_code, std::string& response_payload); + +bool CurlRequestThreadFunc(CurlTaskContext task); + + + +#endif // WITH_CURL diff --git a/source/server/ScriptEngine.cpp b/source/server/ScriptEngine.cpp index 79fb4463..c723784c 100644 --- a/source/server/ScriptEngine.cpp +++ b/source/server/ScriptEngine.cpp @@ -26,6 +26,7 @@ along with Foobar. If not, see . #include "sequencer.h" #include "config.h" #include "messaging.h" +#include "CurlHelpers.h" #include "scriptstdstring/scriptstdstring.h" // angelscript addon #include "scriptmath/scriptmath.h" // angelscript addon #include "scriptmath3d/scriptmath3d.h" // angelscript addon @@ -44,19 +45,8 @@ along with Foobar. If not, see . using namespace std; -// cross platform assert -#ifdef _WIN32 -#include "windows.h" // for Sleep() -extern "C" { -_CRTIMP void __cdecl _wassert(_In_z_ const wchar_t * _Message, _In_z_ const wchar_t *_File, _In_ unsigned _Line); -} -# define assert_net(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) ) -#else // _WIN32 - #include - # define assert_net(expr) assert(expr) -#endif // _WIN32 #ifdef __GNUC__ @@ -65,6 +55,9 @@ _CRTIMP void __cdecl _wassert(_In_z_ const wchar_t * _Message, _In_z_ const wcha #endif +#include +#include + // Stream_register_t wrapper std::string stream_register_get_name(RoRnet::StreamRegister *reg) { @@ -197,6 +190,9 @@ int ScriptEngine::loadScript(std::string scriptname) { func = mod->GetFunctionByDecl("void gameCmd(int, string)"); if (func) addCallback("gameCmd", func, NULL); + func = mod->GetFunctionByDecl("void curlStatus(curlStatusType, int, int, string, string)"); + if (func) addCallback("curlStatus", func, NULL); + // Create and configure our context context = engine->CreateContext(); //context->SetLineCallback(asMETHOD(ScriptEngine,LineCallback), this, asCALL_THISCALL); @@ -371,6 +367,10 @@ void ScriptEngine::init() { result = engine->RegisterObjectMethod("ServerScriptClass", "int cmd(int uid, string cmd)", asMETHOD(ServerScript, sendGameCommand), asCALL_THISCALL); assert_net(result >= 0); + result = engine->RegisterObjectMethod("ServerScriptClass", "void curlRequestAsync(string url, string displayname)", + asMETHOD(ServerScript, curlRequestAsync), asCALL_THISCALL); + + assert_net(result >= 0); result = engine->RegisterObjectMethod("ServerScriptClass", "int getNumClients()", asMETHOD(ServerScript, getNumClients), asCALL_THISCALL); assert_net(result >= 0); @@ -518,6 +518,7 @@ void ScriptEngine::init() { // Register authorizations result = engine->RegisterEnum("authType"); assert_net(result >= 0); + result = engine->RegisterEnumValue("authType", "AUTH_NONE", RoRnet::AUTH_NONE); assert_net(result >= 0); result = engine->RegisterEnumValue("authType", "AUTH_ADMIN", RoRnet::AUTH_ADMIN); @@ -545,6 +546,20 @@ void ScriptEngine::init() { result = engine->RegisterEnumValue("serverSayType", "FROM_RULES", FROM_RULES); assert_net(result >= 0); + // Register curl update type for `curlStatus` callback + result = engine->RegisterEnum("curlStatusType"); + assert_net(result >= 0); + result = engine->RegisterEnumValue("curlStatusType", "CURL_STATUS_INVALID", CURL_STATUS_INVALID); + assert_net(result >= 0); + result = engine->RegisterEnumValue("curlStatusType", "CURL_STATUS_START", CURL_STATUS_START); + assert_net(result >= 0); + result = engine->RegisterEnumValue("curlStatusType", "CURL_STATUS_PROGRESS", CURL_STATUS_PROGRESS); + assert_net(result >= 0); + result = engine->RegisterEnumValue("curlStatusType", "CURL_STATUS_SUCCESS", CURL_STATUS_SUCCESS); + assert_net(result >= 0); + result = engine->RegisterEnumValue("curlStatusType", "CURL_STATUS_FAILURE", CURL_STATUS_FAILURE); + assert_net(result >= 0); + // register constants result = engine->RegisterGlobalProperty("const int TO_ALL", (void *) &TO_ALL); assert_net(result >= 0); @@ -796,6 +811,44 @@ void ScriptEngine::gameCmd(int uid, const std::string &cmd) { return; } +void ScriptEngine::curlStatus(CurlStatusType type, int n1, int n2, string displayname, string message) +{ + // Params `n1` and `n2` depend on status type : + // - for CURL_STATUS_PROGRESS, n1 = bytes downloaded, n2 = total bytes, + // - otherwise, n1 = CURL return code, n2 = HTTP result code. + // ------------------------------------------------------------------- + + if (!engine) return; + if (!context) context = engine->CreateContext(); + int r; + + // Copy the callback list, because the callback list itself may get changed while executing the script + callbackList queue(callbacks["curlStatus"]); + + // loop over all callbacks + for (unsigned int i = 0; i < queue.size(); ++i) { + // prepare the call + r = context->Prepare(queue[i].func); + if (r < 0) continue; + + // Set the object if present (if we don't set it, then we call a global function) + if (queue[i].obj != NULL) { + context->SetObject(queue[i].obj); + if (r < 0) continue; + } + + // Set the arguments + context->SetArgDWord(0, (asDWORD)type); + context->SetArgDWord(1, (asDWORD)n1); + context->SetArgDWord(2, (asDWORD)n2); + context->SetArgObject(3, (void*)&displayname); + context->SetArgObject(4, (void*)&message); + + // Execute it + r = context->Execute(); + } +} + void ScriptEngine::TimerThreadMain() { while (this->GetTimerThreadState() == ThreadState::RUNNING) { // sleep 200 miliseconds @@ -867,6 +920,8 @@ void ScriptEngine::addCallbackScript(const std::string &type, const std::string funcDecl = "void " + _func + "(int, int)"; else if (type == "streamAdded") funcDecl = "int " + _func + "(int, StreamRegister@)"; + else if (type == "curlStatus") + funcDecl = "void " + _func + "(curlStatusType, int, int, string, string)"; else { setException("Type " + type + " does not exist! Possible type strings: 'frameStep', 'playerChat', 'gameCmd', 'playerAdded', 'playerDeleted', 'streamAdded'."); @@ -954,6 +1009,8 @@ void ScriptEngine::deleteCallbackScript(const std::string &type, const std::stri funcDecl = "void " + _func + "(int, int)"; else if (type == "streamAdded") funcDecl = "int " + _func + "(int, StreamRegister@)"; + else if (type == "curlStatus") + funcDecl = "void " + _func + "(curlStatusType, int, int, string, string)"; else { setException("Type " + type + " does not exist! Possible type strings: 'frameStep', 'playerChat', 'gameCmd', 'playerAdded', 'playerDeleted', 'streamAdded'."); @@ -1129,6 +1186,18 @@ int ServerScript::sendGameCommand(int uid, std::string cmd) { return seq->sendGameCommand(uid, cmd); } +void ServerScript::curlRequestAsync(std::string url, string displayname) { +#if WITH_CURL + CurlTaskContext context; + context.ctc_url = url; + context.ctc_displayname = displayname; + context.ctc_script_engine = this->mse; + + std::packaged_task pktask(CurlRequestThreadFunc); + std::thread(std::move(pktask), context).detach(); +#endif +} + int ServerScript::getNumClients() { return seq->getNumClients(); } diff --git a/source/server/ScriptEngine.h b/source/server/ScriptEngine.h index 8050b595..215b8e69 100644 --- a/source/server/ScriptEngine.h +++ b/source/server/ScriptEngine.h @@ -23,6 +23,7 @@ along with Foobar. If not, see . #ifdef WITH_ANGELSCRIPT #include "UnicodeStrings.h" +#include "CurlHelpers.h" #include #include #include @@ -61,6 +62,9 @@ class ScriptEngine { ~ScriptEngine(); + /// @name callbacks + /// @{ + int loadScript(std::string scriptName); void playerDeleted(int uid, int crash, bool doNestedCall = false); @@ -73,8 +77,17 @@ class ScriptEngine { void gameCmd(int uid, const std::string &cmd); + /** + * Params `n1` and `n2` depend on status type : + * - for CURL_STATUS_PROGRESS, n1 = bytes downloaded, n2 = total bytes, + * - otherwise, n1 = CURL return code, n2 = HTTP result code. + */ + void curlStatus(CurlStatusType type, int n1, int n2, std::string displayname, std::string message); + int frameStep(float dt); + /// @} + /** * Gets the currently used AngelScript script engine. * @return a pointer to the currently used AngelScript script engine @@ -283,6 +296,15 @@ class ServerScript { int rangeRandomInt(int from, int to); void broadcastUserInfo(int uid); + + /** + * Launches a background task, use `curlStatus` callback to monitor progress and receive result. + * @param displayname The "correlation ID" - the label passed to the callback to identify the transfer. + * @remark Callback signature: `curlStatus(curlStatusType, int n1, int n2, string displayname, string message)` + * - for CURL_STATUS_PROGRESS, n1 = bytes downloaded, n2 = total bytes, + * - otherwise, n1 = CURL return code, n2 = HTTP result code. + */ + void curlRequestAsync(std::string url, std::string displayname); }; #endif // WITH_ANGELSCRIPT diff --git a/source/server/sequencer.h b/source/server/sequencer.h index d3cd0aae..d6395728 100644 --- a/source/server/sequencer.h +++ b/source/server/sequencer.h @@ -132,8 +132,6 @@ class Client { SpamFilter& GetSpamFilter() { return m_spamfilter; } - Sequencer* GetSequencer() { } - RoRnet::UserInfo user; //!< user information int drop_state; // dropping outgoing packets?