feat(mining): xmrig updater service — fetch/verify/install the latest miner from Gitea
Adds util/XmrigUpdater: a background-thread service (mirrors util/Bootstrap) that pulls the latest DRG-XMRig release from the project's Gitea, verifies it, and installs the miner binary into the daemon directory. Service layer only; the mining-tab UI hook comes next. Flow: GET /api/v1/repos/DragonX/drg-xmrig/releases/latest -> pick the asset matching this platform (…-linux-x64.zip / …-win-x64.zip; no macOS build -> graceful "unavailable") -> download (libcurl, TLS verified) -> verify the archive SHA-256 -> extract with miniz, flattening the versioned subdir the archive nests the binary in -> verify the extracted binary's SHA-256 in memory before writing it -> atomic install (+chmod +x on POSIX). On Windows also extracts WinRing0x64.sys; config.json/README.md are skipped. Security (download-and-execute): TLS is verified, and BOTH the archive and the inner binary are checked against the SHA-256 checksums published in the release body (parsed as "<hex> <name>" lines) — install is refused on a missing or mismatched checksum. Split into a pure core (xmrig_updater_core.cpp: release parse, asset/platform match, checksum parse, SHA-256) and the curl/miniz worker (xmrig_updater.cpp). The core is unit-tested against a real captured release fixture (tests/fixtures/xmrig/release_latest.json); an env-gated (DRAGONX_TEST_NETWORK=1) integration test exercises the worker live and was verified end-to-end on linux-x64 (inner binary SHA-256 matches the published value). Both variants build; suite passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
325
src/util/xmrig_updater.cpp
Normal file
325
src/util/xmrig_updater.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
// 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 <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 {
|
||||
|
||||
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||
{
|
||||
static_cast<std::string*>(userp)->append(static_cast<char*>(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<FILE*>(userp));
|
||||
}
|
||||
|
||||
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::isDone() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
return progress_.state == State::Done || progress_.state == State::Failed;
|
||||
}
|
||||
|
||||
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_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<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…";
|
||||
}
|
||||
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<std::mutex> lk(mutex_);
|
||||
progress_.latest_tag = rel.tag;
|
||||
progress_.installed_tag = installedTag;
|
||||
}
|
||||
if (idx < 0) {
|
||||
setProgress(State::Failed, "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; }
|
||||
|
||||
const std::string token = currentXmrigPlatformToken();
|
||||
const int idx = selectXmrigAsset(rel, token);
|
||||
if (idx < 0) {
|
||||
setProgress(State::Failed, "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 archive SHA-256 against the published checksum (refuse on missing/mismatch).
|
||||
setProgress(State::Verifying, "Verifying download…");
|
||||
{
|
||||
std::ifstream f(zipPath, std::ios::binary);
|
||||
const std::string bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Extract the wanted binaries (flatten the versioned subdir), verifying the miner binary's
|
||||
// SHA-256 in memory before writing the executable to disk.
|
||||
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
|
||||
|
||||
size_t outSize = 0;
|
||||
void* mem = mz_zip_reader_extract_to_heap(&zip, i, &outSize, 0);
|
||||
if (!mem) { failed = true; break; }
|
||||
|
||||
// For the miner binary, verify the inner-binary SHA-256 before writing the executable.
|
||||
if (base == minerName) {
|
||||
const std::string actual = sha256Hex(mem, outSize);
|
||||
const auto it = checksums.find(minerName);
|
||||
if (it == checksums.end() || actual != it->second) {
|
||||
mz_free(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
|
||||
Reference in New Issue
Block a user