3772: Add client-side HTTP server info r=def- a=heinrich5991

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.
## Checklist

- [x] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [ ] Considered possible null pointers and out of bounds array indexing
- [ ] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: heinrich5991 <heinrich5991@gmail.com>
This commit is contained in:
bors[bot] 2021-05-26 21:00:49 +00:00 committed by GitHub
commit efde7523e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 2627 additions and 491 deletions

View file

@ -1508,6 +1508,7 @@ set_src(ENGINE_INTERFACE GLOB src/engine
server.h
serverbrowser.h
sound.h
sqlite.h
steam.h
storage.h
textrender.h
@ -1571,7 +1572,8 @@ set_src(ENGINE_SHARED GLOB src/engine/shared
protocol_ex_msgs.h
ringbuffer.cpp
ringbuffer.h
serverbrowser.cpp
serverinfo.cpp
serverinfo.h
snapshot.cpp
snapshot.h
storage.cpp
@ -1645,6 +1647,7 @@ set(DEPS ${DEP_JSON} ${DEP_MD5} ${ZLIB_DEP})
# Libraries
set(LIBS
${CRYPTO_LIBRARIES}
${SQLite3_LIBRARIES}
${WEBSOCKETS_LIBRARIES}
${ZLIB_LIBRARIES}
${PLATFORM_LIBS}
@ -1729,8 +1732,13 @@ if(CLIENT)
notifications.h
serverbrowser.cpp
serverbrowser.h
serverbrowser_http.cpp
serverbrowser_http.h
serverbrowser_ping_cache.cpp
serverbrowser_ping_cache.h
sound.cpp
sound.h
sqlite.cpp
steam.cpp
text.cpp
updater.cpp
@ -2079,7 +2087,6 @@ endif()
set(LIBS_SERVER
${LIBS}
${MYSQL_LIBRARIES}
${SQLite3_LIBRARIES}
${TARGET_ANTIBOT}
${MINIUPNPC_LIBRARIES}
# Add pthreads (on non-Windows) at the end, so that other libraries can depend
@ -2205,6 +2212,9 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
netaddr.cpp
packer.cpp
prng.cpp
secure_random.cpp
serverbrowser.cpp
serverinfo.cpp
sorted_array.cpp
str.cpp
strip_path_and_extension.cpp
@ -2218,6 +2228,15 @@ 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
src/engine/client/serverbrowser_http.h
src/engine/client/serverbrowser_ping_cache.cpp
src/engine/client/serverbrowser_ping_cache.h
src/engine/client/sqlite.cpp
src/engine/server/name_ban.cpp
src/engine/server/name_ban.h
src/game/server/teehistorian.cpp
@ -2232,8 +2251,8 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
$<TARGET_OBJECTS:game-shared>
${DEPS}
)
target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${GTEST_LIBRARIES})
target_include_directories(${TARGET_TESTRUNNER} PRIVATE ${GTEST_INCLUDE_DIRS})
target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${CURL_LIBRARIES} ${GTEST_LIBRARIES})
target_include_directories(${TARGET_TESTRUNNER} PRIVATE ${CURL_INCLUDE_DIRS} ${GTEST_INCLUDE_DIRS})
list(APPEND TARGETS_OWN ${TARGET_TESTRUNNER})
list(APPEND TARGETS_LINK ${TARGET_TESTRUNNER})

View file

@ -2205,6 +2205,19 @@ int fs_makedir(const char *path)
#endif
}
int fs_removedir(const char *path)
{
#if defined(CONF_FAMILY_WINDOWS)
if(_rmdir(path) == 0)
return 0;
return -1;
#else
if(rmdir(path) == 0)
return 0;
return -1;
#endif
}
int fs_is_dir(const char *path)
{
#if defined(CONF_FAMILY_WINDOWS)
@ -2283,9 +2296,11 @@ int fs_parent_dir(char *path)
int fs_remove(const char *filename)
{
if(remove(filename) != 0)
return 1;
return 0;
#if defined(CONF_FAMILY_WINDOWS)
return _unlink(filename) != 0;
#else
return unlink(filename) != 0;
#endif
}
int fs_rename(const char *oldname, const char *newname)
@ -3608,6 +3623,34 @@ int secure_rand(void)
return (int)(i % RAND_MAX);
}
// From https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2.
static unsigned int find_next_power_of_two_minus_one(unsigned int n)
{
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 4;
n |= n >> 16;
return n;
}
int secure_rand_below(int below)
{
unsigned int mask = find_next_power_of_two_minus_one(below);
dbg_assert(below > 0, "below must be positive");
while(1)
{
unsigned int n;
secure_random_fill(&n, sizeof(n));
n &= mask;
if((int)n < below)
{
return n;
}
}
}
#if defined(__cplusplus)
}
#endif

View file

@ -1638,6 +1638,21 @@ int fs_listdir_info(const char *dir, FS_LISTDIR_INFO_CALLBACK cb, int type, void
*/
int fs_makedir(const char *path);
/*
Function: fs_removedir
Removes a directory
Parameters:
path - Directory to remove
Returns:
Returns 0 on success. Negative value on failure.
Remarks:
Cannot remove a non-empty directory.
*/
int fs_removedir(const char *path);
/*
Function: fs_makedir_rec_for
Recursively create directories for a file
@ -1724,6 +1739,7 @@ int fs_parent_dir(char *path);
Remarks:
- The strings are treated as zero-terminated strings.
- Returns an error if the path specifies a directory name.
*/
int fs_remove(const char *filename);
@ -2193,6 +2209,16 @@ void secure_random_fill(void *bytes, unsigned length);
*/
int secure_rand(void);
/*
Function: secure_rand_below
Returns a random nonnegative integer below the given number,
with a uniform distribution.
Parameters:
below - Upper limit (exclusive) of integers to return.
*/
int secure_rand_below(int below);
#ifdef __cplusplus
}
#endif

View file

