From ef5db013b38d4fcf01f72f07fce277589d06f961 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Tue, 16 Feb 2021 12:37:55 +0100 Subject: [PATCH] add socketio, http post & get --- .gitmodules | 17 +++-- .idea/vcs.xml | 1 + CMakeLists.txt | 43 +++++++---- include/Http.h | 9 +++ include/SocketIO.h | 69 ++++++++++++++++++ include/THeartbeatThread.h | 10 +++ include/TResourceManager.h | 19 +++++ src/Http.cpp | 143 +++++++++++++++++++++++++++++++++++++ src/SocketIO.cpp | 109 ++++++++++++++++++++++++++++ src/THeartbeatThread.cpp | 57 +++++++++++++++ src/TResourceManager.cpp | 26 +++++++ src/main.cpp | 3 + 12 files changed, 487 insertions(+), 19 deletions(-) create mode 100644 include/Http.h create mode 100644 include/SocketIO.h create mode 100644 include/THeartbeatThread.h create mode 100644 include/TResourceManager.h create mode 100644 src/Http.cpp create mode 100644 src/SocketIO.cpp create mode 100644 src/THeartbeatThread.cpp create mode 100644 src/TResourceManager.cpp 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;