diff --git a/src/util/bootstrap.cpp b/src/util/bootstrap.cpp index 66fbde0..160c356 100644 --- a/src/util/bootstrap.cpp +++ b/src/util/bootstrap.cpp @@ -17,6 +17,7 @@ #include #include #include "../util/logger.h" +#include "http_download.h" namespace fs = std::filesystem; @@ -277,10 +278,10 @@ void Bootstrap::runDownloadAndInstall(const std::string& dataDir, const std::str setProgress(State::Failed, "Verification cancelled"); // the download is kept return; } - // Checksum failure — delete the corrupt file so next attempt re-downloads const std::string why = getProgress().error; // verifyChecksums' own reason std::error_code ec; - fs::remove(zipPath, ec); + if (last_verify_mismatch_) // delete only a genuinely corrupt archive; + fs::remove(zipPath, ec); // keep it on a missing/unusable sidecar or fetch blip setProgress(State::Failed, why.empty() ? std::string("Checksum verification failed") : why); return; } @@ -872,59 +873,20 @@ bool Bootstrap::cleanChainData(const std::string& dataDir) { // Small-file download (checksums) // --------------------------------------------------------------------------- -static size_t writeStringCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t total = size * nmemb; - auto* str = static_cast(userp); - if (str->size() + total > (64u * 1024)) return 0; // refuse an oversized checksum file -> curl aborts - str->append(static_cast(contents), total); - return total; -} - -std::string Bootstrap::downloadSmallFile(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, writeStringCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast(64 * 1024)); - 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); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, abortOnCancelCallback); - curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &cancel_requested_); - - CURLcode res = curl_easy_perform(curl); - curl_easy_cleanup(curl); - - if (res != CURLE_OK) { - DEBUG_LOGF("[Bootstrap] Failed to download %s: %s\n", url.c_str(), curl_easy_strerror(res)); - return {}; - } - return result; -} - // --------------------------------------------------------------------------- // Checksum helpers // --------------------------------------------------------------------------- -std::string Bootstrap::parseChecksumFile(const std::string& content) { - // Typical format: " " or just "" - // Extract the first whitespace-delimited token. +std::string Bootstrap::parseChecksumFile(const std::string& content, std::size_t expectedHexLen) { std::istringstream iss(content); std::string token; - if (iss >> token) { - // Normalise to lowercase - std::transform(token.begin(), token.end(), token.begin(), - [](unsigned char c) { return std::tolower(c); }); - return token; - } - return {}; + if (!(iss >> token)) return {}; + if (token.size() != expectedHexLen) return {}; // reject error pages / wrong digests + for (char c : token) + if (!std::isxdigit(static_cast(c))) return {}; + std::transform(token.begin(), token.end(), token.begin(), + [](unsigned char c) { return std::tolower(c); }); + return token; } std::string Bootstrap::computeSHA256(const std::string& filePath, float pctBase, float pctRange) { @@ -1116,91 +1078,61 @@ std::string Bootstrap::computeMD5(const std::string& filePath, float pctBase, fl // --------------------------------------------------------------------------- bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& baseUrl) { + last_verify_mismatch_ = false; setProgress(State::Verifying, "Downloading checksums..."); - std::string sha256Url = baseUrl + "/" + kZipName + ".sha256"; - std::string md5Url = baseUrl + "/" + kZipName + ".md5"; - - std::string sha256Content = downloadSmallFile(sha256Url); - std::string md5Content = downloadSmallFile(md5Url); + HttpGetOptions opt; opt.cancel = &cancel_requested_; + std::string sha256Content = httpGetString(baseUrl + "/" + kZipName + ".sha256", "[bootstrap]", opt); if (cancel_requested_) return false; + std::string expectedSha = parseChecksumFile(sha256Content, 64); + const bool haveSHA256 = !expectedSha.empty(); - bool haveSHA256 = !sha256Content.empty(); - bool haveMD5 = !md5Content.empty(); + std::string expectedMd5; + bool haveMD5 = false; + if (!haveSHA256) { // MD5 only as a fallback when no SHA-256 is published + std::string md5Content = httpGetString(baseUrl + "/" + kZipName + ".md5", "[bootstrap]", opt); + if (cancel_requested_) return false; + expectedMd5 = parseChecksumFile(md5Content, 32); + haveMD5 = !expectedMd5.empty(); + } if (!haveSHA256 && !haveMD5) { - // Fail CLOSED: refuse to install an unverified multi-GB archive (mirrors the xmrig/daemon - // updaters, which refuse when no checksum is published). Trusting an unverified download of - // this size is a real integrity/security risk; a missing checksum is a server-side gap to fix. - DEBUG_LOGF("[Bootstrap] No checksum published — refusing to install an unverified archive\n"); + DEBUG_LOGF("[Bootstrap] No checksum published - refusing to install an unverified archive\n"); setProgress(State::Failed, "Verification failed: no checksum was published for the bootstrap. " "Refusing to install an unverified archive."); - return false; + return false; // fail CLOSED; archive is KEPT } - // Determine progress ranges: if both checksums exist, split 0-50% / 50-100% - float sha256Base = 0.0f, sha256Range = 0.0f; - float md5Base = 0.0f, md5Range = 0.0f; - if (haveSHA256 && haveMD5) { - sha256Base = 0.0f; sha256Range = 50.0f; - md5Base = 50.0f; md5Range = 50.0f; - } else if (haveSHA256) { - sha256Base = 0.0f; sha256Range = 100.0f; - } else { - md5Base = 0.0f; md5Range = 100.0f; - } - - // --- SHA-256 --- if (haveSHA256) { - setProgress(State::Verifying, "Verifying SHA-256...", 0, 1); // percent = 0 - std::string expected = parseChecksumFile(sha256Content); - std::string actual = computeSHA256(zipPath, sha256Base, sha256Range); - + setProgress(State::Verifying, "Verifying SHA-256...", 0, 1); + std::string actual = computeSHA256(zipPath, 0.0f, 100.0f); if (cancel_requested_) return false; - - if (expected.empty() || actual.empty()) { - DEBUG_LOGF("[Bootstrap] SHA-256: could not compute/parse (expected=%s, actual=%s)\n", - expected.c_str(), actual.c_str()); - setProgress(State::Failed, "SHA-256 verification error"); - return false; - } - - if (expected != actual) { + if (actual.empty()) { setProgress(State::Failed, "SHA-256 verification error"); return false; } + if (expectedSha != actual) { + last_verify_mismatch_ = true; DEBUG_LOGF("[Bootstrap] SHA-256 MISMATCH!\n expected: %s\n actual: %s\n", - expected.c_str(), actual.c_str()); + expectedSha.c_str(), actual.c_str()); setProgress(State::Failed, - "SHA-256 mismatch — the server's checksum file may be out of date.\n" - "Expected: " + expected.substr(0, 16) + "...\n" + "SHA-256 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n" + "Expected: " + expectedSha.substr(0, 16) + "...\n" "Got: " + actual.substr(0, 16) + "...\n" "Try again or use a fresh download."); return false; } DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str()); - } - - // --- MD5 --- - if (haveMD5) { - setProgress(State::Verifying, "Verifying MD5...", (double)md5Base, 100.0); - std::string expected = parseChecksumFile(md5Content); - std::string actual = computeMD5(zipPath, md5Base, md5Range); - + } else { // haveMD5 + setProgress(State::Verifying, "Verifying MD5...", 0, 1); + std::string actual = computeMD5(zipPath, 0.0f, 100.0f); if (cancel_requested_) return false; - - if (expected.empty() || actual.empty()) { - DEBUG_LOGF("[Bootstrap] MD5: could not compute/parse (expected=%s, actual=%s)\n", - expected.c_str(), actual.c_str()); - setProgress(State::Failed, "MD5 verification error"); - return false; - } - - if (expected != actual) { - DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n", - expected.c_str(), actual.c_str()); + if (actual.empty()) { setProgress(State::Failed, "MD5 verification error"); return false; } + if (expectedMd5 != actual) { + last_verify_mismatch_ = true; + DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n", + expectedMd5.c_str(), actual.c_str()); setProgress(State::Failed, - "MD5 mismatch — the server's checksum file may be out of date.\n" - "Expected: " + expected + "\n" - "Got: " + actual + "\n" + "MD5 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n" + "Expected: " + expectedMd5 + "\nGot: " + actual + "\n" "Try again or use a fresh download."); return false; } diff --git a/src/util/bootstrap.h b/src/util/bootstrap.h index a28d206..11defc2 100644 --- a/src/util/bootstrap.h +++ b/src/util/bootstrap.h @@ -137,6 +137,10 @@ public: /// stripped from each component, so "./wallet.dat" and "DRAGONX.conf." are protected too. static bool isProtectedEntry(const std::string& name); + /// First whitespace-delimited token of a checksum file, lowercased, iff it is exactly + /// expectedHexLen hex chars; "" otherwise (an HTTP error page is never a checksum). + static std::string parseChecksumFile(const std::string& content, std::size_t expectedHexLen); + private: mutable std::mutex mutex_; Progress progress_; @@ -158,9 +162,6 @@ private: bool cleanChainData(const std::string& dataDir); void setProgress(State state, const std::string& text, double downloaded = 0, double total = 0); - /// Download a small text file (checksum) into a string. Returns empty on failure. - std::string downloadSmallFile(const std::string& url); - /// Compute SHA-256 of a file, return lowercase hex digest. /// pctBase/pctRange map file progress onto a portion of the overall percent. std::string computeSHA256(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f); @@ -169,9 +170,6 @@ private: /// pctBase/pctRange map file progress onto a portion of the overall percent. std::string computeMD5(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f); - /// Parse the first hex token from a checksum file (handles " " format). - static std::string parseChecksumFile(const std::string& content); - /// Verify downloaded zip against remote checksums. Returns true if valid. bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl); @@ -188,6 +186,10 @@ private: int last_curl_{0}; std::string download_error_; + /// Set by verifyChecksums (worker thread only): true only on a real digest MISMATCH, so the caller + /// deletes the archive only then - never for a missing/unusable sidecar or a transient fetch error. + bool last_verify_mismatch_{false}; + // libcurl progress callback (static → calls into instance via clientp) static int progressCallback(void* clientp, long long dltotal, long long dlnow, long long ultotal, long long ulnow); diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 2b596f4..7233fa5 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -3946,6 +3946,21 @@ static void plantOldBootstrapChain(const fs::path& data, const std::string& wall { std::ofstream f(data / "wallet.dat", std::ios::binary); f.write(walletBytes.data(), (std::streamsize)walletBytes.size()); } } +void testBootstrapParseChecksum() +{ + using dragonx::util::Bootstrap; + const std::string h64(64, 'a'), h32(32, 'b'); + EXPECT_EQ(Bootstrap::parseChecksumFile(h64 + " DRAGONX.zip", 64), h64); + { std::string up(64, 'A'); EXPECT_EQ(Bootstrap::parseChecksumFile(up, 64), std::string(64, 'a')); } + EXPECT_EQ(Bootstrap::parseChecksumFile("...", 64), std::string()); + EXPECT_EQ(Bootstrap::parseChecksumFile(std::string(63, 'a'), 64), std::string()); + EXPECT_EQ(Bootstrap::parseChecksumFile(std::string(65, 'a'), 64), std::string()); + { std::string bad(63, 'a'); bad += 'g'; EXPECT_EQ(Bootstrap::parseChecksumFile(bad, 64), std::string()); } + EXPECT_EQ(Bootstrap::parseChecksumFile(h32, 32), h32); + EXPECT_EQ(Bootstrap::parseChecksumFile(h32, 64), std::string()); + EXPECT_EQ(Bootstrap::parseChecksumFile("", 64), std::string()); +} + void testBootstrapSpaceSufficient() { using dragonx::util::Bootstrap; @@ -18432,6 +18447,7 @@ int main() testResourceNeedsWrite(); testXmrigProvisionPolicy(); testBootstrapSpaceSufficient(); + testBootstrapParseChecksum(); testBootstrapPlanArchive(); #ifndef _WIN32 testBootstrapReclaimable();