1157: Add a way to call for external moderator help r=Learath2 a=heinrich5991

This is done by HTTP POSTing to a location specified by
`sv_modhelp_url`. We also provide a `src/modhelp/server.py` which can
use theses POSTs to forward them to Discord servers.

The POST contains a JSON object payload, with the keys `"port"` which
contains the server port, `"player_id"` which contains the calling
player's client ID, `"player_name"` which contains the calling player's
nick and `"message"` which is the user-specified message.

Make JSON-escaping function public, add tests and fix bugs uncovered by
these tests.

Supersedes #1129.

1160: Fix warning about incompatible function pointers r=Learath2 a=heinrich5991

This comes at the cost of one allocation per started thread. This should
be okay because we're about to invoke a syscall anyway.

Co-authored-by: heinrich5991 <heinrich5991@gmail.com>
This commit is contained in:
bors[bot] 2018-06-24 14:12:48 +00:00
commit ebb9481857
25 changed files with 505 additions and 269 deletions

View file

@ -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 $<$<CONFIG:Debug>: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)

View file

@ -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

View file

@ -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)
if(pthread_create(&id, NULL, thread_run, data) != 0)
{
return 0;
}
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

View file

@ -40,6 +40,7 @@
#include <engine/shared/compression.h>
#include <engine/shared/datafile.h>
#include <engine/shared/demo.h>
#include <engine/shared/fetcher.h>
#include <engine/shared/filecollection.h>
#include <engine/shared/ghost.h>
#include <engine/shared/network.h>
@ -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<CFetchTask>(Storage(), aUrl, m_aMapdownloadFilename, IStorage::TYPE_SAVE, UseDDNetCA, true);
m_pMapdownloadTask = std::make_shared<CGetFile>(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<CFetchTask>(Storage(), aUrl, "ddnet-info.json.tmp", IStorage::TYPE_SAVE, true, true);
m_pDDNetInfoTask = std::make_shared<CGetFile>(Storage(), aUrl, "ddnet-info.json.tmp", IStorage::TYPE_SAVE, true, true);
Engine()->AddJob(m_pDDNetInfoTask);
}

View file

@ -5,7 +5,7 @@
#include <memory>
#include "fetcher.h"
#include <engine/shared/fetcher.h>
class CGraph
{
@ -131,7 +131,7 @@ class CClient : public IClient, public CDemoPlayer::IListener
char m_aCmdConnect[256];
// map download
std::shared_ptr<CFetchTask> m_pMapdownloadTask;
std::shared_ptr<CGetFile> 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<CFetchTask> m_pDDNetInfoTask;
std::shared_ptr<CGetFile> m_pDDNetInfoTask;
// time
CSmoothTime m_GameTime[2];

View file

@ -1,142 +0,0 @@
#include <base/system.h>
#include <engine/engine.h>
#include <engine/storage.h>
#include <engine/shared/config.h>
#include <game/version.h>
#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;
}

View file

@ -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);
}
}

View file

@ -2,7 +2,7 @@
#define ENGINE_CLIENT_UPDATER_H
#include <engine/updater.h>
#include "fetcher.h"
#include <engine/shared/fetcher.h>
#include <map>
#include <string>

View file

@ -19,6 +19,7 @@
#include <engine/shared/datafile.h>
#include <engine/shared/demo.h>
#include <engine/shared/econ.h>
#include <engine/shared/fetcher.h>
#include <engine/shared/filecollection.h>
#include <engine/shared/netban.h>
#include <engine/shared/network.h>
@ -2875,6 +2876,8 @@ int main(int argc, const char **argv) // ignore_convention
pEngineMasterServer->Init();
pEngineMasterServer->Load();
FetcherInit();
// register all console commands
pServer->RegisterCommands();

View file

@ -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")

View file

@ -0,0 +1,253 @@
#include <base/system.h>
#include <engine/engine.h>
#include <engine/storage.h>
#include <engine/shared/config.h>
#include <game/version.h>
#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;
}

View file

@ -5,7 +5,16 @@
#include <engine/storage.h>
#include <engine/kernel.h>
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<int> m_State;
std::atomic<bool> 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<int> 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

View file

@ -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 <base/system.h>
#include "jobs.h"
IJob::IJob() :

View file

@ -3,6 +3,8 @@
#ifndef ENGINE_SHARED_JOBS_H
#define ENGINE_SHARED_JOBS_H
#include <base/system.h>
#include <atomic>
#include <memory>

View file

@ -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 <engine/engine.h>
#include <engine/shared/config.h>
#include <engine/shared/protocol.h>
#include <game/server/teams.h>
@ -1392,7 +1393,7 @@ 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;
@ -1403,16 +1404,28 @@ void CGameContext::ConModHelp(IConsole::IResult *pResult, void *pUserData)
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<CPostJson>(g_Config.m_SvModhelpUrl, aJson));
}
}
#if defined(CONF_SQL)

View file

@ -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)")

View file

@ -7,6 +7,7 @@
#include <engine/shared/config.h>
#include <engine/map.h>
#include <engine/console.h>
#include <engine/engine.h>
#include <engine/shared/datafile.h>
#include <engine/shared/linereader.h>
#include <engine/storage.h>
@ -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<IServer>();
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pEngine = Kernel()->RequestInterface<IEngine>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
m_ChatPrintCBIndex = Console()->RegisterPrintCallback(0, SendChatResponse, this);
@ -2593,6 +2611,7 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/)
{
m_pServer = Kernel()->RequestInterface<IServer>();
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pEngine = Kernel()->RequestInterface<IEngine>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
m_World.SetGameServer(this);
m_Events.SetGameServer(this);

View file

@ -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);

View file

@ -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;

View file

@ -6,6 +6,7 @@
// this include should perhaps be removed
#include "entities/character.h"
#include "gamecontext.h"
#include <engine/shared/fetcher.h>
// 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<CPostJson> m_pPostJson;
};
#endif

View file

@ -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;

View file

@ -1,4 +1,5 @@
#include <engine/console.h>
#include <engine/shared/fetcher.h>
#include <engine/shared/packer.h>
#include <engine/shared/protocol.h>
#include <game/generated/protocol.h>

View file

@ -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

43
src/modhelp/server.py Executable file
View file

@ -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()

22
src/test/json.cpp Normal file
View file

@ -0,0 +1,22 @@
#include <gtest/gtest.h>
#include <engine/shared/fetcher.h>
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");
}