// 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 "TServer.h" #include "Client.h" #include "Common.h" #include "CustomAssert.h" #include "TLuaEngine.h" #include "TNetwork.h" #include "TPPSMonitor.h" #include #include #include #include #include #include #include "LuaAPI.h" #undef GetObject // Fixes Windows #include "Json.h" static std::optional> GetPidVid(const std::string& str) { auto IDSep = str.find('-'); std::string pid = str.substr(0, IDSep); std::string vid = str.substr(IDSep + 1); if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { try { int PID = stoi(pid); int VID = stoi(vid); return { { PID, VID } }; } catch (const std::exception&) { return std::nullopt; } } return std::nullopt; } TEST_CASE("GetPidVid") { SUBCASE("Valid singledigit") { const auto MaybePidVid = GetPidVid("0-1"); CHECK(MaybePidVid); auto [pid, vid] = MaybePidVid.value(); CHECK_EQ(pid, 0); CHECK_EQ(vid, 1); } SUBCASE("Valid doubledigit") { const auto MaybePidVid = GetPidVid("10-12"); CHECK(MaybePidVid); auto [pid, vid] = MaybePidVid.value(); CHECK_EQ(pid, 10); CHECK_EQ(vid, 12); } SUBCASE("Valid doubledigit 2") { const auto MaybePidVid = GetPidVid("10-2"); CHECK(MaybePidVid); auto [pid, vid] = MaybePidVid.value(); CHECK_EQ(pid, 10); CHECK_EQ(vid, 2); } SUBCASE("Valid doubledigit 3") { const auto MaybePidVid = GetPidVid("33-23"); CHECK(MaybePidVid); auto [pid, vid] = MaybePidVid.value(); CHECK_EQ(pid, 33); CHECK_EQ(vid, 23); } SUBCASE("Valid doubledigit 4") { const auto MaybePidVid = GetPidVid("3-23"); CHECK(MaybePidVid); auto [pid, vid] = MaybePidVid.value(); CHECK_EQ(pid, 3); CHECK_EQ(vid, 23); } SUBCASE("Empty string") { const auto MaybePidVid = GetPidVid(""); CHECK(!MaybePidVid); } SUBCASE("Invalid separator") { const auto MaybePidVid = GetPidVid("0x0"); CHECK(!MaybePidVid); } SUBCASE("Missing pid") { const auto MaybePidVid = GetPidVid("-0"); CHECK(!MaybePidVid); } SUBCASE("Missing vid") { const auto MaybePidVid = GetPidVid("0-"); CHECK(!MaybePidVid); } SUBCASE("Invalid pid") { const auto MaybePidVid = GetPidVid("x-0"); CHECK(!MaybePidVid); } SUBCASE("Invalid vid") { const auto MaybePidVid = GetPidVid("0-x"); CHECK(!MaybePidVid); } } TServer::TServer(const std::vector& Arguments) { beammp_info("BeamMP Server v" + Application::ServerVersionString()); Application::SetSubsystemStatus("Server", Application::Status::Starting); Application::SetSubsystemStatus("Server", Application::Status::Good); } void TServer::RemoveClient(const std::weak_ptr& WeakClientPtr) { std::shared_ptr LockedClientPtr { nullptr }; try { LockedClientPtr = WeakClientPtr.lock(); } catch (const std::exception&) { // silently fail, as there's nothing to do return; } beammp_assert(LockedClientPtr != nullptr); TClient& Client = *LockedClientPtr; beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")"); Client.ClearCars(); WriteLock Lock(mClientsMutex); mClients.erase(WeakClientPtr.lock()); } void TServer::ForEachClient(const std::function)>& Fn) { decltype(mClients) Clients; { ReadLock lock(mClientsMutex); Clients = mClients; } for (auto& Client : Clients) { if (!Fn(Client)) { break; } } } size_t TServer::ClientCount() const { ReadLock Lock(mClientsMutex); return mClients.size(); } void TServer::GlobalParser(const std::weak_ptr& Client, std::vector&& Packet, TPPSMonitor& PPSMonitor, TNetwork& Network) { constexpr std::string_view ABG = "ABG:"; if (Packet.size() >= ABG.size() && std::equal(Packet.begin(), Packet.begin() + ABG.size(), ABG.begin(), ABG.end())) { Packet.erase(Packet.begin(), Packet.begin() + ABG.size()); try { Packet = DeComp(Packet); } catch (const InvalidDataError& ) { auto LockedClient = Client.lock(); beammp_errorf("Failed to decompress packet from client {}. The client sent invalid data and will now be disconnected.", LockedClient->GetID()); Network.ClientKick(*LockedClient, "Sent invalid compressed packet (this is likely a bug on your end)"); return; } catch (const std::runtime_error& e) { auto LockedClient = Client.lock(); beammp_errorf("Failed to decompress packet from client {}: {}. The server might be out of RAM! The client will now be disconnected.", LockedClient->GetID(), e.what()); Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)"); return; } } if (Packet.empty()) { return; } if (Client.expired()) { return; } auto LockedClient = Client.lock(); std::any Res; char Code = Packet.at(0); std::string StringPacket(reinterpret_cast(Packet.data()), Packet.size()); // V to Y if (Code <= 89 && Code >= 86) { PPSMonitor.IncrementInternalPPS(); Network.SendToAll(LockedClient.get(), Packet, false, false); return; } switch (Code) { case 'H': // initial connection if (!Network.SyncClient(Client)) { // TODO handle } return; case 'p': if (!Network.Respond(*LockedClient, StringToVector("p"), false)) { // failed to send LockedClient->Disconnect("Failed to send ping"); } else { Network.UpdatePlayer(*LockedClient); } return; case 'O': if (Packet.size() > 1000) { beammp_debug(("Received data from: ") + LockedClient->GetName() + (" Size: ") + std::to_string(Packet.size())); } ParseVehicle(*LockedClient, StringPacket, Network); return; case 'C': { if (Packet.size() < 4 || std::find(Packet.begin() + 3, Packet.end(), ':') == Packet.end()) break; const auto PacketAsString = std::string(reinterpret_cast(Packet.data()), Packet.size()); std::string Message = ""; const auto ColonPos = PacketAsString.find(':', 3); if (ColonPos != std::string::npos && ColonPos + 2 < PacketAsString.size()) { Message = PacketAsString.substr(ColonPos + 2); } if (Message.empty()) { beammp_debugf("Empty chat message received from '{}' ({}), ignoring it", LockedClient->GetName(), LockedClient->GetID()); return; } auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", "", LockedClient->GetID(), LockedClient->GetName(), Message); TLuaEngine::WaitForAll(Futures); LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1)); bool Rejected = std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Elem) { return !Elem->Error && Elem->Result.is() && bool(Elem->Result.as()); }); if (!Rejected) { std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message); Network.SendToAll(nullptr, StringToVector(SanitizedPacket), true, true); } auto PostFutures = LuaAPI::MP::Engine->TriggerEvent("postChatMessage", "", !Rejected, LockedClient->GetID(), LockedClient->GetName(), Message); LuaAPI::MP::Engine->ReportErrors(PostFutures); return; } case 'E': HandleEvent(*LockedClient, StringPacket); return; case 'N': Network.SendToAll(LockedClient.get(), Packet, false, true); return; case 'Z': // position packet PPSMonitor.IncrementInternalPPS(); Network.SendToAll(LockedClient.get(), Packet, false, false); HandlePosition(*LockedClient, StringPacket); return; default: return; } } void TServer::HandleEvent(TClient& c, const std::string& RawData) { // E:Name:Data // Data is allowed to have ':' if (RawData.size() < 2) { beammp_debugf("Client '{}' ({}) tried to send an empty event, ignoring", c.GetName(), c.GetID()); return; } auto NameDataSep = RawData.find(':', 2); if (NameDataSep == std::string::npos) { beammp_warn("received event in invalid format (missing ':'), got: '" + RawData + "'"); } std::string Name = RawData.substr(2, NameDataSep - 2); std::string Data = RawData.substr(NameDataSep + 1); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent(Name, "", c.GetID(), Data)); } bool TServer::IsUnicycle(TClient& c, const std::string& CarJson) { try { auto Car = nlohmann::json::parse(CarJson); const std::string jbm = "jbm"; if (Car.contains(jbm) && Car[jbm].is_string() && Car[jbm] == "unicycle") { return true; } } catch (const std::exception& e) { beammp_warn("Failed to parse vehicle data as json for client " + std::to_string(c.GetID()) + ": '" + CarJson + "'."); } return false; } bool TServer::ShouldSpawn(TClient& c, const std::string& CarJson, int ID) { if (IsUnicycle(c, CarJson) && c.GetUnicycleID() < 0) { c.SetUnicycleID(ID); return true; } else { return c.GetCarCount() < Application::Settings.getAsInt(Settings::Key::General_MaxCars); } } void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network) { if (Pckt.length() < 6) return; std::string Packet = Pckt; char Code = Packet.at(1); int PID = -1; int VID = -1; std::string Data = Packet.substr(3), pid, vid; switch (Code) { // Spawned Destroyed Switched/Moved NotFound Reset case 's': beammp_tracef("got 'Os' packet: '{}' ({})", Packet, Packet.size()); if (Data.at(0) == '0') { int CarID = c.GetOpenCarID(); beammp_debugf("'{}' created a car with ID {}", c.GetName(), CarID); std::string CarJson = Packet.substr(5); Packet = "Os:" + c.GetRoles() + ":" + c.GetName() + ":" + std::to_string(c.GetID()) + "-" + std::to_string(CarID) + ":" + CarJson; auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleSpawn", "", c.GetID(), CarID, Packet.substr(3)); TLuaEngine::WaitForAll(Futures); bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Result) { return !Result->Error && Result->Result.is() && Result->Result.as() != 0; }); bool SpawnConfirmed = false; if (ShouldSpawn(c, CarJson, CarID) && !ShouldntSpawn) { c.AddNewCar(CarID, Packet); Network.SendToAll(nullptr, StringToVector(Packet), true, true); SpawnConfirmed = true; } else { if (!Network.Respond(c, StringToVector(Packet), true)) { // TODO: handle } std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(CarID); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), CarID)); if (!Network.Respond(c, StringToVector(Destroy), true)) { // TODO: handle } beammp_debugf("{} (force : car limit/lua) removed ID {}", c.GetName(), CarID); SpawnConfirmed = false; } auto PostFutures = LuaAPI::MP::Engine->TriggerEvent("postVehicleSpawn", "", SpawnConfirmed, c.GetID(), CarID, Packet.substr(3)); // the post event is not cancellable so we dont wait for it LuaAPI::MP::Engine->ReportErrors(PostFutures); } return; case 'c': { beammp_trace(std::string(("got 'Oc' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); if (MaybePidVid) { std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { auto Futures = LuaAPI::MP::Engine->TriggerEvent("onVehicleEdited", "", c.GetID(), VID, Packet.substr(3)); TLuaEngine::WaitForAll(Futures); bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Result) { return !Result->Error && Result->Result.is() && Result->Result.as() != 0; }); auto FoundPos = Packet.find('{'); FoundPos = FoundPos == std::string::npos ? 0 : FoundPos; // attempt at sanitizing this bool Allowed = false; if ((c.GetUnicycleID() != VID || IsUnicycle(c, Packet.substr(FoundPos))) && !ShouldntAllow) { Network.SendToAll(&c, StringToVector(Packet), false, true); Apply(c, VID, Packet); Allowed = true; } else { if (c.GetUnicycleID() == VID) { c.SetUnicycleID(-1); } std::string Destroy = "Od:" + std::to_string(c.GetID()) + "-" + std::to_string(VID); Network.SendToAll(nullptr, StringToVector(Destroy), true, true); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), VID)); c.DeleteCar(VID); Allowed = false; } auto PostFutures = LuaAPI::MP::Engine->TriggerEvent("postVehicleEdited", "", Allowed, c.GetID(), VID, Packet.substr(3)); // the post event is not cancellable so we dont wait for it LuaAPI::MP::Engine->ReportErrors(PostFutures); } return; } case 'd': { beammp_trace(std::string(("got 'Od' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); if (MaybePidVid) { std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { if (c.GetUnicycleID() == VID) { c.SetUnicycleID(-1); } Network.SendToAll(nullptr, StringToVector(Packet), true, true); // TODO: should this trigger on all vehicle deletions? LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", c.GetID(), VID)); c.DeleteCar(VID); beammp_debug(c.GetName() + (" deleted car with ID ") + std::to_string(VID)); } return; } case 'r': { beammp_trace(std::string(("got 'Or' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); if (MaybePidVid) { std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { Data = Data.substr(Data.find('{')); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleReset", "", c.GetID(), VID, Data)); Network.SendToAll(&c, StringToVector(Packet), false, true); } return; } case 't': { beammp_trace(std::string(("got 'Ot' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); if (MaybePidVid) { std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { Network.SendToAll(&c, StringToVector(Packet), false, true); } return; } case 'm': { Network.SendToAll(&c, StringToVector(Packet), false, true); return; } case 'p': { beammp_trace(std::string(("got 'Op' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); auto MaybePidVid = GetPidVid(Data.substr(0, Data.find(':', 1))); if (MaybePidVid) { std::tie(PID, VID) = MaybePidVid.value(); } if (PID != -1 && VID != -1 && PID == c.GetID()) { Data = Data.substr(Data.find('[')); LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehiclePaintChanged", "", c.GetID(), VID, Data)); Network.SendToAll(&c, StringToVector(Packet), false, true); } return; } default: beammp_trace(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")"))); return; } } void TServer::Apply(TClient& c, int VID, const std::string& pckt) { auto FoundPos = pckt.find('{'); if (FoundPos == std::string::npos) { beammp_error("Malformed packet received, no '{' found"); return; } std::string Packet = pckt.substr(FoundPos); std::string VD = c.GetCarData(VID); if (VD.empty()) { beammp_error("Tried to apply change to vehicle that does not exist"); return; } std::string Header = VD.substr(0, VD.find('{')); FoundPos = VD.find('{'); if (FoundPos == std::string::npos) { return; } VD = VD.substr(FoundPos); rapidjson::Document Veh, Pack; Veh.Parse(VD.c_str()); if (Veh.HasParseError()) { beammp_error("Could not get vehicle config!"); return; } Pack.Parse(Packet.c_str()); if (Pack.HasParseError() || Pack.IsNull()) { beammp_error("Could not get active vehicle config!"); return; } for (auto& M : Pack.GetObject()) { if (Veh[M.name].IsNull()) { Veh.AddMember(M.name, M.value, Veh.GetAllocator()); } else { Veh[M.name] = Pack[M.name]; } } rapidjson::StringBuffer Buffer; rapidjson::Writer writer(Buffer); Veh.Accept(writer); c.SetCarData(VID, Header + Buffer.GetString()); } void TServer::InsertClient(const std::shared_ptr& NewClient) { beammp_debug("inserting client (" + std::to_string(ClientCount()) + ")"); WriteLock Lock(mClientsMutex); // TODO why is there 30+ threads locked here (void)mClients.insert(NewClient); } struct PidVidData { int PID; int VID; std::string Data; }; static std::optional ParsePositionPacket(const std::string& Packet) { if (Packet.size() < 3) { // invalid packet return std::nullopt; } // Zp:PID-VID:DATA std::string withoutCode = Packet.substr(3); // parse veh ID if (auto DataBeginPos = withoutCode.find('{'); DataBeginPos != std::string::npos && DataBeginPos != 0) { // separator is :{, so position of { minus one auto PidVidOnly = withoutCode.substr(0, DataBeginPos - 1); auto MaybePidVid = GetPidVid(PidVidOnly); if (MaybePidVid) { int PID = -1; int VID = -1; // FIXME: check that the VID and PID are valid, so that we don't waste memory std::tie(PID, VID) = MaybePidVid.value(); std::string Data = withoutCode.substr(DataBeginPos); return PidVidData { .PID = PID, .VID = VID, .Data = Data, }; } else { // invalid packet return std::nullopt; } } // invalid packet return std::nullopt; } TEST_CASE("ParsePositionPacket") { const auto TestData = R"({"tim":10.428000331623,"vel":[-2.4171722121385e-05,-9.7184734153252e-06,-7.6420763232237e-06],"rot":[-0.0001296154171915,0.0031575385950029,0.98994906610295,0.14138903660382],"rvel":[5.3640324636461e-05,-9.9824529946024e-05,5.1664064641372e-05],"pos":[-0.27281248907838,-0.20515357944633,0.49695488960431],"ping":0.032999999821186})"; SUBCASE("All the pids and vids") { for (int pid = 0; pid < 100; ++pid) { for (int vid = 0; vid < 100; ++vid) { std::optional MaybeRes = ParsePositionPacket(fmt::format("Zp:{}-{}:{}", pid, vid, TestData)); CHECK(MaybeRes.has_value()); CHECK_EQ(MaybeRes.value().PID, pid); CHECK_EQ(MaybeRes.value().VID, vid); CHECK_EQ(MaybeRes.value().Data, TestData); } } } } void TServer::HandlePosition(TClient& c, const std::string& Packet) { if (auto Parsed = ParsePositionPacket(Packet); Parsed.has_value()) { c.SetCarPosition(Parsed.value().VID, Parsed.value().Data); } }