ddnet/src/engine/client/http.cpp

347 lines
8 KiB
C++
Raw Normal View History

#include "http.h"
#include <base/system.h>
#include <engine/engine.h>
#include <engine/external/json-parser/json.h>
#include <engine/shared/config.h>
#include <engine/storage.h>
#include <game/version.h>
#if !defined(CONF_FAMILY_WINDOWS)
#include <signal.h>
#endif
#define WIN32_LEAN_AND_MEAN
#include "curl/curl.h"
#include "curl/easy.h"
// TODO: Non-global pls?
static CURLSH *gs_Share;
static LOCK gs_aLocks[CURL_LOCK_DATA_LAST + 1];
static int GetLockIndex(int Data)
{
if(!(0 <= Data && Data < CURL_LOCK_DATA_LAST))
{
Data = CURL_LOCK_DATA_LAST;
}
return Data;
}
static void CurlLock(CURL *pHandle, curl_lock_data Data, curl_lock_access Access, void *pUser) ACQUIRE(gs_aLocks[GetLockIndex(Data)])
{
(void)pHandle;
(void)Access;
(void)pUser;
lock_wait(gs_aLocks[GetLockIndex(Data)]);
}
static void CurlUnlock(CURL *pHandle, curl_lock_data Data, void *pUser) RELEASE(gs_aLocks[GetLockIndex(Data)])
{
(void)pHandle;
(void)pUser;
lock_unlock(gs_aLocks[GetLockIndex(Data)]);
}
bool HttpInit(IStorage *pStorage)
{
if(curl_global_init(CURL_GLOBAL_DEFAULT))
{
return true;
}
gs_Share = curl_share_init();
if(!gs_Share)
{
return true;
}
// print curl version
{
curl_version_info_data *pVersion = curl_version_info(CURLVERSION_NOW);
dbg_msg("http", "libcurl version %s (compiled = " LIBCURL_VERSION ")", pVersion->version);
}
2020-10-26 14:14:07 +00:00
for(auto &Lock : gs_aLocks)
{
2020-10-26 14:14:07 +00:00
Lock = lock_create();
}
curl_share_setopt(gs_Share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
curl_share_setopt(gs_Share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
curl_share_setopt(gs_Share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
curl_share_setopt(gs_Share, CURLSHOPT_LOCKFUNC, CurlLock);
curl_share_setopt(gs_Share, CURLSHOPT_UNLOCKFUNC, CurlUnlock);
#if !defined(CONF_FAMILY_WINDOWS)
// As a multithreaded application we have to tell curl to not install signal
// handlers and instead ignore SIGPIPE from OpenSSL ourselves.
signal(SIGPIPE, SIG_IGN);
#endif
return false;
}
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);
}
CRequest::CRequest(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress) :
m_Timeout(Timeout),
m_Size(0),
m_Progress(0),
m_LogProgress(LogProgress),
m_State(HTTP_QUEUED),
m_Abort(false)
{
str_copy(m_aUrl, pUrl, sizeof(m_aUrl));
}
void CRequest::Run()
{
int FinalState;
if(!BeforeInit())
{
FinalState = HTTP_ERROR;
}
else
{
CURL *pHandle = curl_easy_init();
FinalState = RunImpl(pHandle);
curl_easy_cleanup(pHandle);
}
m_State = OnCompletion(FinalState);
}
int CRequest::RunImpl(CURL *pHandle)
{
if(!pHandle)
{
return HTTP_ERROR;
}
if(g_Config.m_DbgCurl)
{
curl_easy_setopt(pHandle, CURLOPT_VERBOSE, 1L);
}
char aErr[CURL_ERROR_SIZE];
curl_easy_setopt(pHandle, CURLOPT_ERRORBUFFER, aErr);
curl_easy_setopt(pHandle, CURLOPT_CONNECTTIMEOUT_MS, m_Timeout.ConnectTimeoutMs);
curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_LIMIT, m_Timeout.LowSpeedLimit);
curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_TIME, m_Timeout.LowSpeedTime);
curl_easy_setopt(pHandle, CURLOPT_SHARE, gs_Share);
curl_easy_setopt(pHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
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, m_aUrl);
curl_easy_setopt(pHandle, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(pHandle, CURLOPT_USERAGENT, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")");
curl_easy_setopt(pHandle, CURLOPT_ACCEPT_ENCODING, ""); // Use any compression algorithm supported by libcurl.
curl_easy_setopt(pHandle, CURLOPT_WRITEDATA, this);
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);
if(!AfterInit(pHandle))
{
return HTTP_ERROR;
}
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL)
2021-05-28 10:11:59 +00:00
dbg_msg("http", "fetching %s", m_aUrl);
m_State = HTTP_RUNNING;
int Ret = curl_easy_perform(pHandle);
if(Ret != CURLE_OK)
{
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE)
dbg_msg("http", "%s failed. libcurl error: %s", m_aUrl, aErr);
return (Ret == CURLE_ABORTED_BY_CALLBACK) ? HTTP_ABORTED : HTTP_ERROR;
}
else
{
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL)
dbg_msg("http", "task done %s", m_aUrl);
return HTTP_DONE;
}
}
size_t CRequest::WriteCallback(char *pData, size_t Size, size_t Number, void *pUser)
{
return ((CRequest *)pUser)->OnData(pData, Size * Number);
}
int CRequest::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;
}
CHead::CHead(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress) :
Reduce spamminess of http server browser As reported by fokkonaut on Discord. Not sure if we lose relevant information. Previously: [2021-05-28 12:08:54][http]: http https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:55][http]: task done https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:55][http]: http https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task done https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][serverbrowse_http]: found master, url='https://master2.ddnet.tw/ddnet/15/servers.json' time=2437ms [2021-05-28 12:08:57][http]: http https://master4.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task failed. libcurl error: Could not resolve host: master4.ddnet.tw [2021-05-28 12:08:57][http]: http https://master3.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task failed. libcurl error: Could not resolve host: master3.ddnet.tw [2021-05-28 12:08:57][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:58][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:58][serverbrowse_http]: found master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=201ms [2021-05-28 12:08:58][serverbrowse_http]: determined best master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=201ms [2021-05-28 12:08:58][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:58][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json Now: [2021-05-28 12:29:58][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:29:59][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:30:00][serverbrowse_http]: found master, url='https://master2.ddnet.tw/ddnet/15/servers.json' time=799ms [2021-05-28 12:30:00][serverbrowse_http]: found master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=43ms [2021-05-28 12:30:00][serverbrowse_http]: determined best master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=43ms
2021-05-28 10:18:53 +00:00
CRequest(pUrl, Timeout, LogProgress)
Add client-side HTTP server info Summary ======= The idea of this is that clients will not have to ping each server for server infos which takes long, leaks the client's IP address even to servers the user does not join and is a DoS vector of the game servers for attackers. For the Internet, DDNet and KoG tab, the server list is entirely fetched from the master server, filtering out servers that don't belong into the list. The favorites tab is also supposed to work that way, except for servers that are marked as "also ping this server if it's not in the master server list". The LAN tab continues to broadcast the server info packet to find servers in the LAN. How does it work? ================= The client ships with a list of master server list URLs. On first start, the client checks which of these work and selects the fastest one. Querying the server list is a HTTP GET request on that URL. The response is a JSON document that contains server infos, server addresses as URLs and an approximate location. It can also contain a legacy server list which is a list of bare IP addresses similar to the functionality the old master servers provided via UDP. This allows us to backtrack on the larger update if it won't work out. Lost functionality ================== (also known as user-visible changes) Since the client doesn't ping each server in the list anymore, it has no way of knowing its latency to the servers. This is alleviated a bit by providing an approximate location for each server (continent) so the client only has to know its own location for approximating pings.
2018-07-11 20:46:04 +00:00
{
}
CHead::~CHead()
{
}
bool CHead::AfterInit(void *pCurl)
{
CURL *pHandle = pCurl;
curl_easy_setopt(pHandle, CURLOPT_NOBODY, 1L);
return true;
}
CGet::CGet(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress) :
Reduce spamminess of http server browser As reported by fokkonaut on Discord. Not sure if we lose relevant information. Previously: [2021-05-28 12:08:54][http]: http https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:55][http]: task done https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:55][http]: http https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task done https://master2.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][serverbrowse_http]: found master, url='https://master2.ddnet.tw/ddnet/15/servers.json' time=2437ms [2021-05-28 12:08:57][http]: http https://master4.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task failed. libcurl error: Could not resolve host: master4.ddnet.tw [2021-05-28 12:08:57][http]: http https://master3.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task failed. libcurl error: Could not resolve host: master3.ddnet.tw [2021-05-28 12:08:57][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:57][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:58][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:58][serverbrowse_http]: found master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=201ms [2021-05-28 12:08:58][serverbrowse_http]: determined best master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=201ms [2021-05-28 12:08:58][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:08:58][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json Now: [2021-05-28 12:29:58][http]: http https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:29:59][http]: task done https://master1.ddnet.tw/ddnet/15/servers.json [2021-05-28 12:30:00][serverbrowse_http]: found master, url='https://master2.ddnet.tw/ddnet/15/servers.json' time=799ms [2021-05-28 12:30:00][serverbrowse_http]: found master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=43ms [2021-05-28 12:30:00][serverbrowse_http]: determined best master, url='https://master1.ddnet.tw/ddnet/15/servers.json' time=43ms
2021-05-28 10:18:53 +00:00
CRequest(pUrl, Timeout, LogProgress),
m_BufferSize(0),
m_BufferLength(0),
m_pBuffer(NULL)
{
}
CGet::~CGet()
{
m_BufferSize = 0;
m_BufferLength = 0;
free(m_pBuffer);
m_pBuffer = NULL;
}
unsigned char *CGet::Result() const
{
if(State() != HTTP_DONE)
{
return NULL;
}
return m_pBuffer;
}
unsigned char *CGet::TakeResult()
{
unsigned char *pResult = Result();
if(pResult)
{
m_BufferSize = 0;
m_BufferLength = 0;
m_pBuffer = NULL;
}
return pResult;
}
json_value *CGet::ResultJson() const
{
unsigned char *pResult = Result();
if(!pResult)
{
return NULL;
}
return json_parse((char *)pResult, m_BufferLength);
}
size_t CGet::OnData(char *pData, size_t DataSize)
{
if(DataSize == 0)
{
return DataSize;
}
bool Reallocate = false;
if(m_BufferSize == 0)
{
m_BufferSize = 1024;
Reallocate = true;
}
while(m_BufferLength + DataSize > m_BufferSize)
{
m_BufferSize *= 2;
Reallocate = true;
}
if(Reallocate)
{
m_pBuffer = (unsigned char *)realloc(m_pBuffer, m_BufferSize);
}
mem_copy(m_pBuffer + m_BufferLength, pData, DataSize);
m_BufferLength += DataSize;
return DataSize;
}
CGetFile::CGetFile(IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType, CTimeout Timeout, HTTPLOG LogProgress) :
CRequest(pUrl, Timeout, LogProgress),
m_pStorage(pStorage),
m_File(0),
m_StorageType(StorageType)
{
str_copy(m_aDest, pDest, sizeof(m_aDest));
if(m_StorageType == -2)
m_pStorage->GetBinaryPath(m_aDest, m_aDestFull, sizeof(m_aDestFull));
else
m_pStorage->GetCompletePath(m_StorageType, m_aDest, m_aDestFull, sizeof(m_aDestFull));
}
bool CGetFile::BeforeInit()
{
if(fs_makedir_rec_for(m_aDestFull) < 0)
{
dbg_msg("http", "i/o error, cannot create folder for: %s", m_aDestFull);
return false;
}
m_File = io_open(m_aDestFull, IOFLAG_WRITE);
if(!m_File)
{
dbg_msg("http", "i/o error, cannot open file: %s", m_aDest);
return false;
}
return true;
}
size_t CGetFile::OnData(char *pData, size_t DataSize)
{
return io_write(m_File, pData, DataSize);
}
int CGetFile::OnCompletion(int State)
{
if(m_File && io_close(m_File) != 0)
{
2020-10-15 15:48:22 +00:00
dbg_msg("http", "i/o error, cannot close file: %s", m_aDest);
State = HTTP_ERROR;
}
if(State == HTTP_ERROR || State == HTTP_ABORTED)
{
m_pStorage->RemoveFile(m_aDestFull, IStorage::TYPE_ABSOLUTE);
}
return State;
}
CPostJson::CPostJson(const char *pUrl, CTimeout Timeout, const char *pJson) :
CRequest(pUrl, Timeout)
{
str_copy(m_aJson, pJson, sizeof(m_aJson));
}
bool CPostJson::AfterInit(void *pCurl)
{
CURL *pHandle = pCurl;
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);
return true;
}