From 87bd14cbe83c0eca42a6c15f4712415627941df2 Mon Sep 17 00:00:00 2001 From: pg9182 <96569817+pg9182@users.noreply.github.com> Date: Mon, 6 Mar 2023 12:02:53 -0500 Subject: Replace HTTP auth server with Atlas connectionless packet --- NorthstarDLL/engine/hoststate.cpp | 3 - NorthstarDLL/masterserver/masterserver.cpp | 173 +++++++++++++++++++++- NorthstarDLL/masterserver/masterserver.h | 3 + NorthstarDLL/server/auth/serverauthentication.cpp | 103 ++----------- NorthstarDLL/server/auth/serverauthentication.h | 9 +- NorthstarDLL/server/servernethooks.cpp | 40 +++-- NorthstarDLL/server/serverpresence.cpp | 9 +- NorthstarDLL/server/serverpresence.h | 3 - 8 files changed, 212 insertions(+), 131 deletions(-) (limited to 'NorthstarDLL') diff --git a/NorthstarDLL/engine/hoststate.cpp b/NorthstarDLL/engine/hoststate.cpp index d598bd2c..50862e99 100644 --- a/NorthstarDLL/engine/hoststate.cpp +++ b/NorthstarDLL/engine/hoststate.cpp @@ -87,7 +87,6 @@ void, __fastcall, (CHostState* self)) g_pServerPresence->SetPlaylist(GetCurrentPlaylistName()); g_pServerPresence->SetPort(Cvar_hostport->GetInt()); - g_pServerAuthentication->StartPlayerAuthServer(); g_pServerAuthentication->m_bNeedLocalAuthForNewgame = false; } @@ -115,7 +114,6 @@ void, __fastcall, (CHostState* self)) // no server presence, can't do it because no map name in hoststate // and also not super important for sp saves really - g_pServerAuthentication->StartPlayerAuthServer(); g_pServerAuthentication->m_bNeedLocalAuthForNewgame = false; } @@ -143,7 +141,6 @@ void, __fastcall, (CHostState* self)) spdlog::info("HostState: GameShutdown"); g_pServerPresence->DestroyPresence(); - g_pServerAuthentication->StopPlayerAuthServer(); CHostState__State_GameShutdown(self); diff --git a/NorthstarDLL/masterserver/masterserver.cpp b/NorthstarDLL/masterserver/masterserver.cpp index 9b439027..c71be961 100644 --- a/NorthstarDLL/masterserver/masterserver.cpp +++ b/NorthstarDLL/masterserver/masterserver.cpp @@ -7,6 +7,7 @@ #include "mods/modmanager.h" #include "shared/misccommands.h" #include "util/version.h" +#include "server/auth/bansystem.h" #include "rapidjson/document.h" #include "rapidjson/stringbuffer.h" @@ -783,7 +784,171 @@ void MasterServerManager::ProcessConnectionlessPacketSigreq1(std::string data) std::string type = obj["type"].GetString(); - // TODO + if (type == "connect") + { + if (!obj.HasMember("token") || !obj["token"].IsString()) + { + spdlog::error("failed to handle Atlas connect request: missing or invalid connection token field"); + return; + } + std::string token = obj["token"].GetString(); + + if (!m_handledServerConnections.contains(token)) + m_handledServerConnections.insert(token); + else + return; // already handled + + spdlog::info("handling Atlas connect request {}", data); + + if (!obj.HasMember("uid") || !obj["uid"].IsUint64()) + { + spdlog::error("failed to handle Atlas connect request {}: missing or invalid uid field", token); + return; + } + uint64_t uid = obj["uid"].GetUint64(); + + std::string username; + if (obj.HasMember("username") && obj["username"].IsString()) + username = obj["username"].GetString(); + + std::string reject; + if (!g_pBanSystem->IsUIDAllowed(uid)) + reject = "Banned from this server."; + + std::string pdata; + if (reject == "") + { + spdlog::info("getting pdata for connection {} (uid={} username={})", token, uid, username); + + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format("{}/server/connect?serverId={}&token={}", Cvar_ns_masterserver_hostname->GetString(), m_sOwnServerId, token) + .c_str()); + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &pdata); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLcode::CURLE_OK) + { + spdlog::error("failed to make Atlas connect pdata request {}: {}", token, curl_easy_strerror(result)); + curl_easy_cleanup(curl); + return; + } + + long respStatus = -1; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &respStatus); + + curl_easy_cleanup(curl); + + if (respStatus != 200) + { + rapidjson_document obj; + obj.Parse(pdata.c_str()); + + if (!obj.HasParseError() && obj.HasMember("error") && obj["error"].IsObject()) + spdlog::error( + "failed to make Atlas connect pdata request {}: response status {}, error: {} ({})", + token, + respStatus, + ((obj["error"].HasMember("enum") && obj["error"]["enum"].IsString()) ? obj["error"]["enum"].GetString() : ""), + ((obj["error"].HasMember("msg") && obj["error"]["msg"].IsString()) ? obj["error"]["msg"].GetString() : "")); + else + spdlog::error("failed to make Atlas connect pdata request {}: response status {}", token, respStatus); + return; + } + + if (!pdata.length()) + { + spdlog::error("failed to make Atlas connect pdata request {}: pdata response is empty", token); + return; + } + + if (pdata.length() > R2::PERSISTENCE_MAX_SIZE) + { + spdlog::error( + "failed to make Atlas connect pdata request {}: pdata is too large (max={} len={})", + token, + R2::PERSISTENCE_MAX_SIZE, + pdata.length()); + return; + } + } + + if (reject == "") + spdlog::info("accepting connection {} (uid={} username={}) with {} bytes of pdata", token, uid, username, pdata.length()); + else + spdlog::info("rejecting connection {} (uid={} username={}) with reason \"{}\"", token, uid, username, reject); + + if (reject == "") + g_pServerAuthentication->AddRemotePlayer(token, uid, username, pdata); + + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + char* rejectEnc = curl_easy_escape(curl, reject.c_str(), reject.length()); + if (!rejectEnc) + { + spdlog::error("failed to handle Atlas connect request {}: failed to escape reject", token); + return; + } + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/connect?serverId={}&token={}&reject={}", + Cvar_ns_masterserver_hostname->GetString(), + m_sOwnServerId, + token, + rejectEnc) + .c_str()); + curl_free(rejectEnc); + + // note: we don't actually have any POST data, so we can't use CURLOPT_POST or the behavior is undefined (e.g., hangs in wine) + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + + std::string buf; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLcode::CURLE_OK) + { + spdlog::error("failed to respond to Atlas connect request {}: {}", token, curl_easy_strerror(result)); + curl_easy_cleanup(curl); + return; + } + + long respStatus = -1; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &respStatus); + + curl_easy_cleanup(curl); + + if (respStatus != 200) + { + rapidjson_document obj; + obj.Parse(buf.c_str()); + + if (!obj.HasParseError() && obj.HasMember("error") && obj["error"].IsObject()) + spdlog::error( + "failed to respond to Atlas connect request {}: response status {}, error: {} ({})", + token, + respStatus, + ((obj["error"].HasMember("enum") && obj["error"]["enum"].IsString()) ? obj["error"]["enum"].GetString() : ""), + ((obj["error"].HasMember("msg") && obj["error"]["msg"].IsString()) ? obj["error"]["msg"].GetString() : "")); + else + spdlog::error("failed to respond to Atlas connect request {}: response status {}", token, respStatus); + return; + } + } + + return; + } spdlog::error("invalid Atlas connectionless packet request: unknown type {}", type); } @@ -1037,10 +1202,9 @@ void MasterServerPresenceReporter::InternalAddServer(const ServerPresence* pServ CURLOPT_URL, fmt::format( "{}/server/" - "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", + "add_server?port={}&authPort=udp&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", hostname.c_str(), threadedPresence.m_iPort, - threadedPresence.m_iAuthPort, nameEscaped, descEscaped, mapEscaped, @@ -1185,12 +1349,11 @@ void MasterServerPresenceReporter::InternalUpdateServer(const ServerPresence* pS CURLOPT_URL, fmt::format( "{}/server/" - "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&" + "update_values?id={}&port={}&authPort=udp&name={}&description={}&map={}&playlist={}&playerCount={}&" "maxPlayers={}&password={}", hostname.c_str(), serverId.c_str(), threadedPresence.m_iPort, - threadedPresence.m_iAuthPort, nameEscaped, descEscaped, mapEscaped, diff --git a/NorthstarDLL/masterserver/masterserver.h b/NorthstarDLL/masterserver/masterserver.h index 5cd6a695..e87b31a2 100644 --- a/NorthstarDLL/masterserver/masterserver.h +++ b/NorthstarDLL/masterserver/masterserver.h @@ -6,6 +6,7 @@ #include #include #include +#include extern ConVar* Cvar_ns_masterserver_hostname; extern ConVar* Cvar_ns_curl_log_enable; @@ -116,6 +117,8 @@ class MasterServerManager std::optional m_currentServer; std::string m_sCurrentServerPassword; + std::unordered_set m_handledServerConnections; + public: MasterServerManager(); diff --git a/NorthstarDLL/server/auth/serverauthentication.cpp b/NorthstarDLL/server/auth/serverauthentication.cpp index 4d9eb19f..8f62e1dd 100644 --- a/NorthstarDLL/server/auth/serverauthentication.cpp +++ b/NorthstarDLL/server/auth/serverauthentication.cpp @@ -14,104 +14,30 @@ #include "client/r2client.h" #include "server/r2server.h" -#include "httplib.h" - #include #include +#include #include AUTOHOOK_INIT() -const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!"; - // global vars ServerAuthenticationManager* g_pServerAuthentication; CBaseServer__RejectConnectionType CBaseServer__RejectConnection; -void ServerAuthenticationManager::StartPlayerAuthServer() +void ServerAuthenticationManager::AddRemotePlayer(std::string token, uint64_t uid, std::string username, std::string pdata) { - if (m_bRunningPlayerAuthThread) - { - spdlog::warn("ServerAuthenticationManager::StartPlayerAuthServer was called while m_bRunningPlayerAuthThread is true"); - return; - } - - g_pServerPresence->SetAuthPort(Cvar_ns_player_auth_port->GetInt()); // set auth port for presence - m_bRunningPlayerAuthThread = true; - - // listen is a blocking call so thread this - std::thread serverThread( - [this] - { - // this is just a super basic way to verify that servers have ports open, masterserver will try to read this before ensuring - // server is legit - m_PlayerAuthServer.Get( - "/verify", - [](const httplib::Request& request, httplib::Response& response) - { response.set_content(AUTHSERVER_VERIFY_STRING, "text/plain"); }); - - m_PlayerAuthServer.Post( - "/authenticate_incoming_player", - [this](const httplib::Request& request, httplib::Response& response) - { - if (!request.has_param("id") || !request.has_param("authToken") || request.body.size() >= R2::PERSISTENCE_MAX_SIZE || - !request.has_param("serverAuthToken") || - strcmp(g_pMasterServerManager->m_sOwnServerAuthToken, request.get_param_value("serverAuthToken").c_str())) - { - response.set_content("{\"success\":false}", "application/json"); - return; - } - - uint64_t uid; - try - { - uid = std::strtoull(request.get_param_value("id").c_str(), nullptr, 10); - } - catch (std::exception const& ex) - { - response.set_content("{\"success\":false}", "application/json"); - return; - } - if (!g_pBanSystem->IsUIDAllowed(uid)) - { - response.set_content("{\"success\":false,\"reject\":\"Banned from this server.\"}", "application/json"); - return; - } - - RemoteAuthData newAuthData {}; - strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), request.get_param_value("id").c_str(), sizeof(newAuthData.uid) - 1); - strncpy_s( - newAuthData.username, - sizeof(newAuthData.username), - request.get_param_value("username").c_str(), - sizeof(newAuthData.username) - 1); - - newAuthData.pdataSize = request.body.size(); - newAuthData.pdata = new char[newAuthData.pdataSize]; - memcpy(newAuthData.pdata, request.body.c_str(), newAuthData.pdataSize); - - std::lock_guard guard(m_AuthDataMutex); - m_RemoteAuthenticationData.insert(std::make_pair(request.get_param_value("authToken"), newAuthData)); - - response.set_content("{\"success\":true}", "application/json"); - }); - - m_PlayerAuthServer.listen("0.0.0.0", Cvar_ns_player_auth_port->GetInt()); - }); - - serverThread.detach(); -} + std::string uidS = std::to_string(uid); -void ServerAuthenticationManager::StopPlayerAuthServer() -{ - if (!m_bRunningPlayerAuthThread) - { - spdlog::warn("ServerAuthenticationManager::StopPlayerAuthServer was called while m_bRunningPlayerAuthThread is false"); - return; - } + RemoteAuthData newAuthData {}; + strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), uidS.c_str(), uidS.length()); + strncpy_s(newAuthData.username, sizeof(newAuthData.username), username.c_str(), username.length()); + newAuthData.pdata = new char[pdata.length()]; + newAuthData.pdataSize = pdata.length(); + memcpy(newAuthData.pdata, pdata.c_str(), newAuthData.pdataSize); - m_bRunningPlayerAuthThread = false; - m_PlayerAuthServer.stop(); + std::lock_guard guard(m_AuthDataMutex); + m_RemoteAuthenticationData[token] = newAuthData; } void ServerAuthenticationManager::AddPlayer(R2::CBaseClient* pPlayer, const char* pToken) @@ -323,11 +249,11 @@ bool,, (R2::CBaseClient* self, char* pName, void* pNetChannel, char bFakePlayer, if (!bFakePlayer) { if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, pName, pVerifiedName)) - pAuthenticationFailure = "Invalid name."; + pAuthenticationFailure = "Invalid Name."; else if (!g_pBanSystem->IsUIDAllowed(iNextPlayerUid)) - pAuthenticationFailure = "Banned from this server."; + pAuthenticationFailure = "Banned From server."; else if (!g_pServerAuthentication->CheckAuthentication(self, iNextPlayerUid, pNextPlayerToken)) - pAuthenticationFailure = "Authentication failed."; + pAuthenticationFailure = "Authentication Failed."; } else // need to copy name for bots still strncpy_s(pVerifiedName, pName, 63); @@ -423,7 +349,6 @@ ON_DLL_LOAD_RELIESON("engine.dll", ServerAuthentication, (ConCommand, ConVar), ( g_pServerAuthentication = new ServerAuthenticationManager; - g_pServerAuthentication->Cvar_ns_player_auth_port = new ConVar("ns_player_auth_port", "8081", FCVAR_GAMEDLL, ""); g_pServerAuthentication->Cvar_ns_erase_auth_info = new ConVar("ns_erase_auth_info", "1", FCVAR_GAMEDLL, "Whether auth info should be erased from this server on disconnect or crash"); g_pServerAuthentication->Cvar_ns_auth_allow_insecure = diff --git a/NorthstarDLL/server/auth/serverauthentication.h b/NorthstarDLL/server/auth/serverauthentication.h index 2ca07a8a..dd0e13af 100644 --- a/NorthstarDLL/server/auth/serverauthentication.h +++ b/NorthstarDLL/server/auth/serverauthentication.h @@ -1,6 +1,5 @@ #pragma once #include "core/convar/convar.h" -#include "httplib.h" #include "engine/r2engine.h" #include #include @@ -27,11 +26,7 @@ extern CBaseServer__RejectConnectionType CBaseServer__RejectConnection; class ServerAuthenticationManager { - private: - httplib::Server m_PlayerAuthServer; - public: - ConVar* Cvar_ns_player_auth_port; ConVar* Cvar_ns_erase_auth_info; ConVar* Cvar_ns_auth_allow_insecure; ConVar* Cvar_ns_auth_allow_insecure_write; @@ -41,14 +36,12 @@ class ServerAuthenticationManager std::unordered_map m_PlayerAuthenticationData; bool m_bAllowDuplicateAccounts = false; - bool m_bRunningPlayerAuthThread = false; bool m_bNeedLocalAuthForNewgame = false; bool m_bForceResetLocalPlayerPersistence = false; bool m_bStartingLocalSPGame = false; public: - void StartPlayerAuthServer(); - void StopPlayerAuthServer(); + void AddRemotePlayer(std::string token, uint64_t uid, std::string username, std::string pdata); void AddPlayer(R2::CBaseClient* pPlayer, const char* pAuthToken); void RemovePlayer(R2::CBaseClient* pPlayer); diff --git a/NorthstarDLL/server/servernethooks.cpp b/NorthstarDLL/server/servernethooks.cpp index c367c1e1..905d3251 100644 --- a/NorthstarDLL/server/servernethooks.cpp +++ b/NorthstarDLL/server/servernethooks.cpp @@ -12,8 +12,8 @@ AUTOHOOK_INIT() static ConVar* Cvar_net_debug_atlas_packet; static ConVar* Cvar_net_debug_atlas_packet_insecure; -#define HMACSHA256_LEN (256 / 8) -BCRYPT_ALG_HANDLE HMACSHA256; +static BCRYPT_ALG_HANDLE HMACSHA256; +constexpr size_t HMACSHA256_LEN = 256 / 8; static bool InitHMACSHA256() { @@ -42,13 +42,10 @@ static bool InitHMACSHA256() return true; } -// note: all Atlas connectionless packets should be idempotent so multiple attempts can be made to mitigate packet loss -// note: all long-running Atlas connectionless packet handlers should be started in a new thread (with copies of the data) to avoid blocking -// networking - +// compare the HMAC-SHA256(data, key) against sig (note: all strings are treated as raw binary data) static bool VerifyHMACSHA256(std::string key, std::string sig, std::string data) { - bool result = false; + uint8_t invalid = 1; char hash[HMACSHA256_LEN]; NTSTATUS status; @@ -72,18 +69,21 @@ static bool VerifyHMACSHA256(std::string key, std::string sig, std::string data) goto cleanup; } - if (std::string(hash, sizeof(hash)) == sig) + // constant-time compare + if (sig.length() == sizeof(hash)) { - result = true; - goto cleanup; + invalid = 0; + for (size_t i = 0; i < sizeof(hash); i++) + invalid |= (uint8_t)(sig[i]) ^ (uint8_t)(hash[i]); } cleanup: if (h) BCryptDestroyHash(h); - return result; + return !invalid; } +// v1 HMACSHA256-signed masterserver request (HMAC-SHA256(JSONData, MasterServerToken) + JSONData) static void ProcessAtlasConnectionlessPacketSigreq1(R2::netpacket_t* packet, bool dbg, std::string pType, std::string pData) { if (pData.length() < HMACSHA256_LEN) @@ -129,7 +129,7 @@ static void ProcessAtlasConnectionlessPacketSigreq1(R2::netpacket_t* packet, boo if (dbg) spdlog::info("got Atlas connectionless packet (size={} type={} data={})", packet->size, pType, pData); - std::thread t([pData] { g_pMasterServerManager->ProcessConnectionlessPacketSigreq1(pData); }); + std::thread t(&MasterServerManager::ProcessConnectionlessPacketSigreq1, g_pMasterServerManager, pData); t.detach(); return; @@ -152,6 +152,10 @@ static void ProcessAtlasConnectionlessPacket(R2::netpacket_t* packet) } } + // note: all Atlas connectionless packets should be idempotent so multiple attempts can be made to mitigate packet loss + // note: all long-running Atlas connectionless packet handlers should be started in a new thread (with copies of the data) to avoid + // blocking networking + // v1 HMACSHA256-signed masterserver request if (pType == "sigreq1") { @@ -166,13 +170,22 @@ static void ProcessAtlasConnectionlessPacket(R2::netpacket_t* packet) AUTOHOOK(ProcessConnectionlessPacket, engine.dll + 0x117800, bool, , (void* a1, R2::netpacket_t* packet)) { + // packet->data consists of 0xFFFFFFFF (int32 -1) to indicate packets aren't split, followed by a header consisting of a single + // character, which is used to uniquely identify the packet kind. Most kinds follow this with a null-terminated string payload + // then an arbitrary amoount of data. + + // T (no rate limits since we authenticate packets before doing anything expensive) if (4 < packet->size && packet->data[4] == 'T') { ProcessAtlasConnectionlessPacket(packet); return false; } + + // check rate limits for the original unconnected packets if (!g_pServerLimits->CheckConnectionlessPacketLimits(packet)) return false; + + // A, H, I, N return ProcessConnectionlessPacket(a1, packet); } @@ -202,7 +215,4 @@ ON_DLL_LOAD_RELIESON("engine.dll", ServerNetHooks, ConVar, (CModule module)) "0", FCVAR_NONE, "Whether to disable signature verification for Atlas connectionless packets (DANGEROUS: this allows anyone to impersonate Atlas)"); - - if (Cvar_net_debug_atlas_packet_insecure->GetBool()) - spdlog::warn("DANGEROUS: Atlas connectionless packet signature verification disabled; anyone will be able to impersonate Atlas"); } diff --git a/NorthstarDLL/server/serverpresence.cpp b/NorthstarDLL/server/serverpresence.cpp index e0b29cb6..945f5810 100644 --- a/NorthstarDLL/server/serverpresence.cpp +++ b/NorthstarDLL/server/serverpresence.cpp @@ -106,9 +106,8 @@ void ServerPresenceManager::AddPresenceReporter(ServerPresenceReporter* reporter void ServerPresenceManager::CreatePresence() { // reset presence fields that rely on runtime server state - // these being: port/auth port, map/playlist name, and playercount/maxplayers + // these being: port, map/playlist name, and playercount/maxplayers m_ServerPresence.m_iPort = 0; - m_ServerPresence.m_iAuthPort = 0; m_ServerPresence.m_iPlayerCount = 0; // this should actually be 0 at this point, so shouldn't need updating later m_ServerPresence.m_iMaxPlayers = 0; @@ -170,12 +169,6 @@ void ServerPresenceManager::SetPort(const int iPort) m_ServerPresence.m_iPort = iPort; } -void ServerPresenceManager::SetAuthPort(const int iAuthPort) -{ - // update authport - m_ServerPresence.m_iAuthPort = iAuthPort; -} - void ServerPresenceManager::SetName(const std::string sServerNameUnicode) { // update name diff --git a/NorthstarDLL/server/serverpresence.h b/NorthstarDLL/server/serverpresence.h index 3df2b68d..3aabecde 100644 --- a/NorthstarDLL/server/serverpresence.h +++ b/NorthstarDLL/server/serverpresence.h @@ -5,7 +5,6 @@ struct ServerPresence { public: int m_iPort; - int m_iAuthPort; std::string m_sServerId; @@ -28,7 +27,6 @@ struct ServerPresence ServerPresence(const ServerPresence* obj) { m_iPort = obj->m_iPort; - m_iAuthPort = obj->m_iAuthPort; m_sServerId = obj->m_sServerId; @@ -83,7 +81,6 @@ class ServerPresenceManager void RunFrame(double flCurrentTime); void SetPort(const int iPort); - void SetAuthPort(const int iPort); void SetName(const std::string sServerNameUnicode); void SetDescription(const std::string sServerDescUnicode); -- cgit v1.2.3