F8 (security). Two related fixes to how the wallet decides whether an RPC target is
safe to send Basic-auth credentials to:
- isLocalHost() was matching any host that merely *starts* "127." via
rfind("127.",0)==0, so "127.evil.com" (and "127.0.0.1.attacker", "127.300.0.1",
"1270.0.0.1") were misclassified as loopback and treated as local. It now uses a
strict isExactIPv4Loopback() parser: exactly four 0-255 dot-separated octets with
the first == 127. localhost / ::1 / [::1] handling is unchanged.
- A remote rpchost over plain HTTP (no rpctls=1) previously only produced a
dismissible warning and then sent rpcuser:rpcpassword in cleartext, where a
local-network MITM could capture them. tryConnect() now REFUSES that connection
(clear status line + one-time notification, no creds sent) unless the user opts in
explicitly with rpcallowplaintext=1 in DRAGONX.conf (new
ConnectionConfig::allow_plaintext_remote, parsed in parseConfFile; policy in the
new allowsPlaintextRemote()). Local/embedded daemons and rpctls=1 remotes are
unaffected.
BREAKING: a wallet configured for remote plaintext RPC will stop connecting until
rpcallowplaintext=1 (or rpctls=1) is added to DRAGONX.conf. Must be called out in the
release notes. The Settings-toggle UI is deferred (the conf-key opt-in is the recovery
path; see docs/daemon-startup-hardening.md).
Adds testIsLocalHost and testAllowsPlaintextRemote to test_phase4.cpp; one i18n key
(English) added to i18n.cpp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
608 lines
20 KiB
C++
608 lines
20 KiB
C++
// 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 <sodium.h>
|
|
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <vector>
|
|
#include <chrono>
|
|
|
|
#include "../util/logger.h"
|
|
#include "../util/platform.h"
|
|
#include "../util/xmrig_updater.h" // util::sha256Hex
|
|
|
|
#ifdef _WIN32
|
|
#include <shlobj.h>
|
|
#else
|
|
#include <pwd.h>
|
|
#include <unistd.h>
|
|
#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<uint32_t>(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<char>(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 <ObsidianDragonDir>/dragonx/ — no longer in the legacy ZcashParams dir.
|
|
return resources::getDaemonDirectory();
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::string joinParamPath(const std::string& dir, const std::string& file) {
|
|
#ifdef _WIN32
|
|
return dir + "\\" + file;
|
|
#else
|
|
return dir + "/" + file;
|
|
#endif
|
|
}
|
|
|
|
// "<size>:<mtime>" fingerprint used to skip re-hashing an unchanged file. Empty on error.
|
|
std::string paramStatLine(const std::string& path) {
|
|
std::error_code ec;
|
|
auto sz = fs::file_size(path, ec);
|
|
if (ec) return {};
|
|
auto mtime = fs::last_write_time(path, ec);
|
|
long long ticks = ec ? 0 :
|
|
std::chrono::duration_cast<std::chrono::seconds>(mtime.time_since_epoch()).count();
|
|
return std::to_string(static_cast<unsigned long long>(sz)) + ":" + std::to_string(ticks);
|
|
}
|
|
|
|
bool paramHashMatches(const std::string& path, const std::string& expectedHex) {
|
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
if (!f) return false;
|
|
std::streamsize sz = f.tellg();
|
|
if (sz <= 0) return false;
|
|
f.seekg(0, std::ios::beg);
|
|
std::vector<char> buf(static_cast<size_t>(sz));
|
|
if (!f.read(buf.data(), sz)) return false;
|
|
std::string got = util::sha256Hex(buf.data(), buf.size());
|
|
return !got.empty() && got == expectedHex;
|
|
}
|
|
|
|
// The verification cache: <params_dir>/.sapling_verified holds one paramStatLine per param,
|
|
// in list order, from the last successful hash check.
|
|
bool saplingMarkerMatches(const std::string& markerPath, const std::vector<std::string>& expected) {
|
|
for (const auto& s : expected) if (s.empty()) return false; // couldn't stat -> don't trust
|
|
std::ifstream f(markerPath);
|
|
if (!f) return false;
|
|
std::vector<std::string> lines;
|
|
std::string l;
|
|
while (std::getline(f, l)) lines.push_back(l);
|
|
return lines == expected;
|
|
}
|
|
|
|
void writeSaplingMarker(const std::string& markerPath, const std::vector<std::string>& lines) {
|
|
std::ofstream f(markerPath, std::ios::trunc);
|
|
if (!f) return;
|
|
for (const auto& l : lines) f << l << "\n";
|
|
}
|
|
|
|
// Canonical Zcash-family Sapling trusted-setup param digests — identical bytes across every
|
|
// fork/platform. Source of truth: scripts/build-lite-backend-artifact.sh ensure_sapling_params().
|
|
// Keep in sync if the params are ever rotated.
|
|
const std::pair<std::string, std::string> kSaplingParamDigests[] = {
|
|
{ "sapling-spend.params", "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13" },
|
|
{ "sapling-output.params", "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4" },
|
|
};
|
|
|
|
} // namespace
|
|
|
|
bool Connection::verifySaplingParamsIn(
|
|
const std::string& dir,
|
|
const std::vector<std::pair<std::string, std::string>>& digests)
|
|
{
|
|
if (dir.empty()) {
|
|
DEBUG_LOGF("verifySaplingParams: params dir is empty\n");
|
|
return false;
|
|
}
|
|
if (digests.empty()) return false;
|
|
|
|
// 1) Every param must exist.
|
|
std::vector<std::string> paths;
|
|
paths.reserve(digests.size());
|
|
for (const auto& d : digests) {
|
|
std::string p = joinParamPath(dir, d.first);
|
|
if (!fs::exists(p)) {
|
|
DEBUG_LOGF("verifySaplingParams: %s MISSING\n", p.c_str());
|
|
return false;
|
|
}
|
|
paths.push_back(std::move(p));
|
|
}
|
|
|
|
// 2) Fast path: if the cached marker matches the current size:mtime of every param, trust
|
|
// the previous successful hash instead of re-hashing ~48MB on every startup.
|
|
const std::string markerPath = joinParamPath(dir, ".sapling_verified");
|
|
std::vector<std::string> current;
|
|
current.reserve(paths.size());
|
|
for (const auto& p : paths) current.push_back(paramStatLine(p));
|
|
if (saplingMarkerMatches(markerPath, current)) {
|
|
return true;
|
|
}
|
|
|
|
// 3) Integrity-check each param against its pinned SHA-256. A truncated or corrupt param
|
|
// (a partial extraction, or a Linux bundle where the file merely *exists*) is rejected
|
|
// here instead of being handed to the daemon and failing later on a shielded operation.
|
|
for (size_t i = 0; i < paths.size(); ++i) {
|
|
if (!paramHashMatches(paths[i], digests[i].second)) {
|
|
DEBUG_LOGF("verifySaplingParams: %s FAILED integrity check (truncated or corrupt)\n",
|
|
paths[i].c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 4) Record the verified state so later startups take the fast path.
|
|
writeSaplingMarker(markerPath, current);
|
|
DEBUG_LOGF("verifySaplingParams: %zu params verified (sha256)\n", paths.size());
|
|
return true;
|
|
}
|
|
|
|
bool Connection::verifySaplingParams()
|
|
{
|
|
std::vector<std::pair<std::string, std::string>> digests;
|
|
for (const auto& d : kSaplingParamDigests) digests.emplace_back(d.first, d.second);
|
|
return verifySaplingParamsIn(getSaplingParamsDir(), digests);
|
|
}
|
|
|
|
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);
|
|
} else if (key == "rpcallowplaintext") {
|
|
config.allow_plaintext_remote = parseBoolValue(value);
|
|
}
|
|
}
|
|
|
|
if (!config.rpcuser.empty() || !config.rpcpassword.empty()) {
|
|
config.auth_source = AuthSource::ConfigFile;
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
ConnectionConfig Connection::autoDetectConfig()
|
|
{
|
|
ConnectionConfig config;
|
|
|
|
// Ensure the data directory exists. Use the non-throwing helper and report any failure
|
|
// via config.dir_error so callers can surface it — the old throwing create_directories()
|
|
// overload could raise an uncaught filesystem_error straight through autoDetectConfig()'s
|
|
// callers (read-only home, permission denied, etc.).
|
|
std::string data_dir = getDefaultDataDir();
|
|
if (!util::Platform::ensureDirectory(data_dir, &config.dir_error)) {
|
|
DEBUG_LOGF("[ERROR] autoDetectConfig: %s\n", config.dir_error.c_str());
|
|
return config; // data dir unusable — bail early with dir_error set
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// True only for a well-formed IPv4 loopback literal (127.0.0.0/8): exactly four dot-separated
|
|
// 0-255 octets with the first == 127. Rejects "127.evil.com", "127.0.0.1.attacker",
|
|
// "127.300.0.1", "1270.0.0.1", etc. — the old rfind("127.",0)==0 prefix matched all of those.
|
|
static bool isExactIPv4Loopback(const std::string& host)
|
|
{
|
|
int octets = 0, value = 0, digits = 0;
|
|
bool firstIs127 = false;
|
|
for (size_t i = 0; i <= host.size(); ++i) {
|
|
const char c = (i < host.size()) ? host[i] : '.'; // trailing sentinel flushes the last octet
|
|
if (c == '.') {
|
|
if (digits == 0 || digits > 3 || value > 255) return false;
|
|
if (octets == 0) firstIs127 = (value == 127);
|
|
++octets;
|
|
value = 0;
|
|
digits = 0;
|
|
} else if (c >= '0' && c <= '9') {
|
|
value = value * 10 + (c - '0');
|
|
++digits;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return octets == 4 && firstIs127;
|
|
}
|
|
|
|
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" ||
|
|
isExactIPv4Loopback(lowered);
|
|
}
|
|
|
|
bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
|
|
{
|
|
return !config.use_tls && !isLocalHost(config.host);
|
|
}
|
|
|
|
bool Connection::allowsPlaintextRemote(const ConnectionConfig& config)
|
|
{
|
|
// Explicit opt-in (DRAGONX.conf: rpcallowplaintext=1) to send credentials over a plaintext
|
|
// link to a remote host. Off by default — see usesPlaintextRemote().
|
|
return config.allow_plaintext_remote;
|
|
}
|
|
|
|
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();
|
|
|
|
// The file holds the freshly-generated rpcuser/rpcpassword in plaintext. ofstream creates it
|
|
// with the process umask (typically world-readable 0644), so restrict it to owner read/write
|
|
// before another local user can read the credentials.
|
|
{
|
|
namespace fs = std::filesystem;
|
|
std::error_code ec;
|
|
fs::permissions(path, fs::perms::owner_read | fs::perms::owner_write,
|
|
fs::perm_options::replace, ec);
|
|
if (ec) DEBUG_LOGF("Could not restrict config permissions on %s: %s\n", path.c_str(), ec.message().c_str());
|
|
}
|
|
|
|
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
|