76 Commits

Author SHA1 Message Date
Tixx
4f03d21dea Move mod caching directory log 2024-11-14 11:12:19 +01:00
Lion
00bd5be4d0 add PR template 2024-11-13 16:20:54 +01:00
Tixx
dff2f2712b Bump version 2024-11-07 22:12:20 +01:00
Lion
3effe0d4de log zlib error message and regex fix (#146) 2024-11-07 22:05:40 +01:00
Tixx
d58ff960ec Fix github regex 2024-11-07 21:39:18 +01:00
Tixx
f67f8573e0 Log zlib error messages 2024-11-07 21:36:38 +01:00
Lion
8a8e0be1a1 Print message from auth (#141) 2024-11-05 10:32:39 +01:00
Tixx
e0041666ca Clarify and change auth message log 2024-11-05 10:26:10 +01:00
Tixx
ed686333ec Print message from auth 2024-11-05 10:15:10 +01:00
Lion
8938fd84ea Add beammp.gg to the list of allowed links (#143) 2024-11-02 23:29:40 +01:00
Tixx
bd4c9c34a9 Add BeamMP github to the list of allowed links 2024-11-02 22:33:26 +01:00
Tixx
8519e279a7 Add beammp.gg to the list of allowed links 2024-11-02 22:18:46 +01:00
Lion Kortlepel
54895eb1b0 bump version 2024-11-01 12:53:55 +01:00
Lion
1423c1193b Speed up response times by waiting for http requests on another thread. (#137)
If, for example, the client requests the serverlist multiple times and
then tries to login the launcher will first wait for those requests to
finish. Thereby putting the other core communication (such as login) on
hold.
2024-11-01 12:13:21 +01:00
Lion
288e76594d Fix port cli argument (#142)
Fixes --port and -p by proccessing the config file before the cli
arguments, before it would first set --port because it was passed and
then overwrite it with the value from the config. Also removed some
useless code related to cli args.
2024-11-01 12:11:26 +01:00
Tixx
4fdc3c4031 Fix --port 2024-10-20 16:59:00 +02:00
Tixx
708da44fec Remove unused code 2024-10-20 16:57:47 +02:00
Tixx
6b6e304cfd Switch to std::async 2024-10-18 19:23:53 +02:00
Tixx
06cb366bb5 Add mutex to CoreSend 2024-10-16 23:12:02 +02:00
Tixx
0b35f0484f put blocking http requests on another thread 2024-10-16 23:12:02 +02:00
Lion
9dbbd8298d Switch to only timeout on connection (#140) 2024-10-15 19:32:55 +02:00
Tixx
ca9dd1ae75 Switch to only timeout on connection 2024-10-14 20:29:19 +02:00
Lion
9ebd218856 Fix empty modlist (#136)
This PR fixes the launcher getting confused when the server sends an
empty mod list using the new downloading system.
Related server PR: https://github.com/BeamMP/BeamMP-Server/pull/377
2024-10-12 22:10:47 +02:00
Tixx
d9874ce70e Make return from parsemodinfo look better 2024-10-12 21:12:12 +02:00
Tixx
423519f31e Only listen on localhost ipv4 (#134)
This avoids the firewall popup on windows.
2024-10-12 20:58:29 +02:00
Tixx
3f12bb757a Mod info logs and check for old format 2024-10-10 21:35:27 +02:00
Lion Kortlepel
7d52e44434 only listen on localhost ipv4 2024-10-10 16:14:16 +02:00
Tixx
4fbd25b551 Handle new modlist being empty but still valid 2024-10-09 19:41:38 +02:00
Tixx
3cf1a2e51b Add mod info debug log 2024-10-09 19:39:27 +02:00
Lion Kortlepel
49874fd633 Revert "remove 'D' socket initialization code"
This reverts commit 6a23518eff.
2024-10-09 18:00:43 +02:00
Lion Kortlepel
6a23518eff remove 'D' socket initialization code 2024-10-09 17:36:54 +02:00
Lion Kortlepel
3297b3e62e fix not recognizing empty mod lists on new mod list 2024-10-09 17:35:50 +02:00
Lion Kortlepel
76cfc47a2f log invocation 2024-10-07 00:43:25 +02:00
Lion Kortlepel
7b59cb6f87 fix various commandline argument related things 2024-10-07 00:33:43 +02:00
Lion Kortlepel
0eba745d4c remove silly license 2024-10-07 00:33:29 +02:00
Lion
259b21502e Add command-line options (#90)
This PR adds command-line options as outlined in #74 

Closes #74
2024-10-06 23:49:47 +02:00
Tixx
afac729505 Ixmplement game arguments for linux 2024-10-06 15:56:48 +02:00
Lion
f57ebb7a92 follow HTTP redirects (#133)
Follow HTTP redirects on HTTP Get and Post functions
2024-10-06 15:20:49 +02:00
snepsnepsnep
ace96b7e33 follow HTTP redirects 2024-10-05 23:45:50 +02:00
Lion Kortlepel
fcb51adcb8 bump version 2024-10-05 21:05:02 +02:00
Tixx
0c53ff4cd4 Pass game arguments to beamng on windows 2024-10-05 18:34:39 +02:00
Tixx
68a4d64387 Fix linux relauch 2024-10-05 18:08:25 +02:00
Tixx
5bdd8c11da Fix relaunch 2024-10-05 18:01:47 +02:00
Tixx
47681cda50 Let user know about update even if --no-update was specified 2024-10-05 18:01:36 +02:00
Tixx
c99fecfa1c Fix debug log 2024-10-05 17:26:14 +02:00
Tixx
d26e0320e4 Add back support for old dev argument with warning 2024-10-05 17:26:14 +02:00
Tixx
57422a6105 Add optional dev config value 2024-10-05 17:20:40 +02:00
Tixx
467c8dc584 Add no-update flag 2024-10-05 17:20:40 +02:00
Tixx
2ddb576e72 Log core port 2024-10-05 17:20:40 +02:00
Tixx
e242057583 Improve port cli flag 2024-10-05 17:20:40 +02:00
Tixx
06686688fc Move console clear to main to avoid clearing logs 2024-10-05 17:20:40 +02:00
Tixx
aca61886d0 add command-line options 2024-10-05 17:20:40 +02:00
Lion
768f11f6ec Add mod hashing, improve download protocol (#129) 2024-10-04 23:36:57 +02:00
Lion
7944e9dbe8 Switch to curl for Get and Post (#132)
Because we can. I got a segfault while testing but then it didn't happen
again, so I'm tempted to call it done.
2024-10-04 23:23:49 +02:00
Lion Kortlepel
0c68f91fb2 remove debug print 2024-10-04 23:22:50 +02:00
Lion
b8fdbc4ed9 Fix GetGamePath (#130)
Previously, the registry was used to get the local appdata folder for
the user folder. I've switched this over to a windows api function which
fixes some cases where the launcher wouldn't be able to find the appdata
folder in the registry.
2024-10-04 23:12:56 +02:00
Lion Kortlepel
85908e42d5 fix download code, error checking 2024-10-04 23:12:23 +02:00
Lion Kortlepel
5c77e60f29 remove mis-merged code 2024-10-04 23:04:30 +02:00
Lion Kortlepel
c74455e0fe switch to curl for Get and Post 2024-10-04 22:59:29 +02:00
Lion
dc13e4a03c remove extra return 2024-10-04 14:13:04 +02:00
Tixx
1d7eb64fe0 Get localappdata via winapi instead of registry 2024-10-03 22:42:49 +02:00
Lion Kortlepel
1676d4174e make mods not keep the hash when copying them 2024-09-29 02:36:41 +02:00
Lion Kortlepel
ad468a8971 remove debug prints 2024-09-29 02:04:39 +02:00
Lion Kortlepel
d3805f2cfd fix mod deleting misnamed mods 2024-09-29 01:57:15 +02:00
Lion Kortlepel
9f1cc15b15 fix bugs with new download 2024-09-29 01:15:57 +02:00
Lion Kortlepel
c0fb4e4ad6 implement support for new mod hashing and download 2024-09-29 00:33:15 +02:00
Lion
7600372ca1 Fix linux executable name after BNG0.33.2 (#126) 2024-09-28 16:51:02 +02:00
Lion
54cd5b5e0e Add additional SSL Verify logging (#127) 2024-09-28 16:50:30 +02:00
Mackenzie
ede6fcd7dd log SSL errors 2024-09-27 20:33:14 +01:00
Mackenzie
eaeacbd8de log non-200 status codes 2024-09-27 20:23:28 +01:00
O1LER
0ffed00bcb rename linux executable for bng0.33.2 2024-09-27 17:48:46 +02:00
Lion
c0c3d6b30e Add download speed to UI (#125) 2024-09-24 21:59:01 +02:00
Lion
9c59a83f04 turn off stdout, stderr of the game on linux (#124) 2024-09-24 21:58:37 +02:00
Lion Kortlepel
95436cb073 turn off stdout, stderr of the game on linux 2024-09-24 21:56:55 +02:00
Lion Kortlepel
cbb5502a40 send download speed to game UI, bump version to 2.1.4 2024-09-24 21:50:09 +02:00
Lion Kortlepel
d6dfe85f69 add download speed to ingame ui 2024-09-24 21:10:10 +02:00
21 changed files with 861 additions and 456 deletions

6
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,6 @@
Please replace this text <-> with your PR description and leave the below declarations intact.
---
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.

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ Resources/
bin/
compile_commands.json
key
out/

View File

@@ -17,6 +17,7 @@ add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT)
file(GLOB source_files "src/*.cpp" "src/*/*.cpp" "src/*/*.hpp" "include/*.h" "include/*/*.h" "include/*/*/*.h" "include/*.hpp" "include/*/*.hpp" "include/*/*/*.hpp")
find_package(httplib CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(CURL REQUIRED)
add_executable(${PROJECT_NAME} ${source_files})
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "BeamMP-Launcher")
@@ -25,15 +26,15 @@ if (WIN32)
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE
ZLIB::ZLIB OpenSSL::SSL OpenSSL::Crypto ws2_32 httplib::httplib nlohmann_json::nlohmann_json)
ZLIB::ZLIB OpenSSL::SSL OpenSSL::Crypto ws2_32 httplib::httplib nlohmann_json::nlohmann_json CURL::libcurl)
elseif (LINUX)
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE
ZLIB::ZLIB OpenSSL::SSL OpenSSL::Crypto)
target_link_libraries(${PROJECT_NAME} PRIVATE
ZLIB::ZLIB OpenSSL::SSL OpenSSL::Crypto CURL::libcurl)
else(WIN32) #MINGW
add_definitions("-D_WIN32_WINNT=0x0600")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -s --static")
target_link_libraries(${PROJECT_NAME} ssl crypto ws2_32 ssp crypt32 z)
target_link_libraries(${PROJECT_NAME} ssl crypto ws2_32 ssp crypt32 z CURL::libcurl)
endif(WIN32)
target_include_directories(${PROJECT_NAME} PRIVATE "include")

View File

@@ -9,8 +9,8 @@ The launcher is the way we communitcate to outside the game, it does a few autom
In the root directory of the project,
1. `cmake -DCMAKE_BUILD_TYPE=Release . -B bin -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static`
2. `cmake --build bin --parallel --config Release`
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
## How to build - Debug
@@ -18,10 +18,4 @@ In the root directory of the project,
1. `cmake . -B bin -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static`
2. `cmake --build bin --parallel`
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.
Copyright (c) 2019-present Anonymous275.
BeamMP Launcher code is not in the public domain and is not free software.
One must be granted explicit permission by the copyright holder in order to modify or distribute any part of the source or binaries,
the only permission that has been granted is to use the software in its compiled form as distributed from the BeamMP.com website.
Anything else is prohibited. Modified works may not be published and have be upstreamed to the official repository.
Remember to change `C:/vcpkg` to wherever you have vcpkg installed.

View File

@@ -17,5 +17,4 @@ public:
static void StartProxy();
public:
static bool isDownload;
static inline bool SkipSslVerify = false;
};

View File

@@ -26,7 +26,6 @@ extern int ClientID;
extern int LastPort;
extern bool ModLoaded;
extern bool Terminate;
extern int DEFAULT_PORT;
extern uint64_t UDPSock;
extern uint64_t TCPSock;
extern std::string Branch;

24
include/Options.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <string>
struct Options {
#if defined(_WIN32)
std::string executable_name = "BeamMP-Launcher.exe";
#elif defined(__linux__)
std::string executable_name = "BeamMP-Launcher";
#endif
unsigned int port = 4444;
bool verbose = false;
bool no_download = false;
bool no_update = false;
bool no_launch = false;
const char **game_arguments = nullptr;
int game_arguments_length = 0;
const char** argv = nullptr;
int argc = 0;
};
void InitOptions(int argc, const char *argv[], Options &options);
extern Options options;

View File

@@ -10,12 +10,10 @@
#include <string>
#include <vector>
void InitLauncher(int argc, char* argv[]);
std::string GetEP(char* P = nullptr);
void InitLauncher();
std::string GetEP(const char* P = nullptr);
std::string GetGamePath();
std::string GetVer();
std::string GetPatch();
std::string GetEN();
void ConfigInit();
extern bool Dev;

View File

@@ -25,7 +25,7 @@ std::vector<char> Comp(std::span<const char> input) {
reinterpret_cast<const Bytef*>(input.data()),
static_cast<uLongf>(input.size()));
if (res != Z_OK) {
error("zlib compress() failed: " + std::to_string(res));
error("zlib compress() failed (code: " + std::to_string(res) + ", message: " + zError(res) + ")");
throw std::runtime_error("zlib compress() failed");
}
debug("zlib compressed " + std::to_string(input.size()) + " B to " + std::to_string(output_size) + " B");
@@ -52,7 +52,7 @@ std::vector<char> DeComp(std::span<const char> input) {
output_buffer.resize(output_buffer.size() * 2);
output_size = output_buffer.size();
} else if (res != Z_OK) {
error("zlib uncompress() failed: " + std::to_string(res));
error("zlib uncompress() failed (code: " + std::to_string(res) + ", message: " + zError(res) + ")");
throw std::runtime_error("zlib uncompress() failed");
} else if (res == Z_OK) {
break;

View File

@@ -8,6 +8,7 @@
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
#include "Options.h"
namespace fs = std::filesystem;
std::string Branch;
@@ -15,7 +16,7 @@ std::string CachingDirectory = "./Resources";
void ParseConfig(const nlohmann::json& d) {
if (d["Port"].is_number()) {
DEFAULT_PORT = d["Port"].get<int>();
options.port = d["Port"].get<int>();
}
// Default -1
// Release 1
@@ -29,7 +30,14 @@ void ParseConfig(const nlohmann::json& d) {
}
if (d.contains("CachingDirectory") && d["CachingDirectory"].is_string()) {
CachingDirectory = d["CachingDirectory"].get<std::string>();
info("Mod caching directory: " + CachingDirectory);
}
if (d.contains("Dev") && d["Dev"].is_boolean()) {
bool dev = d["Dev"].get<bool>();
options.verbose = dev;
options.no_download = dev;
options.no_launch = dev;
options.no_update = dev;
}
}

View File

@@ -8,6 +8,7 @@
#if defined(_WIN32)
#include <windows.h>
#include <shlobj.h>
#elif defined(__linux__)
#include "vdf_parser.hpp"
#include <pwd.h>
@@ -22,6 +23,7 @@
#include <Security/Init.h>
#include <filesystem>
#include <thread>
#include "Options.h"
unsigned long GamePID = 0;
#if defined(_WIN32)
@@ -40,17 +42,17 @@ std::string GetGamePath() {
Path = QueryKey(hKey, 4);
if (Path.empty()) {
sk = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders)";
openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey);
if (openRes != ERROR_SUCCESS) {
sk = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders)";
openRes = RegOpenKeyEx(HKEY_CURRENT_USER, sk, 0, KEY_ALL_ACCESS, &hKey);
Path = "";
char appDataPath[MAX_PATH];
HRESULT result = SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath);
if (SUCCEEDED(result)) {
Path = appDataPath;
}
if (openRes != ERROR_SUCCESS) {
if (Path.empty()) {
fatal("Cannot get Local Appdata directory");
}
Path = QueryKey(hKey, 5);
Path += "\\BeamNG.drive\\";
}
@@ -82,7 +84,14 @@ void StartGame(std::string Dir) {
std::string BaseDir = Dir; //+"\\Bin64";
// Dir += R"(\Bin64\BeamNG.drive.x64.exe)";
Dir += "\\BeamNG.drive.exe";
bSuccess = CreateProcessA(Dir.c_str(), nullptr, nullptr, nullptr, TRUE, 0, nullptr, BaseDir.c_str(), &si, &pi);
std::string gameArgs = "";
for (int i = 0; i < options.game_arguments_length; i++) {
gameArgs += " ";
gameArgs += options.game_arguments[i];
}
bSuccess = CreateProcessA(nullptr, (LPSTR)(Dir + gameArgs).c_str(), nullptr, nullptr, TRUE, 0, nullptr, BaseDir.c_str(), &si, &pi);
if (bSuccess) {
info("Game Launched!");
GamePID = pi.dwProcessId;
@@ -97,10 +106,20 @@ void StartGame(std::string Dir) {
#elif defined(__linux__)
void StartGame(std::string Dir) {
int status;
std::string filename = (Dir + "/BinLinux/BeamNG.x64");
char* argv[] = { filename.data(), NULL };
std::string filename = (Dir + "/BinLinux/BeamNG.drive.x64");
std::vector<const char*> argv;
argv.push_back(filename.data());
for (int i = 0; i < options.game_arguments_length; i++) {
argv.push_back(options.game_arguments[i]);
}
argv.push_back(nullptr);
pid_t pid;
int result = posix_spawn(&pid, filename.c_str(), NULL, NULL, argv, environ);
posix_spawn_file_actions_t spawn_actions;
posix_spawn_file_actions_init(&spawn_actions);
posix_spawn_file_actions_addclose(&spawn_actions, STDOUT_FILENO);
posix_spawn_file_actions_addclose(&spawn_actions, STDERR_FILENO);
int result = posix_spawn(&pid, filename.c_str(), &spawn_actions, nullptr, const_cast<char**>(argv.data()), environ);
if (result != 0) {
error("Failed to Launch the game! launcher closing soon");
@@ -116,7 +135,7 @@ void StartGame(std::string Dir) {
#endif
void InitGame(const std::string& Dir) {
if (!Dev) {
if (!options.no_launch) {
std::thread Game(StartGame, Dir);
Game.detach();
}

View File

@@ -12,6 +12,7 @@
#include <fstream>
#include <sstream>
#include <thread>
#include "Options.h"
std::string getDate() {
time_t tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
@@ -55,7 +56,7 @@ void info(const std::string& toPrint) {
}
void debug(const std::string& toPrint) {
std::string Print = getDate() + "[DEBUG] " + toPrint + "\n";
if (Dev) {
if (options.verbose) {
std::cout << Print;
}
addToLog(Print);

View File

@@ -30,11 +30,14 @@
#include <nlohmann/json.hpp>
#include <set>
#include <thread>
#include <mutex>
#include "Options.h"
#include <future>
extern int TraceBack;
std::set<std::string>* ConfList = nullptr;
bool TCPTerminate = false;
int DEFAULT_PORT = 4444;
bool Terminate = false;
bool LoginAuth = false;
std::string Username = "";
@@ -87,7 +90,11 @@ void StartSync(const std::string& Data) {
info("Connecting to server");
}
std::mutex sendMutex;
void CoreSend(std::string data) {
std::lock_guard lock(sendMutex);
if (CoreSocket != -1) {
int res = send(CoreSocket, (data + "\n").c_str(), int(data.size()) + 1, 0);
if (res < 0) {
@@ -97,7 +104,7 @@ void CoreSend(std::string data) {
}
bool IsAllowedLink(const std::string& Link) {
std::regex link_pattern(R"(https:\/\/(?:\w+)?(?:\.)?(?:beammp\.com|discord\.gg|patreon\.com\/BeamMP))");
std::regex link_pattern(R"(https:\/\/(?:\w+)?(?:\.)?(?:beammp\.com|beammp\.gg|github\.com\/BeamMP\/|discord\.gg|patreon\.com\/BeamMP))");
std::smatch link_match;
return std::regex_search(Link, link_match, link_pattern) && link_match.position() == 0;
}
@@ -110,11 +117,15 @@ void Parse(std::string Data, SOCKET CSocket) {
case 'A':
Data = Data.substr(0, 1);
break;
case 'B':
NetReset();
Terminate = true;
TCPTerminate = true;
Data = Code + HTTP::Get("https://backend.beammp.com/servers-info");
case 'B': {
NetReset();
Terminate = true;
TCPTerminate = true;
Data.clear();
auto future = std::async(std::launch::async, []() {
CoreSend("B" + HTTP::Get("https://backend.beammp.com/servers-info"));
});
}
break;
case 'C':
StartSync(Data);
@@ -210,7 +221,10 @@ void Parse(std::string Data, SOCKET CSocket) {
}
Data = "N" + Auth.dump();
} else {
Data = "N" + Login(Data.substr(Data.find(':') + 1));
auto future = std::async(std::launch::async, [data = std::move(Data)]() {
CoreSend("N" + Login(data.substr(data.find(':') + 1)));
});
Data.clear();
}
break;
case 'W':
@@ -226,12 +240,8 @@ void Parse(std::string Data, SOCKET CSocket) {
Data.clear();
break;
}
if (!Data.empty() && CSocket != -1) {
int res = send(CSocket, (Data + "\n").c_str(), int(Data.size()) + 1, 0);
if (res < 0) {
debug("(Core) send failed with error: " + std::to_string(WSAGetLastError()));
}
}
if (!Data.empty())
CoreSend(Data);
}
void GameHandler(SOCKET Client) {
CoreSocket = Client;
@@ -288,7 +298,7 @@ void localRes() {
ConfList = new std::set<std::string>;
}
void CoreMain() {
debug("Core Network on start!");
debug("Core Network on start! port: " + std::to_string(options.port));
SOCKET LSocket, CSocket;
struct addrinfo* res = nullptr;
struct addrinfo hints { };
@@ -306,7 +316,7 @@ void CoreMain() {
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
iRes = getaddrinfo(nullptr, std::to_string(DEFAULT_PORT).c_str(), &hints, &res);
iRes = getaddrinfo("127.0.0.1", std::to_string(options.port).c_str(), &hints, &res);
if (iRes) {
debug("(Core) addr info failed with error: " + std::to_string(iRes));
WSACleanup();

View File

@@ -26,6 +26,7 @@
#include <mutex>
#include <string>
#include <thread>
#include "Options.h"
std::chrono::time_point<std::chrono::high_resolution_clock> PingStart, PingEnd;
bool GConnected = false;
@@ -161,7 +162,7 @@ SOCKET SetupListener() {
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
iRes = getaddrinfo(nullptr, std::to_string(DEFAULT_PORT + 1).c_str(), &hints, &result);
iRes = getaddrinfo(nullptr, std::to_string(options.port + 1).c_str(), &hints, &result);
if (iRes != 0) {
error("(Proxy) info failed with error: " + std::to_string(iRes));
WSACleanup();

View File

@@ -1,308 +1,269 @@
// Copyright (c) 2019-present Anonymous275.
// BeamMP Launcher code is not in the public domain and is not free software.
// One must be granted explicit permission by the copyright holder in order to modify or distribute any part of the source or binaries.
// Anything else is prohibited. Modified works may not be published and have be upstreamed to the official repository.
///
/// Created by Anonymous275 on 7/18/2020
///
#include "Http.h"
#include <Logger.h>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <httplib.h>
#include <iostream>
#include <mutex>
#include <nlohmann/json.hpp>
#include <Startup.h>
#include <Network/network.hpp>
#include <Utils.h>
void WriteHttpDebug(const httplib::Client& client, const std::string& method, const std::string& target, const httplib::Result& result) try {
const std::filesystem::path folder = ".https_debug";
std::filesystem::create_directories(folder);
if (!std::filesystem::exists(folder / "WHAT IS THIS FOLDER.txt")) {
std::ofstream ignore { folder / "WHAT IS THIS FOLDER.txt" };
ignore << "This folder exists to help debug current issues with the backend. Do not share this folder with anyone but BeamMP staff. It contains detailed logs of any failed http requests." << std::endl;
}
const auto file = folder / (method + ".json");
// 1 MB limit
if (std::filesystem::exists(file) && std::filesystem::file_size(file) > 1'000'000) {
std::filesystem::rename(file, file.generic_string() + ".bak");
}
std::ofstream of { file, std::ios::app };
nlohmann::json js {
{ "utc", std::chrono::system_clock::now().time_since_epoch().count() },
{ "target", target },
{ "client_info", {
{ "openssl_verify_result", client.get_openssl_verify_result() },
{ "host", client.host() },
{ "port", client.port() },
{ "socket_open", client.is_socket_open() },
{ "valid", client.is_valid() },
} },
};
if (result) {
auto value = result.value();
js["result"] = {};
js["result"]["body"] = value.body;
js["result"]["status"] = value.status;
js["result"]["headers"] = value.headers;
js["result"]["version"] = value.version;
js["result"]["location"] = value.location;
js["result"]["reason"] = value.reason;
}
of << js.dump();
} catch (const std::exception& e) {
error(e.what());
}
bool HTTP::isDownload = false;
std::string HTTP::Get(const std::string& IP) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
auto pos = IP.find('/', 10);
httplib::Client cli(IP.substr(0, pos).c_str());
cli.set_connection_timeout(std::chrono::seconds(10));
cli.set_follow_location(true);
if (SkipSslVerify) {
debug("Skipping SSL server validation via --skip-ssl-verify");
cli.enable_server_certificate_verification(false);
}
auto res = cli.Get(IP.substr(pos).c_str(), ProgressBar);
std::string Ret;
if (res) {
if (res->status == 200) {
Ret = res->body;
} else {
WriteHttpDebug(cli, "GET", IP, res);
error("Failed to GET '" + IP + "': " + res->reason + ", ssl verify = " + std::to_string(cli.get_openssl_verify_result()));
}
} else {
if (isDownload) {
std::cout << "\n";
}
WriteHttpDebug(cli, "GET", IP, res);
error("HTTP Get failed on " + to_string(res.error()) + ", ssl verify = " + std::to_string(cli.get_openssl_verify_result()));
}
return Ret;
}
std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
auto pos = IP.find('/', 10);
httplib::Client cli(IP.substr(0, pos).c_str());
cli.set_connection_timeout(std::chrono::seconds(10));
if (SkipSslVerify) {
debug("Skipping SSL server validation via --skip-ssl-verify");
cli.enable_server_certificate_verification(false);
}
std::string Ret;
if (!Fields.empty()) {
httplib::Result res = cli.Post(IP.substr(pos).c_str(), Fields, "application/json");
if (res) {
if (res->status != 200) {
error(res->reason);
}
Ret = res->body;
} else {
WriteHttpDebug(cli, "POST", IP, res);
error("HTTP Post failed on " + to_string(res.error()) + ", ssl verify = " + std::to_string(cli.get_openssl_verify_result()));
}
} else {
httplib::Result res = cli.Post(IP.substr(pos).c_str());
if (res) {
if (res->status != 200) {
error(res->reason);
}
Ret = res->body;
} else {
WriteHttpDebug(cli, "POST", IP, res);
error("HTTP Post failed on " + to_string(res.error()) + ", ssl verify = " + std::to_string(cli.get_openssl_verify_result()));
}
}
if (Ret.empty())
return "-1";
else
return Ret;
}
bool HTTP::ProgressBar(size_t c, size_t t) {
if (isDownload) {
static double last_progress, progress_bar_adv;
progress_bar_adv = round(c / double(t) * 25);
std::cout << "\r";
std::cout << "Progress : [ ";
std::cout << round(c / double(t) * 100);
std::cout << "% ] [";
int i;
for (i = 0; i <= progress_bar_adv; i++)
std::cout << "#";
for (i = 0; i < 25 - progress_bar_adv; i++)
std::cout << ".";
std::cout << "]";
last_progress = round(c / double(t) * 100);
}
return true;
}
bool HTTP::Download(const std::string& IP, const std::string& Path) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
isDownload = true;
std::string Ret = Get(IP);
isDownload = false;
if (Ret.empty())
return false;
std::ofstream File(Path, std::ios::binary);
if (File.is_open()) {
File << Ret;
File.close();
std::cout << "\n";
info("Download Complete!");
} else {
error("Failed to open file directory: " + Path);
return false;
}
return true;
}
void set_headers(httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Request-Method", "POST, OPTIONS, GET");
res.set_header("Access-Control-Request-Headers", "X-API-Version");
}
void HTTP::StartProxy() {
std::thread proxy([&]() {
httplib::Server HTTPProxy;
httplib::Headers headers = {
{ "User-Agent", "BeamMP-Launcher/" + GetVer() + GetPatch() },
{ "Accept", "*/*" }
};
httplib::Client backend("https://backend.beammp.com");
httplib::Client forum("https://forum.beammp.com");
const std::string pattern = ".*";
auto handle_request = [&](const httplib::Request& req, httplib::Response& res) {
set_headers(res);
if (req.has_header("X-BMP-Authentication")) {
headers.emplace("X-BMP-Authentication", PrivateKey);
}
if (req.has_header("X-API-Version")) {
headers.emplace("X-API-Version", req.get_header_value("X-API-Version"));
}
const std::vector<std::string> path = Utils::Split(req.path, "/");
httplib::Result cli_res;
const std::string method = req.method;
std::string host = "";
if (!path.empty())
host = path[0];
if (host == "backend") {
std::string remaining_path = req.path.substr(std::strlen("/backend"));
if (method == "GET")
cli_res = backend.Get(remaining_path, headers);
else if (method == "POST")
cli_res = backend.Post(remaining_path, headers);
} else if (host == "avatar") {
bool error = false;
std::string username;
std::string avatar_size = "100";
if (path.size() > 1) {
username = path[1];
} else {
error = true;
}
if (path.size() > 2) {
try {
if (std::stoi(path[2]) > 0)
avatar_size = path[2];
} catch (std::exception&) {}
}
httplib::Result summary_res;
if (!error) {
summary_res = forum.Get("/u/" + username + ".json", headers);
if (!summary_res || summary_res->status != 200) {
error = true;
}
}
if (!error) {
try {
nlohmann::json d = nlohmann::json::parse(summary_res->body, nullptr, false); // can fail with parse_error
auto user = d.at("user"); // can fail with out_of_range
auto avatar_link_json = user.at("avatar_template"); // can fail with out_of_range
auto avatar_link = avatar_link_json.get<std::string>();
size_t start_pos = avatar_link.find("{size}");
if (start_pos != std::string::npos)
avatar_link.replace(start_pos, std::strlen("{size}"), avatar_size);
cli_res = forum.Get(avatar_link, headers);
} catch (std::exception&) {
error = true;
}
}
if (error) {
cli_res = forum.Get("/user_avatar/forum.beammp.com/user/0/0.png", headers);
}
} else {
res.set_content("Host not found", "text/plain");
return;
}
if (cli_res) {
res.set_content(cli_res->body, cli_res->get_header_value("Content-Type"));
} else {
res.set_content(to_string(cli_res.error()), "text/plain");
}
};
HTTPProxy.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) {
handle_request(req, res);
});
HTTPProxy.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
handle_request(req, res);
});
ProxyPort = HTTPProxy.bind_to_any_port("0.0.0.0");
debug("HTTP Proxy listening on port " + std::to_string(ProxyPort));
HTTPProxy.listen_after_bind();
});
proxy.detach();
}
// Copyright (c) 2019-present Anonymous275.
// BeamMP Launcher code is not in the public domain and is not free software.
// One must be granted explicit permission by the copyright holder in order to modify or distribute any part of the source or binaries.
// Anything else is prohibited. Modified works may not be published and have be upstreamed to the official repository.
///
/// Created by Anonymous275 on 7/18/2020
///
#include "Http.h"
#include <Logger.h>
#include <Network/network.hpp>
#include <Startup.h>
#include <Utils.h>
#include <cmath>
#include <curl/curl.h>
#include <curl/easy.h>
#include <filesystem>
#include <fstream>
#include <httplib.h>
#include <iostream>
#include <mutex>
#include <nlohmann/json.hpp>
void WriteHttpDebug(const httplib::Client& client, const std::string& method, const std::string& target, const httplib::Result& result) try {
const std::filesystem::path folder = ".https_debug";
std::filesystem::create_directories(folder);
if (!std::filesystem::exists(folder / "WHAT IS THIS FOLDER.txt")) {
std::ofstream ignore { folder / "WHAT IS THIS FOLDER.txt" };
ignore << "This folder exists to help debug current issues with the backend. Do not share this folder with anyone but BeamMP staff. It contains detailed logs of any failed http requests." << std::endl;
}
const auto file = folder / (method + ".json");
// 1 MB limit
if (std::filesystem::exists(file) && std::filesystem::file_size(file) > 1'000'000) {
std::filesystem::rename(file, file.generic_string() + ".bak");
}
std::ofstream of { file, std::ios::app };
nlohmann::json js {
{ "utc", std::chrono::system_clock::now().time_since_epoch().count() },
{ "target", target },
{ "client_info", {
{ "openssl_verify_result", client.get_openssl_verify_result() },
{ "host", client.host() },
{ "port", client.port() },
{ "socket_open", client.is_socket_open() },
{ "valid", client.is_valid() },
} },
};
if (result) {
auto value = result.value();
js["result"] = {};
js["result"]["body"] = value.body;
js["result"]["status"] = value.status;
js["result"]["headers"] = value.headers;
js["result"]["version"] = value.version;
js["result"]["location"] = value.location;
js["result"]["reason"] = value.reason;
}
of << js.dump();
} catch (const std::exception& e) {
error(e.what());
}
static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
std::string* Result = reinterpret_cast<std::string*>(userp);
std::string NewContents(reinterpret_cast<char*>(contents), size * nmemb);
*Result += NewContents;
return size * nmemb;
}
bool HTTP::isDownload = false;
std::string HTTP::Get(const std::string& IP) {
std::string Ret;
static thread_local CURL* curl = curl_easy_init();
if (curl) {
CURLcode res;
curl_easy_setopt(curl, CURLOPT_URL, IP.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
error("GET to " + IP + " failed: " + std::string(curl_easy_strerror(res)));
return "";
}
} else {
error("Curl easy init failed");
return "";
}
return Ret;
}
std::string HTTP::Post(const std::string& IP, const std::string& Fields) {
std::string Ret;
static thread_local CURL* curl = curl_easy_init();
if (curl) {
CURLcode res;
curl_easy_setopt(curl, CURLOPT_URL, IP.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, Fields.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, Fields.size());
struct curl_slist* list = nullptr;
list = curl_slist_append(list, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
res = curl_easy_perform(curl);
curl_slist_free_all(list);
if (res != CURLE_OK) {
error("POST to " + IP + " failed: " + std::string(curl_easy_strerror(res)));
return "";
}
} else {
error("Curl easy init failed");
return "";
}
return Ret;
}
bool HTTP::Download(const std::string& IP, const std::string& Path) {
static std::mutex Lock;
std::scoped_lock Guard(Lock);
info("Downloading an update (this may take a while)");
std::string Ret = Get(IP);
if (Ret.empty()) {
error("Download failed");
return false;
}
std::ofstream File(Path, std::ios::binary);
if (File.is_open()) {
File << Ret;
File.close();
info("Download Complete!");
} else {
error("Failed to open file directory: " + Path);
return false;
}
return true;
}
void set_headers(httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Request-Method", "POST, OPTIONS, GET");
res.set_header("Access-Control-Request-Headers", "X-API-Version");
}
void HTTP::StartProxy() {
std::thread proxy([&]() {
httplib::Server HTTPProxy;
httplib::Headers headers = {
{ "User-Agent", "BeamMP-Launcher/" + GetVer() + GetPatch() },
{ "Accept", "*/*" }
};
httplib::Client backend("https://backend.beammp.com");
httplib::Client forum("https://forum.beammp.com");
const std::string pattern = ".*";
auto handle_request = [&](const httplib::Request& req, httplib::Response& res) {
set_headers(res);
if (req.has_header("X-BMP-Authentication")) {
headers.emplace("X-BMP-Authentication", PrivateKey);
}
if (req.has_header("X-API-Version")) {
headers.emplace("X-API-Version", req.get_header_value("X-API-Version"));
}
const std::vector<std::string> path = Utils::Split(req.path, "/");
httplib::Result cli_res;
const std::string method = req.method;
std::string host = "";
if (!path.empty())
host = path[0];
if (host == "backend") {
std::string remaining_path = req.path.substr(std::strlen("/backend"));
if (method == "GET")
cli_res = backend.Get(remaining_path, headers);
else if (method == "POST")
cli_res = backend.Post(remaining_path, headers);
} else if (host == "avatar") {
bool error = false;
std::string username;
std::string avatar_size = "100";
if (path.size() > 1) {
username = path[1];
} else {
error = true;
}
if (path.size() > 2) {
try {
if (std::stoi(path[2]) > 0)
avatar_size = path[2];
} catch (std::exception&) { }
}
httplib::Result summary_res;
if (!error) {
summary_res = forum.Get("/u/" + username + ".json", headers);
if (!summary_res || summary_res->status != 200) {
error = true;
}
}
if (!error) {
try {
nlohmann::json d = nlohmann::json::parse(summary_res->body, nullptr, false); // can fail with parse_error
auto user = d.at("user"); // can fail with out_of_range
auto avatar_link_json = user.at("avatar_template"); // can fail with out_of_range
auto avatar_link = avatar_link_json.get<std::string>();
size_t start_pos = avatar_link.find("{size}");
if (start_pos != std::string::npos)
avatar_link.replace(start_pos, std::strlen("{size}"), avatar_size);
cli_res = forum.Get(avatar_link, headers);
} catch (std::exception&) {
error = true;
}
}
if (error) {
cli_res = forum.Get("/user_avatar/forum.beammp.com/user/0/0.png", headers);
}
} else {
res.set_content("Host not found", "text/plain");
return;
}
if (cli_res) {
res.set_content(cli_res->body, cli_res->get_header_value("Content-Type"));
} else {
res.set_content(to_string(cli_res.error()), "text/plain");
}
};
HTTPProxy.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) {
handle_request(req, res);
});
HTTPProxy.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
handle_request(req, res);
});
ProxyPort = HTTPProxy.bind_to_any_port("127.0.0.1");
debug("HTTP Proxy listening on port " + std::to_string(ProxyPort));
HTTPProxy.listen_after_bind();
});
proxy.detach();
}

View File

@@ -8,7 +8,12 @@
#include "Network/network.hpp"
#include <chrono>
#include <iomanip>
#include <ios>
#include <mutex>
#include <nlohmann/json.hpp>
#include <openssl/err.h>
#include <openssl/evp.h>
#if defined(_WIN32)
#include <ws2tcpip.h>
@@ -23,6 +28,7 @@
#include "Logger.h"
#include "Startup.h"
#include <Utils.h>
#include <atomic>
#include <cmath>
#include <cstring>
@@ -31,7 +37,8 @@
#include <future>
#include <iostream>
#include <thread>
#include <Utils.h>
#include "hashpp.h"
namespace fs = std::filesystem;
@@ -123,11 +130,19 @@ void UpdateUl(bool D, const std::string& msg) {
UlStatus = "UlLoading Resource " + msg;
}
float DownloadSpeed = 0;
void AsyncUpdate(uint64_t& Rcv, uint64_t Size, const std::string& Name) {
do {
double pr = double(Rcv) / double(Size) * 100;
std::string Per = std::to_string(trunc(pr * 10) / 10);
UpdateUl(true, Name + " (" + Per.substr(0, Per.find('.') + 2) + "%)");
std::string SpeedString = "";
if (DownloadSpeed > 0.01) {
std::stringstream ss;
ss << " at " << std::setprecision(1) << std::fixed << DownloadSpeed << " Mbit/s";
SpeedString = ss.str();
}
UpdateUl(true, Name + " (" + Per.substr(0, Per.find('.') + 2) + "%)" + SpeedString);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} while (!Terminate && Rcv < Size);
}
@@ -144,12 +159,12 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
std::vector<char> File(Size);
uint64_t Rcv = 0;
auto start = std::chrono::high_resolution_clock::now();
int i = 0;
do {
auto start = std::chrono::high_resolution_clock::now();
// receive at most some MB at a time
int Len = std::min(int(Size - Rcv), 2 * 1024 * 1024);
int Len = std::min(int(Size - Rcv), 1 * 1024 * 1024);
int32_t Temp = recv(Sock, &File[Rcv], Len, MSG_WAITALL);
if (Temp < 1) {
info(std::to_string(Temp));
@@ -161,12 +176,13 @@ std::vector<char> TCPRcvRaw(SOCKET Sock, uint64_t& GRcv, uint64_t Size) {
Rcv += Temp;
GRcv += Temp;
// every 8th iteration calculate download speed for that iteration
auto end = std::chrono::high_resolution_clock::now();
auto difference = end - start;
float bits_per_s = float(Rcv * 8) / float(std::chrono::duration_cast<std::chrono::milliseconds>(difference).count());
float megabits_per_s = bits_per_s / 1000;
DownloadSpeed = megabits_per_s;
// every 8th iteration print the speed
if (i % 8 == 0) {
auto end = std::chrono::high_resolution_clock::now();
auto difference = end - start;
float bits_per_s = float(Temp * 8) / float(std::chrono::duration_cast<std::chrono::milliseconds>(difference).count());
float megabits_per_s = bits_per_s / 1000;
debug("Download speed: " + std::to_string(uint32_t(megabits_per_s)) + "Mbit/s");
}
++i;
@@ -203,7 +219,38 @@ SOCKET InitDSock() {
return DSock;
}
std::vector<char> SingleNormalDownload(SOCKET MSock, uint64_t Size, const std::string& Name) {
DownloadSpeed = 0;
uint64_t GRcv = 0;
std::thread Au([&] { AsyncUpdate(GRcv, Size, Name); });
const std::vector<char> MData = TCPRcvRaw(MSock, GRcv, Size);
if (MData.empty()) {
KillSocket(MSock);
Terminate = true;
Au.join();
return {};
}
// ensure that GRcv is good before joining the async update thread
GRcv = MData.size();
if (GRcv != Size) {
error("Something went wrong during download; didn't get enough data. Expected " + std::to_string(Size) + " bytes, got " + std::to_string(GRcv) + " bytes instead");
Terminate = true;
Au.join();
return {};
}
Au.join();
return MData;
}
std::vector<char> MultiDownload(SOCKET MSock, SOCKET DSock, uint64_t Size, const std::string& Name) {
DownloadSpeed = 0;
uint64_t GRcv = 0;
uint64_t MSize = Size / 2;
@@ -240,7 +287,7 @@ std::vector<char> MultiDownload(SOCKET MSock, SOCKET DSock, uint64_t Size, const
Au.join();
std::vector<char> Result{};
std::vector<char> Result {};
Result.insert(Result.begin(), MData.begin(), MData.end());
Result.insert(Result.end(), DData.begin(), DData.end());
return Result;
@@ -252,13 +299,248 @@ void InvalidResource(const std::string& File) {
Terminate = true;
}
std::string GetSha256HashReallyFast(const std::string& filename) {
try {
EVP_MD_CTX* mdctx;
const EVP_MD* md;
uint8_t sha256_value[EVP_MAX_MD_SIZE];
md = EVP_sha256();
if (md == nullptr) {
throw std::runtime_error("EVP_sha256() failed");
}
mdctx = EVP_MD_CTX_new();
if (mdctx == nullptr) {
throw std::runtime_error("EVP_MD_CTX_new() failed");
}
if (!EVP_DigestInit_ex2(mdctx, md, NULL)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestInit_ex2() failed");
}
std::ifstream stream(filename, std::ios::binary);
const size_t FileSize = std::filesystem::file_size(filename);
size_t Read = 0;
std::vector<char> Data;
while (Read < FileSize) {
Data.resize(size_t(std::min<size_t>(FileSize - Read, 4096)));
size_t RealDataSize = Data.size();
stream.read(Data.data(), std::streamsize(Data.size()));
if (stream.eof() || stream.fail()) {
RealDataSize = size_t(stream.gcount());
}
Data.resize(RealDataSize);
if (RealDataSize == 0) {
break;
}
if (RealDataSize > 0 && !EVP_DigestUpdate(mdctx, Data.data(), Data.size())) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestUpdate() failed");
}
Read += RealDataSize;
}
unsigned int sha256_len = 0;
if (!EVP_DigestFinal_ex(mdctx, sha256_value, &sha256_len)) {
EVP_MD_CTX_free(mdctx);
throw std::runtime_error("EVP_DigestFinal_ex() failed");
}
EVP_MD_CTX_free(mdctx);
std::string result;
for (size_t i = 0; i < sha256_len; i++) {
char buf[3];
sprintf(buf, "%02x", sha256_value[i]);
buf[2] = 0;
result += buf;
}
return result;
} catch (const std::exception& e) {
error("Sha256 hashing of '" + filename + "' failed: " + e.what());
return "";
}
}
struct ModInfo {
static std::pair<bool, std::vector<ModInfo>> ParseModInfosFromPacket(const std::string& packet) {
bool success = false;
std::vector<ModInfo> modInfos;
try {
auto json = nlohmann::json::parse(packet);
if (json.empty()) {
return std::make_pair(true, modInfos);
}
for (const auto& entry : json) {
ModInfo modInfo {
.FileName = entry["file_name"],
.FileSize = entry["file_size"],
.Hash = entry["hash"],
.HashAlgorithm = entry["hash_algorithm"],
};
modInfos.push_back(modInfo);
success = true;
}
} catch (const std::exception& e) {
debug(std::string("Failed to receive mod list: ") + e.what());
warn("Failed to receive new mod list format! This server may be outdated, but everything should still work as expected.");
}
return std::make_pair(success, modInfos);
}
std::string FileName;
size_t FileSize;
std::string Hash;
std::string HashAlgorithm;
};
void NewSyncResources(SOCKET Sock, const std::string& Mods, const std::vector<ModInfo> ModInfos) {
if (ModInfos.empty()) {
CoreSend("L");
TCPSend("Done", Sock);
info("Done!");
return;
}
if (!SecurityWarning())
return;
info("Checking Resources...");
CheckForDir();
std::string t;
for (const auto& mod : ModInfos) {
t += mod.FileName + ";";
}
if (t.empty())
CoreSend("L");
else
CoreSend("L" + t);
t.clear();
info("Syncing...");
int ModNo = 1;
int TotalMods = ModInfos.size();
for (auto ModInfoIter = ModInfos.begin(), AlsoModInfoIter = ModInfos.begin(); ModInfoIter != ModInfos.end() && !Terminate; ++ModInfoIter, ++AlsoModInfoIter) {
if (ModInfoIter->Hash.length() < 8 || ModInfoIter->HashAlgorithm != "sha256") {
error("Unsupported hash algorithm or invalid hash for '" + ModInfoIter->FileName + "'");
Terminate = true;
return;
}
auto FileName = std::filesystem::path(ModInfoIter->FileName).stem().string() + "-" + ModInfoIter->Hash.substr(0, 8) + std::filesystem::path(ModInfoIter->FileName).extension().string();
auto PathToSaveTo = (fs::path(CachingDirectory) / FileName).string();
if (fs::exists(PathToSaveTo) && GetSha256HashReallyFast(PathToSaveTo) == ModInfoIter->Hash) {
debug("Mod '" + FileName + "' found in cache");
std::this_thread::sleep_for(std::chrono::milliseconds(50));
try {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
}
auto modname = ModInfoIter->FileName;
#if defined(__linux__)
// Linux version of the game doesnt support uppercase letters in mod names
for (char& c : modname) {
c = ::tolower(c);
}
#endif
debug("Mod name: " + modname);
auto name = std::filesystem::path(GetGamePath()) / "mods/multiplayer" / modname;
std::string tmp_name = name.string();
tmp_name += ".tmp";
fs::copy_file(PathToSaveTo, tmp_name, fs::copy_options::overwrite_existing);
fs::rename(tmp_name, name);
} catch (std::exception& e) {
error("Failed copy to the mods folder! " + std::string(e.what()));
Terminate = true;
continue;
}
WaitForConfirm();
continue;
}
CheckForDir();
std::string FName = ModInfoIter->FileName;
do {
debug("Loading file '" + FName + "' to '" + PathToSaveTo + "'");
TCPSend("f" + ModInfoIter->FileName, Sock);
std::string Data = TCPRcv(Sock);
if (Data == "CO" || Terminate) {
Terminate = true;
UUl("Server cannot find " + FName);
break;
}
std::string Name = std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + FName;
std::vector<char> DownloadedFile = SingleNormalDownload(Sock, ModInfoIter->FileSize, Name);
if (Terminate)
break;
UpdateUl(false, std::to_string(ModNo) + "/" + std::to_string(TotalMods) + ": " + FName);
// 1. write downloaded file to disk
{
std::ofstream OutFile(PathToSaveTo, std::ios::binary | std::ios::trunc);
OutFile.write(DownloadedFile.data(), DownloadedFile.size());
OutFile.flush();
}
// 2. verify size
if (std::filesystem::file_size(PathToSaveTo) != DownloadedFile.size()) {
error("Failed to write the entire file '" + PathToSaveTo + "' correctly (file size mismatch)");
Terminate = true;
}
} while (fs::file_size(PathToSaveTo) != ModInfoIter->FileSize && !Terminate);
if (!Terminate) {
if (!fs::exists(GetGamePath() + "mods/multiplayer")) {
fs::create_directories(GetGamePath() + "mods/multiplayer");
}
// Linux version of the game doesnt support uppercase letters in mod names
#if defined(__linux__)
for (char& c : FName) {
c = ::tolower(c);
}
#endif
fs::copy_file(PathToSaveTo, std::filesystem::path(GetGamePath()) / "mods/multiplayer" / FName, fs::copy_options::overwrite_existing);
}
WaitForConfirm();
++ModNo;
}
if (!Terminate) {
TCPSend("Done", Sock);
info("Done!");
} else {
UlStatus = "Ulstart";
info("Connection Terminated!");
}
}
void SyncResources(SOCKET Sock) {
std::string Ret = Auth(Sock);
debug("Mod info: " + Ret);
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);
if (success) {
NewSyncResources(Sock, Ret, modInfo);
return;
}
}
if (Ret.empty())
return;
if (!SecurityWarning())
return;
return;
info("Checking Resources...");
CheckForDir();
@@ -383,6 +665,7 @@ void SyncResources(SOCKET Sock) {
}
WaitForConfirm();
}
KillSocket(DSock);
if (!Terminate) {
TCPSend("Done", Sock);

107
src/Options.cpp Normal file
View File

@@ -0,0 +1,107 @@
#include "Options.h"
#include "Logger.h"
#include <cstdlib>
#include <filesystem>
void InitOptions(int argc, const char *argv[], Options &options) {
int i = 1;
options.argc = argc;
options.argv = argv;
std::string AllOptions;
for (int i = 0; i < argc; ++i) {
AllOptions += std::string(argv[i]);
if (i + 1 < argc) {
AllOptions += " ";
}
}
debug("Launcher was invoked as: '" + AllOptions + "'");
if (argc > 2) {
if (std::string(argv[1]) == "0" && std::string(argv[2]) == "0") {
options.verbose = true;
options.no_download = true;
options.no_launch = true;
options.no_update = true;
warn("You are using deprecated commandline arguments, please use --dev instead");
return;
}
}
options.executable_name = std::string(argv[0]);
while (i < argc) {
std::string argument(argv[i]);
if (argument == "-p" || argument == "--port") {
if (i + 1 >= argc) {
std::string error_message =
"No port specified, resorting to default (";
error_message += std::to_string(options.port);
error_message += ")";
error(error_message);
i++;
continue;
}
int port = options.port;
try {
port = std::stoi(argv[i + 1]);
} catch (std::exception& e) {
error("Invalid port specified: " + std::string(argv[i + 1]) + " " + std::string(e.what()));
}
if (port <= 0) {
std::string error_message =
"Port invalid, must be a non-zero positive "
"integer, resorting to default (";
error_message += options.port;
error_message += ")";
error(error_message);
i++;
continue;
}
options.port = port;
i++;
} else if (argument == "-v" || argument == "--verbose") {
options.verbose = true;
} else if (argument == "--no-download") {
options.no_download = true;
} else if (argument == "--no-update") {
options.no_update = true;
} else if (argument == "--no-launch") {
options.no_launch = true;
} else if (argument == "--dev") {
options.verbose = true;
options.no_download = true;
options.no_launch = true;
options.no_update = true;
} else if (argument == "--" || argument == "--game") {
options.game_arguments = &argv[i + 1];
options.game_arguments_length = argc - i - 1;
break;
} else if (argument == "--help" || argument == "-h" || argument == "/?") {
std::cout << "USAGE:\n"
"\t" + std::filesystem::path(options.executable_name).filename().string() + " [OPTIONS] [-- <GAME ARGS>...]\n"
"\n"
"OPTIONS:\n"
"\t--port <port> -p Change the default listen port to <port>. This must be configured ingame, too\n"
"\t--verbose -v Verbose mode, prints debug messages\n"
"\t--no-download Skip downloading and installing the BeamMP Lua mod\n"
"\t--no-update Skip applying launcher updates (you must update manually)\n"
"\t--no-launch Skip launching the game (you must launch the game manually)\n"
"\t--dev Developer mode, same as --verbose --no-download --no-launch --no-update\n"
"\t--game <args...> Passes ALL following arguments to the game, see also `--`\n"
<< std::flush;
exit(0);
} else {
warn("Unknown option: " + argument);
}
i++;
}
}

View File

@@ -58,7 +58,7 @@ std::string Login(const std::string& fields) {
try {
std::string Buffer = HTTP::Post("https://auth.beammp.com/userlogin", fields);
if (Buffer == "-1") {
if (Buffer.empty()) {
return GetFail("Failed to communicate with the auth system!");
}
@@ -91,6 +91,7 @@ std::string Login(const std::string& fields) {
if (d.contains("message")) {
d.erase("private_key");
d.erase("public_key");
debug("Authentication result: " + d["message"].get<std::string>());
return d.dump();
}
return GetFail("Invalid message parsing!");
@@ -119,7 +120,7 @@ void CheckLocalKey() {
nlohmann::json d = nlohmann::json::parse(Buffer, nullptr, false);
if (Buffer == "-1" || Buffer.at(0) != '{' || d.is_discarded()) {
if (Buffer.empty() || Buffer.at(0) != '{' || d.is_discarded()) {
error(Buffer);
info("Invalid answer from authentication servers.");
UpdateKey(nullptr);
@@ -137,7 +138,6 @@ void CheckLocalKey() {
if (d.contains("id")) {
UserID = d["id"].get<int>();
}
// info(Role);
} else {
info("Auto-Authentication unsuccessful please re-login!");
UpdateKey(nullptr);

View File

@@ -8,6 +8,7 @@
#include "zip_file.h"
#include <charconv>
#include <cstring>
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <string>
@@ -25,9 +26,9 @@
#include <filesystem>
#include <fstream>
#include <thread>
#include "Options.h"
extern int TraceBack;
bool Dev = false;
int ProxyPort = 0;
namespace fs = std::filesystem;
@@ -81,13 +82,13 @@ std::string GetEN() {
}
std::string GetVer() {
return "2.1";
return "2.3";
}
std::string GetPatch() {
return ".3";
return ".2";
}
std::string GetEP(char* P) {
std::string GetEP(const char* P) {
static std::string Ret = [&]() {
std::string path(P);
return path.substr(0, path.find_last_of("\\/") + 1);
@@ -95,11 +96,11 @@ std::string GetEP(char* P) {
return Ret;
}
#if defined(_WIN32)
void ReLaunch(int argc, char* args[]) {
void ReLaunch() {
std::string Arg;
for (int c = 2; c <= argc; c++) {
for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1];
Arg += " ";
Arg += args[c - 1];
}
info("Relaunch!");
system("cls");
@@ -108,11 +109,11 @@ void ReLaunch(int argc, char* args[]) {
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
}
void URelaunch(int argc, char* args[]) {
void URelaunch() {
std::string Arg;
for (int c = 2; c <= argc; c++) {
for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1];
Arg += " ";
Arg += args[c - 1];
}
ShellExecute(nullptr, "open", (GetEP() + GetEN()).c_str(), Arg.c_str(), nullptr, SW_SHOWNORMAL);
ShowWindow(GetConsoleWindow(), 0);
@@ -120,47 +121,50 @@ void URelaunch(int argc, char* args[]) {
exit(1);
}
#elif defined(__linux__)
void ReLaunch(int argc, char* args[]) {
void ReLaunch() {
std::string Arg;
for (int c = 2; c <= argc; c++) {
for (int c = 2; c <= options.argc; c++) {
Arg += options.argv[c - 1];
Arg += " ";
Arg += args[c - 1];
}
info("Relaunch!");
system("clear");
execl((GetEP() + GetEN()).c_str(), Arg.c_str(), NULL);
int ret = execv(options.executable_name.c_str(), const_cast<char**>(options.argv));
if (ret < 0) {
error(std::string("execv() failed with: ") + strerror(errno) + ". Failed to relaunch");
exit(1);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
}
void URelaunch(int argc, char* args[]) {
std::string Arg;
for (int c = 2; c <= argc; c++) {
Arg += " ";
Arg += args[c - 1];
void URelaunch() {
int ret = execv(options.executable_name.c_str(), const_cast<char**>(options.argv));
if (ret < 0) {
error(std::string("execv() failed with: ") + strerror(errno) + ". Failed to relaunch");
exit(1);
}
execl((GetEP() + GetEN()).c_str(), Arg.c_str(), NULL);
std::this_thread::sleep_for(std::chrono::seconds(1));
exit(1);
}
#endif
void CheckName(int argc, char* args[]) {
void CheckName() {
#if defined(_WIN32)
std::string DN = GetEN(), CDir = args[0], FN = CDir.substr(CDir.find_last_of('\\') + 1);
std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('\\') + 1);
#elif defined(__linux__)
std::string DN = GetEN(), CDir = args[0], FN = CDir.substr(CDir.find_last_of('/') + 1);
std::string DN = GetEN(), CDir = options.executable_name, FN = CDir.substr(CDir.find_last_of('/') + 1);
#endif
if (FN != DN) {
if (fs::exists(DN))
remove(DN.c_str());
if (fs::exists(DN))
ReLaunch(argc, args);
ReLaunch();
std::rename(FN.c_str(), DN.c_str());
URelaunch(argc, args);
URelaunch();
}
}
void CheckForUpdates(int argc, char* args[], const std::string& CV) {
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);
@@ -170,39 +174,30 @@ void CheckForUpdates(int argc, char* args[], const std::string& CV) {
std::string FileHash = hashpp::get::getFileHash(hashpp::ALGORITHMS::SHA2_256, EP);
if (FileHash != LatestHash && IsOutdated(Version(VersionStrToInts(GetVer() + GetPatch())), Version(VersionStrToInts(LatestVersion))) && !Dev) {
info("Launcher update found!");
if (FileHash != LatestHash && IsOutdated(Version(VersionStrToInts(GetVer() + GetPatch())), Version(VersionStrToInts(LatestVersion)))) {
if (!options.no_update) {
info("Launcher update found!");
#if defined(__linux__)
error("Auto update is NOT implemented for the Linux version. Please update manually ASAP as updates contain security patches.");
error("Auto update is NOT implemented for the Linux version. Please update manually ASAP as updates contain security patches.");
#else
fs::remove(Back);
fs::rename(EP, Back);
info("Downloading Launcher update " + LatestHash);
HTTP::Download(
"https://backend.beammp.com/builds/launcher?download=true"
"&pk="
+ PublicKey + "&branch=" + Branch,
EP);
URelaunch(argc, args);
fs::remove(Back);
fs::rename(EP, Back);
info("Downloading Launcher update " + LatestHash);
HTTP::Download(
"https://backend.beammp.com/builds/launcher?download=true"
"&pk="
+ PublicKey + "&branch=" + Branch,
EP);
URelaunch();
#endif
} else {
warn("Launcher update was found, but not updating because --no-update or --dev was specified.");
}
} else
info("Launcher version is up to date");
TraceBack++;
}
void CustomPort(int argc, char* argv[]) {
if (argc > 1) {
std::string Port = argv[1];
if (Port.find_first_not_of("0123456789") == std::string::npos) {
if (std::stoi(Port) > 1000) {
DEFAULT_PORT = std::stoi(Port);
warn("Running on custom port : " + std::to_string(DEFAULT_PORT));
}
}
if (argc > 2)
Dev = true;
}
}
#ifdef _WIN32
void LinuxPatch() {
@@ -234,25 +229,21 @@ void LinuxPatch() {
#endif
#if defined(_WIN32)
void InitLauncher(int argc, char* argv[]) {
void InitLauncher() {
SetConsoleTitleA(("BeamMP Launcher v" + std::string(GetVer()) + GetPatch()).c_str());
InitLog();
CheckName(argc, argv);
CheckName();
LinuxPatch();
CheckLocalKey();
ConfigInit();
CustomPort(argc, argv);
CheckForUpdates(argc, argv, std::string(GetVer()) + GetPatch());
CheckForUpdates(std::string(GetVer()) + GetPatch());
}
#elif defined(__linux__)
void InitLauncher(int argc, char* argv[]) {
InitLog();
void InitLauncher() {
info("BeamMP Launcher v" + GetVer() + GetPatch());
CheckName(argc, argv);
CheckName();
CheckLocalKey();
ConfigInit();
CustomPort(argc, argv);
CheckForUpdates(argc, argv, std::string(GetVer()) + GetPatch());
CheckForUpdates(std::string(GetVer()) + GetPatch());
}
#endif
@@ -316,7 +307,7 @@ void PreGame(const std::string& GamePath) {
CheckMP(GetGamePath() + "mods/multiplayer");
info("Game user path: " + GetGamePath());
if (!Dev) {
if (!options.no_download) {
std::string LatestHash = HTTP::Get("https://backend.beammp.com/sha/mod?branch=" + Branch + "&pk=" + PublicKey);
transform(LatestHash.begin(), LatestHash.end(), LatestHash.begin(), ::tolower);
LatestHash.erase(std::remove_if(LatestHash.begin(), LatestHash.end(),

View File

@@ -10,8 +10,12 @@
#include "Network/network.hpp"
#include "Security/Init.h"
#include "Startup.h"
#include <curl/curl.h>
#include <iostream>
#include <thread>
#include "Options.h"
Options options;
[[noreturn]] void flush() {
while (true) {
@@ -20,30 +24,27 @@
}
}
int main(int argc, char** argv) try {
#ifdef DEBUG
std::thread th(flush);
th.detach();
#endif
int main(int argc, const char** argv) try {
#if defined(_WIN32)
system("cls");
#elif defined(__linux__)
system("clear");
#endif
#ifdef DEBUG
std::thread th(flush);
th.detach();
#endif
curl_global_init(CURL_GLOBAL_ALL);
GetEP(argv[0]);
for (int i = 0; i < argc; ++i) {
if (std::string_view(argv[i]) == "--skip-ssl-verify") {
info("SSL verification skip enabled");
HTTP::SkipSslVerify = true;
}
}
InitLauncher(argc, argv);
InitLog();
ConfigInit();
InitOptions(argc, argv, options);
info("Mod caching directory: " + CachingDirectory);
InitLauncher();
info("IMPORTANT: You MUST keep this window open to play BeamMP!");

View File

@@ -3,6 +3,7 @@
"cpp-httplib",
"nlohmann-json",
"zlib",
"openssl"
"openssl",
"curl"
]
}