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>
This commit is contained in:
2026-07-02 19:17:44 -05:00
parent de11850c74
commit 9c533fa485
5 changed files with 152 additions and 144 deletions

View File

@@ -505,6 +505,7 @@ set(APP_SOURCES
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/pool_stats_service.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
@@ -1049,6 +1050,7 @@ if(BUILD_TESTING)
src/util/logger.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp

View File

@@ -10,6 +10,7 @@
#include "daemon_updater.h"
#include "xmrig_updater.h" // util::sha256Hex, util::verifyXmrigSignature (shared crypto)
#include "http_download.h"
#include "logger.h"
#include <curl/curl.h>
@@ -35,24 +36,6 @@ namespace {
constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024; // 256 MiB
constexpr std::size_t kMaxMemberBytes = 128u * 1024 * 1024; // 128 MiB per extracted file
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));
}
// 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<DaemonUpdater*>(clientp);
return up->onDownloadProgress(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
}
std::string baseName(const std::string& path)
{
const auto slash = path.find_last_of("/\\");
@@ -120,64 +103,13 @@ void DaemonUpdater::cancel()
std::string DaemonUpdater::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("[daemon-updater] GET %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
return {};
}
return result;
return httpGetString(url, "[daemon-updater]");
}
bool DaemonUpdater::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("[daemon-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;
return httpDownloadToFile(url, destPath, "[daemon-updater]", kMaxArchiveBytes,
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
}
void DaemonUpdater::startCheck(const std::string& installedVersion)

108
src/util/http_download.cpp Normal file
View File

@@ -0,0 +1,108 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "http_download.h"
#include "logger.h"
#include <curl/curl.h>
#include <cstdio>
#include <filesystem>
#include <system_error>
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));
}
// Bridge libcurl's C progress callback to the caller's std::function: publish live byte counts
// and abort the transfer (return non-zero) when the callback returns false (cancellation).
int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t)
{
auto* fn = static_cast<const std::function<bool(double, double)>*>(clientp);
return (*fn)(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
}
} // namespace
std::string httpGetString(const std::string& url, const char* logTag)
{
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("%s GET %s failed: %s (HTTP %ld)\n",
logTag, url.c_str(), curl_easy_strerror(res), httpCode);
return {};
}
return result;
}
bool httpDownloadToFile(const std::string& url, const std::string& destPath,
const char* logTag, long long maxBytes,
const std::function<bool(double, double)>& onProgress)
{
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, static_cast<curl_off_t>(maxBytes)); // 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, &onProgress);
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("%s download %s failed: %s (HTTP %ld)\n",
logTag, url.c_str(), curl_easy_strerror(res), httpCode);
std::error_code ec; fs::remove(destPath, ec);
return false;
}
return true;
}
} // namespace util
} // namespace dragonx

34
src/util/http_download.h Normal file
View File

@@ -0,0 +1,34 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Shared libcurl download primitives for the in-app updaters (miner + daemon). Both the
// XmrigUpdater and DaemonUpdater workers issued byte-identical libcurl GET/download code —
// including the security-critical TLS-verify and max-body-size options. Sharing it here means
// those options are defined exactly once, so a curl/TLS change can't silently drift between the
// two signature-verified download-and-execute paths. The header stays curl-free (callers that
// only download don't need <curl/curl.h>).
#pragma once
#include <functional>
#include <string>
namespace dragonx {
namespace util {
// GET `url` over TLS (peer + host verified) into a string. Follows redirects; 30s transfer /
// 15s connect timeout; browser-agnostic User-Agent. Returns "" on ANY failure (curl error or
// non-2xx status). `logTag` (e.g. "[xmrig-updater]") prefixes the debug log on failure.
std::string httpGetString(const std::string& url, const char* logTag);
// Download `url` over TLS (peer + host verified) to `destPath`, refusing bodies larger than
// `maxBytes` (a defense ahead of the checksum/signature check). `onProgress(downloaded, total)`
// is invoked live during the transfer; returning false aborts it (used for cancellation). On
// any failure the partial file is removed and false is returned. `logTag` prefixes debug logs.
bool httpDownloadToFile(const std::string& url, const std::string& destPath,
const char* logTag, long long maxBytes,
const std::function<bool(double downloaded, double total)>& onProgress);
} // namespace util
} // namespace dragonx

View File

@@ -8,6 +8,7 @@
#include "xmrig_updater.h"
#include "http_download.h"
#include "logger.h"
#include <curl/curl.h>
@@ -31,24 +32,6 @@ namespace {
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<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));
}
// 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<XmrigUpdater*>(clientp);
return up->onDownloadProgress(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
}
std::string baseName(const std::string& path)
{
const auto slash = path.find_last_of("/\\");
@@ -109,64 +92,13 @@ void XmrigUpdater::cancel()
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;
return httpGetString(url, "[xmrig-updater]");
}
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;
return httpDownloadToFile(url, destPath, "[xmrig-updater]", kMaxArchiveBytes,
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
}
void XmrigUpdater::startCheck(const std::string& installedTag)