diff options
Diffstat (limited to 'NorthstarDLL/mods/autodownload/moddownloader.cpp')
-rw-r--r-- | NorthstarDLL/mods/autodownload/moddownloader.cpp | 638 |
1 files changed, 0 insertions, 638 deletions
diff --git a/NorthstarDLL/mods/autodownload/moddownloader.cpp b/NorthstarDLL/mods/autodownload/moddownloader.cpp deleted file mode 100644 index 165399e3..00000000 --- a/NorthstarDLL/mods/autodownload/moddownloader.cpp +++ /dev/null @@ -1,638 +0,0 @@ -#include "moddownloader.h" -#include <rapidjson/fwd.h> -#include <mz_strm_mem.h> -#include <mz.h> -#include <mz_strm.h> -#include <mz_zip.h> -#include <mz_compat.h> -#include <thread> -#include <future> -#include <bcrypt.h> -#include <winternl.h> -#include <fstream> - -ModDownloader* g_pModDownloader; - -ModDownloader::ModDownloader() -{ - spdlog::info("Mod downloader initialized"); - - // Initialise mods list URI - char* clachar = strstr(GetCommandLineA(), CUSTOM_MODS_URL_FLAG); - if (clachar) - { - std::string url; - int iFlagStringLength = strlen(CUSTOM_MODS_URL_FLAG); - std::string cla = std::string(clachar); - if (strncmp(cla.substr(iFlagStringLength, 1).c_str(), "\"", 1)) - { - int space = cla.find(" "); - url = cla.substr(iFlagStringLength, space - iFlagStringLength); - } - else - { - std::string quote = "\""; - int quote1 = cla.find(quote); - int quote2 = (cla.substr(quote1 + 1)).find(quote); - url = cla.substr(quote1 + 1, quote2); - } - spdlog::info("Found custom verified mods URL in command line argument: {}", url); - modsListUrl = strdup(url.c_str()); - } - else - { - spdlog::info("Custom verified mods URL not found in command line arguments, using default URL."); - modsListUrl = strdup(DEFAULT_MODS_LIST_URL); - } -} - -size_t WriteToString(void* ptr, size_t size, size_t count, void* stream) -{ - ((std::string*)stream)->append((char*)ptr, 0, size * count); - return size * count; -} - -void ModDownloader::FetchModsListFromAPI() -{ - std::thread requestThread( - [this]() - { - CURLcode result; - CURL* easyhandle; - rapidjson::Document verifiedModsJson; - std::string url = modsListUrl; - - curl_global_init(CURL_GLOBAL_ALL); - easyhandle = curl_easy_init(); - std::string readBuffer; - - // Fetching mods list from GitHub repository - curl_easy_setopt(easyhandle, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(easyhandle, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(easyhandle, CURLOPT_URL, url.c_str()); - curl_easy_setopt(easyhandle, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(easyhandle, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(easyhandle, CURLOPT_WRITEFUNCTION, WriteToString); - result = curl_easy_perform(easyhandle); - - if (result == CURLcode::CURLE_OK) - { - spdlog::info("Mods list successfully fetched."); - } - else - { - spdlog::error("Fetching mods list failed: {}", curl_easy_strerror(result)); - goto REQUEST_END_CLEANUP; - } - - // Load mods list into local state - spdlog::info("Loading mods configuration..."); - verifiedModsJson.Parse(readBuffer); - for (auto i = verifiedModsJson.MemberBegin(); i != verifiedModsJson.MemberEnd(); ++i) - { - std::string name = i->name.GetString(); - std::string dependency = i->value["DependencyPrefix"].GetString(); - - std::unordered_map<std::string, VerifiedModVersion> modVersions; - rapidjson::Value& versions = i->value["Versions"]; - assert(versions.IsArray()); - for (auto& attribute : versions.GetArray()) - { - assert(attribute.IsObject()); - std::string version = attribute["Version"].GetString(); - std::string checksum = attribute["Checksum"].GetString(); - modVersions.insert({version, {.checksum = checksum}}); - } - - VerifiedModDetails modConfig = {.dependencyPrefix = dependency, .versions = modVersions}; - verifiedMods.insert({name, modConfig}); - spdlog::info("==> Loaded configuration for mod \"" + name + "\""); - } - - spdlog::info("Done loading verified mods list."); - - REQUEST_END_CLEANUP: - curl_easy_cleanup(easyhandle); - }); - requestThread.detach(); -} - -size_t WriteData(void* ptr, size_t size, size_t nmemb, FILE* stream) -{ - size_t written; - written = fwrite(ptr, size, nmemb, stream); - return written; -} - -int ModDownloader::ModFetchingProgressCallback( - void* ptr, curl_off_t totalDownloadSize, curl_off_t finishedDownloadSize, curl_off_t totalToUpload, curl_off_t nowUploaded) -{ - if (totalDownloadSize != 0 && finishedDownloadSize != 0) - { - ModDownloader* instance = static_cast<ModDownloader*>(ptr); - auto currentDownloadProgress = roundf(static_cast<float>(finishedDownloadSize) / totalDownloadSize * 100); - instance->modState.progress = finishedDownloadSize; - instance->modState.total = totalDownloadSize; - instance->modState.ratio = currentDownloadProgress; - } - - return 0; -} - -std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view modName, std::string_view modVersion) -{ - // Retrieve mod prefix from local mods list, or use mod name as mod prefix if bypass flag is set - std::string modPrefix = strstr(GetCommandLineA(), VERIFICATION_FLAG) ? modName.data() : verifiedMods[modName.data()].dependencyPrefix; - // Build archive distant URI - std::string archiveName = std::format("{}-{}.zip", modPrefix, modVersion.data()); - std::string url = STORE_URL + archiveName; - spdlog::info(std::format("Fetching mod archive from {}", url)); - - // Download destination - std::filesystem::path downloadPath = std::filesystem::temp_directory_path() / archiveName; - spdlog::info(std::format("Downloading archive to {}", downloadPath.generic_string())); - - // Update state - modState.state = DOWNLOADING; - - // Download the actual archive - bool failed = false; - FILE* fp = fopen(downloadPath.generic_string().c_str(), "wb"); - CURLcode result; - CURL* easyhandle; - easyhandle = curl_easy_init(); - - curl_easy_setopt(easyhandle, CURLOPT_URL, url.data()); - curl_easy_setopt(easyhandle, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(easyhandle, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(easyhandle, CURLOPT_WRITEFUNCTION, WriteData); - curl_easy_setopt(easyhandle, CURLOPT_NOPROGRESS, 0L); - curl_easy_setopt(easyhandle, CURLOPT_XFERINFOFUNCTION, ModDownloader::ModFetchingProgressCallback); - curl_easy_setopt(easyhandle, CURLOPT_XFERINFODATA, this); - result = curl_easy_perform(easyhandle); - - if (result == CURLcode::CURLE_OK) - { - spdlog::info("Mod archive successfully fetched."); - goto REQUEST_END_CLEANUP; - } - else - { - spdlog::error("Fetching mod archive failed: {}", curl_easy_strerror(result)); - failed = true; - goto REQUEST_END_CLEANUP; - } - -REQUEST_END_CLEANUP: - curl_easy_cleanup(easyhandle); - fclose(fp); - return failed ? std::optional<fs::path>() : std::optional<fs::path>(downloadPath); -} - -bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecksum) -{ - if (strstr(GetCommandLineA(), VERIFICATION_FLAG)) - { - spdlog::info("Bypassing mod verification due to flag set up."); - return true; - } - - // Update state - modState.state = CHECKSUMING; - - NTSTATUS status; - BCRYPT_ALG_HANDLE algorithmHandle = NULL; - BCRYPT_HASH_HANDLE hashHandle = NULL; - std::vector<uint8_t> hash; - DWORD hashLength = 0; - DWORD resultLength = 0; - std::stringstream ss; - - constexpr size_t bufferSize {1 << 12}; - std::vector<char> buffer(bufferSize, '\0'); - std::ifstream fp(modPath.generic_string(), std::ios::binary); - - // Open an algorithm handle - // This sample passes BCRYPT_HASH_REUSABLE_FLAG with BCryptAlgorithmProvider(...) to load a provider which supports reusable hash - status = BCryptOpenAlgorithmProvider( - &algorithmHandle, // Alg Handle pointer - BCRYPT_SHA256_ALGORITHM, // Cryptographic Algorithm name (null terminated unicode string) - NULL, // Provider name; if null, the default provider is loaded - BCRYPT_HASH_REUSABLE_FLAG); // Flags; Loads a provider which supports reusable hash - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - - // Obtain the length of the hash - status = BCryptGetProperty( - algorithmHandle, // Handle to a CNG object - BCRYPT_HASH_LENGTH, // Property name (null terminated unicode string) - (PBYTE)&hashLength, // Address of the output buffer which recieves the property value - sizeof(hashLength), // Size of the buffer in bytes - &resultLength, // Number of bytes that were copied into the buffer - 0); // Flags - if (!NT_SUCCESS(status)) - { - // goto cleanup; - modState.state = MOD_CORRUPTED; - return false; - } - - // Create a hash handle - status = BCryptCreateHash( - algorithmHandle, // Handle to an algorithm provider - &hashHandle, // A pointer to a hash handle - can be a hash or hmac object - NULL, // Pointer to the buffer that recieves the hash/hmac object - 0, // Size of the buffer in bytes - NULL, // A pointer to a key to use for the hash or MAC - 0, // Size of the key in bytes - 0); // Flags - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - - // Hash archive content - if (!fp.is_open()) - { - spdlog::error("Unable to open archive."); - modState.state = FAILED_READING_ARCHIVE; - return false; - } - fp.seekg(0, fp.beg); - while (fp.good()) - { - fp.read(buffer.data(), bufferSize); - std::streamsize bytesRead = fp.gcount(); - if (bytesRead > 0) - { - status = BCryptHashData(hashHandle, (PBYTE)buffer.data(), bytesRead, 0); - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - } - } - - hash = std::vector<uint8_t>(hashLength); - - // Obtain the hash of the message(s) into the hash buffer - status = BCryptFinishHash( - hashHandle, // Handle to the hash or MAC object - hash.data(), // A pointer to a buffer that receives the hash or MAC value - hashLength, // Size of the buffer in bytes - 0); // Flags - if (!NT_SUCCESS(status)) - { - modState.state = MOD_CORRUPTED; - goto cleanup; - } - - // Convert hash to string using bytes raw values - ss << std::hex << std::setfill('0'); - for (int i = 0; i < hashLength; i++) - { - ss << std::hex << std::setw(2) << static_cast<int>(hash.data()[i]); - } - - spdlog::info("Expected checksum: {}", expectedChecksum.data()); - spdlog::info("Computed checksum: {}", ss.str()); - return expectedChecksum.compare(ss.str()) == 0; - -cleanup: - if (NULL != hashHandle) - { - BCryptDestroyHash(hashHandle); // Handle to hash/MAC object which needs to be destroyed - } - - if (NULL != algorithmHandle) - { - BCryptCloseAlgorithmProvider( - algorithmHandle, // Handle to the algorithm provider which needs to be closed - 0); // Flags - } - - return false; -} - -bool ModDownloader::IsModAuthorized(std::string_view modName, std::string_view modVersion) -{ - if (strstr(GetCommandLineA(), VERIFICATION_FLAG)) - { - spdlog::info("Bypassing mod verification due to flag set up."); - return true; - } - - if (!verifiedMods.contains(modName.data())) - { - return false; - } - - std::unordered_map<std::string, VerifiedModVersion> versions = verifiedMods[modName.data()].versions; - return versions.count(modVersion.data()) != 0; -} - -int GetModArchiveSize(unzFile file, unz_global_info64 info) -{ - int totalSize = 0; - - for (int i = 0; i < info.number_entry; i++) - { - char zipFilename[256]; - unz_file_info64 fileInfo; - unzGetCurrentFileInfo64(file, &fileInfo, zipFilename, sizeof(zipFilename), NULL, 0, NULL, 0); - - totalSize += fileInfo.uncompressed_size; - - if ((i + 1) < info.number_entry) - { - unzGoToNextFile(file); - } - } - - // Reset file pointer for archive extraction - unzGoToFirstFile(file); - - return totalSize; -} - -void ModDownloader::ExtractMod(fs::path modPath) -{ - unzFile file; - std::string name; - fs::path modDirectory; - - file = unzOpen(modPath.generic_string().c_str()); - if (file == NULL) - { - spdlog::error("Cannot open archive located at {}.", modPath.generic_string()); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - unz_global_info64 gi; - int status; - status = unzGetGlobalInfo64(file, &gi); - if (status != UNZ_OK) - { - spdlog::error("Failed getting information from archive (error code: {})", status); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - // Update state - modState.state = EXTRACTING; - modState.total = GetModArchiveSize(file, gi); - modState.progress = 0; - - // Mod directory name (removing the ".zip" fom the archive name) - name = modPath.filename().string(); - name = name.substr(0, name.length() - 4); - modDirectory = GetRemoteModFolderPath() / name; - - for (int i = 0; i < gi.number_entry; i++) - { - char zipFilename[256]; - unz_file_info64 fileInfo; - status = unzGetCurrentFileInfo64(file, &fileInfo, zipFilename, sizeof(zipFilename), NULL, 0, NULL, 0); - - // Extract file - { - std::error_code ec; - fs::path fileDestination = modDirectory / zipFilename; - spdlog::info("=> {}", fileDestination.generic_string()); - - // Create parent directory if needed - if (!std::filesystem::exists(fileDestination.parent_path())) - { - spdlog::info("Parent directory does not exist, creating it.", fileDestination.generic_string()); - if (!std::filesystem::create_directories(fileDestination.parent_path(), ec) && ec.value() != 0) - { - spdlog::error("Parent directory ({}) creation failed.", fileDestination.parent_path().generic_string()); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - } - - // If current file is a directory, create directory... - if (fileDestination.generic_string().back() == '/') - { - // Create directory - if (!std::filesystem::create_directory(fileDestination, ec) && ec.value() != 0) - { - spdlog::error("Directory creation failed: {}", ec.message()); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - } - // ...else create file - else - { - // Ensure file is in zip archive - if (unzLocateFile(file, zipFilename, 0) != UNZ_OK) - { - spdlog::error("File \"{}\" was not found in archive.", zipFilename); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - // Create file - const int bufferSize = 8192; - void* buffer; - int err = UNZ_OK; - FILE* fout = NULL; - - // Open zip file to prepare its extraction - status = unzOpenCurrentFile(file); - if (status != UNZ_OK) - { - spdlog::error("Could not open file {} from archive.", zipFilename); - modState.state = FAILED_READING_ARCHIVE; - goto EXTRACTION_CLEANUP; - } - - // Create destination file - fout = fopen(fileDestination.generic_string().c_str(), "wb"); - if (fout == NULL) - { - spdlog::error("Failed creating destination file."); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - - // Allocate memory for buffer - buffer = (void*)malloc(bufferSize); - if (buffer == NULL) - { - spdlog::error("Error while allocating memory."); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - - // Extract file to destination - do - { - err = unzReadCurrentFile(file, buffer, bufferSize); - if (err < 0) - { - spdlog::error("error {} with zipfile in unzReadCurrentFile", err); - break; - } - if (err > 0) - { - if (fwrite(buffer, (unsigned)err, 1, fout) != 1) - { - spdlog::error("error in writing extracted file\n"); - err = UNZ_ERRNO; - break; - } - } - - // Update extraction stats - modState.progress += bufferSize; - modState.ratio = roundf(static_cast<float>(modState.progress) / modState.total * 100); - } while (err > 0); - - if (err != UNZ_OK) - { - spdlog::error("An error occurred during file extraction (code: {})", err); - modState.state = FAILED_WRITING_TO_DISK; - goto EXTRACTION_CLEANUP; - } - err = unzCloseCurrentFile(file); - if (err != UNZ_OK) - { - spdlog::error("error {} with zipfile in unzCloseCurrentFile", err); - } - - // Cleanup - if (fout) - fclose(fout); - } - } - - // Go to next file - if ((i + 1) < gi.number_entry) - { - status = unzGoToNextFile(file); - if (status != UNZ_OK) - { - spdlog::error("Error while browsing archive files (error code: {}).", status); - goto EXTRACTION_CLEANUP; - } - } - } - -EXTRACTION_CLEANUP: - if (unzClose(file) != MZ_OK) - { - spdlog::error("Failed closing mod archive after extraction."); - } -} - -void ModDownloader::DownloadMod(std::string modName, std::string modVersion) -{ - // Check if mod can be auto-downloaded - if (!IsModAuthorized(std::string_view(modName), std::string_view(modVersion))) - { - spdlog::warn("Tried to download a mod that is not verified, aborting."); - return; - } - - std::thread requestThread( - [this, modName, modVersion]() - { - fs::path archiveLocation; - - // Download mod archive - std::string expectedHash = verifiedMods[modName].versions[modVersion].checksum; - std::optional<fs::path> fetchingResult = FetchModFromDistantStore(std::string_view(modName), std::string_view(modVersion)); - if (!fetchingResult.has_value()) - { - spdlog::error("Something went wrong while fetching archive, aborting."); - modState.state = MOD_FETCHING_FAILED; - goto REQUEST_END_CLEANUP; - } - archiveLocation = fetchingResult.value(); - if (!IsModLegit(archiveLocation, std::string_view(expectedHash))) - { - spdlog::warn("Archive hash does not match expected checksum, aborting."); - modState.state = MOD_CORRUPTED; - goto REQUEST_END_CLEANUP; - } - - // Extract downloaded mod archive - ExtractMod(archiveLocation); - - REQUEST_END_CLEANUP: - try - { - remove(archiveLocation); - } - catch (const std::exception& a) - { - spdlog::error("Error while removing downloaded archive: {}", a.what()); - } - - modState.state = DONE; - spdlog::info("Done downloading {}.", modName); - }); - - requestThread.detach(); -} - -ON_DLL_LOAD_RELIESON("engine.dll", ModDownloader, (ConCommand), (CModule module)) -{ - g_pModDownloader = new ModDownloader(); - g_pModDownloader->FetchModsListFromAPI(); -} - -ADD_SQFUNC( - "bool", NSIsModDownloadable, "string name, string version", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - g_pSquirrel<context>->newarray(sqvm, 0); - - const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1); - const SQChar* modVersion = g_pSquirrel<context>->getstring(sqvm, 2); - - bool result = g_pModDownloader->IsModAuthorized(modName, modVersion); - g_pSquirrel<context>->pushbool(sqvm, result); - - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("void", NSDownloadMod, "string name, string version", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1); - const SQChar* modVersion = g_pSquirrel<context>->getstring(sqvm, 2); - g_pModDownloader->DownloadMod(modName, modVersion); - - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("ModInstallState", NSGetModInstallState, "", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - g_pSquirrel<context>->pushnewstructinstance(sqvm, 4); - - // state - g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.state); - g_pSquirrel<context>->sealstructslot(sqvm, 0); - - // progress - g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.progress); - g_pSquirrel<context>->sealstructslot(sqvm, 1); - - // total - g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.total); - g_pSquirrel<context>->sealstructslot(sqvm, 2); - - // ratio - g_pSquirrel<context>->pushfloat(sqvm, g_pModDownloader->modState.ratio); - g_pSquirrel<context>->sealstructslot(sqvm, 3); - - return SQRESULT_NOTNULL; -} |