// 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 #include #include #include 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(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(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(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; } // 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*>(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(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*>(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& 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; } 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(size) > maxBytes) return false; f.seekg(0, std::ios::beg); out.resize(static_cast(size)); if (!f.read(out.data(), size) || f.gcount() != size) { out.clear(); return false; } return true; } } // namespace util } // namespace dragonx