// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // xmrig_manager.cpp — Pool mining process management via drg-xmrig. // Spawns xmrig, monitors via HTTP API, tracks hashrate and shares. #include "xmrig_manager.h" #include "../resources/embedded_resources.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "../util/logger.h" #include "../util/pool_registry.h" #ifdef _WIN32 #include #include #include #include #else #include #include #include #include #include #include #include #endif namespace fs = std::filesystem; using json = nlohmann::json; namespace dragonx { namespace daemon { // ============================================================================ // Helpers // ============================================================================ static std::string randomHexToken(int bytes = 16) { static const char hex[] = "0123456789abcdef"; std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dist(0, 15); std::string out; out.reserve(bytes * 2); for (int i = 0; i < bytes * 2; ++i) out.push_back(hex[dist(gen)]); return out; } static int randomPort() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dist(18000, 18999); return dist(gen); } static std::string getConfigDir() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) { return std::string(path) + "\\ObsidianDragon"; } return "."; #else const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw ? pw->pw_dir : "/tmp"; } return std::string(home) + "/.config/ObsidianDragon"; #endif } // libcurl write callback static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) { auto* s = static_cast(userdata); const size_t add = sz * n; // Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or // MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02) constexpr size_t kMaxStatsBytes = 1u << 20; // 1 MiB if (s->size() + add > kMaxStatsBytes) return 0; s->append(static_cast(ptr), add); return add; } // True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target // that a public mining pool would never be — used to refuse a background stats GET to it (M-09). static bool hostLooksInternal(const std::string& host) { if (host.empty() || host == "localhost") return true; if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 || host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true; if (host.rfind("172.", 0) == 0) { // 172.16.0.0 - 172.31.255.255 const int second = std::atoi(host.c_str() + 4); if (second >= 16 && second <= 31) return true; } if (host.find(':') != std::string::npos) { // IPv6 literal: loopback / ULA / link-local if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 || host.rfind("fe80", 0) == 0) return true; } if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true; if (host.find('.') == std::string::npos) return true; // bare single-label name = LAN/hosts, not a pool return false; } // ============================================================================ // Lifecycle // ============================================================================ XmrigManager::XmrigManager() = default; XmrigManager::~XmrigManager() { should_stop_ = true; if (isRunning()) { stop(3000); } // Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is // false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04) if (monitor_thread_.joinable()) monitor_thread_.join(); } // ============================================================================ // Binary discovery // ============================================================================ std::string XmrigManager::findXmrigBinary() { // Use the embedded_resources system (same pattern as daemon) std::string path = resources::getXmrigPath(); if (!path.empty() && fs::exists(path)) { return path; } // Fallback: system PATH #ifdef _WIN32 FILE* f = _popen("where xmrig.exe 2>nul", "r"); #else FILE* f = popen("which xmrig 2>/dev/null", "r"); #endif if (f) { char line[512]; if (fgets(line, sizeof(line), f)) { std::string s(line); while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back(); if (!s.empty() && fs::exists(s)) { #ifdef _WIN32 _pclose(f); #else pclose(f); #endif return s; } } #ifdef _WIN32 _pclose(f); #else pclose(f); #endif } return {}; } // ============================================================================ // Config generation // ============================================================================ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath) { api_port_ = randomPort(); api_token_ = randomHexToken(16); int hw = (int)std::thread::hardware_concurrency(); if (hw < 1) hw = 1; // Use explicit thread count (not just a hint) threads_ = (cfg.threads > 0) ? cfg.threads : std::max(1, hw / 2); if (threads_ > hw) threads_ = hw; json j; j["autosave"] = false; j["background"] = false; j["colors"] = false; j["http"] = { {"enabled", true}, {"host", "127.0.0.1"}, {"port", api_port_}, {"access-token", api_token_}, {"restricted", true} }; j["randomx"] = { {"init", -1}, {"mode", "auto"}, {"1gb-pages", false}, {"numa", true}, {"scratchpad_prefetch_mode", 1} }; j["cpu"] = { {"enabled", true}, {"huge-pages", cfg.hugepages}, {"max-threads-hint", 100}, // Use 100% of allotted threads {"priority", 0}, // Idle priority (lowest) - prevents UI lag {"yield", true} // Yield to other processes }; j["pools"] = json::array({ { {"algo", cfg.algo}, {"url", cfg.pool_url}, {"user", cfg.wallet_address}, {"pass", cfg.worker_name}, {"keepalive", true}, {"tls", cfg.tls} } }); j["donate-level"] = 0; j["print-time"] = 10; j["retries"] = 5; j["retry-pause"] = 5; try { fs::create_directories(fs::path(outPath).parent_path()); const std::string dumped = j.dump(4); #ifndef _WIN32 // Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never // in a world/group-readable file — even for a local attacker who opened it in the old // create-then-chmod window and held the fd open across the chmod. (L-01) int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd < 0) { setLastError("Cannot write xmrig config: " + outPath); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); return false; } size_t off = 0; bool wrote = true; while (off < dumped.size()) { ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off); if (nw <= 0) { wrote = false; break; } off += static_cast(nw); } ::close(fd); if (!wrote) { setLastError("Cannot write xmrig config: " + outPath); return false; } return true; #else std::ofstream ofs(outPath, std::ios::trunc); if (!ofs.is_open()) { setLastError("Cannot write xmrig config: " + outPath); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); return false; } ofs << dumped; ofs.close(); return true; #endif } catch (const std::exception& e) { setLastError(std::string("Config write error: ") + e.what()); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); return false; } } // ============================================================================ // start / stop // ============================================================================ bool XmrigManager::start(const Config& cfg) { if (state_ == State::Running || state_ == State::Starting) { setLastError("Already running"); DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str()); return false; } state_ = State::Starting; should_stop_ = false; setLastError(std::string()); { std::lock_guard lk(output_mutex_); process_output_.clear(); } { std::lock_guard lk(stats_mutex_); stats_ = PoolStats{}; } // Extract pool hostname for stats API queries { std::string url = cfg.pool_url; // Strip protocol prefix if present auto pos = url.find("://"); if (pos != std::string::npos) url = url.substr(pos + 3); // Strip port suffix pos = url.find(':'); if (pos != std::string::npos) url = url.substr(0, pos); pool_host_ = url; } // Find binary std::string binary = findXmrigBinary(); if (binary.empty()) { setLastError("xmrig binary not found"); state_ = State::Error; DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n"); return false; } DEBUG_LOGF("[INFO] XmrigManager: found binary at %s\n", binary.c_str()); // Generate config std::string cfgDir = getConfigDir(); #ifdef _WIN32 std::string cfgPath = cfgDir + "\\xmrig-pool.json"; #else std::string cfgPath = cfgDir + "/xmrig-pool.json"; #endif if (!generateConfig(cfg, cfgPath)) { state_ = State::Error; DEBUG_LOGF("[ERROR] XmrigManager: failed to generate config\n"); return false; } DEBUG_LOGF("[INFO] XmrigManager: config written to %s (API port %d, threads %d)\n", cfgPath.c_str(), api_port_, threads_); // Spawn process if (!startProcess(binary, cfgPath, threads_)) { state_ = State::Error; return false; } // Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set // State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable // std::thread calls std::terminate() and aborts the whole wallet. (M-04) if (monitor_thread_.joinable()) monitor_thread_.join(); monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this); state_ = State::Running; DEBUG_LOGF("[INFO] XmrigManager: started\n"); return true; } void XmrigManager::stop(int wait_ms) { if (state_ == State::Stopped || state_ == State::Stopping) return; state_ = State::Stopping; should_stop_ = true; #ifdef _WIN32 if (process_handle_) { // Try graceful termination first TerminateProcess(process_handle_, 0); WaitForSingleObject(process_handle_, wait_ms); CloseHandle(process_handle_); process_handle_ = nullptr; } if (stdout_read_) { CloseHandle(stdout_read_); stdout_read_ = nullptr; } #else if (process_pid_ > 0) { // Send SIGTERM to process group kill(-process_pid_, SIGTERM); // Poll-wait with drain auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(wait_ms); while (std::chrono::steady_clock::now() < deadline) { drainOutput(); int status = 0; pid_t ret = waitpid(process_pid_, &status, WNOHANG); if (ret == process_pid_ || ret < 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(50)); } // If still alive, SIGKILL if (kill(process_pid_, 0) == 0) { kill(-process_pid_, SIGKILL); waitpid(process_pid_, nullptr, 0); } process_pid_ = 0; } if (stdout_fd_ >= 0) { close(stdout_fd_); stdout_fd_ = -1; } #endif if (monitor_thread_.joinable()) monitor_thread_.join(); state_ = State::Stopped; DEBUG_LOGF("[INFO] XmrigManager: stopped\n"); } // ============================================================================ // Process spawning — platform-specific // ============================================================================ #ifdef _WIN32 bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) { SECURITY_ATTRIBUTES sa{}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; HANDLE hRead = nullptr, hWrite = nullptr; if (!CreatePipe(&hRead, &hWrite, &sa, 0)) { setLastError("CreatePipe failed"); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); return false; } SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); // Use explicit --threads to enforce exact thread count (not just a hint) std::string cmdLine = "\"" + xmrigPath + "\" --config=\"" + cfgPath + "\" --threads=" + std::to_string(threads); STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.hStdOutput = hWrite; si.hStdError = hWrite; si.wShowWindow = SW_HIDE; PROCESS_INFORMATION pi{}; BOOL ok = CreateProcessA( nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | IDLE_PRIORITY_CLASS, nullptr, nullptr, &si, &pi ); CloseHandle(hWrite); if (!ok) { CloseHandle(hRead); DWORD err = GetLastError(); char errBuf[256]; FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, 0, errBuf, sizeof(errBuf), NULL); setLastError("CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf); DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str()); return false; } process_handle_ = pi.hProcess; stdout_read_ = hRead; CloseHandle(pi.hThread); return true; } bool XmrigManager::isRunning() const { if (!process_handle_) return false; DWORD exit_code; GetExitCodeProcess(process_handle_, &exit_code); return exit_code == STILL_ACTIVE; } double XmrigManager::getMemoryUsageMB() const { if (!process_handle_) return 0.0; PROCESS_MEMORY_COUNTERS pmc; ZeroMemory(&pmc, sizeof(pmc)); pmc.cb = sizeof(pmc); if (GetProcessMemoryInfo(process_handle_, &pmc, sizeof(pmc))) { return static_cast(pmc.WorkingSetSize) / (1024.0 * 1024.0); } return 0.0; } void XmrigManager::drainOutput() { if (!stdout_read_) return; char buf[4096]; DWORD avail = 0; while (PeekNamedPipe(stdout_read_, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) { DWORD nread = 0; DWORD toRead = std::min(avail, (DWORD)sizeof(buf)); if (ReadFile(stdout_read_, buf, toRead, &nread, nullptr) && nread > 0) { std::lock_guard lk(output_mutex_); appendOutput(buf, nread); } else { break; } } } #else // ---- POSIX ---- bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) { int pipefd[2]; if (pipe(pipefd) != 0) { setLastError("pipe() failed"); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); return false; } pid_t pid = fork(); if (pid < 0) { setLastError("fork() failed"); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); close(pipefd[0]); close(pipefd[1]); return false; } if (pid == 0) { // Child close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[1]); // Detach from controlling terminal's stdin to prevent SIGTTIN/SIGTTOU // when running in a new process group (setpgid below). int devnull = open("/dev/null", O_RDONLY); if (devnull >= 0) { dup2(devnull, STDIN_FILENO); close(devnull); } // Ignore job-control signals that a background process group may receive signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); // New process group so we can kill the whole group setpgid(0, 0); // Lowest priority to reduce UI lag (nice value 19 = minimum priority) if (nice(19) == -1 && errno != 0) { /* ignore failure */ } std::string cfgArg = "--config=" + cfgPath; std::string threadsArg = "--threads=" + std::to_string(threads); const char* argv[] = { xmrigPath.c_str(), cfgArg.c_str(), threadsArg.c_str(), nullptr }; execv(xmrigPath.c_str(), const_cast(argv)); _exit(127); } // Parent close(pipefd[1]); process_pid_ = pid; stdout_fd_ = pipefd[0]; // Non-blocking reads int flags = fcntl(stdout_fd_, F_GETFL, 0); fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK); return true; } bool XmrigManager::isRunning() const { // Use state_ instead of waitpid() to avoid races with moitorProcess // which also calls waitpid. state_ is atomic and always correct. State s = state_.load(std::memory_order_relaxed); return (s == State::Running || s == State::Starting); } double XmrigManager::getMemoryUsageMB() const { if (process_pid_ <= 0) return 0.0; #ifdef __APPLE__ // macOS: use ps to read RSS for xmrig process char cmd[128]; snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d 2>/dev/null", process_pid_); FILE* fp = popen(cmd, "r"); if (!fp) return 0.0; char line[64]; double mb = 0.0; if (fgets(line, sizeof(line), fp)) { long rss_kb = atol(line); if (rss_kb > 0) mb = static_cast(rss_kb) / 1024.0; } pclose(fp); return mb; #else char path[64]; snprintf(path, sizeof(path), "/proc/%d/statm", process_pid_); FILE* fp = fopen(path, "r"); if (!fp) return 0.0; long dummy = 0, pages = 0; // statm: size resident shared text lib data dt // We want resident (2nd field) if (fscanf(fp, "%ld %ld", &dummy, &pages) != 2) pages = 0; fclose(fp); long pageSize = sysconf(_SC_PAGESIZE); return static_cast(pages * pageSize) / (1024.0 * 1024.0); #endif } void XmrigManager::drainOutput() { if (stdout_fd_ < 0) return; char buf[4096]; while (true) { ssize_t n = read(stdout_fd_, buf, sizeof(buf)); if (n > 0) { std::lock_guard lk(output_mutex_); appendOutput(buf, (size_t)n); } else { break; } } } #endif // platform // ============================================================================ // Output management // ============================================================================ void XmrigManager::appendOutput(const char* data, size_t len) { // Caller must hold output_mutex_ static constexpr size_t MAX_OUTPUT = 1024 * 1024; // 1 MB cap process_output_.append(data, len); if (process_output_.size() > MAX_OUTPUT) { // Trim from the front at a newline boundary size_t cut = process_output_.size() - MAX_OUTPUT; auto pos = process_output_.find('\n', cut); if (pos != std::string::npos) process_output_.erase(0, pos + 1); else process_output_.erase(0, cut); } } std::vector XmrigManager::getRecentLines(int maxLines) const { std::lock_guard lk(output_mutex_); std::vector lines; if (process_output_.empty()) return lines; // Walk backwards collecting lines size_t end = process_output_.size(); while ((int)lines.size() < maxLines && end > 0) { size_t nl = process_output_.rfind('\n', end - 1); if (nl == std::string::npos) { lines.push_back(process_output_.substr(0, end)); break; } if (nl + 1 < end) lines.push_back(process_output_.substr(nl + 1, end - nl - 1)); end = nl; } std::reverse(lines.begin(), lines.end()); // Remove empty trailing line while (!lines.empty() && lines.back().empty()) lines.pop_back(); return lines; } // ============================================================================ // Monitor thread // ============================================================================ void XmrigManager::monitorProcess() { // Wait a few seconds for xmrig HTTP API to start up before first poll for (int i = 0; i < 30 && !should_stop_; i++) { drainOutput(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } int poll_counter = 0; int pool_api_counter = 0; while (!should_stop_) { drainOutput(); // Check if the child process is still alive (monitor thread only) #ifdef _WIN32 if (process_handle_) { DWORD exitCode = 0; if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) { DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode); state_ = State::Error; setLastError("xmrig process exited unexpectedly"); break; } } #else if (process_pid_ > 0) { int status = 0; pid_t ret = waitpid(process_pid_, &status, WNOHANG); if (ret == process_pid_ || ret < 0) { DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret); state_ = State::Error; setLastError("xmrig process exited unexpectedly"); break; } } #endif // Poll HTTP stats every ~2 seconds (20 * 100ms) if (++poll_counter >= 20) { poll_counter = 0; fetchStatsHttp(); } // Poll pool-side stats every ~30 seconds (300 * 100ms) if (++pool_api_counter >= 300) { pool_api_counter = 0; fetchPoolApiStats(); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } drainOutput(); // Final drain } // ============================================================================ // Stats polling via HTTP API // ============================================================================ void XmrigManager::pollStats() { // No-op on UI thread — stats are fetched by the monitor thread. // Just drain stdout so log lines stay fresh. drainOutput(); } void XmrigManager::fetchStatsHttp() { if (state_ != State::Running) return; // Drain stdout while we're at it drainOutput(); // Build URL char url[256]; snprintf(url, sizeof(url), "http://127.0.0.1:%d/2/summary", api_port_); std::string responseData; CURL* curl = curl_easy_init(); if (!curl) { DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl_easy_init failed\n"); return; } // Set up auth header std::string authHeader = "Authorization: Bearer " + api_token_; struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, authHeader.c_str()); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 2000L); // 2s timeout — generous for loaded system curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 1000L); // 1s connect timeout curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // Avoid signal issues in threads CURLcode res = curl_easy_perform(curl); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { static int s_fail_count = 0; if (++s_fail_count <= 5 || s_fail_count % 30 == 0) { DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl failed (%d): %s url=%s\n", s_fail_count, curl_easy_strerror(res), url); } return; } try { json resp = json::parse(responseData); std::lock_guard lk(stats_mutex_); // Miner version (top-level in /2/summary) — lets the UI show the actually // running miner's version even when no release tag was persisted (bundled miner). if (resp.contains("version") && resp["version"].is_string()) stats_.version = resp["version"].get(); if (resp.contains("hashrate") && resp["hashrate"].contains("total")) { auto& total = resp["hashrate"]["total"]; if (total.is_array() && total.size() >= 3) { stats_.hashrate_10s = total[0].is_null() ? 0.0 : total[0].get(); stats_.hashrate_60s = total[1].is_null() ? 0.0 : total[1].get(); stats_.hashrate_15m = total[2].is_null() ? 0.0 : total[2].get(); } } if (resp.contains("connection")) { auto& conn = resp["connection"]; stats_.accepted = conn.value("accepted", (int64_t)0); stats_.rejected = conn.value("rejected", (int64_t)0); stats_.uptime_sec = conn.value("uptime", (int64_t)0); stats_.pool_diff = conn.value("diff", 0.0); stats_.pool_url = conn.value("pool", std::string{}); stats_.algo = conn.value("algo", std::string{}); stats_.connected = (stats_.uptime_sec > 0); } // Parse memory usage from "resources" section if (resp.contains("resources") && resp["resources"].contains("memory")) { auto& mem = resp["resources"]["memory"]; stats_.memory_free = mem.value("free", (int64_t)0); stats_.memory_total = mem.value("total", (int64_t)0); stats_.memory_used = mem.value("resident_set_memory", (int64_t)0); } // Parse active thread count from hashrate.threads array if (resp.contains("hashrate") && resp["hashrate"].contains("threads")) { auto& threads = resp["hashrate"]["threads"]; if (threads.is_array()) { stats_.threads_active = static_cast(threads.size()); } } else if (resp.contains("cpu") && resp["cpu"].contains("threads")) { // Fallback: get from cpu section stats_.threads_active = resp["cpu"].value("threads", 0); } } catch (...) { // Malformed JSON — ignore, retry next poll } } // ============================================================================ // Pool-side stats (hashrate reported by the pool) // ============================================================================ void XmrigManager::fetchPoolApiStats() { if (state_ != State::Running || pool_host_.empty()) return; // Resolve the stats endpoint + JSON schema for this pool. Known pools carry their // own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore // /api/pools); unknown/custom hosts fall back to the .is convention. const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_); // SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a // loopback/private/link-local/single-label target — those aren't public mining pools, and a // paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted // registry statsUrl and are exempt. (M-09) if (!known && hostLooksInternal(pool_host_)) return; const std::string url = known ? known->statsUrl : ("https://" + pool_host_ + "/api/stats"); std::string responseData; CURL* curl = curl_easy_init(); if (!curl) return; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents. curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)"); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); CURLcode res = curl_easy_perform(curl); curl_easy_cleanup(curl); if (res != CURLE_OK) return; bool ok = false; const double poolHR = util::parsePoolHashrate( known ? known->schema : util::PoolStatsSchema::DragonXIs, responseData, known ? known->miningcorePoolId : std::string{}, ok); if (!ok) return; std::lock_guard lk(stats_mutex_); stats_.pool_hashrate = poolHR; } // ============================================================================ // Installed-miner version detection (` --version`, cached) // ============================================================================ namespace { std::mutex g_installed_ver_mutex; std::string g_installed_ver; std::atomic g_ver_detect_started{false}; // Extract the first "D.D[.D...]" version token from `--version` output (skips the // build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none. std::string parseMinerVersion(const std::string& out) { for (size_t i = 0; i < out.size(); ++i) { if (std::isdigit(static_cast(out[i]))) { size_t j = i; int dots = 0; while (j < out.size() && (std::isdigit(static_cast(out[j])) || out[j] == '.')) { if (out[j] == '.') ++dots; ++j; } if (dots >= 1 && (j - i) >= 3) { // Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"), // matching what the running miner's API reports. size_t end = j; if (end < out.size() && out[end] == '-') { size_t k = end + 1; while (k < out.size() && std::isalnum(static_cast(out[k]))) ++k; if (k > end + 1) end = k; } return out.substr(i, end - i); } i = j; } } return {}; } } // namespace void XmrigManager::startVersionDetection() { if (g_ver_detect_started.exchange(true)) return; // one-shot std::thread([]() { const std::string bin = findXmrigBinary(); std::string ver; // Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an // app-controlled path, but this closes command injection if it ever isn't. (M-10) // Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"") // on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows // paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10) const bool binShellSafe = !bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos; if (binShellSafe) { const std::string cmd = "\"" + bin + "\" --version 2>&1"; #ifdef _WIN32 FILE* fp = _popen(cmd.c_str(), "r"); #else FILE* fp = popen(cmd.c_str(), "r"); #endif if (fp) { std::string out; char buf[256]; size_t n; while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n); #ifdef _WIN32 _pclose(fp); #else pclose(fp); #endif ver = parseMinerVersion(out); } } std::lock_guard lk(g_installed_ver_mutex); g_installed_ver = ver; }).detach(); } std::string XmrigManager::installedVersion() { std::lock_guard lk(g_installed_ver_mutex); return g_installed_ver; } } // namespace daemon } // namespace dragonx