fix(bootstrap): keep the archive unless a checksum truly mismatches
Any verification failure deleted the downloaded archive and forced a full multi-GB re-download on the next attempt. But most failures are not a corrupt archive: a missing or unreadable checksum sidecar, or a fetch blip while pulling the checksum, would throw away a perfectly good file. On top of that the checksum parser took the first token of whatever came back, so an HTTP error page could be mistaken for a digest. - The archive is now removed only on a genuine digest mismatch. A missing sidecar, an unusable one, or a transient fetch error keeps the file so the next attempt can reuse or resume it; the no-checksum case still fails closed and keeps the archive. - The checksum parser now accepts a token only if it is exactly the expected length and all hex, lowercasing it; anything else (an error page, a truncated or wrong-width digest) is rejected. - The small-file fetch moved onto the shared capped HTTP helper, and MD5 is consulted only as a fallback when no SHA-256 is published. Tests cover the checksum parser across good, wrong-length, non-hex, and error-page inputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+44
-112
@@ -17,6 +17,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "http_download.h"
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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
|
setProgress(State::Failed, "Verification cancelled"); // the download is kept
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Checksum failure — delete the corrupt file so next attempt re-downloads
|
|
||||||
const std::string why = getProgress().error; // verifyChecksums' own reason
|
const std::string why = getProgress().error; // verifyChecksums' own reason
|
||||||
std::error_code ec;
|
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);
|
setProgress(State::Failed, why.empty() ? std::string("Checksum verification failed") : why);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -872,59 +873,20 @@ bool Bootstrap::cleanChainData(const std::string& dataDir) {
|
|||||||
// Small-file download (checksums)
|
// 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<std::string*>(userp);
|
|
||||||
if (str->size() + total > (64u * 1024)) return 0; // refuse an oversized checksum file -> curl aborts
|
|
||||||
str->append(static_cast<const char*>(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<curl_off_t>(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
|
// Checksum helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
std::string Bootstrap::parseChecksumFile(const std::string& content) {
|
std::string Bootstrap::parseChecksumFile(const std::string& content, std::size_t expectedHexLen) {
|
||||||
// Typical format: "<hex_hash> <filename>" or just "<hex_hash>"
|
|
||||||
// Extract the first whitespace-delimited token.
|
|
||||||
std::istringstream iss(content);
|
std::istringstream iss(content);
|
||||||
std::string token;
|
std::string token;
|
||||||
if (iss >> token) {
|
if (!(iss >> token)) return {};
|
||||||
// Normalise to lowercase
|
if (token.size() != expectedHexLen) return {}; // reject error pages / wrong digests
|
||||||
std::transform(token.begin(), token.end(), token.begin(),
|
for (char c : token)
|
||||||
[](unsigned char c) { return std::tolower(c); });
|
if (!std::isxdigit(static_cast<unsigned char>(c))) return {};
|
||||||
return token;
|
std::transform(token.begin(), token.end(), token.begin(),
|
||||||
}
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
return {};
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Bootstrap::computeSHA256(const std::string& filePath, float pctBase, float pctRange) {
|
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) {
|
bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& baseUrl) {
|
||||||
|
last_verify_mismatch_ = false;
|
||||||
setProgress(State::Verifying, "Downloading checksums...");
|
setProgress(State::Verifying, "Downloading checksums...");
|
||||||
|
|
||||||
std::string sha256Url = baseUrl + "/" + kZipName + ".sha256";
|
HttpGetOptions opt; opt.cancel = &cancel_requested_;
|
||||||
std::string md5Url = baseUrl + "/" + kZipName + ".md5";
|
|
||||||
|
|
||||||
std::string sha256Content = downloadSmallFile(sha256Url);
|
|
||||||
std::string md5Content = downloadSmallFile(md5Url);
|
|
||||||
|
|
||||||
|
std::string sha256Content = httpGetString(baseUrl + "/" + kZipName + ".sha256", "[bootstrap]", opt);
|
||||||
if (cancel_requested_) return false;
|
if (cancel_requested_) return false;
|
||||||
|
std::string expectedSha = parseChecksumFile(sha256Content, 64);
|
||||||
|
const bool haveSHA256 = !expectedSha.empty();
|
||||||
|
|
||||||
bool haveSHA256 = !sha256Content.empty();
|
std::string expectedMd5;
|
||||||
bool haveMD5 = !md5Content.empty();
|
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) {
|
if (!haveSHA256 && !haveMD5) {
|
||||||
// Fail CLOSED: refuse to install an unverified multi-GB archive (mirrors the xmrig/daemon
|
DEBUG_LOGF("[Bootstrap] No checksum published - refusing to install an unverified archive\n");
|
||||||
// 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");
|
|
||||||
setProgress(State::Failed, "Verification failed: no checksum was published for the bootstrap. "
|
setProgress(State::Failed, "Verification failed: no checksum was published for the bootstrap. "
|
||||||
"Refusing to install an unverified archive.");
|
"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) {
|
if (haveSHA256) {
|
||||||
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1); // percent = 0
|
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1);
|
||||||
std::string expected = parseChecksumFile(sha256Content);
|
std::string actual = computeSHA256(zipPath, 0.0f, 100.0f);
|
||||||
std::string actual = computeSHA256(zipPath, sha256Base, sha256Range);
|
|
||||||
|
|
||||||
if (cancel_requested_) return false;
|
if (cancel_requested_) return false;
|
||||||
|
if (actual.empty()) { setProgress(State::Failed, "SHA-256 verification error"); return false; }
|
||||||
if (expected.empty() || actual.empty()) {
|
if (expectedSha != actual) {
|
||||||
DEBUG_LOGF("[Bootstrap] SHA-256: could not compute/parse (expected=%s, actual=%s)\n",
|
last_verify_mismatch_ = true;
|
||||||
expected.c_str(), actual.c_str());
|
|
||||||
setProgress(State::Failed, "SHA-256 verification error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expected != actual) {
|
|
||||||
DEBUG_LOGF("[Bootstrap] SHA-256 MISMATCH!\n expected: %s\n actual: %s\n",
|
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,
|
setProgress(State::Failed,
|
||||||
"SHA-256 mismatch — the server's checksum file may be out of date.\n"
|
"SHA-256 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n"
|
||||||
"Expected: " + expected.substr(0, 16) + "...\n"
|
"Expected: " + expectedSha.substr(0, 16) + "...\n"
|
||||||
"Got: " + actual.substr(0, 16) + "...\n"
|
"Got: " + actual.substr(0, 16) + "...\n"
|
||||||
"Try again or use a fresh download.");
|
"Try again or use a fresh download.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
|
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
|
||||||
}
|
} else { // haveMD5
|
||||||
|
setProgress(State::Verifying, "Verifying MD5...", 0, 1);
|
||||||
// --- MD5 ---
|
std::string actual = computeMD5(zipPath, 0.0f, 100.0f);
|
||||||
if (haveMD5) {
|
|
||||||
setProgress(State::Verifying, "Verifying MD5...", (double)md5Base, 100.0);
|
|
||||||
std::string expected = parseChecksumFile(md5Content);
|
|
||||||
std::string actual = computeMD5(zipPath, md5Base, md5Range);
|
|
||||||
|
|
||||||
if (cancel_requested_) return false;
|
if (cancel_requested_) return false;
|
||||||
|
if (actual.empty()) { setProgress(State::Failed, "MD5 verification error"); return false; }
|
||||||
if (expected.empty() || actual.empty()) {
|
if (expectedMd5 != actual) {
|
||||||
DEBUG_LOGF("[Bootstrap] MD5: could not compute/parse (expected=%s, actual=%s)\n",
|
last_verify_mismatch_ = true;
|
||||||
expected.c_str(), actual.c_str());
|
DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n",
|
||||||
setProgress(State::Failed, "MD5 verification error");
|
expectedMd5.c_str(), actual.c_str());
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expected != actual) {
|
|
||||||
DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n",
|
|
||||||
expected.c_str(), actual.c_str());
|
|
||||||
setProgress(State::Failed,
|
setProgress(State::Failed,
|
||||||
"MD5 mismatch — the server's checksum file may be out of date.\n"
|
"MD5 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n"
|
||||||
"Expected: " + expected + "\n"
|
"Expected: " + expectedMd5 + "\nGot: " + actual + "\n"
|
||||||
"Got: " + actual + "\n"
|
|
||||||
"Try again or use a fresh download.");
|
"Try again or use a fresh download.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ public:
|
|||||||
/// stripped from each component, so "./wallet.dat" and "DRAGONX.conf." are protected too.
|
/// stripped from each component, so "./wallet.dat" and "DRAGONX.conf." are protected too.
|
||||||
static bool isProtectedEntry(const std::string& name);
|
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:
|
private:
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
Progress progress_;
|
Progress progress_;
|
||||||
@@ -158,9 +162,6 @@ private:
|
|||||||
bool cleanChainData(const std::string& dataDir);
|
bool cleanChainData(const std::string& dataDir);
|
||||||
void setProgress(State state, const std::string& text, double downloaded = 0, double total = 0);
|
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.
|
/// Compute SHA-256 of a file, return lowercase hex digest.
|
||||||
/// pctBase/pctRange map file progress onto a portion of the overall percent.
|
/// 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);
|
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.
|
/// 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);
|
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 "<hash> <filename>" format).
|
|
||||||
static std::string parseChecksumFile(const std::string& content);
|
|
||||||
|
|
||||||
/// Verify downloaded zip against remote checksums. Returns true if valid.
|
/// Verify downloaded zip against remote checksums. Returns true if valid.
|
||||||
bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl);
|
bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl);
|
||||||
|
|
||||||
@@ -188,6 +186,10 @@ private:
|
|||||||
int last_curl_{0};
|
int last_curl_{0};
|
||||||
std::string download_error_;
|
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)
|
// libcurl progress callback (static → calls into instance via clientp)
|
||||||
static int progressCallback(void* clientp, long long dltotal, long long dlnow,
|
static int progressCallback(void* clientp, long long dltotal, long long dlnow,
|
||||||
long long ultotal, long long ulnow);
|
long long ultotal, long long ulnow);
|
||||||
|
|||||||
@@ -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()); }
|
{ 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("<!DOCTYPE html><html>...", 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()
|
void testBootstrapSpaceSufficient()
|
||||||
{
|
{
|
||||||
using dragonx::util::Bootstrap;
|
using dragonx::util::Bootstrap;
|
||||||
@@ -18432,6 +18447,7 @@ int main()
|
|||||||
testResourceNeedsWrite();
|
testResourceNeedsWrite();
|
||||||
testXmrigProvisionPolicy();
|
testXmrigProvisionPolicy();
|
||||||
testBootstrapSpaceSufficient();
|
testBootstrapSpaceSufficient();
|
||||||
|
testBootstrapParseChecksum();
|
||||||
testBootstrapPlanArchive();
|
testBootstrapPlanArchive();
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
testBootstrapReclaimable();
|
testBootstrapReclaimable();
|
||||||
|
|||||||
Reference in New Issue
Block a user