@ -26,7 +26,6 @@
#include <engine/input.h>
#include <engine/keys.h>
#include <engine/map.h>
#include <engine/masterserver.h>
#include <engine/serverbrowser.h>
#include <engine/sound.h>
#include <engine/steam.h>
@ -336,6 +335,12 @@ CClient::CClient() :
m_Points = -1;
m_CurrentServerInfoRequestTime = -1;
m_CurrentServerPingInfoType = -1;
m_CurrentServerPingBasicToken = -1;
m_CurrentServerPingToken = -1;
mem_zero(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid));
m_CurrentServerCurrentPingTime = -1;
m_CurrentServerNextPingTime = -1;
m_CurrentInput[0] = 0;
m_CurrentInput[1] = 0;
@ -656,6 +661,7 @@ void CClient::EnterGame()
OnEnterGame();
ServerInfoRequest(); // fresh one for timeout protection
m_CurrentServerNextPingTime = time_get() + time_freq() / 2;
m_aTimeoutCodeSent[0] = false;
m_aTimeoutCodeSent[1] = false;
}
@ -739,6 +745,7 @@ void CClient::Connect(const char *pAddress, const char *pPassword)
else
str_copy(m_Password, pPassword, sizeof(m_Password));
m_CanReceiveServerCapabilities = true;
// Deregister Rcon commands from last connected server, might not have called
// DisconnectWithReason if the server was shut down
m_RconAuthed[0] = 0;
@ -776,13 +783,18 @@ void CClient::DisconnectWithReason(const char *pReason)
//
m_RconAuthed[0] = 0;
m_CanReceiveServerCapabilities = true;
m_ServerSentCapabilities = false;
m_UseTempRconCommands = 0;
m_pConsole->DeregisterTempAll();
m_NetClient[CLIENT_MAIN].Disconnect(pReason);
SetState(IClient::STATE_OFFLINE);
m_pMap->Unload();
m_CurrentServerPingInfoType = -1;
m_CurrentServerPingBasicToken = -1;
m_CurrentServerPingToken = -1;
mem_zero(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid));
m_CurrentServerCurrentPingTime = -1;
m_CurrentServerNextPingTime = -1;
// disable all downloads
m_MapdownloadChunk = 0;
@ -1251,105 +1263,8 @@ const char *CClient::LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedS
return pError;
}
bool CClient::PlayerScoreNameLess(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1)
{
if(p0.m_Player && !p1.m_Player)
return true;
if(!p0.m_Player && p1.m_Player)
return false;
int Score0 = p0.m_Score;
int Score1 = p1.m_Score;
if(Score0 == -9999)
Score0 = INT_MIN;
if(Score1 == -9999)
Score1 = INT_MIN;
if(Score0 > Score1)
return true;
if(Score0 < Score1)
return false;
return str_comp_nocase(p0.m_aName, p1.m_aName) < 0;
}
void CClient::ProcessConnlessPacket(CNetChunk *pPacket)
{
//server count from master server
if(pPacket->m_DataSize == (int)sizeof(SERVERBROWSE_COUNT) + 2 && mem_comp(pPacket->m_pData, SERVERBROWSE_COUNT, sizeof(SERVERBROWSE_COUNT)) == 0)
{
unsigned char *pP = (unsigned char *)pPacket->m_pData;
pP += sizeof(SERVERBROWSE_COUNT);
int ServerCount = ((*pP) << 8) | *(pP + 1);
int ServerID = -1;
for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; i++)
{
if(!m_pMasterServer->IsValid(i))
continue;
NETADDR tmp = m_pMasterServer->GetAddr(i);
if(net_addr_comp(&pPacket->m_Address, &tmp) == 0)
{
ServerID = i;
break;
}
}
if(ServerCount > -1 && ServerID != -1)
{
m_pMasterServer->SetCount(ServerID, ServerCount);
if(g_Config.m_Debug)
dbg_msg("mastercount", "server %d got %d servers", ServerID, ServerCount);
}
}
// server list from master server
if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_LIST) &&
mem_comp(pPacket->m_pData, SERVERBROWSE_LIST, sizeof(SERVERBROWSE_LIST)) == 0)
{
// check for valid master server address
bool Valid = false;
for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; ++i)
{
if(m_pMasterServer->IsValid(i))
{
NETADDR Addr = m_pMasterServer->GetAddr(i);
if(net_addr_comp(&pPacket->m_Address, &Addr) == 0)
{
Valid = true;
break;
}
}
}
if(!Valid)
return;
int Size = pPacket->m_DataSize - sizeof(SERVERBROWSE_LIST);
int Num = Size / sizeof(CMastersrvAddr);
CMastersrvAddr *pAddrs = (CMastersrvAddr *)((char *)pPacket->m_pData + sizeof(SERVERBROWSE_LIST));
for(int i = 0; i < Num; i++)
{
NETADDR Addr;
static unsigned char s_IPV4Mapping[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
// copy address
if(!mem_comp(s_IPV4Mapping, pAddrs[i].m_aIp, sizeof(s_IPV4Mapping)))
{
mem_zero(&Addr, sizeof(Addr));
Addr.type = NETTYPE_IPV4;
Addr.ip[0] = pAddrs[i].m_aIp[12];
Addr.ip[1] = pAddrs[i].m_aIp[13];
Addr.ip[2] = pAddrs[i].m_aIp[14];
Addr.ip[3] = pAddrs[i].m_aIp[15];
}
else
{
Addr.type = NETTYPE_IPV6;
mem_copy(Addr.ip, pAddrs[i].m_aIp, sizeof(Addr.ip));
}
Addr.port = (pAddrs[i].m_aPort[0] << 8) | pAddrs[i].m_aPort[1];
m_ServerBrowser.Set(Addr, IServerBrowser::SET_MASTER_ADD, -1, 0x0);
}
}
// server info
if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_INFO))
{
@ -1530,19 +1445,9 @@ void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData,
if(!Up.Error() || IgnoreError)
{
std::sort(Info.m_aClients, Info.m_aClients + Info.m_NumReceivedClients, PlayerScoreNameLess);
if(!DuplicatedPacket && (!pEntry || !pEntry->m_GotInfo || SavedType >= pEntry->m_Info.m_Type))
{
m_ServerBrowser.Set(*pFrom, IServerBrowser::SET_TOKEN, Token, &Info);
pEntry = m_ServerBrowser.Find(*pFrom);
if(SavedType == SERVERINFO_VANILLA && Is64Player(&Info) && pEntry)
{
pEntry->m_Request64Legacy = true;
// Force a quick update.
m_ServerBrowser.RequestImpl64(pEntry->m_Addr, pEntry);
}
}
// Player info is irrelevant for the client (while connected),
@ -1561,6 +1466,30 @@ void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData,
m_CurrentServerInfo.m_NetAddr = m_ServerAddress;
m_CurrentServerInfoRequestTime = -1;
}
bool ValidPong = false;
if(!m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && SavedType >= m_CurrentServerPingInfoType)
{
if(RawType == SERVERINFO_VANILLA)
{
ValidPong = Token == m_CurrentServerPingBasicToken;
}
else if(RawType == SERVERINFO_EXTENDED)
{
ValidPong = Token == m_CurrentServerPingToken;
}
}
if(ValidPong)
{
int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
m_ServerBrowser.SetCurrentServerPing(m_ServerAddress, LatencyMs);
m_CurrentServerPingInfoType = SavedType;
m_CurrentServerCurrentPingTime = -1;
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "got pong from current server, latency=%dms", LatencyMs);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
}
}
}
@ -1609,6 +1538,7 @@ static CServerCapabilities GetServerCapabilities(int Version, int Flags)
}
Result.m_ChatTimeoutCode = DDNet;
Result.m_AnyPlayerFlag = DDNet;
Result.m_PingEx = false;
if(Version >= 1)
{
Result.m_ChatTimeoutCode = Flags & SERVERCAPFLAG_CHATTIMEOUTCODE;
@ -1617,6 +1547,10 @@ static CServerCapabilities GetServerCapabilities(int Version, int Flags)
{
Result.m_AnyPlayerFlag = Flags & SERVERCAPFLAG_ANYPLAYERFLAG;
}
if(Version >= 3)
{
Result.m_PingEx = Flags & SERVERCAPFLAG_PINGEX;
}
return Result;
}
@ -1814,6 +1748,35 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket)
CMsgPacker Msg(NETMSG_PING_REPLY, true);
SendMsg(&Msg, 0);
}
else if(Msg == NETMSG_PINGEX)
{
CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID));
if(Unpacker.Error())
{
return;
}
CMsgPacker Msg(NETMSG_PONGEX, true);
Msg.AddRaw(pID, sizeof(*pID));
SendMsg(&Msg, MSGFLAG_FLUSH);
}
else if(Msg == NETMSG_PONGEX)
{
CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID));
if(Unpacker.Error())
{
return;
}
if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pID == m_CurrentServerPingUuid)
{
int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
m_ServerBrowser.SetCurrentServerPing(m_ServerAddress, LatencyMs);
m_CurrentServerCurrentPingTime = -1;
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "got pong from current server, latency=%dms", LatencyMs);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
}
}
else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD)
{
if(!g_Config.m_ClDummy)
@ -2842,9 +2805,36 @@ void CClient::Update()
m_CurrentServerInfoRequestTime >= 0 &&
time_get() > m_CurrentServerInfoRequestTime)
{
m_ServerBrowser.RequestCurrentServer(m_ServerAddress);
m_ServerBrowser.RequestCurrentServer(m_ServerAddress, nullptr, nullptr);
m_CurrentServerInfoRequestTime = time_get() + time_freq() * 2;
}
// periodically ping server
if(State() == IClient::STATE_ONLINE &&
m_CurrentServerNextPingTime >= 0 &&
time_get() > m_CurrentServerNextPingTime)
{
int64 Now = time_get();
int64 Freq = time_freq();
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "pinging current server%s", !m_ServerCapabilities.m_PingEx ? ", using fallback via server info" : "");
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
m_CurrentServerPingUuid = RandomUuid();
if(!m_ServerCapabilities.m_PingEx)
{
m_ServerBrowser.RequestCurrentServer(m_ServerAddress, &m_CurrentServerPingBasicToken, &m_CurrentServerPingToken);
}
else
{
CMsgPacker Msg(NETMSG_PINGEX, true);
Msg.AddRaw(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid));
SendMsg(&Msg, MSGFLAG_FLUSH);
}
m_CurrentServerCurrentPingTime = Now;
m_CurrentServerNextPingTime = Now + 600 * Freq; // ping every 10 minutes
}
}
m_LastDummy = (bool)g_Config.m_ClDummy;
@ -2930,9 +2920,6 @@ void CClient::Update()
}
}
// update the maser server registry
MasterServer()->Update();
// update the server browser
m_ServerBrowser.Update(m_ResortServerBrowser);
m_ResortServerBrowser = false;
@ -2983,7 +2970,6 @@ void CClient::InitInterfaces()
m_pGameClient = Kernel()->RequestInterface<IGameClient>();
m_pInput = Kernel()->RequestInterface<IEngineInput>();
m_pMap = Kernel()->RequestInterface<IEngineMap>();
m_pMasterServer = Kernel()->RequestInterface<IEngineMasterServer>();
m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
m_pConfig = m_pConfigManager->Values();
#if defined(CONF_AUTOUPDATE)
@ -3099,9 +3085,6 @@ void CClient::Run()
// init the input
Input()->Init();
// start refreshing addresses while we load
MasterServer()->RefreshAddresses(m_NetClient[CLIENT_MAIN].NetType());
// init the editor
m_pEditor->Init();
@ -3654,8 +3637,18 @@ void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
NETADDR Addr;
if(net_addr_from_str(&Addr, pResult->GetString(0)) == 0)
if(net_addr_from_str(&Addr, pResult->GetString(0)) != 0)
{
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "invalid address '%s'", pResult->GetString(0));
pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
return;
}
pSelf->m_ServerBrowser.AddFavorite(Addr);
if(pResult->NumArguments() > 1 && str_find(pResult->GetString(1), "allow_ping"))
{
pSelf->m_ServerBrowser.FavoriteAllowPing(Addr, true);
}
}
void CClient::Con_RemoveFavorite(IConsole::IResult *pResult, void *pUserData)
@ -4201,7 +4194,7 @@ void CClient::RegisterCommands()
m_pConsole->Register("record", "?r[file]", CFGFLAG_CLIENT, Con_Record, this, "Record to the file");
m_pConsole->Register("stoprecord", "", CFGFLAG_CLIENT, Con_StopRecord, this, "Stop recording");
m_pConsole->Register("add_demomarker", "", CFGFLAG_CLIENT, Con_AddDemoMarker, this, "Add demo timeline marker");
m_pConsole->Register("add_favorite", "r[host|ip]", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite");
m_pConsole->Register("add_favorite", "s[host|ip] ?s['allow_ping']", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite");
m_pConsole->Register("remove_favorite", "r[host|ip]", CFGFLAG_CLIENT, Con_RemoveFavorite, this, "Remove a server from favorites");
m_pConsole->Register("demo_slice_start", "", CFGFLAG_CLIENT, Con_DemoSliceBegin, this, "");
m_pConsole->Register("demo_slice_end", "", CFGFLAG_CLIENT, Con_DemoSliceEnd, this, "");
@ -4307,7 +4300,7 @@ int main(int argc, const char **argv) // ignore_convention
pClient->RegisterInterfaces();
// create the components
IEngine *pEngine = CreateEngine("DDNet", Silent, 1);
IEngine *pEngine = CreateEngine("DDNet", Silent, 2);
IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT);
IStorage *pStorage = CreateStorage("Teeworlds", IStorage::STORAGETYPE_CLIENT, argc, argv); // ignore_convention
IConfigManager *pConfigManager = CreateConfigManager();
@ -4315,7 +4308,6 @@ int main(int argc, const char **argv) // ignore_convention
IEngineInput *pEngineInput = CreateEngineInput();
IEngineTextRender *pEngineTextRender = CreateEngineTextRender();
IEngineMap *pEngineMap = CreateEngineMap();
IEngineMasterServer *pEngineMasterServer = CreateEngineMasterServer();
IDiscord *pDiscord = CreateDiscord();
ISteam *pSteam = CreateSteam();
@ -4344,9 +4336,6 @@ int main(int argc, const char **argv) // ignore_convention
RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineMap); // IEngineMap
RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IMap *>(pEngineMap), false);
RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineMasterServer); // IEngineMasterServer
RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IMasterServer *>(pEngineMasterServer), false);
RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateEditor(), false);
RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateGameClient(), false);
RegisterFail = RegisterFail || !pKernel->RegisterInterface(pStorage);
@ -4365,8 +4354,6 @@ int main(int argc, const char **argv) // ignore_convention
pEngine->Init();
pConfigManager->Init();
pConsole->Init();
pEngineMasterServer->Init();
pEngineMasterServer->Load();
// register all console commands
pClient->RegisterCommands();

View file

@ -80,6 +80,7 @@ class CServerCapabilities
public:
bool m_ChatTimeoutCode;
bool m_AnyPlayerFlag;
bool m_PingEx;
};
class CClient : public IClient, public CDemoPlayer::IListener
@ -99,7 +100,6 @@ class CClient : public IClient, public CDemoPlayer::IListener
IUpdater *m_pUpdater;
IDiscord *m_pDiscord;
ISteam *m_pSteam;
IEngineMasterServer *m_pMasterServer;
enum
{
@ -243,6 +243,13 @@ class CClient : public IClient, public CDemoPlayer::IListener
class CServerInfo m_CurrentServerInfo;
int64 m_CurrentServerInfoRequestTime; // >= 0 should request, == -1 got info
int m_CurrentServerPingInfoType;
int m_CurrentServerPingBasicToken;
int m_CurrentServerPingToken;
CUuid m_CurrentServerPingUuid;
int64 m_CurrentServerCurrentPingTime; // >= 0 request running
int64 m_CurrentServerNextPingTime; // >= 0 should request
// version info
struct CVersionInfo
{
@ -276,7 +283,6 @@ public:
IEngineInput *Input() { return m_pInput; }
IEngineSound *Sound() { return m_pSound; }
IGameClient *GameClient() { return m_pGameClient; }
IEngineMasterServer *MasterServer() { return m_pMasterServer; }
IConfigManager *ConfigManager() { return m_pConfigManager; }
CConfig *Config() { return m_pConfig; }
IStorage *Storage() { return m_pStorage; }
@ -361,8 +367,6 @@ public:
const char *LoadMap(const char *pName, const char *pFilename, SHA256_DIGEST *pWantedSha256, unsigned WantedCrc);
const char *LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedSha256, int WantedCrc);
static bool PlayerScoreNameLess(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1);
void ProcessConnlessPacket(CNetChunk *pPacket);
void ProcessServerInfo(int Type, NETADDR *pFrom, const void *pData, int DataSize);
void ProcessServerPacket(CNetChunk *pPacket);

View file

@ -186,6 +186,22 @@ int CRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, doubl
return pTask->m_Abort ? -1 : 0;
}
CHead::CHead(const char *pUrl, CTimeout Timeout) :
CRequest(pUrl, Timeout)
{
}
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) :
CRequest(pUrl, Timeout),
m_BufferSize(0),

View file

