add socketio, http post & get

This commit is contained in:
Lion Kortlepel 2021-02-16 12:37:55 +01:00 committed by Anonymous275
parent 4cda6e8bc3
commit ef5db013b3
12 changed files with 487 additions and 19 deletions

15
.gitmodules vendored
View File

@ -1,6 +1,15 @@
[submodule "commandline"]
path = commandline
url = https://github.com/lionkor/commandline
[submodule "include/commandline"] [submodule "include/commandline"]
path = 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

1
.idea/vcs.xml generated
View File

@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/include/commandline" vcs="Git" />
</component> </component>
</project> </project>

View File

@ -2,14 +2,24 @@ cmake_minimum_required(VERSION 3.13)
project(Server) project(Server)
set(CMAKE_CXX_STANDARD 17) 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") add_subdirectory("include/commandline")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
if (UNIX) if (UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -static-libstdc++") 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_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -s -fno-builtin") 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) elseif (WIN32)
message(STATUS "MSVC -> forcing use of statically-linked runtime.") message(STATUS "MSVC -> forcing use of statically-linked runtime.")
STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}) STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE})
@ -21,21 +31,24 @@ find_package(Boost REQUIRED COMPONENTS system thread)
add_executable(BeamMP-Server add_executable(BeamMP-Server
src/main.cpp src/main.cpp
src/TConsole.cpp include/TConsole.h src/TConsole.cpp
src/TServer.cpp include/TServer.h src/TServer.cpp
src/Compat.cpp include/Compat.h src/Compat.cpp
src/Common.cpp include/Common.h src/Common.cpp
src/Client.cpp include/Client.h src/Client.cpp
src/VehicleData.cpp include/VehicleData.h src/VehicleData.cpp
src/TConfig.cpp include/TConfig.h src/TConfig.cpp
src/TLuaEngine.cpp include/TLuaEngine.h src/TLuaEngine.cpp
src/TLuaFile.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") target_include_directories(BeamMP-Server PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/commandline")
find_package(Lua REQUIRED) 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) find_package(OpenSSL REQUIRED)

9
include/Http.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <string>
#include <unordered_map>
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<std::string, std::string>& fields, const std::string& body, bool json);
}

69
include/SocketIO.h Normal file
View File

@ -0,0 +1,69 @@
#pragma once
#include <atomic>
#include <deque>
#include <mutex>
#include <sio_client.h>
#include <thread>
/*
* 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<Event> _Queue;
friend std::unique_ptr<SocketIO> std::make_unique<SocketIO>();
};

View File

@ -0,0 +1,10 @@
#pragma once
#include "IThreaded.h"
#include "Common.h"
class THeartbeatThread : public IThreaded {
public:
THeartbeatThread();
void operator()() override;
};

View File

@ -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;
};

143
src/Http.cpp Normal file
View File

@ -0,0 +1,143 @@
#include "Http.h"
#include "CustomAssert.h"
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
namespace ssl = net::ssl; // from <boost/asio/ssl.hpp>
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
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<http::string_body> 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<http::string_body> 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<std::string, std::string>& 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<beast::tcp_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<int>(::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<http::string_body> 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<std::string>(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<http::string_body> 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";
}
}

109
src/SocketIO.cpp Normal file
View File

@ -0,0 +1,109 @@
#include "SocketIO.h"
#include "Logger.h"
#include "Settings.h"
static std::unique_ptr<SocketIO> SocketIOInstance = std::make_unique<SocketIO>();
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;
}

57
src/THeartbeatThread.cpp Normal file
View File

@ -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);
}
}

26
src/TResourceManager.cpp Normal file
View File

@ -0,0 +1,26 @@
#include "TResourceManager.h"
#include <algorithm>
#include <filesystem>
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");
}

View File

@ -4,6 +4,7 @@
#include "TConfig.h" #include "TConfig.h"
#include "TConsole.h" #include "TConsole.h"
#include "TLuaEngine.h" #include "TLuaEngine.h"
#include "TResourceManager.h"
#include "TServer.h" #include "TServer.h"
#include <atomic> #include <atomic>
#include <functional> #include <functional>
@ -44,6 +45,8 @@ int main(int argc, char** argv) {
TServer Server(argc, argv); TServer Server(argc, argv);
TConfig Config("Server.cfg"); TConfig Config("Server.cfg");
TLuaEngine LuaEngine(Server); TLuaEngine LuaEngine(Server);
TResourceManager ResourceManager;
THeartbeatThread Heartbeat;
// TODO: replace // TODO: replace
bool Shutdown = false; bool Shutdown = false;