Merge branch 'develop' into containers-are-lovely

This commit is contained in:
Edo 2024-01-20 15:41:15 +01:00 committed by GitHub
commit a5bb81f32b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 196 additions and 15 deletions

View File

@ -59,6 +59,7 @@
#include "Modules/Threading.hpp" #include "Modules/Threading.hpp"
#include "Modules/Toast.hpp" #include "Modules/Toast.hpp"
#include "Modules/UIFeeder.hpp" #include "Modules/UIFeeder.hpp"
#include "Modules/Updater.hpp"
#include "Modules/VisionFile.hpp" #include "Modules/VisionFile.hpp"
#include "Modules/Voice.hpp" #include "Modules/Voice.hpp"
#include "Modules/Vote.hpp" #include "Modules/Vote.hpp"
@ -172,6 +173,7 @@ namespace Components
Register(new Threading()); Register(new Threading());
Register(new Toast()); Register(new Toast());
Register(new UIFeeder()); Register(new UIFeeder());
Register(new Updater());
Register(new VisionFile()); Register(new VisionFile());
Register(new Voice()); Register(new Voice());
Register(new Vote()); Register(new Vote());

View File

@ -435,7 +435,7 @@ namespace Components
MongooseLogBuffer.push_back(c); MongooseLogBuffer.push_back(c);
} }
void Download::ReplyError(mg_connection* connection, int code) void Download::ReplyError(mg_connection* connection, int code, std::string messageOverride)
{ {
std::string msg{}; std::string msg{};
switch(code) switch(code)
@ -453,6 +453,11 @@ namespace Components
break; break;
} }
if (!messageOverride.empty())
{
msg = messageOverride;
}
mg_http_reply(connection, code, "Content-Type: text/plain\r\n", "%s", msg.c_str()); mg_http_reply(connection, code, "Content-Type: text/plain\r\n", "%s", msg.c_str());
} }
@ -462,6 +467,31 @@ namespace Components
mg_http_reply(connection, 200, formatted.c_str(), "%s", data.c_str()); mg_http_reply(connection, 200, formatted.c_str(), "%s", data.c_str());
} }
bool VerifyPassword([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
const std::string g_password = *Game::g_password ? (*Game::g_password)->current.string : "";
if (g_password.empty()) return true;
// SHA256 hashes are 64 characters long but we're gonna be safe here
char buffer[128]{};
const auto len = mg_http_get_var(&hm->query, "password", buffer, sizeof(buffer));
if (len <= 0)
{
Download::ReplyError(c, 403, "Password Required");
return false;
}
const auto password = std::string(buffer, len);
if (password != Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(g_password), ""))
{
Download::ReplyError(c, 403, "Invalid Password");
return false;
}
return true;
}
std::optional<std::string> Download::InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) std::optional<std::string> Download::InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{ {
if (!(*Game::com_sv_running)->current.enabled) if (!(*Game::com_sv_running)->current.enabled)
@ -524,6 +554,12 @@ namespace Components
static nlohmann::json jsonList; static nlohmann::json jsonList;
static std::filesystem::path fsGamePre; static std::filesystem::path fsGamePre;
if (!VerifyPassword(c, hm))
{
// Custom reply done in VerifyPassword
return {};
}
const std::filesystem::path fsGame = (*Game::fs_gameDirVar)->current.string; const std::filesystem::path fsGame = (*Game::fs_gameDirVar)->current.string;
if (!fsGame.empty() && (fsGamePre != fsGame)) if (!fsGame.empty() && (fsGamePre != fsGame))
@ -572,6 +608,12 @@ namespace Components
static std::string mapNamePre; static std::string mapNamePre;
static nlohmann::json jsonList; static nlohmann::json jsonList;
if (!VerifyPassword(c, hm))
{
// Custom reply done in VerifyPassword
return {};
}
const std::string mapName = Party::IsInUserMapLobby() ? (*Game::ui_mapname)->current.string : Maps::GetUserMap()->getName(); const std::string mapName = Party::IsInUserMapLobby() ? (*Game::ui_mapname)->current.string : Maps::GetUserMap()->getName();
if (!Maps::GetUserMap()->isValid() && !Party::IsInUserMapLobby()) if (!Maps::GetUserMap()->isValid() && !Party::IsInUserMapLobby())
{ {

View File

@ -17,6 +17,8 @@ namespace Components
static void InitiateClientDownload(const std::string& mod, bool needPassword, bool map = false); static void InitiateClientDownload(const std::string& mod, bool needPassword, bool map = false);
static void InitiateMapDownload(const std::string& map, bool needPassword); static void InitiateMapDownload(const std::string& map, bool needPassword);
static void ReplyError(mg_connection* connection, int code, std::string messageOverride = {});
static Dvar::Var SV_wwwDownload; static Dvar::Var SV_wwwDownload;
static Dvar::Var SV_wwwBaseUrl; static Dvar::Var SV_wwwBaseUrl;
@ -105,7 +107,6 @@ namespace Components
static bool DownloadFile(ClientDownload* download, unsigned int index); static bool DownloadFile(ClientDownload* download, unsigned int index);
static void LogFn(char c, void* param); static void LogFn(char c, void* param);
static void ReplyError(mg_connection* connection, int code);
static void Reply(mg_connection* connection, const std::string& contentType, const std::string& data); static void Reply(mg_connection* connection, const std::string& contentType, const std::string& data);
static std::optional<std::string> FileHandler(mg_connection* c, const mg_http_message* hm); static std::optional<std::string> FileHandler(mg_connection* c, const mg_http_message* hm);

View File

@ -246,9 +246,10 @@ namespace Components
auto workingDir = std::filesystem::current_path().string(); auto workingDir = std::filesystem::current_path().string();
const std::string binary = *Game::sys_exitCmdLine; const std::string binary = *Game::sys_exitCmdLine;
const std::string command = binary == "iw4x-sp.exe" ? "iw4x-sp" : "iw4x";
SetEnvironmentVariableA("MW2_INSTALL", workingDir.data()); SetEnvironmentVariableA("MW2_INSTALL", workingDir.data());
Utils::Library::LaunchProcess(binary, "-singleplayer", workingDir); Utils::Library::LaunchProcess(binary, std::format("{} --pass \"{}\"", command, GetCommandLineA()), workingDir);
} }
__declspec(naked) void QuickPatch::SND_GetAliasOffset_Stub() __declspec(naked) void QuickPatch::SND_GetAliasOffset_Stub()
@ -320,7 +321,7 @@ namespace Components
Utils::Hook::Set<void(*)(Game::XAssetHeader, void*)>(0x51FCDD, QuickPatch::R_AddImageToList_Hk); Utils::Hook::Set<void(*)(Game::XAssetHeader, void*)>(0x51FCDD, QuickPatch::R_AddImageToList_Hk);
Utils::Hook::Set<const char*>(0x41DB8C, "iw4-sp.exe"); Utils::Hook::Set<const char*>(0x41DB8C, "iw4x-sp.exe");
Utils::Hook(0x4D6989, QuickPatch::Sys_SpawnQuitProcess_Hk, HOOK_CALL).install()->quick(); Utils::Hook(0x4D6989, QuickPatch::Sys_SpawnQuitProcess_Hk, HOOK_CALL).install()->quick();
// Fix crash as nullptr goes unchecked // Fix crash as nullptr goes unchecked

View File

@ -6,6 +6,8 @@
namespace Components namespace Components
{ {
HANDLE Singleton::Mutex;
bool Singleton::FirstInstance = true; bool Singleton::FirstInstance = true;
bool Singleton::IsFirstInstance() bool Singleton::IsFirstInstance()
@ -13,6 +15,14 @@ namespace Components
return FirstInstance; return FirstInstance;
} }
void Singleton::preDestroy()
{
if (INVALID_HANDLE_VALUE != Mutex)
{
CloseHandle(Mutex);
}
}
Singleton::Singleton() Singleton::Singleton()
{ {
if (Flags::HasFlag("version")) if (Flags::HasFlag("version"))
@ -31,7 +41,8 @@ namespace Components
if (Loader::IsPerformingUnitTests() || Dedicated::IsEnabled() || ZoneBuilder::IsEnabled()) return; if (Loader::IsPerformingUnitTests() || Dedicated::IsEnabled() || ZoneBuilder::IsEnabled()) return;
FirstInstance = (CreateMutexA(nullptr, FALSE, "iw4x_mutex") && GetLastError() != ERROR_ALREADY_EXISTS); Mutex = CreateMutexA(nullptr, FALSE, "iw4x_mutex");
FirstInstance = ((INVALID_HANDLE_VALUE != Mutex) && GetLastError() != ERROR_ALREADY_EXISTS);
if (!FirstInstance && !ConnectProtocol::Used() && MessageBoxA(nullptr, "Do you want to start another instance?\nNot all features will be available!", "Game already running", MB_ICONEXCLAMATION | MB_YESNO) == IDNO) if (!FirstInstance && !ConnectProtocol::Used() && MessageBoxA(nullptr, "Do you want to start another instance?\nNot all features will be available!", "Game already running", MB_ICONEXCLAMATION | MB_YESNO) == IDNO)
{ {

View File

@ -7,9 +7,12 @@ namespace Components
public: public:
Singleton(); Singleton();
void preDestroy() override;
static bool IsFirstInstance(); static bool IsFirstInstance();
private: private:
static HANDLE Mutex;
static bool FirstInstance; static bool FirstInstance;
}; };
} }

View File

@ -0,0 +1,107 @@
#include <STDInclude.hpp>
#include "Updater.hpp"
#include "Scheduler.hpp"
#include <Utils/WebIO.hpp>
#include <rapidjson/document.h>
namespace Components
{
namespace
{
const Game::dvar_t* cl_updateAvailable;
// If they use the alterware-launcher once to install they will have this file
// If they don't, what are they waiting for?
constexpr auto* REVISION_FILE = ".iw4xrevision";
constexpr auto* GITHUB_REMOTE_URL = "https://api.github.com/repos/iw4x/iw4x-client/releases/latest";
constexpr auto* INSTALL_GUIDE_REMOTE_URL = "https://forum.alterware.dev/t/how-to-install-the-alterware-launcher/56";
void CheckForUpdate()
{
std::string revision;
if (!Utils::IO::ReadFile(REVISION_FILE, &revision) || revision.empty())
{
Logger::Print("{} does not exist. Notifying the user an update is available\n", REVISION_FILE);
Game::Dvar_SetBool(cl_updateAvailable, true);
}
const auto result = Utils::WebIO("IW4x", GITHUB_REMOTE_URL).setTimeout(5000)->get();
if (result.empty())
{
// Nothing to do in this situation. We won't know if we need to update or not
Logger::Print("Could not fetch latest tag from GitHub\n");
return;
}
rapidjson::Document doc{};
const rapidjson::ParseResult parseResult = doc.Parse(result);
if (!parseResult || !doc.IsObject())
{
// Nothing to do in this situation. We won't know if we need to update or not
Logger::Print("GitHub sent an invalid reply (malformed JSON)\n");
return;
}
if (!doc.HasMember("tag_name") || !doc["tag_name"].IsString())
{
// Nothing to do in this situation. We won't know if we need to update or not
Logger::Print("GitHub sent an invalid reply (missing 'tag_name' JSON member)\n");
return;
}
const auto* tag = doc["tag_name"].GetString();
if (revision != tag)
{
// A new version came out!
Game::Dvar_SetBool(cl_updateAvailable, true);
}
}
// Depending on Linux/Windows 32/64 there are a few things we must check
std::optional<std::string> GetLauncher()
{
if (Utils::IO::FileExists("alterware-launcher.exe"))
{
return "alterware-launcher.exe";
}
if (Utils::IO::FileExists("alterware-launcher-x86.exe"))
{
return "alterware-launcher-x86.exe";
}
if (Utils::IsWineEnvironment() && Utils::IO::FileExists("alterware-launcher"))
{
return "alterware-launcher";
}
return {};
}
}
Updater::Updater()
{
cl_updateAvailable = Game::Dvar_RegisterBool("cl_updateAvailable", false, Game::DVAR_NONE, "Whether an update is available or not");
Scheduler::Once(CheckForUpdate, Scheduler::Pipeline::ASYNC);
UIScript::Add("checkForUpdate", [](const UIScript::Token& /*token*/, const Game::uiInfo_s* /*info*/)
{
CheckForUpdate();
});
UIScript::Add("getAutoUpdate", [](const UIScript::Token& /*token*/, const Game::uiInfo_s* /*info*/)
{
if (const auto exe = GetLauncher(); exe.has_value())
{
Game::Sys_QuitAndStartProcess(exe.value().data());
return;
}
// No launcher was found on the system, time to tell them to download it from GitHub
Utils::OpenUrl(INSTALL_GUIDE_REMOTE_URL);
});
}
}

View File

@ -0,0 +1,10 @@
#pragma once
namespace Components
{
class Updater : public Component
{
public:
Updater();
};
}

View File

@ -25,6 +25,7 @@ namespace Game
Sys_SetValue_t Sys_SetValue = Sys_SetValue_t(0x4B2F50); Sys_SetValue_t Sys_SetValue = Sys_SetValue_t(0x4B2F50);
Sys_CreateFile_t Sys_CreateFile = Sys_CreateFile_t(0x4B2EF0); Sys_CreateFile_t Sys_CreateFile = Sys_CreateFile_t(0x4B2EF0);
Sys_OutOfMemErrorInternal_t Sys_OutOfMemErrorInternal = Sys_OutOfMemErrorInternal_t(0x4B2E60); Sys_OutOfMemErrorInternal_t Sys_OutOfMemErrorInternal = Sys_OutOfMemErrorInternal_t(0x4B2E60);
Sys_QuitAndStartProcess_t Sys_QuitAndStartProcess = Sys_QuitAndStartProcess_t(0x45FCF0);
char(*sys_exitCmdLine)[1024] = reinterpret_cast<char(*)[1024]>(0x649FB68); char(*sys_exitCmdLine)[1024] = reinterpret_cast<char(*)[1024]>(0x649FB68);

View File

@ -71,6 +71,9 @@ namespace Game
typedef void(*Sys_OutOfMemErrorInternal_t)(const char* filename, int line); typedef void(*Sys_OutOfMemErrorInternal_t)(const char* filename, int line);
extern Sys_OutOfMemErrorInternal_t Sys_OutOfMemErrorInternal; extern Sys_OutOfMemErrorInternal_t Sys_OutOfMemErrorInternal;
typedef void(*Sys_QuitAndStartProcess_t)(const char*);
extern Sys_QuitAndStartProcess_t Sys_QuitAndStartProcess;
extern char(*sys_exitCmdLine)[1024]; extern char(*sys_exitCmdLine)[1024];
extern RTL_CRITICAL_SECTION* s_criticalSection; extern RTL_CRITICAL_SECTION* s_criticalSection;

View File

@ -104,20 +104,20 @@ namespace Utils
void Library::LaunchProcess(const std::string& process, const std::string& commandLine, const std::string& currentDir) void Library::LaunchProcess(const std::string& process, const std::string& commandLine, const std::string& currentDir)
{ {
STARTUPINFOA startup_info; STARTUPINFOA startupInfo;
PROCESS_INFORMATION process_info; PROCESS_INFORMATION processInfo;
ZeroMemory(&startup_info, sizeof(startup_info)); ZeroMemory(&startupInfo, sizeof(startupInfo));
ZeroMemory(&process_info, sizeof(process_info)); ZeroMemory(&processInfo, sizeof(processInfo));
startup_info.cb = sizeof(startup_info); startupInfo.cb = sizeof(startupInfo);
CreateProcessA(process.data(), const_cast<char*>(commandLine.data()), nullptr, CreateProcessA(process.data(), const_cast<char*>(commandLine.data()), nullptr,
nullptr, false, NULL, nullptr, currentDir.data(), nullptr, false, NULL, nullptr, currentDir.data(),
&startup_info, &process_info); &startupInfo, &processInfo);
if (process_info.hThread && process_info.hThread != INVALID_HANDLE_VALUE) if (processInfo.hThread && processInfo.hThread != INVALID_HANDLE_VALUE)
CloseHandle(process_info.hThread); CloseHandle(processInfo.hThread);
if (process_info.hProcess && process_info.hProcess != INVALID_HANDLE_VALUE) if (processInfo.hProcess && processInfo.hProcess != INVALID_HANDLE_VALUE)
CloseHandle(process_info.hProcess); CloseHandle(processInfo.hProcess);
} }
} }