@ -66,6 +66,16 @@ public:
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);
~CHead();
};
class CGet : public CRequest
{
virtual size_t OnData(char *pData, size_t DataSize);

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,18 @@
#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/memheap.h>
class IServerBrowserHttp;
class IServerBrowserPingCache;
class CServerBrowser : public IServerBrowser
{
public:
@ -18,6 +23,7 @@ public:
public:
NETADDR m_Addr;
int64 m_RequestTime;
bool m_RequestIgnoreInfo;
int m_GotInfo;
bool m_Request64Legacy;
CServerInfo m_Info;
@ -79,7 +85,7 @@ public:
// interface functions
void Refresh(int Type);
bool IsRefreshing() const;
bool IsRefreshingMasters() const;
bool IsGettingServerlist() const;
int LoadingProgression() const;
int NumServers() const { return m_NumServers; }
@ -97,8 +103,11 @@ public:
int NumSortedServers() const { return m_NumSortedServers; }
const CServerInfo *SortedGet(int Index) const;
bool GotInfo(const NETADDR &Addr) const;
bool IsFavorite(const NETADDR &Addr) const;
bool IsFavoritePingAllowed(const NETADDR &Addr) const;
void AddFavorite(const NETADDR &Addr);
void FavoriteAllowPing(const NETADDR &Addr, bool AllowPing);
void RemoveFavorite(const NETADDR &Addr);
void LoadDDNetRanks();
@ -123,7 +132,8 @@ public:
//
void Update(bool ForceResort);
void Set(const NETADDR &Addr, int Type, int Token, const CServerInfo *pInfo);
void RequestCurrentServer(const NETADDR &Addr) const;
void RequestCurrentServer(const NETADDR &Addr, int *pBasicToken, int *pToken) const;
void SetCurrentServerPing(const NETADDR &Addr, int Ping);
void SetBaseInfo(class CNetClient *pClient, const char *pNetVersion);
@ -134,19 +144,27 @@ public:
private:
CNetClient *m_pNetClient;
IMasterServer *m_pMasterServer;
class IConsole *m_pConsole;
class IEngine *m_pEngine;
class IFriends *m_pFriends;
class IStorage *m_pStorage;
char m_aNetVersion[128];
bool m_RefreshingHttp = false;
IServerBrowserHttp *m_pHttp = nullptr;
IServerBrowserPingCache *m_pPingCache = nullptr;
const char *m_pHttpPrevBestUrl = nullptr;
CHeap m_ServerlistHeap;
CServerEntry **m_ppServerlist;
int *m_pSortedServerlist;
NETADDR m_aFavoriteServers[MAX_FAVORITES];
bool m_aFavoriteServersAllowPing[MAX_FAVORITES];
int m_NumFavoriteServers;
CNetwork m_aNetworks[NUM_NETWORKS];
int m_OwnLocation = CServerInfo::LOC_UNKNOWN;
json_value *m_pDDNetInfo;
@ -155,13 +173,10 @@ private:
CServerEntry *m_pFirstReqServer; // request list
CServerEntry *m_pLastReqServer;
int m_NumRequests;
int m_MasterServerCount;
//used instead of g_Config.br_max_requests to get more servers
int m_CurrentMaxRequests;
int m_LastPacketTick;
int m_NeedRefresh;
int m_NumSortedServers;
@ -180,6 +195,8 @@ private:
bool m_SortOnNextUpdate;
int FindFavorite(const NETADDR &Addr) const;
int GenerateToken(const NETADDR &Addr) const;
static int GetBasicToken(int Token);
static int GetExtraToken(int Token);
@ -198,13 +215,18 @@ private:
void Sort();
int SortHash() const;
void UpdateFromHttp();
CServerEntry *Add(const NETADDR &Addr);
void RemoveRequest(CServerEntry *pEntry);
void RequestImpl(const NETADDR &Addr, CServerEntry *pEntry) const;
void RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken) const;
void RegisterCommands();
static void Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData);
void SetInfo(CServerEntry *pEntry, const CServerInfo &Info);
void SetLatency(const NETADDR Addr, int Latency);
static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData);
};

View file

@ -0,0 +1,474 @@
#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/linereader.h>
#include <engine/shared/serverinfo.h>
#include <engine/storage.h>
#include <memory>
class CChooseMaster
{
public:
typedef bool (*VALIDATOR)(json_value *pJson);
enum
{
MAX_URLS = 16,
};
CChooseMaster(IEngine *pEngine, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex);
virtual ~CChooseMaster() {}
bool GetBestUrl(const char **pBestUrl) const;
void Reset();
bool IsRefreshing() const { return m_pJob && m_pJob->Status() != IJob::STATE_DONE; }
void Refresh();
private:
int GetBestIndex() const;
class CData
{
public:
std::atomic_int m_BestIndex{-1};
// Constant after construction.
VALIDATOR m_pfnValidator;
int m_NumUrls;
char m_aaUrls[MAX_URLS][256];
};
class CJob : public IJob
{
std::shared_ptr<CData> m_pData;
virtual void Run();
public:
CJob(std::shared_ptr<CData> pData) :
m_pData(std::move(pData)) {}
virtual ~CJob() {}
};
IEngine *m_pEngine;
int m_PreviousBestIndex;
std::shared_ptr<CData> m_pData;
std::shared_ptr<CJob> m_pJob;
};
CChooseMaster::CChooseMaster(IEngine *pEngine, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex) :
m_pEngine(pEngine),
m_PreviousBestIndex(PreviousBestIndex)
{
dbg_assert(NumUrls >= 0, "no master URLs");
dbg_assert(NumUrls <= MAX_URLS, "too many master URLs");
dbg_assert(PreviousBestIndex >= -1, "previous best index negative and not -1");
dbg_assert(PreviousBestIndex < NumUrls, "previous best index too high");
m_pData = std::make_shared<CData>();
m_pData->m_pfnValidator = pfnValidator;
m_pData->m_NumUrls = NumUrls;
for(int i = 0; i < m_pData->m_NumUrls; i++)
{
str_copy(m_pData->m_aaUrls[i], ppUrls[i], sizeof(m_pData->m_aaUrls[i]));
}
}
int CChooseMaster::GetBestIndex() const
{
int BestIndex = m_pData->m_BestIndex.load();
if(BestIndex >= 0)
{
return BestIndex;
}
else
{
return m_PreviousBestIndex;
}
}
bool CChooseMaster::GetBestUrl(const char **ppBestUrl) const
{
int Index = GetBestIndex();
if(Index < 0)
{
*ppBestUrl = nullptr;
return true;
}
*ppBestUrl = m_pData->m_aaUrls[Index];
return false;
}
void CChooseMaster::Reset()
{
m_PreviousBestIndex = -1;
m_pData->m_BestIndex.store(-1);
}
void CChooseMaster::Refresh()
{
m_pEngine->AddJob(m_pJob = std::make_shared<CJob>(m_pData));
}
void CChooseMaster::CJob::Run()
{
// Check masters in a random order.
int aRandomized[MAX_URLS] = {0};
for(int i = 0; i < m_pData->m_NumUrls; i++)
{
aRandomized[i] = i;
}
// https://en.wikipedia.org/w/index.php?title=Fisher%E2%80%93Yates_shuffle&oldid=1002922479#The_modern_algorithm
// The equivalent version.
for(int i = 0; i <= m_pData->m_NumUrls - 2; i++)
{
int j = i + secure_rand_below(m_pData->m_NumUrls - i);
std::swap(aRandomized[i], aRandomized[j]);
}
// Do a HEAD request to ensure that a connection is established and
// then do a GET request to check how fast we can get the server list.
//
// 10 seconds connection timeout, lower than 8KB/s for 10 seconds to
// fail.
CTimeout Timeout{10000, 8000, 10};
int aTimeMs[MAX_URLS];
for(int i = 0; i < m_pData->m_NumUrls; i++)
{
aTimeMs[i] = -1;
const char *pUrl = m_pData->m_aaUrls[aRandomized[i]];
CHead Head(pUrl, Timeout);
IEngine::RunJobBlocking(&Head);
if(Head.State() != HTTP_DONE)
{
continue;
}
int64 StartTime = time_get();
CGet Get(pUrl, Timeout);
IEngine::RunJobBlocking(&Get);
int Time = (time_get() - StartTime) * 1000 / time_freq();
if(Get.State() != HTTP_DONE)
{
continue;
}
json_value *pJson = Get.ResultJson();
if(!pJson)
{
continue;
}
bool ParseFailure = m_pData->m_pfnValidator(pJson);
json_value_free(pJson);
if(ParseFailure)
{
continue;
}
dbg_msg("serverbrowse_http", "found master, url='%s' time=%dms", pUrl, Time);
aTimeMs[i] = Time;
}
// Determine index of the minimum time.
int BestIndex = -1;
int BestTime = 0;
for(int i = 0; i < m_pData->m_NumUrls; i++)
{
if(aTimeMs[i] < 0)
{
continue;
}
if(BestIndex == -1 || aTimeMs[i] < BestTime)
{
BestTime = aTimeMs[i];
BestIndex = aRandomized[i];
}
}
if(BestIndex == -1)
{
dbg_msg("serverbrowse_http", "WARNING: no usable masters found");
return;
}
dbg_msg("serverbrowse_http", "determined best master, url='%s' time=%dms", m_pData->m_aaUrls[BestIndex], BestTime);
m_pData->m_BestIndex.store(BestIndex);
}
class CServerBrowserHttp : public IServerBrowserHttp
{
public:
CServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, const char **ppUrls, int NumUrls, int PreviousBestIndex);
virtual ~CServerBrowserHttp() {}
void Update();
bool IsRefreshing() { return m_State != STATE_DONE; }
void Refresh();
bool GetBestUrl(const char **pBestUrl) const { return m_pChooseMaster->GetBestUrl(pBestUrl); }
int NumServers() const
{
return m_aServers.size();
}
const NETADDR &ServerAddress(int Index) const
{
return m_aServers[Index].m_Addr;
}
void Server(int Index, NETADDR *pAddr, CServerInfo *pInfo) const
{
const CEntry &Entry = m_aServers[Index];
*pAddr = Entry.m_Addr;
*pInfo = Entry.m_Info;
}
int NumLegacyServers() const
{
return m_aLegacyServers.size();
}
const NETADDR &LegacyServer(int Index) const
{
return m_aLegacyServers[Index];
}
private:
enum
{
STATE_DONE,
STATE_WANTREFRESH,
STATE_REFRESHING,
};
class CEntry
{
public:
NETADDR m_Addr;
CServerInfo m_Info;
};
static bool Validate(json_value *pJson);
static bool Parse(json_value *pJson, std::vector<CEntry> *paServers, std::vector<NETADDR> *paLegacyServers);
IEngine *m_pEngine;
IConsole *m_pConsole;
int m_State = STATE_DONE;
std::shared_ptr<CGet> m_pGetServers;
std::unique_ptr<CChooseMaster> m_pChooseMaster;
std::vector<CEntry> m_aServers;
std::vector<NETADDR> m_aLegacyServers;
};
CServerBrowserHttp::CServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, const char **ppUrls, int NumUrls, int PreviousBestIndex) :
m_pEngine(pEngine),
m_pConsole(pConsole),
m_pChooseMaster(new CChooseMaster(pEngine, Validate, ppUrls, NumUrls, PreviousBestIndex))
{
m_pChooseMaster->Refresh();
}
void CServerBrowserHttp::Update()
{
if(m_State == STATE_WANTREFRESH)
{
const char *pBestUrl;
if(m_pChooseMaster->GetBestUrl(&pBestUrl))
{
if(!m_pChooseMaster->IsRefreshing())
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_http", "no working serverlist URL found");
m_State = STATE_DONE;
}
return;
}
m_pEngine->AddJob(m_pGetServers = std::make_shared<CGet>(pBestUrl, CTimeout{0, 0, 0}));
m_State = STATE_REFRESHING;
}
else if(m_State == STATE_REFRESHING)
{
if(m_pGetServers->State() == HTTP_QUEUED || m_pGetServers->State() == HTTP_RUNNING)
{
return;
}
m_State = STATE_DONE;
std::shared_ptr<CGet> pGetServers = nullptr;
std::swap(m_pGetServers, pGetServers);
bool Success = true;
json_value *pJson = pGetServers->ResultJson();
Success = Success && pJson;
Success = Success && !Parse(pJson, &m_aServers, &m_aLegacyServers);
json_value_free(pJson);
if(!Success)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_http", "failed getting serverlist, trying to find best URL");
m_pChooseMaster->Reset();
m_pChooseMaster->Refresh();
}
}
}
void CServerBrowserHttp::Refresh()
{
if(m_State == STATE_WANTREFRESH)
{
m_pChooseMaster->Refresh();
}
m_State = STATE_WANTREFRESH;
Update();
}
bool ServerbrowserParseUrl(NETADDR *pOut, const char *pUrl)
{
char aHost[128];
const char *pRest = str_startswith(pUrl, "tw-0.6+udp://");
if(!pRest)
{
return true;
}
int Length = str_length(pRest);
int Start = 0;
int End = Length;
for(int i = 0; i < Length; i++)
{
if(pRest[i] == '@')
{
if(Start != 0)
{
// Two at signs.
return true;
}
Start = i + 1;
}
else if(pRest[i] == '/' || pRest[i] == '?' || pRest[i] == '#')
{
End = i;
break;
}
}
str_truncate(aHost, sizeof(aHost), pRest + Start, End - Start);
if(net_addr_from_str(pOut, aHost))
{
return true;
}
return false;
}
bool CServerBrowserHttp::Validate(json_value *pJson)
{
std::vector<CEntry> aServers;
std::vector<NETADDR> aLegacyServers;
return Parse(pJson, &aServers, &aLegacyServers);
}
bool CServerBrowserHttp::Parse(json_value *pJson, std::vector<CEntry> *paServers, std::vector<NETADDR> *paLegacyServers)
{
std::vector<CEntry> aServers;
std::vector<NETADDR> aLegacyServers;
const json_value &Json = *pJson;
const json_value &Servers = Json["servers"];
const json_value &LegacyServers = Json["servers_legacy"];
if(Servers.type != json_array || (LegacyServers.type != json_array && LegacyServers.type != json_none))
{
return true;
}
for(unsigned int i = 0; i < Servers.u.array.length; i++)
{
const json_value &Server = Servers[i];
const json_value &Addresses = Server["addresses"];
const json_value &Info = Server["info"];
const json_value &Location = Server["location"];
int ParsedLocation = CServerInfo::LOC_UNKNOWN;
CServerInfo2 ParsedInfo;
if(Addresses.type != json_array || (Location.type != json_string && Location.type != json_none))
{
return true;
}
if(Location.type == json_string)
{
if(CServerInfo::ParseLocation(&ParsedLocation, Location))
{
return true;
}
}
if(CServerInfo2::FromJson(&ParsedInfo, &Info))
{
//dbg_msg("dbg/serverbrowser", "skipped due to info, i=%d", i);
// Only skip the current server on parsing
// failure; the server info is "user input" by
// the game server and can be set to arbitrary
// values.
continue;
}
CServerInfo SetInfo = ParsedInfo;
SetInfo.m_Location = ParsedLocation;
for(unsigned int a = 0; a < Addresses.u.array.length; a++)
{
const json_value &Address = Addresses[a];
if(Address.type != json_string)
{
return true;
}
// TODO: Address address handling :P
NETADDR ParsedAddr;
if(ServerbrowserParseUrl(&ParsedAddr, Addresses[a]))
{
//dbg_msg("dbg/serverbrowser", "unknown address, i=%d a=%d", i, a);
// Skip unknown addresses.
continue;
}
aServers.push_back({ParsedAddr, SetInfo});
}
}
if(LegacyServers.type == json_array)
{
for(unsigned int i = 0; i < LegacyServers.u.array.length; i++)
{
const json_value &Address = LegacyServers[i];
NETADDR ParsedAddr;
if(Address.type != json_string || net_addr_from_str(&ParsedAddr, Address))
{
return true;
}
aLegacyServers.push_back(ParsedAddr);
}
}
*paServers = aServers;
*paLegacyServers = aLegacyServers;
return false;
}
static const char *DEFAULT_SERVERLIST_URLS[] = {
"https://master1.ddnet.tw/ddnet/15/servers.json",
"https://master2.ddnet.tw/ddnet/15/servers.json",
"https://master3.ddnet.tw/ddnet/15/servers.json",
"https://master4.ddnet.tw/ddnet/15/servers.json",
};
IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, IStorage *pStorage, const char *pPreviousBestUrl)
{
char aaUrls[CChooseMaster::MAX_URLS][256];
const char *apUrls[CChooseMaster::MAX_URLS] = {0};
const char **ppUrls = apUrls;
int NumUrls = 0;
IOHANDLE File = pStorage->OpenFile("serverlist_urls.cfg", IOFLAG_READ, IStorage::TYPE_ALL);
if(File)
{
CLineReader Lines;
Lines.Init(File);
while(NumUrls < CChooseMaster::MAX_URLS)
{
const char *pLine = Lines.Get();
if(!pLine)
{
break;
}
str_copy(aaUrls[NumUrls], pLine, sizeof(aaUrls[NumUrls]));
apUrls[NumUrls] = aaUrls[NumUrls];
NumUrls += 1;
}
}
if(NumUrls == 0)
{
ppUrls = DEFAULT_SERVERLIST_URLS;
NumUrls = sizeof(DEFAULT_SERVERLIST_URLS) / sizeof(DEFAULT_SERVERLIST_URLS[0]);
}
int PreviousBestIndex = -1;
for(int i = 0; i < NumUrls; i++)
{
if(str_comp(ppUrls[i], pPreviousBestUrl) == 0)
{
PreviousBestIndex = i;
break;
}
}
return new CServerBrowserHttp(pEngine, pConsole, ppUrls, NumUrls, PreviousBestIndex);
}

