diff --git a/CMakeLists.txt b/CMakeLists.txt index 67e666ef..9d54c039 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,4 +37,4 @@ else(WIN32) #MINGW set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -s --static") target_link_libraries(${PROJECT_NAME} ssl crypto ws2_32 ssp crypt32 z CURL::libcurl) endif(WIN32) -target_include_directories(${PROJECT_NAME} PRIVATE "include") +target_include_directories(${PROJECT_NAME} PRIVATE "include" "include/Network") diff --git a/include/Http.h b/include/Http.h index 1e755906..6d013415 100644 --- a/include/Http.h +++ b/include/Http.h @@ -11,9 +11,9 @@ #include class HTTP { public: - static bool Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash); - static std::string Post(const std::string& IP, const std::string& Fields); - static std::string Get(const std::string& IP); + static bool Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash, const bool& redirect = true); + static std::string Post(std::string IP, const std::string& Fields, const bool& redirect = true); + static std::string Get(std::string IP, const bool& redirect = true); static bool ProgressBar(size_t c, size_t t); static void StartProxy(); public: diff --git a/include/Network/RegionHandler.h b/include/Network/RegionHandler.h new file mode 100644 index 00000000..45f34cd5 --- /dev/null +++ b/include/Network/RegionHandler.h @@ -0,0 +1,22 @@ +/* + Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors. + Licensed under AGPL-3.0 (or later), see . + SPDX-License-Identifier: AGPL-3.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class RegionHandler final { +public: + RegionHandler() = delete; + static void TopLevelDomainFailed(); + static std::string RegionToTopLevelDomain(); + static std::string RedirectURL(const std::string& URL); +private: + static inline unsigned int mRegionIndex { 0 }; + const static inline std::array mValidTLDs {"beammp.com", "beammp.net"}; +}; diff --git a/include/Options.h b/include/Options.h index da56051b..26fd1316 100644 --- a/include/Options.h +++ b/include/Options.h @@ -20,6 +20,7 @@ struct Options { bool no_update = false; bool no_launch = false; const char* user_path = nullptr; + std::string region; const char **game_arguments = nullptr; int game_arguments_length = 0; const char** argv = nullptr; diff --git a/src/Config.cpp b/src/Config.cpp index d7e41fa5..d244f85d 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -49,6 +49,9 @@ void ParseConfig(const nlohmann::json& d) { deleteDuplicateMods = d["DeleteDuplicateMods"].get(); } + if (d.contains("Region") && d["Region"].is_string()) { + options.region = d["Region"].get(); + } } void ConfigInit() { diff --git a/src/Network/Core.cpp b/src/Network/Core.cpp index 8dddfdd8..84e25b6b 100644 --- a/src/Network/Core.cpp +++ b/src/Network/Core.cpp @@ -5,6 +5,7 @@ */ #include "Http.h" +#include "RegionHandler.h" #include "Network/network.hpp" #include "Security/Init.h" #include "Utils.h" @@ -226,7 +227,8 @@ void Parse(std::string Data, SOCKET CSocket) { TCPTerminate = true; Data.clear(); futures.push_back(std::async(std::launch::async, []() { - CoreSend("B" + HTTP::Get("https://backend.beammp.com/servers-info")); + std::string resp = HTTP::Get("https://backend.beammp.com/servers-info", true); + CoreSend("B" + resp); })); } break; diff --git a/src/Network/Http.cpp b/src/Network/Http.cpp index 0b8489ab..001a1e8e 100644 --- a/src/Network/Http.cpp +++ b/src/Network/Http.cpp @@ -6,6 +6,8 @@ #include "Http.h" +#include "RegionHandler.h" +#include "Options.h" #include #include #include @@ -29,9 +31,12 @@ static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* } bool HTTP::isDownload = false; -std::string HTTP::Get(const std::string& IP) { +std::string HTTP::Get(std::string IP, const bool& redirect) { std::string Ret; static thread_local CURL* curl = curl_easy_init(); + if (redirect) { + IP = RegionHandler::RedirectURL(IP); + } if (curl) { CURLcode res; char errbuf[CURL_ERROR_SIZE]; @@ -46,7 +51,18 @@ std::string HTTP::Get(const std::string& IP) { if (res != CURLE_OK) { error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res))); error("Curl error: " + std::string(errbuf)); - return ""; + if (!redirect) { + return ""; + } + RegionHandler::TopLevelDomainFailed(); + IP = RegionHandler::RedirectURL(IP); + curl_easy_setopt(curl, CURLOPT_URL, IP.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res))); + error("Curl error: " + std::string(errbuf)); + return ""; + } } } else { error("Curl easy init failed"); @@ -55,9 +71,12 @@ std::string HTTP::Get(const std::string& IP) { return Ret; } -std::string HTTP::Post(const std::string& IP, const std::string& Fields) { +std::string HTTP::Post(std::string IP, const std::string& Fields, const bool& redirect) { std::string Ret; static thread_local CURL* curl = curl_easy_init(); + if (redirect) { + IP = RegionHandler::RedirectURL(IP); + } if (curl) { CURLcode res; char errbuf[CURL_ERROR_SIZE]; @@ -79,7 +98,18 @@ std::string HTTP::Post(const std::string& IP, const std::string& Fields) { if (res != CURLE_OK) { error("POST to " + IP + " failed: " + std::string(curl_easy_strerror(res))); error("Curl error: " + std::string(errbuf)); - return ""; + if (!redirect) { + return ""; + } + RegionHandler::TopLevelDomainFailed(); + IP = RegionHandler::RedirectURL(IP); + curl_easy_setopt(curl, CURLOPT_URL, IP.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res))); + error("Curl error: " + std::string(errbuf)); + return ""; + } } } else { error("Curl easy init failed"); @@ -88,12 +118,12 @@ std::string HTTP::Post(const std::string& IP, const std::string& Fields) { return Ret; } -bool HTTP::Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash) { +bool HTTP::Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash, const bool& redirect) { static std::mutex Lock; std::scoped_lock Guard(Lock); info("Downloading an update (this may take a while)"); - std::string Ret = Get(IP); + std::string Ret = Get(IP, redirect); if (Ret.empty()) { error("Download failed"); diff --git a/src/Network/RegionHandler.cpp b/src/Network/RegionHandler.cpp new file mode 100644 index 00000000..04481b8c --- /dev/null +++ b/src/Network/RegionHandler.cpp @@ -0,0 +1,39 @@ +/* + Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors. + Licensed under AGPL-3.0 (or later), see . + SPDX-License-Identifier: AGPL-3.0-or-later +*/ + +#include "RegionHandler.h" +#include "Logger.h" +#include "Options.h" + +#include + +void RegionHandler::TopLevelDomainFailed() +{ + info("Top level domain of " + mValidTLDs[mRegionIndex % mValidTLDs.size()] + " didn't respond correctly , changing domain to " + mValidTLDs[(mRegionIndex + 1) % mValidTLDs.size()]); + mRegionIndex++; +} + +std::string RegionHandler::RegionToTopLevelDomain() +{ + static bool isDeveloperRegion = options.region == "Developer"; + if (isDeveloperRegion) { + return "beammp.dev"; + } + return mValidTLDs[mRegionIndex % mValidTLDs.size()]; // Global +} + +std::string RegionHandler::RedirectURL(const std::string &URL) +{ + std::regex link_pattern(R"(^(https:\/\/.*)beammp\.com(\/.*)?$)"); + std::smatch link_match; + if (std::regex_search(URL, link_match, link_pattern) && link_match.position() == 0) { + //TLD matched beammp.com + std::string before = link_match[1].str(); // "https://..." up to beammp + std::string after = link_match[2].matched ? link_match[2].str() : ""; // "/path" or "" + return before + RegionToTopLevelDomain() + after; + } + return URL; //if it didn't match, just return the unmodified URL +} diff --git a/src/Options.cpp b/src/Options.cpp index 0f08a83f..90135482 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -92,6 +92,13 @@ void InitOptions(int argc, const char *argv[], Options &options) { } options.user_path = argv[i + 1]; i++; + } else if (argument == "--region") { + if (i + 1 >= argc) { + error("You must specify a region after the `--region` argument"); + } + options.region = argv[i + 1]; + info("Set the region to: " + options.region); + i++; } else if (argument == "--" || argument == "--game") { options.game_arguments = &argv[i + 1]; options.game_arguments_length = argc - i - 1; diff --git a/src/Security/Login.cpp b/src/Security/Login.cpp index 4c14d41b..7fe6fe01 100644 --- a/src/Security/Login.cpp +++ b/src/Security/Login.cpp @@ -6,6 +6,8 @@ #include "Http.h" +#include "Options.h" +#include "RegionHandler.h" #include "Logger.h" #include #include diff --git a/src/Startup.cpp b/src/Startup.cpp index 47ca46a8..a2618a41 100644 --- a/src/Startup.cpp +++ b/src/Startup.cpp @@ -24,6 +24,7 @@ #include "Security/Init.h" #include "Startup.h" #include "Utils.h" +#include "RegionHandler.h" #include "hashpp.h" #include #include @@ -332,8 +333,7 @@ bool VerifySignature(const std::filesystem::path& filePath) void CheckForUpdates(const std::string& CV) { std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/launcher?branch=" + Branch + "&pk=" + PublicKey); - std::string LatestVersion = HTTP::Get( - "https://backend.beammp.com/version/launcher?branch=" + Branch + "&pk=" + PublicKey); + std::string LatestVersion = HTTP::Get("https://backend.beammp.com/version/launcher?branch=" + Branch + "&pk=" + PublicKey); std::regex sha256_pattern(R"(^[a-fA-F0-9]{64}$)"); std::smatch match; @@ -357,11 +357,13 @@ void CheckForUpdates(const std::string& CV) { #else info("Downloading Launcher update " + LatestHash); std::wstring DownloadLocation = GetBP() / (beammp_wide("new_") + GetEN()); - if (HTTP::Download( + bool downloadSuccess = false; + downloadSuccess = HTTP::Download( "https://backend.beammp.com/builds/launcher?download=true" "&pk=" + PublicKey + "&branch=" + Branch, - DownloadLocation, LatestHash)) { + DownloadLocation, LatestHash); + if (downloadSuccess) { if (!VerifySignature(DownloadLocation) || !CheckThumbprint(DownloadLocation)) { std::error_code ec; fs::remove(DownloadLocation, ec);