From 9c533fa485004cd6b8817a5dd2f21989dc0b89bd Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 19:17:44 -0500 Subject: [PATCH] =?UTF-8?q?refactor(audit):=20batch=205=20=E2=80=94=20shar?= =?UTF-8?q?e=20the=20updater=20HTTP/download=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CMakeLists.txt | 2 + src/util/daemon_updater.cpp | 76 ++----------------------- src/util/http_download.cpp | 108 ++++++++++++++++++++++++++++++++++++ src/util/http_download.h | 34 ++++++++++++ src/util/xmrig_updater.cpp | 76 ++----------------------- 5 files changed, 152 insertions(+), 144 deletions(-) create mode 100644 src/util/http_download.cpp create mode 100644 src/util/http_download.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e581e9..08a00a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/util/daemon_updater.cpp b/src/util/daemon_updater.cpp index a869e85..c8a9cc8 100644 --- a/src/util/daemon_updater.cpp +++ b/src/util/daemon_updater.cpp @@ -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 @@ -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(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("/\\"); @@ -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) diff --git a/src/util/http_download.cpp b/src/util/http_download.cpp new file mode 100644 index 0000000..5fe7ba1 --- /dev/null +++ b/src/util/http_download.cpp @@ -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 + +#include +#include +#include + +namespace fs = std::filesystem; + +namespace dragonx { +namespace util { + +namespace { + +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)); +} + +// 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*>(clientp); + return (*fn)(static_cast(dlnow), static_cast(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& 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(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 diff --git a/src/util/http_download.h b/src/util/http_download.h new file mode 100644 index 0000000..3ec9a8e --- /dev/null +++ b/src/util/http_download.h @@ -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 ). + +#pragma once + +#include +#include + +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& onProgress); + +} // namespace util +} // namespace dragonx diff --git a/src/util/xmrig_updater.cpp b/src/util/xmrig_updater.cpp index 047d120..d9c8d6c 100644 --- a/src/util/xmrig_updater.cpp +++ b/src/util/xmrig_updater.cpp @@ -8,6 +8,7 @@ #include "xmrig_updater.h" +#include "http_download.h" #include "logger.h" #include @@ -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(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("/\\"); @@ -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)