View file

@ -0,0 +1,30 @@
#ifndef ENGINE_CLIENT_SERVERBROWSER_HTTP_H
#define ENGINE_CLIENT_SERVERBROWSER_HTTP_H
#include <base/system.h>
class CServerInfo;
class IConsole;
class IEngine;
class IStorage;
class IServerBrowserHttp
{
public:
virtual ~IServerBrowserHttp() {}
virtual void Update() = 0;
virtual bool IsRefreshing() = 0;
virtual void Refresh() = 0;
virtual bool GetBestUrl(const char **pBestUrl) const = 0;
virtual int NumServers() const = 0;
virtual const NETADDR &ServerAddress(int Index) const = 0;
virtual void Server(int Index, NETADDR *pAddr, CServerInfo *pInfo) const = 0;
virtual int NumLegacyServers() const = 0;
virtual const NETADDR &LegacyServer(int Index) const = 0;
};
IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, IStorage *pStorage, const char *pPreviousBestUrl);
#endif // ENGINE_CLIENT_SERVERBROWSER_HTTP_H

View file

@ -0,0 +1,207 @@
#include "serverbrowser_ping_cache.h"
#include <engine/console.h>
#include <engine/sqlite.h>
#include <sqlite3.h>
#include <algorithm>
#include <stdio.h>
#include <vector>
class CServerBrowserPingCache : public IServerBrowserPingCache
{
public:
CServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage);
virtual ~CServerBrowserPingCache() {}
void Load();
void CachePing(NETADDR Addr, int Ping);
void GetPingCache(const CEntry **ppEntries, int *pNumEntries);
private:
IConsole *m_pConsole;
CSqlite m_pDisk;
CSqliteStmt m_pLoadStmt;
CSqliteStmt m_pStoreStmt;
std::vector<CEntry> m_aEntries;
std::vector<CEntry> m_aNewEntries;
};
CServerBrowserPingCache::CServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage) :
m_pConsole(pConsole)
{
m_pDisk = SqliteOpen(pConsole, pStorage, "cache.sqlite3");
if(!m_pDisk)
{
pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to open cache.sqlite3");
return;
}
sqlite3 *pSqlite = m_pDisk.get();
static const char TABLE[] = "CREATE TABLE IF NOT EXISTS server_pings (ip_address TEXT PRIMARY KEY NOT NULL, ping INTEGER NOT NULL, utc_timestamp TEXT NOT NULL)";
if(SQLITE_HANDLE_ERROR(sqlite3_exec(pSqlite, TABLE, nullptr, nullptr, nullptr)))
{
m_pDisk = nullptr;
pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to create server_pings table");
return;
}
m_pLoadStmt = SqlitePrepare(pConsole, pSqlite, "SELECT ip_address, ping FROM server_pings");
m_pStoreStmt = SqlitePrepare(pConsole, pSqlite, "INSERT OR REPLACE INTO server_pings (ip_address, ping, utc_timestamp) VALUES (?, ?, datetime('now'))");
}
void CServerBrowserPingCache::Load()
{
if(m_pDisk)
{
int PrevNewEntriesSize = m_aNewEntries.size();
sqlite3 *pSqlite = m_pDisk.get();
IConsole *pConsole = m_pConsole;
bool Error = false;
bool WarnedForBadAddress = false;
Error = Error || !m_pLoadStmt;
while(!Error)
{
int StepResult = SQLITE_HANDLE_ERROR(sqlite3_step(m_pLoadStmt.get()));
if(StepResult == SQLITE_DONE)
{
break;
}
else if(StepResult == SQLITE_ROW)
{
const char *pIpAddress = (const char *)sqlite3_column_text(m_pLoadStmt.get(), 0);
int Ping = sqlite3_column_int(m_pLoadStmt.get(), 1);
NETADDR Addr;
if(net_addr_from_str(&Addr, pIpAddress))
{
if(!WarnedForBadAddress)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "invalid address: %s", pIpAddress);
pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", aBuf);
WarnedForBadAddress = true;
}
continue;
}
m_aNewEntries.push_back(CEntry{Addr, Ping});
}
else
{
Error = true;
}
}
if(Error)
{
pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to load ping cache");
m_aNewEntries.resize(PrevNewEntriesSize);
}
}
}
void CServerBrowserPingCache::CachePing(NETADDR Addr, int Ping)
{
Addr.port = 0;
m_aNewEntries.push_back(CEntry{Addr, Ping});
if(m_pDisk)
{
sqlite3 *pSqlite = m_pDisk.get();
IConsole *pConsole = m_pConsole;
char aAddr[NETADDR_MAXSTRSIZE];
net_addr_str(&Addr, aAddr, sizeof(aAddr), false);
bool Error = false;
Error = Error || !m_pStoreStmt;
Error = Error || SQLITE_HANDLE_ERROR(sqlite3_reset(m_pStoreStmt.get())) != SQLITE_OK;
Error = Error || SQLITE_HANDLE_ERROR(sqlite3_bind_text(m_pStoreStmt.get(), 1, aAddr, -1, SQLITE_STATIC)) != SQLITE_OK;
Error = Error || SQLITE_HANDLE_ERROR(sqlite3_bind_int(m_pStoreStmt.get(), 2, Ping)) != SQLITE_OK;
Error = Error || SQLITE_HANDLE_ERROR(sqlite3_step(m_pStoreStmt.get())) != SQLITE_DONE;
if(Error)
{
pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_ping_cache", "failed to store ping");
}
}
}
void CServerBrowserPingCache::GetPingCache(const CEntry **ppEntries, int *pNumEntries)
{
if(!m_aNewEntries.empty())
{
class CAddrComparer
{
public:
bool operator()(const CEntry &a, const CEntry &b)
{
return net_addr_comp(&a.m_Addr, &b.m_Addr) < 0;
}
};
std::vector<CEntry> aOldEntries;
std::swap(m_aEntries, aOldEntries);
// Remove duplicates, keeping newer ones.
std::stable_sort(m_aNewEntries.begin(), m_aNewEntries.end(), CAddrComparer());
{
unsigned To = 0;
for(unsigned int From = 0; From < m_aNewEntries.size(); From++)
{
if(To < From)
{
m_aNewEntries[To] = m_aNewEntries[From];
}
if(From + 1 >= m_aNewEntries.size() ||
net_addr_comp(&m_aNewEntries[From].m_Addr, &m_aNewEntries[From + 1].m_Addr) != 0)
{
To++;
}
}
m_aNewEntries.resize(To);
}
// Only keep the new entries where there are duplicates.
m_aEntries.reserve(m_aNewEntries.size() + aOldEntries.size());
{
unsigned i = 0;
unsigned j = 0;
while(i < aOldEntries.size() && j < m_aNewEntries.size())
{
int Cmp = net_addr_comp(&aOldEntries[i].m_Addr, &m_aNewEntries[j].m_Addr);
if(Cmp != 0)
{
if(Cmp < 0)
{
m_aEntries.push_back(aOldEntries[i]);
i++;
}
else
{
m_aEntries.push_back(m_aNewEntries[j]);
j++;
}
}
else
{
// Ignore the old element if we have both.
i++;
}
}
// Add the remaining elements.
for(; i < aOldEntries.size(); i++)
{
m_aEntries.push_back(aOldEntries[i]);
}
for(; j < m_aNewEntries.size(); j++)
{
m_aEntries.push_back(m_aNewEntries[j]);
}
}
m_aNewEntries.clear();
}
*ppEntries = &m_aEntries[0];
*pNumEntries = m_aEntries.size();
}
IServerBrowserPingCache *CreateServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage)
{
return new CServerBrowserPingCache(pConsole, pStorage);
}

View file

@ -0,0 +1,29 @@
#ifndef ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H
#define ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H
#include <base/system.h>
class IConsole;
class IStorage;
class IServerBrowserPingCache
{
public:
class CEntry
{
public:
NETADDR m_Addr;
int m_Ping;
};
virtual ~IServerBrowserPingCache() {}
virtual void Load() = 0;
virtual void CachePing(NETADDR Addr, int Ping) = 0;
// The returned list is sorted by address, the addresses don't have a
// port.
virtual void GetPingCache(const CEntry **ppEntries, int *pNumEntries) = 0;
};
IServerBrowserPingCache *CreateServerBrowserPingCache(IConsole *pConsole, IStorage *pStorage);
#endif // ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H

View file

