From fc3be9c2728e8506b8b3fa3fbf9192b9457b1d52 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Wed, 6 May 2026 19:23:28 -0400 Subject: [PATCH] vpn: DragonRelayBackend implementation --- app/vpn/dragonrelaybackend.cpp | 231 +++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 app/vpn/dragonrelaybackend.cpp diff --git a/app/vpn/dragonrelaybackend.cpp b/app/vpn/dragonrelaybackend.cpp new file mode 100644 index 0000000..1ea2877 --- /dev/null +++ b/app/vpn/dragonrelaybackend.cpp @@ -0,0 +1,231 @@ +// app/vpn/dragonrelaybackend.cpp +#include "dragonrelaybackend.h" + +#include +#include +#include +#include + +// ── Constructor / Destructor ────────────────────────────────────────────────── + +DragonRelayBackend::DragonRelayBackend(QObject *parent) + : QObject(parent) + , m_settings(QStringLiteral("WildDragon"), QStringLiteral("DragonMoonlight")) +{ + // ── Wire RelayClient signals ────────────────────────────────────────────── + connect(&m_relay, &RelayClient::loginDone, + this, &DragonRelayBackend::onLoginDone); + connect(&m_relay, &RelayClient::vpnPeerProvisioned, + this, &DragonRelayBackend::onVPNPeerProvisioned); + connect(&m_relay, &RelayClient::vpnError, + this, &DragonRelayBackend::onVPNError); + connect(&m_relay, &RelayClient::hostsFetched, + this, &DragonRelayBackend::onHostsFetched); + connect(&m_relay, &RelayClient::hostsError, + this, &DragonRelayBackend::onHostsError); + + // ── Wire TunnelManager signals ──────────────────────────────────────────── + connect(&m_tunnel, &TunnelManager::tunnelUp, + this, &DragonRelayBackend::onTunnelUp); + connect(&m_tunnel, &TunnelManager::tunnelDown, + this, &DragonRelayBackend::onTunnelDown); + connect(&m_tunnel, &TunnelManager::error, + this, &DragonRelayBackend::onTunnelError); + + // ── Host poll timer (every 30 s while tunnel is up) ─────────────────────── + m_hostPollTimer.setInterval(30'000); + m_hostPollTimer.setSingleShot(false); + connect(&m_hostPollTimer, &QTimer::timeout, + this, &DragonRelayBackend::pollHosts); + + // Restore last-used connection settings so QML can pre-fill the form + m_savedURL = m_settings.value(QStringLiteral("relay/url")).toString(); + m_savedUser = m_settings.value(QStringLiteral("relay/username")).toString(); +} + +DragonRelayBackend::~DragonRelayBackend() { + // Best-effort cleanup — don't block the destructor + m_hostPollTimer.stop(); + m_tunnel.stop(); +} + +// ── Status helper ───────────────────────────────────────────────────────────── + +void DragonRelayBackend::setStatus(Status s, const QString &text) { + bool changed = (m_status != s) || (!text.isEmpty() && text != m_statusText); + m_status = s; + if (!text.isEmpty()) + m_statusText = text; + if (changed) + emit statusChanged(); +} + +// ── connectRelay ────────────────────────────────────────────────────────────── + +void DragonRelayBackend::connectRelay(const QString &url, + const QString &username, + const QString &password) { + if (m_status == Connecting || m_status == TunnelUp || m_status == Ready) + return; + + m_savedURL = url; + m_savedUser = username; + m_savedPass = password; + + // Persist URL + username (NOT password) + m_settings.setValue(QStringLiteral("relay/url"), url); + m_settings.setValue(QStringLiteral("relay/username"), username); + + setStatus(Connecting, QStringLiteral("Logging in…")); + m_relay.setBaseURL(url); + m_relay.login(username, password); +} + +// ── disconnectRelay ─────────────────────────────────────────────────────────── + +void DragonRelayBackend::disconnectRelay() { + stopHostPoll(); + m_tunnel.stop(); // triggers onTunnelDown asynchronously + m_relay.deleteVPNPeer(); // fire-and-forget + + m_hosts.clear(); + m_tunnelIP.clear(); + emit hostsChanged(); + emit tunnelIPChanged(); + setStatus(Disconnected, QStringLiteral("Disconnected")); +} + +// ── streamHost ──────────────────────────────────────────────────────────────── + +void DragonRelayBackend::streamHost(const QString &ip, const QString &app) { + if (m_status != Ready && m_status != TunnelUp) { + qWarning() << "DragonRelayBackend::streamHost called while not ready"; + return; + } + // Delegate to moonlight-qt's existing stream launch mechanism. + // The standard Moonlight PC model uses ComputerManager; we invoke it + // via a QProcess for now so we don't have to patch PC discovery internals. + // TODO: wire directly into Moonlight's ComputerManager once the fork is + // more deeply integrated. +#if defined(Q_OS_WIN) + QProcess::startDetached(QStringLiteral("moonlight"), { + QStringLiteral("stream"), + ip, + app + }); +#elif defined(Q_OS_MACOS) + QProcess::startDetached(QStringLiteral("moonlight"), { + QStringLiteral("stream"), + ip, + app + }); +#else + QProcess::startDetached(QStringLiteral("moonlight"), { + QStringLiteral("stream"), + ip, + app + }); +#endif +} + +// ── refreshHosts ───────────────────────────────────────────────────────────── + +void DragonRelayBackend::refreshHosts() { + if (m_status == TunnelUp || m_status == Ready) + m_relay.fetchHosts(); +} + +// ── pollHosts ───────────────────────────────────────────────────────────────── + +void DragonRelayBackend::pollHosts() { + m_relay.fetchHosts(); +} + +void DragonRelayBackend::startHostPoll() { + m_relay.fetchHosts(); // immediate fetch + m_hostPollTimer.start(); +} + +void DragonRelayBackend::stopHostPoll() { + m_hostPollTimer.stop(); +} + +// ── RelayClient slots ───────────────────────────────────────────────────────── + +void DragonRelayBackend::onLoginDone(bool ok, const QString &err) { + if (!ok) { + setStatus(Error, QStringLiteral("Login failed: ") + err); + return; + } + setStatus(Connecting, QStringLiteral("Provisioning VPN peer…")); + m_relay.provisionVPNPeer(); +} + +void DragonRelayBackend::onVPNPeerProvisioned(const RelayVPNConf &conf) { + setStatus(Connecting, QStringLiteral("Starting WireGuard tunnel…")); + + WireGuardConfig cfg; + QString parseErr; + if (!WireGuardConfig::fromConf(conf.conf, cfg, parseErr)) { + setStatus(Error, QStringLiteral("Bad VPN config: ") + parseErr); + m_relay.deleteVPNPeer(); + return; + } + + m_tunnel.start(cfg); + // onTunnelUp/onTunnelError fired from TunnelManager +} + +void DragonRelayBackend::onVPNError(const QString &err) { + setStatus(Error, QStringLiteral("VPN error: ") + err); +} + +void DragonRelayBackend::onHostsFetched(const QList &hosts) { + m_hosts.clear(); + for (const auto &h : hosts) { + QVariantMap m; + m[QStringLiteral("name")] = h.name; + m[QStringLiteral("ip")] = h.ip; + m[QStringLiteral("port")] = h.port; + m[QStringLiteral("online")] = true; + m[QStringLiteral("source")] = QStringLiteral("relay"); + m_hosts.append(m); + } + emit hostsChanged(); + + Status next = m_hosts.isEmpty() ? TunnelUp : Ready; + if (m_status != next) { + QString text = m_hosts.isEmpty() + ? QStringLiteral("Tunnel up — no hosts yet") + : QStringLiteral("Connected — %1 host(s)").arg(m_hosts.size()); + setStatus(next, text); + } +} + +void DragonRelayBackend::onHostsError(const QString &err) { + qWarning() << "DragonRelayBackend: hosts fetch error:" << err; + // Don't downgrade to Error state — the tunnel is still up; just log it +} + +// ── TunnelManager slots ─────────────────────────────────────────────────────── + +void DragonRelayBackend::onTunnelUp(const QString &localIP) { + m_tunnelIP = localIP; + emit tunnelIPChanged(); + setStatus(TunnelUp, QStringLiteral("Tunnel up (%1) — fetching hosts…").arg(localIP)); + startHostPoll(); +} + +void DragonRelayBackend::onTunnelDown() { + stopHostPoll(); + m_tunnelIP.clear(); + emit tunnelIPChanged(); + if (m_status != Disconnected) + setStatus(Disconnected, QStringLiteral("Tunnel closed")); +} + +void DragonRelayBackend::onTunnelError(const QString &err) { + stopHostPoll(); + m_relay.deleteVPNPeer(); + setStatus(Error, QStringLiteral("Tunnel error: ") + err); +}