// BeamMP, the BeamNG.drive multiplayer mod. // Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors. // // BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include "THeartbeatThread.h" #include "ChronoWrapper.h" #include "Client.h" #include "Common.h" #include "Http.h" // #include "SocketIO.h" #include #include #include #include namespace json = rapidjson; void THeartbeatThread::operator()() { RegisterThread("Heartbeat"); std::string Body; std::string T; // these are "hot-change" related variables static std::string Last; static std::chrono::high_resolution_clock::time_point LastNormalUpdateTime = std::chrono::high_resolution_clock::now(); static std::chrono::high_resolution_clock::time_point LastUpdateReminderTime = std::chrono::high_resolution_clock::now(); bool isAuth = false; std::chrono::high_resolution_clock::duration UpdateReminderTimePassed; while (!Application::IsShuttingDown()) { auto UpdateReminderTimeout = ChronoWrapper::TimeFromStringWithLiteral(Application::Settings.getAsString(Settings::Key::Misc_UpdateReminderTime)); Body = GenerateCall(); // a hot-change occurs when a setting has changed, to update the backend of that change. auto Now = std::chrono::high_resolution_clock::now(); bool Unchanged = Last == Body; auto TimePassed = (Now - LastNormalUpdateTime); UpdateReminderTimePassed = (Now - LastUpdateReminderTime); auto Threshold = Unchanged ? 30 : 5; if (TimePassed < std::chrono::seconds(Threshold)) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } beammp_debug("heartbeat (after " + std::to_string(std::chrono::duration_cast(TimePassed).count()) + "s)"); Last = Body; LastNormalUpdateTime = Now; auto Target = "/heartbeat"; unsigned int ResponseCode = 0; json::Document Doc; bool Ok = false; for (const auto& Url : Application::GetBackendUrlsInOrder()) { T = Http::POST(Url + Target, Body, "application/json", &ResponseCode, { { "api-v", "2" } }); if (!Application::Settings.getAsBool(Settings::Key::General_Private)) { beammp_debug("Backend response was: `" + T + "`"); } Doc.Parse(T.data(), T.size()); if (Doc.HasParseError() || !Doc.IsObject()) { if (!Application::Settings.getAsBool(Settings::Key::General_Private)) { beammp_trace("Backend response failed to parse as valid json"); } } else if (ResponseCode != 200) { beammp_errorf("Response code from the heartbeat: {}", ResponseCode); } else { // all ok Ok = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } std::string Status {}; std::string Code {}; std::string Message {}; const auto StatusKey = "status"; const auto CodeKey = "code"; const auto MessageKey = "msg"; if (Ok) { if (Doc.HasMember(StatusKey) && Doc[StatusKey].IsString()) { Status = Doc[StatusKey].GetString(); } else { Ok = false; } if (Doc.HasMember(CodeKey) && Doc[CodeKey].IsString()) { Code = Doc[CodeKey].GetString(); } else { Ok = false; } if (Doc.HasMember(MessageKey) && Doc[MessageKey].IsString()) { Message = Doc[MessageKey].GetString(); } else { Ok = false; } if (!Ok) { beammp_error("Missing/invalid json members in backend response"); } } else { if (!Application::Settings.getAsBool(Settings::Key::General_Private)) { beammp_warn("Backend failed to respond to a heartbeat. Your server may temporarily disappear from the server list. This is not an error, and will likely resolve itself soon. Direct connect will still work."); } } if (Ok && !isAuth && !Application::Settings.getAsBool(Settings::Key::General_Private)) { if (Status == "2000") { beammp_info(("Authenticated! " + Message)); isAuth = true; } else if (Status == "200") { beammp_info(("Resumed authenticated session! " + Message)); isAuth = true; } else { if (Message.empty()) { Message = "Backend didn't provide a reason."; } beammp_error("Backend REFUSED the auth key. Reason: " + Message); } } if (isAuth || Application::Settings.getAsBool(Settings::Key::General_Private)) { Application::SetSubsystemStatus("Heartbeat", Application::Status::Good); } if (!Application::Settings.getAsBool(Settings::Key::Misc_ImScaredOfUpdates) && UpdateReminderTimePassed.count() > UpdateReminderTimeout.count()) { LastUpdateReminderTime = std::chrono::high_resolution_clock::now(); Application::CheckForUpdates(); } } } std::string THeartbeatThread::GenerateCall() { nlohmann::json Ret = { { "players", std::to_string(mServer.ClientCount()) }, { "maxplayers", std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) }, { "port", std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)) }, { "map", Application::Settings.getAsString(Settings::Key::General_Map) }, { "private", Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false" }, { "version", Application::ServerVersionString() }, { "clientversion", Application::ClientMinimumVersion().AsString() }, { "name", Application::Settings.getAsString(Settings::Key::General_Name) }, { "tags", Application::Settings.getAsString(Settings::Key::General_Tags) }, { "guests", Application::Settings.getAsBool(Settings::Key::General_AllowGuests) ? "true" : "false" }, { "modlist", mResourceManager.TrimmedList() }, { "modstotalsize", std::to_string(mResourceManager.MaxModSize()) }, { "modstotal", std::to_string(mResourceManager.ModsLoaded()) }, { "playerslist", GetPlayers() }, { "desc", Application::Settings.getAsString(Settings::Key::General_Description) } }; lastCall = Ret.dump(); // Add sensitive information here because value of lastCall is used for the information packet. Ret["uuid"] = Application::Settings.getAsString(Settings::Key::General_AuthKey); return Ret.dump(); } THeartbeatThread::THeartbeatThread(TResourceManager& ResourceManager, TServer& Server) : mResourceManager(ResourceManager) , mServer(Server) { Application::SetSubsystemStatus("Heartbeat", Application::Status::Starting); Application::RegisterShutdownHandler([&] { Application::SetSubsystemStatus("Heartbeat", Application::Status::ShuttingDown); if (mThread.joinable()) { mThread.join(); } Application::SetSubsystemStatus("Heartbeat", Application::Status::Shutdown); }); Start(); } std::string THeartbeatThread::GetPlayers() { std::string Return; mServer.ForEachClient([&](const std::weak_ptr& ClientPtr) -> bool { ReadLock Lock(mServer.GetClientMutex()); if (!ClientPtr.expired()) { Return += ClientPtr.lock()->GetName() + ";"; } return true; }); return Return; } /*THeartbeatThread::~THeartbeatThread() { }*/