@ -0,0 +1,60 @@
#include <base/system.h>
#include <engine/console.h>
#include <engine/sqlite.h>
#include <sqlite3.h>
void CSqliteDeleter::operator()(sqlite3 *pSqlite)
{
sqlite3_close(pSqlite);
}
void CSqliteStmtDeleter::operator()(sqlite3_stmt *pStmt)
{
sqlite3_finalize(pStmt);
}
int SqliteHandleError(IConsole *pConsole, int Error, sqlite3 *pSqlite, const char *pContext)
{
if(Error != SQLITE_OK && Error != SQLITE_DONE && Error != SQLITE_ROW)
{
char aBuf[512];
str_format(aBuf, sizeof(aBuf), "%s at %s", sqlite3_errmsg(pSqlite), pContext);
pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "sqlite3", aBuf);
}
return Error;
}
CSqlite SqliteOpen(IConsole *pConsole, IStorage *pStorage, const char *pPath)
{
char aFullPath[MAX_PATH_LENGTH];
pStorage->GetCompletePath(IStorage::TYPE_SAVE, pPath, aFullPath, sizeof(aFullPath));
sqlite3 *pSqlite = nullptr;
bool ErrorOpening = SQLITE_HANDLE_ERROR(sqlite3_open(aFullPath, &pSqlite)) != SQLITE_OK;
// Even on error, the database is initialized and needs to be freed.
// Except on allocation failure, but then it'll be nullptr which is
// also fine.
CSqlite pResult{pSqlite};
if(ErrorOpening)
{
return nullptr;
}
bool Error = false;
Error = Error || SQLITE_HANDLE_ERROR(sqlite3_exec(pSqlite, "PRAGMA journal_mode = WAL", nullptr, nullptr, nullptr));
Error = Error || SQLITE_HANDLE_ERROR(sqlite3_exec(pSqlite, "PRAGMA synchronous = NORMAL", nullptr, nullptr, nullptr));
if(Error)
{
return nullptr;
}
return pResult;
}
CSqliteStmt SqlitePrepare(IConsole *pConsole, sqlite3 *pSqlite, const char *pStatement)
{
sqlite3_stmt *pTemp;
if(SQLITE_HANDLE_ERROR(sqlite3_prepare_v2(pSqlite, pStatement, -1, &pTemp, nullptr)))
{
return nullptr;
}
return CSqliteStmt{pTemp};
}

View file

@ -32,8 +32,10 @@ public:
virtual void Init() = 0;
virtual void InitLogfile() = 0;
virtual void AddJob(std::shared_ptr<IJob> pJob) = 0;
static void RunJobBlocking(IJob *pJob);
};
extern IEngine *CreateEngine(const char *pAppname, bool Silent, int Jobs);
extern IEngine *CreateTestEngine(const char *pAppname, int Jobs);
#endif

View file

@ -226,6 +226,12 @@ typedef struct _json_value
};
}
/* DDNet additions */
inline operator int () const
{
return (json_int_t) *this;
}
inline operator bool () const
{
if (type != json_boolean)

View file

@ -1138,7 +1138,7 @@ void CServer::SendCapabilities(int ClientID)
{
CMsgPacker Msg(NETMSG_CAPABILITIES, true);
Msg.AddInt(SERVERCAP_CURVERSION); // version
Msg.AddInt(SERVERCAPFLAG_DDNET | SERVERCAPFLAG_CHATTIMEOUTCODE | SERVERCAPFLAG_ANYPLAYERFLAG); // flags
Msg.AddInt(SERVERCAPFLAG_DDNET | SERVERCAPFLAG_CHATTIMEOUTCODE | SERVERCAPFLAG_ANYPLAYERFLAG | SERVERCAPFLAG_PINGEX); // flags
SendMsg(&Msg, MSGFLAG_VITAL, ClientID);
}
@ -1699,6 +1699,17 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket)
CMsgPacker Msg(NETMSG_PING_REPLY, true);
SendMsg(&Msg, 0, ClientID);
}
else if(Msg == NETMSG_PINGEX)
{
CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID));
if(Unpacker.Error())
{
return;
}
CMsgPacker Msg(NETMSG_PONGEX, true);
Msg.AddRaw(pID, sizeof(*pID));
SendMsg(&Msg, MSGFLAG_FLUSH, ClientID);
}
else
{
if(g_Config.m_Debug)

View file

@ -11,15 +11,26 @@
#define DDNET_INFO "ddnet-info.json"
/*
Structure: CServerInfo
*/
class CServerInfo
{
public:
/*
Structure: CInfoClient
*/
enum
{
LOC_UNKNOWN = 0,
LOC_AFRICA,
LOC_ASIA,
LOC_AUSTRALIA,
LOC_EUROPE,
LOC_NORTH_AMERICA,
LOC_SOUTH_AMERICA,
// Special case China because it has an exceptionally bad
// connection to the outside due to the Great Firewall of
// China:
// https://en.wikipedia.org/w/index.php?title=Great_Firewall&oldid=1019589632
LOC_CHINA,
NUM_LOCS,
};
class CClient
{
public:
@ -50,6 +61,8 @@ public:
int m_Flags;
bool m_Favorite;
bool m_Official;
int m_Location;
bool m_LatencyIsEstimated;
int m_Latency; // in ms
int m_HasRank;
char m_aGameType[16];
@ -63,6 +76,9 @@ public:
mutable int m_NumFilteredPlayers;
mutable CUIElement *m_pUIElement;
static int EstimateLatency(int Loc1, int Loc2);
static bool ParseLocation(int *pResult, const char *pString);
};
bool IsVanilla(const CServerInfo *pInfo);
@ -114,6 +130,7 @@ public:
SET_DDNET_ADD,
SET_KOG_ADD,
SET_TOKEN,
SET_HTTPINFO,
NETWORK_DDNET = 0,
NETWORK_KOG = 1,
@ -121,8 +138,8 @@ public:
};
virtual void Refresh(int Type) = 0;
virtual bool IsGettingServerlist() const = 0;
virtual bool IsRefreshing() const = 0;
virtual bool IsRefreshingMasters() const = 0;
virtual int LoadingProgression() const = 0;
virtual int NumServers() const = 0;
@ -133,8 +150,11 @@ public:
virtual int NumSortedServers() const = 0;
virtual const CServerInfo *SortedGet(int Index) const = 0;
virtual bool GotInfo(const NETADDR &Addr) const = 0;
virtual bool IsFavorite(const NETADDR &Addr) const = 0;
virtual bool IsFavoritePingAllowed(const NETADDR &Addr) const = 0;
virtual void AddFavorite(const NETADDR &Addr) = 0;
virtual void FavoriteAllowPing(const NETADDR &Addr, bool AllowPing) = 0;
virtual void RemoveFavorite(const NETADDR &Addr) = 0;
virtual int NumCountries(int Network) = 0;

View file

@ -63,6 +63,8 @@ MACRO_CONFIG_INT(BrFilterUnfinishedMap, br_filter_unfinished_map, 0, 0, 1, CFGFL
MACRO_CONFIG_STR(BrFilterExcludeCountries, br_filter_exclude_countries, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out DDNet servers by country")
MACRO_CONFIG_STR(BrFilterExcludeTypes, br_filter_exclude_types, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out DDNet servers by type (mod)")
MACRO_CONFIG_INT(BrIndicateFinished, br_indicate_finished, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show whether you have finished a DDNet map (transmits your player name to info2.ddnet.tw/info)")
MACRO_CONFIG_STR(BrLocation, br_location, 16, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Override location for ping estimation, available: auto, af, as, as:cn, eu, na, oc, sa (Automatic, Africa, Asia, China, Europe, North America, Oceania/Australia, South America")
MACRO_CONFIG_STR(BrCachedBestServerinfoUrl, br_cached_best_serverinfo_url, 256, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Do not set this variable, instead create a serverlist_urls.cfg next to settings_ddnet.cfg to specify all possible serverlist URLs")
MACRO_CONFIG_STR(BrFilterExcludeCountriesKoG, br_filter_exclude_countries_kog, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out kog servers by country")
MACRO_CONFIG_STR(BrFilterExcludeTypesKoG, br_filter_exclude_types_kog, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out kog servers by type (mod)")

View file

@ -53,7 +53,9 @@ public:
}
}
CEngine(const char *pAppname, bool Silent, int Jobs)
CEngine(bool Test, const char *pAppname, bool Silent, int Jobs)
{
if(!Test)
{
if(!Silent)
dbg_logger_stdout();
@ -72,6 +74,7 @@ public:
// init the network
net_init();
CNetBase::Init();
}
m_JobPool.Init(Jobs);
@ -104,4 +107,10 @@ public:
}
};
IEngine *CreateEngine(const char *pAppname, bool Silent, int Jobs) { return new CEngine(pAppname, Silent, Jobs); }
void IEngine::RunJobBlocking(IJob *pJob)
{
CJobPool::RunBlocking(pJob);
}
IEngine *CreateEngine(const char *pAppname, bool Silent, int Jobs) { return new CEngine(false, pAppname, Silent, Jobs); }
IEngine *CreateTestEngine(const char *pAppname, int Jobs) { return new CEngine(true, pAppname, true, Jobs); }

View file

@ -75,9 +75,7 @@ void CJobPool::WorkerThread(void *pUser)
// do the job if we have one
if(pJob)
{
pJob->m_Status = IJob::STATE_RUNNING;
pJob->Run();
pJob->m_Status = IJob::STATE_DONE;
RunBlocking(pJob.get());
}
}
}
@ -104,3 +102,10 @@ void CJobPool::Add(std::shared_ptr<IJob> pJob)
lock_unlock(m_Lock);
sphore_signal(&m_Semaphore);
}
void CJobPool::RunBlocking(IJob *pJob)
{
pJob->m_Status = IJob::STATE_RUNNING;
pJob->Run();
pJob->m_Status = IJob::STATE_DONE;
}

View file

@ -59,5 +59,6 @@ public:
void Init(int NumThreads);
void Add(std::shared_ptr<IJob> pJob);
static void RunBlocking(IJob *pJob);
};
#endif

View file

@ -20,10 +20,11 @@ enum
UNPACKMESSAGE_OK,
UNPACKMESSAGE_ANSWER,
SERVERCAP_CURVERSION = 2,
SERVERCAP_CURVERSION = 3,
SERVERCAPFLAG_DDNET = 1 << 0,
SERVERCAPFLAG_CHATTIMEOUTCODE = 1 << 1,
SERVERCAPFLAG_ANYPLAYERFLAG = 1 << 2,
SERVERCAPFLAG_PINGEX = 1 << 3,
};
void RegisterUuids(class CUuidManager *pManager);

View file

@ -27,3 +27,5 @@ UUID(NETMSG_RCONTYPE, "rcon-type@ddnet.tw")
UUID(NETMSG_MAP_DETAILS, "map-details@ddnet.tw")
UUID(NETMSG_CAPABILITIES, "capabilities@ddnet.tw")
UUID(NETMSG_CLIENTVER, "clientver@ddnet.tw")
UUID(NETMSG_PINGEX, "ping@ddnet.tw")
UUID(NETMSG_PONGEX, "pong@ddnet.tw")

View file

@ -1,72 +0,0 @@
#include <base/system.h>
#include <engine/serverbrowser.h>
// gametypes
bool IsVanilla(const CServerInfo *pInfo)
{
return !str_comp(pInfo->m_aGameType, "DM") || !str_comp(pInfo->m_aGameType, "TDM") || !str_comp(pInfo->m_aGameType, "CTF");
}
bool IsCatch(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "catch");
}
bool IsInsta(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "idm") || str_find_nocase(pInfo->m_aGameType, "itdm") || str_find_nocase(pInfo->m_aGameType, "ictf");
}
bool IsFNG(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "fng");
}
bool IsRace(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "race") || IsFastCap(pInfo) || IsDDRace(pInfo);
}
bool IsFastCap(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "fastcap");
}
bool IsDDRace(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "ddrace") || str_find_nocase(pInfo->m_aGameType, "mkrace") || IsDDNet(pInfo);
}
bool IsBlockInfectionZ(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "blockz") ||
str_find_nocase(pInfo->m_aGameType, "infectionz");
}
bool IsBlockWorlds(const CServerInfo *pInfo)
{
return (str_comp_nocase_num(pInfo->m_aGameType, "bw ", 4) == 0) || (str_comp_nocase(pInfo->m_aGameType, "bw") == 0);
}
bool IsCity(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "city");
}
bool IsDDNet(const CServerInfo *pInfo)
{
return str_find_nocase(pInfo->m_aGameType, "ddracenet") || str_find_nocase(pInfo->m_aGameType, "ddnet") || IsBlockInfectionZ(pInfo);
}
// other
bool Is64Player(const CServerInfo *pInfo)
{
return str_find(pInfo->m_aGameType, "64") || str_find(pInfo->m_aName, "64") || IsDDNet(pInfo) || IsBlockInfectionZ(pInfo) || IsBlockWorlds(pInfo);
}
bool IsPlus(const CServerInfo *pInfo)
{
return str_find(pInfo->m_aGameType, "+");
}

