diff --git a/include/Client.h b/include/Client.h index fd239a1..5418391 100644 --- a/include/Client.h +++ b/include/Client.h @@ -10,6 +10,7 @@ #include "BoostAliases.h" #include "Common.h" #include "Compat.h" +#include "TResourceManager.h" #include "VehicleData.h" class TServer; @@ -103,6 +104,8 @@ public: TimeType::time_point ConnectionTime {}; + ModMap AllowedMods; + private: void InsertVehicle(int ID, const std::string& Data); diff --git a/include/Common.h b/include/Common.h index de618b0..3a915b0 100644 --- a/include/Common.h +++ b/include/Common.h @@ -55,6 +55,7 @@ constexpr std::string_view StrLogChat = "LogChat"; constexpr std::string_view StrSendErrors = "Misc.SendErrors"; constexpr std::string_view StrSendErrorsMessageEnabled = "Misc.SendErrorsShowMessage"; constexpr std::string_view StrHideUpdateMessages = "Misc.ImScaredOfUpdates"; +constexpr std::string_view StrIncludeSubdirectories = "Misc.IncludeSubdirectories"; // HTTP constexpr std::string_view StrHTTPServerEnabled = "HTTP.HTTPServerEnabled"; @@ -79,6 +80,9 @@ struct Version { template using SparseArray = std::unordered_map; +template +using HashMap = std::unordered_map; + using boost::variant; using boost::container::flat_map; diff --git a/include/TLuaEngine.h b/include/TLuaEngine.h index 45f329a..14e4ca9 100644 --- a/include/TLuaEngine.h +++ b/include/TLuaEngine.h @@ -35,14 +35,15 @@ namespace fs = std::filesystem; /** * std::variant means, that TLuaArgTypes may be one of the Types listed as template args */ -using TLuaValue = std::variant, float>; +using TLuaValue = std::variant, std::unordered_map, float>; enum TLuaType { String = 0, Int = 1, Json = 2, Bool = 3, StringStringMap = 4, - Float = 5, + StringSizeTMap = 5, + Float = 6, }; class TLuaPlugin; diff --git a/include/TNetwork.h b/include/TNetwork.h index 8a29f4a..deef42f 100644 --- a/include/TNetwork.h +++ b/include/TNetwork.h @@ -46,6 +46,7 @@ private: void Looper(const std::weak_ptr& c); int OpenID(); void OnDisconnect(const std::weak_ptr& ClientPtr); + ModMap GetClientMods(TClient& Client); void HandleResourcePackets(TClient& c, const std::vector& Packet); void SendFile(TClient& c, const std::string& Name); static bool TCPSendRaw(TClient& C, ip::tcp::socket& socket, const uint8_t* Data, size_t Size); diff --git a/include/TResourceManager.h b/include/TResourceManager.h index 6dc8e43..100c297 100644 --- a/include/TResourceManager.h +++ b/include/TResourceManager.h @@ -1,21 +1,22 @@ #pragma once #include "Common.h" +#include + +using ModMap = HashMap; class TResourceManager { public: TResourceManager(); - [[nodiscard]] size_t MaxModSize() const { return mMaxModSize; } - [[nodiscard]] std::string FileList() const { return mFileList; } - [[nodiscard]] std::string TrimmedList() const { return mTrimmedList; } - [[nodiscard]] std::string FileSizes() const { return mFileSizes; } - [[nodiscard]] int ModsLoaded() const { return mModsLoaded; } + [[nodiscard]] size_t TotalModsSize() const { return mTotalModSize; } + [[nodiscard]] ModMap FileMap() const { return mMods; } + [[nodiscard]] static std::string FormatForBackend(const ModMap& mods); + [[nodiscard]] static std::string FormatForClient(const ModMap& mods); + [[nodiscard]] static std::optional IsModValid(std::string& pathString, const ModMap& mods); + [[nodiscard]] int LoadedModCount() const { return mMods.size(); } private: - size_t mMaxModSize = 0; - std::string mFileSizes; - std::string mFileList; - std::string mTrimmedList; - int mModsLoaded = 0; + size_t mTotalModSize = 0; // size of all mods + ModMap mMods; // map of mod names }; \ No newline at end of file diff --git a/src/TConfig.cpp b/src/TConfig.cpp index 420fd28..eb8528f 100644 --- a/src/TConfig.cpp +++ b/src/TConfig.cpp @@ -87,6 +87,8 @@ void TConfig::FlushToFile() { SetComment(data["Misc"][StrSendErrors.data()].comments(), " You can turn on/off the SendErrors message you get on startup here"); data["Misc"][StrSendErrorsMessageEnabled.data()] = Application::GetSettingBool(StrSendErrorsMessageEnabled.data()); SetComment(data["Misc"][StrSendErrorsMessageEnabled.data()].comments(), " If SendErrors is `true`, the server will send helpful info about crashes and other issues back to the BeamMP developers. This info may include your config, who is on your server at the time of the error, and similar general information. This kind of data is vital in helping us diagnose and fix issues faster. This has no impact on server performance. You can opt-out of this system by setting this to `false`"); + data["Misc"][StrIncludeSubdirectories.data()] = Application::GetSettingBool(StrIncludeSubdirectories.data()); + SetComment(data["Misc"][StrIncludeSubdirectories.data()].comments(), " Whether or not to include subdirectories in General.ResourceFolder/Client when searching for `.zip`s"); // HTTP data["HTTP"][StrSSLKeyPath.data()] = Application::GetSettingString(StrSSLKeyPath.data()); data["HTTP"][StrSSLCertPath.data()] = Application::GetSettingString(StrSSLCertPath.data()); diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index 43ad7cb..736f56e 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -145,9 +145,9 @@ std::string THeartbeatThread::GenerateCall() { << "&version=" << Application::ServerVersionString() << "&clientversion=" << std::to_string(Application::ClientMajorVersion()) + ".0" // FIXME: Wtf. << "&name=" << Application::GetSettingString(StrName) - << "&modlist=" << mResourceManager.TrimmedList() - << "&modstotalsize=" << mResourceManager.MaxModSize() - << "&modstotal=" << mResourceManager.ModsLoaded() + << "&modlist=" << TResourceManager::FormatForBackend(mResourceManager.FileMap()) + << "&modstotalsize=" << mResourceManager.TotalModsSize() + << "&modstotal=" << mResourceManager.LoadedModCount() << "&playerslist=" << GetPlayers() << "&desc=" << Application::GetSettingString(StrDescription); return Ret.str(); diff --git a/src/TLuaEngine.cpp b/src/TLuaEngine.cpp index e584843..5d6bfcc 100644 --- a/src/TLuaEngine.cpp +++ b/src/TLuaEngine.cpp @@ -1054,6 +1054,15 @@ void TLuaEngine::StateThreadData::operator()() { LuaArgs.push_back(sol::make_object(StateView, Table)); break; } + case TLuaType::StringSizeTMap: { + auto Map = std::get>(Arg); + auto Table = StateView.create_table(); + for (const auto& [k, v] : Map) { + Table[k] = v; + } + LuaArgs.push_back(sol::make_object(StateView, Table)); + break; + } default: beammp_error("Unknown argument type, passed as nil"); break; diff --git a/src/TNetwork.cpp b/src/TNetwork.cpp index e9e1121..6d457b1 100644 --- a/src/TNetwork.cpp +++ b/src/TNetwork.cpp @@ -614,6 +614,34 @@ void TNetwork::SyncResources(TClient& c) { } } +ModMap TNetwork::GetClientMods(TClient& Client) { + auto AllMods = mResourceManager.FileMap(); + + auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerRequestMods", "", Client.GetName(), Client.GetRoles(), Client.IsGuest(), Client.GetIdentifiers(), AllMods); + TLuaEngine::WaitForAll(Futures); + + ModMap AllowedMods = AllMods; + + for (const std::shared_ptr& Future : Futures) { + sol::table Result; + if (!Future->Error && Future->Result.is()) { + Result = Future->Result.as(); + + for (const auto& [name, size] : AllMods) { + auto val = Result.get>(name); + if (!val.has_value()) { + AllowedMods.erase(name); + beammp_debugf("Not sending mod '{}' to player '{}' (from state '{}')", name, Client.GetName(), Future->StateId); + } + } + } + } + + Client.AllowedMods = AllowedMods; + + return AllowedMods; +} + void TNetwork::HandleResourcePackets(TClient& c, const std::vector& Packet) { if (Packet.empty()) return; @@ -627,7 +655,7 @@ void TNetwork::HandleResourcePackets(TClient& c, const std::vector& Pac case 'S': if (SubCode == 'R') { beammp_debug("Sending Mod Info"); - std::string ToSend = mResourceManager.FileList() + mResourceManager.FileSizes(); + std::string ToSend = TResourceManager::FormatForClient(GetClientMods(c)); if (ToSend.empty()) ToSend = "-"; if (!TCPSend(c, StringToVector(ToSend))) { @@ -641,24 +669,16 @@ void TNetwork::HandleResourcePackets(TClient& c, const std::vector& Pac } void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { - beammp_infof("{} ({}) requesting : '{}'", c.GetName(), c.GetID(), UnsafeName); + beammp_infof("{} ({}) requesting mod: '{}'", c.GetName(), c.GetID(), UnsafeName); - if (!fs::path(UnsafeName).has_filename()) { - if (!TCPSend(c, StringToVector("CO"))) { - c.Disconnect("TCP send failed in SendFile, when trying to cancel file transfer because the requested file doesn't contain a valid filename"); - return; - } - beammp_warn("File " + UnsafeName + " is not a file!"); - return; - } - auto FileName = fs::path(UnsafeName).filename().string(); - FileName = Application::GetSettingString(StrResourceFolder) + "/Client/" + FileName; + auto FileName = UnsafeName; - if (!std::filesystem::exists(FileName)) { + auto res = TResourceManager::IsModValid(FileName, c.AllowedMods); + + if (res.has_value()) { if (!TCPSend(c, StringToVector("CO"))) { - c.Disconnect("TCP send failed in SendFile, when trying to cancel file transfer because the requested file doesn't exist or couldn't be accessed"); + c.Disconnect("TCP send failed in SendFile, when trying to cancel file transfer because " + res.value()); } - beammp_warn("File " + UnsafeName + " could not be accessed!"); return; } diff --git a/src/TResourceManager.cpp b/src/TResourceManager.cpp index d33cde7..1e29e29 100644 --- a/src/TResourceManager.cpp +++ b/src/TResourceManager.cpp @@ -5,31 +5,80 @@ namespace fs = std::filesystem; +std::string TResourceManager::FormatForBackend(const ModMap& mods) { + std::string monkey; + for (const auto& [name, size] : mods) { + monkey += fs::path(name).filename().string() + ';'; + } + return monkey; +} + +std::string TResourceManager::FormatForClient(const ModMap& mods) { + std::string monkey; + for (const auto& [name, size] : mods) { + monkey += '/' + name + ';'; + } + for (const auto& [name, size] : mods) { + monkey += std::to_string(size) + ';'; + } + return monkey; +} + +/// @brief Sanitizes a requested mod string +/// @param pathString Raw mod path string +/// @param mods List of allowed mods for this client +/// @return Error, if any +std::optional TResourceManager::IsModValid(std::string& pathString, const ModMap& mods) { + auto path = fs::path(pathString); + if (!path.has_filename()) { + beammp_warn("File " + pathString + " is not a file!"); + return { "the requested file doesn't contain a valid filename" }; + } + + auto BasePath = fs::path(Application::GetSettingString(StrResourceFolder) + "/Client"); + + auto CombinedPath = fs::path(BasePath.string() + pathString).lexically_normal(); + + // beammp_infof("path: {}, base: {}, combined: {}", pathString, BasePath.string(), CombinedPath.string()); + + if (!std::filesystem::exists(CombinedPath)) { + beammp_warn("File " + pathString + " could not be accessed!"); + return { "the requested file doesn't exist or couldn't be accessed" }; + } + + auto relative = fs::relative(CombinedPath, BasePath); + + if (mods.count(relative.string()) == 0) { + beammp_warn("File " + pathString + " is disallowed for this player!"); + return { "the requested file is disallowed for this player" }; + } + + pathString = CombinedPath.string(); + return {}; +} + TResourceManager::TResourceManager() { Application::SetSubsystemStatus("ResourceManager", Application::Status::Starting); - std::string Path = Application::GetSettingString(StrResourceFolder) + "/Client"; - if (!fs::exists(Path)) - fs::create_directories(Path); - for (const auto& entry : fs::directory_iterator(Path)) { - std::string File(entry.path().string()); - if (auto pos = File.find(".zip"); pos != std::string::npos) { - if (File.length() - pos == 4) { - std::replace(File.begin(), File.end(), '\\', '/'); - mFileList += File + ';'; - if (auto i = File.find_last_of('/'); i != std::string::npos) { - ++i; - File = File.substr(i, pos - i); - } - mTrimmedList += "/" + fs::path(File).filename().string() + ';'; - mFileSizes += std::to_string(size_t(fs::file_size(entry.path()))) + ';'; - mMaxModSize += size_t(fs::file_size(entry.path())); - mModsLoaded++; - } + std::string BasePath = Application::GetSettingString(StrResourceFolder) + "/Client"; + if (!fs::exists(BasePath)) + fs::create_directories(BasePath); + std::vector modNames; + + auto iterator = fs::recursive_directory_iterator(BasePath, fs::directory_options::follow_directory_symlink | fs::directory_options::skip_permission_denied); + for (const auto& entry : iterator) { + if (iterator.depth() > 0 && !Application::GetSettingBool(StrIncludeSubdirectories)) + continue; + if ((entry.is_regular_file() || entry.is_symlink()) && entry.path().extension() == ".zip") { + auto relativePath = fs::relative(entry.path(), BasePath); + beammp_infof("mod entry: {}", relativePath.string()); + + mMods[relativePath.string()] = entry.file_size(); + mTotalModSize += entry.file_size(); } } - if (mModsLoaded) { - beammp_info("Loaded " + std::to_string(mModsLoaded) + " Mods"); + if (!mMods.empty()) { + beammp_infof("Loaded {} mod{}", mMods.size(), mMods.size() != 1 ? 's' : ' '); } Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);