diff options
Diffstat (limited to 'NorthstarDedicatedTest/masterserver.cpp')
-rw-r--r-- | NorthstarDedicatedTest/masterserver.cpp | 1280 |
1 files changed, 678 insertions, 602 deletions
diff --git a/NorthstarDedicatedTest/masterserver.cpp b/NorthstarDedicatedTest/masterserver.cpp index d49252bc..74bd9af5 100644 --- a/NorthstarDedicatedTest/masterserver.cpp +++ b/NorthstarDedicatedTest/masterserver.cpp @@ -3,7 +3,6 @@ #include "concommand.h" #include "gameutils.h" #include "hookutils.h" -#include "httplib.h" #include "serverauthentication.h" #include "gameutils.h" #include "rapidjson/document.h" @@ -13,6 +12,9 @@ #include "modmanager.h" #include "misccommands.h" +// 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; @@ -21,6 +23,9 @@ ConVar* Cvar_ns_server_name; ConVar* Cvar_ns_server_desc; ConVar* Cvar_ns_server_password; +// Source ConVar +ConVar* Cvar_hostname; + MasterServerManager* g_MasterServerManager; typedef void(*CHostState__State_NewGameType)(CHostState* hostState); @@ -89,16 +94,16 @@ RemoteServerInfo::RemoteServerInfo(const char* newId, const char* newName, const maxPlayers = newMaxPlayers; } -void MasterServerManager::LazyCreateHttpClient() +void MasterServerManager::SetCommonHttpClientOptions(CURL* curl) { - if (m_httpClient) - return; - - m_httpClient = new httplib::Client(Cvar_ns_masterserver_hostname->m_pszString); - m_httpClient->set_connection_timeout(25); - m_httpClient->set_read_timeout(25); - m_httpClient->set_write_timeout(25); - m_httpClient->set_tcp_nodelay(true); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + //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() @@ -111,51 +116,10 @@ void MasterServerManager::ClearServerList() m_requestingServerList = false; } -bool MasterServerManager::AuthenticateOriginWithMasterServerThread(std::string uidStr, std::string tokenStr) +size_t CurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) { - spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr); - - LazyCreateHttpClient(); - if (auto result = m_httpClient->Get(fmt::format("/client/origin_auth?id={}&token={}", uidStr, tokenStr).c_str())) - { - m_successfullyConnected = true; - - rapidjson::Document originAuthInfo; - originAuthInfo.Parse(result->body.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 {}", result->body); - 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 {}", HttplibErrorToString(result.error())); - m_successfullyConnected = false; - } - - // we goto this instead of returning so we always hit this -REQUEST_END_CLEANUP: - m_bOriginAuthWithMasterServerInProgress = false; - m_bOriginAuthWithMasterServerDone = true; - - return m_successfullyConnected; + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; } void MasterServerManager::AuthenticateOriginWithMasterServer(char* uid, char* originToken) @@ -170,144 +134,61 @@ void MasterServerManager::AuthenticateOriginWithMasterServer(char* uid, char* or std::thread requestThread([this, uidStr, tokenStr]() { - for (int i = 0; i < 5; i++) - { - if (!AuthenticateOriginWithMasterServerThread(uidStr, tokenStr)) - spdlog::info("retrying..."); - else - return; - } - }); - - requestThread.detach(); -} - -bool MasterServerManager::RequestServerListThread() -{ - // 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->m_pszString); - - LazyCreateHttpClient(); - if (auto result = m_httpClient->Get("/client/servers")) - { - m_successfullyConnected = true; - - rapidjson::Document serverInfoJson; - serverInfoJson.Parse(result->body.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(result->body); - goto REQUEST_END_CLEANUP; - } + spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr); - if (!serverInfoJson.IsArray()) - { - spdlog::error("Failed reading masterserver response: root object is not an array"); - goto REQUEST_END_CLEANUP; - } + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - rapidjson::GenericArray<false, rapidjson::Value> serverArray = serverInfoJson.GetArray(); + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/origin_auth?id={}&token={}", Cvar_ns_masterserver_hostname->m_pszString, 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); - spdlog::info("Got {} servers", serverArray.Size()); + CURLcode result = curl_easy_perform(curl); - for (auto& serverObj : serverArray) - { - if (!serverObj.IsObject()) + if (result == CURLcode::CURLE_OK) { - spdlog::error("Failed reading masterserver response: member of server array is not an object"); - goto REQUEST_END_CLEANUP; - } + m_successfullyConnected = true; - // 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; - }; + rapidjson_document originAuthInfo; + originAuthInfo.Parse(readBuffer.c_str()); - const char* id = serverObj["id"].GetString(); + if (originAuthInfo.HasParseError()) + { + spdlog::error("Failed reading origin auth info response: encountered parse error \"{}\"", rapidjson::GetParseError_En(originAuthInfo.GetParseError())); + goto REQUEST_END_CLEANUP; + } - RemoteServerInfo* newServer = nullptr; + if (!originAuthInfo.IsObject() || !originAuthInfo.HasMember("success")) + { + spdlog::error("Failed reading origin auth info response: malformed response object {}", readBuffer); + goto REQUEST_END_CLEANUP; + } - 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)) + if (originAuthInfo["success"].IsTrue() && originAuthInfo.HasMember("token") && originAuthInfo["token"].IsString()) { - 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; + 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"); } - - // 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()) + else { - 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); + spdlog::error("Failed performing northstar origin auth: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; } - 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 {}", HttplibErrorToString(result.error())); - m_successfullyConnected = false; - } - - // we goto this instead of returning so we always hit this -REQUEST_END_CLEANUP: - m_requestingServerList = false; - m_scriptRequestingServerList = 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); + }); - return m_successfullyConnected; + requestThread.detach(); } void MasterServerManager::RequestServerList() @@ -317,103 +198,142 @@ void MasterServerManager::RequestServerList() std::thread requestThread([this]() { - for (int i = 0; i < 3; i++) - { - if (!RequestServerListThread()) - spdlog::info("retrying..."); - else - return; - } - }); + // make sure we never have 2 threads writing at once + // i sure do hope this is actually threadsafe + while (m_requestingServerList) + Sleep(100); - requestThread.detach(); -} + m_requestingServerList = true; + m_scriptRequestingServerList = true; -bool MasterServerManager::RequestMainMenuPromosThread() -{ - while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone) - Sleep(500); + spdlog::info("Requesting server list from {}", Cvar_ns_masterserver_hostname->m_pszString); - LazyCreateHttpClient(); - if (auto result = m_httpClient->Get("/client/mainmenupromos")) - { - m_successfullyConnected = true; + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - rapidjson::Document mainMenuPromoJson; - mainMenuPromoJson.Parse(result->body.c_str()); + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/servers", Cvar_ns_masterserver_hostname->m_pszString).c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - if (mainMenuPromoJson.HasParseError()) - { - spdlog::error("Failed reading masterserver main menu promos response: encountered parse error \"{}\"", rapidjson::GetParseError_En(mainMenuPromoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } + CURLcode result = curl_easy_perform(curl); - if (!mainMenuPromoJson.IsObject()) - { - spdlog::error("Failed reading masterserver main menu promos response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } + if (result == CURLcode::CURLE_OK) + { + m_successfullyConnected = true; - if (mainMenuPromoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(result->body); - goto REQUEST_END_CLEANUP; - } + rapidjson_document serverInfoJson; + serverInfoJson.Parse(readBuffer.c_str()); - 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; - } + if (serverInfoJson.HasParseError()) + { + spdlog::error("Failed reading masterserver response: encountered parse error \"{}\"", rapidjson::GetParseError_En(serverInfoJson.GetParseError())); + 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(); + if (serverInfoJson.IsObject() && serverInfoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + goto REQUEST_END_CLEANUP; + } - 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(); + if (!serverInfoJson.IsArray()) + { + spdlog::error("Failed reading masterserver response: root object is not an array"); + goto REQUEST_END_CLEANUP; + } - m_MainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString(); - m_MainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString(); - m_MainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt(); + rapidjson::GenericArray<false, rapidjson_document::GenericValue> serverArray = serverInfoJson.GetArray(); - m_MainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString(); - m_MainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString(); - m_MainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt(); + spdlog::info("Got {} servers", serverArray.Size()); - m_bHasMainMenuPromoData = true; - } - else - { - spdlog::error("Failed requesting main menu promos: error {}", HttplibErrorToString(result.error())); - m_successfullyConnected = false; - } + 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); + } + + 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); + }); -REQUEST_END_CLEANUP: - // nothing lol - return m_successfullyConnected; + requestThread.detach(); } void MasterServerManager::RequestMainMenuPromos() @@ -422,107 +342,102 @@ void MasterServerManager::RequestMainMenuPromos() std::thread requestThread([this]() { - for (int i = 0; i < 3; i++) - { - if (!RequestMainMenuPromosThread()) - spdlog::info("retrying..."); - else - return; - } - }); + while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone) + Sleep(500); - requestThread.detach(); -} + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); -bool MasterServerManager::AuthenticateWithOwnServerThread(char* uid, char* playerToken) -{ - LazyCreateHttpClient(); - if (auto result = m_httpClient->Post(fmt::format("/client/auth_with_self?id={}&playerToken={}", uid, playerToken).c_str())) - { - m_successfullyConnected = true; + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/mainmenupromos", Cvar_ns_masterserver_hostname->m_pszString).c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - rapidjson::Document authInfoJson; - authInfoJson.Parse(result->body.c_str()); + CURLcode result = curl_easy_perform(curl); - if (authInfoJson.HasParseError()) - { - spdlog::error("Failed reading masterserver authentication response: encountered parse error \"{}\"", rapidjson::GetParseError_En(authInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } + if (result == CURLcode::CURLE_OK) + { + m_successfullyConnected = true; - if (!authInfoJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } + rapidjson_document mainMenuPromoJson; + mainMenuPromoJson.Parse(readBuffer.c_str()); - if (authInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(result->body); - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson["success"].IsTrue()) - { - spdlog::error("Authentication with masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } + 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 (!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; - } + if (!mainMenuPromoJson.IsObject()) + { + spdlog::error("Failed reading masterserver main menu promos response: root object is not an object"); + goto REQUEST_END_CLEANUP; + } - AuthData newAuthData; - strncpy(newAuthData.uid, authInfoJson["id"].GetString(), sizeof(newAuthData.uid)); - newAuthData.uid[sizeof(newAuthData.uid) - 1] = 0; + if (mainMenuPromoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + goto REQUEST_END_CLEANUP; + } - newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size(); - newAuthData.pdata = new char[newAuthData.pdataSize]; - //memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize); + 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; + } - 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; - } + m_MainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString(); + m_MainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString(); + m_MainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString(); - newAuthData.pdata[i++] = (char)byte.GetUint(); - } + 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(); - 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_MainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString(); + m_MainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString(); + m_MainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt(); - m_successfullyAuthenticatedWithGameServer = true; - } - else - { - spdlog::error("Failed authenticating with own server: error {}", HttplibErrorToString(result.error())); - m_successfullyConnected = false; - m_successfullyAuthenticatedWithGameServer = false; - m_scriptAuthenticatingWithGameServer = false; - } + m_MainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString(); + m_MainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString(); + m_MainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt(); -REQUEST_END_CLEANUP: - m_authenticatingWithGameServer = false; - m_scriptAuthenticatingWithGameServer = false; + m_bHasMainMenuPromoData = true; + } + else + { + spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result)); + m_successfullyConnected = 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; - } + REQUEST_END_CLEANUP: + // nothing lol + curl_easy_cleanup(curl); + }); - return m_successfullyConnected; + requestThread.detach(); } void MasterServerManager::AuthenticateWithOwnServer(char* uid, char* playerToken) @@ -535,89 +450,111 @@ void MasterServerManager::AuthenticateWithOwnServer(char* uid, char* playerToken m_scriptAuthenticatingWithGameServer = true; m_successfullyAuthenticatedWithGameServer = false; - std::thread requestThread([this, uid, playerToken]() + std::string uidStr(uid); + std::string tokenStr(playerToken); + + std::thread requestThread([this, uidStr, tokenStr]() { - for (int i = 0; i < 5; i++) + 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->m_pszString, 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) { - if (!AuthenticateWithOwnServerThread(uid, playerToken)) - spdlog::info("retrying..."); - else - return; - } - }); + m_successfullyConnected = true; - requestThread.detach(); -} + rapidjson_document authInfoJson; + authInfoJson.Parse(readBuffer.c_str()); -bool MasterServerManager::AuthenticateWithServerThread(char* uid, char* playerToken, char* serverId, char* password) -{ - // esnure that any persistence saving is done, so we know masterserver has newest - while (m_savingPersistentData) - Sleep(100); + if (authInfoJson.HasParseError()) + { + spdlog::error("Failed reading masterserver authentication response: encountered parse error \"{}\"", rapidjson::GetParseError_En(authInfoJson.GetParseError())); + goto REQUEST_END_CLEANUP; + } - LazyCreateHttpClient(); - spdlog::info("Attempting authentication with server of id \"{}\"", serverId); + if (!authInfoJson.IsObject()) + { + spdlog::error("Failed reading masterserver authentication response: root object is not an object"); + goto REQUEST_END_CLEANUP; + } - if (auto result = m_httpClient->Post(fmt::format("/client/auth_with_server?id={}&playerToken={}&server={}&password={}", uid, playerToken, serverId, password).c_str())) - { - m_successfullyConnected = true; + if (authInfoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + goto REQUEST_END_CLEANUP; + } - rapidjson::Document connectionInfoJson; - connectionInfoJson.Parse(result->body.c_str()); + if (!authInfoJson["success"].IsTrue()) + { + spdlog::error("Authentication with masterserver failed: \"success\" is not true"); + goto REQUEST_END_CLEANUP; + } - if (connectionInfoJson.HasParseError()) - { - spdlog::error("Failed reading masterserver authentication response: encountered parse error \"{}\"", rapidjson::GetParseError_En(connectionInfoJson.GetParseError())); - 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; + } - if (!connectionInfoJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } + AuthData newAuthData; + strncpy(newAuthData.uid, authInfoJson["id"].GetString(), sizeof(newAuthData.uid)); + newAuthData.uid[sizeof(newAuthData.uid) - 1] = 0; - if (connectionInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(result->body); - goto REQUEST_END_CLEANUP; - } + newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size(); + newAuthData.pdata = new char[newAuthData.pdataSize]; + //memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize); - if (!connectionInfoJson["success"].IsTrue()) - { - spdlog::error("Authentication with masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } + 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; + } - 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; - } + 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_pendingConnectionInfo.ip.S_un.S_addr = inet_addr(connectionInfoJson["ip"].GetString()); - m_pendingConnectionInfo.port = connectionInfoJson["port"].GetInt(); + 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; + } - strncpy(m_pendingConnectionInfo.authToken, connectionInfoJson["authToken"].GetString(), 31); - m_pendingConnectionInfo.authToken[31] = 0; + REQUEST_END_CLEANUP: + m_authenticatingWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; - m_hasPendingConnectionInfo = true; - m_successfullyAuthenticatedWithGameServer = true; - } - else - { - spdlog::error("Failed authenticating with server: error {}", HttplibErrorToString(result.error())); - m_successfullyConnected = false; - m_successfullyAuthenticatedWithGameServer = 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; + } -REQUEST_END_CLEANUP: - m_authenticatingWithGameServer = false; - m_scriptAuthenticatingWithGameServer = false; + curl_easy_cleanup(curl); + }); - return m_successfullyConnected; + requestThread.detach(); } void MasterServerManager::AuthenticateWithServer(char* uid, char* playerToken, char* serverId, char* password) @@ -630,129 +567,99 @@ void MasterServerManager::AuthenticateWithServer(char* uid, char* playerToken, c m_scriptAuthenticatingWithGameServer = true; m_successfullyAuthenticatedWithGameServer = false; - std::thread requestThread([this, uid, playerToken, serverId, password]() - { - for (int i = 0; i < 5; i++) - { - if (!AuthenticateWithServerThread(uid, playerToken, serverId, password)) - spdlog::info("retrying..."); - else - return; - } - }); - - requestThread.detach(); -} - -bool MasterServerManager::AddSelfToServerListThread(int port, int authPort, char* name, char* description, char* map, char* playlist, int maxPlayers, char* password) -{ - m_ownServerId[0] = 0; + std::string uidStr(uid); + std::string tokenStr(playerToken); + std::string serverIdStr(serverId); + std::string passwordStr(password); - std::string request; - if (*password) - request = fmt::format("/server/add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", port, authPort, name, description, map, playlist, maxPlayers, password); - else - request = fmt::format("/server/add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password=", port, authPort, name, description, map, playlist, maxPlayers); + 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); - // build modinfo obj - rapidjson::Document modinfoDoc; - modinfoDoc.SetObject(); - modinfoDoc.AddMember("Mods", rapidjson::Value(rapidjson::kArrayType), modinfoDoc.GetAllocator()); + spdlog::info("Attempting authentication with server of id \"{}\"", serverIdStr); - int currentModIndex = 0; - for (Mod& mod : g_ModManager->m_loadedMods) - { - if (!mod.Enabled || (!mod.RequiredOnClient && !mod.Pdiff.size())) - continue; + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - modinfoDoc["Mods"].PushBack(rapidjson::Value(rapidjson::kObjectType), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator()); + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - currentModIndex++; - } + { + char* escapedPassword = curl_easy_escape(curl, passwordStr.c_str(), passwordStr.length()); - rapidjson::StringBuffer buffer; - buffer.Clear(); - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - modinfoDoc.Accept(writer); - const char* modInfoString = buffer.GetString(); + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/auth_with_server?id={}&playerToken={}&server={}&password={}", Cvar_ns_masterserver_hostname->m_pszString, uidStr, tokenStr, serverIdStr, escapedPassword).c_str()); + + curl_free(escapedPassword); + } - httplib::MultipartFormDataItems requestItems = { - {"modinfo", std::string(modInfoString, buffer.GetSize()), "modinfo.json", "application/octet-stream"} - }; + CURLcode result = curl_easy_perform(curl); - LazyCreateHttpClient(); - if (auto result = m_httpClient->Post(request.c_str(), requestItems)) - { - m_successfullyConnected = true; + if (result == CURLcode::CURLE_OK) + { + m_successfullyConnected = true; - rapidjson::Document serverAddedJson; - serverAddedJson.Parse(result->body.c_str()); + rapidjson_document connectionInfoJson; + connectionInfoJson.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 (connectionInfoJson.HasParseError()) + { + spdlog::error("Failed reading masterserver authentication response: encountered parse error \"{}\"", rapidjson::GetParseError_En(connectionInfoJson.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 (!connectionInfoJson.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(result->body); - 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 (!serverAddedJson["success"].IsTrue()) - { - spdlog::error("Adding server to masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } + if (!connectionInfoJson["success"].IsTrue()) + { + spdlog::error("Authentication with masterserver failed: \"success\" is not true"); + goto REQUEST_END_CLEANUP; + } - if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString()) - { - spdlog::error("Failed reading masterserver response: malformed json object"); - 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; + } - strncpy(m_ownServerId, serverAddedJson["id"].GetString(), sizeof(m_ownServerId)); - m_ownServerId[sizeof(m_ownServerId) - 1] = 0; + 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; - // 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] { - while (*m_ownServerId) + m_hasPendingConnectionInfo = true; + m_successfullyAuthenticatedWithGameServer = true; + } + else { - Sleep(10000); - for (int i = 0; i < 3; i++) - { - if (!(int)m_httpClient->Post(fmt::format("/server/heartbeat?id={}&playerCount={}", m_ownServerId, g_ServerAuthenticationManager->m_additionalPlayerData.size()).c_str()).error()) - break; - else - spdlog::warn("retrying heartbeat..."); - } + spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result)); + m_successfullyConnected = false; + m_successfullyAuthenticatedWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; } - }); - heartbeatThread.detach(); - } - else - { - spdlog::error("Failed adding self to server list: error {}", HttplibErrorToString(result.error())); - m_successfullyConnected = false; - } + REQUEST_END_CLEANUP: + m_authenticatingWithGameServer = false; + m_scriptAuthenticatingWithGameServer = false; + curl_easy_cleanup(curl); + }); -REQUEST_END_CLEANUP: - return m_successfullyConnected; + requestThread.detach(); } void MasterServerManager::AddSelfToServerList(int port, int authPort, char* name, char* description, char* map, char* playlist, int maxPlayers, char* password) @@ -768,34 +675,164 @@ void MasterServerManager::AddSelfToServerList(int port, int authPort, char* name m_bRequireClientAuth = true; - std::thread requestThread([this, port, authPort, name, description, map, playlist, maxPlayers, password] + 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] { - for (int i = 0; i < 5; i++) + m_ownServerId[0] = 0; + m_ownServerAuthToken[0] = 0; + + // build modinfo obj + rapidjson_document modinfoDoc; + modinfoDoc.SetObject(); + modinfoDoc.AddMember("Mods", rapidjson_document::GenericValue(rapidjson::kArrayType), modinfoDoc.GetAllocator()); + + int currentModIndex = 0; + for (Mod& mod : g_ModManager->m_loadedMods) + { + if (!mod.Enabled || (!mod.RequiredOnClient && !mod.Pdiff.size())) + continue; + + modinfoDoc["Mods"].PushBack(rapidjson_document::GenericValue(rapidjson::kObjectType), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator()); + + currentModIndex++; + } + + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + modinfoDoc.Accept(writer); + const char* modInfoString = buffer.GetString(); + + 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, modInfoString, buffer.GetSize()); + 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 { - if (!AddSelfToServerListThread(port, authPort, name, description, map, playlist, maxPlayers, password)) - spdlog::info("retrying..."); - else - return; + 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->m_pszString, 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); } - }); - requestThread.detach(); -} + CURLcode result = curl_easy_perform(curl); -bool MasterServerManager::UpdateServerMapAndPlaylistThread(char* map, char* playlist, int maxPlayers) -{ - // we dont process this at all atm, maybe do later, but atm not necessary - LazyCreateHttpClient(); - if (auto result = m_httpClient->Post(fmt::format("/server/update_values?id={}&map={}&playlist={}&maxPlayers={}", m_ownServerId, map, playlist, maxPlayers).c_str())) - { - m_successfullyConnected = true; - } - else - { - m_successfullyConnected = false; - } + 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); + 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; + + // 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); + + do + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/server/heartbeat?id={}&playerCount={}", Cvar_ns_masterserver_hostname->m_pszString, m_ownServerId, g_ServerAuthenticationManager->m_additionalPlayerData.size()).c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLcode::CURLE_OK) + 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; + } - return m_successfullyConnected; + REQUEST_END_CLEANUP: + curl_easy_cleanup(curl); + curl_mime_free(mime); + }); + + requestThread.detach(); } void MasterServerManager::UpdateServerMapAndPlaylist(char* map, char* playlist, int maxPlayers) @@ -804,15 +841,38 @@ void MasterServerManager::UpdateServerMapAndPlaylist(char* map, char* playlist, if (!*m_ownServerId) return; - std::thread requestThread([this, map, playlist, maxPlayers] + std::string strMap(map); + std::string strPlaylist(playlist); + + std::thread requestThread([this, strMap, strPlaylist, maxPlayers] { - for (int i = 0; i < 3; i++) + 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 { - if (!UpdateServerMapAndPlaylistThread(map, playlist, maxPlayers)) - spdlog::info("retrying..."); - else - return; + 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->m_pszString, 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(); @@ -824,44 +884,30 @@ void MasterServerManager::UpdateServerPlayerCount(int playerCount) if (!*m_ownServerId) return; - std::thread requestThread([this, playerCount] { + std::thread requestThread([this, playerCount] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - // we dont process this at all atm, maybe do later, but atm not necessary - LazyCreateHttpClient(); - if (auto result = m_httpClient->Post(fmt::format("/server/update_values?id={}&playerCount={}", m_ownServerId, playerCount).c_str())) - { + 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->m_pszString, 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(); } -bool MasterServerManager::WritePlayerPersistentDataThread(std::string playerIdTemp, char* pdata, size_t pdataSize) -{ - httplib::MultipartFormDataItems requestItems = { - { "pdata", std::string(pdata, pdataSize), "file.pdata", "application/octet-stream"} - }; - - // we dont process this at all atm, maybe do later, but atm not necessary - LazyCreateHttpClient(); - if (auto result = m_httpClient->Post(fmt::format("/accounts/write_persistence?id={}&serverId={}", playerIdTemp, m_ownServerId).c_str(), requestItems)) - { - m_successfullyConnected = true; - } - else - { - m_successfullyConnected = false; - } - - m_savingPersistentData = false; - return m_successfullyConnected; -} - 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 @@ -872,35 +918,43 @@ void MasterServerManager::WritePlayerPersistentData(char* playerId, char* pdata, return; } - std::string playerIdTemp(playerId); - std::thread requestThread([this, playerIdTemp, pdata, pdataSize] + std::string strPlayerId(playerId); + std::string strPdata(pdata, pdataSize); + + std::thread requestThread([this, strPlayerId, strPdata, pdataSize] { - for (int i = 0; i < 5; i++) - { - if (!WritePlayerPersistentDataThread(playerIdTemp, pdata, pdataSize)) - spdlog::info("retrying..."); - else - return; - } - }); + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - requestThread.detach(); -} + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/accounts/write_persistence?id={}&serverId={}", Cvar_ns_masterserver_hostname->m_pszString, 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); -bool MasterServerManager::RemoveSelfFromServerListThread() -{ - // we dont process this at all atm, maybe do later, but atm not necessary - LazyCreateHttpClient(); - if (auto result = m_httpClient->Delete(fmt::format("/server/remove_server?id={}", m_ownServerId).c_str())) - { - m_successfullyConnected = true; - } - else - { - m_successfullyConnected = false; - } + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); - m_ownServerId[0] = 0; + 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() @@ -910,13 +964,23 @@ void MasterServerManager::RemoveSelfFromServerList() return; std::thread requestThread([this] { - for (int i = 0; i < 5; i++) - { - if (!RemoveSelfFromServerListThread()) - spdlog::info("retrying..."); - else - return; - } + 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->m_pszString, m_ownServerId).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(); @@ -943,6 +1007,10 @@ void CHostState__State_NewGameHook(CHostState* hostState) if (maxPlayersVar) // GetCurrentPlaylistVar can return null so protect against this maxPlayers = std::stoi(maxPlayersVar); + // Copy new server name cvar to source + Cvar_hostname->m_pszString = Cvar_ns_server_name->m_pszString; + Cvar_hostname->m_StringLength = Cvar_ns_server_name->m_StringLength; + g_MasterServerManager->AddSelfToServerList(Cvar_hostport->m_nValue, Cvar_ns_player_auth_port->m_nValue, Cvar_ns_server_name->m_pszString, Cvar_ns_server_desc->m_pszString, hostState->m_levelName, (char*)GetCurrentPlaylistName(), maxPlayers, Cvar_ns_server_password->m_pszString); g_ServerAuthenticationManager->StartPlayerAuthServer(); g_ServerAuthenticationManager->m_bNeedLocalAuthForNewgame = false; @@ -978,6 +1046,11 @@ void CHostState__State_GameShutdownHook(CHostState* hostState) CHostState__State_GameShutdown(hostState); } +MasterServerManager::MasterServerManager() : m_pendingConnectionInfo{}, m_ownServerId{ "" }, m_ownClientAuthToken{ "" } +{ + +} + void InitialiseSharedMasterServer(HMODULE baseAddress) { Cvar_ns_masterserver_hostname = RegisterConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); @@ -989,6 +1062,9 @@ void InitialiseSharedMasterServer(HMODULE baseAddress) Cvar_ns_server_password = RegisterConVar("ns_server_password", "", FCVAR_GAMEDLL, ""); Cvar_ns_report_server_to_masterserver = RegisterConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, ""); Cvar_ns_report_sp_server_to_masterserver = RegisterConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, ""); + + Cvar_hostname = *(ConVar**)((char*)baseAddress + 0x1315bae8); + g_MasterServerManager = new MasterServerManager; RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "", FCVAR_CLIENTDLL); @@ -998,4 +1074,4 @@ void InitialiseSharedMasterServer(HMODULE baseAddress) ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E5D0, CHostState__State_ChangeLevelMPHook, reinterpret_cast<LPVOID*>(&CHostState__State_ChangeLevelMP)); ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E520, CHostState__State_ChangeLevelSPHook, reinterpret_cast<LPVOID*>(&CHostState__State_ChangeLevelSP)); ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x16E640, CHostState__State_GameShutdownHook, reinterpret_cast<LPVOID*>(&CHostState__State_GameShutdown)); -}
\ No newline at end of file +} |