// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // XmrigUpdater background worker (libcurl download + miniz extract). The pure, no-I/O helpers it // calls (release parsing, asset/platform matching, checksum parsing, SHA-256) live in // xmrig_updater_core.cpp so they can be unit-tested without curl/miniz. #include "xmrig_updater.h" #include "logger.h" #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace dragonx { namespace util { namespace { // Reject anything implausibly large for a CPU miner archive, before it can fill memory/disk — // a defense even ahead of the checksum check (which only runs once the file is fully downloaded). constexpr curl_off_t kMaxArchiveBytes = 64LL * 1024 * 1024; // 64 MiB constexpr std::size_t kMaxMemberBytes = 64u * 1024 * 1024; // 64 MiB per extracted file size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp) { static_cast(userp)->append(static_cast(contents), size * nmemb); return size * nmemb; } size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp) { return std::fwrite(contents, size, nmemb, static_cast(userp)); } // libcurl progress callback: publish live byte counts and abort the transfer on cancel(). int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t) { auto* up = static_cast(clientp); return up->onDownloadProgress(static_cast(dlnow), static_cast(dltotal)) ? 0 : 1; } std::string baseName(const std::string& path) { const auto slash = path.find_last_of("/\\"); return slash == std::string::npos ? path : path.substr(slash + 1); } } // namespace XmrigUpdater::~XmrigUpdater() { cancel_requested_ = true; if (worker_.joinable()) worker_.join(); } void XmrigUpdater::setProgress(State state, const std::string& text, double done, double total) { std::lock_guard lk(mutex_); progress_.state = state; progress_.status_text = text; if (done > 0) progress_.downloaded_bytes = done; if (total > 0) progress_.total_bytes = total; progress_.percent = (progress_.total_bytes > 0) ? static_cast(100.0 * progress_.downloaded_bytes / progress_.total_bytes) : progress_.percent; if (state == State::Failed) progress_.error = text; } XmrigUpdater::Progress XmrigUpdater::getProgress() const { std::lock_guard lk(mutex_); return progress_; } bool XmrigUpdater::onDownloadProgress(double downloadedBytes, double totalBytes) { { std::lock_guard lk(mutex_); progress_.downloaded_bytes = downloadedBytes; if (totalBytes > 0) progress_.total_bytes = totalBytes; progress_.percent = (progress_.total_bytes > 0) ? static_cast(100.0 * progress_.downloaded_bytes / progress_.total_bytes) : progress_.percent; } return !cancel_requested_.load(); // false -> curl aborts the transfer } bool XmrigUpdater::isDone() const { std::lock_guard lk(mutex_); return progress_.state == State::Done || progress_.state == State::Failed || progress_.state == State::Unavailable; } void XmrigUpdater::cancel() { cancel_requested_ = true; } std::string XmrigUpdater::httpGet(const std::string& url) { CURL* curl = curl_easy_init(); if (!curl) return {}; std::string result; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); const CURLcode res = curl_easy_perform(curl); long httpCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) { DEBUG_LOGF("[xmrig-updater] GET %s failed: %s (HTTP %ld)\n", url.c_str(), curl_easy_strerror(res), httpCode); return {}; } return result; } bool XmrigUpdater::downloadToFile(const std::string& url, const std::string& destPath) { FILE* fp = std::fopen(destPath.c_str(), "wb"); if (!fp) return false; CURL* curl = curl_easy_init(); if (!curl) { std::fclose(fp); return false; } curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L); curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, kMaxArchiveBytes); // refuse oversized bodies // Live progress + cancellation: the callback publishes byte counts and aborts on cancel(). curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); const CURLcode res = curl_easy_perform(curl); long httpCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); std::fclose(fp); if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) { DEBUG_LOGF("[xmrig-updater] download %s failed: %s (HTTP %ld)\n", url.c_str(), curl_easy_strerror(res), httpCode); std::error_code ec; fs::remove(destPath, ec); return false; } return true; } void XmrigUpdater::startCheck(const std::string& installedTag) { if (worker_running_.exchange(true)) return; cancel_requested_ = false; if (worker_.joinable()) worker_.join(); { std::lock_guard lk(mutex_); progress_ = Progress{}; progress_.installed_tag = installedTag; progress_.state = State::Checking; progress_.status_text = "Checking for the latest miner…"; } worker_ = std::thread([this, installedTag] { runCheck(installedTag); worker_running_ = false; }); } void XmrigUpdater::startInstall(const std::string& targetDir) { if (worker_running_.exchange(true)) return; cancel_requested_ = false; if (worker_.joinable()) worker_.join(); { std::lock_guard lk(mutex_); progress_.state = State::Downloading; progress_.error.clear(); progress_.status_text = "Preparing…"; } worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; }); } void XmrigUpdater::runCheck(std::string installedTag) { const std::string body = httpGet(kApiUrl); if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; } const XmrigRelease rel = parseXmrigRelease(body); if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; } const std::string token = currentXmrigPlatformToken(); const int idx = selectXmrigAsset(rel, token); { std::lock_guard lk(mutex_); progress_.latest_tag = rel.tag; progress_.installed_tag = installedTag; } if (idx < 0) { setProgress(State::Unavailable, "No miner build is available for this platform (" + (token.empty() ? "unknown" : token) + ")."); return; } const bool updateAvailable = installedTag.empty() || installedTag != rel.tag; { std::lock_guard lk(mutex_); progress_.update_available = updateAvailable; } if (updateAvailable) setProgress(State::UpdateAvailable, "A new miner is available (" + rel.tag + ")."); else setProgress(State::UpToDate, "The miner is up to date (" + rel.tag + ")."); } void XmrigUpdater::runInstall(std::string targetDir) { setProgress(State::Checking, "Checking for the latest miner…"); const std::string apiBody = httpGet(kApiUrl); if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; } const XmrigRelease rel = parseXmrigRelease(apiBody); if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; } const std::string token = currentXmrigPlatformToken(); const int idx = selectXmrigAsset(rel, token); if (idx < 0) { setProgress(State::Unavailable, "No miner build is available for this platform (" + (token.empty() ? "unknown" : token) + ")."); return; } const XmrigReleaseAsset& asset = rel.assets[idx]; const auto checksums = parseXmrigChecksums(rel.body); { std::lock_guard lk(mutex_); progress_.latest_tag = rel.tag; } if (cancel_requested_) { setProgress(State::Failed, "Cancelled."); return; } std::error_code ec; fs::create_directories(targetDir, ec); // 1. Download the archive. const std::string zipPath = (fs::path(targetDir) / ".drg-xmrig-download.zip").string(); setProgress(State::Downloading, "Downloading " + asset.name + "…", 0, static_cast(asset.size)); if (!downloadToFile(asset.downloadUrl, zipPath)) { setProgress(State::Failed, "Download failed."); return; } if (cancel_requested_) { fs::remove(zipPath, ec); setProgress(State::Failed, "Cancelled."); return; } // 2. Verify the downloaded archive. Read it once, then (a) compare its SHA-256 to the published // checksum and (b) — when a signing key is pinned — verify a detached ed25519 signature over // the archive bytes against that key, so a checksum rewritten in a tampered release body is // not sufficient to install. setProgress(State::Verifying, "Verifying download…"); { std::ifstream f(zipPath, std::ios::binary); if (!f) { fs::remove(zipPath, ec); setProgress(State::Failed, "Could not read the downloaded archive."); return; } const std::string bytes((std::istreambuf_iterator(f)), std::istreambuf_iterator()); if (f.bad()) { fs::remove(zipPath, ec); setProgress(State::Failed, "Could not read the downloaded archive."); return; } // 2a. SHA-256 (integrity / transit corruption). const std::string actual = sha256Hex(bytes.data(), bytes.size()); const auto it = checksums.find(asset.name); if (it == checksums.end()) { fs::remove(zipPath, ec); setProgress(State::Failed, "No published checksum for " + asset.name + " — refusing to install."); return; } if (actual != it->second) { fs::remove(zipPath, ec); setProgress(State::Failed, "Archive checksum mismatch — refusing to install (possible tampering)."); return; } // 2b. Detached signature (authenticity) — only when a public key is pinned (opt-in). const std::string pubKey = kXmrigSignaturePublicKeyBase64; if (!pubKey.empty()) { const int sigIdx = selectXmrigSignatureAsset(rel, asset.name); if (sigIdx < 0) { if (kXmrigRequireSignature) { fs::remove(zipPath, ec); setProgress(State::Failed, "No signature published for this release — refusing to install."); return; } DEBUG_LOGF("[xmrig-updater] no signature asset for %s; proceeding on checksum only\n", asset.name.c_str()); } else { setProgress(State::Verifying, "Verifying signature…"); const std::string sigContent = httpGet(rel.assets[sigIdx].downloadUrl); if (sigContent.empty() || !verifyXmrigSignature(bytes, sigContent, pubKey)) { fs::remove(zipPath, ec); setProgress(State::Failed, "Signature verification failed — refusing to install."); return; } } } } // 3. Extract the wanted binaries (flatten the versioned subdir). No per-member hash check is // needed: the whole archive was already verified above (SHA-256, and the ed25519 signature // when a key is pinned), so every member is authentic by transitivity. (A per-member check // keyed on the inner filename is also ambiguous — e.g. both the linux and macOS archives // contain a binary literally named "xmrig".) setProgress(State::Extracting, "Installing miner…"); const std::vector wanted = xmrigExtractBasenames(token); const std::string minerName = wanted.front(); // "xmrig" / "xmrig.exe" mz_zip_archive zip{}; if (!mz_zip_reader_init_file(&zip, zipPath.c_str(), 0)) { fs::remove(zipPath, ec); setProgress(State::Failed, "Could not open the downloaded archive."); return; } bool minerInstalled = false; bool failed = false; const int numFiles = static_cast(mz_zip_reader_get_num_files(&zip)); for (int i = 0; i < numFiles && !failed; ++i) { mz_zip_archive_file_stat st; if (!mz_zip_reader_file_stat(&zip, i, &st)) continue; if (mz_zip_reader_is_file_a_directory(&zip, i)) continue; const std::string base = baseName(st.m_filename); if (std::find(wanted.begin(), wanted.end(), base) == wanted.end()) continue; // skip config/readme // Reject an implausibly large member before decompressing it into memory. if (st.m_uncomp_size > kMaxMemberBytes) { failed = true; break; } size_t outSize = 0; void* mem = mz_zip_reader_extract_to_heap(&zip, i, &outSize, 0); if (!mem) { failed = true; break; } // Atomic write: temp file in target dir, then rename over the destination. const std::string finalPath = (fs::path(targetDir) / base).string(); const std::string tmpPath = finalPath + ".tmp"; { std::ofstream of(tmpPath, std::ios::binary | std::ios::trunc); if (!of) { mz_free(mem); failed = true; break; } of.write(static_cast(mem), static_cast(outSize)); } mz_free(mem); fs::remove(finalPath, ec); // Windows rename won't clobber an existing file fs::rename(tmpPath, finalPath, ec); if (ec) { fs::remove(tmpPath, ec); failed = true; break; } #ifndef _WIN32 if (base == minerName) { fs::permissions(finalPath, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec, fs::perm_options::replace, ec); } #endif if (base == minerName) minerInstalled = true; } mz_zip_reader_end(&zip); fs::remove(zipPath, ec); if (failed) { setProgress(State::Failed, "Could not verify/install the miner binary."); return; } if (!minerInstalled) { setProgress(State::Failed, "Miner binary not found in the archive."); return; } setProgress(State::Done, "Miner installed (" + rel.tag + ")."); } } // namespace util } // namespace dragonx