diff --git a/CMakeLists.txt b/CMakeLists.txt index a19ba1b7c..31193db9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -349,6 +349,9 @@ if(WEBSOCKETS) show_dependency_status("Websockets" WEBSOCKETS) endif() +if(NOT(CURL_FOUND)) + message(SEND_ERROR "You must install Curl to compile the DDNet") +endif() if(NOT(PYTHONINTERP_FOUND)) message(SEND_ERROR "You must install Python to compile DDNet") endif() @@ -361,9 +364,6 @@ if(WEBSOCKETS AND NOT(WEBSOCKETS_FOUND)) message(SEND_ERROR "You must install libwebsockets to compile the DDNet server with websocket support") endif() -if(CLIENT AND NOT(CURL_FOUND)) - message(SEND_ERROR "You must install Curl to compile the DDNet client") -endif() if(CLIENT AND NOT(FREETYPE_FOUND)) message(SEND_ERROR "You must install Freetype to compile the DDNet client") endif() @@ -606,6 +606,8 @@ set_glob(ENGINE_SHARED GLOB src/engine/shared econ.cpp econ.h engine.cpp + fetcher.cpp + fetcher.h fifo.cpp fifo.h filecollection.cpp @@ -693,7 +695,7 @@ list(APPEND TARGETS_DEP md5) set(DEPS ${DEP_MD5} ${ZLIB_DEP}) # Libraries -set(LIBS ${CMAKE_THREAD_LIBS_INIT} ${WEBSOCKETS_LIBRARIES} ${ZLIB_LIBRARIES} ${PLATFORM_LIBS}) +set(LIBS ${CMAKE_THREAD_LIBS_INIT} ${CURL_LIBRARIES} ${WEBSOCKETS_LIBRARIES} ${ZLIB_LIBRARIES} ${PLATFORM_LIBS}) # Targets add_library(engine-shared EXCLUDE_FROM_ALL OBJECT ${ENGINE_INTERFACE} ${ENGINE_SHARED} ${ENGINE_GENERATED_SHARED} ${BASE}) @@ -712,8 +714,6 @@ if(CLIENT) backend_sdl.h client.cpp client.h - fetcher.cpp - fetcher.h friends.cpp friends.h graphics_threaded.cpp @@ -840,7 +840,6 @@ if(CLIENT) # Libraries set(LIBS_CLIENT ${LIBS} - ${CURL_LIBRARIES} ${FREETYPE_LIBRARIES} ${GLEW_LIBRARIES} ${PNGLITE_LIBRARIES} @@ -880,7 +879,6 @@ if(CLIENT) target_link_libraries(${TARGET_CLIENT} ${LIBS_CLIENT}) target_include_directories(${TARGET_CLIENT} PRIVATE - ${CURL_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} ${GLEW_INCLUDE_DIRS} ${OGG_INCLUDE_DIRS} @@ -1107,6 +1105,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) fs.cpp git_revision.cpp jobs.cpp + json.cpp mapbugs.cpp name_ban.cpp str.cpp @@ -1476,11 +1475,10 @@ foreach(target ${TARGETS}) endif() endforeach() -if(MSVC) - set_property(TARGET ${TARGET_CLIENT} APPEND PROPERTY LINK_FLAGS /SAFESEH:NO) # Disable SafeSEH because the shipped libraries don't support it. -endif() - foreach(target ${TARGETS_LINK}) + if(MSVC) + set_property(TARGET ${target} APPEND PROPERTY LINK_FLAGS /SAFESEH:NO) # Disable SafeSEH because the shipped libraries don't support it (would cause error LNK2026 otherwise). + endif() if(TARGET_OS STREQUAL "mac") target_link_libraries(${target} -stdlib=libc++) target_link_libraries(${target} -mmacosx-version-min=10.7) @@ -1510,6 +1508,7 @@ foreach(target ${TARGETS_OWN}) target_include_directories(${target} PRIVATE ${PROJECT_BINARY_DIR}/src) target_include_directories(${target} PRIVATE src) target_compile_definitions(${target} PRIVATE $<$:CONF_DEBUG>) + target_include_directories(${target} PRIVATE ${CURL_INCLUDE_DIRS}) target_include_directories(${target} PRIVATE ${ZLIB_INCLUDE_DIRS}) target_compile_definitions(${target} PRIVATE GLEW_STATIC) if(WEBSOCKETS) diff --git a/appveyor.yml b/appveyor.yml index 5b199eb7f..b2e2cc0a1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,8 +21,14 @@ test_script: - cmd: cmake --build build64 --config Debug --target run_tests - cmd: cmake --build build32 --config Release --target run_tests - cmd: cmake --build build64 --config Release --target run_tests - - cmd: build32\Release\DDNet-Server shutdown - - cmd: build64\Release\DDNet-Server shutdown + - cmd: | + cd build32 + Release\DDNet-Server shutdown + cd .. + - cmd: | + cd build64 + Release\DDNet-Server shutdown + cd .. after_build: - cmd: cmake --build build32 --config Release --target package diff --git a/src/base/system.c b/src/base/system.c index 4277d8c3d..71ab21725 100644 --- a/src/base/system.c +++ b/src/base/system.c @@ -664,17 +664,44 @@ void aio_wait(ASYNCIO *aio) thread_wait(thread); } +struct THREAD_RUN +{ + void (*threadfunc)(void *); + void *u; +}; + +#if defined(CONF_FAMILY_UNIX) +static void *thread_run(void *user) +#elif defined(CONF_FAMILY_WINDOWS) +static unsigned int __stdcall thread_run(void *user) +#else +#error not implemented +#endif +{ + struct THREAD_RUN *data = user; + void (*threadfunc)(void *) = data->threadfunc; + void *u = data->u; + free(data); + threadfunc(u); + return 0; +} + void *thread_init(void (*threadfunc)(void *), void *u) { + struct THREAD_RUN *data = malloc(sizeof(*data)); + data->threadfunc = threadfunc; + data->u = u; #if defined(CONF_FAMILY_UNIX) - pthread_t id; - if(pthread_create(&id, NULL, (void *(*)(void*))threadfunc, u) != 0) { - return 0; + pthread_t id; + if(pthread_create(&id, NULL, thread_run, data) != 0) + { + return 0; + } + return (void*)id; } - return (void*)id; #elif defined(CONF_FAMILY_WINDOWS) - return CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadfunc, u, 0, NULL); + return CreateThread(NULL, 0, thread_run, data, 0, NULL); #else #error not implemented #endif diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index f2403f5b6..dd9997999 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -65,7 +66,6 @@ #include "friends.h" #include "serverbrowser.h" -#include "fetcher.h" #include "updater.h" #include "client.h" @@ -1521,7 +1521,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket) str_append(aUrl, aEscaped, sizeof(aUrl)); - m_pMapdownloadTask = std::make_shared(Storage(), aUrl, m_aMapdownloadFilename, IStorage::TYPE_SAVE, UseDDNetCA, true); + m_pMapdownloadTask = std::make_shared(Storage(), aUrl, m_aMapdownloadFilename, IStorage::TYPE_SAVE, UseDDNetCA, true); Engine()->AddJob(m_pMapdownloadTask); } else @@ -2478,15 +2478,15 @@ void CClient::Update() if(m_pMapdownloadTask) { - if(m_pMapdownloadTask->State() == CFetchTask::STATE_DONE) + if(m_pMapdownloadTask->State() == HTTP_DONE) FinishMapDownload(); - else if(m_pMapdownloadTask->State() == CFetchTask::STATE_ERROR) + else if(m_pMapdownloadTask->State() == HTTP_ERROR) { dbg_msg("webdl", "http failed, falling back to gameserver"); ResetMapDownload(); SendMapRequest(); } - else if(m_pMapdownloadTask->State() == CFetchTask::STATE_ABORTED) + else if(m_pMapdownloadTask->State() == HTTP_ABORTED) { m_pMapdownloadTask = NULL; } @@ -2494,14 +2494,14 @@ void CClient::Update() if(m_pDDNetInfoTask) { - if(m_pDDNetInfoTask->State() == CFetchTask::STATE_DONE) + if(m_pDDNetInfoTask->State() == HTTP_DONE) FinishDDNetInfo(); - else if(m_pDDNetInfoTask->State() == CFetchTask::STATE_ERROR) + else if(m_pDDNetInfoTask->State() == HTTP_ERROR) { dbg_msg("ddnet-info", "download failed"); ResetDDNetInfo(); } - else if(m_pDDNetInfoTask->State() == CFetchTask::STATE_ABORTED) + else if(m_pDDNetInfoTask->State() == HTTP_ABORTED) { m_pDDNetInfoTask = NULL; } @@ -3684,7 +3684,7 @@ void CClient::RequestDDNetInfo() str_append(aUrl, aEscaped, sizeof(aUrl)); } - m_pDDNetInfoTask = std::make_shared(Storage(), aUrl, "ddnet-info.json.tmp", IStorage::TYPE_SAVE, true, true); + m_pDDNetInfoTask = std::make_shared(Storage(), aUrl, "ddnet-info.json.tmp", IStorage::TYPE_SAVE, true, true); Engine()->AddJob(m_pDDNetInfoTask); } diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 7e19083de..e8004a948 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -5,7 +5,7 @@ #include -#include "fetcher.h" +#include class CGraph { @@ -131,7 +131,7 @@ class CClient : public IClient, public CDemoPlayer::IListener char m_aCmdConnect[256]; // map download - std::shared_ptr m_pMapdownloadTask; + std::shared_ptr m_pMapdownloadTask; char m_aMapdownloadFilename[256]; char m_aMapdownloadName[256]; IOHANDLE m_MapdownloadFile; @@ -140,7 +140,7 @@ class CClient : public IClient, public CDemoPlayer::IListener int m_MapdownloadAmount; int m_MapdownloadTotalsize; - std::shared_ptr m_pDDNetInfoTask; + std::shared_ptr m_pDDNetInfoTask; // time CSmoothTime m_GameTime[2]; diff --git a/src/engine/client/fetcher.cpp b/src/engine/client/fetcher.cpp deleted file mode 100644 index 57aa5e962..000000000 --- a/src/engine/client/fetcher.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include -#include -#include -#include -#include - -#define WIN32_LEAN_AND_MEAN -#include "curl/curl.h" -#include "curl/easy.h" - -#include "fetcher.h" - -double CFetchTask::Current() const { return m_Current; } -double CFetchTask::Size() const { return m_Size; } -int CFetchTask::Progress() const { return m_Progress; } -int CFetchTask::State() const { return m_State; } -const char *CFetchTask::Dest() const { return m_aDest; } - -void CFetchTask::Abort() { m_Abort = true; }; - -CFetchTask::CFetchTask(IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType, bool UseDDNetCA, bool CanTimeout) : - m_pStorage(pStorage), - m_StorageType(StorageType), - m_UseDDNetCA(UseDDNetCA), - m_CanTimeout(CanTimeout), - m_Size(0), - m_Progress(0), - m_State(CFetchTask::STATE_QUEUED), - m_Abort(false) -{ - str_copy(m_aUrl, pUrl, sizeof(m_aUrl)); - str_copy(m_aDest, pDest, sizeof(m_aDest)); -} - - -bool FetcherInit() -{ - return !curl_global_init(CURL_GLOBAL_DEFAULT); -} - -void EscapeUrl(char *pBuf, int Size, const char *pStr) -{ - char *pEsc = curl_easy_escape(0, pStr, 0); - str_copy(pBuf, pEsc, Size); - curl_free(pEsc); -} - -void CFetchTask::Run() -{ - CURL *pHandle = curl_easy_init(); - if(!pHandle) - { - m_State = STATE_ERROR; - return; - } - - char aPath[512]; - if(m_StorageType == -2) - m_pStorage->GetBinaryPath(m_aDest, aPath, sizeof(aPath)); - else - m_pStorage->GetCompletePath(m_StorageType, m_aDest, aPath, sizeof(aPath)); - - if(fs_makedir_rec_for(aPath) < 0) - dbg_msg("fetcher", "i/o error, cannot create folder for: %s", aPath); - - IOHANDLE File = io_open(aPath, IOFLAG_WRITE); - - if(!File) - { - dbg_msg("fetcher", "i/o error, cannot open file: %s", m_aDest); - m_State = CFetchTask::STATE_ERROR; - return; - } - - char aErr[CURL_ERROR_SIZE]; - curl_easy_setopt(pHandle, CURLOPT_ERRORBUFFER, aErr); - - //curl_easy_setopt(pHandle, CURLOPT_VERBOSE, 1L); - if(m_CanTimeout) - { - curl_easy_setopt(pHandle, CURLOPT_CONNECTTIMEOUT_MS, (long)g_Config.m_ClHTTPConnectTimeoutMs); - curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_LIMIT, (long)g_Config.m_ClHTTPLowSpeedLimit); - curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_TIME, (long)g_Config.m_ClHTTPLowSpeedTime); - } - else - { - curl_easy_setopt(pHandle, CURLOPT_CONNECTTIMEOUT_MS, 0); - curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_LIMIT, 0); - curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_TIME, 0); - } - curl_easy_setopt(pHandle, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(pHandle, CURLOPT_MAXREDIRS, 4L); - curl_easy_setopt(pHandle, CURLOPT_FAILONERROR, 1L); - if(m_UseDDNetCA) - { - char aCAFile[512]; - m_pStorage->GetBinaryPath("data/ca-ddnet.pem", aCAFile, sizeof aCAFile); - curl_easy_setopt(pHandle, CURLOPT_CAINFO, aCAFile); - } - curl_easy_setopt(pHandle, CURLOPT_URL, m_aUrl); - curl_easy_setopt(pHandle, CURLOPT_WRITEDATA, File); - curl_easy_setopt(pHandle, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(pHandle, CURLOPT_NOPROGRESS, 0); - curl_easy_setopt(pHandle, CURLOPT_PROGRESSDATA, this); - curl_easy_setopt(pHandle, CURLOPT_PROGRESSFUNCTION, ProgressCallback); - curl_easy_setopt(pHandle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(pHandle, CURLOPT_USERAGENT, "DDNet " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")"); - - dbg_msg("fetcher", "downloading %s", m_aDest); - m_State = CFetchTask::STATE_RUNNING; - int Ret = curl_easy_perform(pHandle); - io_close(File); - if(Ret != CURLE_OK) - { - dbg_msg("fetcher", "task failed. libcurl error: %s", aErr); - m_State = (Ret == CURLE_ABORTED_BY_CALLBACK) ? CFetchTask::STATE_ABORTED : CFetchTask::STATE_ERROR; - } - else - { - dbg_msg("fetcher", "task done %s", m_aDest); - m_State = CFetchTask::STATE_DONE; - } - - curl_easy_cleanup(pHandle); - - OnCompletion(); -} - -size_t CFetchTask::WriteCallback(char *pData, size_t Size, size_t Number, void *pFile) -{ - return io_write((IOHANDLE)pFile, pData, Size * Number); -} - -int CFetchTask::ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr) -{ - CFetchTask *pTask = (CFetchTask *)pUser; - pTask->m_Current = DlCurr; - pTask->m_Size = DlTotal; - pTask->m_Progress = (100 * DlCurr) / (DlTotal ? DlTotal : 1); - pTask->OnProgress(); - return pTask->m_Abort ? -1 : 0; -} diff --git a/src/engine/client/updater.cpp b/src/engine/client/updater.cpp index efd8c1a41..b7f05b07b 100644 --- a/src/engine/client/updater.cpp +++ b/src/engine/client/updater.cpp @@ -11,7 +11,7 @@ using std::string; using std::map; -class CUpdaterFetchTask : public CFetchTask +class CUpdaterFetchTask : public CGetFile { char m_aBuf[256]; char m_aBuf2[256]; @@ -41,7 +41,7 @@ static const char *GetUpdaterDestPath(char *pBuf, int BufSize, const char *pFile } CUpdaterFetchTask::CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath) : - CFetchTask(pUpdater->m_pStorage, GetUpdaterUrl(m_aBuf, sizeof(m_aBuf), pFile), GetUpdaterDestPath(m_aBuf2, sizeof(m_aBuf), pFile, pDestPath), -2, true, false), + CGetFile(pUpdater->m_pStorage, GetUpdaterUrl(m_aBuf, sizeof(m_aBuf), pFile), GetUpdaterDestPath(m_aBuf2, sizeof(m_aBuf), pFile, pDestPath), -2, true, false), m_pUpdater(pUpdater) { } @@ -63,16 +63,16 @@ void CUpdaterFetchTask::OnCompletion() b = b ? b : Dest(); if(!str_comp(b, "update.json")) { - if(State() == CFetchTask::STATE_DONE) + if(State() == HTTP_DONE) m_pUpdater->SetCurrentState(IUpdater::GOT_MANIFEST); - else if(State() == CFetchTask::STATE_ERROR) + else if(State() == HTTP_ERROR) m_pUpdater->SetCurrentState(IUpdater::FAIL); } else if(!str_comp(b, m_pUpdater->m_aLastFile)) { - if(State() == CFetchTask::STATE_DONE) + if(State() == HTTP_DONE) m_pUpdater->SetCurrentState(IUpdater::MOVE_FILES); - else if(State() == CFetchTask::STATE_ERROR) + else if(State() == HTTP_ERROR) m_pUpdater->SetCurrentState(IUpdater::FAIL); } } diff --git a/src/engine/client/updater.h b/src/engine/client/updater.h index 0761966a7..6be13f854 100644 --- a/src/engine/client/updater.h +++ b/src/engine/client/updater.h @@ -2,7 +2,7 @@ #define ENGINE_CLIENT_UPDATER_H #include -#include "fetcher.h" +#include #include #include diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 9e8cc3c56..b79e15d29 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -2875,6 +2876,8 @@ int main(int argc, const char **argv) // ignore_convention pEngineMasterServer->Init(); pEngineMasterServer->Load(); + FetcherInit(); + // register all console commands pServer->RegisterCommands(); diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 3773b7168..c91b45382 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -161,6 +161,7 @@ MACRO_CONFIG_INT(SvPlayerDemoRecord, sv_player_demo_record, 0, 0, 1, CFGFLAG_SER MACRO_CONFIG_INT(SvDemoChat, sv_demo_chat, 0, 0, 1, CFGFLAG_SERVER, "Record chat for demos") MACRO_CONFIG_INT(SvServerInfoPerSecond, sv_server_info_per_second, 50, 1, 1000, CFGFLAG_SERVER, "Maximum number of complete server info responses that are sent out per second") MACRO_CONFIG_INT(SvVanConnPerSecond, sv_van_conn_per_second, 10, 1, 1000, CFGFLAG_SERVER, "Antispoof specific ratelimit") +MACRO_CONFIG_STR(SvModhelpUrl, sv_modhelp_url, 128, "", CFGFLAG_SERVER|CFGFLAG_NONTEEHISTORIC, "HTTP URL to POST moderator help requests to") MACRO_CONFIG_STR(EcBindaddr, ec_bindaddr, 128, "localhost", CFGFLAG_ECON, "Address to bind the external console to. Anything but 'localhost' is dangerous") MACRO_CONFIG_INT(EcPort, ec_port, 0, 0, 0, CFGFLAG_ECON, "Port to use for the external console") diff --git a/src/engine/shared/fetcher.cpp b/src/engine/shared/fetcher.cpp new file mode 100644 index 000000000..f613733e9 --- /dev/null +++ b/src/engine/shared/fetcher.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include "curl/curl.h" +#include "curl/easy.h" + +#include "fetcher.h" + +double CGetFile::Current() const { return m_Current; } +double CGetFile::Size() const { return m_Size; } +int CGetFile::Progress() const { return m_Progress; } +int CGetFile::State() const { return m_State; } +const char *CGetFile::Dest() const { return m_aDest; } + +void CGetFile::Abort() { m_Abort = true; }; + +CGetFile::CGetFile(IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType, bool UseDDNetCA, bool CanTimeout) : + m_pStorage(pStorage), + m_StorageType(StorageType), + m_UseDDNetCA(UseDDNetCA), + m_CanTimeout(CanTimeout), + m_Size(0), + m_Progress(0), + m_State(HTTP_QUEUED), + m_Abort(false) +{ + str_copy(m_aUrl, pUrl, sizeof(m_aUrl)); + str_copy(m_aDest, pDest, sizeof(m_aDest)); +} + + +bool FetcherInit() +{ + return !curl_global_init(CURL_GLOBAL_DEFAULT); +} + +void EscapeUrl(char *pBuf, int Size, const char *pStr) +{ + char *pEsc = curl_easy_escape(0, pStr, 0); + str_copy(pBuf, pEsc, Size); + curl_free(pEsc); +} + +static void SetCurlOptions(CURL *pHandle, char *pErrorBuffer, const char *pUrl, bool CanTimeout) +{ + //curl_easy_setopt(pHandle, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(pHandle, CURLOPT_ERRORBUFFER, pErrorBuffer); + + if(CanTimeout) + { + curl_easy_setopt(pHandle, CURLOPT_CONNECTTIMEOUT_MS, (long)g_Config.m_ClHTTPConnectTimeoutMs); + curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_LIMIT, (long)g_Config.m_ClHTTPLowSpeedLimit); + curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_TIME, (long)g_Config.m_ClHTTPLowSpeedTime); + } + else + { + curl_easy_setopt(pHandle, CURLOPT_CONNECTTIMEOUT_MS, 0L); + curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_LIMIT, 0L); + curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_TIME, 0L); + } + curl_easy_setopt(pHandle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(pHandle, CURLOPT_MAXREDIRS, 4L); + curl_easy_setopt(pHandle, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(pHandle, CURLOPT_URL, pUrl); + curl_easy_setopt(pHandle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(pHandle, CURLOPT_USERAGENT, "DDNet " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")"); +} + +void CGetFile::Run() +{ + CURL *pHandle = curl_easy_init(); + if(!pHandle) + { + m_State = HTTP_ERROR; + return; + } + + char aPath[512]; + if(m_StorageType == -2) + m_pStorage->GetBinaryPath(m_aDest, aPath, sizeof(aPath)); + else + m_pStorage->GetCompletePath(m_StorageType, m_aDest, aPath, sizeof(aPath)); + + if(fs_makedir_rec_for(aPath) < 0) + dbg_msg("fetcher", "i/o error, cannot create folder for: %s", aPath); + + IOHANDLE File = io_open(aPath, IOFLAG_WRITE); + + if(!File) + { + dbg_msg("fetcher", "i/o error, cannot open file: %s", m_aDest); + m_State = HTTP_ERROR; + return; + } + + char aErr[CURL_ERROR_SIZE]; + SetCurlOptions(pHandle, aErr, m_aUrl, m_CanTimeout); + if(m_UseDDNetCA) + { + char aCAFile[512]; + m_pStorage->GetBinaryPath("data/ca-ddnet.pem", aCAFile, sizeof aCAFile); + curl_easy_setopt(pHandle, CURLOPT_CAINFO, aCAFile); + } + curl_easy_setopt(pHandle, CURLOPT_WRITEDATA, File); + curl_easy_setopt(pHandle, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(pHandle, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(pHandle, CURLOPT_PROGRESSDATA, this); + curl_easy_setopt(pHandle, CURLOPT_PROGRESSFUNCTION, ProgressCallback); + + dbg_msg("fetcher", "downloading %s", m_aDest); + m_State = HTTP_RUNNING; + int Ret = curl_easy_perform(pHandle); + io_close(File); + if(Ret != CURLE_OK) + { + dbg_msg("fetcher", "task failed. libcurl error: %s", aErr); + m_State = (Ret == CURLE_ABORTED_BY_CALLBACK) ? HTTP_ABORTED : HTTP_ERROR; + } + else + { + dbg_msg("fetcher", "task done %s", m_aDest); + m_State = HTTP_DONE; + } + + curl_easy_cleanup(pHandle); + + OnCompletion(); +} + +size_t CGetFile::WriteCallback(char *pData, size_t Size, size_t Number, void *pFile) +{ + return io_write((IOHANDLE)pFile, pData, Size * Number); +} + +int CGetFile::ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr) +{ + CGetFile *pTask = (CGetFile *)pUser; + pTask->m_Current = DlCurr; + pTask->m_Size = DlTotal; + pTask->m_Progress = (100 * DlCurr) / (DlTotal ? DlTotal : 1); + pTask->OnProgress(); + return pTask->m_Abort ? -1 : 0; +} + +int CPostJson::State() const { return m_State; } + +CPostJson::CPostJson(const char *pUrl, const char *pJson) + : m_State(HTTP_QUEUED) +{ + str_copy(m_aUrl, pUrl, sizeof(m_aUrl)); + str_copy(m_aJson, pJson, sizeof(m_aJson)); +} + +void CPostJson::Run() +{ + CURL *pHandle = curl_easy_init(); + if(!pHandle) + { + m_State = HTTP_ERROR; + return; + } + + char aErr[CURL_ERROR_SIZE]; + SetCurlOptions(pHandle, aErr, m_aUrl, true); + + curl_slist *pHeaders = NULL; + pHeaders = curl_slist_append(pHeaders, "Content-Type: application/json"); + curl_easy_setopt(pHandle, CURLOPT_HTTPHEADER, pHeaders); + curl_easy_setopt(pHandle, CURLOPT_POSTFIELDS, m_aJson); + + dbg_msg("fetcher", "posting to %s", m_aUrl); + m_State = HTTP_RUNNING; + int Result = curl_easy_perform(pHandle); + + curl_slist_free_all(pHeaders); + if(Result != CURLE_OK) + { + dbg_msg("fetcher", "task failed. libcurl error: %s", aErr); + m_State = HTTP_ERROR; + } + else + { + dbg_msg("fetcher", "posting to %s done", m_aUrl); + m_State = HTTP_DONE; + } + + curl_easy_cleanup(pHandle); + + OnCompletion(); +} + +static char EscapeJsonChar(char c) +{ + switch(c) + { + case '\"': return '\"'; + case '\\': return '\\'; + case '\b': return 'b'; + case '\n': return 'n'; + case '\r': return 'r'; + case '\t': return 't'; + // Don't escape '\f', who uses that. :) + default: return 0; + } +} + +char *EscapeJson(char *pBuffer, int BufferSize, const char *pString) +{ + dbg_assert(BufferSize > 0, "can't null-terminate the string"); + // Subtract the space for null termination early. + BufferSize--; + + char *pResult = pBuffer; + while(BufferSize && *pString) + { + char c = *pString; + pString++; + char Escaped = EscapeJsonChar(c); + if(Escaped) + { + if(BufferSize < 2) + { + break; + } + *pBuffer++ = '\\'; + *pBuffer++ = Escaped; + BufferSize -= 2; + } + // Assuming ASCII/UTF-8, "if control character". + else if((unsigned char)c < 0x20) + { + // \uXXXX + if(BufferSize < 6) + { + break; + } + str_format(pBuffer, BufferSize, "\\u%04x", c); + pBuffer += 6; + BufferSize -= 6; + } + else + { + *pBuffer++ = c; + BufferSize--; + } + } + *pBuffer = 0; + return pResult; +} diff --git a/src/engine/client/fetcher.h b/src/engine/shared/fetcher.h similarity index 55% rename from src/engine/client/fetcher.h rename to src/engine/shared/fetcher.h index 99179c731..0beda5175 100644 --- a/src/engine/client/fetcher.h +++ b/src/engine/shared/fetcher.h @@ -5,7 +5,16 @@ #include #include -class CFetchTask : public IJob +enum +{ + HTTP_ERROR = -1, + HTTP_QUEUED, + HTTP_RUNNING, + HTTP_DONE, + HTTP_ABORTED, +}; + +class CGetFile : public IJob { private: IStorage *m_pStorage; @@ -19,7 +28,7 @@ private: double m_Size; double m_Current; int m_Progress; - int m_State; + std::atomic m_State; std::atomic m_Abort; @@ -32,16 +41,7 @@ private: void Run(); public: - enum - { - STATE_ERROR = -1, - STATE_QUEUED, - STATE_RUNNING, - STATE_DONE, - STATE_ABORTED, - }; - - CFetchTask(IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType = -2, bool UseDDNetCA = false, bool CanTimeout = true); + CGetFile(IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType = -2, bool UseDDNetCA = false, bool CanTimeout = true); double Current() const; double Size() const; @@ -51,6 +51,24 @@ public: void Abort(); }; + +class CPostJson : public IJob +{ +private: + char m_aUrl[256]; + char m_aJson[1024]; + std::atomic m_State; + + void Run(); + + virtual void OnCompletion() { } + +public: + CPostJson(const char *pUrl, const char *pJson); + int State() const; +}; + bool FetcherInit(); -void EscapeUrl(char *pBud, int Size, const char *pStr); +void EscapeUrl(char *pBuf, int Size, const char *pStr); +char *EscapeJson(char *pBuffer, int BufferSize, const char *pString); #endif diff --git a/src/engine/shared/jobs.cpp b/src/engine/shared/jobs.cpp index 00e37b540..e0b1bdef3 100644 --- a/src/engine/shared/jobs.cpp +++ b/src/engine/shared/jobs.cpp @@ -1,6 +1,5 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ -#include #include "jobs.h" IJob::IJob() : diff --git a/src/engine/shared/jobs.h b/src/engine/shared/jobs.h index 3a73a3eb1..844d0ba1b 100644 --- a/src/engine/shared/jobs.h +++ b/src/engine/shared/jobs.h @@ -3,6 +3,8 @@ #ifndef ENGINE_SHARED_JOBS_H #define ENGINE_SHARED_JOBS_H +#include + #include #include diff --git a/src/game/server/ddracechat.cpp b/src/game/server/ddracechat.cpp index 21788fa8b..8b30cf037 100644 --- a/src/game/server/ddracechat.cpp +++ b/src/game/server/ddracechat.cpp @@ -1,5 +1,6 @@ /* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ #include "gamecontext.h" +#include #include #include #include @@ -1392,27 +1393,39 @@ void CGameContext::ConProtectedKill(IConsole::IResult *pResult, void *pUserData) } } -void CGameContext::ConModHelp(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConModhelp(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *) pUserData; - if (!CheckClientID(pResult->m_ClientID)) + if(!CheckClientID(pResult->m_ClientID)) return; CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; - if (!pPlayer) + if(!pPlayer) return; - if(pPlayer->m_ModHelpTick > pSelf->Server()->Tick()) + if(pPlayer->m_pPostJson) { - char aBuf[126]; - str_format(aBuf, sizeof(aBuf), "You must wait %d seconds to execute this command again.", - (pPlayer->m_ModHelpTick - pSelf->Server()->Tick()) / pSelf->Server()->TickSpeed()); - pSelf->SendChatTarget(pResult->m_ClientID, aBuf); + pSelf->SendChatTarget(pResult->m_ClientID, "Your last request hasn't finished processing yet, please slow down"); return; } - pPlayer->m_ModHelpTick = pSelf->Server()->Tick() + g_Config.m_SvModHelpDelay * pSelf->Server()->TickSpeed(); + int CurTick = pSelf->Server()->Tick(); + if(pPlayer->m_ModhelpTick != -1) + { + int TickSpeed = pSelf->Server()->TickSpeed(); + int NextModhelpTick = pPlayer->m_ModhelpTick + g_Config.m_SvModhelpDelay * TickSpeed; + if(NextModhelpTick > CurTick) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "You must wait %d seconds before you can execute this command again.", + (NextModhelpTick - CurTick) / TickSpeed); + pSelf->SendChatTarget(pResult->m_ClientID, aBuf); + return; + } + } + + pPlayer->m_ModhelpTick = CurTick; char aBuf[512]; str_format(aBuf, sizeof(aBuf), "Moderator help is requested by '%s' (ID: %d):", @@ -1424,10 +1437,33 @@ void CGameContext::ConModHelp(IConsole::IResult *pResult, void *pUserData) { if(pSelf->m_apPlayers[i] && pSelf->Server()->ClientAuthed(i)) { - pSelf->SendChatTarget(pSelf->m_apPlayers[i]->GetCID(), aBuf); - pSelf->SendChatTarget(pSelf->m_apPlayers[i]->GetCID(), pResult->GetString(0)); + pSelf->SendChatTarget(i, aBuf); + pSelf->SendChatTarget(i, pResult->GetString(0)); } } + if(g_Config.m_SvModhelpUrl[0]) + { + bool ModeratorPresent = false; + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(pSelf->m_apPlayers[i] && pSelf->Server()->ClientAuthed(i)) + { + ModeratorPresent = true; + break; + } + } + + char aJson[512]; + char aPlayerName[64]; + char aMessage[128]; + str_format(aJson, sizeof(aJson), "{\"port\":%d,\"moderator_present\":%s,\"player_id\":%d,\"player_name\":\"%s\",\"message\":\"%s\"}", + g_Config.m_SvPort, + ModeratorPresent ? "true" : "false", + pResult->m_ClientID, + EscapeJson(aPlayerName, sizeof(aPlayerName), pSelf->Server()->ClientName(pResult->m_ClientID)), + EscapeJson(aMessage, sizeof(aMessage), pResult->GetString(0))); + pSelf->Engine()->AddJob(pPlayer->m_pPostJson = std::make_shared(g_Config.m_SvModhelpUrl, aJson)); + } } #if defined(CONF_SQL) diff --git a/src/game/server/ddracechat.h b/src/game/server/ddracechat.h index d19c645ec..8e73e720f 100644 --- a/src/game/server/ddracechat.h +++ b/src/game/server/ddracechat.h @@ -50,7 +50,7 @@ CHAT_COMMAND("rescue", "", CFGFLAG_CHAT|CFGFLAG_SERVER, ConRescue, this, "Telepo CHAT_COMMAND("kill", "", CFGFLAG_CHAT|CFGFLAG_SERVER, ConProtectedKill, this, "Kill yourself") -CHAT_COMMAND("modhelp", "r[message]", CFGFLAG_CHAT|CFGFLAG_SERVER, ConModHelp, this, "Request the help of a moderator with a description of the problem") +CHAT_COMMAND("modhelp", "r[message]", CFGFLAG_CHAT|CFGFLAG_SERVER, ConModhelp, this, "Request the help of a moderator with a description of the problem") #if defined(CONF_SQL) CHAT_COMMAND("times", "?s[player name] ?i[number of times to skip]", CFGFLAG_CHAT|CFGFLAG_SERVER, ConTimes, this, "/times ?s?i shows last 5 times of the server or of a player beginning with name s starting with time i (i = 1 by default)") diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 65df500a3..1b558fdbe 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -891,6 +892,22 @@ void CGameContext::OnTick() m_aMutes[i] = m_aMutes[m_NumMutes]; } } + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(m_apPlayers[i] && m_apPlayers[i]->m_pPostJson) + { + switch(m_apPlayers[i]->m_pPostJson->State()) + { + case HTTP_DONE: + m_apPlayers[i]->m_pPostJson = NULL; + break; + case HTTP_ERROR: + dbg_msg("modhelp", "http request failed for cid=%d", i); + m_apPlayers[i]->m_pPostJson = NULL; + break; + } + } + } if(Server()->Tick() % (g_Config.m_SvAnnouncementInterval * Server()->TickSpeed() * 60) == 0) { @@ -2551,6 +2568,7 @@ void CGameContext::OnConsoleInit() { m_pServer = Kernel()->RequestInterface(); m_pConsole = Kernel()->RequestInterface(); + m_pEngine = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); m_ChatPrintCBIndex = Console()->RegisterPrintCallback(0, SendChatResponse, this); @@ -2593,6 +2611,7 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/) { m_pServer = Kernel()->RequestInterface(); m_pConsole = Kernel()->RequestInterface(); + m_pEngine = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); m_World.SetGameServer(this); m_Events.SetGameServer(this); diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index e3c78b21b..8a3946b6d 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -53,12 +53,15 @@ enum NUM_TUNEZONES = 256 }; +class IConsole; +class IEngine; class IStorage; class CGameContext : public IGameServer { IServer *m_pServer; - class IConsole *m_pConsole; + IConsole *m_pConsole; + IEngine *m_pEngine; IStorage *m_pStorage; CLayers m_Layers; CCollision m_Collision; @@ -113,7 +116,8 @@ class CGameContext : public IGameServer bool m_Resetting; public: IServer *Server() const { return m_pServer; } - class IConsole *Console() { return m_pConsole; } + IConsole *Console() { return m_pConsole; } + IEngine *Engine() { return m_pEngine; } IStorage *Storage() { return m_pStorage; } CCollision *Collision() { return &m_Collision; } CTuningParams *Tuning() { return &m_Tuning; } @@ -353,7 +357,7 @@ private: static void ConUnmute(IConsole::IResult *pResult, void *pUserData); static void ConMutes(IConsole::IResult *pResult, void *pUserData); static void ConModerate(IConsole::IResult *pResult, void *pUserData); - static void ConModHelp(IConsole::IResult *pResult, void *pUserData); + static void ConModhelp(IConsole::IResult *pResult, void *pUserData); static void ConList(IConsole::IResult *pResult, void *pUserData); static void ConSetDDRTeam(IConsole::IResult *pResult, void *pUserData); diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 5db306043..255a937c2 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -70,7 +70,7 @@ void CPlayer::Reset() m_LastWhisperTo = -1; m_LastSetSpectatorMode = 0; m_TimeoutCode[0] = '\0'; - m_ModHelpTick = 0; + m_ModhelpTick = -1; m_TuneZone = 0; m_TuneZoneOld = m_TuneZone; diff --git a/src/game/server/player.h b/src/game/server/player.h index 49daf14df..5b832022f 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -6,6 +6,7 @@ // this include should perhaps be removed #include "entities/character.h" #include "gamecontext.h" +#include // player object class CPlayer @@ -174,7 +175,7 @@ public: int m_ChatScore; bool m_Moderating; - int m_ModHelpTick; + int m_ModhelpTick; bool AfkTimer(int new_target_x, int new_target_y); //returns true if kicked void AfkVoteTimer(CNetObj_PlayerInput *NewTarget); @@ -197,6 +198,7 @@ public: #if defined(CONF_SQL) int64 m_LastSQLQuery; #endif + std::shared_ptr m_pPostJson; }; #endif diff --git a/src/game/server/teehistorian.cpp b/src/game/server/teehistorian.cpp index 744c53e2b..81a3673f6 100644 --- a/src/game/server/teehistorian.cpp +++ b/src/game/server/teehistorian.cpp @@ -28,63 +28,6 @@ enum TEEHISTORIAN_EX, }; -static char EscapeJsonChar(char c) -{ - switch(c) - { - case '\"': return '\"'; - case '\\': return '\\'; - case '\b': return 'b'; - case '\n': return 'n'; - case '\r': return 'r'; - case '\t': return 't'; - // Don't escape '\f', who uses that. :) - default: return 0; - } -} - -static char *EscapeJson(char *pBuffer, int BufferSize, const char *pString) -{ - dbg_assert(BufferSize > 0, "can't null-terminate the string"); - // Subtract the space for null termination early. - BufferSize--; - - char *pResult = pBuffer; - while(BufferSize && *pString) - { - char c = *pString; - pString++; - char Escaped = EscapeJsonChar(c); - if(Escaped) - { - if(BufferSize < 2) - { - break; - } - *pBuffer++ = '\\'; - *pBuffer++ = Escaped; - BufferSize -= 2; - } - // Assuming ASCII/UTF-8, "if control character". - else if(c < 0x20) - { - // \uXXXX - if(BufferSize < 6) - { - break; - } - str_format(pBuffer, BufferSize, "\\u%04x", c); - BufferSize -= 6; - } - else - { - *pBuffer++ = c; - } - } - *pBuffer = 0; - return pResult; -} - CTeeHistorian::CTeeHistorian() { m_State = STATE_START; diff --git a/src/game/server/teehistorian.h b/src/game/server/teehistorian.h index 5fd47ff16..943f3ebd9 100644 --- a/src/game/server/teehistorian.h +++ b/src/game/server/teehistorian.h @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/game/variables.h b/src/game/variables.h index c6b7127be..e5c9ada92 100644 --- a/src/game/variables.h +++ b/src/game/variables.h @@ -159,7 +159,7 @@ MACRO_CONFIG_INT(SvSendVotesPerTick, sv_send_votes_per_tick, 5, 1, 15, CFGFLAG_S MACRO_CONFIG_INT(SvRescue, sv_rescue, 0, 0, 1, CFGFLAG_SERVER, "Allow /rescue command so players can teleport themselves out of freeze") MACRO_CONFIG_INT(SvRescueDelay, sv_rescue_delay, 5, 0, 1000, CFGFLAG_SERVER, "Number of seconds between two rescues") -MACRO_CONFIG_INT(SvModHelpDelay, sv_modhelp_delay, 60, 0, 0, CFGFLAG_SERVER, "Number of seconds to wait before executing /modhelp again") +MACRO_CONFIG_INT(SvModhelpDelay, sv_modhelp_delay, 60, 0, 0, CFGFLAG_SERVER, "Number of seconds to wait before executing /modhelp again") // debug #ifdef CONF_DEBUG // this one can crash the server if not used correctly diff --git a/src/modhelp/server.py b/src/modhelp/server.py new file mode 100755 index 000000000..311853c26 --- /dev/null +++ b/src/modhelp/server.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +from flask import Flask, make_response, request +import http +import re +import requests + +SERVER=None +# Generate one by right-clicking on the server icon in the sidebar, clicking on +# "Server Settings" → "Webhooks" → "Create Webhook". You can then select the +# channel in which the messages should appear. Copy the "Webhook URL" to the +# following config variable: +# DISCORD_WEBHOOK="https://discordapp.com/api/webhooks/.../..." +DISCORD_WEBHOOK=None + +app = Flask(__name__) + +def sanitize(s): + return re.sub(r"([^\0- 0-9A-Za-z])", r"\\\1", s) + +def no_content(): + return make_response("", http.HTTPStatus.NO_CONTENT) + +@app.route("/modhelp", methods=['POST']) +def modhelp(): + json = request.get_json() + + if "server" not in json: + if SERVER: + json["server"] = SERVER + + if "server" not in json: + user = "{port}".format(**json) + else: + user = "{server}:{port}".format(**json) + message = "<{player_id}:{player_name}> {message}".format(**json) + + if DISCORD_WEBHOOK: + try: + requests.post(DISCORD_WEBHOOK, data={"username": user, "content": sanitize(message)}).raise_for_status() + except requests.HTTPError as e: + print(repr(e)) + raise + return no_content() diff --git a/src/test/json.cpp b/src/test/json.cpp new file mode 100644 index 000000000..b46f6db1c --- /dev/null +++ b/src/test/json.cpp @@ -0,0 +1,22 @@ +#include + +#include + +TEST(Json, Escape) +{ + char aBuf[128]; + char aSmall[2]; + char aSix[6]; + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), ""), ""); + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), "a"), "a"); + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), "\n"), "\\n"); + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), "\\"), "\\\\"); // https://www.xkcd.com/1638/ + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), "\x1b"), "\\u001b"); // escape + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), "愛"), "愛"); + EXPECT_STREQ(EscapeJson(aBuf, sizeof(aBuf), "😂"), "😂"); + + // Truncations + EXPECT_STREQ(EscapeJson(aSmall, sizeof(aSmall), "\\"), ""); + EXPECT_STREQ(EscapeJson(aSix, sizeof(aSix), "\x01"), ""); + EXPECT_STREQ(EscapeJson(aSix, sizeof(aSix), "aaaaaa"), "aaaaa"); +}