diff --git a/include/Common.h b/include/Common.h index 5eaec2e..e401d2a 100644 --- a/include/Common.h +++ b/include/Common.h @@ -3,6 +3,7 @@ #include "TSentry.h" extern TSentry Sentry; +#include #include #include #include @@ -65,6 +66,9 @@ public: static std::string GetBackup1Hostname() { return "backup1.beammp.com"; } static std::string GetBackup2Hostname() { return "backup2.beammp.com"; } static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; } + static void CheckForUpdates(); + static std::array VersionStrToInts(const std::string& str); + static bool IsOutdated(const std::array& Current, const std::array& Newest); private: static inline std::string mPPS; diff --git a/include/Http.h b/include/Http.h index 35916bf..6c3cec3 100644 --- a/include/Http.h +++ b/include/Http.h @@ -4,7 +4,7 @@ #include namespace Http { -std::string GET(const std::string& host, int port, const std::string& target); +std::string GET(const std::string& host, int port, const std::string& target, unsigned int* status = nullptr); std::string POST(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, bool json, int* status = nullptr); namespace Status { std::string ToString(int code); diff --git a/src/Common.cpp b/src/Common.cpp index dc773d4..4e8ada5 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -2,12 +2,16 @@ #include "TConsole.h" #include +#include #include #include +#include #include #include #include +#include "Http.h" + std::unique_ptr Application::mConsole = std::make_unique(); void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) { @@ -25,6 +29,53 @@ void Application::GracefullyShutdown() { } } +std::array Application::VersionStrToInts(const std::string& str) { + std::array Version; + std::stringstream ss(str); + for (int& i : Version) { + std::string Part; + std::getline(ss, Part, '.'); + std::from_chars(Part.begin().base(), Part.end().base(), i); + } + return Version; +} + +bool Application::IsOutdated(const std::array& Current, const std::array& Newest) { + if (Newest[0] > Current[0]) { + return true; + } else if (Newest[0] == Current[0] && Newest[1] > Current[1]) { + return true; + } else if (Newest[0] == Current[0] && Newest[1] == Current[1] && Newest[2] > Current[2]) { + return true; + } else { + return false; + } +} + +void Application::CheckForUpdates() { + // checks current version against latest version + std::regex VersionRegex { R"(\d\.\d\.\d)" }; + auto Response = Http::GET("kortlepel.com", 443, "/v/s.html"); + bool Matches = std::regex_match(Response, VersionRegex); + if (Matches) { + auto MyVersion = VersionStrToInts(ServerVersion()); + auto RemoteVersion = VersionStrToInts(Response); + if (IsOutdated(MyVersion, RemoteVersion)) { + warn("NEW VERSION OUT! There's a new version (v" + Response + ") of the BeamMP-Server available! For info on how to update your server, visit https://wiki.beammp.com/en/home/server-maintenance#updating-the-server."); + } else { + info("Server up-to-date!"); + } + } else { + warn("Unable to fetch version from backend."); +#if DEBUG + debug("got " + Response); +#endif // DEBUG + Sentry.CreateExclusiveContext(); + Sentry.SetContext("get-response", { { "response", Response } }); + Sentry.LogError("failed to get server version", _file_basename, _line); + } +} + std::string Comp(std::string Data) { std::array C {}; // obsolete diff --git a/src/Http.cpp b/src/Http.cpp index 2a4c11b..c3e9536 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -15,42 +15,81 @@ 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 +std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) { 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); + // Check command line arguments. + int version = 11; - http::request req { http::verb::get, target, 11 /* http 1.1 */ }; + // The io_context is required for all I/O + net::io_context ioc; - req.set(http::field::host, host); - // tell the server what we are (boost beast) - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + // The SSL context is required, and holds certificates + ssl::context ctx(ssl::context::tlsv12_client); - http::write(stream, req); + // This holds the root certificate used for verification + // we don't do / have this + // load_root_certificates(ctx); - // used for reading - beast::flat_buffer buffer; - http::response response; + // Verify the remote server's certificate + ctx.set_verify_mode(ssl::verify_none); - http::read(stream, buffer, response); + // These objects perform our I/O + tcp::resolver resolver(ioc); + beast::ssl_stream stream(ioc, ctx); - 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 + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { + beast::error_code ec { static_cast(::ERR_get_error()), net::error::get_ssl_category() }; + throw beast::system_error { ec }; } - return result; + // Look up the domain name + auto const results = resolver.resolve(host.c_str(), std::to_string(port)); - } catch (const std::exception& e) { - Application::Console().Write(e.what()); + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream).connect(results); + + // Perform the SSL handshake + stream.handshake(ssl::stream_base::client); + + // Set up an HTTP GET request message + http::request req { http::verb::get, target, version }; + req.set(http::field::host, host); + + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // Send the HTTP request to the remote host + http::write(stream, req); + + // This buffer is used for reading and must be persisted + beast::flat_buffer buffer; + + // Declare a container to hold the response + http::response res; + + // Receive the HTTP response + http::read(stream, buffer, res); + + // Gracefully close the stream + beast::error_code ec; + stream.shutdown(ec); + if (ec == net::error::eof) { + // Rationale: + // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error + ec = {}; + } + + if (status) { + *status = res.base().result_int(); + } + + if (ec) + throw beast::system_error { ec }; + + // If we get here then the connection is closed gracefully + return std::string(res.body()); + } catch (std::exception const& e) { + Application::Console().Write(__func__ + std::string(": ") + e.what()); return "-1"; } } diff --git a/src/main.cpp b/src/main.cpp index bc927e3..c1b255b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "TSentry.h" #include "Common.h" +#include "CustomAssert.h" #include "Http.h" #include "TConfig.h" #include "THeartbeatThread.h" @@ -39,6 +40,8 @@ void UnixSignalHandler(int sig) { // global, yes, this is ugly, no, it cant be done another way TSentry Sentry { SECRET_SENTRY_URL }; +#include + int main(int argc, char** argv) try { #ifdef __unix #if DEBUG @@ -55,9 +58,18 @@ int main(int argc, char** argv) try { bool Shutdown = false; Application::RegisterShutdownHandler([&Shutdown] { Shutdown = true; }); + Assert(!Application::IsOutdated(std::array { 1, 0, 0 }, std::array { 1, 0, 0 })); + Assert(!Application::IsOutdated(std::array { 1, 0, 1 }, std::array { 1, 0, 0 })); + Assert(Application::IsOutdated(std::array { 1, 0, 0 }, std::array { 1, 0, 1 })); + Assert(Application::IsOutdated(std::array { 1, 0, 0 }, std::array { 1, 1, 0 })); + Assert(Application::IsOutdated(std::array { 1, 0, 0 }, std::array { 2, 0, 0 })); + Assert(!Application::IsOutdated(std::array { 2, 0, 0 }, std::array { 1, 0, 1 })); + TServer Server(argc, argv); TConfig Config; + Application::CheckForUpdates(); + if (Config.Failed()) { info("Closing in 10 seconds"); std::this_thread::sleep_for(std::chrono::seconds(10));