5069: Some HTTP fixes factored out of the HTTP masterserver PR r=def- a=heinrich5991

CC #5064

Co-authored-by: heinrich5991 <heinrich5991@gmail.com>
This commit is contained in:
bors[bot] 2022-05-15 22:20:11 +00:00 committed by GitHub
commit a041ff8aed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 663 additions and 370 deletions

1
.gitignore vendored
View file

@ -74,6 +74,7 @@ versionsrv
!src/twping
generated
target
/build*

View file

@ -606,7 +606,7 @@ if(CLIENT AND VULKAN)
show_dependency_status("Vulkan" VULKAN)
endif()
if(CLIENT AND NOT(CURL_FOUND))
if(NOT(CURL_FOUND))
message(SEND_ERROR "You must install Curl to compile DDNet")
endif()
if(NOT(PYTHONINTERP_FOUND))
@ -1713,6 +1713,8 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared
filecollection.cpp
filecollection.h
global_uuid_manager.cpp
http.cpp
http.h
huffman.cpp
huffman.h
image_manipulation.cpp
@ -1825,6 +1827,7 @@ set(DEPS ${DEP_JSON} ${DEP_MD5} ${ZLIB_DEP})
# Libraries
set(LIBS
${CRYPTO_LIBRARIES}
${CURL_LIBRARIES}
${SQLite3_LIBRARIES}
${WEBSOCKETS_LIBRARIES}
${ZLIB_LIBRARIES}
@ -1909,8 +1912,6 @@ if(CLIENT)
graphics_defines.h
graphics_threaded.cpp
graphics_threaded.h
http.cpp
http.h
input.cpp
input.h
keynames.h
@ -2067,7 +2068,6 @@ if(CLIENT)
# Libraries
set(LIBS_CLIENT
${LIBS}
${CURL_LIBRARIES}
${FREETYPE_LIBRARIES}
${GLEW_LIBRARIES}
${PNGLITE_LIBRARIES}
@ -2143,7 +2143,6 @@ if(CLIENT)
endif()
target_include_directories(${TARGET_CLIENT} SYSTEM PRIVATE
${CURL_INCLUDE_DIRS}
${FREETYPE_INCLUDE_DIRS}
${GLEW_INCLUDE_DIRS}
${OGG_INCLUDE_DIRS}
@ -2490,8 +2489,6 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
set(TESTS_EXTRA
src/engine/client/blocklist_driver.cpp
src/engine/client/blocklist_driver.h
src/engine/client/http.cpp
src/engine/client/http.h
src/engine/client/serverbrowser.cpp
src/engine/client/serverbrowser.h
src/engine/client/serverbrowser_http.cpp
@ -2523,7 +2520,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
${DEPS}
)
target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${CURL_LIBRARIES} ${MYSQL_LIBRARIES} ${GTEST_LIBRARIES})
target_include_directories(${TARGET_TESTRUNNER} SYSTEM PRIVATE ${CURL_INCLUDE_DIRS} ${GTEST_INCLUDE_DIRS})
target_include_directories(${TARGET_TESTRUNNER} SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS})
list(APPEND TARGETS_OWN ${TARGET_TESTRUNNER})
list(APPEND TARGETS_LINK ${TARGET_TESTRUNNER})
@ -3003,7 +3000,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} SYSTEM PRIVATE ${SQLite3_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
target_include_directories(${target} SYSTEM PRIVATE ${CURL_INCLUDE_DIRS} ${SQLite3_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
target_compile_definitions(${target} PRIVATE GLEW_STATIC)
if(CRYPTO_FOUND)
target_compile_definitions(${target} PRIVATE CONF_OPENSSL)

@ -1 +1 @@
Subproject commit 44a5db01b7f3ffa984fd291bb945684c2980207d
Subproject commit d0b31d605c5b75e61fd7ec451e339a12945bf1a6

View file

@ -3012,6 +3012,152 @@ int str_hex_decode(void *dst, int dst_size, const char *src)
return 0;
}
void str_base64(char *dst, int dst_size, const void *data_raw, int data_size)
{
static const char DIGITS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const unsigned char *data = (const unsigned char *)data_raw;
unsigned value = 0;
int num_bits = 0;
int i = 0;
int o = 0;
dst_size -= 1;
dst[dst_size] = 0;
while(true)
{
if(num_bits < 6 && i < data_size)
{
value = (value << 8) | data[i];
num_bits += 8;
i += 1;
}
if(o == dst_size)
{
return;
}
if(num_bits > 0)
{
unsigned padded;
if(num_bits >= 6)
{
padded = (value >> (num_bits - 6)) & 0x3f;
}
else
{
padded = (value << (6 - num_bits)) & 0x3f;
}
dst[o] = DIGITS[padded];
num_bits -= 6;
o += 1;
}
else if(o % 4 != 0)
{
dst[o] = '=';
o += 1;
}
else
{
dst[o] = 0;
return;
}
}
}
static int base64_digit_value(char digit)
{
if('A' <= digit && digit <= 'Z')
{
return digit - 'A';
}
else if('a' <= digit && digit <= 'z')
{
return digit - 'a' + 26;
}
else if('0' <= digit && digit <= '9')
{
return digit - '0' + 52;
}
else if(digit == '+')
{
return 62;
}
else if(digit == '/')
{
return 63;
}
return -1;
}
int str_base64_decode(void *dst_raw, int dst_size, const char *data)
{
unsigned char *dst = (unsigned char *)dst_raw;
int data_len = str_length(data);
int i;
int o = 0;
if(data_len % 4 != 0)
{
return -3;
}
if(data_len / 4 * 3 > dst_size)
{
// Output buffer too small.
return -2;
}
for(i = 0; i < data_len; i += 4)
{
int num_output_bytes = 3;
char copy[4];
int d[4];
int value;
int b;
mem_copy(copy, data + i, sizeof(copy));
if(i == data_len - 4)
{
if(copy[3] == '=')
{
copy[3] = 'A';
num_output_bytes = 2;
if(copy[2] == '=')
{
copy[2] = 'A';
num_output_bytes = 1;
}
}
}
d[0] = base64_digit_value(copy[0]);
d[1] = base64_digit_value(copy[1]);
d[2] = base64_digit_value(copy[2]);
d[3] = base64_digit_value(copy[3]);
if(d[0] == -1 || d[1] == -1 || d[2] == -1 || d[3] == -1)
{
// Invalid digit.
return -1;
}
value = (d[0] << 18) | (d[1] << 12) | (d[2] << 6) | d[3];
for(b = 0; b < 3; b++)
{
unsigned char byte_value = (value >> (16 - 8 * b)) & 0xff;
if(b < num_output_bytes)
{
dst[o] = byte_value;
o += 1;
}
else
{
if(byte_value != 0)
{
// Padding not zeroed.
return -2;
}
}
}
}
return o;
}
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"

View file

@ -84,7 +84,11 @@ bool dbg_assert_has_failed();
*
* @see dbg_assert
*/
void dbg_break();
#if defined(__cplusplus)
[[noreturn]]
#endif
void
dbg_break();
/**
* Prints a debug message.
@ -1592,6 +1596,41 @@ void str_hex(char *dst, int dst_size, const void *data, int data_size);
- The contents of the buffer is only valid on success
*/
int str_hex_decode(void *dst, int dst_size, const char *src);
/*
Function: str_base64
Takes a datablock and generates the base64 encoding of it.
Parameters:
dst - Buffer to fill with base64 data
dst_size - Size of the buffer
data - Data to turn into base64
data - Size of the data
Remarks:
- The destination buffer will be zero-terminated
*/
void str_base64(char *dst, int dst_size, const void *data, int data_size);
/*
Function: str_base64_decode
Takes a base64 string without any whitespace and correct
padding and returns a byte array.
Parameters:
dst - Buffer for the byte array
dst_size - Size of the buffer
data - String to decode
Returns:
<0 - Error
>= 0 - Success, length of the resulting byte buffer
Remarks:
- The contents of the buffer is only valid on success
*/
int str_base64_decode(void *dst, int dst_size, const char *data);
/*
Function: str_timestamp
Copies a time stamp in the format year-month-day_hour-minute-second to the string.

View file

@ -34,7 +34,6 @@
#include <engine/storage.h>
#include <engine/textrender.h>
#include <engine/client/http.h>
#include <engine/client/notifications.h>
#include <engine/shared/assertion_logger.h>
#include <engine/shared/compression.h>
@ -43,6 +42,7 @@
#include <engine/shared/demo.h>
#include <engine/shared/fifo.h>
#include <engine/shared/filecollection.h>
#include <engine/shared/http.h>
#include <engine/shared/json.h>
#include <engine/shared/network.h>
#include <engine/shared/packer.h>
@ -1721,7 +1721,8 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
bool UseConfigUrl = str_comp(g_Config.m_ClMapDownloadUrl, "https://maps2.ddnet.tw") != 0 || m_aMapDownloadUrl[0] == '\0';
str_format(aUrl, sizeof(aUrl), "%s/%s", UseConfigUrl ? g_Config.m_ClMapDownloadUrl : m_aMapDownloadUrl, aEscaped);
m_pMapdownloadTask = std::make_shared<CGetFile>(Storage(), aUrl, m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE, CTimeout{g_Config.m_ClMapDownloadConnectTimeoutMs, g_Config.m_ClMapDownloadLowSpeedLimit, g_Config.m_ClMapDownloadLowSpeedTime});
m_pMapdownloadTask = HttpGetFile(aUrl, Storage(), m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
m_pMapdownloadTask->Timeout(CTimeout{g_Config.m_ClMapDownloadConnectTimeoutMs, g_Config.m_ClMapDownloadLowSpeedLimit, g_Config.m_ClMapDownloadLowSpeedTime});
Engine()->AddJob(m_pMapdownloadTask);
}
else
@ -4609,7 +4610,9 @@ void CClient::RequestDDNetInfo()
}
// Use ipv4 so we can know the ingame ip addresses of players before they join game servers
m_pDDNetInfoTask = std::make_shared<CGetFile>(Storage(), aUrl, m_aDDNetInfoTmp, IStorage::TYPE_SAVE, CTimeout{10000, 500, 10}, HTTPLOG::ALL, IPRESOLVE::V4);
m_pDDNetInfoTask = HttpGetFile(aUrl, Storage(), m_aDDNetInfoTmp, IStorage::TYPE_SAVE);
m_pDDNetInfoTask->Timeout(CTimeout{10000, 500, 10});
m_pDDNetInfoTask->IpResolve(IPRESOLVE::V4);
Engine()->AddJob(m_pDDNetInfoTask);
}

View file

@ -12,7 +12,6 @@
#include <engine/client/demoedit.h>
#include <engine/client/friends.h>
#include <engine/client/ghost.h>
#include <engine/client/http.h>
#include <engine/client/serverbrowser.h>
#include <engine/client/updater.h>
#include <engine/discord.h>
@ -25,6 +24,7 @@
#include <engine/shared/config.h>
#include <engine/shared/demo.h>
#include <engine/shared/fifo.h>
#include <engine/shared/http.h>
#include <engine/shared/network.h>
#include <engine/sound.h>
#include <engine/steam.h>
@ -180,7 +180,7 @@ class CClient : public IClient, public CDemoPlayer::IListener
char m_aCmdEditMap[IO_MAX_PATH_LENGTH];
// map download
std::shared_ptr<CGetFile> m_pMapdownloadTask;
std::shared_ptr<CHttpRequest> m_pMapdownloadTask;
char m_aMapdownloadFilename[256];
char m_aMapdownloadFilenameTemp[256];
char m_aMapdownloadName[256];
@ -198,7 +198,7 @@ class CClient : public IClient, public CDemoPlayer::IListener
SHA256_DIGEST m_MapDetailsSha256;
char m_aDDNetInfoTmp[64];
std::shared_ptr<CGetFile> m_pDDNetInfoTask;
std::shared_ptr<CHttpRequest> m_pDDNetInfoTask;
// time
CSmoothTime m_GameTime[NUM_DUMMIES];

View file

@ -1,5 +1,7 @@
#include "demoedit.h"
#include <engine/shared/demo.h>
#include <engine/storage.h>
CDemoEdit::CDemoEdit(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, IStorage *pStorage, const char *pDemo, const char *pDst, int StartTick, int EndTick) :
m_SnapshotDelta(*pSnapshotDelta),

View file

@ -1,11 +1,11 @@
#ifndef ENGINE_CLIENT_DEMOEDIT_H
#define ENGINE_CLIENT_DEMOEDIT_H
#include <engine/client/http.h>
#include <engine/shared/demo.h>
#include <engine/shared/jobs.h>
#include <engine/shared/snapshot.h>
#define CONNECTLINK "ddnet:"
class IStorage;
class CDemoEdit : public IJob
{
@ -20,7 +20,7 @@ class CDemoEdit : public IJob
int m_EndTick;
public:
CDemoEdit(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, IStorage *pStorage, const char *pDemo, const char *pDst, int StartTick, int EndTick);
CDemoEdit(const char *pNetVersion, CSnapshotDelta *pSnapshotDelta, IStorage *pStorage, const char *pDemo, const char *pDst, int StartTick, int EndTick);
void Run();
char *Destination() { return m_aDst; }
};

View file

@ -1,158 +0,0 @@
#ifndef ENGINE_CLIENT_HTTP_H
#define ENGINE_CLIENT_HTTP_H
#include <atomic>
#include <engine/kernel.h>
#include <engine/shared/jobs.h>
#include <engine/storage.h>
typedef struct _json_value json_value;
typedef void CURL;
enum
{
HTTP_ERROR = -1,
HTTP_QUEUED,
HTTP_RUNNING,
HTTP_DONE,
HTTP_ABORTED,
};
enum class HTTPLOG
{
NONE,
FAILURE,
ALL,
};
enum class IPRESOLVE
{
WHATEVER,
V4,
V6,
};
struct CTimeout
{
long ConnectTimeoutMs;
long LowSpeedLimit;
long LowSpeedTime;
};
class CRequest : public IJob
{
// Abort the request with an error if `BeforeInit()` or `AfterInit()`
// returns false. Also abort the request if `OnData()` returns
// something other than `DataSize`.
virtual bool BeforeInit() { return true; }
virtual bool AfterInit(void *pCurl) { return true; }
virtual size_t OnData(char *pData, size_t DataSize) = 0;
virtual void OnProgress() {}
char m_aUrl[256];
CTimeout m_Timeout;
std::atomic<double> m_Size;
std::atomic<double> m_Current;
std::atomic<int> m_Progress;
HTTPLOG m_LogProgress;
IPRESOLVE m_IpResolve;
std::atomic<int> m_State;
std::atomic<bool> m_Abort;
static int ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr);
static size_t WriteCallback(char *pData, size_t Size, size_t Number, void *pUser);
void Run();
int RunImpl(CURL *pHandle);
protected:
virtual int OnCompletion(int State) { return State; }
public:
CRequest(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress = HTTPLOG::ALL, IPRESOLVE IpResolve = IPRESOLVE::WHATEVER);
double Current() const { return m_Current.load(std::memory_order_relaxed); }
double Size() const { return m_Size.load(std::memory_order_relaxed); }
int Progress() const { return m_Progress.load(std::memory_order_relaxed); }
int State() const { return m_State; }
void Abort() { m_Abort = true; }
};
class CHead : public CRequest
{
virtual size_t OnData(char *pData, size_t DataSize) { return DataSize; }
virtual bool AfterInit(void *pCurl);
public:
CHead(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress = HTTPLOG::ALL);
~CHead();
};
class CGet : public CRequest
{
virtual size_t OnData(char *pData, size_t DataSize);
size_t m_BufferSize;
size_t m_BufferLength;
unsigned char *m_pBuffer;
public:
CGet(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress = HTTPLOG::ALL);
~CGet();
size_t ResultSize() const
{
if(!Result())
{
return 0;
}
else
{
return m_BufferSize;
}
}
unsigned char *Result() const;
unsigned char *TakeResult();
json_value *ResultJson() const;
};
class CGetFile : public CRequest
{
virtual size_t OnData(char *pData, size_t DataSize);
virtual bool BeforeInit();
IStorage *m_pStorage;
char m_aDestFull[IO_MAX_PATH_LENGTH];
IOHANDLE m_File;
protected:
char m_aDest[IO_MAX_PATH_LENGTH];
int m_StorageType;
virtual int OnCompletion(int State);
public:
CGetFile(IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType = -2, CTimeout Timeout = CTimeout{4000, 500, 5}, HTTPLOG LogProgress = HTTPLOG::ALL, IPRESOLVE IpResolve = IPRESOLVE::WHATEVER);
const char *Dest() const { return m_aDest; }
};
class CPostJson : public CRequest
{
virtual size_t OnData(char *pData, size_t DataSize) { return DataSize; }
virtual bool AfterInit(void *pCurl);
char m_aJson[1024];
public:
CPostJson(const char *pUrl, CTimeout Timeout, const char *pJson);
};
bool HttpInit(IStorage *pStorage);
void EscapeUrl(char *pBuf, int Size, const char *pStr);
#endif // ENGINE_CLIENT_HTTP_H

View file

@ -7,6 +7,7 @@
#include <algorithm>
#include <climits>
#include <vector>
#include <base/hash_ctxt.h>
#include <base/math.h>

View file

@ -3,13 +3,13 @@
#ifndef ENGINE_CLIENT_SERVERBROWSER_H
#define ENGINE_CLIENT_SERVERBROWSER_H
#include <engine/client/http.h>
#include <engine/config.h>
#include <engine/console.h>
#include <engine/external/json-parser/json.h>
#include <engine/masterserver.h>
#include <engine/serverbrowser.h>
#include <engine/shared/config.h>
#include <engine/shared/http.h>
#include <engine/shared/memheap.h>
class IServerBrowserHttp;

View file

@ -1,17 +1,17 @@
#include "serverbrowser_http.h"
#include "http.h"
#include <engine/console.h>
#include <engine/engine.h>
#include <engine/external/json-parser/json.h>
#include <engine/serverbrowser.h>
#include <engine/shared/http.h>
#include <engine/shared/jobs.h>
#include <engine/shared/linereader.h>
#include <engine/shared/serverinfo.h>
#include <engine/storage.h>
#include <memory>
#include <vector>
class CChooseMaster
{
@ -46,8 +46,8 @@ private:
{
LOCK m_Lock;
std::shared_ptr<CData> m_pData;
std::unique_ptr<CHead> m_pHead PT_GUARDED_BY(m_Lock);
std::unique_ptr<CGet> m_pGet PT_GUARDED_BY(m_Lock);
std::unique_ptr<CHttpRequest> m_pHead PT_GUARDED_BY(m_Lock);
std::unique_ptr<CHttpRequest> m_pGet PT_GUARDED_BY(m_Lock);
virtual void Run();
public:
@ -166,9 +166,11 @@ void CChooseMaster::CJob::Run()
{
aTimeMs[i] = -1;
const char *pUrl = m_pData->m_aaUrls[aRandomized[i]];
CHead *pHead = new CHead(pUrl, Timeout, HTTPLOG::FAILURE);
CHttpRequest *pHead = HttpHead(pUrl).release();
pHead->Timeout(Timeout);
pHead->LogProgress(HTTPLOG::FAILURE);
lock_wait(m_Lock);
m_pHead = std::unique_ptr<CHead>(pHead);
m_pHead = std::unique_ptr<CHttpRequest>(pHead);
lock_unlock(m_Lock);
IEngine::RunJobBlocking(pHead);
if(pHead->State() == HTTP_ABORTED)
@ -181,9 +183,11 @@ void CChooseMaster::CJob::Run()
continue;
}
int64_t StartTime = time_get_microseconds();
CGet *pGet = new CGet(pUrl, Timeout, HTTPLOG::FAILURE);
CHttpRequest *pGet = HttpGet(pUrl).release();
pGet->Timeout(Timeout);
pGet->LogProgress(HTTPLOG::FAILURE);
lock_wait(m_Lock);
m_pGet = std::unique_ptr<CGet>(pGet);
m_pGet = std::unique_ptr<CHttpRequest>(pGet);
lock_unlock(m_Lock);
IEngine::RunJobBlocking(pGet);
int Time = (time_get_microseconds() - StartTime) / 1000;
@ -290,7 +294,7 @@ private:
IConsole *m_pConsole;
int m_State = STATE_DONE;
std::shared_ptr<CGet> m_pGetServers;
std::shared_ptr<CHttpRequest> m_pGetServers;
std::unique_ptr<CChooseMaster> m_pChooseMaster;
std::vector<CEntry> m_aServers;
@ -327,9 +331,10 @@ void CServerBrowserHttp::Update()
}
return;
}
m_pGetServers = HttpGet(pBestUrl);
// 10 seconds connection timeout, lower than 8KB/s for 10 seconds to fail.
CTimeout Timeout{10000, 8000, 10};
m_pEngine->AddJob(m_pGetServers = std::make_shared<CGet>(pBestUrl, Timeout));
m_pGetServers->Timeout(CTimeout{10000, 8000, 10});
m_pEngine->AddJob(m_pGetServers);
m_State = STATE_REFRESHING;
}
else if(m_State == STATE_REFRESHING)
@ -339,7 +344,7 @@ void CServerBrowserHttp::Update()
return;
}
m_State = STATE_DONE;
std::shared_ptr<CGet> pGetServers = nullptr;
std::shared_ptr<CHttpRequest> pGetServers = nullptr;
std::swap(m_pGetServers, pGetServers);
bool Success = true;

View file

@ -12,7 +12,7 @@
using std::map;
using std::string;
class CUpdaterFetchTask : public CGetFile
class CUpdaterFetchTask : public CHttpRequest
{
char m_aBuf[256];
char m_aBuf2[256];
@ -44,9 +44,10 @@ static const char *GetUpdaterDestPath(char *pBuf, int BufSize, const char *pFile
}
CUpdaterFetchTask::CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath) :
CGetFile(pUpdater->m_pStorage, GetUpdaterUrl(m_aBuf, sizeof(m_aBuf), pFile), GetUpdaterDestPath(m_aBuf2, sizeof(m_aBuf), pFile, pDestPath), -2, CTimeout{0, 0, 0}),
CHttpRequest(GetUpdaterUrl(m_aBuf, sizeof(m_aBuf), pFile)),
m_pUpdater(pUpdater)
{
WriteToFile(pUpdater->m_pStorage, GetUpdaterDestPath(m_aBuf2, sizeof(m_aBuf2), pFile, pDestPath), -2);
}
void CUpdaterFetchTask::OnProgress()
@ -59,7 +60,7 @@ void CUpdaterFetchTask::OnProgress()
int CUpdaterFetchTask::OnCompletion(int State)
{
State = CGetFile::OnCompletion(State);
State = CHttpRequest::OnCompletion(State);
const char *b = 0;
for(const char *a = Dest(); *a; a++)

View file

@ -1,7 +1,7 @@
#ifndef ENGINE_CLIENT_UPDATER_H
#define ENGINE_CLIENT_UPDATER_H
#include <engine/client/http.h>
#include <engine/shared/http.h>
#include <engine/updater.h>
#include <map>
#include <string>

View file

@ -5,12 +5,13 @@
#include <engine/map.h>
#include <engine/shared/protocol.h>
#include <game/client/ui.h>
#include "kernel.h"
#define DDNET_INFO "ddnet-info.json"
class CUIElement;
class CServerInfo
{
public:

View file

@ -12,8 +12,6 @@
#include <engine/shared/video.h>
#endif
#include <game/generated/protocol.h>
#include "compression.h"
#include "demo.h"
#include "memheap.h"

View file

@ -1,5 +1,6 @@
#include "http.h"
#include <base/math.h>
#include <base/system.h>
#include <engine/engine.h>
#include <engine/external/json-parser/json.h>
@ -12,8 +13,8 @@
#endif
#define WIN32_LEAN_AND_MEAN
#include "curl/curl.h"
#include "curl/easy.h"
#include <curl/curl.h>
#include <curl/easy.h>
// TODO: Non-global pls?
static CURLSH *gs_Share;
@ -86,19 +87,31 @@ void EscapeUrl(char *pBuf, int Size, const char *pStr)
curl_free(pEsc);
}
CRequest::CRequest(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress, IPRESOLVE IpResolve) :
m_Timeout(Timeout),
m_Size(0),
m_Progress(0),
m_LogProgress(LogProgress),
m_IpResolve(IpResolve),
m_State(HTTP_QUEUED),
m_Abort(false)
CHttpRequest::CHttpRequest(const char *pUrl)
{
str_copy(m_aUrl, pUrl, sizeof(m_aUrl));
}
void CRequest::Run()
CHttpRequest::~CHttpRequest()
{
if(!m_WriteToFile)
{
m_BufferSize = 0;
m_BufferLength = 0;
free(m_pBuffer);
m_pBuffer = nullptr;
}
curl_slist_free_all((curl_slist *)m_pHeaders);
m_pHeaders = nullptr;
if(m_pBody)
{
m_BodyLength = 0;
free(m_pBody);
m_pBody = nullptr;
}
}
void CHttpRequest::Run()
{
int FinalState;
if(!BeforeInit())
@ -115,8 +128,29 @@ void CRequest::Run()
m_State = OnCompletion(FinalState);
}
int CRequest::RunImpl(CURL *pHandle)
bool CHttpRequest::BeforeInit()
{
if(m_WriteToFile)
{
if(fs_makedir_rec_for(m_aDestAbsolute) < 0)
{
dbg_msg("http", "i/o error, cannot create folder for: %s", m_aDest);
return false;
}
m_File = io_open(m_aDestAbsolute, IOFLAG_WRITE);
if(!m_File)
{
dbg_msg("http", "i/o error, cannot open file: %s", m_aDest);
return false;
}
}
return true;
}
int CHttpRequest::RunImpl(CURL *pUser)
{
CURL *pHandle = (CURL *)pUser;
if(!pHandle)
{
return HTTP_ERROR;
@ -150,9 +184,10 @@ int CRequest::RunImpl(CURL *pHandle)
curl_easy_setopt(pHandle, CURLOPT_PROGRESSFUNCTION, ProgressCallback);
curl_easy_setopt(pHandle, CURLOPT_IPRESOLVE, m_IpResolve == IPRESOLVE::V4 ? CURL_IPRESOLVE_V4 : m_IpResolve == IPRESOLVE::V6 ? CURL_IPRESOLVE_V6 : CURL_IPRESOLVE_WHATEVER);
if(curl_version_info(CURLVERSION_NOW)->version_num < 0x076800)
if(curl_version_info(CURLVERSION_NOW)->version_num < 0x074400)
{
// Causes crashes, see https://github.com/ddnet/ddnet/issues/4342
// Causes crashes, see https://github.com/ddnet/ddnet/issues/4342.
// No longer a problem in curl 7.68 and above, and 0x44 = 68.
curl_easy_setopt(pHandle, CURLOPT_FORBID_REUSE, 1L);
}
@ -160,10 +195,25 @@ int CRequest::RunImpl(CURL *pHandle)
curl_easy_setopt(pHandle, CURLOPT_CAINFO, "data/cacert.pem");
#endif
if(!AfterInit(pHandle))
switch(m_Type)
{
return HTTP_ERROR;
case REQUEST::GET:
break;
case REQUEST::HEAD:
curl_easy_setopt(pHandle, CURLOPT_NOBODY, 1L);
break;
case REQUEST::POST:
case REQUEST::POST_JSON:
if(m_Type == REQUEST::POST_JSON)
{
Header("Content-Type: application/json");
}
curl_easy_setopt(pHandle, CURLOPT_POSTFIELDS, m_pBody);
curl_easy_setopt(pHandle, CURLOPT_POSTFIELDSIZE, m_BodyLength);
break;
}
curl_easy_setopt(pHandle, CURLOPT_HTTPHEADER, m_pHeaders);
if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL)
dbg_msg("http", "fetching %s", m_aUrl);
@ -183,14 +233,42 @@ int CRequest::RunImpl(CURL *pHandle)
}
}
size_t CRequest::WriteCallback(char *pData, size_t Size, size_t Number, void *pUser)
size_t CHttpRequest::OnData(char *pData, size_t DataSize)
{
return ((CRequest *)pUser)->OnData(pData, Size * Number);
if(!m_WriteToFile)
{
if(DataSize == 0)
{
return DataSize;
}
size_t NewBufferSize = maximum((size_t)1024, m_BufferSize);
while(m_BufferLength + DataSize > NewBufferSize)
{
NewBufferSize *= 2;
}
if(NewBufferSize != m_BufferSize)
{
m_pBuffer = (unsigned char *)realloc(m_pBuffer, NewBufferSize);
m_BufferSize = NewBufferSize;
}
mem_copy(m_pBuffer + m_BufferLength, pData, DataSize);
m_BufferLength += DataSize;
return DataSize;
}
else
{
return io_write(m_File, pData, DataSize);
}
}
int CRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr)
size_t CHttpRequest::WriteCallback(char *pData, size_t Size, size_t Number, void *pUser)
{
CGetFile *pTask = (CGetFile *)pUser;
return ((CHttpRequest *)pUser)->OnData(pData, Size * Number);
}
int CHttpRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr)
{
CHttpRequest *pTask = (CHttpRequest *)pUser;
pTask->m_Current.store(DlCurr, std::memory_order_relaxed);
pTask->m_Size.store(DlTotal, std::memory_order_relaxed);
pTask->m_Progress.store((100 * DlCurr) / (DlTotal ? DlTotal : 1), std::memory_order_relaxed);
@ -198,131 +276,10 @@ int CRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, doubl
return pTask->m_Abort ? -1 : 0;
}
CHead::CHead(const char *pUrl, CTimeout Timeout, HTTPLOG LogProgress) :
CRequest(pUrl, Timeout, LogProgress)
int CHttpRequest::OnCompletion(int State)
{
}
CHead::~CHead() = default;
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) :
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)
if(m_WriteToFile)
{
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, IPRESOLVE IpResolve) :
CRequest(pUrl, Timeout, LogProgress, IpResolve),
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)
{
dbg_msg("http", "i/o error, cannot close file: %s", m_aDest);
@ -331,26 +288,51 @@ int CGetFile::OnCompletion(int State)
if(State == HTTP_ERROR || State == HTTP_ABORTED)
{
m_pStorage->RemoveFile(m_aDestFull, IStorage::TYPE_ABSOLUTE);
fs_remove(m_aDestAbsolute);
}
}
return State;
}
CPostJson::CPostJson(const char *pUrl, CTimeout Timeout, const char *pJson) :
CRequest(pUrl, Timeout)
void CHttpRequest::WriteToFile(IStorage *pStorage, const char *pDest, int StorageType)
{
str_copy(m_aJson, pJson, sizeof(m_aJson));
m_WriteToFile = true;
str_copy(m_aDest, pDest, sizeof(m_aDest));
if(StorageType == -2)
{
pStorage->GetBinaryPath(m_aDest, m_aDestAbsolute, sizeof(m_aDestAbsolute));
}
else
{
pStorage->GetCompletePath(StorageType, m_aDest, m_aDestAbsolute, sizeof(m_aDestAbsolute));
}
}
bool CPostJson::AfterInit(void *pCurl)
void CHttpRequest::Header(const char *pNameColonValue)
{
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;
m_pHeaders = curl_slist_append((curl_slist *)m_pHeaders, pNameColonValue);
}
void CHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const
{
if(m_WriteToFile || State() != HTTP_DONE)
{
*ppResult = nullptr;
*pResultLength = 0;
return;
}
*ppResult = m_pBuffer;
*pResultLength = m_BufferLength;
}
json_value *CHttpRequest::ResultJson() const
{
unsigned char *pResult;
size_t ResultLength;
Result(&pResult, &ResultLength);
if(!pResult)
{
return nullptr;
}
return json_parse((char *)pResult, ResultLength);
}

190
src/engine/shared/http.h Normal file
View file

@ -0,0 +1,190 @@
#ifndef ENGINE_SHARED_HTTP_H
#define ENGINE_SHARED_HTTP_H
#include <atomic>
#include <engine/shared/jobs.h>
typedef struct _json_value json_value;
class IStorage;
enum
{
HTTP_ERROR = -1,
HTTP_QUEUED,
HTTP_RUNNING,
HTTP_DONE,
HTTP_ABORTED,
};
enum class HTTPLOG
{
NONE,
FAILURE,
ALL,
};
enum class IPRESOLVE
{
WHATEVER,
V4,
V6,
};
struct CTimeout
{
long ConnectTimeoutMs;
long LowSpeedLimit;
long LowSpeedTime;
};
class CHttpRequest : public IJob
{
enum class REQUEST
{
GET = 0,
HEAD,
POST,
POST_JSON,
};
char m_aUrl[256] = {0};
void *m_pHeaders = nullptr;
unsigned char *m_pBody = nullptr;
size_t m_BodyLength = 0;
CTimeout m_Timeout = CTimeout{0, 0, 0};
REQUEST m_Type = REQUEST::GET;
bool m_WriteToFile = false;
// If `m_WriteToFile` is false.
size_t m_BufferSize = 0;
size_t m_BufferLength = 0;
unsigned char *m_pBuffer = nullptr;
// If `m_WriteToFile` is true.
IOHANDLE m_File = nullptr;
char m_aDestAbsolute[IO_MAX_PATH_LENGTH] = {0};
char m_aDest[IO_MAX_PATH_LENGTH] = {0};
std::atomic<double> m_Size{0.0};
std::atomic<double> m_Current{0.0};
std::atomic<int> m_Progress{0};
HTTPLOG m_LogProgress = HTTPLOG::ALL;
IPRESOLVE m_IpResolve = IPRESOLVE::WHATEVER;
std::atomic<int> m_State{HTTP_QUEUED};
std::atomic<bool> m_Abort{false};
void Run();
// Abort the request with an error if `BeforeInit()` returns false.
bool BeforeInit();
int RunImpl(void *pUser);
// Abort the request if `OnData()` returns something other than
// `DataSize`.
size_t OnData(char *pData, size_t DataSize);
static int ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr);
static size_t WriteCallback(char *pData, size_t Size, size_t Number, void *pUser);
protected:
virtual void OnProgress() {}
virtual int OnCompletion(int State);
public:
CHttpRequest(const char *pUrl);
~CHttpRequest();
void Timeout(CTimeout Timeout) { m_Timeout = Timeout; }
void LogProgress(HTTPLOG LogProgress) { m_LogProgress = LogProgress; }
void IpResolve(IPRESOLVE IpResolve) { m_IpResolve = IpResolve; }
void WriteToFile(IStorage *pStorage, const char *pDest, int StorageType);
void Head() { m_Type = REQUEST::HEAD; }
void Post(const unsigned char *pData, size_t DataLength)
{
m_Type = REQUEST::POST;
m_BodyLength = DataLength;
m_pBody = (unsigned char *)malloc(DataLength);
mem_copy(m_pBody, pData, DataLength);
}
void PostJson(const char *pJson)
{
m_Type = REQUEST::POST_JSON;
m_BodyLength = str_length(pJson);
m_pBody = (unsigned char *)malloc(m_BodyLength);
mem_copy(m_pBody, pJson, m_BodyLength);
}
void Header(const char *pNameColonValue);
void HeaderString(const char *pName, const char *pValue)
{
char aHeader[256];
str_format(aHeader, sizeof(aHeader), "%s: %s", pName, pValue);
Header(aHeader);
}
void HeaderInt(const char *pName, int Value)
{
char aHeader[256];
str_format(aHeader, sizeof(aHeader), "%s: %d", pName, Value);
Header(aHeader);
}
const char *Dest()
{
if(m_WriteToFile)
{
return m_aDest;
}
else
{
return nullptr;
}
}
double Current() const { return m_Current.load(std::memory_order_relaxed); }
double Size() const { return m_Size.load(std::memory_order_relaxed); }
int Progress() const { return m_Progress.load(std::memory_order_relaxed); }
int State() const { return m_State; }
void Abort() { m_Abort = true; }
void Result(unsigned char **ppResult, size_t *pResultLength) const;
json_value *ResultJson() const;
};
inline std::unique_ptr<CHttpRequest> HttpHead(const char *pUrl)
{
std::unique_ptr<CHttpRequest> pResult = std::unique_ptr<CHttpRequest>(new CHttpRequest(pUrl));
pResult->Head();
return pResult;
}
inline std::unique_ptr<CHttpRequest> HttpGet(const char *pUrl)
{
return std::unique_ptr<CHttpRequest>(new CHttpRequest(pUrl));
}
inline std::unique_ptr<CHttpRequest> HttpGetFile(const char *pUrl, IStorage *pStorage, const char *pOutputFile, int StorageType)
{
std::unique_ptr<CHttpRequest> pResult = HttpGet(pUrl);
pResult->WriteToFile(pStorage, pOutputFile, StorageType);
pResult->Timeout(CTimeout{4000, 500, 5});
return pResult;
}
inline std::unique_ptr<CHttpRequest> HttpPost(const char *pUrl, const unsigned char *pData, size_t DataLength)
{
std::unique_ptr<CHttpRequest> pResult = std::unique_ptr<CHttpRequest>(new CHttpRequest(pUrl));
pResult->Post(pData, DataLength);
return pResult;
}
inline std::unique_ptr<CHttpRequest> HttpPostJson(const char *pUrl, const char *pJson)
{
std::unique_ptr<CHttpRequest> pResult = std::unique_ptr<CHttpRequest>(new CHttpRequest(pUrl));
pResult->PostJson(pJson);
return pResult;
}
bool HttpInit(IStorage *pStorage);
void EscapeUrl(char *pBuf, int Size, const char *pStr);
#endif // ENGINE_SHARED_HTTP_H

View file

@ -1,9 +1,12 @@
#include "serverinfo.h"
#include "json.h"
#include <base/math.h>
#include <engine/external/json-parser/json.h>
#include <engine/serverbrowser.h>
#include <cstdio>
static bool IsAllowedHex(char c)
{
static const char ALLOWED[] = "0123456789abcdefABCDEF";

View file

@ -29,18 +29,22 @@ static bool IsVanillaSkin(const char *pName)
int CSkins::CGetPngFile::OnCompletion(int State)
{
State = CGetFile::OnCompletion(State);
State = CHttpRequest::OnCompletion(State);
if(State != HTTP_ERROR && State != HTTP_ABORTED && !m_pSkins->LoadSkinPNG(m_Info, m_aDest, m_aDest, m_StorageType))
if(State != HTTP_ERROR && State != HTTP_ABORTED && !m_pSkins->LoadSkinPNG(m_Info, Dest(), Dest(), IStorage::TYPE_SAVE))
{
State = HTTP_ERROR;
}
return State;
}
CSkins::CGetPngFile::CGetPngFile(CSkins *pSkins, IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType, CTimeout Timeout, HTTPLOG LogProgress) :
CGetFile(pStorage, pUrl, pDest, StorageType, Timeout, LogProgress), m_pSkins(pSkins)
CSkins::CGetPngFile::CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest) :
CHttpRequest(pUrl),
m_pSkins(pSkins)
{
WriteToFile(pStorage, pDest, IStorage::TYPE_SAVE);
Timeout(CTimeout{0, 0, 0});
LogProgress(HTTPLOG::NONE);
}
struct SSkinScanUser
@ -438,7 +442,7 @@ int CSkins::FindImpl(const char *pName)
str_format(aUrl, sizeof(aUrl), "%s%s.png", g_Config.m_ClSkinDownloadUrl, aEscapedName);
char aBuf[IO_MAX_PATH_LENGTH];
str_format(Skin.m_aPath, sizeof(Skin.m_aPath), "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, sizeof(aBuf), pName));
Skin.m_pTask = std::make_shared<CGetPngFile>(this, Storage(), aUrl, Skin.m_aPath, IStorage::TYPE_SAVE, CTimeout{0, 0, 0}, HTTPLOG::NONE);
Skin.m_pTask = std::make_shared<CGetPngFile>(this, aUrl, Storage(), Skin.m_aPath);
m_pClient->Engine()->AddJob(Skin.m_pTask);
m_aDownloadSkins.add(Skin);
return -1;

View file

@ -5,14 +5,14 @@
#include <base/color.h>
#include <base/tl/sorted_array.h>
#include <base/vmath.h>
#include <engine/client/http.h>
#include <engine/shared/http.h>
#include <game/client/component.h>
#include <game/client/skin.h>
class CSkins : public CComponent
{
public:
class CGetPngFile : public CGetFile
class CGetPngFile : public CHttpRequest
{
CSkins *m_pSkins;
@ -20,7 +20,7 @@ public:
virtual int OnCompletion(int State) override;
public:
CGetPngFile(CSkins *pSkins, IStorage *pStorage, const char *pUrl, const char *pDest, int StorageType = -2, CTimeout Timeout = CTimeout{4000, 500, 5}, HTTPLOG LogProgress = HTTPLOG::ALL);
CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest);
CImageInfo m_Info;
};

View file

@ -221,6 +221,84 @@ TEST(Str, HexDecode)
EXPECT_STREQ(aOut, "ABCD");
}
void StrBase64Str(char *pBuffer, int BufferSize, const char *pString)
{
str_base64(pBuffer, BufferSize, pString, str_length(pString));
}
TEST(Str, Base64)
{
char aBuf[128];
str_base64(aBuf, sizeof(aBuf), "\0", 1);
EXPECT_STREQ(aBuf, "AA==");
str_base64(aBuf, sizeof(aBuf), "\0\0", 2);
EXPECT_STREQ(aBuf, "AAA=");
str_base64(aBuf, sizeof(aBuf), "\0\0\0", 3);
EXPECT_STREQ(aBuf, "AAAA");
StrBase64Str(aBuf, sizeof(aBuf), "");
EXPECT_STREQ(aBuf, "");
// https://en.wikipedia.org/w/index.php?title=Base64&oldid=1033503483#Output_padding
StrBase64Str(aBuf, sizeof(aBuf), "pleasure.");
EXPECT_STREQ(aBuf, "cGxlYXN1cmUu");
StrBase64Str(aBuf, sizeof(aBuf), "leasure.");
EXPECT_STREQ(aBuf, "bGVhc3VyZS4=");
StrBase64Str(aBuf, sizeof(aBuf), "easure.");
EXPECT_STREQ(aBuf, "ZWFzdXJlLg==");
StrBase64Str(aBuf, sizeof(aBuf), "asure.");
EXPECT_STREQ(aBuf, "YXN1cmUu");
StrBase64Str(aBuf, sizeof(aBuf), "sure.");
EXPECT_STREQ(aBuf, "c3VyZS4=");
}
TEST(Str, Base64Decode)
{
char aOut[17];
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), ""), 0);
EXPECT_STREQ(aOut, "XXXXXXXXXXXXXXXX");
// https://en.wikipedia.org/w/index.php?title=Base64&oldid=1033503483#Output_padding
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), "cGxlYXN1cmUu"), 9);
EXPECT_STREQ(aOut, "pleasure.XXXXXXX");
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), "bGVhc3VyZS4="), 8);
EXPECT_STREQ(aOut, "leasure.XXXXXXXX");
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), "ZWFzdXJlLg=="), 7);
EXPECT_STREQ(aOut, "easure.XXXXXXXXX");
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), "YXN1cmUu"), 6);
EXPECT_STREQ(aOut, "asure.XXXXXXXXXX");
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), "c3VyZS4="), 5);
EXPECT_STREQ(aOut, "sure.XXXXXXXXXXX");
str_copy(aOut, "XXXXXXXXXXXXXXXX", sizeof(aOut));
EXPECT_EQ(str_base64_decode(aOut, sizeof(aOut), "////"), 3);
EXPECT_STREQ(aOut, "\xff\xff\xffXXXXXXXXXXXXX");
}
TEST(Str, Base64DecodeError)
{
char aBuf[128];
// Wrong padding.
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "A"), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "AA"), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "AAA"), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "A==="), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "=AAA"), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "===="), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "AAA=AAAA"), 0);
// Invalid characters.
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "----"), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "AAAA "), 0);
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "AAA "), 0);
// Invalid padding values.
EXPECT_LT(str_base64_decode(aBuf, sizeof(aBuf), "//=="), 0);
}
TEST(Str, Tokenize)
{
char aTest[] = "GER,RUS,ZAF,BRA,CAN";