ReUnion/reunion/src/server_info.cpp
2024-06-20 20:43:04 +07:00

732 lines
18 KiB
C++

#include "precompiled.h"
CServerInfo* g_ServerInfo;
const char* CServerInfo::DETAILS = "details";
const char* CServerInfo::PLAYERS = "players";
const char* CServerInfo::CONNECT = "connect";
const char* CServerInfo::CHALLENGE = "challenge";
const char* CServerInfo::GETCHALLENGE = "getchallenge";
CServerInfo::CServerInfo()
{
m_maxPlayers = 0;
m_pEdicts = nullptr;
m_pcv_hostname = nullptr;
m_pcv_sv_tags = nullptr;
m_pcv_sv_visiblemaxplayers = nullptr;
m_pcv_sv_password = nullptr;
m_pcv_net_address = nullptr;
m_pEntityInterface = nullptr;
m_realTime = 0.0;
m_port = 0;
m_appId = 0;
memset(m_appVersion, 0, sizeof(m_appVersion));
memset(m_gameDir, 0, sizeof(m_gameDir));
m_multipacketId = rand() | (rand() << 16);
// Query emulator
m_respSource.updateInterval = QUERY_UPDATE_INTERVAL;
m_respSource.write = &CServerInfo::writeSourceResponse;
m_respGoldSrc.updateInterval = QUERY_UPDATE_INTERVAL;
m_respGoldSrc.write = &CServerInfo::writeGoldSourceResponse;
m_respPlayers.updateInterval = PLAYERS_UPDATE_INTERVAL;
m_respPlayers.write = &CServerInfo::writePlayersList;
m_respRules.updateInterval = RULES_UPDATE_INTERVAL;
m_respRules.write = &CServerInfo::writeRulesList;
m_queryLimiter.addExceptIPs(g_ReunionConfig->getExceptIPs());
m_lastRatesCheck = g_RehldsFuncs->GetRealTime();
}
void CServerInfo::writeSourceResponse(CSizeBuf& szbuf) const
{
szbuf.WriteLong(CONNECTIONLESS_HEADER); // connectionless header
szbuf.WriteByte(S2A_INFO); // data header
szbuf.WriteByte(48); // protocol
szbuf.WriteString(getHostName()); // hostname
szbuf.WriteString(getMapName()); // mapname
if (m_queryLimiter.isUnderFlood()) {
szbuf.WriteChar('\0'); // gamedir
szbuf.WriteChar('\0'); // gamename
} else {
szbuf.WriteString(getGameDir()); // gamedir
szbuf.WriteString(getGameDescription()); // gamename
}
szbuf.WriteShort(getAppId()); // appid
szbuf.WriteByte((uint8)getPlayersCount()); // players
szbuf.WriteByte((uint8)getVisibleMaxPlayers()); // maxplayers
szbuf.WriteByte((uint8)getPlayingBots()); // bots
szbuf.WriteByte('d'); // server type
szbuf.WriteByte(getOS()); // server os
szbuf.WriteByte(getIsPasswordSet()); // password
szbuf.WriteByte(getSecure()); // secure
szbuf.WriteString(getAppVersion()); // app version
// Extra Data Flag (EDF)
uint8_t edf = EDF_FLAG_PORT;
const char *pchTags = getTags();
if (pchTags && pchTags[0])
edf |= EDF_FLAG_GAME_TAGS;
szbuf.WriteByte(edf);
// Server port
if (edf & EDF_FLAG_PORT)
szbuf.WriteShort(getPort());
// Server's SteamID
if (edf & EDF_FLAG_STEAMID)
szbuf.WriteUint64(Steam_GSGetSteamID());
// Port short Spectator port number for SourceTV
// Name string Name of the spectator server for SourceTV
if (edf & EDF_FLAG_SOURCE_TV)
szbuf.WriteShort(0);
// Keywords string Tags that describe the game according to the server (for future use)
if (edf & EDF_FLAG_GAME_TAGS)
szbuf.WriteString(getTags());
// GameID long long The server's 64-bit GameID
// If this is present, a more accurate AppID is present in the low 24 bits
// The earlier AppID could have been truncated as it was forced into 16-bit storage
if (edf & EDF_FLAG_GAMEID)
szbuf.WriteUint64(getAppId());
}
void CServerInfo::writeGoldSourceResponse(CSizeBuf& szbuf) const
{
szbuf.WriteLong(CONNECTIONLESS_HEADER); // connectionless header
szbuf.WriteByte(S2A_INFO_DETAILED); // data header (detailed mode)
szbuf.WriteString(getServerAddress()); // server address
szbuf.WriteString(getHostName()); // hostname
szbuf.WriteString(getMapName()); // mapname
szbuf.WriteString(getGameDir()); // gamedir
szbuf.WriteString(getGameDescription()); // gamename
szbuf.WriteByte((uint8)getPlayersCount()); // players
szbuf.WriteByte((uint8)getVisibleMaxPlayers()); // maxplayers
szbuf.WriteByte(47); // protocol
szbuf.WriteByte('d'); // server type
szbuf.WriteByte(getOS()); // server os
szbuf.WriteByte(getIsPasswordSet()); // password
szbuf.WriteByte(FALSE); // mod info
szbuf.WriteByte(getSecure()); // secure
szbuf.WriteByte((uint8)getPlayingBots()); // bots
}
void CServerInfo::writePlayersList(CSizeBuf& szbuf) const
{
szbuf.WriteLong(CONNECTIONLESS_HEADER); // connectionless header
szbuf.WriteByte(S2A_PLAYERS); // data header
byte* pcount = szbuf.GetData() + szbuf.GetCurSize();
szbuf.WriteByte(0); // players count
size_t count = 0;
for (int i = 0; i < m_maxPlayers; i++)
{
const CReunionPlayer *player = g_Players[i];
if (!player->getClient()->IsConnected())
continue;
szbuf.WriteByte((uint8)(count + (int)player->getClient()->IsFakeClient() * 128)); // index of player chunk starting from 0
szbuf.WriteString(player->getClient()->GetName()); // name
szbuf.WriteLong(int(m_pEdicts[i + 1].v.frags)); // frags
szbuf.WriteFloat(m_realTime - player->getConnectionTime()); // playing time
count++;
}
*pcount = byte(count);
}
void CServerInfo::writeRulesList(CSizeBuf& szbuf) const
{
szbuf.WriteLong(CONNECTIONLESS_HEADER); // connectionless header
szbuf.WriteByte(S2A_RULES); // data header
uint16* pcount = (uint16 *)(szbuf.GetData() + szbuf.GetCurSize());
szbuf.WriteShort(0); // cvars count
size_t count = 0;
for (cvar_t* var = g_RehldsFuncs->GetCvarVars(); var; var = var->next)
{
if (var->flags & FCVAR_SERVER)
{
szbuf.WriteString(var->name);
if (var->flags & FCVAR_PROTECTED)
{
if (var->string[0] && strcmp(var->string, "none") != 0)
szbuf.WriteChar('1');
else
szbuf.WriteChar('0');
szbuf.WriteChar('\0');
}
else
szbuf.WriteString(var->string);
if (szbuf.IsOverflowed())
break;
count++;
}
}
*pcount = uint16(count);
}
void CServerInfo::sendResponse(const netadr_t& adr, response_t& response)
{
if (float(m_realTime - response.lastUpdate) > response.updateInterval) {
response.lastUpdate = m_realTime;
response.cache.Clear();
(this->*response.write)(response.cache);
}
if (response.cache.GetCurSize() <= STEAM_MAX_PACKET_SIZE || !g_ReunionConfig->isAllowSplitPackets())
return g_RehldsFuncs->NET_SendPacket(response.cache.GetCurSize(), response.cache.GetData(), adr);
uint8_t buf[STEAM_MAX_PACKET_SIZE];
size_t fragSize = STEAM_MAX_PACKET_SIZE - sizeof(long) - sizeof(long) - sizeof(byte);
size_t fragCount = (response.cache.GetCurSize() + fragSize - 1) / fragSize;
size_t offset = 0;
for (size_t packetNumber = 0; packetNumber < fragCount; packetNumber++) {
if (packetNumber == fragCount - 1)
fragSize = response.cache.GetCurSize() - offset;
uint8_t packetID = (uint8_t)((packetNumber << 4) + fragCount);
CSizeBuf sendbuf(buf, sizeof buf);
sendbuf.WriteLong(MULTIPACKET_HEADER); // connectionless header
sendbuf.WriteLong(m_multipacketId); // sequence number
sendbuf.WriteByte(packetID); // fragment number
sendbuf.Write(response.cache.GetData() + offset, fragSize);
g_RehldsFuncs->NET_SendPacket(sendbuf.GetCurSize(), sendbuf.GetData(), adr);
offset += fragSize;
}
m_multipacketId++;
}
void CServerInfo::sendQueryChallenge(const netadr_t& to)
{
uint8 buf[16];
CSizeBuf szbuf(buf, sizeof buf);
szbuf.WriteLong(CONNECTIONLESS_HEADER);
szbuf.WriteChar(S2C_CHALLENGE);
szbuf.WriteLong(g_RehldsFuncs->SV_GetChallenge(to));
g_RehldsFuncs->NET_SendPacket(szbuf.GetCurSize(), szbuf.GetData(), to);
}
void CServerInfo::sendServerInfo(const netadr_t& to, server_answer_type sat)
{
switch (sat)
{
case sat_source:
sendResponse(to, m_respSource);
break;
case sat_goldsource:
sendResponse(to, m_respGoldSrc);
break;
case sat_hybrid:
sendResponse(to, m_respGoldSrc);
if (g_ReunionConfig->allowFixBuggedQuery() && m_queryBugfix.isBuggedQuery(to)) {
sendEmptyPlayersList(to);
}
sendResponse(to, m_respSource);
break;
default:
util_syserror("invalid response type %u\n", sat);
break;
}
}
void CServerInfo::sendPlayersList(const netadr_t& to)
{
sendResponse(to, m_respPlayers);
}
void CServerInfo::sendEmptyPlayersList(const netadr_t& to)
{
uint8 buf[16];
CSizeBuf szbuf(buf, sizeof buf);
szbuf.WriteLong(CONNECTIONLESS_HEADER); // connectionless header
szbuf.WriteByte(S2A_PLAYERS); // data header
szbuf.WriteByte(0); // some clients crashing if not 0
g_RehldsFuncs->NET_SendPacket(szbuf.GetCurSize(), szbuf.GetData(), to);
}
void CServerInfo::sendRulesList(const netadr_t& to)
{
sendResponse(to, m_respRules);
}
#define equal(x, s) !memcmp(x, s, sizeof s - 1)
bool CServerInfo::handleQueryGlobal(IRehldsHook_PreprocessPacket* chain, CSizeBuf& szbuf, const netadr_t& from)
{
switch (szbuf.ReadChar())
{
/* Source style requests. All requests starting from registered chars are handled in steam client library. */
case A2S_INFO:
{
// Ignore invalid requests
if (szbuf.GetMaxSize() < A2S_INFO_LEN || szbuf.GetData()[5] != 'S') // "Source Engine Query"
return false;
g_RehldsFuncs->NET_SendPacket(m_respSource.cache.GetCurSize(), m_respSource.cache.GetData(), from);
return false;
}
case A2S_PLAYER:
{
if (szbuf.GetCurSize() < A2S_PLAYER_MINLEN) // actual len is 10, but 9 also valid
return false;
int challenge = szbuf.ReadLong();
if (challenge <= 0 || !g_RehldsFuncs->CheckChallenge(from, challenge)) {
sendQueryChallenge(from);
}
else {
sendPlayersList(from);
}
return false;
}
/* GoldSource style requests */
case 'c': // connect/challenge
{
if (equal(szbuf.GetData() + 4, CONNECT) && szbuf.GetCurSize() > 64) {
break;
}
if (equal(szbuf.GetData() + 4, CHALLENGE)) {
break;
}
return false;
}
case 'g': // getchallenge
{
if (equal(szbuf.GetData() + 4, GETCHALLENGE)) {
break;
}
return false;
}
default:
return false;
}
return chain->callNext(szbuf.GetData(), szbuf.GetCurSize(), from);
}
bool CServerInfo::handleQuery(IRehldsHook_PreprocessPacket* chain, uint8* data, unsigned int len, const netadr_t& from)
{
m_queryLimiter.incomingPacket(len + PACKET_HEADER_SIZE);
if (len < 5) {
return false;
}
CSizeBuf szbuf(data, len, len);
// Not connectionless
if (szbuf.ReadLong() != -1) {
return chain->callNext(data, len, from);
}
if (g_ReunionConfig->enableQueryLimiter()) {
m_queryLimiter.incomingQuery();
if (m_queryLimiter.getUseGlobalRateLimit()) {
return m_queryLimiter.allowGlobalQuery() ? handleQueryGlobal(chain, szbuf, from) : false;
}
}
//LCPrintf(true, "Connectionless packet %i: %.*s\n", len, max(0, int(len) - 4), szbuf.GetData() + 4);
switch (szbuf.ReadChar())
{
/* Source style requests. All requests starting from registered chars are handled in steam client library. */
case A2S_INFO:
{
// Ignore invalid requests
if (szbuf.GetMaxSize() < A2S_INFO_LEN || data[5] != 'S') // "Source Engine Query"
return false;
if (m_queryLimiter.allowQuery(from)) {
server_answer_type sat = g_ReunionConfig->getServerAnswerType();
if (sat == sat_hybrid && m_queryLimiter.isUnderFlood()) {
sat = sat_source;
}
sendServerInfo(from, sat);
}
return false;
}
case A2S_RULES:
{
if (len != A2S_RULES_MINLEN)
return false;
int challenge = szbuf.ReadLong();
if (challenge <= 0 || !g_RehldsFuncs->CheckChallenge(from, challenge)) { // HLSW often uses outdated challenge
sendQueryChallenge(from);
}
else {
sendRulesList(from);
}
return false;
}
case A2S_PLAYER:
{
if (len < A2S_PLAYER_MINLEN) // actual len is 10, but 9 also valid
return false;
int challenge = szbuf.ReadLong();
if (challenge <= 0 || !g_RehldsFuncs->CheckChallenge(from, challenge)) {
sendQueryChallenge(from);
}
else {
sendPlayersList(from);
}
return false;
}
// ignore incoming server query responces
case S2A_INFO:
case S2A_PLAYERS:
case S2A_INFO_OLD:
case S2A_INFO_DETAILED:
return false;
/* GoldSource style requests */
// old 'add to favorites' query
case 'd': // details
{
if (!equal(data + 4, DETAILS))
break;
server_answer_type sat = g_ReunionConfig->getServerAnswerType();
if (sat != sat_source && m_queryLimiter.allowQuery(from) && !m_queryLimiter.isUnderFlood()) {
sendServerInfo(from, sat_goldsource);
}
return false;
}
case 'p':
{
if (!equal(data + 4, PLAYERS))
break;
server_answer_type sat = g_ReunionConfig->getServerAnswerType();
if (sat != sat_source) {
if (m_queryLimiter.allowQuery(from) && !m_queryLimiter.isUnderFlood()) {
sendPlayersList(from);
}
else {
sendEmptyPlayersList(from);
}
}
break;
}
case 'c': // connect/challenge
{
if (equal(data + 4, CONNECT) && len < 64) {
return false;
}
if (equal(data + 4, CHALLENGE) && !m_queryLimiter.allowQuery(from, true)) {
return false;
}
break;
}
case 'g': // getchallenge
{
if (equal(data + 4, GETCHALLENGE) && !m_queryLimiter.allowQuery(from, true)) {
return false;
}
break;
}
default:
break;
}
// query not handled
return chain->callNext(data, len, from);
}
bool CServerInfo::isAddressBanned(const netadr_t& from) const
{
return m_queryBanList.isBanned(*(uint32 *)from.ip);
}
void CServerInfo::banAddress(const char* addr, uint32_t time)
{
m_queryBanList.addBan(inet_addr(addr), time);
}
void CServerInfo::unbanAddress(const char* addr)
{
m_queryBanList.removeBan(inet_addr(addr));
}
void CServerInfo::printBans()
{
m_queryBanList.print();
}
void CServerInfo::startFrame()
{
m_realTime = g_RehldsFuncs->GetRealTime();
if (!g_ReunionConfig->enableQueryLimiter())
return;
double realtime = g_RehldsFuncs->GetRealTime();
float delta = float(realtime - m_lastRatesCheck);
if (delta > QUERY_CHECK_INTERVAL) {
m_lastRatesCheck = realtime;
m_queryBanList.removeExpired(realtime);
m_queryLimiter.checkRates(realtime, delta, m_queryBanList);
}
}
edict_t* CServerInfo::getEdict(size_t index) const
{
return m_pEdicts + index;
}
void CServerInfo::serverActivate(edict_t* edicts, int maxclients)
{
m_pEdicts = edicts;
m_maxPlayers = maxclients;
GET_HOOK_TABLES(PLID, nullptr, &m_pEntityInterface, nullptr);
m_pcv_hostname = g_engfuncs.pfnCVarGetPointer("hostname");
m_pcv_sv_tags = g_engfuncs.pfnCVarGetPointer("sv_tags");
m_pcv_sv_visiblemaxplayers = g_engfuncs.pfnCVarGetPointer("sv_visiblemaxplayers");
m_pcv_sv_password = g_engfuncs.pfnCVarGetPointer("sv_password");
m_pcv_net_address = g_engfuncs.pfnCVarGetPointer("net_address");
m_appId = parseAppId();
parseAppVersion(m_appVersion, sizeof m_appVersion - 1);
m_port = atoi(g_engfuncs.pfnCVarGetString("hostport"));
if (!m_port) {
m_port = atoi(g_engfuncs.pfnCVarGetString("port"));
if (!m_port) {
m_port = PORT_SERVER;
}
}
g_engfuncs.pfnGetGameDir(m_gameDir);
}
const char* CServerInfo::getHostName() const
{
return m_pcv_hostname->string;
}
const char* CServerInfo::getTags() const
{
return m_pcv_sv_tags ? m_pcv_sv_tags->string : nullptr;
}
const char* CServerInfo::getMapName()
{
return STRING(gpGlobals->mapname);
}
const char* CServerInfo::getGameDir() const
{
return m_gameDir;
}
const char* CServerInfo::getGameDescription() const
{
if (m_pEntityInterface->pfnGetGameDescription)
{
return m_pEntityInterface->pfnGetGameDescription();
}
return MDLL_GetGameDescription();
}
size_t CServerInfo::getPlayersCount()
{
return Reunion_GetConnectedPlayersCount();
}
size_t CServerInfo::getPlayingBots() const
{
size_t count = 0;
for (int i = 1; i <= m_maxPlayers; i++)
{
if (!(m_pEdicts[i].v.flags & FL_FAKECLIENT))
continue;
if (m_pEdicts[i].v.flags & (FL_SPECTATOR | FL_DORMANT))
continue;
count++;
}
return count;
}
int CServerInfo::getMaxPlayers() const
{
return m_maxPlayers;
}
int CServerInfo::getVisibleMaxPlayers() const
{
int visible = atoi(m_pcv_sv_visiblemaxplayers->string);
return (visible < 0) ? m_maxPlayers : visible;
}
char CServerInfo::getOS()
{
#ifdef _WIN32
return 'w';
#else
return 'l';
#endif
}
uint8 CServerInfo::getIsPasswordSet() const
{
return (m_pcv_sv_password->string[0] && strcmp(m_pcv_sv_password->string, "none")) ? TRUE : FALSE;
}
uint8 CServerInfo::getSecure()
{
return g_RehldsFuncs->GSBSecure();
}
const char* CServerInfo::getServerAddress() const
{
return m_pcv_net_address->string;
}
double CServerInfo::getRealTime() const
{
return m_realTime;
}
int CServerInfo::getPort() const
{
return m_port;
}
int CServerInfo::getAppId() const
{
return m_appId;
}
const char* CServerInfo::getAppVersion() const
{
return m_appVersion;
}
int CServerInfo::parseAppId()
{
int appId = 0;
char filename[MAX_PATH];
snprintf(filename, sizeof filename, "./%s/steam_appid.txt", m_gameDir);
FILE* fp = fopen(filename, "rt");
if (fp) {
char line[256];
if (fgets(line, sizeof line, fp))
appId = atoi(line);
fclose(fp);
}
if (!appId) {
appId = HLDS_APPID;
}
return appId;
}
void CServerInfo::parseAppVersion(char* buf, size_t maxlen)
{
char filename[MAX_PATH];
snprintf(filename, sizeof filename, "./%s/steam.inf", m_gameDir);
FILE* fp = fopen(filename, "rt");
if (fp) {
char line[256];
while (fgets(line, sizeof line, fp)) {
char* value = strchr(line, '=');
if (!value) {
continue;
}
*value++ = '\0';
trimbuf(line);
trimbuf(value);
if (!strcmp(line, "PatchVersion=")) {
snprintf(buf, maxlen, "%s/Stdio", value);
break;
}
}
fclose(fp);
}
if (!buf[0]) {
strncpy(buf, HLDS_APPVERSION, maxlen);
}
}
bool Reunion_PreprocessPacket(IRehldsHook_PreprocessPacket* chain, uint8* data, unsigned int len, const netadr_t& from)
{
if (g_ServerInfo->isAddressBanned(from))
return false;
return g_ServerInfo->handleQuery(chain, data, len, from);
}
bool Reunion_Init_ServerInfo()
{
g_ServerInfo = new CServerInfo();
g_RehldsHookchains->PreprocessPacket()->registerHook(&Reunion_PreprocessPacket);
return true;
}