Compare commits

..

11 Commits

Author SHA1 Message Date
Tixx d3daa6621c Add delay between handshake attempts 2026-08-12 04:26:44 +02:00
Tixx a9faac25fc Bump version to v2.8.1 2026-08-11 22:19:49 +02:00
Tixx f532e58ee0 avoid exposing game to partially written mod files during first download (#250)
This avoids mod loading failure messages caused by partially copied mod
files after initial download, the same way it is already avoided while
loading files from cache.

Addresses some instances of
https://github.com/BeamMP/BeamMP-Launcher/issues/91

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2026-08-02 22:22:48 +02:00
Katharine Chui 8b4fefd518 avoid exposing game to partially written mod files during first download
This avoids mod loading failure messages caused by partially
copied mod files after initial download, the same way it is
already avoided while loading files from cache.
2026-08-02 18:52:26 +02:00
Tixx 5fc8f580f4 Fix download size int casting (#244)
Fixed an int casting issue that caused weird download failures, improve
error handling and added missing return statement which unintentionally
forwarded data to the game (without the socket even being there)

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2026-08-02 13:54:54 +02:00
Tixx 6334a0a915 prevent GameSend send() call on invalid socket (#240)
prevents WSAENOTSOCK by not allowing a `send()` call on an
invalid/closed socket when the game client isn't connected

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
2026-08-02 13:53:30 +02:00
Tixx 44211e7235 Fix incorrect string_view usage in log message
and I somehow forgot the semicolon
2026-08-02 13:18:59 +02:00
Tixx 901b7df809 Log debug message on GameSend connection check 2026-08-02 13:06:33 +02:00
Tixx a6e3db6b08 Merge branch 'master' into download-fix 2026-08-02 13:04:25 +02:00
Tixx eeca50b37e Fix download size int casting 2026-04-21 17:14:42 +02:00
galacticlemonade 2a92a08587 guard GameSend when game is not connected
prevents WSAENOTSOCK
2026-04-08 15:05:58 -06:00
14 changed files with 63 additions and 131 deletions
+1 -1
View File
@@ -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" "include/Network")
target_include_directories(${PROJECT_NAME} PRIVATE "include")
+3 -3
View File
@@ -11,9 +11,9 @@
#include <string>
class HTTP {
public:
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 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 ProgressBar(size_t c, size_t t);
static void StartProxy();
public:
-22
View File
@@ -1,22 +0,0 @@
/*
Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
Licensed under AGPL-3.0 (or later), see <https://www.gnu.org/licenses/>.
SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <vector>
#include <string>
#include <array>
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<std::string, 2> mValidTLDs {"beammp.com", "beammp.net"};
};
-1
View File
@@ -20,7 +20,6 @@ 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;
-3
View File
@@ -49,9 +49,6 @@ void ParseConfig(const nlohmann::json& d) {
deleteDuplicateMods = d["DeleteDuplicateMods"].get<bool>();
}
if (d.contains("Region") && d["Region"].is_string()) {
options.region = d["Region"].get<std::string>();
}
}
void ConfigInit() {
+1 -3
View File
@@ -5,7 +5,6 @@
*/
#include "Http.h"
#include "RegionHandler.h"
#include "Network/network.hpp"
#include "Security/Init.h"
#include "Utils.h"
@@ -227,8 +226,7 @@ void Parse(std::string Data, SOCKET CSocket) {
TCPTerminate = true;
Data.clear();
futures.push_back(std::async(std::launch::async, []() {
std::string resp = HTTP::Get("https://backend.beammp.com/servers-info", true);
CoreSend("B" + resp);
CoreSend("B" + HTTP::Get("https://backend.beammp.com/servers-info"));
}));
}
break;
+5
View File
@@ -51,6 +51,10 @@ int KillSocket(uint64_t Dead) {
}
void GameSend(std::string_view Data) {
if (!GConnected) {
debug("Tried to call GameSend but socket was not connected. Data: " + std::string(Data));
return;
}
static std::mutex Lock;
std::scoped_lock Guard(Lock);
auto ToSend = Utils::PrependHeader<std::string_view>(Data);
@@ -195,6 +199,7 @@ void ParserAsync(std::string_view Data) {
return;
case 'U':
magic = Data.substr(1);
return;
default:
break;
}
+6 -36
View File
@@ -6,8 +6,6 @@
#include "Http.h"
#include "RegionHandler.h"
#include "Options.h"
#include <Logger.h>
#include <Network/network.hpp>
#include <Startup.h>
@@ -31,12 +29,9 @@ static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void*
}
bool HTTP::isDownload = false;
std::string HTTP::Get(std::string IP, const bool& redirect) {
std::string HTTP::Get(const std::string& IP) {
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];
@@ -51,18 +46,7 @@ std::string HTTP::Get(std::string IP, const bool& redirect) {
if (res != CURLE_OK) {
error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res)));
error("Curl error: " + std::string(errbuf));
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 "";
}
return "";
}
} else {
error("Curl easy init failed");
@@ -71,12 +55,9 @@ std::string HTTP::Get(std::string IP, const bool& redirect) {
return Ret;
}
std::string HTTP::Post(std::string IP, const std::string& Fields, const bool& redirect) {
std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
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];
@@ -98,18 +79,7 @@ std::string HTTP::Post(std::string IP, const std::string& Fields, const bool& re
if (res != CURLE_OK) {
error("POST to " + IP + " failed: " + std::string(curl_easy_strerror(res)));
error("Curl error: " + std::string(errbuf));
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 "";
}
return "";
}
} else {
error("Curl easy init failed");
@@ -118,12 +88,12 @@ std::string HTTP::Post(std::string IP, const std::string& Fields, const bool& re
return Ret;
}
bool HTTP::Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash, const bool& redirect) {
bool HTTP::Download(const std::string& IP, const beammp_fs_string& Path, const std::string& Hash) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
info("Downloading an update (this may take a while)");
std::string Ret = Get(IP, redirect);
std::string Ret = Get(IP);
if (Ret.empty()) {
error("Download failed");
-39
View File
@@ -1,39 +0,0 @@
/*
Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
Licensed under AGPL-3.0 (or later), see <https://www.gnu.org/licenses/>.
SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include "RegionHandler.h"
#include "Logger.h"
#include "Options.h"
#include <regex>
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
}
+36 -5
View File
@@ -163,7 +163,12 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
int i = 0;
do {
// receive at most some MB at a time
int Len = std::min(int(Size - Rcv), 1 * 1024 * 1024);
uint64_t Len = std::min<uint64_t>((Size - Rcv), 1 * 1024 * 1024);
if (Len == 0) {
error("Download size miscalculation");
break;
}
int Temp = RecvWaitAll(Sock, &File[Rcv], Len);
if (Temp == -1 || Temp == 0) {
debug("Recv returned: " + std::to_string(Temp));
@@ -302,7 +307,7 @@ void InvalidResource(const std::string& File) {
}
struct ModInfo {
static std::pair<bool, std::vector<ModInfo>> ParseModInfosFromPacket(const std::string& packet) {
static std::optional<std::pair<bool, std::vector<ModInfo>>> ParseModInfosFromPacket(const std::string& packet) {
bool success = false;
std::vector<ModInfo> modInfos;
try {
@@ -323,6 +328,12 @@ struct ModInfo {
modInfo.Protected = entry["protected"];
}
if (auto fsFile = std::filesystem::path(modInfo.FileName);
!fsFile.has_filename() || fsFile.filename().string() != modInfo.FileName ||
!fsFile.filename().has_extension() || fsFile.filename().extension() != ".zip"){
return std::nullopt;
}
modInfos.push_back(modInfo);
success = true;
}
@@ -578,8 +589,17 @@ void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<Mo
c = ::tolower(c);
}
#endif
auto name = std::filesystem::path(GetGamePath()) / "mods/multiplayer" / FName;
auto tmp_name = name;
tmp_name += ".tmp";
fs::copy_file(PathToSaveTo, std::filesystem::path(GetGamePath()) / "mods/multiplayer" / FName, fs::copy_options::overwrite_existing);
std::error_code ec;
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing, ec);
if (ec) {
error(beammp_wide("Error in copy_file during download of ") + beammp_fs_string(PathToSaveTo) + beammp_wide(": ") + Utils::ToWString(ec.message()));
break;
}
fs::rename(tmp_name, name);
UpdateModUsage(FName);
}
WaitForConfirm();
@@ -602,7 +622,13 @@ void SyncResources(SOCKET Sock) {
if (Ret.starts_with("R")) {
debug("This server is likely outdated, not trying to parse new mod info format");
} else {
auto [success, modInfo] = ModInfo::ParseModInfosFromPacket(Ret);
auto ParsedInfo = ModInfo::ParseModInfosFromPacket(Ret);
if (!ParsedInfo.has_value()) {
error("Invalid mod info");
Terminate = true;
return;
}
auto [success, modInfo] = ParsedInfo.value();
if (success) {
NewSyncResources(Sock, Ret, modInfo);
@@ -738,7 +764,12 @@ void SyncResources(SOCKET Sock) {
}
#endif
fs::copy_file(PathToSaveTo, GetGamePath() / beammp_wide("mods/multiplayer") / Utils::ToWString(FName), fs::copy_options::overwrite_existing);
auto name = GetGamePath() / beammp_wide("mods/multiplayer") / Utils::ToWString(FName);
auto tmp_name = name;
tmp_name += L".tmp";
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing);
fs::rename(tmp_name, name);
UpdateModUsage(FN->substr(pos));
}
WaitForConfirm();
+6 -2
View File
@@ -7,6 +7,7 @@
#include "Network/network.hpp"
#include "Zlib/Compressor.h"
#include <stdexcept>
#include <thread>
#if defined(_WIN32)
#include <ws2tcpip.h>
@@ -93,9 +94,12 @@ void UDPClientMain(const std::string& IP, int Port) {
ToServer->sin_port = htons(Port);
inet_pton(AF_INET, IP.c_str(), &ToServer->sin_addr);
UDPSock = socket(AF_INET, SOCK_DGRAM, 0);
if (!magic.empty())
for (int i = 0; i < 10; i++)
if (!magic.empty()) {
for (int i = 0; i < 10; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
UDPSend(magic);
}
}
GameSend("P" + std::to_string(ClientID));
TCPSend("H", TCPSock);
UDPSend("p");
-7
View File
@@ -92,13 +92,6 @@ 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;
-2
View File
@@ -6,8 +6,6 @@
#include "Http.h"
#include "Options.h"
#include "RegionHandler.h"
#include "Logger.h"
#include <filesystem>
#include <fstream>
+5 -7
View File
@@ -24,7 +24,6 @@
#include "Security/Init.h"
#include "Startup.h"
#include "Utils.h"
#include "RegionHandler.h"
#include "hashpp.h"
#include <filesystem>
#include <fstream>
@@ -87,7 +86,7 @@ std::string GetVer() {
return "2.8";
}
std::string GetPatch() {
return ".0";
return ".1";
}
beammp_fs_string GetEP(const beammp_fs_char* P) {
@@ -333,7 +332,8 @@ 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,13 +357,11 @@ void CheckForUpdates(const std::string& CV) {
#else
info("Downloading Launcher update " + LatestHash);
std::wstring DownloadLocation = GetBP() / (beammp_wide("new_") + GetEN());
bool downloadSuccess = false;
downloadSuccess = HTTP::Download(
if (HTTP::Download(
"https://backend.beammp.com/builds/launcher?download=true"
"&pk="
+ PublicKey + "&branch=" + Branch,
DownloadLocation, LatestHash);
if (downloadSuccess) {
DownloadLocation, LatestHash)) {
if (!VerifySignature(DownloadLocation) || !CheckThumbprint(DownloadLocation)) {
std::error_code ec;
fs::remove(DownloadLocation, ec);