mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2025-07-01 15:26:59 +00:00
Client resource hash database and client resource protection (#430)
# Mod database This PR adds a local database of mods, which is used to cache mod hashes and protection status. ## Mod hash caching Mod hashes will now be cached based on last write date. This will speed up server startup because only the mods with changes will have to be hashed. ## Mod protection You can now protect mods! This will allow you to host a server with copyrighted content without actually hosting the copyrighted content. Just run `protectmod <filename with .zip> <true/false>` in the console to protect a mod. Users that join a server with protected mods will have to obtain the file themselves and put it in their launcher's resources folder. The launcher will inform the user about this if the file is missing. ## Mod reloading You can now reload client mods while the server is running by using `reloadmods` in the console. Keep in mind that this is mainly intended for development, therefore it will **not** force client to rejoin and neither will is hot-reload mods on the client. --- By creating this pull request, I understand that code that is AI generated or otherwise automatically generated may be rejected without further discussion. I declare that I fully understand all code I pushed into this PR, and wrote all this code myself and own the rights to this code.
This commit is contained in:
commit
b7cf304d49
@ -59,6 +59,8 @@ private:
|
|||||||
void Command_Settings(const std::string& cmd, const std::vector<std::string>& args);
|
void Command_Settings(const std::string& cmd, const std::vector<std::string>& args);
|
||||||
void Command_Clear(const std::string&, const std::vector<std::string>& args);
|
void Command_Clear(const std::string&, const std::vector<std::string>& args);
|
||||||
void Command_Version(const std::string& cmd, const std::vector<std::string>& args);
|
void Command_Version(const std::string& cmd, const std::vector<std::string>& args);
|
||||||
|
void Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args);
|
||||||
|
void Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args);
|
||||||
|
|
||||||
void Command_Say(const std::string& FullCommand);
|
void Command_Say(const std::string& FullCommand);
|
||||||
bool EnsureArgsCount(const std::vector<std::string>& args, size_t n);
|
bool EnsureArgsCount(const std::vector<std::string>& args, size_t n);
|
||||||
@ -77,6 +79,8 @@ private:
|
|||||||
{ "clear", [this](const auto& a, const auto& b) { Command_Clear(a, b); } },
|
{ "clear", [this](const auto& a, const auto& b) { Command_Clear(a, b); } },
|
||||||
{ "say", [this](const auto&, const auto&) { Command_Say(""); } }, // shouldn't actually be called
|
{ "say", [this](const auto&, const auto&) { Command_Say(""); } }, // shouldn't actually be called
|
||||||
{ "version", [this](const auto& a, const auto& b) { Command_Version(a, b); } },
|
{ "version", [this](const auto& a, const auto& b) { Command_Version(a, b); } },
|
||||||
|
{ "protectmod", [this](const auto& a, const auto& b) { Command_ProtectMod(a, b); } },
|
||||||
|
{ "reloadmods", [this](const auto& a, const auto& b) { Command_ReloadMods(a, b); } },
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<Commandline> mCommandline { nullptr };
|
std::unique_ptr<Commandline> mCommandline { nullptr };
|
||||||
|
@ -45,6 +45,8 @@ public:
|
|||||||
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
|
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
|
||||||
void UpdatePlayer(TClient& Client);
|
void UpdatePlayer(TClient& Client);
|
||||||
|
|
||||||
|
TResourceManager& ResourceManager() const { return mResourceManager; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void UDPServerMain();
|
void UDPServerMain();
|
||||||
void TCPServerMain();
|
void TCPServerMain();
|
||||||
|
@ -30,10 +30,10 @@ public:
|
|||||||
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
|
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
|
||||||
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
|
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
|
||||||
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
|
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
|
||||||
|
[[nodiscard]] nlohmann::json GetMods() const { return mMods; }
|
||||||
[[nodiscard]] std::string NewFileList() const;
|
|
||||||
|
|
||||||
void RefreshFiles();
|
void RefreshFiles();
|
||||||
|
void SetProtected(const std::string& ModName, bool Protected);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
size_t mMaxModSize = 0;
|
size_t mMaxModSize = 0;
|
||||||
|
@ -217,7 +217,9 @@ void TConsole::Command_Help(const std::string&, const std::vector<std::string>&
|
|||||||
settings [command] sets or gets settings for the server, run `settings help` for more info
|
settings [command] sets or gets settings for the server, run `settings help` for more info
|
||||||
status how the server is doing and what it's up to
|
status how the server is doing and what it's up to
|
||||||
clear clears the console window
|
clear clears the console window
|
||||||
version displays the server version)";
|
version displays the server version
|
||||||
|
protectmod <name> <value> sets whether a mod is protected, value can be true or false
|
||||||
|
reloadmods reloads all mods from the Resources Client folder)";
|
||||||
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
|
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +264,32 @@ void TConsole::Command_Version(const std::string& cmd, const std::vector<std::st
|
|||||||
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
|
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
|
||||||
Application::Console().WriteRaw(openssl_version);
|
Application::Console().WriteRaw(openssl_version);
|
||||||
}
|
}
|
||||||
|
void TConsole::Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args) {
|
||||||
|
if (!EnsureArgsCount(args, 2)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& ModName = args.at(0);
|
||||||
|
const auto& Protect = args.at(1);
|
||||||
|
|
||||||
|
for (auto mod : mLuaEngine->Network().ResourceManager().GetMods()) {
|
||||||
|
if (mod["file_name"].get<std::string>() == ModName) {
|
||||||
|
mLuaEngine->Network().ResourceManager().SetProtected(ModName, Protect == "true");
|
||||||
|
Application::Console().WriteRaw("Mod " + ModName + " is now " + (Protect == "true" ? "protected" : "unprotected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Application::Console().WriteRaw("Mod " + ModName + " not found.");
|
||||||
|
}
|
||||||
|
void TConsole::Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args) {
|
||||||
|
if (!EnsureArgsCount(args, 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mLuaEngine->Network().ResourceManager().RefreshFiles();
|
||||||
|
Application::Console().WriteRaw("Mods reloaded.");
|
||||||
|
}
|
||||||
|
|
||||||
void TConsole::Command_Kick(const std::string&, const std::vector<std::string>& args) {
|
void TConsole::Command_Kick(const std::string&, const std::vector<std::string>& args) {
|
||||||
if (!EnsureArgsCount(args, 1, size_t(-1))) {
|
if (!EnsureArgsCount(args, 1, size_t(-1))) {
|
||||||
|
@ -786,7 +786,7 @@ void TNetwork::Parse(TClient& c, const std::vector<uint8_t>& Packet) {
|
|||||||
case 'S':
|
case 'S':
|
||||||
if (SubCode == 'R') {
|
if (SubCode == 'R') {
|
||||||
beammp_debug("Sending Mod Info");
|
beammp_debug("Sending Mod Info");
|
||||||
std::string ToSend = mResourceManager.NewFileList();
|
std::string ToSend = mResourceManager.GetMods().dump();
|
||||||
beammp_debugf("Mod Info: {}", ToSend);
|
beammp_debugf("Mod Info: {}", ToSend);
|
||||||
if (!TCPSend(c, StringToVector(ToSend))) {
|
if (!TCPSend(c, StringToVector(ToSend))) {
|
||||||
ClientKick(c, "TCP Send 'SY' failed");
|
ClientKick(c, "TCP Send 'SY' failed");
|
||||||
@ -808,6 +808,15 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto FileName = fs::path(UnsafeName).filename().string();
|
auto FileName = fs::path(UnsafeName).filename().string();
|
||||||
|
|
||||||
|
for (auto mod : mResourceManager.GetMods()) {
|
||||||
|
if (mod["file_name"].get<std::string>() == FileName && mod["protected"] == true) {
|
||||||
|
beammp_warn("Client tried to access protected file " + UnsafeName);
|
||||||
|
c.Disconnect("Mod is protected thus cannot be downloaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FileName = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/" + FileName;
|
FileName = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/" + FileName;
|
||||||
|
|
||||||
if (!std::filesystem::exists(FileName)) {
|
if (!std::filesystem::exists(FileName)) {
|
||||||
|
@ -58,22 +58,58 @@ TResourceManager::TResourceManager() {
|
|||||||
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
|
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TResourceManager::NewFileList() const {
|
|
||||||
return mMods.dump();
|
|
||||||
}
|
|
||||||
void TResourceManager::RefreshFiles() {
|
void TResourceManager::RefreshFiles() {
|
||||||
mMods.clear();
|
mMods.clear();
|
||||||
std::unique_lock Lock(mModsMutex);
|
std::unique_lock Lock(mModsMutex);
|
||||||
|
|
||||||
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
|
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
|
||||||
|
|
||||||
|
nlohmann::json modsDB;
|
||||||
|
|
||||||
|
if (std::filesystem::exists(Path + "/mods.json")) {
|
||||||
|
try {
|
||||||
|
std::ifstream stream(Path + "/mods.json");
|
||||||
|
|
||||||
|
stream >> modsDB;
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
beammp_errorf("Failed to load mods.json: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& entry : fs::directory_iterator(Path)) {
|
for (const auto& entry : fs::directory_iterator(Path)) {
|
||||||
std::string File(entry.path().string());
|
std::string File(entry.path().string());
|
||||||
|
|
||||||
|
if (entry.path().filename().string() == "mods.json") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
|
if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
|
||||||
beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
|
beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modsDB.contains(entry.path().filename().string())) {
|
||||||
|
auto& dbEntry = modsDB[entry.path().filename().string()];
|
||||||
|
if (entry.last_write_time().time_since_epoch().count() > dbEntry["lastwrite"] || std::filesystem::file_size(File) != dbEntry["filesize"].get<size_t>()) {
|
||||||
|
beammp_infof("File '{}' has been modified, rehashing", File);
|
||||||
|
} else {
|
||||||
|
dbEntry["exists"] = true;
|
||||||
|
|
||||||
|
mMods.push_back(nlohmann::json {
|
||||||
|
{ "file_name", std::filesystem::path(File).filename() },
|
||||||
|
{ "file_size", std::filesystem::file_size(File) },
|
||||||
|
{ "hash_algorithm", "sha256" },
|
||||||
|
{ "hash", dbEntry["hash"] },
|
||||||
|
{ "protected", dbEntry["protected"] } });
|
||||||
|
|
||||||
|
beammp_debugf("Mod '{}' loaded from cache", File);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EVP_MD_CTX* mdctx;
|
EVP_MD_CTX* mdctx;
|
||||||
const EVP_MD* md;
|
const EVP_MD* md;
|
||||||
@ -133,9 +169,73 @@ void TResourceManager::RefreshFiles() {
|
|||||||
{ "file_size", std::filesystem::file_size(File) },
|
{ "file_size", std::filesystem::file_size(File) },
|
||||||
{ "hash_algorithm", "sha256" },
|
{ "hash_algorithm", "sha256" },
|
||||||
{ "hash", result },
|
{ "hash", result },
|
||||||
});
|
{ "protected", false } });
|
||||||
|
|
||||||
|
modsDB[std::filesystem::path(File).filename().string()] = {
|
||||||
|
{ "lastwrite", entry.last_write_time().time_since_epoch().count() },
|
||||||
|
{ "hash", result },
|
||||||
|
{ "filesize", std::filesystem::file_size(File) },
|
||||||
|
{ "protected", false },
|
||||||
|
{ "exists", true }
|
||||||
|
};
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what());
|
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto it = modsDB.begin(); it != modsDB.end();) {
|
||||||
|
if (!it.value().contains("exists")) {
|
||||||
|
it = modsDB.erase(it);
|
||||||
|
} else {
|
||||||
|
it.value().erase("exists");
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ofstream stream(Path + "/mods.json");
|
||||||
|
|
||||||
|
stream << modsDB.dump(4);
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
beammp_error("Failed to update mod DB: " + std::string(e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TResourceManager::SetProtected(const std::string& ModName, bool Protected) {
|
||||||
|
std::unique_lock Lock(mModsMutex);
|
||||||
|
|
||||||
|
for (auto& mod : mMods) {
|
||||||
|
if (mod["file_name"].get<std::string>() == ModName) {
|
||||||
|
mod["protected"] = Protected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto modsDBPath = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/mods.json";
|
||||||
|
|
||||||
|
if (std::filesystem::exists(modsDBPath)) {
|
||||||
|
try {
|
||||||
|
nlohmann::json modsDB;
|
||||||
|
|
||||||
|
std::fstream stream(modsDBPath);
|
||||||
|
|
||||||
|
stream >> modsDB;
|
||||||
|
|
||||||
|
if (modsDB.contains(ModName)) {
|
||||||
|
modsDB[ModName]["protected"] = Protected;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.clear();
|
||||||
|
stream.seekp(0, std::ios::beg);
|
||||||
|
|
||||||
|
stream << modsDB.dump(4);
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
beammp_errorf("Failed to update mods.json: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user