View file

@ -0,0 +1,256 @@
#include "serverinfo.h"
#include "json.h"
#include <engine/external/json-parser/json.h>
#include <engine/serverbrowser.h>
static bool IsAllowedHex(char c)
{
static const char ALLOWED[] = "0123456789abcdefABCDEF";
for(int i = 0; i < (int)sizeof(ALLOWED) - 1; i++)
{
if(c == ALLOWED[i])
{
return true;
}
}
return false;
}
bool ParseCrc(unsigned int *pResult, const char *pString)
{
if(str_length(pString) != 8)
{
return true;
}
for(int i = 0; i < 8; i++)
{
if(!IsAllowedHex(pString[i]))
{
return true;
}
}
return sscanf(pString, "%08x", pResult) != 1;
}
bool CServerInfo2::FromJson(CServerInfo2 *pOut, const json_value *pJson)
{
bool Result = FromJsonRaw(pOut, pJson);
if(Result)
{
return Result;
}
return pOut->Validate();
}
bool CServerInfo2::Validate() const
{
bool Error = false;
Error = Error || m_MaxClients < m_MaxPlayers;
Error = Error || m_NumClients < m_NumPlayers;
Error = Error || m_MaxClients < m_NumClients;
Error = Error || m_MaxPlayers < m_NumPlayers;
return Error;
}
bool CServerInfo2::FromJsonRaw(CServerInfo2 *pOut, const json_value *pJson)
{
/*
TODO: What to do if we have more players than we can store?
TODO: What to do on incomplete infos?
*/
mem_zero(pOut, sizeof(*pOut));
bool Error;
const json_value &ServerInfo = *pJson;
const json_value &MaxClients = ServerInfo["max_clients"];
const json_value &MaxPlayers = ServerInfo["max_players"];
const json_value &Passworded = ServerInfo["passworded"];
const json_value &GameType = ServerInfo["game_type"];
const json_value &Name = ServerInfo["name"];
const json_value &MapName = ServerInfo["map"]["name"];
//const json_value &MapCrc = ServerInfo["map"]["crc"];
//const json_value &MapSize = ServerInfo["map"]["size"];
const json_value &Version = ServerInfo["version"];
const json_value &Clients = ServerInfo["clients"];
Error = false;
Error = Error || MaxClients.type != json_integer;
Error = Error || MaxPlayers.type != json_integer;
Error = Error || Passworded.type != json_boolean;
Error = Error || GameType.type != json_string;
Error = Error || Name.type != json_string;
Error = Error || MapName.type != json_string;
//Error = Error || MapCrc.type != json_string;
//Error = Error || MapSize.type != json_integer;
Error = Error || Version.type != json_string;
Error = Error || Clients.type != json_array;
if(Error)
{
return true;
}
pOut->m_MaxClients = MaxClients;
pOut->m_MaxPlayers = MaxPlayers;
pOut->m_Passworded = Passworded;
str_copy(pOut->m_aGameType, GameType, sizeof(pOut->m_aGameType));
str_copy(pOut->m_aName, Name, sizeof(pOut->m_aName));
str_copy(pOut->m_aMapName, MapName, sizeof(pOut->m_aMapName));
//if(ParseCrc(&pOut->m_MapCrc, MapCrc)) { return true; }
//pOut->m_MapSize = MapSize;
str_copy(pOut->m_aVersion, Version, sizeof(pOut->m_aVersion));
pOut->m_NumClients = 0;
pOut->m_NumPlayers = 0;
for(unsigned i = 0; i < Clients.u.array.length; i++)
{
const json_value &Client = Clients[i];
const json_value &Name = Client["name"];
const json_value &Clan = Client["clan"];
const json_value &Country = Client["country"];
const json_value &Score = Client["score"];
//const json_value &Team = Client["team"];
const json_value &IsPlayer = Client["is_player"];
Error = false;
Error = Error || Name.type != json_string;
Error = Error || Clan.type != json_string;
Error = Error || Country.type != json_integer;
Error = Error || Score.type != json_integer;
//Error = Error || Team.type != json_integer;
Error = Error || IsPlayer.type != json_boolean;
if(Error)
{
return true;
}
if(i < MAX_CLIENTS)
{
CClient *pClient = &pOut->m_aClients[i];
str_copy(pClient->m_aName, Name, sizeof(pClient->m_aName));
str_copy(pClient->m_aClan, Clan, sizeof(pClient->m_aClan));
pClient->m_Country = Country;
pClient->m_Score = Score;
pClient->m_IsPlayer = IsPlayer;
}
pOut->m_NumClients++;
if((bool)IsPlayer)
{
pOut->m_NumPlayers++;
}
}
return false;
}
bool CServerInfo2::operator==(const CServerInfo2 &Other) const
{
bool Unequal;
Unequal = false;
Unequal = Unequal || m_MaxClients != Other.m_MaxClients;
Unequal = Unequal || m_NumClients != Other.m_NumClients;
Unequal = Unequal || m_MaxPlayers != Other.m_MaxPlayers;
Unequal = Unequal || m_NumPlayers != Other.m_NumPlayers;
Unequal = Unequal || m_Passworded != Other.m_Passworded;
Unequal = Unequal || str_comp(m_aGameType, Other.m_aGameType) != 0;
Unequal = Unequal || str_comp(m_aName, Other.m_aName) != 0;
Unequal = Unequal || str_comp(m_aMapName, Other.m_aMapName) != 0;
//Unequal = Unequal || m_MapCrc != Other.m_MapCrc;
//Unequal = Unequal || m_MapSize != Other.m_MapSize;
Unequal = Unequal || str_comp(m_aVersion, Other.m_aVersion) != 0;
if(Unequal)
{
return false;
}
for(int i = 0; i < m_NumClients; i++)
{
Unequal = false;
Unequal = Unequal || str_comp(m_aClients[i].m_aName, Other.m_aClients[i].m_aName) != 0;
Unequal = Unequal || str_comp(m_aClients[i].m_aClan, Other.m_aClients[i].m_aClan) != 0;
Unequal = Unequal || m_aClients[i].m_Country != Other.m_aClients[i].m_Country;
Unequal = Unequal || m_aClients[i].m_Score != Other.m_aClients[i].m_Score;
//Unequal = Unequal || m_aClients[i].m_Team != Other.m_aClients[i].m_Team;
Unequal = Unequal || m_aClients[i].m_IsPlayer != Other.m_aClients[i].m_IsPlayer;
if(Unequal)
{
return false;
}
}
return true;
}
void CServerInfo2::ToJson(char *pBuffer, int BufferSize) const
{
dbg_assert(BufferSize > 0, "empty buffer");
/*
char aGameType[32];
char aName[128];
char aMapName[64];
char aVersion[64];
str_format(pBuffer, BufferSize, "{\"max_clients\":%d,\"max_players\":%d,\"passworded\":%s,\"game_type\":\"%s\",\"name\":\"%s\",\"map\":{\"name\":\"%s\",\"crc\":\"%08x\",\"size\":%d},\"version\":\"%s\",\"clients\":[",
m_MaxClients,
m_MaxPlayers,
JsonBool(m_Passworded),
EscapeJson(aGameType, sizeof(aGameType), m_aGameType),
EscapeJson(aName, sizeof(aName), m_aName),
EscapeJson(aMapName, sizeof(aMapName), m_aMapName),
m_MapCrc,
m_MapSize,
EscapeJson(aVersion, sizeof(aVersion), m_aVersion));
{
int Length = str_length(pBuffer);
pBuffer += Length;
BufferSize -= Length;
}
for(int i = 0; i < m_NumClients; i++)
{
char aName[MAX_NAME_LENGTH * 2];
char aClan[MAX_NAME_LENGTH * 2];
str_format(pBuffer, BufferSize, "%s{\"name\":\"%s\",\"clan\":\"%s\",\"country\":%d,\"score\":%d,\"team\":%d}",
i != 0 ? "," : "",
EscapeJson(aName, sizeof(aName), m_aClients[i].m_aName),
EscapeJson(aClan, sizeof(aClan), m_aClients[i].m_aClan),
m_aClients[i].m_Country,
m_aClients[i].m_Score,
m_aClients[i].m_Team);
{
int Length = str_length(pBuffer);
pBuffer += Length;
BufferSize -= Length;
}
}
str_format(pBuffer, BufferSize, "]}");
*/
}
CServerInfo2::operator CServerInfo() const
{
CServerInfo Result = {0};
Result.m_MaxClients = m_MaxClients;
Result.m_NumClients = m_NumClients;
Result.m_MaxPlayers = m_MaxPlayers;
Result.m_NumPlayers = m_NumPlayers;
Result.m_Flags = m_Passworded ? SERVER_FLAG_PASSWORD : 0;
str_copy(Result.m_aGameType, m_aGameType, sizeof(Result.m_aGameType));
str_copy(Result.m_aName, m_aName, sizeof(Result.m_aName));
str_copy(Result.m_aMap, m_aMapName, sizeof(Result.m_aMap));
//Result.m_MapCrc = m_MapCrc;
//Result.m_MapSize = m_MapSize;
str_copy(Result.m_aVersion, m_aVersion, sizeof(Result.m_aVersion));
for(int i = 0; i < std::min(m_NumClients, (int)MAX_CLIENTS); i++)
{
str_copy(Result.m_aClients[i].m_aName, m_aClients[i].m_aName, sizeof(Result.m_aClients[i].m_aName));
str_copy(Result.m_aClients[i].m_aClan, m_aClients[i].m_aClan, sizeof(Result.m_aClients[i].m_aClan));
Result.m_aClients[i].m_Country = m_aClients[i].m_Country;
Result.m_aClients[i].m_Score = m_aClients[i].m_Score;
//Result.m_aClients[i].m_Team = m_aClients[i].m_Team;
Result.m_aClients[i].m_Player = m_aClients[i].m_IsPlayer;
}
Result.m_NumReceivedClients = std::min(m_NumClients, (int)MAX_CLIENTS);
Result.m_Latency = -1;
return Result;
}

View file

@ -0,0 +1,48 @@
#ifndef ENGINE_SHARED_SERVERINFO_H
#define ENGINE_SHARED_SERVERINFO_H
#include "protocol.h"
typedef struct _json_value json_value;
class CServerInfo;
class CServerInfo2
{
public:
class CClient
{
public:
char m_aName[MAX_NAME_LENGTH];
char m_aClan[MAX_CLAN_LENGTH];
int m_Country;
int m_Score;
//int m_Team;
bool m_IsPlayer;
};
CClient m_aClients[MAX_CLIENTS];
int m_MaxClients;
int m_NumClients; // Indirectly serialized.
int m_MaxPlayers;
int m_NumPlayers; // Not serialized.
bool m_Passworded;
char m_aGameType[16];
char m_aName[64];
char m_aMapName[32];
//unsigned int m_MapCrc;
//int m_MapSize;
char m_aVersion[32];
bool operator==(const CServerInfo2 &Other) const;
bool operator!=(const CServerInfo2 &Other) const { return !(*this == Other); }
static bool FromJson(CServerInfo2 *pOut, const json_value *pJson);
static bool FromJsonRaw(CServerInfo2 *pOut, const json_value *pJson);
bool Validate() const;
void ToJson(char *pBuffer, int BufferSize) const;
operator CServerInfo() const;
};
bool ParseCrc(unsigned int *pResult, const char *pString);
#endif // ENGINE_SHARED_SERVERINFO_H

View file

@ -594,7 +594,10 @@ void IStorage::StripPathAndExtension(const char *pFilename, char *pBuffer, int B
str_copy(pBuffer, pExtractedName, Length);
}
IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments) { return CStorage::Create(pApplicationName, StorageType, NumArgs, ppArguments); }
IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments)
{
return CStorage::Create(pApplicationName, StorageType, NumArgs, ppArguments);
}
IStorage *CreateLocalStorage()
{
@ -610,3 +613,13 @@ IStorage *CreateLocalStorage()
}
return pStorage;
}
IStorage *CreateTempStorage(const char *pDirectory)
{
CStorage *pStorage = new CStorage();
if(!pStorage)
{
return nullptr;
}
pStorage->AddPath(pDirectory);
return pStorage;
}

