Compare commits

...

36 Commits

Author SHA1 Message Date
Lion Kortlepel
e3416804e4 bump version 2024-11-01 12:50:11 +01:00
Lion
9ad4f61209 Information packet (#382)
This PR adds an "I" packet which returns the server information. This
can be used by external programs and the launcher to get information
about a server without having to connect it to. It can be toggled in the
config and in lua.
2024-11-01 12:27:31 +01:00
Tixx
aed6311146 Run clang-format on THeartbeatThread.cpp 2024-11-01 12:22:56 +01:00
Lion
5e41cefa87 Paint packet (#381)
Adds a packet for live color updating `vid:[{}]`
2024-11-01 12:18:06 +01:00
Tixx
b1710ee826 Add explanation for why uuid is added later 2024-10-20 18:33:49 +02:00
Lion
03b0bd4d2c Add 'P' packet on UDP (#379)
This can be used to check if UDP works :)

I've tested this a bit with snep ingame, even spamming this on localhost
with a large number of connections seems to not impact gameplay at all.
2024-10-20 18:29:12 +02:00
Tixx
5179ac1fdc Paint packet 2024-10-20 15:38:07 +02:00
Tixx
54e31ce2ec Move backend heartbeat to json 2024-10-14 00:42:14 +02:00
Tixx
4abe9b8636 Add informationpacket setting to the config 2024-10-14 00:39:12 +02:00
Tixx
956d6f50e1 Add setting for the information packet 2024-10-14 00:39:12 +02:00
Tixx
6aeb2eb736 Add server identification packet 2024-10-14 00:39:12 +02:00
Lion Kortlepel
f40d4c1ddd add 'P' packet on UDP
this can be used to check if UDP works :)
2024-10-13 22:13:05 +02:00
Lion
4f052a3e0a Make modlist an empty array by default instead of null (#377)
Related launcher PR: https://github.com/BeamMP/BeamMP-Launcher/pull/136
2024-10-12 21:06:41 +02:00
Tixx
2bf9e7d916 Make modlist [] by default instead of null 2024-10-09 19:42:47 +02:00
Lion
89c906232d Report correct client minimum version to the backend (#376) 2024-10-09 18:06:03 +02:00
Lion
c39beb5b72 reuse minclientversion where possible
Co-authored-by: Tixx <83774803+WiserTixx@users.noreply.github.com>
2024-10-09 18:03:01 +02:00
Lion Kortlepel
7dd2d89ad9 clarify auth version reject message 2024-10-09 16:48:40 +02:00
Lion Kortlepel
3403c8acba fix version check on auth 2024-10-09 16:44:38 +02:00
Lion Kortlepel
0a6eecee69 report correct client minimum version to the backend 2024-10-09 16:37:16 +02:00
Lion
cf3985ce00 Add Lua function to get a player's role (#366)
Adds `MP.GetPlayerRole(player_id)` to the Lua API to get a player's role
("USER", "EA", "MDEV", "STAFF", "ET") by their player id.
Currently you can only get someone's role in onPlayerAuth from the
parameters and in onVehicleSpawned and onVehicleEdited from the packet
data, but not in onPlayerJoin for example without storing it.
2024-10-05 16:09:37 +02:00
Lion Kortlepel
b04c5068ea bump version 2024-10-05 16:09:02 +02:00
Lion
077bb6b1cd Add player limit bypass to onPlayerAuth (#372)
With this PR, returning 2 in onPlayerAuth will allow the player to join
without checking if the server is full. This makes it easier for plugin
developers to allow for example their staff to join without having to
change the max player count.
2024-10-05 16:07:53 +02:00
Lion
0850cde1fb Add MP.SendNotification (#373)
Adds MP.SendNotification(message, icon, category (optional) ) to the Lua
api. Uses the newly added "N" packet in the mod.
2024-10-05 16:07:27 +02:00
Lion
611e53b484 Mod hashing + better download (#374) 2024-10-04 23:29:11 +02:00
Tixx
f039f57f11 Fix error messages on sendnotification 2024-10-04 20:24:30 +02:00
Lion Kortlepel
5d34090952 fix stupid read size error leading to corrupt zip 2024-09-29 01:34:38 +02:00
Lion Kortlepel
88ca17236a remove two-socket download 2024-09-29 01:15:48 +02:00
Lion Kortlepel
a4b62d013c implement mod hashing + new download 2024-09-29 00:32:52 +02:00
Tixx
9a0270cb09 Return nil instead of "" when there's no client 2024-09-28 21:05:04 +02:00
Lion
55f1a3c734 Add MP.Get (#369)
Adds `MP.Get(ConfigID)` to the lua api to get the current server
settings.

```lua
lua> print(MP.Get(MP.Settings.Name]))
[LUA] BeamMP Server
lua> MP.Set(MP.Settings.Name, 'Hello World')
[INFO] Set `Name` to Hello World
lua> print(MP.Get(MP.Settings.Name))
[LUA] Hello World
```

Closes #146
2024-09-28 20:30:14 +02:00
Tixx
bb3c762d68 Add player limit bypass to onPlayerAuth 2024-09-28 14:52:04 +02:00
Tixx
3ade7f5743 Add MP.SendNotification 2024-09-28 13:35:25 +02:00
Tixx
9d44c2063c Remove break after return 2024-09-22 15:34:13 +02:00
Tixx
17185da53b Add MP.Get 2024-09-21 23:17:08 +02:00
Tixx
623dfa17d5 Remove expiry check and add braces 2024-09-20 14:45:41 +02:00
Tixx
caafb216c9 Add MP.GetPlayerRole(player_id) 2024-09-19 07:51:07 +02:00
18 changed files with 311 additions and 178 deletions

View File

@@ -66,7 +66,6 @@ public:
std::string GetCarData(int Ident);
std::string GetCarPositionRaw(int Ident);
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
void SetDownSock(ip::tcp::socket&& CSock) { mDownSocket = std::move(CSock); }
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }
void Disconnect(std::string_view Reason);
bool IsDisconnected() const { return !mSocket.is_open(); }
@@ -75,8 +74,6 @@ public:
[[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; }
[[nodiscard]] const ip::udp::endpoint& GetUDPAddr() const { return mUDPAddress; }
[[nodiscard]] ip::udp::endpoint& GetUDPAddr() { return mUDPAddress; }
[[nodiscard]] ip::tcp::socket& GetDownSock() { return mDownSocket; }
[[nodiscard]] const ip::tcp::socket& GetDownSock() const { return mDownSocket; }
[[nodiscard]] ip::tcp::socket& GetTCPSock() { return mSocket; }
[[nodiscard]] const ip::tcp::socket& GetTCPSock() const { return mSocket; }
[[nodiscard]] std::string GetRoles() const { return mRole; }
@@ -122,7 +119,6 @@ private:
SparseArray<std::string> mVehiclePosition;
std::string mName = "Unknown Client";
ip::tcp::socket mSocket;
ip::tcp::socket mDownSocket;
ip::udp::endpoint mUDPAddress {};
int mUnicycleID = -1;
std::string mRole;

View File

@@ -74,7 +74,7 @@ public:
static TConsole& Console() { return mConsole; }
static std::string ServerVersionString();
static const Version& ServerVersion() { return mVersion; }
static uint8_t ClientMajorVersion() { return 2; }
static Version ClientMinimumVersion() { return Version { 2, 2, 0 }; }
static std::string PPS() { return mPPS; }
static void SetPPS(const std::string& NewPPS) { mPPS = NewPPS; }
@@ -127,7 +127,7 @@ private:
static inline std::mutex mShutdownHandlersMutex {};
static inline std::deque<TShutdownHandler> mShutdownHandlers {};
static inline Version mVersion { 3, 5, 1 };
static inline Version mVersion { 3, 7, 0 };
};
void SplitString(std::string const& str, const char delim, std::vector<std::string>& out);
@@ -162,13 +162,13 @@ void RegisterThread(const std::string& str);
#else
#define _function_name std::string(__func__)
#endif
#ifndef NDEBUG
#define DEBUG
#endif
#if defined(DEBUG)
// if this is defined, we will show the full function signature infront of
// each info/debug/warn... call instead of the 'filename:line' format.
#if defined(BMP_FULL_FUNCTION_NAMES)
@@ -178,7 +178,7 @@ void RegisterThread(const std::string& str);
#endif
#endif // defined(DEBUG)
#define beammp_warn(x) Application::Console().Write(_this_location + std::string("[WARN] ") + (x))
#define beammp_info(x) Application::Console().Write(_this_location + std::string("[INFO] ") + (x))
#define beammp_error(x) \
@@ -221,7 +221,7 @@ void RegisterThread(const std::string& str);
#else
#define beammp_trace(x)
#endif // defined(DEBUG)
#define beammp_errorf(...) beammp_error(fmt::format(__VA_ARGS__))
#define beammp_infof(...) beammp_info(fmt::format(__VA_ARGS__))
#define beammp_debugf(...) beammp_debug(fmt::format(__VA_ARGS__))

View File

@@ -35,8 +35,10 @@ namespace MP {
inline size_t GetPlayerCount() { return Engine->Server().ClientCount(); }
std::pair<bool, std::string> DropPlayer(int ID, std::optional<std::string> MaybeReason);
std::pair<bool, std::string> SendChatMessage(int ID, const std::string& Message);
std::pair<bool, std::string> SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category);
std::pair<bool, std::string> RemoveVehicle(int PlayerID, int VehicleID);
void Set(int ConfigID, sol::object NewValue);
TLuaValue Get(int ConfigID);
bool IsPlayerGuest(int ID);
bool IsPlayerConnected(int ID);
void Sleep(size_t Ms);

View File

@@ -86,7 +86,8 @@ struct Settings {
General_LogChat,
General_ResourceFolder,
General_Debug,
General_AllowGuests
General_AllowGuests,
General_InformationPacket,
};
Sync<std::unordered_map<Key, SettingsTypeVariant>> SettingsMap;

View File

@@ -29,6 +29,7 @@ public:
//~THeartbeatThread();
void operator()() override;
static inline std::string lastCall = "";
private:
std::string GenerateCall();
std::string GetPlayers();

View File

@@ -263,6 +263,7 @@ private:
sol::table Lua_TriggerGlobalEvent(const std::string& EventName, sol::variadic_args EventArgs);
sol::table Lua_TriggerLocalEvent(const std::string& EventName, sol::variadic_args EventArgs);
sol::table Lua_GetPlayerIdentifiers(int ID);
std::variant<std::string, sol::nil_t> Lua_GetPlayerRole(int ID);
sol::table Lua_GetPlayers();
std::string Lua_GetPlayerName(int ID);
sol::table Lua_GetPlayerVehicles(int ID);

View File

@@ -58,7 +58,6 @@ private:
std::mutex mOpenIDMutex;
std::vector<uint8_t> UDPRcvFromClient(ip::udp::endpoint& ClientEndpoint);
void HandleDownload(TConnection&& TCPSock);
void OnConnect(const std::weak_ptr<TClient>& c);
void TCPClient(const std::weak_ptr<TClient>& c);
void Looper(const std::weak_ptr<TClient>& c);
@@ -67,7 +66,7 @@ private:
void Parse(TClient& c, const std::vector<uint8_t>& 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);
static void SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std::string& Name);
static void SendFileToClient(TClient& c, size_t Size, const std::string& Name);
static const uint8_t* SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size);
};

View File

@@ -19,6 +19,7 @@
#pragma once
#include "Common.h"
#include <nlohmann/json.hpp>
class TResourceManager {
public:
@@ -30,10 +31,17 @@ public:
[[nodiscard]] std::string FileSizes() const { return mFileSizes; }
[[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
[[nodiscard]] std::string NewFileList() const;
void RefreshFiles();
private:
size_t mMaxModSize = 0;
std::string mFileSizes;
std::string mFileList;
std::string mTrimmedList;
int mModsLoaded = 0;
std::mutex mModsMutex;
nlohmann::json mMods = nlohmann::json::array();
};

View File

@@ -144,7 +144,6 @@ void TClient::EnqueuePacket(const std::vector<uint8_t>& Packet) {
TClient::TClient(TServer& Server, ip::tcp::socket&& Socket)
: mServer(Server)
, mSocket(std::move(Socket))
, mDownSocket(ip::tcp::socket(Server.IoCtx()))
, mLastPingTime(std::chrono::high_resolution_clock::now()) {
}

View File

@@ -205,6 +205,37 @@ std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::stri
return Result;
}
std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::string& Message, const std::string& Icon, const std::string& Category) {
std::pair<bool, std::string> Result;
std::string Packet = "N" + Category + ":" + Icon + ":" + Message;
if (ID == -1) {
Engine->Network().SendToAll(nullptr, StringToVector(Packet), true, true);
Result.first = true;
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send notification to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("SendNotification invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
return Result;
}
return Result;
}
std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
std::pair<bool, std::string> Result;
auto MaybeClient = GetClient(Engine->Server(), PID);
@@ -286,12 +317,44 @@ void LuaAPI::MP::Set(int ConfigID, sol::object NewValue) {
beammp_lua_error("set invalid argument [2] expected string");
}
break;
case 7: // Information packet
if (NewValue.is<bool>()) {
Application::Settings.set(Settings::Key::General_InformationPacket, NewValue.as<bool>());
beammp_info(std::string("Set `InformationPacket` to ") + (Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? "true" : "false"));
} else {
beammp_lua_error("set invalid argument [2] expected boolean");
}
break;
default:
beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this.");
break;
}
}
TLuaValue LuaAPI::MP::Get(int ConfigID) {
switch (ConfigID) {
case 0: // debug
return Application::Settings.getAsBool(Settings::Key::General_Debug);
case 1: // private
return Application::Settings.getAsBool(Settings::Key::General_Private);
case 2: // max cars
return Application::Settings.getAsInt(Settings::Key::General_MaxCars);
case 3: // max players
return Application::Settings.getAsInt(Settings::Key::General_MaxPlayers);
case 4: // Map
return Application::Settings.getAsString(Settings::Key::General_Map);
case 5: // Name
return Application::Settings.getAsString(Settings::Key::General_Name);
case 6: // Desc
return Application::Settings.getAsString(Settings::Key::General_Description);
case 7: // Information packet
return Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
default:
beammp_warn("Invalid config ID \"" + std::to_string(ConfigID) + "\". Use `MP.Settings.*` enum for this.");
return 0;
}
}
void LuaAPI::MP::Sleep(size_t Ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(Ms));
}

View File

@@ -34,6 +34,7 @@ Settings::Settings() {
{ General_ResourceFolder, std::string("Resources") },
{ General_Debug, false },
{ General_AllowGuests, true },
{ General_InformationPacket, true },
{ Misc_SendErrorsShowMessage, true },
{ Misc_SendErrors, true },
{ Misc_ImScaredOfUpdates, true },
@@ -54,6 +55,7 @@ Settings::Settings() {
{ { "General", "ResourceFolder" }, { General_ResourceFolder, READ_ONLY } },
{ { "General", "Debug" }, { General_Debug, READ_WRITE } },
{ { "General", "AllowGuests" }, { General_AllowGuests, READ_WRITE } },
{ { "General", "InformationPacket" }, { General_InformationPacket, READ_WRITE } },
{ { "Misc", "SendErrorsShowMessage" }, { Misc_SendErrorsShowMessage, READ_WRITE } },
{ { "Misc", "SendErrors" }, { Misc_SendErrors, READ_WRITE } },
{ { "Misc", "ImScaredOfUpdates" }, { Misc_ImScaredOfUpdates, READ_WRITE } },

View File

@@ -56,6 +56,8 @@ static constexpr std::string_view StrLogChat = "LogChat";
static constexpr std::string_view EnvStrLogChat = "BEAMMP_LOG_CHAT";
static constexpr std::string_view StrAllowGuests = "AllowGuests";
static constexpr std::string_view EnvStrAllowGuests = "BEAMMP_ALLOW_GUESTS";
static constexpr std::string_view StrInformationPacket = "InformationPacket";
static constexpr std::string_view EnvStrInformationPacket = "BEAMMP_INFORMATION_PACKET";
static constexpr std::string_view StrPassword = "Password";
// Misc
@@ -132,6 +134,8 @@ void TConfig::FlushToFile() {
SetComment(data["General"][StrLogChat.data()].comments(), " Whether to log chat messages in the console / log");
data["General"][StrDebug.data()] = Application::Settings.getAsBool(Settings::Key::General_Debug);
data["General"][StrPrivate.data()] = Application::Settings.getAsBool(Settings::Key::General_Private);
SetComment(data["General"][StrInformationPacket.data()].comments(), " Whether to allow unconnected clients to get the public server information without joining");
data["General"][StrInformationPacket.data()] = Application::Settings.getAsBool(Settings::Key::General_InformationPacket);
data["General"][StrAllowGuests.data()] = Application::Settings.getAsBool(Settings::Key::General_AllowGuests);
SetComment(data["General"][StrAllowGuests.data()].comments(), " Whether to allow guests");
data["General"][StrPort.data()] = Application::Settings.getAsInt(Settings::Key::General_Port);
@@ -248,6 +252,7 @@ void TConfig::ParseFromFile(std::string_view name) {
// Read into new Settings Singleton
TryReadValue(data, "General", StrDebug, EnvStrDebug, Settings::Key::General_Debug);
TryReadValue(data, "General", StrPrivate, EnvStrPrivate, Settings::Key::General_Private);
TryReadValue(data, "General", StrInformationPacket, EnvStrInformationPacket, Settings::Key::General_InformationPacket);
TryReadValue(data, "General", StrPort, EnvStrPort, Settings::Key::General_Port);
TryReadValue(data, "General", StrMaxCars, EnvStrMaxCars, Settings::Key::General_MaxCars);
TryReadValue(data, "General", StrMaxPlayers, EnvStrMaxPlayers, Settings::Key::General_MaxPlayers);
@@ -299,6 +304,7 @@ void TConfig::PrintDebug() {
}
beammp_debug(std::string(StrDebug) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Debug) ? "true" : "false"));
beammp_debug(std::string(StrPrivate) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_Private) ? "true" : "false"));
beammp_debug(std::string(StrInformationPacket) + ": " + std::string(Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? "true" : "false"));
beammp_debug(std::string(StrPort) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)));
beammp_debug(std::string(StrMaxCars) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxCars)));
beammp_debug(std::string(StrMaxPlayers) + ": " + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)));

