diff options
author | BobTheBob <32057864+BobTheBob9@users.noreply.github.com> | 2022-08-24 00:32:31 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-24 00:32:31 +0100 |
commit | f9bc3c9d1834cb8bd5f872b749b057c33e8b0102 (patch) | |
tree | e96e2da0d95798dd42eddf644a82a74555db858f /NorthstarDLL/masterserver.cpp | |
parent | 812893d8219daa60f5b5b7fd22cbd6b175603399 (diff) | |
download | NorthstarLauncher-f9bc3c9d1834cb8bd5f872b749b057c33e8b0102.tar.gz NorthstarLauncher-f9bc3c9d1834cb8bd5f872b749b057c33e8b0102.zip |
Adjust folder structure (#242)
* Adjust folder structure
* change launcher directory name
Diffstat (limited to 'NorthstarDLL/masterserver.cpp')
-rw-r--r-- | NorthstarDLL/masterserver.cpp | 1408 |
1 files changed, 1408 insertions, 0 deletions
diff --git a/NorthstarDLL/masterserver.cpp b/NorthstarDLL/masterserver.cpp new file mode 100644 index 00000000..d5ef2d9f --- /dev/null +++ b/NorthstarDLL/masterserver.cpp @@ -0,0 +1,1408 @@ +#include "pch.h" +#include "masterserver.h" +#include "concommand.h" +#include "gameutils.h" +#include "hookutils.h" +#include "serverauthentication.h" +#include "gameutils.h" +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" +#include "rapidjson/error/en.h" +#include "modmanager.h" +#include "misccommands.h" +#include <cstring> +#include <regex> +#include "version.h" +#include <chrono> +// NOTE for anyone reading this: we used to use httplib for requests here, but it had issues, so we're moving to curl now for masterserver +// requests so httplib is used exclusively for server stuff now + +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; + +MasterServerManager* g_MasterServerManager; + +typedef void (*CHostState__State_NewGameType)(CHostState* hostState); +CHostState__State_NewGameType CHostState__State_NewGame; + +typedef void (*CHostState__State_ChangeLevelMPType)(CHostState* hostState); +CHostState__State_ChangeLevelMPType CHostState__State_ChangeLevelMP; + +typedef void (*CHostState__State_ChangeLevelSPType)(CHostState* hostState); +CHostState__State_ChangeLevelSPType CHostState__State_ChangeLevelSP; + +typedef void (*CHostState__State_GameShutdownType)(CHostState* hostState); +CHostState__State_GameShutdownType CHostState__State_GameShutdown; + +// 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; +} + +void UpdateServerInfoFromUnicodeToUTF8() +{ + g_MasterServerManager->ns_auth_srvName = unescape_unicode(Cvar_ns_server_name->GetString()); + g_MasterServerManager->ns_auth_srvDesc = unescape_unicode(Cvar_ns_server_desc->GetString()); +} + +const char* HttplibErrorToString(httplib::Error error) +{ + switch (error) + { + case httplib::Error::Success: + return "httplib::Error::Success"; + case httplib::Error::Unknown: + return "httplib::Error::Unknown"; + case httplib::Error::Connection: + return "httplib::Error::Connection"; + case httplib::Error::BindIPAddress: + return "httplib::Error::BindIPAddress"; + case httplib::Error::Read: + return "httplib::Error::Read"; + case httplib::Error::Write: + return "httplib::Error::Write"; + case httplib::Error::ExceedRedirectCount: + return "httplib::Error::ExceedRedirectCount"; + case httplib::Error::Canceled: + return "httplib::Error::Canceled"; + case httplib::Error::SSLConnection: + return "httplib::Error::SSLConnection"; + case httplib::Error::SSLLoadingCerts: + return "httplib::Error::SSLLoadingCerts"; + case httplib::Error::SSLServerVerification: + return "httplib::Error::SSLServerVerification"; + case httplib::Error::UnsupportedMultipartBoundaryChars: + return "httplib::Error::UnsupportedMultipartBoundaryChars"; + } + + return ""; +} + +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 (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_requestingServerList = true; + + m_remoteServers.clear(); + + m_requestingServerList = 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(char* uid, 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_successfullyConnected = 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_ownClientAuthToken, originAuthInfo["token"].GetString(), sizeof(m_ownClientAuthToken)); + m_ownClientAuthToken[sizeof(m_ownClientAuthToken) - 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_successfullyConnected = 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_scriptRequestingServerList = 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_requestingServerList) + Sleep(100); + + m_requestingServerList = true; + m_scriptRequestingServerList = 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_successfullyConnected = 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_remoteServers) + { + // 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_remoteServers.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_remoteServers.begin(), + m_remoteServers.end(), + [](RemoteServerInfo& a, RemoteServerInfo& b) { return a.playerCount > b.playerCount; }); + } + else + { + spdlog::error("Failed requesting servers: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; + } + + // we goto this instead of returning so we always hit this + REQUEST_END_CLEANUP: + m_requestingServerList = false; + m_scriptRequestingServerList = 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_successfullyConnected = 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); + if (mainMenuPromoJson["error"].HasMember("enum")) + s_authfail_reason = std::string(mainMenuPromoJson["error"]["enum"].GetString()); + else + s_authfail_reason = std::string("No error message provided"); + 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_MainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString(); + m_MainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString(); + m_MainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString(); + + m_MainMenuPromoData.largeButtonTitle = mainMenuPromoJson["largeButton"]["Title"].GetString(); + m_MainMenuPromoData.largeButtonText = mainMenuPromoJson["largeButton"]["Text"].GetString(); + m_MainMenuPromoData.largeButtonUrl = mainMenuPromoJson["largeButton"]["Url"].GetString(); + m_MainMenuPromoData.largeButtonImageIndex = mainMenuPromoJson["largeButton"]["ImageIndex"].GetInt(); + + m_MainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString(); + m_MainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString(); + m_MainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt(); + + m_MainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString(); + m_MainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString(); + m_MainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt(); + + m_bHasMainMenuPromoData = true; + } + else + { + spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; + } + + REQUEST_END_CLEANUP: + // nothing lol + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::AuthenticateWithOwnServer(char* uid, char* playerToken) +{ + // dont wait, just stop if we're trying to do 2 auth requests at once + if (m_authenticatingWithGameServer) + return; + + m_authenticatingWithGameServer = true; + m_scriptAuthenticatingWithGameServer = true; + m_successfullyAuthenticatedWithGameServer = 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_successfullyConnected = 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); + if (authInfoJson["error"].HasMember("enum")) + s_authfail_reason = std::string(authInfoJson["error"]["enum"].GetString()); + else + s_authfail_reason = std::string("No error message provided"); + 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_successfullyAuthenticatedWithGameServer = true; + } + else + { + spdlog::error("Failed authenticating with own server: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; + m_successfullyAuthenticatedWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; + } + + REQUEST_END_CLEANUP: + m_authenticatingWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; + + if (m_bNewgameAfterSelfAuth) + { + // pretty sure this is threadsafe? + Cbuf_AddText(Cbuf_GetCurrentPlayer(), "ns_end_reauth_and_leave_to_lobby", cmd_source_t::kCommandSrcCode); + m_bNewgameAfterSelfAuth = false; + } + + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::AuthenticateWithServer(char* uid, char* playerToken, char* serverId, char* password) +{ + // dont wait, just stop if we're trying to do 2 auth requests at once + if (m_authenticatingWithGameServer) + return; + + m_authenticatingWithGameServer = true; + m_scriptAuthenticatingWithGameServer = true; + m_successfullyAuthenticatedWithGameServer = 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_savingPersistentData) + 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_successfullyConnected = 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); + if (connectionInfoJson["error"].HasMember("enum")) + s_authfail_reason = std::string(connectionInfoJson["error"]["enum"].GetString()); + else + s_authfail_reason = std::string("No error message provided"); + 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_hasPendingConnectionInfo = true; + m_successfullyAuthenticatedWithGameServer = true; + } + else + { + spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; + m_successfullyAuthenticatedWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; + } + + REQUEST_END_CLEANUP: + m_authenticatingWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::AddSelfToServerList( + int port, int authPort, char* name, char* description, char* map, char* playlist, int maxPlayers, 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] + { + // Keep track of attempted connects in case of DUPLICATE_SERVER response + int retry_counter = 0; + + START_REQUEST: + m_ownServerId[0] = 0; + m_ownServerAuthToken[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_ownModInfoJson.c_str(), m_ownModInfoJson.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_successfullyConnected = 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); + + // Check for enum member in JSON + if (serverAddedJson["error"].HasMember("enum")) + { + // Check for DUPLICATE_SERVER error response, stop if we tried 10 times + if (strncmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER", 17) == 0 && retry_counter < 10) + { + + spdlog::info("Retrying request in 10 seconds."); + // Incremement retry counter + retry_counter++; + + // Sleep for 10 seconds + std::this_thread::sleep_for(std::chrono::seconds(10)); + + // curl cleanup to retry request + curl_easy_cleanup(curl); + curl_mime_free(mime); + + // go to beginning and retry + goto START_REQUEST; + } + } + 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_ownServerId, serverAddedJson["id"].GetString(), sizeof(m_ownServerId)); + m_ownServerId[sizeof(m_ownServerId) - 1] = 0; + + strncpy(m_ownServerAuthToken, serverAddedJson["serverAuthToken"].GetString(), sizeof(m_ownServerAuthToken)); + m_ownServerAuthToken[sizeof(m_ownServerAuthToken) - 1] = 0; + + spdlog::info("Succesfully added server to the master server."); + + // 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_ownServerId) + 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->ns_auth_srvName.c_str(), NULL); + char* escapedDescNew = curl_easy_escape(curl, g_MasterServerManager->ns_auth_srvDesc.c_str(), NULL); + char* escapedMapNew = curl_easy_escape(curl, g_pHostState->m_levelName, NULL); + char* escapedPlaylistNew = curl_easy_escape(curl, GetCurrentPlaylistName(), NULL); + char* escapedPasswordNew = curl_easy_escape(curl, Cvar_ns_server_password->GetString(), NULL); + + int maxPlayers = 6; + char* maxPlayersVar = 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_ownServerId, + 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_ownModInfoJson.c_str(), m_ownModInfoJson.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_ownServerId) + 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_ownServerId, serverAddedJson["id"].GetString(), sizeof(m_ownServerId)); + m_ownServerId[sizeof(m_ownServerId) - 1] = 0; + } + + if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) + { + strncpy( + m_ownServerAuthToken, + serverAddedJson["serverAuthToken"].GetString(), + sizeof(m_ownServerAuthToken)); + m_ownServerAuthToken[sizeof(m_ownServerAuthToken) - 1] = 0; + } + } + } + else + spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); + + curl_easy_cleanup(curl); + Sleep(10000); + } while (*m_ownServerId); + }); + + heartbeatThread.detach(); + } + else + { + spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; + } + + REQUEST_END_CLEANUP: + curl_easy_cleanup(curl); + curl_mime_free(mime); + }); + + requestThread.detach(); +} + +void MasterServerManager::UpdateServerMapAndPlaylist(char* map, char* playlist, int maxPlayers) +{ + // dont call this if we don't have a server id + if (!*m_ownServerId) + 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_ownServerId, + mapEscaped, + playlistEscaped, + maxPlayers) + .c_str()); + + curl_free(mapEscaped); + curl_free(playlistEscaped); + } + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + m_successfullyConnected = true; + else + m_successfullyConnected = 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_ownServerId) + 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_ownServerId, playerCount) + .c_str()); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + m_successfullyConnected = true; + else + m_successfullyConnected = false; + + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::WritePlayerPersistentData(char* playerId, 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_savingPersistentData = 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_ownServerId) + .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_successfullyConnected = true; + else + m_successfullyConnected = false; + + curl_easy_cleanup(curl); + + m_savingPersistentData = false; + }); + + requestThread.detach(); +} + +void MasterServerManager::RemoveSelfFromServerList() +{ + // dont call this if we don't have a server id + if (!*m_ownServerId || !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_ownServerId).c_str()); + + *m_ownServerId = 0; + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + m_successfullyConnected = true; + else + m_successfullyConnected = false; + + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void ConCommand_ns_fetchservers(const CCommand& args) +{ + g_MasterServerManager->RequestServerList(); +} + +void CHostState__State_NewGameHook(CHostState* hostState) +{ + Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", cmd_source_t::kCommandSrcCode); + Cbuf_Execute(); + + // need to do this to ensure we don't go to private match + if (g_ServerAuthenticationManager->m_bNeedLocalAuthForNewgame) + SetCurrentPlaylist("tdm"); + + // net_data_block_enabled is required for sp, force it if we're on an sp map + // sucks for security but just how it be + if (!strncmp(g_pHostState->m_levelName, "sp_", 3)) + { + Cbuf_AddText(Cbuf_GetCurrentPlayer(), "net_data_block_enabled 1", cmd_source_t::kCommandSrcCode); + Cbuf_Execute(); + } + + CHostState__State_NewGame(hostState); + + int maxPlayers = 6; + char* maxPlayersVar = GetCurrentPlaylistVar("max_players", false); + if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this + maxPlayers = std::stoi(maxPlayersVar); + + // Copy new server name cvar to source + Cvar_hostname->SetValue(Cvar_ns_server_name->GetString()); + // This calls the function that converts unicode strings from servername and serverdesc to UTF-8 + UpdateServerInfoFromUnicodeToUTF8(); + + g_MasterServerManager->AddSelfToServerList( + Cvar_hostport->GetInt(), + Cvar_ns_player_auth_port->GetInt(), + (char*)Cvar_ns_server_name->GetString(), + (char*)Cvar_ns_server_desc->GetString(), + hostState->m_levelName, + (char*)GetCurrentPlaylistName(), + maxPlayers, + (char*)Cvar_ns_server_password->GetString()); + g_ServerAuthenticationManager->StartPlayerAuthServer(); + g_ServerAuthenticationManager->m_bNeedLocalAuthForNewgame = false; +} + +void CHostState__State_ChangeLevelMPHook(CHostState* hostState) +{ + int maxPlayers = 6; + char* maxPlayersVar = GetCurrentPlaylistVar("max_players", false); + if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this + maxPlayers = std::stoi(maxPlayersVar); + + // net_data_block_enabled is required for sp, force it if we're on an sp map + // sucks for security but just how it be + if (!strncmp(g_pHostState->m_levelName, "sp_", 3)) + { + Cbuf_AddText(Cbuf_GetCurrentPlayer(), "net_data_block_enabled 1", cmd_source_t::kCommandSrcCode); + Cbuf_Execute(); + } + + g_MasterServerManager->UpdateServerMapAndPlaylist(hostState->m_levelName, (char*)GetCurrentPlaylistName(), maxPlayers); + CHostState__State_ChangeLevelMP(hostState); +} + +void CHostState__State_ChangeLevelSPHook(CHostState* hostState) +{ + // is this even called? genuinely i don't think so + // from what i can tell, it's not called on mp=>sp change or sp=>sp change + // so idk it's fucked + + int maxPlayers = 6; + char* maxPlayersVar = GetCurrentPlaylistVar("max_players", false); + if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this + maxPlayers = std::stoi(maxPlayersVar); + + g_MasterServerManager->UpdateServerMapAndPlaylist(hostState->m_levelName, (char*)GetCurrentPlaylistName(), maxPlayers); + CHostState__State_ChangeLevelSP(hostState); +} + +void CHostState__State_GameShutdownHook(CHostState* hostState) +{ + g_MasterServerManager->RemoveSelfFromServerList(); + g_ServerAuthenticationManager->StopPlayerAuthServer(); + + CHostState__State_GameShutdown(hostState); +} + +MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_ownServerId {""}, m_ownClientAuthToken {""} {} + +void InitialiseSharedMasterServer(HMODULE baseAddress) +{ + Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); + // unfortunately lib doesn't let us specify a port and still have https work + // Cvar_ns_masterserver_port = new ConVar("ns_masterserver_port", "8080", FCVAR_NONE, ""); + + Cvar_ns_server_name = new ConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, ""); + Cvar_ns_server_desc = new ConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, ""); + Cvar_ns_server_password = new ConVar("ns_server_password", "", FCVAR_GAMEDLL, ""); + Cvar_ns_report_server_to_masterserver = new ConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, ""); + Cvar_ns_report_sp_server_to_masterserver = new ConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, ""); + + Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, ""); + + Cvar_hostname = *(ConVar**)((char*)baseAddress + 0x1315bae8); + + g_MasterServerManager = new MasterServerManager; + + RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "", FCVAR_CLIENTDLL); + + HookEnabler hook; + ENABLER_CREATEHOOK( + hook, (char*)baseAddress + 0x16E7D0, CHostState__State_NewGameHook, reinterpret_cast<LPVOID*>(&CHostState__State_NewGame)); + ENABLER_CREATEHOOK( + hook, + (char*)baseAddress + 0x16E520, + CHostState__State_ChangeLevelMPHook, + reinterpret_cast<LPVOID*>(&CHostState__State_ChangeLevelMP)); + ENABLER_CREATEHOOK( + hook, + (char*)baseAddress + 0x16E5D0, + CHostState__State_ChangeLevelSPHook, + reinterpret_cast<LPVOID*>(&CHostState__State_ChangeLevelSP)); + ENABLER_CREATEHOOK( + hook, + (char*)baseAddress + 0x16E640, + CHostState__State_GameShutdownHook, + reinterpret_cast<LPVOID*>(&CHostState__State_GameShutdown)); +} |