// 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::loginSucceeded, this, [this]() { onLoginDone(true, QString()); }); connect(&m_relay, QOverload::of(&RelayClient::loginFailed), this, [this](const QString &err) { onLoginDone(false, err); }); connect(&m_relay, &RelayClient::vpnProvisioned, this, [this](const RelayVPNConf &conf) { onVPNPeerProvisioned(conf); }); connect(&m_relay, QOverload::of(&RelayClient::vpnProvisionFailed), this, [this](const QString &err) { onVPNError(err); }); connect(&m_relay, &RelayClient::hostsReady, this, [this](const QList &hosts) { onHostsFetched(hosts); }); connect(&m_relay, QOverload::of(&RelayClient::hostsFetchFailed), this, [this](const QString &err) { onHostsError(err); }); // ── 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.login(QUrl(url), username, password); } // ── disconnectRelay ─────────────────────────────────────────────────────────── void DragonRelayBackend::disconnectRelay() { stopHostPoll(); m_tunnel.stop(); // triggers onTunnelDown asynchronously m_relay.revokeVPN(); // fire-and-forget m_hosts.clear(); m_hostDisplays.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 } // ── displaysForHost ─────────────────────────────────────────────────────────── QVariantList DragonRelayBackend::displaysForHost(const QString &hostIP) const { return m_hostDisplays.value(hostIP); } // ── streamHostDisplay ───────────────────────────────────────────────────────── void DragonRelayBackend::streamHostDisplay(const QString &hostIP, int displayIndex) { qDebug() << "Streaming host" << hostIP << "display index" << displayIndex; streamHost(hostIP); // Task 24 will wire actual display param } // ── 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.provisionVPN(); } 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.revokeVPN(); 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(); m_hostDisplays.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); // Store displays for this host m_hostDisplays[h.ip] = h.displays; } 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.revokeVPN(); setStatus(Error, QStringLiteral("Tunnel error: ") + err); }