#include "StarServerQueryThread.hpp" #include "StarLogging.hpp" #include "StarRoot.hpp" #include "StarConfiguration.hpp" #include "StarVersion.hpp" #include "StarUniverseServer.hpp" #include "StarIterator.hpp" namespace Star { ServerQueryThread::ServerQueryThread(UniverseServer* universe, HostAddressWithPort const& bindAddress) : Thread("QueryServer"), m_universe(universe), m_queryServer(bindAddress), m_stop(true), m_lastChallengeCheck(Time::monotonicMilliseconds()) { m_playersResponse.resize(A2S_PACKET_SIZE); m_playersResponse.setByteOrder(ByteOrder::LittleEndian); m_playersResponse.setNullTerminatedStrings(true); m_rulesResponse.resize(A2S_PACKET_SIZE); m_rulesResponse.setByteOrder(ByteOrder::LittleEndian); m_rulesResponse.setNullTerminatedStrings(true); m_generalResponse.resize(A2S_PACKET_SIZE); m_generalResponse.setByteOrder(ByteOrder::LittleEndian); m_generalResponse.setNullTerminatedStrings(true); m_serverPort = 0; m_lastActiveTime = 0; auto& root = Root::singleton(); auto cfg = root.configuration(); m_maxPlayers = cfg->get("maxPlayers").toUInt(); m_serverName = cfg->get("serverName").toString(); m_lastPlayersResponse = 0; m_lastRulesResponse = 0; } ServerQueryThread::~ServerQueryThread() { stop(); join(); } void ServerQueryThread::start() { m_stop = false; Thread::start(); m_lastActiveTime = Time::monotonicMilliseconds(); } void ServerQueryThread::stop() { m_stop = true; m_queryServer.close(); } void ServerQueryThread::sendTo(HostAddressWithPort const& address, DataStreamBuffer* ds) { m_queryServer.send(address, ds->ptr(), ds->size()); } uint8_t ServerQueryThread::serverPlayerCount() { return m_universe->numberOfClients(); } bool ServerQueryThread::serverPassworded() { // TODO: implement return false; } String ServerQueryThread::serverWorldNames() { auto activeWorlds = m_universe->activeWorlds(); if (activeWorlds.empty()) return String("Unknown"); return StringList(activeWorlds.transformed(printWorldId)).join(","); } const char* ServerQueryThread::serverPlugins() { // TODO: implement return "none"; } bool ServerQueryThread::processPacket(HostAddressWithPort const& address, char const* data, size_t length) { uint8_t* buf = (uint8_t*)data; if (length < 5 || buf[0] != 0xff || buf[1] != 0xff || buf[2] != 0xff || buf[3] != 0xff) { // short packet or missing header return false; } // Process packet switch (buf[4]) { case A2S_INFO_REQUEST: { // We use -6 and not -5 as the string should be NULL terminated // but instead of the std::string constructor stopping at the NULL // it includes it :( std::string str((const char*)(buf + 5), length - 6); if (str.compare(A2S_INFO_REQUEST_STRING) != 0) { // Invalid request return false; } m_generalResponse.clear(); m_generalResponse << A2S_HEAD_INT << A2S_INFO_REPLY << A2S_VERSION << m_serverName << serverWorldNames() << GAME_DIR << GAME_DESC << A2S_APPID // Should be SteamAppId but this isn't a short :( << serverPlayerCount() << m_maxPlayers << (uint8_t)0x00 // bots << A2S_TYPE_DEDICATED // dedicated #ifdef STAR_SYSTEM_FAMILY_WINDOWS << A2S_ENV_WINDOWS // os #else << A2S_ENV_LINUX // os #endif << serverPassworded() << A2S_VAC_OFF // secure << StarVersionString << A2S_EDF_PORT // EDF << m_serverPort; sendTo(address, &m_generalResponse); return true; } case A2S_CHALLENGE_REQUEST: sendChallenge(address); return true; case A2S_PLAYER_REQUEST: if (challengeRequest(address, data, length)) return true; if (!validChallenge(address, data, length)) return false; buildPlayerResponse(); sendTo(address, &m_playersResponse); return true; case A2S_RULES_REQUEST: if (challengeRequest(address, data, length)) return true; if (!validChallenge(address, data, length)) return false; buildRuleResponse(); sendTo(address, &m_rulesResponse); return true; } return false; } void ServerQueryThread::buildPlayerResponse() { int64_t now = Time::monotonicMilliseconds(); if (now < m_lastPlayersResponse + responseCacheTime) { return; } auto clientIds = m_universe->clientIds(); uint8_t cnt = (uint8_t)clientIds.count(); int32_t kills = 0; // Not currently supported float timeConnected = 60; // Not supported defaults to 1min m_playersResponse.clear(); m_playersResponse << A2S_HEAD_INT << A2S_PLAYER_REPLY << cnt; uint8_t i = 0; for (auto clientId : clientIds) { m_playersResponse << i++ << m_universe->clientNick(clientId) << kills << timeConnected; } m_lastPlayersResponse = now; } void ServerQueryThread::buildRuleResponse() { int64_t now = Time::monotonicMilliseconds(); if (now < m_lastRulesResponse + responseCacheTime) { return; } uint16_t cnt = 1; m_rulesResponse.clear(); m_rulesResponse << A2S_HEAD_INT << A2S_RULES_REPLY << cnt << "plugins" << serverPlugins(); m_lastRulesResponse = now; } void ServerQueryThread::sendChallenge(HostAddressWithPort const& address) { auto challenge = make_shared(); m_validChallenges[address.address()] = challenge; m_generalResponse.clear(); m_generalResponse << A2S_HEAD_INT << A2S_CHALLENGE_RESPONSE << challenge->getChallenge(); sendTo(address, &m_generalResponse); } void ServerQueryThread::pruneChallenges() { int64_t now = Time::monotonicMilliseconds(); if (now < m_lastChallengeCheck + challengeCheckInterval) { return; } auto expire = now - challengeCheckInterval; auto it = makeSMutableMapIterator(m_validChallenges); while (it.hasNext()) { auto const& pair = it.next(); if (pair.second->before(expire)) { it.remove(); } } m_lastChallengeCheck = now; } void ServerQueryThread::run() { HostAddressWithPort udpAddress; char udpData[MaxUdpData]; while (!m_stop) { try { auto len = m_queryServer.receive(&udpAddress, udpData, MaxUdpData, 100); pruneChallenges(); if (len != 0) processPacket(udpAddress, udpData, len); } catch (SocketClosedException const&) { } catch (std::exception const& e) { Logger::error("ServerQueryThread exception caught: {}", outputException(e, true)); } } } ServerQueryThread::RequestChallenge::RequestChallenge() : m_time(Time::monotonicMilliseconds()), m_challenge(Random::randi32()) {} bool ServerQueryThread::RequestChallenge::before(uint64_t time) { return m_time < time; } int ServerQueryThread::RequestChallenge::getChallenge() { return m_challenge; } bool ServerQueryThread::validChallenge(HostAddressWithPort const& address, char const* data, size_t len) { if (len != 9) { // too much or too little data return false; } if (m_validChallenges.count(address.address()) == 0) { // Don't know this source address ignore return false; } uint8_t const* b = (uint8_t const*)data; int32_t challenge = ((int32_t)b[8] & 0xff) << 24 | ((int32_t)b[7] & 0xff) << 16 | ((int32_t)b[6] & 0xff) << 8 | ((int32_t)b[5] & 0xff); // Note: No byte order swapping needed as protcol performs no conversion if (m_validChallenges.get(address.address())->getChallenge() != challenge) { // Challenges didnt match ignore return false; } // All good return true; } bool ServerQueryThread::challengeRequest(HostAddressWithPort const& address, char const* data, size_t len) { if (len != 9) { // too much or too little data return false; } uint8_t const* buf = (uint8_t const*)data; if ((buf[5] == 0xff) && (buf[6] == 0xff) && (buf[7] == 0xff) && (buf[8] == 0xff)) { sendChallenge(address); return true; } return false; } }