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