Skip to content

Commit b7cf304

Browse files
authored
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.
2 parents ea9c808 + 03d3b87 commit b7cf304

6 files changed

Lines changed: 160 additions & 17 deletions

File tree

include/TConsole.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class TConsole {
5959
void Command_Settings(const std::string& cmd, const std::vector<std::string>& args);
6060
void Command_Clear(const std::string&, const std::vector<std::string>& args);
6161
void Command_Version(const std::string& cmd, const std::vector<std::string>& args);
62+
void Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args);
63+
void Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args);
6264

6365
void Command_Say(const std::string& FullCommand);
6466
bool EnsureArgsCount(const std::vector<std::string>& args, size_t n);
@@ -77,6 +79,8 @@ class TConsole {
7779
{ "clear", [this](const auto& a, const auto& b) { Command_Clear(a, b); } },
7880
{ "say", [this](const auto&, const auto&) { Command_Say(""); } }, // shouldn't actually be called
7981
{ "version", [this](const auto& a, const auto& b) { Command_Version(a, b); } },
82+
{ "protectmod", [this](const auto& a, const auto& b) { Command_ProtectMod(a, b); } },
83+
{ "reloadmods", [this](const auto& a, const auto& b) { Command_ReloadMods(a, b); } },
8084
};
8185

8286
std::unique_ptr<Commandline> mCommandline { nullptr };

include/TNetwork.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class TNetwork {
4545
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
4646
void UpdatePlayer(TClient& Client);
4747

48+
TResourceManager& ResourceManager() const { return mResourceManager; }
49+
4850
private:
4951
void UDPServerMain();
5052
void TCPServerMain();

include/TResourceManager.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ class TResourceManager {
3030
[[nodiscard]] std::string TrimmedList() const { return mTrimmedList; }
3131
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
3232
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
33-
34-
[[nodiscard]] std::string NewFileList() const;
33+
[[nodiscard]] nlohmann::json GetMods() const { return mMods; }
3534

3635
void RefreshFiles();
36+
void SetProtected(const std::string& ModName, bool Protected);
3737

3838
private:
3939
size_t mMaxModSize = 0;

src/TConsole.cpp

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,18 @@ void TConsole::Command_Help(const std::string&, const std::vector<std::string>&
208208
}
209209
static constexpr const char* sHelpString = R"(
210210
Commands:
211-
help displays this help
212-
exit shuts down the server
213-
kick <name> [reason] kicks specified player with an optional reason
214-
list lists all players and info about them
215-
say <message> sends the message to all players in chat
216-
lua [state id] switches to lua, optionally into a specific state id's lua
217-
settings [command] sets or gets settings for the server, run `settings help` for more info
218-
status how the server is doing and what it's up to
219-
clear clears the console window
220-
version displays the server version)";
211+
help displays this help
212+
exit shuts down the server
213+
kick <name> [reason] kicks specified player with an optional reason
214+
list lists all players and info about them
215+
say <message> sends the message to all players in chat
216+
lua [state id] switches to lua, optionally into a specific state id's lua
217+
settings [command] sets or gets settings for the server, run `settings help` for more info
218+
status how the server is doing and what it's up to
219+
clear clears the console window
220+
version displays the server version
221+
protectmod <name> <value> sets whether a mod is protected, value can be true or false
222+
reloadmods reloads all mods from the Resources Client folder)";
221223
Application::Console().WriteRaw("BeamMP-Server Console: " + std::string(sHelpString));
222224
}
223225

@@ -262,6 +264,32 @@ void TConsole::Command_Version(const std::string& cmd, const std::vector<std::st
262264
std::string openssl_version = fmt::format("OpenSSL: v{}.{}.{}", OPENSSL_VERSION_MAJOR, OPENSSL_VERSION_MINOR, OPENSSL_VERSION_PATCH);
263265
Application::Console().WriteRaw(openssl_version);
264266
}
267+
void TConsole::Command_ProtectMod(const std::string& cmd, const std::vector<std::string>& args) {
268+
if (!EnsureArgsCount(args, 2)) {
269+
return;
270+
}
271+
272+
const auto& ModName = args.at(0);
273+
const auto& Protect = args.at(1);
274+
275+
for (auto mod : mLuaEngine->Network().ResourceManager().GetMods()) {
276+
if (mod["file_name"].get<std::string>() == ModName) {
277+
mLuaEngine->Network().ResourceManager().SetProtected(ModName, Protect == "true");
278+
Application::Console().WriteRaw("Mod " + ModName + " is now " + (Protect == "true" ? "protected" : "unprotected"));
279+
return;
280+
}
281+
}
282+
283+
Application::Console().WriteRaw("Mod " + ModName + " not found.");
284+
}
285+
void TConsole::Command_ReloadMods(const std::string& cmd, const std::vector<std::string>& args) {
286+
if (!EnsureArgsCount(args, 0)) {
287+
return;
288+
}
289+
290+
mLuaEngine->Network().ResourceManager().RefreshFiles();
291+
Application::Console().WriteRaw("Mods reloaded.");
292+
}
265293