28
src/engine/sqlite.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef ENGINE_SQLITE_H
#define ENGINE_SQLITE_H
#include <memory>
typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;
class IConsole;
class IStorage;
class CSqliteDeleter
{
public:
void operator()(sqlite3 *pSqlite);
};
class CSqliteStmtDeleter
{
public:
void operator()(sqlite3_stmt *pStmt);
};
typedef std::unique_ptr<sqlite3, CSqliteDeleter> CSqlite;
typedef std::unique_ptr<sqlite3_stmt, CSqliteStmtDeleter> CSqliteStmt;
int SqliteHandleError(IConsole *pConsole, int Error, sqlite3 *pSqlite, const char *pContext);
#define SQLITE_HANDLE_ERROR(x) SqliteHandleError(pConsole, x, &*pSqlite, #x)
CSqlite SqliteOpen(IConsole *pConsole, IStorage *pStorage, const char *pPath);
CSqliteStmt SqlitePrepare(IConsole *pConsole, sqlite3 *pSqlite, const char *pStatement);
#endif // ENGINE_SQLITE_H

View file

@ -44,5 +44,6 @@ public:
extern IStorage *CreateStorage(const char *pApplicationName, int StorageType, int NumArgs, const char **ppArguments);
extern IStorage *CreateLocalStorage();
extern IStorage *CreateTempStorage(const char *pDirectory);
#endif

View file

@ -31,6 +31,27 @@ static const int g_OffsetColPlayers = g_OffsetColMap + 3;
static const int g_OffsetColPing = g_OffsetColPlayers + 3;
static const int g_OffsetColVersion = g_OffsetColPing + 3;
void FormatServerbrowserPing(char *pBuffer, int BufferLength, const CServerInfo *pInfo)
{
if(!pInfo->m_LatencyIsEstimated)
{
str_format(pBuffer, BufferLength, "%d", pInfo->m_Latency);
return;
}
static const char *LOCATION_NAMES[CServerInfo::NUM_LOCS] = {
"", // LOC_UNKNOWN
"AFR", // LOC_AFRICA // Localize("AFR")
"ASI", // LOC_ASIA // Localize("ASI")
"AUS", // LOC_AUSTRALIA // Localize("AUS")
"EUR", // LOC_EUROPE // Localize("EUR")
"NA", // LOC_NORTH_AMERICA // Localize("NA")
"SA", // LOC_SOUTH_AMERICA // Localize("SA")
"CHN", // LOC_CHINA // Localize("CHN")
};
dbg_assert(0 <= pInfo->m_Location && pInfo->m_Location < CServerInfo::NUM_LOCS, "location out of range");
str_format(pBuffer, BufferLength, "%s", Localize(LOCATION_NAMES[pInfo->m_Location]));
}
void CMenus::RenderServerbrowserServerList(CUIRect View)
{
CUIRect Headers;
@ -157,8 +178,8 @@ void CMenus::RenderServerbrowserServerList(CUIRect View)
{
CUIRect MsgBox = View;
if(m_ActivePage == PAGE_INTERNET && ServerBrowser()->IsRefreshingMasters())
UI()->DoLabelScaled(&MsgBox, Localize("Refreshing master servers"), 16.0f, 0);
if(ServerBrowser()->IsGettingServerlist())
UI()->DoLabelScaled(&MsgBox, Localize("Getting serverlist from masterserver"), 16.0f, 0);
else if(!ServerBrowser()->NumServers())
UI()->DoLabelScaled(&MsgBox, Localize("No servers found"), 16.0f, 0);
else if(ServerBrowser()->NumServers() && !NumServers)
@ -405,7 +426,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View)
}
else if(ID == COL_PING)
{
str_format(aTemp, sizeof(aTemp), "%i", pItem->m_Latency);
FormatServerbrowserPing(aTemp, sizeof(aTemp), pItem);
if(g_Config.m_UiColorizePing)
{
ColorRGBA rgb = color_cast<ColorRGBA>(ColorHSLA((300.0f - clamp(pItem->m_Latency, 0, 300)) / 1000.0f, 1.0f, 0.5f));
@ -1017,14 +1038,34 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View)
{
CUIRect Button;
ServerDetails.HSplitBottom(20.0f, &ServerDetails, &Button);
Button.VSplitLeft(5.0f, 0, &Button);
CUIRect ButtonAddFav;
CUIRect ButtonLeakIp;
Button.VSplitMid(&ButtonAddFav, &ButtonLeakIp);
ButtonAddFav.VSplitLeft(5.0f, 0, &ButtonAddFav);
static int s_AddFavButton = 0;
if(DoButton_CheckBox(&s_AddFavButton, Localize("Favorite"), pSelectedServer->m_Favorite, &Button))
static int s_LeakIpButton = 0;
if(DoButton_CheckBox(&s_AddFavButton, Localize("Favorite"), pSelectedServer->m_Favorite, &ButtonAddFav))
{
if(pSelectedServer->m_Favorite)
{
ServerBrowser()->RemoveFavorite(pSelectedServer->m_NetAddr);
}
else
{
ServerBrowser()->AddFavorite(pSelectedServer->m_NetAddr);
if(g_Config.m_UiPage == PAGE_LAN)
{
ServerBrowser()->FavoriteAllowPing(pSelectedServer->m_NetAddr, true);
}
}
}
if(pSelectedServer->m_Favorite)
{
bool IpLeak = ServerBrowser()->IsFavoritePingAllowed(pSelectedServer->m_NetAddr);
if(DoButton_CheckBox(&s_LeakIpButton, Localize("Leak IP"), IpLeak, &ButtonLeakIp))
{
ServerBrowser()->FavoriteAllowPing(pSelectedServer->m_NetAddr, !IpLeak);
}
}
}
@ -1048,7 +1089,7 @@ void CMenus::RenderServerbrowserServerDetail(CUIRect View)
TextRender()->TextEx(&Cursor, pSelectedServer->m_aGameType, -1);
char aTemp[16];
str_format(aTemp, sizeof(aTemp), "%d", pSelectedServer->m_Latency);
FormatServerbrowserPing(aTemp, sizeof(aTemp), pSelectedServer);
RightColumn.HSplitTop(15.0f, &Row, &RightColumn);
TextRender()->SetCursor(&Cursor, Row.x, Row.y + (15.f - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END);
Cursor.m_LineWidth = Row.w;

View file

@ -12,3 +12,39 @@ TEST(Filesystem, CreateCloseDelete)
EXPECT_FALSE(io_close(File));
EXPECT_FALSE(fs_remove(Info.m_aFilename));
}
TEST(Filesystem, CreateDeleteDirectory)
{
CTestInfo Info;
char aFilename[128];
str_format(aFilename, sizeof(aFilename), "%s/test.txt", Info.m_aFilename);
EXPECT_FALSE(fs_makedir(Info.m_aFilename));
IOHANDLE File = io_open(aFilename, IOFLAG_WRITE);
ASSERT_TRUE(File);
EXPECT_FALSE(io_close(File));
// Directory removal fails if there are any files left in the directory.
EXPECT_TRUE(fs_removedir(Info.m_aFilename));
EXPECT_FALSE(fs_remove(aFilename));
EXPECT_FALSE(fs_removedir(Info.m_aFilename));
}
TEST(Filesystem, CantDeleteDirectoryWithRemove)
{
CTestInfo Info;
EXPECT_FALSE(fs_makedir(Info.m_aFilename));
EXPECT_TRUE(fs_remove(Info.m_aFilename)); // Cannot remove directory with file removal function.
EXPECT_FALSE(fs_removedir(Info.m_aFilename));
}
TEST(Filesystem, CantDeleteFileWithRemoveDirectory)
{
CTestInfo Info;
IOHANDLE File = io_open(Info.m_aFilename, IOFLAG_WRITE);
ASSERT_TRUE(File);
EXPECT_FALSE(io_close(File));
EXPECT_TRUE(fs_removedir(Info.m_aFilename)); // Cannot remove file with directory removal function.
EXPECT_FALSE(fs_remove(Info.m_aFilename));
}

View file

@ -23,6 +23,10 @@ protected:
{
m_Pool.Add(std::move(pJob));
}
void RunBlocking(IJob *pJob)
{
CJobPool::RunBlocking(pJob);
}
};
class CJob : public IJob
@ -44,6 +48,15 @@ TEST_F(Jobs, Simple)
Add(std::make_shared<CJob>([] {}));
}
TEST_F(Jobs, RunBlocking)
{
int Result = 0;
CJob Job([&] { Result = 1; });
EXPECT_EQ(Result, 0);
RunBlocking(&Job);
EXPECT_EQ(Result, 1);
}
TEST_F(Jobs, Wait)
{
SEMAPHORE sphore;

View file

@ -0,0 +1,35 @@
#include "test.h"
#include <gtest/gtest.h>
#include <base/system.h>
TEST(SecureRandom, Fill)
{
unsigned int Bits = 0;
while(~Bits)
{
unsigned int Random;
secure_random_fill(&Random, sizeof(Random));
Bits |= Random;
}
}
TEST(SecureRandom, Below1)
{
EXPECT_EQ(secure_rand_below(1), 0);
}
TEST(SecureRandom, Below)
{
int BOUNDS[] = {2, 3, 4, 5, 10, 100, 127, 128, 129};
for(unsigned i = 0; i < sizeof(BOUNDS) / sizeof(BOUNDS[0]); i++)
{
int Below = BOUNDS[i];
for(int j = 0; j < 10; j++)
{
int Random = secure_rand_below(Below);
EXPECT_GE(Random, 0);
EXPECT_LT(Random, Below);
}
}
}

106
src/test/serverbrowser.cpp Normal file
View file

@ -0,0 +1,106 @@
#include <gtest/gtest.h>
#include <engine/client/serverbrowser_ping_cache.h>
#include <engine/console.h>
#include <engine/engine.h>
#include <engine/shared/config.h>
#include <engine/storage.h>
#include <test/test.h>
TEST(ServerBrowser, PingCache)
{
CTestInfo Info;
IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT);
IStorage *pStorage = Info.CreateTestStorage();
IServerBrowserPingCache *pPingCache = CreateServerBrowserPingCache(pConsole, pStorage);
const IServerBrowserPingCache::CEntry *pEntries;
int NumEntries;
pPingCache->GetPingCache(&pEntries, &NumEntries);
EXPECT_EQ(NumEntries, 0);
pPingCache->Load();
pPingCache->GetPingCache(&pEntries, &NumEntries);
EXPECT_EQ(NumEntries, 0);
NETADDR Localhost4, Localhost4Tw, Localhost6, Localhost6Tw;
ASSERT_FALSE(net_addr_from_str(&Localhost4, "127.0.0.1"));
ASSERT_FALSE(net_addr_from_str(&Localhost4Tw, "127.0.0.1:8303"));
ASSERT_FALSE(net_addr_from_str(&Localhost6, "[::1]"));
ASSERT_FALSE(net_addr_from_str(&Localhost6Tw, "[::1]:8304"));
EXPECT_LT(net_addr_comp(&Localhost4, &Localhost6), 0);
// Newer pings overwrite older.
pPingCache->CachePing(Localhost4Tw, 123);
pPingCache->CachePing(Localhost4Tw, 234);
pPingCache->CachePing(Localhost4Tw, 345);
pPingCache->CachePing(Localhost4Tw, 456);
pPingCache->CachePing(Localhost4Tw, 567);
pPingCache->CachePing(Localhost4Tw, 678);
pPingCache->CachePing(Localhost4Tw, 789);
pPingCache->CachePing(Localhost4Tw, 890);
pPingCache->CachePing(Localhost4Tw, 901);
pPingCache->CachePing(Localhost4Tw, 135);
pPingCache->CachePing(Localhost4Tw, 246);
pPingCache->CachePing(Localhost4Tw, 357);
pPingCache->CachePing(Localhost4Tw, 468);
pPingCache->CachePing(Localhost4Tw, 579);
pPingCache->CachePing(Localhost4Tw, 680);
pPingCache->CachePing(Localhost4Tw, 791);
pPingCache->CachePing(Localhost4Tw, 802);
pPingCache->CachePing(Localhost4Tw, 913);
pPingCache->GetPingCache(&pEntries, &NumEntries);
EXPECT_EQ(NumEntries, 1);
if(NumEntries >= 1)
{
EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0);
EXPECT_EQ(pEntries[0].m_Ping, 913);
}
pPingCache->CachePing(Localhost4Tw, 234);
pPingCache->CachePing(Localhost6Tw, 345);
pPingCache->GetPingCache(&pEntries, &NumEntries);
EXPECT_EQ(NumEntries, 2);
if(NumEntries >= 2)
{
EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0);
EXPECT_TRUE(net_addr_comp(&pEntries[1].m_Addr, &Localhost6) == 0);
EXPECT_EQ(pEntries[0].m_Ping, 234);
EXPECT_EQ(pEntries[1].m_Ping, 345);
}
// Port doesn't matter for overwriting.
pPingCache->CachePing(Localhost4, 1337);
pPingCache->GetPingCache(&pEntries, &NumEntries);
EXPECT_EQ(NumEntries, 2);
if(NumEntries >= 2)
{
EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0);
EXPECT_TRUE(net_addr_comp(&pEntries[1].m_Addr, &Localhost6) == 0);
EXPECT_EQ(pEntries[0].m_Ping, 1337);
EXPECT_EQ(pEntries[1].m_Ping, 345);
}
delete pPingCache;
pPingCache = CreateServerBrowserPingCache(pConsole, pStorage);
// Persistence.
pPingCache->Load();
pPingCache->GetPingCache(&pEntries, &NumEntries);
EXPECT_EQ(NumEntries, 2);
if(NumEntries >= 2)
{
EXPECT_TRUE(net_addr_comp(&pEntries[0].m_Addr, &Localhost4) == 0);
EXPECT_TRUE(net_addr_comp(&pEntries[1].m_Addr, &Localhost6) == 0);
EXPECT_EQ(pEntries[0].m_Ping, 1337);
EXPECT_EQ(pEntries[1].m_Ping, 345);
}
delete pPingCache;
delete pStorage;
Info.DeleteTestStorageFilesOnSuccess();
}

