From 2f17ae5c43a8012376b9605ff4c8e8ce6e4b5223 Mon Sep 17 00:00:00 2001 From: Nemirtingas Date: Fri, 6 Dec 2019 11:09:33 +0100 Subject: [PATCH 1/2] Source Query Support --- build_win_find_interfaces.bat | 2 +- dll/network.cpp | 101 ++++++++++++- dll/network.h | 7 +- dll/settings.h | 3 + dll/source_query.cpp | 265 ++++++++++++++++++++++++++++++++++ dll/source_query.h | 32 ++++ dll/steam_gameserver.cpp | 83 ++++++++++- dll/steam_gameserver.h | 17 ++- dll/steam_matchmaking.h | 5 + dll/steam_utils.h | 5 + 10 files changed, 507 insertions(+), 13 deletions(-) create mode 100644 dll/source_query.cpp create mode 100644 dll/source_query.h diff --git a/build_win_find_interfaces.bat b/build_win_find_interfaces.bat index 53c157b..cb1cb48 100755 --- a/build_win_find_interfaces.bat +++ b/build_win_find_interfaces.bat @@ -1,4 +1,4 @@ -@ecbo off +@echo off cd /d "%~dp0" mkdir release\tools del /Q release\tools\* diff --git a/dll/network.cpp b/dll/network.cpp index e726a5c..e23223d 100644 --- a/dll/network.cpp +++ b/dll/network.cpp @@ -16,6 +16,7 @@ . */ #include "network.h" +#include "dll.h" #if defined(STEAM_WIN32) @@ -195,25 +196,26 @@ static int set_socket_nonblocking(sock_t sock) static void kill_socket(sock_t sock) { + if (is_socket_valid(sock)) + { #if defined(STEAM_WIN32) - closesocket(sock); + closesocket(sock); #else - close(sock); + close(sock); #endif + } } static void kill_tcp_socket(struct TCP_Socket &socket) { - if (is_socket_valid(socket.sock)) { - kill_socket(socket.sock); - } + kill_socket(socket.sock); socket = TCP_Socket(); } -static bool initialed; static void run_at_startup() { + static bool initialed = false; if (initialed) { return; } @@ -864,6 +866,22 @@ void Networking::Run() char data[2048]; int len; + if (query_alive && is_socket_valid(query_socket)) { + PRINT_DEBUG("RECV QUERY\n"); + Steam_Client* client = get_steam_client(); + sockaddr_in addr; + addr.sin_family = AF_INET; + + while ((len = receive_packet(query_socket, &ip_port, data, sizeof(data))) >= 0) { + client->steam_gameserver->HandleIncomingPacket(data, len, htonl(ip_port.ip), htons(ip_port.port)); + len = client->steam_gameserver->GetNextOutgoingPacket(data, sizeof(data), &ip_port.ip, &ip_port.port); + + addr.sin_addr.s_addr = htonl(ip_port.ip); + addr.sin_port = htons(ip_port.port); + sendto(query_socket, data, len, 0, (sockaddr*)&addr, sizeof(addr)); + } + } + PRINT_DEBUG("RECV UDP\n"); while((len = receive_packet(udp_socket, &ip_port, data, sizeof(data))) >= 0) { PRINT_DEBUG("recv %i %hhu.%hhu.%hhu.%hhu:%hu\n", len, ((unsigned char *)&ip_port.ip)[0], ((unsigned char *)&ip_port.ip)[1], ((unsigned char *)&ip_port.ip)[2], ((unsigned char *)&ip_port.ip)[3], htons(ip_port.port)); @@ -1215,3 +1233,74 @@ bool Networking::isAlive() { return alive; } + +void Networking::startQuery(IP_PORT ip_port) +{ + if (ip_port.port <= 1024) + return; + + if (!query_alive) + { + if (ip_port.port == MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE) + { + PRINT_DEBUG("Source Query in Shared Mode\n"); + return; + } + + int retry = 0; + constexpr auto max_retry = 10; + + while (retry++ < max_retry) + { + query_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (is_socket_valid(query_socket)) + break; + if (retry > max_retry) + { + reset_last_error(); + return; + } + } + retry = 0; + + sockaddr_in addr; + addr.sin_addr.s_addr = htonl(ip_port.ip); + addr.sin_port = htons(ip_port.port); + addr.sin_family = AF_INET; + + while (retry++ < max_retry) + { + int res = bind(query_socket, (sockaddr*)&addr, sizeof(sockaddr_in)); + if (res == 0) + { + set_socket_nonblocking(query_socket); + break; + } + + if (retry >= max_retry) + { + kill_socket(query_socket); + query_socket = -1; + reset_last_error(); + return; + } + } + + char str_ip[16]; + inet_ntop(AF_INET, &(addr.sin_addr), str_ip, 16); + + PRINT_DEBUG("Started query server on %s:%d\n", str_ip, htons(addr.sin_port)); + } + query_alive = true; +} + +void Networking::shutDownQuery() +{ + query_alive = false; + kill_socket(query_socket); +} + +bool Networking::isQueryAlive() +{ + return query_alive; +} diff --git a/dll/network.h b/dll/network.h index 1a8c064..aeb5a46 100644 --- a/dll/network.h +++ b/dll/network.h @@ -89,9 +89,10 @@ struct Connection { class Networking { bool enabled = false; + bool query_alive; bool alive; std::chrono::high_resolution_clock::time_point last_run; - sock_t udp_socket, tcp_socket; + sock_t query_socket, udp_socket, tcp_socket; uint16 udp_port, tcp_port; uint32 own_ip; std::vector connections; @@ -138,6 +139,10 @@ public: void shutDown(); bool isAlive(); + + void startQuery(IP_PORT ip_port); + void shutDownQuery(); + bool isQueryAlive(); }; #endif diff --git a/dll/settings.h b/dll/settings.h index cfb06b0..354c169 100644 --- a/dll/settings.h +++ b/dll/settings.h @@ -138,6 +138,9 @@ public: //networking bool disable_networking = false; + + //gameserver + bool disable_source_query = false; }; #endif diff --git a/dll/source_query.cpp b/dll/source_query.cpp new file mode 100644 index 0000000..9868fa0 --- /dev/null +++ b/dll/source_query.cpp @@ -0,0 +1,265 @@ +/* Copyright (C) 2019 Mr Goldberg, , Nemirtingas + This file is part of the Goldberg Emulator + + The Goldberg Emulator is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + The Goldberg Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the Goldberg Emulator; if not, see + . */ + +#include "source_query.h" +#include "dll.h" + +using lock_t = std::lock_guard; + +enum class source_query_magic : uint32_t +{ + simple = 0xFFFFFFFFul, + multi = 0xFFFFFFFEul, // <--- TODO ? +}; + +enum class source_query_header : uint8_t +{ + A2S_INFO = 'T', + A2S_PLAYER = 'U', + A2S_RULES = 'V', +}; + +enum class source_response_header : uint8_t +{ + A2S_CHALLENGE = 'A', + A2S_INFO = 'I', + A2S_PLAYER = 'D', + A2S_RULES = 'E', +}; + +enum class source_server_type : uint8_t +{ + dedicated = 'd', + non_dedicated = 'i', + source_tc = 'p', +}; + +enum class source_server_env : uint8_t +{ + linux = 'l', + windows = 'w', + old_mac = 'm', + mac = 'o', +}; + +enum class source_server_visibility : uint8_t +{ + _public = 0, + _private = 1, +}; + +enum class source_server_vac : uint8_t +{ + unsecured = 0, + secured = 1, +}; + +enum source_server_extra_flag : uint8_t +{ + none = 0x00, + gameid = 0x01, + steamid = 0x10, + keywords = 0x20, + spectator = 0x40, + port = 0x80, +}; + +#if defined(STEAM_WIN32) +static constexpr source_server_env my_server_env = source_server_env::windows; +#else +static constexpr source_server_env my_server_env = source_server_env::linux; +#endif + +#pragma pack(push) +#pragma pack(1) + +constexpr char a2s_info_payload[] = "Source Engine Query"; +constexpr size_t a2s_info_payload_size = sizeof(a2s_info_payload); + +struct source_query_data +{ + source_query_magic magic; + source_query_header header; + union + { + char a2s_info_payload[a2s_info_payload_size]; + uint32_t challenge; + }; +}; + +static constexpr size_t source_query_header_size = sizeof(source_query_magic) + sizeof(source_query_header); +static constexpr size_t a2s_query_info_size = source_query_header_size + sizeof(source_query_data::a2s_info_payload); +static constexpr size_t a2s_query_challenge_size = source_query_header_size + sizeof(source_query_data::challenge); + +#pragma pack(pop) + +void serialize_response(std::vector& buffer, const void* _data, size_t len) +{ + const uint8_t* data = reinterpret_cast(_data); + + buffer.insert(buffer.end(), data, data + len); +} + +template +void serialize_response(std::vector& buffer, T const& v) +{ + uint8_t const* data = reinterpret_cast(&v); + serialize_response(buffer, data, sizeof(T)); +} + +template<> +void serialize_response(std::vector& buffer, std::string const& v) +{ + uint8_t const* str = reinterpret_cast(v.c_str()); + serialize_response(buffer, str, v.length()+1); +} + +template +void serialize_response(std::vector & buffer, char(&str)[N]) +{ + serialize_response(buffer, reinterpret_cast(str), N); +} + +void get_challenge(std::vector &challenge_buff) +{ + // TODO: generate the challenge id + serialize_response(challenge_buff, source_query_magic::simple); + serialize_response(challenge_buff, source_response_header::A2S_CHALLENGE); + serialize_response(challenge_buff, static_cast(0x00112233ul)); +} + +std::vector Source_Query::handle_source_query(const void* buffer, size_t len, Gameserver const& gs) +{ + std::vector output_buffer; + + if (len < source_query_header_size) // its not at least 5 bytes long (0xFF 0xFF 0xFF 0xFF 0x??) + return output_buffer; + + source_query_data const& query = *reinterpret_cast(buffer); + + // || gs.max_player_count() == 0 + if (gs.offline() || query.magic != source_query_magic::simple) + return output_buffer; + + switch (query.header) + { + case source_query_header::A2S_INFO: + if (len >= a2s_query_info_size && !strncmp(query.a2s_info_payload, a2s_info_payload, a2s_info_payload_size)) + { + serialize_response(output_buffer, source_query_magic::simple); + serialize_response(output_buffer, source_response_header::A2S_INFO); + serialize_response(output_buffer, static_cast(2)); + serialize_response(output_buffer, gs.server_name()); + serialize_response(output_buffer, gs.map_name()); + serialize_response(output_buffer, gs.mod_dir()); + serialize_response(output_buffer, gs.product()); + serialize_response(output_buffer, static_cast(gs.appid())); + serialize_response(output_buffer, static_cast(gs.num_players())); + serialize_response(output_buffer, static_cast(gs.max_player_count())); + serialize_response(output_buffer, static_cast(gs.bot_player_count())); + serialize_response(output_buffer, (gs.dedicated_server() ? source_server_type::dedicated : source_server_type::non_dedicated));; + serialize_response(output_buffer, my_server_env); + serialize_response(output_buffer, source_server_visibility::_public); + serialize_response(output_buffer, (gs.secure() ? source_server_vac::secured : source_server_vac::unsecured)); + serialize_response(output_buffer, std::to_string(gs.version())); + + uint8_t flags = source_server_extra_flag::none; + + if (gs.port() != 0) + flags |= source_server_extra_flag::port; + + if (gs.spectator_port() != 0) + flags |= source_server_extra_flag::spectator; + + if(CGameID(gs.appid()).IsValid()) + flags |= source_server_extra_flag::gameid; + + if (flags != source_server_extra_flag::none) + serialize_response(output_buffer, flags); + + if (flags & source_server_extra_flag::port) + serialize_response(output_buffer, static_cast(gs.port())); + + // add steamid + + if (flags & source_server_extra_flag::spectator) + { + serialize_response(output_buffer, static_cast(gs.spectator_port())); + serialize_response(output_buffer, gs.spectator_server_name()); + } + + // keywords + + if (flags & source_server_extra_flag::gameid) + serialize_response(output_buffer, CGameID(gs.appid()).ToUint64()); + + } + break; + + case source_query_header::A2S_PLAYER: + if (len >= a2s_query_challenge_size) + { + if (query.challenge == 0xFFFFFFFFul) + { + get_challenge(output_buffer); + } + else if (query.challenge == 0x00112233ul) + { + std::vector> const& players = *get_steam_client()->steam_gameserver->get_players(); + + serialize_response(output_buffer, source_query_magic::simple); + serialize_response(output_buffer, source_response_header::A2S_PLAYER); + serialize_response(output_buffer, static_cast(players.size())); // num_players + + for (int i = 0; i < players.size(); ++i) + { + serialize_response(output_buffer, static_cast(i)); // player index + serialize_response(output_buffer, players[i].second.name); // player name + serialize_response(output_buffer, players[i].second.score); // player score + serialize_response(output_buffer, static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - players[i].second.join_time).count())); + } + + } + } + break; + + case source_query_header::A2S_RULES: + if (len >= a2s_query_challenge_size) + { + if (query.challenge == 0xFFFFFFFFul) + { + get_challenge(output_buffer); + } + else if (query.challenge == 0x00112233ul) + { + auto values = gs.values(); + + serialize_response(output_buffer, source_query_magic::simple); + serialize_response(output_buffer, source_response_header::A2S_RULES); + serialize_response(output_buffer, static_cast(values.size())); + + for (auto const& i : values) + { + serialize_response(output_buffer, i.first); + serialize_response(output_buffer, i.second); + } + } + } + break; + } + return output_buffer; +} diff --git a/dll/source_query.h b/dll/source_query.h new file mode 100644 index 0000000..eb4671a --- /dev/null +++ b/dll/source_query.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2019 Mr Goldberg + This file is part of the Goldberg Emulator + + The Goldberg Emulator is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + The Goldberg Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the Goldberg Emulator; if not, see + . */ + +#ifndef __INCLUDED_SOURCE_QUERY__ +#define __INCLUDED_SOURCE_QUERY__ + +#include "base.h" + +class Source_Query +{ + Source_Query () = delete; + ~Source_Query() = delete; + +public: + static std::vector handle_source_query(const void* buffer, size_t len, Gameserver const& gs); +}; + +#endif \ No newline at end of file diff --git a/dll/steam_gameserver.cpp b/dll/steam_gameserver.cpp index ef7f287..78c38e9 100644 --- a/dll/steam_gameserver.cpp +++ b/dll/steam_gameserver.cpp @@ -16,6 +16,7 @@ . */ #include "steam_gameserver.h" +#include "source_query.h" #define SEND_SERVER_RATE 5.0 @@ -33,6 +34,11 @@ Steam_GameServer::~Steam_GameServer() delete ticket_manager; } +std::vector>* Steam_GameServer::get_players() +{ + return &players; +} + // // Basic server data. These properties, if set, must be set before before calling LogOn. They // may not be changed after logged in. @@ -54,6 +60,10 @@ bool Steam_GameServer::InitGameServer( uint32 unIP, uint16 usGamePort, uint16 us server_data.set_port(usGamePort); server_data.set_query_port(usQueryPort); server_data.set_offline(false); + + if (!settings->disable_source_query) + network->startQuery({ unIP, usQueryPort }); + if (!settings->get_local_game_id().AppID()) settings->set_game_id(CGameID(nGameAppId)); //TODO: flags should be k_unServerFlag flags = unFlags; @@ -71,7 +81,7 @@ void Steam_GameServer::SetProduct( const char *pszProduct ) { PRINT_DEBUG("SetProduct\n"); std::lock_guard lock(global_mutex); - server_data.set_product(pszProduct); + server_data.set_product(pszProduct); // Set product to game name if this is empty } @@ -344,7 +354,22 @@ bool Steam_GameServer::SendUserConnectAndAuthenticate( uint32 unIPClient, const PRINT_DEBUG("SendUserConnectAndAuthenticate %u %u\n", unIPClient, cubAuthBlobSize); std::lock_guard lock(global_mutex); - return ticket_manager->SendUserConnectAndAuthenticate(unIPClient, pvAuthBlob, cubAuthBlobSize, pSteamIDUser); + bool res = ticket_manager->SendUserConnectAndAuthenticate(unIPClient, pvAuthBlob, cubAuthBlobSize, pSteamIDUser); + + if (res) + { + std::pair infos; + if( pSteamIDUser != nullptr) + infos.first = *pSteamIDUser; + + infos.second.join_time = std::chrono::steady_clock::now(); + infos.second.score = 0; + infos.second.name = "Player"; + + players.emplace_back(std::move(infos)); + } + + return res; } @@ -357,7 +382,16 @@ CSteamID Steam_GameServer::CreateUnauthenticatedUserConnection() PRINT_DEBUG("CreateUnauthenticatedUserConnection\n"); std::lock_guard lock(global_mutex); - return ticket_manager->fakeUser(); + CSteamID bot_id = ticket_manager->fakeUser(); + std::pair infos; + infos.first = bot_id; + infos.second.join_time = std::chrono::steady_clock::now(); + infos.second.score = 0; + infos.second.name = "Bot"; + + players.emplace_back(std::move(infos)); + + return bot_id; } @@ -369,6 +403,16 @@ void Steam_GameServer::SendUserDisconnect( CSteamID steamIDUser ) PRINT_DEBUG("SendUserDisconnect\n"); std::lock_guard lock(global_mutex); + auto player_it = std::find_if(players.begin(), players.end(), [&steamIDUser](std::pair& player) + { + return player.first == steamIDUser; + }); + + if (player_it != players.end()) + { + players.erase(player_it); + } + ticket_manager->endAuth(steamIDUser); } @@ -381,7 +425,21 @@ void Steam_GameServer::SendUserDisconnect( CSteamID steamIDUser ) bool Steam_GameServer::BUpdateUserData( CSteamID steamIDUser, const char *pchPlayerName, uint32 uScore ) { PRINT_DEBUG("BUpdateUserData\n"); - return true; + + auto player_it = std::find_if(players.begin(), players.end(), [&steamIDUser](std::pair& player) + { + return player.first == steamIDUser; + }); + + if (player_it != players.end()) + { + if( pchPlayerName != nullptr) + player_it->second.name = pchPlayerName; + + player_it->second.score = uScore; + return true; + } + return false; } // You shouldn't need to call this as it is called internally by SteamGameServer_Init() and can only be called once. @@ -570,6 +628,18 @@ bool Steam_GameServer::HandleIncomingPacket( const void *pData, int cbData, uint { PRINT_DEBUG("HandleIncomingPacket %i %X %i\n", cbData, srcIP, srcPort); std::lock_guard lock(global_mutex); + if (settings->disable_source_query) return true; + + Gameserver_Outgoing_Packet packet; + packet.data = std::move(Source_Query::handle_source_query(pData, cbData, server_data)); + if (packet.data.empty()) + return false; + + + packet.ip = srcIP; + packet.port = srcPort; + + outgoing_packets.emplace_back(std::move(packet)); return true; } @@ -582,6 +652,7 @@ int Steam_GameServer::GetNextOutgoingPacket( void *pOut, int cbMaxOut, uint32 *p { PRINT_DEBUG("GetNextOutgoingPacket\n"); std::lock_guard lock(global_mutex); + if (settings->disable_source_query) return 0; if (outgoing_packets.size() == 0) return 0; if (outgoing_packets.back().data.size() < cbMaxOut) cbMaxOut = outgoing_packets.back().data.size(); @@ -682,6 +753,10 @@ void Steam_GameServer::RunCallbacks() msg.set_allocated_gameserver(new Gameserver(server_data)); msg.mutable_gameserver()->set_offline(true); network->sendToAllIndividuals(&msg, true); + // Shutdown Source Query + network->shutDownQuery(); + // And empty the queue if needed + outgoing_packets.clear(); } } } diff --git a/dll/steam_gameserver.h b/dll/steam_gameserver.h index a6a6f11..3439829 100644 --- a/dll/steam_gameserver.h +++ b/dll/steam_gameserver.h @@ -15,6 +15,9 @@ License along with the Goldberg Emulator; if not, see . */ +#ifndef __INCLUDED_GAMESERVER__ +#define __INCLUDED_GAMESERVER__ + #include "base.h" //----------------------------------------------------------------------------- @@ -22,12 +25,18 @@ //----------------------------------------------------------------------------- struct Gameserver_Outgoing_Packet { - std::string data; + std::vector data; uint32 ip; uint16 port; }; +struct Gameserver_Player_Info_t { + std::chrono::steady_clock::time_point join_time; + std::string name; + uint32 score; +}; + class Steam_GameServer : public ISteamGameServer005, public ISteamGameServer008, @@ -46,6 +55,7 @@ public ISteamGameServer bool logged_in = false; bool call_servers_disconnected = false; Gameserver server_data; + std::vector> players; uint32 flags; bool policy_response_called; @@ -58,6 +68,9 @@ public: Steam_GameServer(class Settings *settings, class Networking *network, class SteamCallBacks *callbacks); ~Steam_GameServer(); + + std::vector>* get_players(); + // // Basic server data. These properties, if set, must be set before before calling LogOn. They // may not be changed after logged in. @@ -327,3 +340,5 @@ public: // void RunCallbacks(); }; + +#endif \ No newline at end of file diff --git a/dll/steam_matchmaking.h b/dll/steam_matchmaking.h index a21cb22..d224fba 100644 --- a/dll/steam_matchmaking.h +++ b/dll/steam_matchmaking.h @@ -15,6 +15,9 @@ License along with the Goldberg Emulator; if not, see . */ +#ifndef __INCLUDED_STEAM_MATCHMAKING__ +#define __INCLUDED_STEAM_MATCHMAKING__ + #include "base.h" #define SEND_LOBBY_RATE 5.0 @@ -1420,3 +1423,5 @@ void Callback(Common_Message *msg) }; + +#endif \ No newline at end of file diff --git a/dll/steam_utils.h b/dll/steam_utils.h index 4c65d0c..5b9bdc9 100644 --- a/dll/steam_utils.h +++ b/dll/steam_utils.h @@ -15,6 +15,9 @@ License along with the Goldberg Emulator; if not, see . */ +#ifndef __INCLUDED_STEAM_UTILS__ +#define __INCLUDED_STEAM_UTILS__ + #include "base.h" #include "local_storage.h" @@ -371,3 +374,5 @@ int FilterText( char* pchOutFilteredText, uint32 nByteSizeOutFilteredText, const } }; + +#endif \ No newline at end of file From 122979b276de86b4d2aab2ae3f09b72513e1289a Mon Sep 17 00:00:00 2001 From: Nemirtingas Date: Sun, 22 Dec 2019 14:16:27 +0000 Subject: [PATCH 2/2] Update source_query.cpp Its a short not an int --- dll/source_query.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dll/source_query.cpp b/dll/source_query.cpp index 9868fa0..9230a7b 100644 --- a/dll/source_query.cpp +++ b/dll/source_query.cpp @@ -250,7 +250,7 @@ std::vector Source_Query::handle_source_query(const void* buffer, size_ serialize_response(output_buffer, source_query_magic::simple); serialize_response(output_buffer, source_response_header::A2S_RULES); - serialize_response(output_buffer, static_cast(values.size())); + serialize_response(output_buffer, static_cast(values.size())); for (auto const& i : values) {