// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "connection.h" #include "../config/version.h" #include "../resources/embedded_resources.h" #include #include #include #include #include #include #include #include "../util/logger.h" #ifdef _WIN32 #include #else #include #include #endif namespace fs = std::filesystem; namespace dragonx { namespace rpc { namespace { std::string generateSecureRandomString(size_t length) { static constexpr char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; static constexpr uint32_t charsetSize = static_cast(sizeof(charset) - 1); if (sodium_init() < 0) { DEBUG_LOGF("Failed to initialize libsodium for RPC credential generation\n"); return {}; } std::string result; result.reserve(length); for (size_t i = 0; i < length; ++i) { result.push_back(charset[randombytes_uniform(charsetSize)]); } return result; } std::string lowercase(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return value; } bool parseBoolValue(const std::string& value) { std::string lowered = lowercase(value); return lowered == "1" || lowered == "true" || lowered == "yes" || lowered == "on"; } bool applyCookieAuth(ConnectionConfig& config, const std::string& dataDir) { std::string cookieUser, cookiePass; if (!Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) { return false; } config.rpcuser = cookieUser; config.rpcpassword = cookiePass; config.auth_source = AuthSource::Cookie; if (config.hush_dir.empty()) config.hush_dir = dataDir; return true; } } // namespace Connection::Connection() = default; Connection::~Connection() = default; std::string Connection::getDefaultDataDir() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) { return std::string(path) + "\\Hush\\DRAGONX"; } return ""; #elif defined(__APPLE__) const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw->pw_dir; } // Match SilentDragonX path: Library/Application Support/Hush/DRAGONX return std::string(home) + "/Library/Application Support/Hush/DRAGONX"; #else const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw->pw_dir; } return std::string(home) + "/.hush/DRAGONX"; #endif } std::string Connection::getDefaultConfPath() { return getDefaultDataDir() + "/" + DRAGONX_CONF_FILENAME; } std::string Connection::getSaplingParamsDir() { // Sapling params are now extracted alongside the daemon binaries // in /dragonx/ — no longer in the legacy ZcashParams dir. return resources::getDaemonDirectory(); } bool Connection::verifySaplingParams() { std::string params_dir = getSaplingParamsDir(); if (params_dir.empty()) { DEBUG_LOGF("verifySaplingParams: params dir is empty\n"); return false; } #ifdef _WIN32 std::string spend_path = params_dir + "\\sapling-spend.params"; std::string output_path = params_dir + "\\sapling-output.params"; #else std::string spend_path = params_dir + "/sapling-spend.params"; std::string output_path = params_dir + "/sapling-output.params"; #endif bool spend_exists = fs::exists(spend_path); bool output_exists = fs::exists(output_path); DEBUG_LOGF("verifySaplingParams: dir=%s\n", params_dir.c_str()); DEBUG_LOGF(" spend: %s -> %s\n", spend_path.c_str(), spend_exists ? "found" : "MISSING"); DEBUG_LOGF(" output: %s -> %s\n", output_path.c_str(), output_exists ? "found" : "MISSING"); return spend_exists && output_exists; } ConnectionConfig Connection::parseConfFile(const std::string& path) { ConnectionConfig config; std::ifstream file(path); if (!file.is_open()) { return config; } std::string line; while (std::getline(file, line)) { // Skip empty lines and comments if (line.empty() || line[0] == '#') { continue; } // Parse key=value size_t eq_pos = line.find('='); if (eq_pos == std::string::npos) { continue; } std::string key = line.substr(0, eq_pos); std::string value = line.substr(eq_pos + 1); // Trim whitespace (including \r from Windows line endings) while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r')) { key.pop_back(); } while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) { value.erase(0, 1); } while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || value.back() == '\r')) { value.pop_back(); } // Map to config if (key == "rpcuser") { config.rpcuser = value; } else if (key == "rpcpassword") { config.rpcpassword = value; } else if (key == "rpcport") { config.port = value; } else if (key == "rpchost" || key == "rpcconnect") { config.host = value; } else if (key == "proxy") { config.proxy = value; } else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") { config.use_tls = parseBoolValue(value); } } if (!config.rpcuser.empty() || !config.rpcpassword.empty()) { config.auth_source = AuthSource::ConfigFile; } return config; } ConnectionConfig Connection::autoDetectConfig() { ConnectionConfig config; // Ensure data directory exists std::string data_dir = getDefaultDataDir(); if (!fs::exists(data_dir)) { DEBUG_LOGF("Creating data directory: %s\n", data_dir.c_str()); fs::create_directories(data_dir); } // Try to find DRAGONX.conf std::string conf_path = getDefaultConfPath(); if (fs::exists(conf_path)) { // Ensure exportdir is present in existing config ensureExportDir(conf_path); // Ensure encryption flags are present ensureEncryptionEnabled(conf_path); config = parseConfFile(conf_path); config.hush_dir = data_dir; } else { // Create a default config file if (createDefaultConfig(conf_path)) { config = parseConfFile(conf_path); config.hush_dir = data_dir; } } // If rpcpassword is empty, the daemon may be using .cookie auth if (config.rpcpassword.empty()) { if (applyCookieAuth(config, data_dir)) { DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n"); } } // Set defaults for missing values if (config.host.empty()) { config.host = DRAGONX_DEFAULT_RPC_HOST; } if (config.port.empty()) { config.port = DRAGONX_DEFAULT_RPC_PORT; } return config; } bool Connection::buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig) { if (base.auth_source == AuthSource::Cookie) { return false; } std::string dataDir = base.hush_dir.empty() ? getDefaultDataDir() : base.hush_dir; ConnectionConfig fallback = base; if (!applyCookieAuth(fallback, dataDir)) { return false; } cookieConfig = std::move(fallback); return true; } bool Connection::isLocalHost(const std::string& host) { std::string lowered = lowercase(host); if (!lowered.empty() && lowered.front() == '[' && lowered.back() == ']') { lowered = lowered.substr(1, lowered.size() - 2); } return lowered == "localhost" || lowered == "localhost." || lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" || lowered == "127.0.0.1" || lowered.rfind("127.", 0) == 0; } bool Connection::usesPlaintextRemote(const ConnectionConfig& config) { return !config.use_tls && !isLocalHost(config.host); } const char* Connection::authSourceName(AuthSource source) { switch (source) { case AuthSource::ConfigFile: return "config"; case AuthSource::Cookie: return "cookie"; case AuthSource::Missing: return "missing"; } return "unknown"; } bool Connection::createDefaultConfig(const std::string& path) { std::string rpcuser = generateSecureRandomString(16); std::string rpcpassword = generateSecureRandomString(32); if (rpcuser.empty() || rpcpassword.empty()) { DEBUG_LOGF("Failed to generate secure RPC credentials for config file: %s\n", path.c_str()); return false; } std::ofstream file(path); if (!file.is_open()) { DEBUG_LOGF("Failed to create config file: %s\n", path.c_str()); return false; } // Get the data directory for exportdir std::string dataDir = getDefaultDataDir(); file << "# DragonX configuration file\n"; file << "# Auto-generated by DragonX Wallet\n"; file << "\n"; file << "rpcuser=" << rpcuser << "\n"; file << "rpcpassword=" << rpcpassword << "\n"; file << "rpcport=" << DRAGONX_DEFAULT_RPC_PORT << "\n"; file << "server=1\n"; file << "txindex=1\n"; file << "exportdir=" << dataDir << "\n"; file << "experimentalfeatures=1\n"; file << "developerencryptwallet=1\n"; file << "addnode=node.dragonx.is\n"; file << "addnode=node1.dragonx.is\n"; file << "addnode=node2.dragonx.is\n"; file << "addnode=node3.dragonx.is\n"; file << "addnode=node4.dragonx.is\n"; file.close(); DEBUG_LOGF("Created default config file: %s\n", path.c_str()); return true; } bool Connection::ensureExportDir(const std::string& confPath) { // Read existing config and check if exportdir is present std::ifstream inFile(confPath); if (!inFile.is_open()) { return false; } std::string contents; std::string line; bool hasExportDir = false; while (std::getline(inFile, line)) { contents += line + "\n"; // Check for exportdir (case insensitive check for key) if (line.find("exportdir=") == 0 || line.find("exportdir =") == 0) { hasExportDir = true; } } inFile.close(); if (hasExportDir) { return true; // Already has exportdir } // Append exportdir to config std::ofstream outFile(confPath, std::ios::app); if (!outFile.is_open()) { DEBUG_LOGF("Failed to open config for appending exportdir: %s\n", confPath.c_str()); return false; } std::string dataDir = getDefaultDataDir(); outFile << "\n# Export directory for wallet backups (required for z_exportwallet)\n"; outFile << "exportdir=" << dataDir << "\n"; outFile.close(); DEBUG_LOGF("Added exportdir to config: %s\n", confPath.c_str()); return true; } bool Connection::ensureEncryptionEnabled(const std::string& confPath) { // Read existing config and check if encryption flags are present std::ifstream inFile(confPath); if (!inFile.is_open()) { return false; } std::string contents; std::string line; bool hasExperimental = false; bool hasEncrypt = false; while (std::getline(inFile, line)) { contents += line + "\n"; if (line.find("experimentalfeatures=") == 0 || line.find("experimentalfeatures =") == 0) { hasExperimental = true; } if (line.find("developerencryptwallet=") == 0 || line.find("developerencryptwallet =") == 0) { hasEncrypt = true; } } inFile.close(); if (hasExperimental && hasEncrypt) { return true; // Already has both flags } // Append missing flags to config std::ofstream outFile(confPath, std::ios::app); if (!outFile.is_open()) { DEBUG_LOGF("Failed to open config for appending encryption flags: %s\n", confPath.c_str()); return false; } outFile << "\n# Enable wallet encryption support\n"; if (!hasExperimental) { outFile << "experimentalfeatures=1\n"; } if (!hasEncrypt) { outFile << "developerencryptwallet=1\n"; } outFile.close(); DEBUG_LOGF("Added encryption flags to config: %s\n", confPath.c_str()); return true; } bool Connection::readAuthCookie(const std::string& dataDir, std::string& user, std::string& password) { if (dataDir.empty()) return false; #ifdef _WIN32 std::string cookiePath = dataDir + "\\.cookie"; #else std::string cookiePath = dataDir + "/.cookie"; #endif std::ifstream file(cookiePath); if (!file.is_open()) return false; std::string cookie; std::getline(file, cookie); file.close(); // Cookie format: __cookie__:base64encodedpassword size_t colonPos = cookie.find(':'); if (colonPos == std::string::npos || colonPos == 0) return false; user = cookie.substr(0, colonPos); password = cookie.substr(colonPos + 1); // Trim \r if present (Windows line endings) while (!password.empty() && (password.back() == '\r' || password.back() == '\n')) { password.pop_back(); } if (user.empty() || password.empty()) return false; DEBUG_LOGF("Read auth cookie from: %s (user=%s)\n", cookiePath.c_str(), user.c_str()); return true; } } // namespace rpc } // namespace dragonx