View File

@@ -20,8 +20,10 @@
#include "ChronoWrapper.h"
#include "Client.h"
#include "Common.h"
#include "Http.h"
// #include "SocketIO.h"
#include <nlohmann/json.hpp>
#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>
#include <sstream>
@@ -64,7 +66,7 @@ void THeartbeatThread::operator()() {
json::Document Doc;
bool Ok = false;
for (const auto& Url : Application::GetBackendUrlsInOrder()) {
T = Http::POST(Url, 443, Target, Body, "application/x-www-form-urlencoded", &ResponseCode, { { "api-v", "2" } });
T = Http::POST(Url, 443, Target, Body, "application/json", &ResponseCode, { { "api-v", "2" } });
Doc.Parse(T.data(), T.size());
if (Doc.HasParseError() || !Doc.IsObject()) {
if (!Application::Settings.getAsBool(Settings::Key::General_Private)) {
@@ -137,25 +139,30 @@ void THeartbeatThread::operator()() {
}
std::string THeartbeatThread::GenerateCall() {
std::stringstream Ret;
nlohmann::json Ret = {
{ "players", mServer.ClientCount() },
{ "maxplayers", Application::Settings.getAsInt(Settings::Key::General_MaxPlayers) },
{ "port", Application::Settings.getAsInt(Settings::Key::General_Port) },
{ "map", Application::Settings.getAsString(Settings::Key::General_Map) },
{ "private", Application::Settings.getAsBool(Settings::Key::General_Private) },
{ "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) },
{ "modlist", mResourceManager.TrimmedList() },
{ "modstotalsize", mResourceManager.MaxModSize() },
{ "modstotal", mResourceManager.ModsLoaded() },
{ "playerslist", GetPlayers() },
{ "desc", Application::Settings.getAsString(Settings::Key::General_Description) }
};
Ret << "uuid=" << Application::Settings.getAsString(Settings::Key::General_AuthKey)
<< "&players=" << mServer.ClientCount()
<< "&maxplayers=" << Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)
<< "&port=" << 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=" << std::to_string(Application::ClientMajorVersion()) + ".0" // FIXME: Wtf.
<< "&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=" << mResourceManager.MaxModSize()
<< "&modstotal=" << mResourceManager.ModsLoaded()
<< "&playerslist=" << GetPlayers()
<< "&desc=" << Application::Settings.getAsString(Settings::Key::General_Description);
return Ret.str();
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)

View File

@@ -595,6 +595,16 @@ sol::table TLuaEngine::StateThreadData::Lua_GetPlayerIdentifiers(int ID) {
}
}
std::variant<std::string, sol::nil_t> TLuaEngine::StateThreadData::Lua_GetPlayerRole(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient) {
return MaybeClient.value().lock()->GetRoles();
} else {
return sol::nil;
}
}
sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
sol::table Result = mStateView.create_table();
mEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
@@ -869,6 +879,15 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
return Lua_GetPositionRaw(PID, VID);
});
MPTable.set_function("SendChatMessage", &LuaAPI::MP::SendChatMessage);
MPTable.set_function("SendNotification", [&](sol::variadic_args Args) {
if (Args.size() == 3) {
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), Args.get<std::string>(2), Args.get<std::string>(1));
} else if (Args.size() == 4) {
LuaAPI::MP::SendNotification(Args.get<int>(0), Args.get<std::string>(1), Args.get<std::string>(2), Args.get<std::string>(3));
} else {
beammp_lua_error("SendNotification expects 2 or 3 arguments.");
}
});
MPTable.set_function("GetPlayers", [&]() -> sol::table {
return Lua_GetPlayers();
});
@@ -883,6 +902,9 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
MPTable.set_function("GetPlayerIdentifiers", [&](int ID) -> sol::table {
return Lua_GetPlayerIdentifiers(ID);
});
MPTable.set_function("GetPlayerRole", [&](int ID) -> std::variant<std::string, sol::nil_t> {
return Lua_GetPlayerRole(ID);
});
MPTable.set_function("Sleep", &LuaAPI::MP::Sleep);
// const std::string& EventName, size_t IntervalMS, int strategy
MPTable.set_function("CreateEventTimer", [&](sol::variadic_args Args) {
@@ -910,6 +932,7 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
mEngine->CancelEventTimers(EventName, mStateId);
});
MPTable.set_function("Set", &LuaAPI::MP::Set);
MPTable.set_function("Get", &LuaAPI::MP::Get);
auto UtilTable = StateView.create_named_table("Util");
UtilTable.set_function("LogDebug", [this](sol::variadic_args args) {
@@ -1001,7 +1024,8 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
"MaxPlayers", 3,
"Map", 4,
"Name", 5,
"Description", 6);
"Description", 6,
"InformationPacket", 7);
MPTable.create_named("CallStrategy",
"BestEffort", CallStrategy::BestEffort,

View File

@@ -20,6 +20,7 @@
#include "Client.h"
#include "Common.h"
#include "LuaAPI.h"
#include "THeartbeatThread.h"
#include "TLuaEngine.h"
#include "TScopedTimer.h"
#include "nlohmann/json.hpp"
@@ -112,9 +113,19 @@ void TNetwork::UDPServerMain() {
try {
ip::udp::endpoint remote_client_ep {};
std::vector<uint8_t> Data = UDPRcvFromClient(remote_client_ep);
auto Pos = std::find(Data.begin(), Data.end(), ':');
if (Data.empty() || Pos > Data.begin() + 2)
if (Data.empty()) {
continue;
}
if (Data.size() == 1 && Data.at(0) == 'P') {
mUDPSock.send_to(const_buffer("P", 1), remote_client_ep, {}, ec);
// ignore errors
(void)ec;
continue;
}
auto Pos = std::find(Data.begin(), Data.end(), ':');
if (Pos > Data.begin() + 2) {
continue;
}
uint8_t ID = uint8_t(Data.at(0)) - 1;
mServer.ForEachClient([&](std::weak_ptr<TClient> ClientPtr) -> bool {
std::shared_ptr<TClient> Client;
@@ -240,16 +251,20 @@ void TNetwork::Identify(TConnection&& RawConnection) {
if (Code == 'C') {
Client = Authentication(std::move(RawConnection));
} else if (Code == 'D') {
HandleDownload(std::move(RawConnection));
beammp_errorf("Old download packet detected - the client is wildly out of date, this will be ignored");
return;
} else if (Code == 'P') {
boost::system::error_code ec;
write(RawConnection.Socket, buffer("P"), ec);
return;
} else if (Code == 'I') {
boost::system::error_code ec;
write(RawConnection.Socket, buffer(Application::Settings.getAsBool(Settings::Key::General_InformationPacket) ? THeartbeatThread::lastCall : ""), ec);
} else {
beammp_errorf("Invalid code got in Identify: '{}'", Code);
}
} catch (const std::exception& e) {
beammp_errorf("Error during handling of code {} - client left in invalid state, closing socket", Code);
beammp_errorf("Error during handling of code {} - client left in invalid state, closing socket: {}", Code, e.what());
boost::system::error_code ec;
RawConnection.Socket.shutdown(socket_base::shutdown_both, ec);
if (ec) {
@@ -262,27 +277,7 @@ void TNetwork::Identify(TConnection&& RawConnection) {
}
}
void TNetwork::HandleDownload(TConnection&& Conn) {
char D;
boost::system::error_code ec;
read(Conn.Socket, buffer(&D, 1), ec);
if (ec) {
Conn.Socket.shutdown(socket_base::shutdown_both, ec);
// ignore ec
return;
}
auto ID = uint8_t(D);
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
auto c = ClientPtr.lock();
if (c->GetID() == ID) {
c->SetDownSock(std::move(Conn.Socket));
}
}
return true;
});
}
std::string HashPassword(const std::string& str) {
std::stringstream ret;
@@ -312,10 +307,11 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
if (Data.size() > 3 && std::equal(Data.begin(), Data.begin() + VC.size(), VC.begin(), VC.end())) {
std::string ClientVersionStr(reinterpret_cast<const char*>(Data.data() + 2), Data.size() - 2);
Version ClientVersion = Application::VersionStrToInts(ClientVersionStr + ".0");
if (ClientVersion.major != Application::ClientMajorVersion()) {
beammp_errorf("Client tried to connect with version '{}', but only versions '{}.x.x' is allowed",
ClientVersion.AsString(), Application::ClientMajorVersion());
ClientKick(*Client, "Outdated Version!");
Version MinClientVersion = Application::ClientMinimumVersion();
if (Application::IsOutdated(ClientVersion, MinClientVersion)) {
beammp_errorf("Client tried to connect with version '{}', but only versions >= {} are allowed",
ClientVersion.AsString(), MinClientVersion.AsString());
ClientKick(*Client, fmt::format("Outdated version, launcher version >={} required to join!", MinClientVersion.AsString()));
return nullptr;
}
} else {
@@ -404,10 +400,21 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerAuth", "", Client->GetName(), Client->GetRoles(), Client->IsGuest(), Client->GetIdentifiers());
TLuaEngine::WaitForAll(Futures);
bool NotAllowed = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && bool(Result->Result.as<int>());
});
bool NotAllowed = false;
bool BypassLimit = false;
for (const auto& Result : Futures) {
if (!Result->Error && Result->Result.is<int>()) {
auto Res = Result->Result.as<int>();
if (Res == 1) {
NotAllowed = true;
break;
} else if (Res == 2) {
BypassLimit = true;
}
}
}
std::string Reason;
bool NotAllowedWithReason = std::any_of(Futures.begin(), Futures.end(),
[&Reason](const std::shared_ptr<TLuaResult>& Result) -> bool {
@@ -440,7 +447,7 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
if (!Allowed) {
return {};
} else if (mServer.ClientCount() < size_t(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers))) {
} else if (mServer.ClientCount() < size_t(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) || BypassLimit) {
beammp_info("Identification success");
mServer.InsertClient(Client);
TCPClient(Client);
@@ -758,11 +765,11 @@ void TNetwork::Parse(TClient& c, const std::vector<uint8_t>& Packet) {
case 'S':
if (SubCode == 'R') {
beammp_debug("Sending Mod Info");
std::string ToSend = mResourceManager.FileList() + mResourceManager.FileSizes();
if (ToSend.empty())
ToSend = "-";
std::string ToSend = mResourceManager.NewFileList();
beammp_debugf("Mod Info: {}", ToSend);
if (!TCPSend(c, StringToVector(ToSend))) {
// TODO: error
ClientKick(c, "TCP Send 'SY' failed");
return;
}
}
return;
@@ -772,8 +779,6 @@ void TNetwork::Parse(TClient& c, const std::vector<uint8_t>& Packet) {
}
void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
beammp_info(c.GetName() + " requesting : " + UnsafeName.substr(UnsafeName.find_last_of('/')));
if (!fs::path(UnsafeName).has_filename()) {
if (!TCPSend(c, StringToVector("CO"))) {
// TODO: handle
@@ -796,87 +801,9 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
// TODO: handle
}
/// Wait for connections
int T = 0;
while (!c.GetDownSock().is_open() && T < 50) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
T++;
}
if (!c.GetDownSock().is_open()) {
beammp_error("Client doesn't have a download socket!");
if (!c.IsDisconnected())
c.Disconnect("Missing download socket");
return;
}
size_t Size = size_t(std::filesystem::file_size(FileName));
size_t MSize = Size / 2;
std::thread SplitThreads[2] {
std::thread([&] {
RegisterThread("SplitLoad_0");
SplitLoad(c, 0, MSize, false, FileName);
}),
std::thread([&] {
RegisterThread("SplitLoad_1");
SplitLoad(c, MSize, Size, true, FileName);
})
};
for (auto& SplitThread : SplitThreads) {
if (SplitThread.joinable()) {
SplitThread.join();
}
}
}
static std::pair<size_t /* count */, size_t /* last chunk */> SplitIntoChunks(size_t FullSize, size_t ChunkSize) {
if (FullSize < ChunkSize) {
return { 0, FullSize };
}
size_t Count = FullSize / (FullSize / ChunkSize);
size_t LastChunkSize = FullSize - (Count * ChunkSize);
return { Count, LastChunkSize };
}
TEST_CASE("SplitIntoChunks") {
size_t FullSize;
size_t ChunkSize;
SUBCASE("Normal case") {
FullSize = 1234567;
ChunkSize = 1234;
}
SUBCASE("Zero original size") {
FullSize = 0;
ChunkSize = 100;
}
SUBCASE("Equal full size and chunk size") {
FullSize = 125;
ChunkSize = 125;
}
SUBCASE("Even split") {
FullSize = 10000;
ChunkSize = 100;
}
SUBCASE("Odd split") {
FullSize = 13;
ChunkSize = 2;
}
SUBCASE("Large sizes") {
FullSize = 10 * GB;
ChunkSize = 125 * MB;
}
auto [Count, LastSize] = SplitIntoChunks(FullSize, ChunkSize);
CHECK((Count * ChunkSize) + LastSize == FullSize);
}
const uint8_t* /* end ptr */ TNetwork::SendSplit(TClient& c, ip::tcp::socket& Socket, const uint8_t* DataPtr, size_t Size) {
if (TCPSendRaw(c, Socket, DataPtr, Size)) {
return DataPtr + Size;
} else {
return nullptr;
}
SendFileToClient(c, Size, FileName);
}
#if defined(BEAMMP_LINUX)
@@ -886,8 +813,8 @@ const uint8_t* /* end ptr */ TNetwork::SendSplit(TClient& c, ip::tcp::socket& So
#include <unistd.h>
#include <signal.h>
#endif
void TNetwork::SplitLoad(TClient& c, size_t Offset, size_t End, bool D, const std::string& Name) {
TScopedTimer timer(fmt::format("Download of {}-{} for '{}'", Offset, End, Name));
void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name) {
TScopedTimer timer(fmt::format("Download of '{}' for client {}", Name, c.GetID()));
#if defined(BEAMMP_LINUX)
signal(SIGPIPE, SIG_IGN);
// on linux, we can use sendfile(2)!
@@ -897,11 +824,11 @@ void TNetwork::SplitLoad(TClient& c, size_t Offset, size_t End, bool D, const st
return;
}
// native handle, needed in order to make native syscalls with it
int socket = D ? c.GetDownSock().native_handle() : c.GetTCPSock().native_handle();
int socket = c.GetTCPSock().native_handle();
ssize_t ret = 0;
auto ToSendTotal = End - Offset;
auto Start = Offset;
auto ToSendTotal = Size;
auto Start = 0;
while (ret < ssize_t(ToSendTotal)) {
auto SysOffset = off_t(Start + size_t(ret));
ret = sendfile(socket, fd, &SysOffset, ToSendTotal - size_t(ret));
@@ -915,35 +842,32 @@ void TNetwork::SplitLoad(TClient& c, size_t Offset, size_t End, bool D, const st
std::ifstream f(Name.c_str(), std::ios::binary);
uint32_t Split = 125 * MB;
std::vector<uint8_t> Data;
if (End > Split)
if (Size > Split)
Data.resize(Split);
else
Data.resize(End);
ip::tcp::socket* TCPSock { nullptr };
if (D)
TCPSock = &c.GetDownSock();
else
TCPSock = &c.GetTCPSock();
while (!c.IsDisconnected() && Offset < End) {
size_t Diff = End - Offset;
Data.resize(Size);
ip::tcp::socket* TCPSock = &c.GetTCPSock();
std::streamsize Sent = 0;
while (!c.IsDisconnected() && Sent < Size) {
size_t Diff = Size - Sent;
if (Diff > Split) {
f.seekg(Offset, std::ios_base::beg);
f.seekg(Sent, std::ios_base::beg);
f.read(reinterpret_cast<char*>(Data.data()), Split);
if (!TCPSendRaw(c, *TCPSock, Data.data(), Split)) {
if (!c.IsDisconnected())
c.Disconnect("TCPSendRaw failed in mod download (1)");
break;
}
Offset += Split;
Sent += Split;
} else {
f.seekg(Offset, std::ios_base::beg);
f.seekg(Sent, std::ios_base::beg);
f.read(reinterpret_cast<char*>(Data.data()), Diff);
if (!TCPSendRaw(c, *TCPSock, Data.data(), int32_t(Diff))) {
if (!c.IsDisconnected())
c.Disconnect("TCPSendRaw failed in mod download (2)");
break;
}
Offset += Diff;
Sent += Diff;
}
}
#endif

View File

@@ -17,9 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "TResourceManager.h"
#include "Common.h"
#include <algorithm>
#include <filesystem>
#include <fmt/core.h>
#include <ios>
#include <nlohmann/json.hpp>
#include <openssl/evp.h>
namespace fs = std::filesystem;
@@ -52,3 +57,83 @@ TResourceManager::TResourceManager() {
Application::SetSubsystemStatus("ResourceManager", Application::Status::Good);
}
std::string TResourceManager::NewFileList() const {
return mMods.dump();
}
void TResourceManager::RefreshFiles() {
mMods.clear();
std::unique_lock Lock(mModsMutex);
std::string Path = Application::Settings.getAsString(Settings::Key::General_ResourceFolder) + "/Client";
for (const auto& entry : fs::directory_iterator(Path)) {
std::string File(entry.path().string());
if (entry.path().extension() != ".zip" || std::filesystem::is_directory(entry.path())) {
beammp_warnf("'{}' is not a ZIP file and will be ignored", File);
continue;
}
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
std::ifstream stream(File, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(File);
size_t Read = 0;
std::vector<char> Data;
while (Read < FileSize) {
Data.resize(size_t(std::min<size_t>(FileSize - Read, 4096)));
size_t RealDataSize = Data.size();
stream.read(Data.data(), std::streamsize(Data.size()));
if (stream.eof() || stream.fail()) {
RealDataSize = size_t(stream.gcount());
}
Data.resize(RealDataSize);
if (RealDataSize == 0) {
break;
}
if (RealDataSize > 0 && !EVP_DigestUpdate(mdctx, Data.data(), Data.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
Read += RealDataSize;
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
result += fmt::format("{:02x}", sha256_value[i]);
}
beammp_debugf("sha256('{}'): {}", File, result);
mMods.push_back(nlohmann::json {
{ "file_name", std::filesystem::path(File).filename() },
{ "file_size", std::filesystem::file_size(File) },
{ "hash_algorithm", "sha256" },
{ "hash", result },
});
} catch (const std::exception& e) {
beammp_errorf("Sha256 hashing of '{}' failed: {}", File, e.what());
}
}
}

View File

@@ -436,6 +436,20 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
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;

View File

@@ -36,19 +36,19 @@
#include <thread>
static const std::string sCommandlineArguments = R"(
USAGE:
USAGE:
BeamMP-Server [arguments]
ARGUMENTS:
--help
--help
Displays this help and exits.
--port=1234
Sets the server's listening TCP and
UDP port. Overrides ENV and ServerConfig.
--config=/path/to/ServerConfig.toml
Absolute or relative path to the
Absolute or relative path to the
Server Config file, including the
filename. For paths and filenames with
filename. For paths and filenames with
spaces, put quotes around the path.
--working-directory=/path/to/folder
Sets the working directory of the Server.
@@ -59,7 +59,7 @@ ARGUMENTS:
EXAMPLES:
BeamMP-Server --config=../MyWestCoastServerConfig.toml
Runs the BeamMP-Server and uses the server config file
Runs the BeamMP-Server and uses the server config file
which is one directory above it and is named
'MyWestCoastServerConfig.toml'.
)";
@@ -190,6 +190,7 @@ int BeamMPServerMain(MainArguments Arguments) {
beammp_trace("Running in debug mode on a debug build");
TResourceManager ResourceManager;
ResourceManager.RefreshFiles();
TPPSMonitor PPSMonitor(Server);
THeartbeatThread Heartbeat(ResourceManager, Server);
TNetwork Network(Server, PPSMonitor, ResourceManager);