Files
ObsidianDragon/src/util/xmrig_updater.cpp
DanS 9c533fa485 refactor(audit): batch 5 — share the updater HTTP/download layer
XmrigUpdater and DaemonUpdater carried byte-identical libcurl code —
httpGet, downloadToFile, and the write/progress callbacks — including the
security-critical TLS-verify (SSL_VERIFYPEER=1 / SSL_VERIFYHOST=2) and
max-body-size options, duplicated across two signature-verified
download-and-execute files that had to be hand-kept in sync.

Hoist that machinery into src/util/http_download.{h,cpp}:
- util::httpGetString(url, logTag)
- util::httpDownloadToFile(url, dest, logTag, maxBytes, onProgress)
threading progress + cancellation through a std::function (returning false
aborts the transfer) instead of the class `this` pointer. Each updater's
httpGet/downloadToFile become one-line forwarders that bind their own log
tag and archive-size cap (64 MiB miner / 256 MiB node, both preserved).

Every curl option is preserved byte-identically (verified by diffing the
setopt lines against the pre-change code — the only deltas are the
now-parameterized MAXFILESIZE cap and the XFERINFODATA payload). No behavior
change; the TLS/size-guard options now live in exactly one place.

Deliberately scoped DOWN per the audit's own guidance: the divergent
per-updater logic (release DTOs/parsers, checksum-table parsing, version
compare, and installResolved's per-member-SHA vs transitive-trust verify,
install strategy, and chmod scope) stays in each class — NOT merged behind a
base-class/virtual-hook layer, which would raise review cost on
signature-verified execute-code and risk a subtle security regression. The
shared-DTO/parser consolidation (~77 test refs of churn on stable code) is
left as a separate, lower-value follow-up.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:17:44 -05:00

386 lines
15 KiB
C++

// 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 "http_download.h"
#include "logger.h"
#include <curl/curl.h>
#include <miniz.h>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>
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
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<std::mutex> 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<float>(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<std::mutex> lk(mutex_);
return progress_;
}
bool XmrigUpdater::onDownloadProgress(double downloadedBytes, double totalBytes)
{
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.downloaded_bytes = downloadedBytes;
if (totalBytes > 0) progress_.total_bytes = totalBytes;
progress_.percent = (progress_.total_bytes > 0)
? static_cast<float>(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<std::mutex> 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)
{
return httpGetString(url, "[xmrig-updater]");
}
bool XmrigUpdater::downloadToFile(const std::string& url, const std::string& destPath)
{
return httpDownloadToFile(url, destPath, "[xmrig-updater]", kMaxArchiveBytes,
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
}
void XmrigUpdater::startCheck(const std::string& installedTag)
{
if (worker_running_.exchange(true)) return;
cancel_requested_ = false;
if (worker_.joinable()) worker_.join();
{
std::lock_guard<std::mutex> 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<std::mutex> lk(mutex_);
progress_.state = State::Downloading;
progress_.error.clear();
progress_.status_text = "Preparing…";
progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0%
progress_.total_bytes = 0;
progress_.percent = 0.0f;
}
worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; });
}
void XmrigUpdater::startListReleases()
{
if (worker_running_.exchange(true)) return;
cancel_requested_ = false;
if (worker_.joinable()) worker_.join();
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = State::Listing;
progress_.error.clear();
progress_.status_text = "Loading releases…";
progress_.downloaded_bytes = 0; // clear any prior op's progress
progress_.total_bytes = 0;
progress_.percent = 0.0f;
}
worker_ = std::thread([this] { runListReleases(); worker_running_ = false; });
}
void XmrigUpdater::startInstallRelease(const std::string& targetDir, XmrigRelease release)
{
if (worker_running_.exchange(true)) return;
cancel_requested_ = false;
if (worker_.joinable()) worker_.join();
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = State::Downloading;
progress_.error.clear();
progress_.status_text = "Preparing…";
progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0%
progress_.total_bytes = 0;
progress_.percent = 0.0f;
}
worker_ = std::thread([this, targetDir, release = std::move(release)] {
installResolved(targetDir, release);
worker_running_ = false;
});
}
std::vector<XmrigRelease> XmrigUpdater::getReleases() const
{
std::lock_guard<std::mutex> lk(mutex_);
return releases_;
}
void XmrigUpdater::runListReleases()
{
const std::string body = httpGet(kReleasesUrl);
if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
std::vector<XmrigRelease> list = parseXmrigReleaseList(body);
const bool empty = list.empty();
{
std::lock_guard<std::mutex> lk(mutex_);
releases_ = std::move(list);
}
if (empty) { setProgress(State::Failed, "No releases found."); return; }
setProgress(State::ReleaseList, "Select a version to install.");
}
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<std::mutex> 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<std::mutex> 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; }
installResolved(targetDir, rel);
}
void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRelease& rel)
{
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<std::mutex> 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<double>(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<char>(f)), std::istreambuf_iterator<char>());
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<std::string> 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<int>(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<const char*>(mem), static_cast<std::streamsize>(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