266294
void TConsole::Command_Kick(const std::string&, const std::vector<std::string>& args) {
267295
if (!EnsureArgsCount(args, 1, size_t(-1))) {

src/TNetwork.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ void TNetwork::Parse(TClient& c, const std::vector<uint8_t>& Packet) {
786786
case 'S':
787787
if (SubCode == 'R') {
788788
beammp_debug("Sending Mod Info");
789-
std::string ToSend = mResourceManager.NewFileList();
789+
std::string ToSend = mResourceManager.GetMods().dump();
790790
beammp_debugf("Mod Info: {}", ToSend);
791791
if (!TCPSend(c, StringToVector(ToSend))) {
792792
ClientKick(c, "TCP Send 'SY' failed");
@@ -808,6 +808,15 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
808808
return;
809809
}
810810
auto FileName = fs::path(UnsafeName).filename().string();
811+
812+
for (auto mod : mResourceManager.GetMods()) {
813+
if (mod["file_name"].get<std::string>() == FileName && mod["protected"] == true) {
814+
beammp_warn("Client tried to access protected file " + UnsafeName);
815+
c.Disconnect("Mod is protected thus cannot be downloaded");
816+
return;
817+
}
818+
}
819+
811820
FileName = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/" + FileName;
812821

813822
if (!std::filesystem::exists(FileName)) {

src/TResourceManager.cpp

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,58 @@ TResourceManager::TResourceManager() {
5858
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
5959
}
6060

61-
std::string TResourceManager::NewFileList() const {
62-
return mMods.dump();
63-
}
6461
void TResourceManager::RefreshFiles() {
6562
mMods.clear();
6663
std::unique_lock Lock(mModsMutex);
6764

6865
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
66+
67+
nlohmann::json modsDB;
68+
69+
if (std::filesystem::exists(Path + "/mods.json")) {
70+
try {
71+
std::ifstream stream(Path + "/mods.json");
72+
73+
stream >> modsDB;
74+
75+
stream.close();
76+
} catch (const std::exception& e) {
77+
beammp_errorf("Failed to load mods.json: {}", e.what());
78+
}
79+
}
80+
6981
for (const auto& entry : fs::directory_iterator(Path)) {
7082
std::string File(entry.path().string());
7183

84+
if (entry.path().filename().string() == "mods.json") {
85+
continue;
86+
}
87+
7288
if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
7389
beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
7490
continue;
7591
}
7692

93+
if (modsDB.contains(entry.path().filename().string())) {
94+
auto& dbEntry = modsDB[entry.path().filename().string()];
95+
if (entry.last_write_time().time_since_epoch().count() > dbEntry["lastwrite"] || std::filesystem::file_size(File) != dbEntry["filesize"].get<size_t>()) {
96+
beammp_infof("File '{}' has been modified, rehashing", File);
97+
} else {
98+
dbEntry["exists"] = true;
99+
100+
mMods.push_back(nlohmann::json {
101+
{ "file_name", std::filesystem::path(File).filename() },
102+
{ "file_size", std::filesystem::file_size(File) },
103+
{ "hash_algorithm", "sha256" },
104+
{ "hash", dbEntry["hash"] },
105+
{ "protected", dbEntry["protected"] } });
106+
107+
beammp_debugf("Mod '{}' loaded from cache", File);
108+
109+
continue;
110+
}
111+
}
112+
77113
try {
78114
EVP_MD_CTX* mdctx;
79115
const EVP_MD* md;
@@ -133,9 +169,73 @@ void TResourceManager::RefreshFiles() {
133169
{ "file_size", std::filesystem::file_size(File) },
134170
{ "hash_algorithm", "sha256" },
135171
{ "hash", result },
136-
});
172+
{ "protected", false } });
173+
174+
modsDB[std::filesystem::path(File).filename().string()] = {
175+
{ "lastwrite", entry.last_write_time().time_since_epoch().count() },
176+
{ "hash", result },
177+
{ "filesize", std::filesystem::file_size(File) },
178+
{ "protected", false },
179+
{ "exists", true }
180+
};
181+
137182
} catch (const std::exception& e) {
138183
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what());
139184
}
140185
}
186+
187+
for (auto it = modsDB.begin(); it != modsDB.end();) {
188+
if (!it.value().contains("exists")) {
189+
it = modsDB.erase(it);
190+
} else {
191+
it.value().erase("exists");
192+
++it;
193+
}
194+
}
195+
196+
try {
197+
std::ofstream stream(Path + "/mods.json");
198+
199+
stream << modsDB.dump(4);
200+
201+
stream.close();
202+
} catch (std::exception& e) {
203+
beammp_error("Failed to update mod DB: " + std::string(e.what()));
204+
}
205+
}
206+
207+
void TResourceManager::SetProtected(const std::string& ModName, bool Protected) {
208+
std::unique_lock Lock(mModsMutex);
209+
210+
for (auto& mod : mMods) {
211+
if (mod["file_name"].get<std::string>() == ModName) {
212+
mod["protected"] = Protected;
213+
break;
214+
}
215+
}
216+
217+
auto modsDBPath = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client/mods.json";
218+
219+
if (std::filesystem::exists(modsDBPath)) {
220+
try {
221+
nlohmann::json modsDB;
222+
223+
std::fstream stream(modsDBPath);
224+
225+
stream >> modsDB;
226+
227+
if (modsDB.contains(ModName)) {
228+
modsDB[ModName]["protected"] = Protected;
229+
}
230+
231+
stream.clear();
232+
stream.seekp(0, std::ios::beg);
233+
234+
stream << modsDB.dump(4);
235+
236+
stream.close();
237+
} catch (const std::exception& e) {
238+
beammp_errorf("Failed to update mods.json: {}", e.what());
239+
}
240+
}
141241
}

0 commit comments

Comments
 (0)