diff --git a/CMakeLists.txt b/CMakeLists.txt index 3452086..98d287f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ find_package(Lua REQUIRED) target_include_directories(BeamMP-Server PUBLIC ${LUA_INCLUDE_DIR}) if (UNIX) - target_link_libraries(BeamMP-Server z pthread stdc++fs ${Boost_LINK_DIRS} ${LUA_LIBRARIES} curl dl) + target_link_libraries(BeamMP-Server z pthread stdc++fs ${Boost_LINK_DIRS} ${LUA_LIBRARIES} curl dl crypto ssl) elseif (WIN32) include(FindLua) find_package(ZLIB REQUIRED) diff --git a/include/Curl/Http.h b/include/Curl/Http.h index b11a88a..714543d 100644 --- a/include/Curl/Http.h +++ b/include/Curl/Http.h @@ -7,5 +7,6 @@ /// #pragma once #include -std::string HttpRequest(const std::string& IP, int port); -std::string PostHTTP(const std::string& IP, const std::string& Fields, bool json); \ No newline at end of file +#include +std::string HttpRequest(const std::string& host, int port, const std::string& target); +std::string PostHTTP(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, bool json); diff --git a/src/Init/Heartbeat.cpp b/src/Init/Heartbeat.cpp index 99cd88c..8177ed0 100644 --- a/src/Init/Heartbeat.cpp +++ b/src/Init/Heartbeat.cpp @@ -13,6 +13,7 @@ #include #include #include +#include void WebsocketInit(); std::string GetPlayers() { @@ -36,8 +37,8 @@ std::string GenerateCall() { << "&desc=" << ServerDesc; return Ret.str(); } -std::string RunPromise(const std::string& IP, const std::string& R) { - std::packaged_task task([&] { return PostHTTP(IP, R, false); }); +std::string RunPromise(const std::string& host, const std::string& target, const std::unordered_map& R, const std::string& body) { + std::packaged_task task([&] { DebugPrintTIDInternal("Heartbeat_POST"); return PostHTTP(host, target, R, body, false); }); std::future f1 = task.get_future(); std::thread t(std::move(task)); t.detach(); @@ -50,20 +51,32 @@ std::string RunPromise(const std::string& IP, const std::string& R) { [[noreturn]] void Heartbeat() { DebugPrintTID(); - std::string R, T; + std::string R; + std::string T; + + // these are "hot-change" related variables + static std::string LastR = ""; + + static std::chrono::high_resolution_clock::time_point LastNormalUpdateTime = std::chrono::high_resolution_clock::now(); bool isAuth = false; while (true) { R = 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 (LastR == R && (Now - LastNormalUpdateTime) < std::chrono::seconds(30)) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + continue; + } + LastR = R; + LastNormalUpdateTime = Now; if (!CustomIP.empty()) R += "&ip=" + CustomIP; - std::string link = "https://beammp.com/heartbeatv2"; - T = RunPromise(link, R); + T = RunPromise("beammp.com", "/heartbeatv2", {}, R); if (T.substr(0, 2) != "20") { //Backend system refused server startup! - std::this_thread::sleep_for(std::chrono::seconds(10)); - std::string Backup = "https://backup1.beammp.com/heartbeatv2"; - T = RunPromise(Backup, R); + std::this_thread::sleep_for(std::chrono::seconds(5)); + T = RunPromise("backup1.beammp.com", "/heartbeatv2", {}, R); if (T.substr(0, 2) != "20") { warn("Backend system refused server! Server might not show in the public list"); } @@ -71,11 +84,12 @@ std::string RunPromise(const std::string& IP, const std::string& R) { //Server Authenticated if (!isAuth) { WebsocketInit(); - if (T.length() == 4)info(("Authenticated!")); - else info(("Resumed authenticated session!")); + if (T.length() == 4) + info(("Authenticated!")); + else + info(("Resumed authenticated session!")); isAuth = true; } - //std::this_thread::sleep_for(std::chrono::seconds(5)); } } void HBInit() { diff --git a/src/Network/Auth.cpp b/src/Network/Auth.cpp index e1f436c..570e774 100644 --- a/src/Network/Auth.cpp +++ b/src/Network/Auth.cpp @@ -20,8 +20,7 @@ std::string GetClientInfo(const std::string& PK) { if (!PK.empty()) { - return PostHTTP("https://auth.beammp.com/pkToUser", R"({"key":")" + PK + "\"}", true); - ; + return PostHTTP("auth.beammp.com", "/pkToUser", {}, R"({"key":")" + PK + "\"}", true); } return ""; } diff --git a/src/Network/Http.cpp b/src/Network/Http.cpp index 7a605f9..c7bbfeb 100644 --- a/src/Network/Http.cpp +++ b/src/Network/Http.cpp @@ -7,69 +7,155 @@ /// #include "CustomAssert.h" -#include #include -class CurlManager{ -public: - CurlManager(){ - curl = curl_easy_init(); - } - ~CurlManager(){ - curl_easy_cleanup(curl); - } - inline CURL* Get(){ - return curl; - } -private: - CURL *curl; -}; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} -std::string HttpRequest(const std::string& IP, int port) { - static thread_local CurlManager M; - std::string readBuffer; - CURL* curl = M.Get(); - CURLcode res; - Assert(curl); - if (curl) { - curl_easy_setopt(curl, CURLOPT_URL, IP.c_str()); - curl_easy_setopt(curl, CURLOPT_PORT, port); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - res = curl_easy_perform(curl); - if (res != CURLE_OK) - return "-1"; +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 + +// UNUSED?! +std::string HttpRequest(const std::string& host, int port, const std::string& target) { + // FIXME: this is likely not very well written. + // 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"; } - return readBuffer; } -std::string PostHTTP(const std::string& IP, const std::string& Fields, bool json) { - auto header = curl_slist { (char*)"Content-Type: application/json" }; - static thread_local CurlManager M; - static std::mutex Lock; - std::scoped_lock Guard(Lock); - CURL* curl = M.Get(); - CURLcode res; - std::string readBuffer; +std::string PostHTTP(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, bool json) { + try { + net::io_context io; - Assert(curl); - if (curl) { - curl_easy_setopt(curl, CURLOPT_URL, IP.c_str()); - if (json) - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, Fields.size()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, Fields.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5); - res = curl_easy_perform(curl); - if (res != CURLE_OK) - return "-1"; + // 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"); + 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, boost::lexical_cast(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"; } - return readBuffer; }