diff --git a/.gitmodules b/.gitmodules
index edc3926..37a5856 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,15 @@
-[submodule "commandline"]
- path = commandline
- url = https://github.com/lionkor/commandline
[submodule "include/commandline"]
path = include/commandline
- url = https://github.com/lionkor/commandline
+ url = https://github.com/lionkor/commandline
+[submodule "socket.io-client-cpp"]
+ path = socket.io-client-cpp
+ url = https://github.com/socketio/socket.io-client-cpp
+[submodule "asio"]
+ path = asio
+ url = https://github.com/chriskohlhoff/asio
+[submodule "rapidjson"]
+ path = rapidjson
+ url = https://github.com/Tencent/rapidjson
+[submodule "websocketpp"]
+ path = websocketpp
+ url = https://github.com/zaphoyd/websocketpp
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..dd314cc 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 60ee713..f574900 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,14 +2,24 @@ cmake_minimum_required(VERSION 3.13)
project(Server)
set(CMAKE_CXX_STANDARD 17)
-set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
+# this has to happen before -DDEBUG since it wont compile properly with -DDEBUG
+include_directories("asio/asio/include")
+include_directories("rapidjson/include")
+include_directories("websocketpp")
+add_subdirectory("socket.io-client-cpp")
add_subdirectory("include/commandline")
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
+
if (UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -static-libstdc++")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -s -fno-builtin")
+ if (SANITIZE)
+ message(STATUS "sanitize is ON")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined,thread")
+ endif (SANITIZE)
elseif (WIN32)
message(STATUS "MSVC -> forcing use of statically-linked runtime.")
STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE})
@@ -19,23 +29,26 @@ endif ()
find_package(Boost REQUIRED COMPONENTS system thread)
-add_executable(BeamMP-Server
- src/main.cpp
- src/TConsole.cpp
- src/TServer.cpp
- src/Compat.cpp
- src/Common.cpp
- src/Client.cpp
- src/VehicleData.cpp
- src/TConfig.cpp
- src/TLuaEngine.cpp
- src/TLuaFile.cpp
-)
-
+add_executable(BeamMP-Server
+ src/main.cpp
+ include/TConsole.h src/TConsole.cpp
+ include/TServer.h src/TServer.cpp
+ include/Compat.h src/Compat.cpp
+ include/Common.h src/Common.cpp
+ include/Client.h src/Client.cpp
+ include/VehicleData.h src/VehicleData.cpp
+ include/TConfig.h src/TConfig.cpp
+ include/TLuaEngine.h src/TLuaEngine.cpp
+ include/TLuaFile.h src/TLuaFile.cpp
+ include/TResourceManager.h src/TResourceManager.cpp
+ include/THeartbeatThread.h src/THeartbeatThread.cpp
+ include/Http.h src/Http.cpp
+ include/SocketIO.h src/SocketIO.cpp)
+
target_include_directories(BeamMP-Server PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/commandline")
find_package(Lua REQUIRED)
-target_include_directories(BeamMP-Server PUBLIC ${Boost_INCLUDE_DIRS} ${LUA_INCLUDE_DIR})
+target_include_directories(BeamMP-Server PUBLIC ${Boost_INCLUDE_DIRS} ${LUA_INCLUDE_DIR} "socket.io-client-cpp/src")
find_package(OpenSSL REQUIRED)
diff --git a/include/Http.h b/include/Http.h
new file mode 100644
index 0000000..e0eb564
--- /dev/null
+++ b/include/Http.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include
+#include
+
+namespace Http {
+std::string GET(const std::string& host, int port, const std::string& target);
+std::string POST(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, bool json);
+}
\ No newline at end of file
diff --git a/include/SocketIO.h b/include/SocketIO.h
new file mode 100644
index 0000000..a73a796
--- /dev/null
+++ b/include/SocketIO.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+/*
+ * We send relevant server events over socket.io to the backend.
+ *
+ * We send all events to `backend.beammp.com`, to the room `/key`
+ * where `key` is the currently active auth-key.
+ */
+
+enum class SocketIOEvent {
+ ConsoleOut,
+ CPUUsage,
+ MemoryUsage,
+ NetworkUsage,
+ PlayerList,
+};
+
+enum class SocketIORoom {
+ None,
+ Stats,
+ Player,
+ Info,
+ Console,
+};
+
+class SocketIO final {
+private:
+ struct Event;
+
+public:
+ enum class EventType {
+ };
+
+ // Singleton pattern
+ static SocketIO& Get();
+
+ void Emit(SocketIORoom Room, SocketIOEvent Event, const std::string& Data);
+
+ ~SocketIO();
+
+ void SetAuthenticated(bool auth) { _Authenticated = auth; }
+
+private:
+ SocketIO();
+
+ void ThreadMain();
+
+ struct Event {
+ std::string Room;
+ std::string Name;
+ std::string Data;
+ };
+
+ bool _Authenticated { false };
+ sio::client _Client;
+ std::thread _Thread;
+ std::atomic_bool _CloseThread { false };
+ std::mutex _QueueMutex;
+ std::deque _Queue;
+
+ friend std::unique_ptr std::make_unique();
+};
+
diff --git a/include/THeartbeatThread.h b/include/THeartbeatThread.h
new file mode 100644
index 0000000..7267cb5
--- /dev/null
+++ b/include/THeartbeatThread.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "IThreaded.h"
+#include "Common.h"
+
+class THeartbeatThread : public IThreaded {
+public:
+ THeartbeatThread();
+ void operator()() override;
+};
\ No newline at end of file
diff --git a/include/TResourceManager.h b/include/TResourceManager.h
new file mode 100644
index 0000000..4035784
--- /dev/null
+++ b/include/TResourceManager.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "Common.h"
+
+class TResourceManager {
+public:
+ TResourceManager();
+
+ [[nodiscard]] uint64_t MaxModSize() const { return mMaxModSize; }
+ [[nodiscard]] std::string FileList() const { return mFileList; }
+ [[nodiscard]] std::string FileSizes() const { return mFileSizes; }
+ [[nodiscard]] int ModsLoaded() const { return mModsLoaded; }
+
+private:
+ uint64_t mMaxModSize = 0;
+ std::string mFileSizes;
+ std::string mFileList;
+ int mModsLoaded = 0;
+};
\ No newline at end of file
diff --git a/src/Http.cpp b/src/Http.cpp
new file mode 100644
index 0000000..aad38e5
--- /dev/null
+++ b/src/Http.cpp
@@ -0,0 +1,143 @@
+#include "Http.h"
+
+#include "CustomAssert.h"
+#include
+#include
+#include
+#include
+
+namespace beast = boost::beast; // from
+namespace http = beast::http; // from
+namespace net = boost::asio; // from
+namespace ssl = net::ssl; // from
+using tcp = net::ip::tcp; // from
+
+std::string Http::GET(const std::string& host, int port, const std::string& target) {
+ // FIXME: doesn't support https
+ // if it causes issues, yell at me and I'll fix it asap. - Lion
+ try {
+ net::io_context io;
+ tcp::resolver resolver(io);
+ beast::tcp_stream stream(io);
+ auto const results = resolver.resolve(host, std::to_string(port));
+ stream.connect(results);
+
+ http::request req { http::verb::get, target, 11 /* http 1.1 */ };
+
+ req.set(http::field::host, host);
+ // tell the server what we are (boost beast)
+ req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
+
+ http::write(stream, req);
+
+ // used for reading
+ beast::flat_buffer buffer;
+ http::response response;
+
+ http::read(stream, buffer, response);
+
+ std::string result(response.body());
+
+ beast::error_code ec;
+ stream.socket().shutdown(tcp::socket::shutdown_both, ec);
+ if (ec && ec != beast::errc::not_connected) {
+ throw beast::system_error { ec }; // goes down to `return "-1"` anyways
+ }
+
+ return result;
+
+ } catch (const std::exception& e) {
+ error(e.what());
+ return "-1";
+ }
+}
+
+std::string Http::POST(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, bool json) {
+ try {
+ net::io_context io;
+
+ // The SSL context is required, and holds certificates
+ ssl::context ctx(ssl::context::tlsv13);
+
+ ctx.set_verify_mode(ssl::verify_none);
+
+ tcp::resolver resolver(io);
+ beast::ssl_stream stream(io, ctx);
+ decltype(resolver)::results_type results;
+ auto try_connect_with_protocol = [&](tcp protocol) {
+ try {
+ results = resolver.resolve(protocol, host, std::to_string(443));
+ if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) {
+ boost::system::error_code ec { static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category() };
+ // FIXME: we could throw and crash, if we like
+ // throw boost::system::system_error { ec };
+ debug("POST " + host + target + " failed.");
+ return false;
+ }
+ beast::get_lowest_layer(stream).connect(results);
+ } catch (const boost::system::system_error&) {
+ return false;
+ }
+ return true;
+ };
+ //bool ok = try_connect_with_protocol(tcp::v6());
+ //if (!ok) {
+ //debug("IPv6 connect failed, trying IPv4");
+ bool ok = try_connect_with_protocol(tcp::v4());
+ if (!ok) {
+ error("failed to resolve or connect in POST " + host + target);
+ return "-1";
+ }
+ //}
+ stream.handshake(ssl::stream_base::client);
+ http::request req { http::verb::post, target, 11 /* http 1.1 */ };
+
+ req.set(http::field::host, host);
+ if (!body.empty()) {
+ if (json) {
+ // FIXME: json is untested.
+ req.set(http::field::content_type, "application/json");
+ } else {
+ req.set(http::field::content_type, "application/x-www-form-urlencoded");
+ }
+ req.set(http::field::content_length, std::to_string(body.size()));
+ req.body() = body;
+ // info("body is " + body + " (" + req.body() + ")");
+ // info("content size is " + std::to_string(body.size()) + " (" + boost::lexical_cast(body.size()) + ")");
+ }
+ for (const auto& pair : fields) {
+ // info("setting " + pair.first + " to " + pair.second);
+ req.set(pair.first, pair.second);
+ }
+
+ std::stringstream oss;
+ oss << req;
+
+ beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(5));
+
+ http::write(stream, req);
+
+ // used for reading
+ beast::flat_buffer buffer;
+ http::response response;
+
+ http::read(stream, buffer, response);
+
+ std::stringstream result;
+ result << response;
+
+ beast::error_code ec;
+ stream.shutdown(ec);
+ // IGNORING ec
+
+ // info(result.str());
+ std::string debug_response_str;
+ std::getline(result, debug_response_str);
+ debug("POST " + host + target + ": " + debug_response_str);
+ return std::string(response.body());
+
+ } catch (const std::exception& e) {
+ error(e.what());
+ return "-1";
+ }
+}
\ No newline at end of file
diff --git a/src/SocketIO.cpp b/src/SocketIO.cpp
new file mode 100644
index 0000000..9c5c4e3
--- /dev/null
+++ b/src/SocketIO.cpp
@@ -0,0 +1,109 @@
+#include "SocketIO.h"
+#include "Logger.h"
+#include "Settings.h"
+
+static std::unique_ptr SocketIOInstance = std::make_unique();
+
+SocketIO& SocketIO::Get() {
+ return *SocketIOInstance;
+}
+
+SocketIO::SocketIO()
+ : _Thread([this] { ThreadMain(); }) {
+ _Client.socket("/")->on("Hello", [&](sio::event&) {
+ DebugPrintTIDInternal("Hello-handler");
+ info("Got 'Hello' from backend socket-io!");
+ });
+ _Client.connect("https://backend.beammp.com");
+ _Client.set_logs_quiet();
+}
+
+SocketIO::~SocketIO() {
+ _CloseThread.store(true);
+ _Thread.join();
+}
+
+static constexpr auto RoomNameFromEnum(SocketIORoom Room) {
+ switch (Room) {
+ case SocketIORoom::None:
+ return "";
+ case SocketIORoom::Console:
+ return "console";
+ case SocketIORoom::Info:
+ return "info";
+ case SocketIORoom::Player:
+ return "player";
+ case SocketIORoom::Stats:
+ return "stats";
+ default:
+ error("unreachable code reached (developer error)");
+ abort();
+ }
+}
+
+static constexpr auto EventNameFromEnum(SocketIOEvent Event) {
+ switch (Event) {
+ case SocketIOEvent::CPUUsage:
+ return "cpu usage";
+ case SocketIOEvent::MemoryUsage:
+ return "memory usage";
+ case SocketIOEvent::ConsoleOut:
+ return "console out";
+ case SocketIOEvent::NetworkUsage:
+ return "network usage";
+ case SocketIOEvent::PlayerList:
+ return "player list";
+ default:
+ error("unreachable code reached (developer error)");
+ abort();
+ }
+}
+
+void SocketIO::Emit(SocketIORoom Room, SocketIOEvent Event, const std::string& Data) {
+ if (!_Authenticated) {
+ debug("trying to emit a socket.io event when not yet authenticated");
+ return;
+ }
+ std::string RoomName = RoomNameFromEnum(Room);
+ std::string EventName = EventNameFromEnum(Event);
+ debug("emitting event \"" + EventName + "\" with data: \"" + Data + "\" in room \"/key/" + RoomName + "\"");
+ std::unique_lock Lock(_QueueMutex);
+ _Queue.push_back({ RoomName, EventName, Data });
+ debug("queue now has " + std::to_string(_Queue.size()) + " events");
+}
+
+void SocketIO::ThreadMain() {
+ bool FirstTime = true;
+ while (!_CloseThread.load()) {
+ if (_Authenticated && FirstTime) {
+ FirstTime = false;
+ DebugPrintTID();
+ }
+ bool empty = false;
+ { // queue lock scope
+ std::unique_lock Lock(_QueueMutex);
+ empty = _Queue.empty();
+ } // end queue lock scope
+ if (empty) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ continue;
+ } else {
+ Event TheEvent;
+ { // queue lock scope
+ std::unique_lock Lock(_QueueMutex);
+ TheEvent = _Queue.front();
+ _Queue.pop_front();
+ } // end queue lock scope
+ debug("sending \"" + TheEvent.Name + "\" event");
+ auto Room = "/" + TheEvent.Room;
+ _Client.socket("/")->emit(TheEvent.Name, TheEvent.Data);
+ debug("sent \"" + TheEvent.Name + "\" event");
+ }
+ }
+ std::cout << "closing " + std::string(__func__) << std::endl;
+
+ _Client.sync_close();
+ _Client.clear_con_listeners();
+ std::cout << "closed" << std::endl;
+
+}
diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp
new file mode 100644
index 0000000..cba3c10
--- /dev/null
+++ b/src/THeartbeatThread.cpp
@@ -0,0 +1,57 @@
+#include "THeartbeatThread.h"
+
+#include "Http.h"
+#include "SocketIO.h"
+
+THeartbeatThread::THeartbeatThread() {
+}
+
+void THeartbeatThread::operator()() {
+ std::string Body;
+ std::string T;
+
+ // these are "hot-change" related variables
+ static std::string Last = "";
+
+ static std::chrono::high_resolution_clock::time_point LastNormalUpdateTime = std::chrono::high_resolution_clock::now();
+ bool isAuth = false;
+ while (true) {
+ Body = GenerateCall();
+ // a hot-change occurs when a setting has changed, to update the backend of that change.
+ auto Now = std::chrono::high_resolution_clock::now();
+ if (Last == Body && (Now - LastNormalUpdateTime) < std::chrono::seconds(30)) {
+ std::this_thread::sleep_for(std::chrono::seconds(5));
+ continue;
+ }
+ Last = Body;
+ LastNormalUpdateTime = Now;
+ if (!Application::Settings.CustomIP.empty())
+ Body += "&ip=" + Application::Settings.CustomIP;
+
+ T = Http::POST("backend.beammp.com", "/heartbeat", {}, Body, false);
+
+ if (T.substr(0, 2) != "20") {
+ //Backend system refused server startup!
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+ T = Http::POST("backend.beammp.com", "/heartbeat", {}, Body, false);
+ // TODO backup2 + HTTP flag (no TSL)
+ if (T.substr(0, 2) != "20") {
+ warn("Backend system refused server! Server might not show in the public list");
+ debug("server returned \"" + T + "\"");
+ isAuth = false;
+ }
+ }
+
+ if (!isAuth) {
+ if (T == "2000") {
+ info(("Authenticated!"));
+ isAuth = true;
+ } else if (T == "200") {
+ info(("Resumed authenticated session!"));
+ isAuth = true;
+ }
+ }
+
+ SocketIO::Get().SetAuthenticated(isAuth);
+ }
+}
diff --git a/src/TResourceManager.cpp b/src/TResourceManager.cpp
new file mode 100644
index 0000000..1a1a083
--- /dev/null
+++ b/src/TResourceManager.cpp
@@ -0,0 +1,26 @@
+#include "TResourceManager.h"
+
+#include
+#include
+
+namespace fs = std::filesystem;
+
+TResourceManager::TResourceManager() {
+ std::string Path = Application::Settings.Resource + "/Client";
+ if (!fs::exists(Path))
+ fs::create_directory(Path);
+ for (const auto& entry : fs::directory_iterator(Path)) {
+ auto pos = entry.path().string().find(".zip");
+ if (pos != std::string::npos) {
+ if (entry.path().string().length() - pos == 4) {
+ mFileList += entry.path().string() + ";";
+ mFileSizes += std::to_string(uint64_t(fs::file_size(entry.path()))) + ";";
+ mMaxModSize += uint64_t(fs::file_size(entry.path()));
+ mModsLoaded++;
+ }
+ }
+ }
+ std::replace(mFileList.begin(), mFileList.end(), '\\', '/');
+ if (mModsLoaded)
+ info("Loaded " + std::to_string(mModsLoaded) + " Mods");
+}
diff --git a/src/main.cpp b/src/main.cpp
index a100323..69c7c64 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -4,6 +4,7 @@
#include "TConfig.h"
#include "TConsole.h"
#include "TLuaEngine.h"
+#include "TResourceManager.h"
#include "TServer.h"
#include
#include
@@ -44,6 +45,8 @@ int main(int argc, char** argv) {
TServer Server(argc, argv);
TConfig Config("Server.cfg");
TLuaEngine LuaEngine(Server);
+ TResourceManager ResourceManager;
+ THeartbeatThread Heartbeat;
// TODO: replace
bool Shutdown = false;