// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "lite_connection_service.h" #include #include #include namespace dragonx { namespace wallet { namespace { bool startsWith(const std::string& value, const char* prefix) { const std::string prefixValue(prefix); return value.size() >= prefixValue.size() && value.compare(0, prefixValue.size(), prefixValue) == 0; } LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint) { LiteServerEndpoint normalized = endpoint; normalized.url = liteTrimCopy(normalized.url); return normalized; } std::vector> usableConfiguredServers( const LiteConnectionSettings& settings) { std::vector> servers; for (std::size_t index = 0; index < settings.servers.size(); ++index) { auto endpoint = trimmedEndpoint(settings.servers[index]); if (!endpoint.enabled || !isLiteServerUrlUsable(endpoint.url)) continue; servers.push_back({index, std::move(endpoint)}); } return servers; } std::string normalizedChainName(const std::string& chainName) { const std::string normalized = liteTrimCopy(chainName); return normalized.empty() ? kDragonXLiteChainName : normalized; } } // namespace std::string liteTrimCopy(const std::string& value) { auto begin = value.begin(); while (begin != value.end() && std::isspace(static_cast(*begin))) ++begin; auto end = value.end(); while (end != begin && std::isspace(static_cast(*(end - 1)))) --end; return std::string(begin, end); } std::vector defaultDragonXLiteServers() { return { {"https://lite.dragonx.is", "DragonX Lite", true}, {"https://lite1.dragonx.is", "DragonX Lite 1", true}, {"https://lite2.dragonx.is", "DragonX Lite 2", true}, {"https://lite3.dragonx.is", "DragonX Lite 3", true}, {"https://lite4.dragonx.is", "DragonX Lite 4", true}, {"https://lite5.dragonx.is", "DragonX Lite 5", true} }; } LiteConnectionSettings defaultLiteConnectionSettings() { LiteConnectionSettings settings; settings.servers = defaultDragonXLiteServers(); settings.selectionMode = LiteServerSelectionMode::Sticky; settings.stickyServerUrl = kDefaultDragonXLiteServer; settings.chainName = kDragonXLiteChainName; settings.randomSelectionSeed = 0; return settings; } // Strict dotted-decimal check for the 127.0.0.0/8 loopback block: exactly four numeric octets // (each 0-255) with the first equal to 127. This must NOT be a prefix match — "127.0.0.1.evil.com" // and "127.evil.com" are attacker-controlled DNS names that a startsWith("127.") test would wrongly // treat as loopback, reopening the plaintext-downgrade hole. static bool isNumericIpv4Loopback(const std::string& h) { int parts = 0; size_t start = 0; while (true) { const size_t dot = h.find('.', start); const std::string seg = h.substr(start, dot == std::string::npos ? std::string::npos : dot - start); if (seg.empty() || seg.size() > 3) return false; int v = 0; for (char c : seg) { if (c < '0' || c > '9') return false; v = v * 10 + (c - '0'); } if (v > 255) return false; if (parts == 0 && v != 127) return false; // 127.0.0.0/8 only ++parts; if (dot == std::string::npos) break; start = dot + 1; } return parts == 4; } static bool isLoopbackLiteHostSpec(const std::string& hostPort) { std::string h = hostPort; const size_t term = h.find_first_of("/?#"); // strip path/query/fragment if (term != std::string::npos) h = h.substr(0, term); const size_t at = h.rfind('@'); // strip userinfo (user:pass@host) if (at != std::string::npos) h = h.substr(at + 1); if (!h.empty() && h.front() == '[') { // [::1]:port (bracketed IPv6) const size_t close = h.find(']'); h = (close == std::string::npos) ? h : h.substr(1, close - 1); } else { // host or host:port const size_t colon = h.find(':'); if (colon != std::string::npos) h = h.substr(0, colon); } return h == "localhost" || h == "::1" || isNumericIpv4Loopback(h); } bool isLiteServerUrlUsable(const std::string& serverUrl) { const std::string normalized = liteTrimCopy(serverUrl); if (startsWith(normalized, "https://")) return true; // SECURITY: plaintext http:// is a TLS downgrade for lightwalletd traffic (view keys, // transactions, addresses). Permit it only for loopback (a local dev lightwalletd); // reject remote plaintext servers instead of silently accepting the downgrade. if (startsWith(normalized, "http://")) return isLoopbackLiteHostSpec(normalized.substr(sizeof("http://") - 1)); return false; } bool isOfficialLiteServer(const std::string& serverUrl) { const std::string url = liteTrimCopy(serverUrl); for (const auto& s : defaultDragonXLiteServers()) if (s.url == url) return true; return false; } const char* liteServerSelectionModeName(LiteServerSelectionMode mode) { switch (mode) { case LiteServerSelectionMode::Sticky: return "Sticky"; case LiteServerSelectionMode::Random: return "Random"; } return "Unknown"; } const char* liteConnectionAvailabilityName(LiteConnectionAvailability availability) { switch (availability) { case LiteConnectionAvailability::Ready: return "Ready"; case LiteConnectionAvailability::UnsupportedBuild: return "UnsupportedBuild"; case LiteConnectionAvailability::BackendUnavailable: return "BackendUnavailable"; case LiteConnectionAvailability::BridgeUnavailable: return "BridgeUnavailable"; case LiteConnectionAvailability::NoUsableServer: return "NoUsableServer"; } return "Unknown"; } LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings) { auto usableServers = usableConfiguredServers(settings); const std::string stickyServerUrl = liteTrimCopy(settings.stickyServerUrl); if (settings.selectionMode == LiteServerSelectionMode::Sticky && isLiteServerUrlUsable(stickyServerUrl)) { auto match = std::find_if(usableServers.begin(), usableServers.end(), [&](const auto& candidate) { return candidate.second.url == stickyServerUrl; }); if (match != usableServers.end()) { return LiteServerSelectionResult{true, match->second, match->first, false, {}}; } return LiteServerSelectionResult{ true, LiteServerEndpoint{stickyServerUrl, "Custom", true}, 0, true, {} }; } if (usableServers.empty()) { return LiteServerSelectionResult{false, {}, 0, false, "no usable lite servers are configured"}; } if (settings.selectionMode == LiteServerSelectionMode::Random) { const std::size_t selectedIndex = settings.randomSelectionSeed % usableServers.size(); return LiteServerSelectionResult{ true, usableServers[selectedIndex].second, usableServers[selectedIndex].first, false, {} }; } return LiteServerSelectionResult{true, usableServers.front().second, usableServers.front().first, false, {}}; } LiteConnectionService::LiteConnectionService(WalletCapabilities capabilities, LiteConnectionSettings settings, LiteClientBridge bridge) : capabilities_(capabilities), settings_(std::move(settings)), bridge_(std::move(bridge)) { } LiteConnectionAvailability LiteConnectionService::availability() const { if (!isLiteBuild(capabilities_)) return LiteConnectionAvailability::UnsupportedBuild; if (!supportsLiteBackend(capabilities_)) return LiteConnectionAvailability::BackendUnavailable; if (!bridge_.available()) return LiteConnectionAvailability::BridgeUnavailable; if (!selectedServer().ok) return LiteConnectionAvailability::NoUsableServer; return LiteConnectionAvailability::Ready; } WalletBackendStatus LiteConnectionService::status() const { const auto currentAvailability = availability(); if (currentAvailability == LiteConnectionAvailability::NoUsableServer) { return statusFor(currentAvailability, selectedServer().error); } return statusFor(currentAvailability); } LiteServerSelectionResult LiteConnectionService::selectedServer() const { return selectLiteServer(settings_); } LiteWalletExistsRequest LiteConnectionService::walletExistsRequest() const { return LiteWalletExistsRequest{normalizedChainName(settings_.chainName)}; } LiteServerHealthRequest LiteConnectionService::serverHealthRequest() const { auto selection = selectedServer(); if (!selection.ok) return {}; return LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer}; } LiteWalletExistsCheckResult LiteConnectionService::checkWalletExists() { LiteWalletExistsCheckResult result; result.request = walletExistsRequest(); const auto readiness = bridgeReadinessStatus(); if (readiness.state != WalletBackendState::Disconnected) { result.status = readiness; result.error = readiness.message; return result; } if (result.request.chainName.empty()) { result.status = statusFor(LiteConnectionAvailability::BackendUnavailable, "lite chain name is empty"); result.error = result.status.message; return result; } result.attempted = true; result.walletExists = bridge_.walletExists(result.request.chainName); result.ok = true; result.status = readiness; return result; } LiteServerHealthCheckResult LiteConnectionService::checkSelectedServerHealth() { LiteServerHealthCheckResult result; result.request = serverHealthRequest(); const auto readiness = bridgeReadinessStatus(); if (readiness.state != WalletBackendState::Disconnected) { result.status = readiness; result.error = readiness.message; return result; } auto selection = selectedServer(); if (!selection.ok) { result.status = statusFor(LiteConnectionAvailability::NoUsableServer, selection.error); result.error = result.status.message; return result; } result.request = LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer}; result.attempted = true; result.serverOnline = bridge_.checkServerOnline(result.request.server.url); result.ok = true; result.status = readiness; return result; } WalletBackendStatus LiteConnectionService::statusFor(LiteConnectionAvailability availability, const std::string& detail) const { switch (availability) { case LiteConnectionAvailability::Ready: return WalletBackendStatus{ WalletBackendState::Disconnected, detail.empty() ? "lite connection scaffold ready; wallet lifecycle is not implemented" : detail, {}, {}, 0.0 }; case LiteConnectionAvailability::UnsupportedBuild: return WalletBackendStatus{ WalletBackendState::Unavailable, "lite connection service is unsupported in full-node builds", {}, {}, 0.0 }; case LiteConnectionAvailability::BackendUnavailable: return WalletBackendStatus{ WalletBackendState::Unavailable, detail.empty() ? "lite backend is not linked" : detail, {}, {}, 0.0 }; case LiteConnectionAvailability::BridgeUnavailable: return WalletBackendStatus{ WalletBackendState::Unavailable, detail.empty() ? bridge_.unavailableReason() : detail, {}, {}, 0.0 }; case LiteConnectionAvailability::NoUsableServer: return WalletBackendStatus{ WalletBackendState::Error, detail.empty() ? "no usable lite servers are configured" : detail, {}, {}, 0.0 }; } return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite connection state", {}, {}, 0.0}; } WalletBackendStatus LiteConnectionService::bridgeReadinessStatus() const { if (!isLiteBuild(capabilities_)) return statusFor(LiteConnectionAvailability::UnsupportedBuild); if (!supportsLiteBackend(capabilities_)) return statusFor(LiteConnectionAvailability::BackendUnavailable); if (!bridge_.available()) return statusFor(LiteConnectionAvailability::BridgeUnavailable); return statusFor(LiteConnectionAvailability::Ready); } } // namespace wallet } // namespace dragonx