Before verifying a downloaded archive, both updaters slurped it into a string with an istreambuf_iterator, which grows the buffer repeatedly and reallocates as it reads a multi-megabyte file. It also placed no upper bound on the size it would read into memory. - A shared readFileExact helper sizes the file first and reads it in a single allocation, failing closed if the file is missing, empty, larger than a caller-supplied cap, or comes up short. A truncated read can never reach the SHA-256 / signature verifier. - Both updaters use it with the existing max-archive cap. Tests cover a good read, a read exactly at the cap, over the cap, an empty file, and a missing file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
7.0 KiB
C++
175 lines
7.0 KiB
C++
// 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 <cstdint>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <system_error>
|
|
#include <utility>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
namespace {
|
|
|
|
// Cap for in-memory text/JSON responses (release metadata, price/candle data). Far above any
|
|
// legitimate body, but bounds memory if a hostile/MITM'd server streams an unbounded response.
|
|
|
|
struct StringSink {
|
|
std::string* body;
|
|
long long cap;
|
|
};
|
|
|
|
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
|
{
|
|
auto* sink = static_cast<StringSink*>(userp);
|
|
const size_t n = size * nmemb;
|
|
// Hard cap: a chunked response omits Content-Length, so CURLOPT_MAXFILESIZE cannot catch it —
|
|
// aborting here (short count) makes curl fail the transfer instead of exhausting memory.
|
|
if (httpSinkAccept(sink->body->size(), n, sink->cap) != n) return 0;
|
|
sink->body->append(static_cast<char*>(contents), n);
|
|
return n;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// httpGet's progress callback: abort the transfer (non-zero) once the caller's cancel flag is set.
|
|
int cancelXferCb(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t)
|
|
{
|
|
return static_cast<const std::atomic<bool>*>(clientp)->load(std::memory_order_relaxed) ? 1 : 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
HttpGetResult httpGet(const std::string& url, const char* logTag, const HttpGetOptions& opt)
|
|
{
|
|
HttpGetResult out;
|
|
if (opt.cancel && opt.cancel->load(std::memory_order_relaxed)) {
|
|
out.cancelled = true;
|
|
out.error = "cancelled";
|
|
return out;
|
|
}
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) { out.error = "failed to initialize curl"; return out; }
|
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
|
|
StringSink sink{&out.body, opt.maxBytes};
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, opt.maxRedirects);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, opt.timeoutSec);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, opt.connectTimeoutSec);
|
|
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // no SIGALRM for timeouts: these run on worker threads
|
|
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(opt.maxBytes)); // reject oversized metadata
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
|
if (opt.cancel) {
|
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, cancelXferCb);
|
|
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, const_cast<std::atomic<bool>*>(opt.cancel));
|
|
}
|
|
const CURLcode res = curl_easy_perform(curl);
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &out.httpStatus);
|
|
curl_easy_cleanup(curl);
|
|
out.transportOk = (res == CURLE_OK);
|
|
out.cancelled = (res == CURLE_ABORTED_BY_CALLBACK);
|
|
if (!out.transportOk) out.error = out.cancelled ? "cancelled" : curl_easy_strerror(res);
|
|
if (!out.cancelled && (!out.transportOk || out.httpStatus < 200 || out.httpStatus >= 300)) {
|
|
DEBUG_LOGF("%s GET %s failed: %s (HTTP %ld)\n",
|
|
logTag, url.c_str(), curl_easy_strerror(res), out.httpStatus);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string httpGetString(const std::string& url, const char* logTag, const HttpGetOptions& opt)
|
|
{
|
|
HttpGetResult r = httpGet(url, logTag, opt);
|
|
if (!r.transportOk || r.httpStatus < 200 || r.httpStatus >= 300) return {};
|
|
return std::move(r.body);
|
|
}
|
|
|
|
std::string httpGetString(const std::string& url, const char* logTag,
|
|
long connectTimeoutSec, long timeoutSec)
|
|
{
|
|
HttpGetOptions opt;
|
|
opt.connectTimeoutSec = connectTimeoutSec;
|
|
opt.timeoutSec = timeoutSec;
|
|
return httpGetString(url, logTag, opt);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool readFileExact(const std::string& path, std::string& out, std::uintmax_t maxBytes)
|
|
{
|
|
out.clear();
|
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
if (!f) return false;
|
|
const std::streamoff size = f.tellg();
|
|
if (size <= 0 || static_cast<std::uintmax_t>(size) > maxBytes) return false;
|
|
f.seekg(0, std::ios::beg);
|
|
out.resize(static_cast<std::size_t>(size));
|
|
if (!f.read(out.data(), size) || f.gcount() != size) { out.clear(); return false; }
|
|
return true;
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|