diff options
Diffstat (limited to 'NorthstarDLL/masterserver.cpp')
-rw-r--r-- | NorthstarDLL/masterserver.cpp | 2448 |
1 files changed, 1224 insertions, 1224 deletions
diff --git a/NorthstarDLL/masterserver.cpp b/NorthstarDLL/masterserver.cpp index 229b2f95..192a27f9 100644 --- a/NorthstarDLL/masterserver.cpp +++ b/NorthstarDLL/masterserver.cpp @@ -1,1225 +1,1225 @@ -#include "pch.h" -#include "masterserver.h" -#include "concommand.h" -#include "playlist.h" -#include "serverauthentication.h" -#include "hoststate.h" -#include "tier0.h" -#include "r2engine.h" -#include "modmanager.h" -#include "misccommands.h" -#include "version.h" - -#include "rapidjson/document.h" -#include "rapidjson/stringbuffer.h" -#include "rapidjson/writer.h" -#include "rapidjson/error/en.h" - -#include <cstring> -#include <regex> - -ConVar* Cvar_ns_masterserver_hostname; -ConVar* Cvar_ns_report_server_to_masterserver; -ConVar* Cvar_ns_report_sp_server_to_masterserver; - -ConVar* Cvar_ns_server_name; -ConVar* Cvar_ns_server_desc; -ConVar* Cvar_ns_server_password; - -ConVar* Cvar_ns_curl_log_enable; - -// Source ConVar -ConVar* Cvar_hostname; -ConVar* Cvar_hostport; - -MasterServerManager* g_MasterServerManager; - -// Convert a hex digit char to integer. -inline int hctod(char c) -{ - if (c >= 'A' && c <= 'F') - { - return c - 'A' + 10; - } - else if (c >= 'a' && c <= 'f') - { - return c - 'a' + 10; - } - else - { - return c - '0'; - } -} - -// This function interprets all 4-hexadecimal-digit unicode codepoint characters like \u4E2D to UTF-8 encoding. -std::string unescape_unicode(const std::string& str) -{ - std::string result; - std::regex r("\\\\u([a-f\\d]{4})", std::regex::icase); - auto matches_begin = std::sregex_iterator(str.begin(), str.end(), r); - auto matches_end = std::sregex_iterator(); - std::smatch last_match; - for (std::sregex_iterator i = matches_begin; i != matches_end; ++i) - { - last_match = *i; - result.append(last_match.prefix()); - unsigned int cp = 0; - for (int i = 2; i <= 5; ++i) - { - cp *= 16; - cp += hctod(last_match.str()[i]); - } - if (cp <= 0x7F) - { - result.push_back(cp); - } - else if (cp <= 0x7FF) - { - result.push_back((cp >> 6) | 0b11000000 & (~(1 << 5))); - result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); - } - else if (cp <= 0xFFFF) - { - result.push_back((cp >> 12) | 0b11100000 & (~(1 << 4))); - result.push_back((cp >> 6) & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); - result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); - } - } - if (!last_match.ready()) - { - return str; - } - else - { - result.append(last_match.suffix()); - } - return result; -} - -RemoteServerInfo::RemoteServerInfo( - const char* newId, - const char* newName, - const char* newDescription, - const char* newMap, - const char* newPlaylist, - int newPlayerCount, - int newMaxPlayers, - bool newRequiresPassword) -{ - // passworded servers don't have public ips - requiresPassword = newRequiresPassword; - - strncpy((char*)id, newId, sizeof(id)); - id[sizeof(id) - 1] = 0; - strncpy((char*)name, newName, sizeof(name)); - name[sizeof(name) - 1] = 0; - - description = std::string(newDescription); - - strncpy((char*)map, newMap, sizeof(map)); - map[sizeof(map) - 1] = 0; - strncpy((char*)playlist, newPlaylist, sizeof(playlist)); - playlist[sizeof(playlist) - 1] = 0; - - playerCount = newPlayerCount; - maxPlayers = newMaxPlayers; -} - -void MasterServerManager::SetCommonHttpClientOptions(CURL* curl) -{ - curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - curl_easy_setopt(curl, CURLOPT_VERBOSE, Cvar_ns_curl_log_enable->GetBool()); - curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); - // curl_easy_setopt(curl, CURLOPT_STDERR, stdout); - if (Tier0::CommandLine()->FindParm("-msinsecure")) // TODO: this check doesn't seem to work - { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - } -} - -void MasterServerManager::ClearServerList() -{ - // this doesn't really do anything lol, probably isn't threadsafe - m_bRequestingServerList = true; - - m_vRemoteServers.clear(); - - m_bRequestingServerList = false; -} - -size_t CurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) -{ - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -void MasterServerManager::AuthenticateOriginWithMasterServer(const char* uid, const char* originToken) -{ - if (m_bOriginAuthWithMasterServerInProgress) - return; - - // do this here so it's instantly set - m_bOriginAuthWithMasterServerInProgress = true; - std::string uidStr(uid); - std::string tokenStr(originToken); - - std::thread requestThread( - [this, uidStr, tokenStr]() - { - spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - std::string readBuffer; - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format("{}/client/origin_auth?id={}&token={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr).c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document originAuthInfo; - originAuthInfo.Parse(readBuffer.c_str()); - - if (originAuthInfo.HasParseError()) - { - spdlog::error( - "Failed reading origin auth info response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(originAuthInfo.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!originAuthInfo.IsObject() || !originAuthInfo.HasMember("success")) - { - spdlog::error("Failed reading origin auth info response: malformed response object {}", readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (originAuthInfo["success"].IsTrue() && originAuthInfo.HasMember("token") && originAuthInfo["token"].IsString()) - { - strncpy(m_sOwnClientAuthToken, originAuthInfo["token"].GetString(), sizeof(m_sOwnClientAuthToken)); - m_sOwnClientAuthToken[sizeof(m_sOwnClientAuthToken) - 1] = 0; - spdlog::info("Northstar origin authentication completed successfully!"); - } - else - spdlog::error("Northstar origin authentication failed"); - } - else - { - spdlog::error("Failed performing northstar origin auth: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - // we goto this instead of returning so we always hit this - REQUEST_END_CLEANUP: - m_bOriginAuthWithMasterServerInProgress = false; - m_bOriginAuthWithMasterServerDone = true; - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::RequestServerList() -{ - // do this here so it's instantly set on call for scripts - m_bScriptRequestingServerList = true; - - std::thread requestThread( - [this]() - { - // make sure we never have 2 threads writing at once - // i sure do hope this is actually threadsafe - while (m_bRequestingServerList) - Sleep(100); - - m_bRequestingServerList = true; - m_bScriptRequestingServerList = true; - - spdlog::info("Requesting server list from {}", Cvar_ns_masterserver_hostname->GetString()); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/servers", Cvar_ns_masterserver_hostname->GetString()).c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document serverInfoJson; - serverInfoJson.Parse(readBuffer.c_str()); - - if (serverInfoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(serverInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (serverInfoJson.IsObject() && serverInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!serverInfoJson.IsArray()) - { - spdlog::error("Failed reading masterserver response: root object is not an array"); - goto REQUEST_END_CLEANUP; - } - - rapidjson::GenericArray<false, rapidjson_document::GenericValue> serverArray = serverInfoJson.GetArray(); - - spdlog::info("Got {} servers", serverArray.Size()); - - for (auto& serverObj : serverArray) - { - if (!serverObj.IsObject()) - { - spdlog::error("Failed reading masterserver response: member of server array is not an object"); - goto REQUEST_END_CLEANUP; - } - - // todo: verify json props are fine before adding to m_remoteServers - if (!serverObj.HasMember("id") || !serverObj["id"].IsString() || !serverObj.HasMember("name") || - !serverObj["name"].IsString() || !serverObj.HasMember("description") || !serverObj["description"].IsString() || - !serverObj.HasMember("map") || !serverObj["map"].IsString() || !serverObj.HasMember("playlist") || - !serverObj["playlist"].IsString() || !serverObj.HasMember("playerCount") || !serverObj["playerCount"].IsNumber() || - !serverObj.HasMember("maxPlayers") || !serverObj["maxPlayers"].IsNumber() || !serverObj.HasMember("hasPassword") || - !serverObj["hasPassword"].IsBool() || !serverObj.HasMember("modInfo") || !serverObj["modInfo"].HasMember("Mods") || - !serverObj["modInfo"]["Mods"].IsArray()) - { - spdlog::error("Failed reading masterserver response: malformed server object"); - continue; - }; - - const char* id = serverObj["id"].GetString(); - - RemoteServerInfo* newServer = nullptr; - - bool createNewServerInfo = true; - for (RemoteServerInfo& server : m_vRemoteServers) - { - // if server already exists, update info rather than adding to it - if (!strncmp((const char*)server.id, id, 32)) - { - server = RemoteServerInfo( - id, - serverObj["name"].GetString(), - serverObj["description"].GetString(), - serverObj["map"].GetString(), - serverObj["playlist"].GetString(), - serverObj["playerCount"].GetInt(), - serverObj["maxPlayers"].GetInt(), - serverObj["hasPassword"].IsTrue()); - newServer = &server; - createNewServerInfo = false; - break; - } - } - - // server didn't exist - if (createNewServerInfo) - newServer = &m_vRemoteServers.emplace_back( - id, - serverObj["name"].GetString(), - serverObj["description"].GetString(), - serverObj["map"].GetString(), - serverObj["playlist"].GetString(), - serverObj["playerCount"].GetInt(), - serverObj["maxPlayers"].GetInt(), - serverObj["hasPassword"].IsTrue()); - - newServer->requiredMods.clear(); - for (auto& requiredMod : serverObj["modInfo"]["Mods"].GetArray()) - { - RemoteModInfo modInfo; - - if (!requiredMod.HasMember("RequiredOnClient") || !requiredMod["RequiredOnClient"].IsTrue()) - continue; - - if (!requiredMod.HasMember("Name") || !requiredMod["Name"].IsString()) - continue; - modInfo.Name = requiredMod["Name"].GetString(); - - if (!requiredMod.HasMember("Version") || !requiredMod["Version"].IsString()) - continue; - modInfo.Version = requiredMod["Version"].GetString(); - - newServer->requiredMods.push_back(modInfo); - } - // Can probably re-enable this later with a -verbose flag, but slows down loading of the server browser quite a bit as - // is - // spdlog::info( - // "Server {} on map {} with playlist {} has {}/{} players", serverObj["name"].GetString(), - // serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(), - // serverObj["maxPlayers"].GetInt()); - } - - std::sort( - m_vRemoteServers.begin(), - m_vRemoteServers.end(), - [](RemoteServerInfo& a, RemoteServerInfo& b) { return a.playerCount > b.playerCount; }); - } - else - { - spdlog::error("Failed requesting servers: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - // we goto this instead of returning so we always hit this - REQUEST_END_CLEANUP: - m_bRequestingServerList = false; - m_bScriptRequestingServerList = false; - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::RequestMainMenuPromos() -{ - m_bHasMainMenuPromoData = false; - - std::thread requestThread( - [this]() - { - while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone) - Sleep(500); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt( - curl, CURLOPT_URL, fmt::format("{}/client/mainmenupromos", Cvar_ns_masterserver_hostname->GetString()).c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document mainMenuPromoJson; - mainMenuPromoJson.Parse(readBuffer.c_str()); - - if (mainMenuPromoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver main menu promos response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(mainMenuPromoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!mainMenuPromoJson.IsObject()) - { - spdlog::error("Failed reading masterserver main menu promos response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (mainMenuPromoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!mainMenuPromoJson.HasMember("newInfo") || !mainMenuPromoJson["newInfo"].IsObject() || - !mainMenuPromoJson["newInfo"].HasMember("Title1") || !mainMenuPromoJson["newInfo"]["Title1"].IsString() || - !mainMenuPromoJson["newInfo"].HasMember("Title2") || !mainMenuPromoJson["newInfo"]["Title2"].IsString() || - !mainMenuPromoJson["newInfo"].HasMember("Title3") || !mainMenuPromoJson["newInfo"]["Title3"].IsString() || - - !mainMenuPromoJson.HasMember("largeButton") || !mainMenuPromoJson["largeButton"].IsObject() || - !mainMenuPromoJson["largeButton"].HasMember("Title") || !mainMenuPromoJson["largeButton"]["Title"].IsString() || - !mainMenuPromoJson["largeButton"].HasMember("Text") || !mainMenuPromoJson["largeButton"]["Text"].IsString() || - !mainMenuPromoJson["largeButton"].HasMember("Url") || !mainMenuPromoJson["largeButton"]["Url"].IsString() || - !mainMenuPromoJson["largeButton"].HasMember("ImageIndex") || - !mainMenuPromoJson["largeButton"]["ImageIndex"].IsNumber() || - - !mainMenuPromoJson.HasMember("smallButton1") || !mainMenuPromoJson["smallButton1"].IsObject() || - !mainMenuPromoJson["smallButton1"].HasMember("Title") || !mainMenuPromoJson["smallButton1"]["Title"].IsString() || - !mainMenuPromoJson["smallButton1"].HasMember("Url") || !mainMenuPromoJson["smallButton1"]["Url"].IsString() || - !mainMenuPromoJson["smallButton1"].HasMember("ImageIndex") || - !mainMenuPromoJson["smallButton1"]["ImageIndex"].IsNumber() || - - !mainMenuPromoJson.HasMember("smallButton2") || !mainMenuPromoJson["smallButton2"].IsObject() || - !mainMenuPromoJson["smallButton2"].HasMember("Title") || !mainMenuPromoJson["smallButton2"]["Title"].IsString() || - !mainMenuPromoJson["smallButton2"].HasMember("Url") || !mainMenuPromoJson["smallButton2"]["Url"].IsString() || - !mainMenuPromoJson["smallButton2"].HasMember("ImageIndex") || - !mainMenuPromoJson["smallButton2"]["ImageIndex"].IsNumber()) - { - spdlog::error("Failed reading masterserver main menu promos response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - m_sMainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString(); - m_sMainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString(); - m_sMainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString(); - - m_sMainMenuPromoData.largeButtonTitle = mainMenuPromoJson["largeButton"]["Title"].GetString(); - m_sMainMenuPromoData.largeButtonText = mainMenuPromoJson["largeButton"]["Text"].GetString(); - m_sMainMenuPromoData.largeButtonUrl = mainMenuPromoJson["largeButton"]["Url"].GetString(); - m_sMainMenuPromoData.largeButtonImageIndex = mainMenuPromoJson["largeButton"]["ImageIndex"].GetInt(); - - m_sMainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString(); - m_sMainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString(); - m_sMainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt(); - - m_sMainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString(); - m_sMainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString(); - m_sMainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt(); - - m_bHasMainMenuPromoData = true; - } - else - { - spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - REQUEST_END_CLEANUP: - // nothing lol - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::AuthenticateWithOwnServer(const char* uid, const char* playerToken) -{ - // dont wait, just stop if we're trying to do 2 auth requests at once - if (m_bAuthenticatingWithGameServer) - return; - - m_bAuthenticatingWithGameServer = true; - m_bScriptAuthenticatingWithGameServer = true; - m_bSuccessfullyAuthenticatedWithGameServer = false; - - std::string uidStr(uid); - std::string tokenStr(playerToken); - - std::thread requestThread( - [this, uidStr, tokenStr]() - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format("{}/client/auth_with_self?id={}&playerToken={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr) - .c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document authInfoJson; - authInfoJson.Parse(readBuffer.c_str()); - - if (authInfoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(authInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (authInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson["success"].IsTrue()) - { - spdlog::error("Authentication with masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson.HasMember("success") || !authInfoJson.HasMember("id") || !authInfoJson["id"].IsString() || - !authInfoJson.HasMember("authToken") || !authInfoJson["authToken"].IsString() || - !authInfoJson.HasMember("persistentData") || !authInfoJson["persistentData"].IsArray()) - { - spdlog::error("Failed reading masterserver authentication response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - AuthData newAuthData {}; - strncpy(newAuthData.uid, authInfoJson["id"].GetString(), sizeof(newAuthData.uid)); - newAuthData.uid[sizeof(newAuthData.uid) - 1] = 0; - - newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size(); - newAuthData.pdata = new char[newAuthData.pdataSize]; - // memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize); - - int i = 0; - // note: persistentData is a uint8array because i had problems getting strings to behave, it sucks but it's just how it be - // unfortunately potentially refactor later - for (auto& byte : authInfoJson["persistentData"].GetArray()) - { - if (!byte.IsUint() || byte.GetUint() > 255) - { - spdlog::error("Failed reading masterserver authentication response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - newAuthData.pdata[i++] = static_cast<char>(byte.GetUint()); - } - - std::lock_guard<std::mutex> guard(g_ServerAuthenticationManager->m_authDataMutex); - g_ServerAuthenticationManager->m_authData.clear(); - g_ServerAuthenticationManager->m_authData.insert(std::make_pair(authInfoJson["authToken"].GetString(), newAuthData)); - - m_bSuccessfullyAuthenticatedWithGameServer = true; - } - else - { - spdlog::error("Failed authenticating with own server: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - m_bSuccessfullyAuthenticatedWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - } - - REQUEST_END_CLEANUP: - m_bAuthenticatingWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - - if (m_bNewgameAfterSelfAuth) - { - // pretty sure this is threadsafe? - R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "ns_end_reauth_and_leave_to_lobby", R2::cmd_source_t::kCommandSrcCode); - m_bNewgameAfterSelfAuth = false; - } - - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::AuthenticateWithServer(const char* uid, const char* playerToken, const char* serverId, const char* password) -{ - // dont wait, just stop if we're trying to do 2 auth requests at once - if (m_bAuthenticatingWithGameServer) - return; - - m_bAuthenticatingWithGameServer = true; - m_bScriptAuthenticatingWithGameServer = true; - m_bSuccessfullyAuthenticatedWithGameServer = false; - - std::string uidStr(uid); - std::string tokenStr(playerToken); - std::string serverIdStr(serverId); - std::string passwordStr(password); - - std::thread requestThread( - [this, uidStr, tokenStr, serverIdStr, passwordStr]() - { - // esnure that any persistence saving is done, so we know masterserver has newest - while (m_bSavingPersistentData) - Sleep(100); - - spdlog::info("Attempting authentication with server of id \"{}\"", serverIdStr); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - { - char* escapedPassword = curl_easy_escape(curl, passwordStr.c_str(), passwordStr.length()); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/client/auth_with_server?id={}&playerToken={}&server={}&password={}", - Cvar_ns_masterserver_hostname->GetString(), - uidStr, - tokenStr, - serverIdStr, - escapedPassword) - .c_str()); - - curl_free(escapedPassword); - } - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document connectionInfoJson; - connectionInfoJson.Parse(readBuffer.c_str()); - - if (connectionInfoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(connectionInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!connectionInfoJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (connectionInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!connectionInfoJson["success"].IsTrue()) - { - spdlog::error("Authentication with masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } - - if (!connectionInfoJson.HasMember("success") || !connectionInfoJson.HasMember("ip") || - !connectionInfoJson["ip"].IsString() || !connectionInfoJson.HasMember("port") || - !connectionInfoJson["port"].IsNumber() || !connectionInfoJson.HasMember("authToken") || - !connectionInfoJson["authToken"].IsString()) - { - spdlog::error("Failed reading masterserver authentication response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - m_pendingConnectionInfo.ip.S_un.S_addr = inet_addr(connectionInfoJson["ip"].GetString()); - m_pendingConnectionInfo.port = (unsigned short)connectionInfoJson["port"].GetUint(); - - strncpy(m_pendingConnectionInfo.authToken, connectionInfoJson["authToken"].GetString(), 31); - m_pendingConnectionInfo.authToken[31] = 0; - - m_bHasPendingConnectionInfo = true; - m_bSuccessfullyAuthenticatedWithGameServer = true; - } - else - { - spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - m_bSuccessfullyAuthenticatedWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - } - - REQUEST_END_CLEANUP: - m_bAuthenticatingWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::AddSelfToServerList( - int port, - int authPort, - const char* name, - const char* description, - const char* map, - const char* playlist, - int maxPlayers, - const char* password) -{ - if (!Cvar_ns_report_server_to_masterserver->GetBool()) - return; - - if (!Cvar_ns_report_sp_server_to_masterserver->GetBool() && !strncmp(map, "sp_", 3)) - { - m_bRequireClientAuth = false; - return; - } - - m_bRequireClientAuth = true; - - std::string strName(name); - std::string strDescription(description); - std::string strMap(map); - std::string strPlaylist(playlist); - std::string strPassword(password); - - std::thread requestThread( - [this, port, authPort, strName, strDescription, strMap, strPlaylist, maxPlayers, strPassword] - { - m_sOwnServerId[0] = 0; - m_sOwnServerAuthToken[0] = 0; - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); - - curl_mime_data(part, m_sOwnModInfoJson.c_str(), m_sOwnModInfoJson.size()); - curl_mime_name(part, "modinfo"); - curl_mime_filename(part, "modinfo.json"); - curl_mime_type(part, "application/json"); - - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - - // format every paramter because computers hate me - { - char* nameEscaped = curl_easy_escape(curl, strName.c_str(), strName.length()); - char* descEscaped = curl_easy_escape(curl, strDescription.c_str(), strDescription.length()); - char* mapEscaped = curl_easy_escape(curl, strMap.c_str(), strMap.length()); - char* playlistEscaped = curl_easy_escape(curl, strPlaylist.c_str(), strPlaylist.length()); - char* passwordEscaped = curl_easy_escape(curl, strPassword.c_str(), strPassword.length()); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", - Cvar_ns_masterserver_hostname->GetString(), - port, - authPort, - nameEscaped, - descEscaped, - mapEscaped, - playlistEscaped, - maxPlayers, - passwordEscaped) - .c_str()); - - curl_free(nameEscaped); - curl_free(descEscaped); - curl_free(mapEscaped); - curl_free(playlistEscaped); - curl_free(passwordEscaped); - } - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document serverAddedJson; - serverAddedJson.Parse(readBuffer.c_str()); - - if (serverAddedJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(serverAddedJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!serverAddedJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (serverAddedJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!serverAddedJson["success"].IsTrue()) - { - spdlog::error("Adding server to masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } - - if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() || - !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString()) - { - spdlog::error("Failed reading masterserver response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - strncpy(m_sOwnServerId, serverAddedJson["id"].GetString(), sizeof(m_sOwnServerId)); - m_sOwnServerId[sizeof(m_sOwnServerId) - 1] = 0; - - strncpy(m_sOwnServerAuthToken, serverAddedJson["serverAuthToken"].GetString(), sizeof(m_sOwnServerAuthToken)); - m_sOwnServerAuthToken[sizeof(m_sOwnServerAuthToken) - 1] = 0; - - // heartbeat thread - // ideally this should actually be done in main thread, rather than on it's own thread, so it'd stop if server freezes - std::thread heartbeatThread( - [this] - { - Sleep(5000); - - // defensive check, as m_ownServer could be set to null during the Sleep(5000) above - if (!*m_sOwnServerId) - return; - - do - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); - - // send all registration info so we have all necessary info to reregister our server if masterserver goes down, - // without a restart this isn't threadsafe :terror: - { - char* escapedNameNew = curl_easy_escape(curl, g_MasterServerManager->m_sUnicodeServerName.c_str(), NULL); - char* escapedDescNew = curl_easy_escape(curl, g_MasterServerManager->m_sUnicodeServerDesc.c_str(), NULL); - char* escapedMapNew = curl_easy_escape(curl, R2::g_pHostState->m_levelName, NULL); - char* escapedPlaylistNew = curl_easy_escape(curl, R2::GetCurrentPlaylistName(), NULL); - char* escapedPasswordNew = curl_easy_escape(curl, Cvar_ns_server_password->GetString(), NULL); - - int maxPlayers = 6; - const char* maxPlayersVar = R2::GetCurrentPlaylistVar("max_players", false); - if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this - maxPlayers = std::stoi(maxPlayersVar); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/" - "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&" - "maxPlayers={}&password={}", - Cvar_ns_masterserver_hostname->GetString(), - m_sOwnServerId, - Cvar_hostport->GetInt(), - Cvar_ns_player_auth_port->GetInt(), - escapedNameNew, - escapedDescNew, - escapedMapNew, - escapedPlaylistNew, - g_ServerAuthenticationManager->m_additionalPlayerData.size(), - maxPlayers, - escapedPasswordNew) - .c_str()); - - curl_free(escapedNameNew); - curl_free(escapedDescNew); - curl_free(escapedMapNew); - curl_free(escapedPlaylistNew); - curl_free(escapedPasswordNew); - } - - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); - - curl_mime_data(part, m_sOwnModInfoJson.c_str(), m_sOwnModInfoJson.size()); - curl_mime_name(part, "modinfo"); - curl_mime_filename(part, "modinfo.json"); - curl_mime_type(part, "application/json"); - - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - - CURLcode result = curl_easy_perform(curl); - - // defensive check, as m_ownServerId could be set to null before this request gets processed - if (!*m_sOwnServerId) - return; - - if (result == CURLcode::CURLE_OK) - { - rapidjson_document serverAddedJson; - serverAddedJson.Parse(readBuffer.c_str()); - - if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject()) - { - if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString()) - { - strncpy(m_sOwnServerId, serverAddedJson["id"].GetString(), sizeof(m_sOwnServerId)); - m_sOwnServerId[sizeof(m_sOwnServerId) - 1] = 0; - } - - if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) - { - strncpy( - m_sOwnServerAuthToken, - serverAddedJson["serverAuthToken"].GetString(), - sizeof(m_sOwnServerAuthToken)); - m_sOwnServerAuthToken[sizeof(m_sOwnServerAuthToken) - 1] = 0; - } - } - } - else - spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); - - curl_easy_cleanup(curl); - Sleep(10000); - } while (*m_sOwnServerId); - }); - - heartbeatThread.detach(); - } - else - { - spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - REQUEST_END_CLEANUP: - curl_easy_cleanup(curl); - curl_mime_free(mime); - }); - - requestThread.detach(); -} - -void MasterServerManager::UpdateServerMapAndPlaylist(const char* map, const char* playlist, int maxPlayers) -{ - // dont call this if we don't have a server id - if (!*m_sOwnServerId) - return; - - std::string strMap(map); - std::string strPlaylist(playlist); - - std::thread requestThread( - [this, strMap, strPlaylist, maxPlayers] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - // escape params - { - char* mapEscaped = curl_easy_escape(curl, strMap.c_str(), strMap.length()); - char* playlistEscaped = curl_easy_escape(curl, strPlaylist.c_str(), strPlaylist.length()); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/update_values?id={}&map={}&playlist={}&maxPlayers={}", - Cvar_ns_masterserver_hostname->GetString(), - m_sOwnServerId, - mapEscaped, - playlistEscaped, - maxPlayers) - .c_str()); - - curl_free(mapEscaped); - curl_free(playlistEscaped); - } - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - m_bSuccessfullyConnected = true; - else - m_bSuccessfullyConnected = false; - - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::UpdateServerPlayerCount(int playerCount) -{ - // dont call this if we don't have a server id - if (!*m_sOwnServerId) - return; - - std::thread requestThread( - [this, playerCount] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/update_values?id={}&playerCount={}", Cvar_ns_masterserver_hostname->GetString(), m_sOwnServerId, playerCount) - .c_str()); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - m_bSuccessfullyConnected = true; - else - m_bSuccessfullyConnected = false; - - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::WritePlayerPersistentData(const char* playerId, const char* pdata, size_t pdataSize) -{ - // still call this if we don't have a server id, since lobbies that aren't port forwarded need to be able to call it - m_bSavingPersistentData = true; - if (!pdataSize) - { - spdlog::warn("attempted to write pdata of size 0!"); - return; - } - - std::string strPlayerId(playerId); - std::string strPdata(pdata, pdataSize); - - std::thread requestThread( - [this, strPlayerId, strPdata, pdataSize] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/accounts/write_persistence?id={}&serverId={}", - Cvar_ns_masterserver_hostname->GetString(), - strPlayerId, - m_sOwnServerId) - .c_str()); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); - - curl_mime_data(part, strPdata.c_str(), pdataSize); - curl_mime_name(part, "pdata"); - curl_mime_filename(part, "file.pdata"); - curl_mime_type(part, "application/octet-stream"); - - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - m_bSuccessfullyConnected = true; - else - m_bSuccessfullyConnected = false; - - curl_easy_cleanup(curl); - - m_bSavingPersistentData = false; - }); - - requestThread.detach(); -} - -void MasterServerManager::RemoveSelfFromServerList() -{ - // dont call this if we don't have a server id - if (!*m_sOwnServerId || !Cvar_ns_report_server_to_masterserver->GetBool()) - return; - - std::thread requestThread( - [this] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format("{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), m_sOwnServerId).c_str()); - - *m_sOwnServerId = 0; - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - m_bSuccessfullyConnected = true; - else - m_bSuccessfullyConnected = false; - - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void ConCommand_ns_fetchservers(const CCommand& args) -{ - g_MasterServerManager->RequestServerList(); -} - -MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {} - -ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, ConCommand, [](HMODULE baseAddress) -{ - g_MasterServerManager = new MasterServerManager; - - Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); - - Cvar_ns_server_name = new ConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, "This server's description", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { - g_MasterServerManager->m_sUnicodeServerName = unescape_unicode(Cvar_ns_server_name->GetString()); - }); - Cvar_ns_server_desc = new ConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, "This server's name", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { - g_MasterServerManager->m_sUnicodeServerName = unescape_unicode(Cvar_ns_server_desc->GetString()); - }); - - Cvar_ns_server_password = new ConVar("ns_server_password", "", FCVAR_GAMEDLL, "This server's password"); - Cvar_ns_report_server_to_masterserver = new ConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver"); - Cvar_ns_report_sp_server_to_masterserver = new ConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver, when started in singleplayer"); - - Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console"); - - Cvar_hostname = *(ConVar**)((char*)baseAddress + 0x1315bae8); - Cvar_hostport = (ConVar*)((char*)baseAddress + 0x13FA6070); - - RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL); +#include "pch.h"
+#include "masterserver.h"
+#include "concommand.h"
+#include "playlist.h"
+#include "serverauthentication.h"
+#include "hoststate.h"
+#include "tier0.h"
+#include "r2engine.h"
+#include "modmanager.h"
+#include "misccommands.h"
+#include "version.h"
+
+#include "rapidjson/document.h"
+#include "rapidjson/stringbuffer.h"
+#include "rapidjson/writer.h"
+#include "rapidjson/error/en.h"
+
+#include <cstring>
+#include <regex>
+
+ConVar* Cvar_ns_masterserver_hostname;
+ConVar* Cvar_ns_report_server_to_masterserver;
+ConVar* Cvar_ns_report_sp_server_to_masterserver;
+
+ConVar* Cvar_ns_server_name;
+ConVar* Cvar_ns_server_desc;
+ConVar* Cvar_ns_server_password;
+
+ConVar* Cvar_ns_curl_log_enable;
+
+// Source ConVar
+ConVar* Cvar_hostname;
+ConVar* Cvar_hostport;
+
+MasterServerManager* g_MasterServerManager;
+
+// Convert a hex digit char to integer.
+inline int hctod(char c)
+{
+ if (c >= 'A' && c <= 'F')
+ {
+ return c - 'A' + 10;
+ }
+ else if (c >= 'a' && c <= 'f')
+ {
+ return c - 'a' + 10;
+ }
+ else
+ {
+ return c - '0';
+ }
+}
+
+// This function interprets all 4-hexadecimal-digit unicode codepoint characters like \u4E2D to UTF-8 encoding.
+std::string unescape_unicode(const std::string& str)
+{
+ std::string result;
+ std::regex r("\\\\u([a-f\\d]{4})", std::regex::icase);
+ auto matches_begin = std::sregex_iterator(str.begin(), str.end(), r);
+ auto matches_end = std::sregex_iterator();
+ std::smatch last_match;
+ for (std::sregex_iterator i = matches_begin; i != matches_end; ++i)
+ {
+ last_match = *i;
+ result.append(last_match.prefix());
+ unsigned int cp = 0;
+ for (int i = 2; i <= 5; ++i)
+ {
+ cp *= 16;
+ cp += hctod(last_match.str()[i]);
+ }
+ if (cp <= 0x7F)
+ {
+ result.push_back(cp);
+ }
+ else if (cp <= 0x7FF)
+ {
+ result.push_back((cp >> 6) | 0b11000000 & (~(1 << 5)));
+ result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6)));
+ }
+ else if (cp <= 0xFFFF)
+ {
+ result.push_back((cp >> 12) | 0b11100000 & (~(1 << 4)));
+ result.push_back((cp >> 6) & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6)));
+ result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6)));
+ }
+ }
+ if (!last_match.ready())
+ {
+ return str;
+ }
+ else
+ {
+ result.append(last_match.suffix());
+ }
+ return result;
+}
+
+RemoteServerInfo::RemoteServerInfo(
+ const char* newId,
+ const char* newName,
+ const char* newDescription,
+ const char* newMap,
+ const char* newPlaylist,
+ int newPlayerCount,
+ int newMaxPlayers,
+ bool newRequiresPassword)
+{
+ // passworded servers don't have public ips
+ requiresPassword = newRequiresPassword;
+
+ strncpy((char*)id, newId, sizeof(id));
+ id[sizeof(id) - 1] = 0;
+ strncpy((char*)name, newName, sizeof(name));
+ name[sizeof(name) - 1] = 0;
+
+ description = std::string(newDescription);
+
+ strncpy((char*)map, newMap, sizeof(map));
+ map[sizeof(map) - 1] = 0;
+ strncpy((char*)playlist, newPlaylist, sizeof(playlist));
+ playlist[sizeof(playlist) - 1] = 0;
+
+ playerCount = newPlayerCount;
+ maxPlayers = newMaxPlayers;
+}
+
+void MasterServerManager::SetCommonHttpClientOptions(CURL* curl)
+{
+ curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, Cvar_ns_curl_log_enable->GetBool());
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent);
+ // curl_easy_setopt(curl, CURLOPT_STDERR, stdout);
+ if (Tier0::CommandLine()->FindParm("-msinsecure")) // TODO: this check doesn't seem to work
+ {
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
+ }
+}
+
+void MasterServerManager::ClearServerList()
+{
+ // this doesn't really do anything lol, probably isn't threadsafe
+ m_bRequestingServerList = true;
+
+ m_vRemoteServers.clear();
+
+ m_bRequestingServerList = false;
+}
+
+size_t CurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp)
+{
+ ((std::string*)userp)->append((char*)contents, size * nmemb);
+ return size * nmemb;
+}
+
+void MasterServerManager::AuthenticateOriginWithMasterServer(const char* uid, const char* originToken)
+{
+ if (m_bOriginAuthWithMasterServerInProgress)
+ return;
+
+ // do this here so it's instantly set
+ m_bOriginAuthWithMasterServerInProgress = true;
+ std::string uidStr(uid);
+ std::string tokenStr(originToken);
+
+ std::thread requestThread(
+ [this, uidStr, tokenStr]()
+ {
+ spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr);
+
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+ std::string readBuffer;
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format("{}/client/origin_auth?id={}&token={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr).c_str());
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ m_bSuccessfullyConnected = true;
+
+ rapidjson_document originAuthInfo;
+ originAuthInfo.Parse(readBuffer.c_str());
+
+ if (originAuthInfo.HasParseError())
+ {
+ spdlog::error(
+ "Failed reading origin auth info response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(originAuthInfo.GetParseError()));
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!originAuthInfo.IsObject() || !originAuthInfo.HasMember("success"))
+ {
+ spdlog::error("Failed reading origin auth info response: malformed response object {}", readBuffer);
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (originAuthInfo["success"].IsTrue() && originAuthInfo.HasMember("token") && originAuthInfo["token"].IsString())
+ {
+ strncpy(m_sOwnClientAuthToken, originAuthInfo["token"].GetString(), sizeof(m_sOwnClientAuthToken));
+ m_sOwnClientAuthToken[sizeof(m_sOwnClientAuthToken) - 1] = 0;
+ spdlog::info("Northstar origin authentication completed successfully!");
+ }
+ else
+ spdlog::error("Northstar origin authentication failed");
+ }
+ else
+ {
+ spdlog::error("Failed performing northstar origin auth: error {}", curl_easy_strerror(result));
+ m_bSuccessfullyConnected = false;
+ }
+
+ // we goto this instead of returning so we always hit this
+ REQUEST_END_CLEANUP:
+ m_bOriginAuthWithMasterServerInProgress = false;
+ m_bOriginAuthWithMasterServerDone = true;
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::RequestServerList()
+{
+ // do this here so it's instantly set on call for scripts
+ m_bScriptRequestingServerList = true;
+
+ std::thread requestThread(
+ [this]()
+ {
+ // make sure we never have 2 threads writing at once
+ // i sure do hope this is actually threadsafe
+ while (m_bRequestingServerList)
+ Sleep(100);
+
+ m_bRequestingServerList = true;
+ m_bScriptRequestingServerList = true;
+
+ spdlog::info("Requesting server list from {}", Cvar_ns_masterserver_hostname->GetString());
+
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/servers", Cvar_ns_masterserver_hostname->GetString()).c_str());
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ m_bSuccessfullyConnected = true;
+
+ rapidjson_document serverInfoJson;
+ serverInfoJson.Parse(readBuffer.c_str());
+
+ if (serverInfoJson.HasParseError())
+ {
+ spdlog::error(
+ "Failed reading masterserver response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(serverInfoJson.GetParseError()));
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (serverInfoJson.IsObject() && serverInfoJson.HasMember("error"))
+ {
+ spdlog::error("Failed reading masterserver response: got fastify error response");
+ spdlog::error(readBuffer);
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!serverInfoJson.IsArray())
+ {
+ spdlog::error("Failed reading masterserver response: root object is not an array");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ rapidjson::GenericArray<false, rapidjson_document::GenericValue> serverArray = serverInfoJson.GetArray();
+
+ spdlog::info("Got {} servers", serverArray.Size());
+
+ for (auto& serverObj : serverArray)
+ {
+ if (!serverObj.IsObject())
+ {
+ spdlog::error("Failed reading masterserver response: member of server array is not an object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ // todo: verify json props are fine before adding to m_remoteServers
+ if (!serverObj.HasMember("id") || !serverObj["id"].IsString() || !serverObj.HasMember("name") ||
+ !serverObj["name"].IsString() || !serverObj.HasMember("description") || !serverObj["description"].IsString() ||
+ !serverObj.HasMember("map") || !serverObj["map"].IsString() || !serverObj.HasMember("playlist") ||
+ !serverObj["playlist"].IsString() || !serverObj.HasMember("playerCount") || !serverObj["playerCount"].IsNumber() ||
+ !serverObj.HasMember("maxPlayers") || !serverObj["maxPlayers"].IsNumber() || !serverObj.HasMember("hasPassword") ||
+ !serverObj["hasPassword"].IsBool() || !serverObj.HasMember("modInfo") || !serverObj["modInfo"].HasMember("Mods") ||
+ !serverObj["modInfo"]["Mods"].IsArray())
+ {
+ spdlog::error("Failed reading masterserver response: malformed server object");
+ continue;
+ };
+
+ const char* id = serverObj["id"].GetString();
+
+ RemoteServerInfo* newServer = nullptr;
+
+ bool createNewServerInfo = true;
+ for (RemoteServerInfo& server : m_vRemoteServers)
+ {
+ // if server already exists, update info rather than adding to it
+ if (!strncmp((const char*)server.id, id, 32))
+ {
+ server = RemoteServerInfo(
+ id,
+ serverObj["name"].GetString(),
+ serverObj["description"].GetString(),
+ serverObj["map"].GetString(),
+ serverObj["playlist"].GetString(),
+ serverObj["playerCount"].GetInt(),
+ serverObj["maxPlayers"].GetInt(),
+ serverObj["hasPassword"].IsTrue());
+ newServer = &server;
+ createNewServerInfo = false;
+ break;
+ }
+ }
+
+ // server didn't exist
+ if (createNewServerInfo)
+ newServer = &m_vRemoteServers.emplace_back(
+ id,
+ serverObj["name"].GetString(),
+ serverObj["description"].GetString(),
+ serverObj["map"].GetString(),
+ serverObj["playlist"].GetString(),
+ serverObj["playerCount"].GetInt(),
+ serverObj["maxPlayers"].GetInt(),
+ serverObj["hasPassword"].IsTrue());
+
+ newServer->requiredMods.clear();
+ for (auto& requiredMod : serverObj["modInfo"]["Mods"].GetArray())
+ {
+ RemoteModInfo modInfo;
+
+ if (!requiredMod.HasMember("RequiredOnClient") || !requiredMod["RequiredOnClient"].IsTrue())
+ continue;
+
+ if (!requiredMod.HasMember("Name") || !requiredMod["Name"].IsString())
+ continue;
+ modInfo.Name = requiredMod["Name"].GetString();
+
+ if (!requiredMod.HasMember("Version") || !requiredMod["Version"].IsString())
+ continue;
+ modInfo.Version = requiredMod["Version"].GetString();
+
+ newServer->requiredMods.push_back(modInfo);
+ }
+ // Can probably re-enable this later with a -verbose flag, but slows down loading of the server browser quite a bit as
+ // is
+ // spdlog::info(
+ // "Server {} on map {} with playlist {} has {}/{} players", serverObj["name"].GetString(),
+ // serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(),
+ // serverObj["maxPlayers"].GetInt());
+ }
+
+ std::sort(
+ m_vRemoteServers.begin(),
+ m_vRemoteServers.end(),
+ [](RemoteServerInfo& a, RemoteServerInfo& b) { return a.playerCount > b.playerCount; });
+ }
+ else
+ {
+ spdlog::error("Failed requesting servers: error {}", curl_easy_strerror(result));
+ m_bSuccessfullyConnected = false;
+ }
+
+ // we goto this instead of returning so we always hit this
+ REQUEST_END_CLEANUP:
+ m_bRequestingServerList = false;
+ m_bScriptRequestingServerList = false;
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::RequestMainMenuPromos()
+{
+ m_bHasMainMenuPromoData = false;
+
+ std::thread requestThread(
+ [this]()
+ {
+ while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone)
+ Sleep(500);
+
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(
+ curl, CURLOPT_URL, fmt::format("{}/client/mainmenupromos", Cvar_ns_masterserver_hostname->GetString()).c_str());
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ m_bSuccessfullyConnected = true;
+
+ rapidjson_document mainMenuPromoJson;
+ mainMenuPromoJson.Parse(readBuffer.c_str());
+
+ if (mainMenuPromoJson.HasParseError())
+ {
+ spdlog::error(
+ "Failed reading masterserver main menu promos response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(mainMenuPromoJson.GetParseError()));
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!mainMenuPromoJson.IsObject())
+ {
+ spdlog::error("Failed reading masterserver main menu promos response: root object is not an object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (mainMenuPromoJson.HasMember("error"))
+ {
+ spdlog::error("Failed reading masterserver response: got fastify error response");
+ spdlog::error(readBuffer);
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!mainMenuPromoJson.HasMember("newInfo") || !mainMenuPromoJson["newInfo"].IsObject() ||
+ !mainMenuPromoJson["newInfo"].HasMember("Title1") || !mainMenuPromoJson["newInfo"]["Title1"].IsString() ||
+ !mainMenuPromoJson["newInfo"].HasMember("Title2") || !mainMenuPromoJson["newInfo"]["Title2"].IsString() ||
+ !mainMenuPromoJson["newInfo"].HasMember("Title3") || !mainMenuPromoJson["newInfo"]["Title3"].IsString() ||
+
+ !mainMenuPromoJson.HasMember("largeButton") || !mainMenuPromoJson["largeButton"].IsObject() ||
+ !mainMenuPromoJson["largeButton"].HasMember("Title") || !mainMenuPromoJson["largeButton"]["Title"].IsString() ||
+ !mainMenuPromoJson["largeButton"].HasMember("Text") || !mainMenuPromoJson["largeButton"]["Text"].IsString() ||
+ !mainMenuPromoJson["largeButton"].HasMember("Url") || !mainMenuPromoJson["largeButton"]["Url"].IsString() ||
+ !mainMenuPromoJson["largeButton"].HasMember("ImageIndex") ||
+ !mainMenuPromoJson["largeButton"]["ImageIndex"].IsNumber() ||
+
+ !mainMenuPromoJson.HasMember("smallButton1") || !mainMenuPromoJson["smallButton1"].IsObject() ||
+ !mainMenuPromoJson["smallButton1"].HasMember("Title") || !mainMenuPromoJson["smallButton1"]["Title"].IsString() ||
+ !mainMenuPromoJson["smallButton1"].HasMember("Url") || !mainMenuPromoJson["smallButton1"]["Url"].IsString() ||
+ !mainMenuPromoJson["smallButton1"].HasMember("ImageIndex") ||
+ !mainMenuPromoJson["smallButton1"]["ImageIndex"].IsNumber() ||
+
+ !mainMenuPromoJson.HasMember("smallButton2") || !mainMenuPromoJson["smallButton2"].IsObject() ||
+ !mainMenuPromoJson["smallButton2"].HasMember("Title") || !mainMenuPromoJson["smallButton2"]["Title"].IsString() ||
+ !mainMenuPromoJson["smallButton2"].HasMember("Url") || !mainMenuPromoJson["smallButton2"]["Url"].IsString() ||
+ !mainMenuPromoJson["smallButton2"].HasMember("ImageIndex") ||
+ !mainMenuPromoJson["smallButton2"]["ImageIndex"].IsNumber())
+ {
+ spdlog::error("Failed reading masterserver main menu promos response: malformed json object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ m_sMainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString();
+ m_sMainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString();
+ m_sMainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString();
+
+ m_sMainMenuPromoData.largeButtonTitle = mainMenuPromoJson["largeButton"]["Title"].GetString();
+ m_sMainMenuPromoData.largeButtonText = mainMenuPromoJson["largeButton"]["Text"].GetString();
+ m_sMainMenuPromoData.largeButtonUrl = mainMenuPromoJson["largeButton"]["Url"].GetString();
+ m_sMainMenuPromoData.largeButtonImageIndex = mainMenuPromoJson["largeButton"]["ImageIndex"].GetInt();
+
+ m_sMainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString();
+ m_sMainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString();
+ m_sMainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt();
+
+ m_sMainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString();
+ m_sMainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString();
+ m_sMainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt();
+
+ m_bHasMainMenuPromoData = true;
+ }
+ else
+ {
+ spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result));
+ m_bSuccessfullyConnected = false;
+ }
+
+ REQUEST_END_CLEANUP:
+ // nothing lol
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::AuthenticateWithOwnServer(const char* uid, const char* playerToken)
+{
+ // dont wait, just stop if we're trying to do 2 auth requests at once
+ if (m_bAuthenticatingWithGameServer)
+ return;
+
+ m_bAuthenticatingWithGameServer = true;
+ m_bScriptAuthenticatingWithGameServer = true;
+ m_bSuccessfullyAuthenticatedWithGameServer = false;
+
+ std::string uidStr(uid);
+ std::string tokenStr(playerToken);
+
+ std::thread requestThread(
+ [this, uidStr, tokenStr]()
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format("{}/client/auth_with_self?id={}&playerToken={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr)
+ .c_str());
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ m_bSuccessfullyConnected = true;
+
+ rapidjson_document authInfoJson;
+ authInfoJson.Parse(readBuffer.c_str());
+
+ if (authInfoJson.HasParseError())
+ {
+ spdlog::error(
+ "Failed reading masterserver authentication response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(authInfoJson.GetParseError()));
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!authInfoJson.IsObject())
+ {
+ spdlog::error("Failed reading masterserver authentication response: root object is not an object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (authInfoJson.HasMember("error"))
+ {
+ spdlog::error("Failed reading masterserver response: got fastify error response");
+ spdlog::error(readBuffer);
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!authInfoJson["success"].IsTrue())
+ {
+ spdlog::error("Authentication with masterserver failed: \"success\" is not true");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!authInfoJson.HasMember("success") || !authInfoJson.HasMember("id") || !authInfoJson["id"].IsString() ||
+ !authInfoJson.HasMember("authToken") || !authInfoJson["authToken"].IsString() ||
+ !authInfoJson.HasMember("persistentData") || !authInfoJson["persistentData"].IsArray())
+ {
+ spdlog::error("Failed reading masterserver authentication response: malformed json object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ AuthData newAuthData {};
+ strncpy(newAuthData.uid, authInfoJson["id"].GetString(), sizeof(newAuthData.uid));
+ newAuthData.uid[sizeof(newAuthData.uid) - 1] = 0;
+
+ newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size();
+ newAuthData.pdata = new char[newAuthData.pdataSize];
+ // memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize);
+
+ int i = 0;
+ // note: persistentData is a uint8array because i had problems getting strings to behave, it sucks but it's just how it be
+ // unfortunately potentially refactor later
+ for (auto& byte : authInfoJson["persistentData"].GetArray())
+ {
+ if (!byte.IsUint() || byte.GetUint() > 255)
+ {
+ spdlog::error("Failed reading masterserver authentication response: malformed json object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ newAuthData.pdata[i++] = static_cast<char>(byte.GetUint());
+ }
+
+ std::lock_guard<std::mutex> guard(g_ServerAuthenticationManager->m_authDataMutex);
+ g_ServerAuthenticationManager->m_authData.clear();
+ g_ServerAuthenticationManager->m_authData.insert(std::make_pair(authInfoJson["authToken"].GetString(), newAuthData));
+
+ m_bSuccessfullyAuthenticatedWithGameServer = true;
+ }
+ else
+ {
+ spdlog::error("Failed authenticating with own server: error {}", curl_easy_strerror(result));
+ m_bSuccessfullyConnected = false;
+ m_bSuccessfullyAuthenticatedWithGameServer = false;
+ m_bScriptAuthenticatingWithGameServer = false;
+ }
+
+ REQUEST_END_CLEANUP:
+ m_bAuthenticatingWithGameServer = false;
+ m_bScriptAuthenticatingWithGameServer = false;
+
+ if (m_bNewgameAfterSelfAuth)
+ {
+ // pretty sure this is threadsafe?
+ R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "ns_end_reauth_and_leave_to_lobby", R2::cmd_source_t::kCommandSrcCode);
+ m_bNewgameAfterSelfAuth = false;
+ }
+
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::AuthenticateWithServer(const char* uid, const char* playerToken, const char* serverId, const char* password)
+{
+ // dont wait, just stop if we're trying to do 2 auth requests at once
+ if (m_bAuthenticatingWithGameServer)
+ return;
+
+ m_bAuthenticatingWithGameServer = true;
+ m_bScriptAuthenticatingWithGameServer = true;
+ m_bSuccessfullyAuthenticatedWithGameServer = false;
+
+ std::string uidStr(uid);
+ std::string tokenStr(playerToken);
+ std::string serverIdStr(serverId);
+ std::string passwordStr(password);
+
+ std::thread requestThread(
+ [this, uidStr, tokenStr, serverIdStr, passwordStr]()
+ {
+ // esnure that any persistence saving is done, so we know masterserver has newest
+ while (m_bSavingPersistentData)
+ Sleep(100);
+
+ spdlog::info("Attempting authentication with server of id \"{}\"", serverIdStr);
+
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ {
+ char* escapedPassword = curl_easy_escape(curl, passwordStr.c_str(), passwordStr.length());
+
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/client/auth_with_server?id={}&playerToken={}&server={}&password={}",
+ Cvar_ns_masterserver_hostname->GetString(),
+ uidStr,
+ tokenStr,
+ serverIdStr,
+ escapedPassword)
+ .c_str());
+
+ curl_free(escapedPassword);
+ }
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ m_bSuccessfullyConnected = true;
+
+ rapidjson_document connectionInfoJson;
+ connectionInfoJson.Parse(readBuffer.c_str());
+
+ if (connectionInfoJson.HasParseError())
+ {
+ spdlog::error(
+ "Failed reading masterserver authentication response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(connectionInfoJson.GetParseError()));
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!connectionInfoJson.IsObject())
+ {
+ spdlog::error("Failed reading masterserver authentication response: root object is not an object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (connectionInfoJson.HasMember("error"))
+ {
+ spdlog::error("Failed reading masterserver response: got fastify error response");
+ spdlog::error(readBuffer);
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!connectionInfoJson["success"].IsTrue())
+ {
+ spdlog::error("Authentication with masterserver failed: \"success\" is not true");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!connectionInfoJson.HasMember("success") || !connectionInfoJson.HasMember("ip") ||
+ !connectionInfoJson["ip"].IsString() || !connectionInfoJson.HasMember("port") ||
+ !connectionInfoJson["port"].IsNumber() || !connectionInfoJson.HasMember("authToken") ||
+ !connectionInfoJson["authToken"].IsString())
+ {
+ spdlog::error("Failed reading masterserver authentication response: malformed json object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ m_pendingConnectionInfo.ip.S_un.S_addr = inet_addr(connectionInfoJson["ip"].GetString());
+ m_pendingConnectionInfo.port = (unsigned short)connectionInfoJson["port"].GetUint();
+
+ strncpy(m_pendingConnectionInfo.authToken, connectionInfoJson["authToken"].GetString(), 31);
+ m_pendingConnectionInfo.authToken[31] = 0;
+
+ m_bHasPendingConnectionInfo = true;
+ m_bSuccessfullyAuthenticatedWithGameServer = true;
+ }
+ else
+ {
+ spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result));
+ m_bSuccessfullyConnected = false;
+ m_bSuccessfullyAuthenticatedWithGameServer = false;
+ m_bScriptAuthenticatingWithGameServer = false;
+ }
+
+ REQUEST_END_CLEANUP:
+ m_bAuthenticatingWithGameServer = false;
+ m_bScriptAuthenticatingWithGameServer = false;
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::AddSelfToServerList(
+ int port,
+ int authPort,
+ const char* name,
+ const char* description,
+ const char* map,
+ const char* playlist,
+ int maxPlayers,
+ const char* password)
+{
+ if (!Cvar_ns_report_server_to_masterserver->GetBool())
+ return;
+
+ if (!Cvar_ns_report_sp_server_to_masterserver->GetBool() && !strncmp(map, "sp_", 3))
+ {
+ m_bRequireClientAuth = false;
+ return;
+ }
+
+ m_bRequireClientAuth = true;
+
+ std::string strName(name);
+ std::string strDescription(description);
+ std::string strMap(map);
+ std::string strPlaylist(playlist);
+ std::string strPassword(password);
+
+ std::thread requestThread(
+ [this, port, authPort, strName, strDescription, strMap, strPlaylist, maxPlayers, strPassword]
+ {
+ m_sOwnServerId[0] = 0;
+ m_sOwnServerAuthToken[0] = 0;
+
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ curl_mime* mime = curl_mime_init(curl);
+ curl_mimepart* part = curl_mime_addpart(mime);
+
+ curl_mime_data(part, m_sOwnModInfoJson.c_str(), m_sOwnModInfoJson.size());
+ curl_mime_name(part, "modinfo");
+ curl_mime_filename(part, "modinfo.json");
+ curl_mime_type(part, "application/json");
+
+ curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
+
+ // format every paramter because computers hate me
+ {
+ char* nameEscaped = curl_easy_escape(curl, strName.c_str(), strName.length());
+ char* descEscaped = curl_easy_escape(curl, strDescription.c_str(), strDescription.length());
+ char* mapEscaped = curl_easy_escape(curl, strMap.c_str(), strMap.length());
+ char* playlistEscaped = curl_easy_escape(curl, strPlaylist.c_str(), strPlaylist.length());
+ char* passwordEscaped = curl_easy_escape(curl, strPassword.c_str(), strPassword.length());
+
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/server/add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}",
+ Cvar_ns_masterserver_hostname->GetString(),
+ port,
+ authPort,
+ nameEscaped,
+ descEscaped,
+ mapEscaped,
+ playlistEscaped,
+ maxPlayers,
+ passwordEscaped)
+ .c_str());
+
+ curl_free(nameEscaped);
+ curl_free(descEscaped);
+ curl_free(mapEscaped);
+ curl_free(playlistEscaped);
+ curl_free(passwordEscaped);
+ }
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ m_bSuccessfullyConnected = true;
+
+ rapidjson_document serverAddedJson;
+ serverAddedJson.Parse(readBuffer.c_str());
+
+ if (serverAddedJson.HasParseError())
+ {
+ spdlog::error(
+ "Failed reading masterserver authentication response: encountered parse error \"{}\"",
+ rapidjson::GetParseError_En(serverAddedJson.GetParseError()));
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!serverAddedJson.IsObject())
+ {
+ spdlog::error("Failed reading masterserver authentication response: root object is not an object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (serverAddedJson.HasMember("error"))
+ {
+ spdlog::error("Failed reading masterserver response: got fastify error response");
+ spdlog::error(readBuffer);
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!serverAddedJson["success"].IsTrue())
+ {
+ spdlog::error("Adding server to masterserver failed: \"success\" is not true");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() ||
+ !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString())
+ {
+ spdlog::error("Failed reading masterserver response: malformed json object");
+ goto REQUEST_END_CLEANUP;
+ }
+
+ strncpy(m_sOwnServerId, serverAddedJson["id"].GetString(), sizeof(m_sOwnServerId));
+ m_sOwnServerId[sizeof(m_sOwnServerId) - 1] = 0;
+
+ strncpy(m_sOwnServerAuthToken, serverAddedJson["serverAuthToken"].GetString(), sizeof(m_sOwnServerAuthToken));
+ m_sOwnServerAuthToken[sizeof(m_sOwnServerAuthToken) - 1] = 0;
+
+ // heartbeat thread
+ // ideally this should actually be done in main thread, rather than on it's own thread, so it'd stop if server freezes
+ std::thread heartbeatThread(
+ [this]
+ {
+ Sleep(5000);
+
+ // defensive check, as m_ownServer could be set to null during the Sleep(5000) above
+ if (!*m_sOwnServerId)
+ return;
+
+ do
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L);
+
+ // send all registration info so we have all necessary info to reregister our server if masterserver goes down,
+ // without a restart this isn't threadsafe :terror:
+ {
+ char* escapedNameNew = curl_easy_escape(curl, g_MasterServerManager->m_sUnicodeServerName.c_str(), NULL);
+ char* escapedDescNew = curl_easy_escape(curl, g_MasterServerManager->m_sUnicodeServerDesc.c_str(), NULL);
+ char* escapedMapNew = curl_easy_escape(curl, R2::g_pHostState->m_levelName, NULL);
+ char* escapedPlaylistNew = curl_easy_escape(curl, R2::GetCurrentPlaylistName(), NULL);
+ char* escapedPasswordNew = curl_easy_escape(curl, Cvar_ns_server_password->GetString(), NULL);
+
+ int maxPlayers = 6;
+ const char* maxPlayersVar = R2::GetCurrentPlaylistVar("max_players", false);
+ if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this
+ maxPlayers = std::stoi(maxPlayersVar);
+
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/server/"
+ "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&"
+ "maxPlayers={}&password={}",
+ Cvar_ns_masterserver_hostname->GetString(),
+ m_sOwnServerId,
+ Cvar_hostport->GetInt(),
+ Cvar_ns_player_auth_port->GetInt(),
+ escapedNameNew,
+ escapedDescNew,
+ escapedMapNew,
+ escapedPlaylistNew,
+ g_ServerAuthenticationManager->m_additionalPlayerData.size(),
+ maxPlayers,
+ escapedPasswordNew)
+ .c_str());
+
+ curl_free(escapedNameNew);
+ curl_free(escapedDescNew);
+ curl_free(escapedMapNew);
+ curl_free(escapedPlaylistNew);
+ curl_free(escapedPasswordNew);
+ }
+
+ curl_mime* mime = curl_mime_init(curl);
+ curl_mimepart* part = curl_mime_addpart(mime);
+
+ curl_mime_data(part, m_sOwnModInfoJson.c_str(), m_sOwnModInfoJson.size());
+ curl_mime_name(part, "modinfo");
+ curl_mime_filename(part, "modinfo.json");
+ curl_mime_type(part, "application/json");
+
+ curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
+
+ CURLcode result = curl_easy_perform(curl);
+
+ // defensive check, as m_ownServerId could be set to null before this request gets processed
+ if (!*m_sOwnServerId)
+ return;
+
+ if (result == CURLcode::CURLE_OK)
+ {
+ rapidjson_document serverAddedJson;
+ serverAddedJson.Parse(readBuffer.c_str());
+
+ if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject())
+ {
+ if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString())
+ {
+ strncpy(m_sOwnServerId, serverAddedJson["id"].GetString(), sizeof(m_sOwnServerId));
+ m_sOwnServerId[sizeof(m_sOwnServerId) - 1] = 0;
+ }
+
+ if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString())
+ {
+ strncpy(
+ m_sOwnServerAuthToken,
+ serverAddedJson["serverAuthToken"].GetString(),
+ sizeof(m_sOwnServerAuthToken));
+ m_sOwnServerAuthToken[sizeof(m_sOwnServerAuthToken) - 1] = 0;
+ }
+ }
+ }
+ else
+ spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result));
+
+ curl_easy_cleanup(curl);
+ Sleep(10000);
+ } while (*m_sOwnServerId);
+ });
+
+ heartbeatThread.detach();
+ }
+ else
+ {
+ spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result));
+ m_bSuccessfullyConnected = false;
+ }
+
+ REQUEST_END_CLEANUP:
+ curl_easy_cleanup(curl);
+ curl_mime_free(mime);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::UpdateServerMapAndPlaylist(const char* map, const char* playlist, int maxPlayers)
+{
+ // dont call this if we don't have a server id
+ if (!*m_sOwnServerId)
+ return;
+
+ std::string strMap(map);
+ std::string strPlaylist(playlist);
+
+ std::thread requestThread(
+ [this, strMap, strPlaylist, maxPlayers]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ // escape params
+ {
+ char* mapEscaped = curl_easy_escape(curl, strMap.c_str(), strMap.length());
+ char* playlistEscaped = curl_easy_escape(curl, strPlaylist.c_str(), strPlaylist.length());
+
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/server/update_values?id={}&map={}&playlist={}&maxPlayers={}",
+ Cvar_ns_masterserver_hostname->GetString(),
+ m_sOwnServerId,
+ mapEscaped,
+ playlistEscaped,
+ maxPlayers)
+ .c_str());
+
+ curl_free(mapEscaped);
+ curl_free(playlistEscaped);
+ }
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ m_bSuccessfullyConnected = true;
+ else
+ m_bSuccessfullyConnected = false;
+
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::UpdateServerPlayerCount(int playerCount)
+{
+ // dont call this if we don't have a server id
+ if (!*m_sOwnServerId)
+ return;
+
+ std::thread requestThread(
+ [this, playerCount]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/server/update_values?id={}&playerCount={}", Cvar_ns_masterserver_hostname->GetString(), m_sOwnServerId, playerCount)
+ .c_str());
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ m_bSuccessfullyConnected = true;
+ else
+ m_bSuccessfullyConnected = false;
+
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::WritePlayerPersistentData(const char* playerId, const char* pdata, size_t pdataSize)
+{
+ // still call this if we don't have a server id, since lobbies that aren't port forwarded need to be able to call it
+ m_bSavingPersistentData = true;
+ if (!pdataSize)
+ {
+ spdlog::warn("attempted to write pdata of size 0!");
+ return;
+ }
+
+ std::string strPlayerId(playerId);
+ std::string strPdata(pdata, pdataSize);
+
+ std::thread requestThread(
+ [this, strPlayerId, strPdata, pdataSize]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format(
+ "{}/accounts/write_persistence?id={}&serverId={}",
+ Cvar_ns_masterserver_hostname->GetString(),
+ strPlayerId,
+ m_sOwnServerId)
+ .c_str());
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+
+ curl_mime* mime = curl_mime_init(curl);
+ curl_mimepart* part = curl_mime_addpart(mime);
+
+ curl_mime_data(part, strPdata.c_str(), pdataSize);
+ curl_mime_name(part, "pdata");
+ curl_mime_filename(part, "file.pdata");
+ curl_mime_type(part, "application/octet-stream");
+
+ curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
+
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ m_bSuccessfullyConnected = true;
+ else
+ m_bSuccessfullyConnected = false;
+
+ curl_easy_cleanup(curl);
+
+ m_bSavingPersistentData = false;
+ });
+
+ requestThread.detach();
+}
+
+void MasterServerManager::RemoveSelfFromServerList()
+{
+ // dont call this if we don't have a server id
+ if (!*m_sOwnServerId || !Cvar_ns_report_server_to_masterserver->GetBool())
+ return;
+
+ std::thread requestThread(
+ [this]
+ {
+ CURL* curl = curl_easy_init();
+ SetCommonHttpClientOptions(curl);
+
+ std::string readBuffer;
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
+ curl_easy_setopt(
+ curl,
+ CURLOPT_URL,
+ fmt::format("{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), m_sOwnServerId).c_str());
+
+ *m_sOwnServerId = 0;
+ CURLcode result = curl_easy_perform(curl);
+
+ if (result == CURLcode::CURLE_OK)
+ m_bSuccessfullyConnected = true;
+ else
+ m_bSuccessfullyConnected = false;
+
+ curl_easy_cleanup(curl);
+ });
+
+ requestThread.detach();
+}
+
+void ConCommand_ns_fetchservers(const CCommand& args)
+{
+ g_MasterServerManager->RequestServerList();
+}
+
+MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {}
+
+ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, ConCommand, [](HMODULE baseAddress)
+{
+ g_MasterServerManager = new MasterServerManager;
+
+ Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, "");
+
+ Cvar_ns_server_name = new ConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, "This server's description", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) {
+ g_MasterServerManager->m_sUnicodeServerName = unescape_unicode(Cvar_ns_server_name->GetString());
+ });
+ Cvar_ns_server_desc = new ConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, "This server's name", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) {
+ g_MasterServerManager->m_sUnicodeServerName = unescape_unicode(Cvar_ns_server_desc->GetString());
+ });
+
+ Cvar_ns_server_password = new ConVar("ns_server_password", "", FCVAR_GAMEDLL, "This server's password");
+ Cvar_ns_report_server_to_masterserver = new ConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver");
+ Cvar_ns_report_sp_server_to_masterserver = new ConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver, when started in singleplayer");
+
+ Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console");
+
+ Cvar_hostname = *(ConVar**)((char*)baseAddress + 0x1315bae8);
+ Cvar_hostport = (ConVar*)((char*)baseAddress + 0x13FA6070);
+
+ RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL);
})
\ No newline at end of file |