diff options
Diffstat (limited to 'NorthstarDLL/masterserver.cpp')
-rw-r--r-- | NorthstarDLL/masterserver.cpp | 2058 |
1 files changed, 1029 insertions, 1029 deletions
diff --git a/NorthstarDLL/masterserver.cpp b/NorthstarDLL/masterserver.cpp index a3b8a9a8..75f0bb4b 100644 --- a/NorthstarDLL/masterserver.cpp +++ b/NorthstarDLL/masterserver.cpp @@ -1,1029 +1,1029 @@ -#include "pch.h"
-#include "masterserver.h"
-#include "concommand.h"
-#include "playlist.h"
-#include "serverauthentication.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>
-
-MasterServerManager* g_pMasterServerManager;
-
-ConVar* Cvar_ns_masterserver_hostname;
-ConVar* Cvar_ns_curl_log_enable;
-
-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_s((char*)id, sizeof(id), newId, sizeof(id) - 1);
- strncpy_s((char*)name, sizeof(name), newName, sizeof(name) - 1);
-
- description = std::string(newDescription);
-
- strncpy_s((char*)map, sizeof(map) , newMap, sizeof(map) - 1);
- strncpy_s((char*)playlist, sizeof(playlist) , newPlaylist, sizeof(playlist) - 1);
-
- playerCount = newPlayerCount;
- maxPlayers = newMaxPlayers;
-}
-
-void 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_s(
- m_sOwnClientAuthToken,
- sizeof(m_sOwnClientAuthToken), originAuthInfo["token"].GetString(),
- sizeof(m_sOwnClientAuthToken) - 1);
- 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;
- }
-
- RemoteAuthData newAuthData {};
- strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), authInfoJson["id"].GetString(), sizeof(newAuthData.uid) - 1);
-
- 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_pServerAuthentication->m_AuthDataMutex);
- g_pServerAuthentication->m_RemoteAuthenticationData.clear();
- g_pServerAuthentication->m_RemoteAuthenticationData.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_s(
- m_pendingConnectionInfo.authToken,
- sizeof(m_pendingConnectionInfo.authToken),
- connectionInfoJson["authToken"].GetString(),
- sizeof(m_pendingConnectionInfo.authToken) - 1);
-
- 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::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();
-}
-
-class MasterServerPresenceReporter : public ServerPresenceReporter
-{
- void ReportPresence(const ServerPresence* pServerPresence) override
- {
- // make a copy of presence for multithreading purposes
- ServerPresence threadedPresence(pServerPresence);
-
- if (!*g_pMasterServerManager->m_sOwnServerId)
- {
- // add server
- std::thread addServerThread(
- [threadedPresence]
- {
- g_pMasterServerManager->m_sOwnServerId[0] = 0;
- g_pMasterServerManager->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, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->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, threadedPresence.m_sServerName.c_str(), NULL);
- char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL);
- char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL);
- char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL);
- char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL);
-
- curl_easy_setopt(
- curl,
- CURLOPT_URL,
- fmt::format(
- "{}/server/"
- "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}",
- Cvar_ns_masterserver_hostname->GetString(),
- threadedPresence.m_iPort,
- threadedPresence.m_iAuthPort,
- nameEscaped,
- descEscaped,
- mapEscaped,
- playlistEscaped,
- threadedPresence.m_iMaxPlayers,
- 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)
- {
- g_pMasterServerManager->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_s(
- g_pMasterServerManager->m_sOwnServerId,
- sizeof(g_pMasterServerManager->m_sOwnServerId),
- serverAddedJson["id"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerId) - 1);
-
- strncpy_s(
- g_pMasterServerManager->m_sOwnServerAuthToken,
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken),
- serverAddedJson["serverAuthToken"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1);
- }
- else
- {
- spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result));
- g_pMasterServerManager->m_bSuccessfullyConnected = false;
- }
-
- REQUEST_END_CLEANUP:
- curl_easy_cleanup(curl);
- curl_mime_free(mime);
- });
- addServerThread.detach();
- }
- else
- {
- // update server
- std::thread updateServerThread(
- [threadedPresence]
- {
- 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* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL);
- char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL);
- char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL);
- char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL);
- char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL);
-
- 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(),
- g_pMasterServerManager->m_sOwnServerId,
- threadedPresence.m_iPort,
- threadedPresence.m_iAuthPort,
- nameEscaped,
- descEscaped,
- mapEscaped,
- playlistEscaped,
- threadedPresence.m_iPlayerCount,
- threadedPresence.m_iMaxPlayers,
- passwordEscaped)
- .c_str());
-
- curl_free(nameEscaped);
- curl_free(descEscaped);
- curl_free(mapEscaped);
- curl_free(playlistEscaped);
- curl_free(passwordEscaped);
- }
-
- curl_mime* mime = curl_mime_init(curl);
- curl_mimepart* part = curl_mime_addpart(mime);
-
- curl_mime_data(
- part, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->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);
-
- 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_s(
- g_pMasterServerManager->m_sOwnServerId,
- sizeof(g_pMasterServerManager->m_sOwnServerId),
- serverAddedJson["id"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerId) - 1);
- }
-
- if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString())
- {
-
- strncpy_s(
- g_pMasterServerManager->m_sOwnServerAuthToken,
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken),
- serverAddedJson["serverAuthToken"].GetString(),
- sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1);
- }
- }
- }
- else
- spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result));
-
- curl_easy_cleanup(curl);
- });
- updateServerThread.detach();
- }
- }
-
- void DestroyPresence(const ServerPresence* pServerPresence) override
- {
- // dont call this if we don't have a server id
- if (!*g_pMasterServerManager->m_sOwnServerId)
- 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(), g_pMasterServerManager->m_sOwnServerId)
- .c_str());
-
- *g_pMasterServerManager->m_sOwnServerId = 0;
- CURLcode result = curl_easy_perform(curl);
-
- if (result == CURLcode::CURLE_OK)
- g_pMasterServerManager->m_bSuccessfullyConnected = true;
- else
- g_pMasterServerManager->m_bSuccessfullyConnected = false;
-
- curl_easy_cleanup(curl);
- });
-
- requestThread.detach();
- }
-};
-
-void ConCommand_ns_fetchservers(const CCommand& args)
-{
- g_pMasterServerManager->RequestServerList();
-}
-
-MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {}
-
-ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module))
-{
- g_pMasterServerManager = new MasterServerManager;
-
- Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, "");
- Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console");
-
- RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL);
-
- MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter;
- g_pServerPresence->AddPresenceReporter(presenceReporter);
-}
\ No newline at end of file +#include "pch.h" +#include "masterserver.h" +#include "concommand.h" +#include "playlist.h" +#include "serverauthentication.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> + +MasterServerManager* g_pMasterServerManager; + +ConVar* Cvar_ns_masterserver_hostname; +ConVar* Cvar_ns_curl_log_enable; + +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_s((char*)id, sizeof(id), newId, sizeof(id) - 1); + strncpy_s((char*)name, sizeof(name), newName, sizeof(name) - 1); + + description = std::string(newDescription); + + strncpy_s((char*)map, sizeof(map) , newMap, sizeof(map) - 1); + strncpy_s((char*)playlist, sizeof(playlist) , newPlaylist, sizeof(playlist) - 1); + + playerCount = newPlayerCount; + maxPlayers = newMaxPlayers; +} + +void 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_s( + m_sOwnClientAuthToken, + sizeof(m_sOwnClientAuthToken), originAuthInfo["token"].GetString(), + sizeof(m_sOwnClientAuthToken) - 1); + 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; + } + + RemoteAuthData newAuthData {}; + strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), authInfoJson["id"].GetString(), sizeof(newAuthData.uid) - 1); + + 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_pServerAuthentication->m_AuthDataMutex); + g_pServerAuthentication->m_RemoteAuthenticationData.clear(); + g_pServerAuthentication->m_RemoteAuthenticationData.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_s( + m_pendingConnectionInfo.authToken, + sizeof(m_pendingConnectionInfo.authToken), + connectionInfoJson["authToken"].GetString(), + sizeof(m_pendingConnectionInfo.authToken) - 1); + + 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::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(); +} + +class MasterServerPresenceReporter : public ServerPresenceReporter +{ + void ReportPresence(const ServerPresence* pServerPresence) override + { + // make a copy of presence for multithreading purposes + ServerPresence threadedPresence(pServerPresence); + + if (!*g_pMasterServerManager->m_sOwnServerId) + { + // add server + std::thread addServerThread( + [threadedPresence] + { + g_pMasterServerManager->m_sOwnServerId[0] = 0; + g_pMasterServerManager->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, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->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, threadedPresence.m_sServerName.c_str(), NULL); + char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); + char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); + char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); + char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); + + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/" + "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", + Cvar_ns_masterserver_hostname->GetString(), + threadedPresence.m_iPort, + threadedPresence.m_iAuthPort, + nameEscaped, + descEscaped, + mapEscaped, + playlistEscaped, + threadedPresence.m_iMaxPlayers, + 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) + { + g_pMasterServerManager->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_s( + g_pMasterServerManager->m_sOwnServerId, + sizeof(g_pMasterServerManager->m_sOwnServerId), + serverAddedJson["id"].GetString(), + sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); + + strncpy_s( + g_pMasterServerManager->m_sOwnServerAuthToken, + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), + serverAddedJson["serverAuthToken"].GetString(), + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); + } + else + { + spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); + g_pMasterServerManager->m_bSuccessfullyConnected = false; + } + + REQUEST_END_CLEANUP: + curl_easy_cleanup(curl); + curl_mime_free(mime); + }); + addServerThread.detach(); + } + else + { + // update server + std::thread updateServerThread( + [threadedPresence] + { + 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* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); + char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); + char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); + char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); + char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); + + 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(), + g_pMasterServerManager->m_sOwnServerId, + threadedPresence.m_iPort, + threadedPresence.m_iAuthPort, + nameEscaped, + descEscaped, + mapEscaped, + playlistEscaped, + threadedPresence.m_iPlayerCount, + threadedPresence.m_iMaxPlayers, + passwordEscaped) + .c_str()); + + curl_free(nameEscaped); + curl_free(descEscaped); + curl_free(mapEscaped); + curl_free(playlistEscaped); + curl_free(passwordEscaped); + } + + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); + + curl_mime_data( + part, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->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); + + 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_s( + g_pMasterServerManager->m_sOwnServerId, + sizeof(g_pMasterServerManager->m_sOwnServerId), + serverAddedJson["id"].GetString(), + sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); + } + + if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) + { + + strncpy_s( + g_pMasterServerManager->m_sOwnServerAuthToken, + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), + serverAddedJson["serverAuthToken"].GetString(), + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); + } + } + } + else + spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); + + curl_easy_cleanup(curl); + }); + updateServerThread.detach(); + } + } + + void DestroyPresence(const ServerPresence* pServerPresence) override + { + // dont call this if we don't have a server id + if (!*g_pMasterServerManager->m_sOwnServerId) + 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(), g_pMasterServerManager->m_sOwnServerId) + .c_str()); + + *g_pMasterServerManager->m_sOwnServerId = 0; + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + g_pMasterServerManager->m_bSuccessfullyConnected = true; + else + g_pMasterServerManager->m_bSuccessfullyConnected = false; + + curl_easy_cleanup(curl); + }); + + requestThread.detach(); + } +}; + +void ConCommand_ns_fetchservers(const CCommand& args) +{ + g_pMasterServerManager->RequestServerList(); +} + +MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {} + +ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module)) +{ + g_pMasterServerManager = new MasterServerManager; + + Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); + Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console"); + + RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL); + + MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter; + g_pServerPresence->AddPresenceReporter(presenceReporter); +} |