149
src/test/serverinfo.cpp Normal file
View file

@ -0,0 +1,149 @@
#include <gtest/gtest.h>
#include <engine/serverbrowser.h>
#include <engine/shared/serverinfo.h>
#include <engine/external/json-parser/json.h>
TEST(ServerInfo, ParseLocation)
{
int Result;
EXPECT_TRUE(CServerInfo::ParseLocation(&Result, "xx"));
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "an"));
EXPECT_EQ(Result, CServerInfo::LOC_UNKNOWN);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "af"));
EXPECT_EQ(Result, CServerInfo::LOC_AFRICA);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "eu-n"));
EXPECT_EQ(Result, CServerInfo::LOC_EUROPE);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "na"));
EXPECT_EQ(Result, CServerInfo::LOC_NORTH_AMERICA);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "sa"));
EXPECT_EQ(Result, CServerInfo::LOC_SOUTH_AMERICA);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "as:e"));
EXPECT_EQ(Result, CServerInfo::LOC_ASIA);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "as:cn"));
EXPECT_EQ(Result, CServerInfo::LOC_CHINA);
EXPECT_FALSE(CServerInfo::ParseLocation(&Result, "oc"));
EXPECT_EQ(Result, CServerInfo::LOC_AUSTRALIA);
}
/*
static CServerInfo2 Parse(const char *pJson)
{
CServerInfo2 Out = {0};
json_value *pParsed = json_parse(pJson, str_length(pJson));
EXPECT_TRUE(pParsed);
if(pParsed)
{
EXPECT_FALSE(CServerInfo2::FromJson(&Out, pParsed));
}
return Out;
}
TEST(ServerInfo, Empty)
{
static const char EMPTY[] = "{\"max_clients\":0,\"max_players\":0,\"passworded\":false,\"game_type\":\"\",\"name\":\"\",\"map\":{\"name\":\"\",\"crc\":\"00000000\",\"size\":0},\"version\":\"\",\"clients\":[]}";
CServerInfo2 Empty = {0};
EXPECT_EQ(Parse(EMPTY), Empty);
char aBuf[1024];
Empty.ToJson(aBuf, sizeof(aBuf));
EXPECT_STREQ(aBuf, EMPTY);
}
CServerInfo2 SomethingImpl()
{
CServerInfo2 Something = {0};
str_copy(Something.m_aClients[0].m_aName, "Learath2", sizeof(Something.m_aClients[0].m_aName));
Something.m_aClients[0].m_Country = -1;
Something.m_aClients[0].m_Score = -1;
Something.m_aClients[0].m_Team = 1;
str_copy(Something.m_aClients[1].m_aName, "deen", sizeof(Something.m_aClients[1].m_aName));
str_copy(Something.m_aClients[1].m_aClan, "DDNet", sizeof(Something.m_aClients[1].m_aClan));
Something.m_aClients[1].m_Country = 276;
Something.m_aClients[1].m_Score = 0;
Something.m_aClients[1].m_Team = 0;
Something.m_MaxClients = 16;
Something.m_NumClients = 2;
Something.m_MaxPlayers = 8;
Something.m_NumPlayers = 1;
Something.m_Passworded = true;
str_copy(Something.m_aGameType, "DM", sizeof(Something.m_aGameType));
str_copy(Something.m_aName, "unnamed server", sizeof(Something.m_aName));
str_copy(Something.m_aMapName, "dm1", sizeof(Something.m_aMapName));
Something.m_MapCrc = 0xf2159e6e;
Something.m_MapSize = 5805;
str_copy(Something.m_aVersion, "0.6.4", sizeof(Something.m_aVersion));
return Something;
}
static const CServerInfo2 s_Something = SomethingImpl();
TEST(ServerInfo, Something)
{
static const char SOMETHING[] = "{\"max_clients\":16,\"max_players\":8,\"passworded\":true,\"game_type\":\"DM\",\"name\":\"unnamed server\",\"map\":{\"name\":\"dm1\",\"crc\":\"f2159e6e\",\"size\":5805},\"version\":\"0.6.4\",\"clients\":[{\"name\":\"Learath2\",\"clan\":\"\",\"country\":-1,\"score\":-1,\"team\":1},{\"name\":\"deen\",\"clan\":\"DDNet\",\"country\":276,\"score\":0,\"team\":0}]}";
EXPECT_EQ(Parse(SOMETHING), s_Something);
char aBuf[1024];
s_Something.ToJson(aBuf, sizeof(aBuf));
EXPECT_EQ(Parse(aBuf), s_Something);
}
TEST(ServerInfo, ToServerBrowserServerInfo)
{
CServerInfo Sbsi = s_Something;
EXPECT_EQ(Sbsi.m_MaxClients, s_Something.m_MaxClients);
EXPECT_EQ(Sbsi.m_NumClients, s_Something.m_NumClients);
EXPECT_EQ(Sbsi.m_MaxPlayers, s_Something.m_MaxPlayers);
EXPECT_EQ(Sbsi.m_NumPlayers, s_Something.m_NumPlayers);
EXPECT_EQ((Sbsi.m_Flags&SERVER_FLAG_PASSWORD) != 0, s_Something.m_Passworded);
EXPECT_STREQ(Sbsi.m_aGameType, s_Something.m_aGameType);
EXPECT_STREQ(Sbsi.m_aName, s_Something.m_aName);
EXPECT_STREQ(Sbsi.m_aMap, s_Something.m_aMapName);
EXPECT_EQ(Sbsi.m_MapCrc, s_Something.m_MapCrc);
EXPECT_EQ(Sbsi.m_MapSize, s_Something.m_MapSize);
EXPECT_STREQ(Sbsi.m_aVersion, s_Something.m_aVersion);
for(int i = 0; i < Sbsi.m_NumClients; i++)
{
EXPECT_STREQ(Sbsi.m_aClients[i].m_aName, s_Something.m_aClients[i].m_aName);
EXPECT_STREQ(Sbsi.m_aClients[i].m_aClan, s_Something.m_aClients[i].m_aClan);
EXPECT_EQ(Sbsi.m_aClients[i].m_Country, s_Something.m_aClients[i].m_Country);
EXPECT_EQ(Sbsi.m_aClients[i].m_Score, s_Something.m_aClients[i].m_Score);
EXPECT_EQ(Sbsi.m_aClients[i].m_Player, s_Something.m_aClients[i].m_Team != 0);
}
}
*/
static unsigned int ParseCrcOrDeadbeef(const char *pString)
{
unsigned int Result;
if(ParseCrc(&Result, pString))
{
Result = 0xdeadbeef;
}
return Result;
}
TEST(ServerInfo, Crc)
{
EXPECT_EQ(ParseCrcOrDeadbeef("00000000"), 0);
EXPECT_EQ(ParseCrcOrDeadbeef("00000001"), 1);
EXPECT_EQ(ParseCrcOrDeadbeef("12345678"), 0x12345678);
EXPECT_EQ(ParseCrcOrDeadbeef("9abcdef0"), 0x9abcdef0);
EXPECT_EQ(ParseCrcOrDeadbeef(""), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("a"), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("x"), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("ç"), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("😢"), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("0"), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("000000000"), 0xdeadbeef);
EXPECT_EQ(ParseCrcOrDeadbeef("00000000x"), 0xdeadbeef);
}

View file

@ -2,6 +2,7 @@
#include <gtest/gtest.h>
#include <base/system.h>
#include <engine/storage.h>
CTestInfo::CTestInfo()
{
@ -11,9 +12,106 @@ CTestInfo::CTestInfo()
pTestInfo->test_case_name(), pTestInfo->name(), pid());
}
IStorage *CTestInfo::CreateTestStorage()
{
bool Error = fs_makedir(m_aFilename);
EXPECT_FALSE(Error);
if(Error)
{
return nullptr;
}
return CreateTempStorage(m_aFilename);
}
class CTestInfoPath
{
public:
bool m_IsDirectory;
char m_aData[MAX_PATH_LENGTH];
bool operator<(const CTestInfoPath &Other) const
{
if(m_IsDirectory != Other.m_IsDirectory)
{
return m_IsDirectory < Other.m_IsDirectory;
}
return str_comp(m_aData, Other.m_aData) < 0;
}
};
class CTestCollectData
{
public:
char m_aCurrentDir[MAX_PATH_LENGTH];
std::vector<CTestInfoPath> *m_paEntries;
};
int TestCollect(const char *pName, int IsDir, int Unused, void *pUser)
{
CTestCollectData *pData = (CTestCollectData *)pUser;
if(str_comp(pName, ".") == 0 || str_comp(pName, "..") == 0)
{
return 0;
}
CTestInfoPath Path;
Path.m_IsDirectory = IsDir;
str_format(Path.m_aData, sizeof(Path.m_aData), "%s/%s", pData->m_aCurrentDir, pName);
pData->m_paEntries->push_back(Path);
if(Path.m_IsDirectory)
{
CTestCollectData DataRecursive;
str_copy(DataRecursive.m_aCurrentDir, Path.m_aData, sizeof(DataRecursive.m_aCurrentDir));
DataRecursive.m_paEntries = pData->m_paEntries;
fs_listdir(DataRecursive.m_aCurrentDir, TestCollect, 0, &DataRecursive);
}
return 0;
}
void CTestInfo::DeleteTestStorageFilesOnSuccess()
{
if(::testing::Test::HasFailure())
{
return;
}
std::vector<CTestInfoPath> aEntries;
CTestCollectData Data;
str_copy(Data.m_aCurrentDir, m_aFilename, sizeof(Data.m_aCurrentDir));
Data.m_paEntries = &aEntries;
fs_listdir(Data.m_aCurrentDir, TestCollect, 0, &Data);
CTestInfoPath Path;
Path.m_IsDirectory = true;
str_copy(Path.m_aData, Data.m_aCurrentDir, sizeof(Path.m_aData));
aEntries.push_back(Path);
// Sorts directories after files.
std::sort(aEntries.begin(), aEntries.end());
// Don't delete too many files.
ASSERT_LE(aEntries.size(), 10);
for(auto &Entry : aEntries)
{
if(Entry.m_IsDirectory)
{
ASSERT_FALSE(fs_removedir(Entry.m_aData));
}
else
{
ASSERT_FALSE(fs_remove(Entry.m_aData));
}
}
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
net_init();
if(secure_random_init())
{
fprintf(stderr, "random init failed\n");
return 1;
}
return RUN_ALL_TESTS();
}

View file

@ -1,9 +1,13 @@
#ifndef TEST_TEST_H
#define TEST_TEST_H
class IStorage;
class CTestInfo
{
public:
CTestInfo();
IStorage *CreateTestStorage();
void DeleteTestStorageFilesOnSuccess();
char m_aFilename[64];
};
#endif // TEST_TEST_H