dragonmoonlight/app/vpn/dragonrelaybackend.cpp

232 lines
7.7 KiB
C++
Raw Normal View History

2026-05-06 19:23:28 -04:00
// app/vpn/dragonrelaybackend.cpp
#include "dragonrelaybackend.h"
#include <QDebug>
#include <QMap>
#include <QProcess>
#include <QVariantMap>
// Constructor / Destructor
2026-05-06 19:23:28 -04:00
DragonRelayBackend::DragonRelayBackend(QObject *parent)
: QObject(parent)
, m_settings(QStringLiteral("WildDragon"), QStringLiteral("DragonMoonlight"))
{
// Wire RelayClient signals
2026-05-06 20:18:22 -04:00
connect(&m_relay, &RelayClient::loginSucceeded,
this, [this]() { onLoginDone(true, QString()); });
connect(&m_relay, &RelayClient::loginFailed,
2026-05-06 20:18:22 -04:00
this, [this](const QString &err) { onLoginDone(false, err); });
connect(&m_relay, &RelayClient::vpnProvisioned,
this, [this](const RelayVPNConf &conf) { onVPNPeerProvisioned(conf); });
connect(&m_relay, &RelayClient::vpnProvisionFailed,
2026-05-06 20:18:22 -04:00
this, [this](const QString &err) { onVPNError(err); });
connect(&m_relay, &RelayClient::hostsReady,
this, [this](const QList<RelayHost> &hosts) { onHostsFetched(hosts); });
connect(&m_relay, &RelayClient::hostsFetchFailed,
2026-05-06 20:18:22 -04:00
this, [this](const QString &err) { onHostsError(err); });
2026-05-06 19:23:28 -04:00
// Wire TunnelManager signals
2026-05-06 19:23:28 -04:00
connect(&m_tunnel, &TunnelManager::tunnelUp,
this, &DragonRelayBackend::onTunnelUp);
connect(&m_tunnel, &TunnelManager::tunnelDown,
this, &DragonRelayBackend::onTunnelDown);
connect(&m_tunnel, &TunnelManager::tunnelError,
2026-05-06 19:23:28 -04:00
this, &DragonRelayBackend::onTunnelError);
// Host poll timer (every 30 s while tunnel is up)
m_hostPollTimer.setInterval(30 * 1000);
2026-05-06 19:23:28 -04:00
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() {
m_hostPollTimer.stop();
m_tunnel.stop();
}
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();
}
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 - keychain integration is a TODO).
2026-05-06 19:23:28 -04:00
m_settings.setValue(QStringLiteral("relay/url"), url);
m_settings.setValue(QStringLiteral("relay/username"), username);
setStatus(Connecting, QStringLiteral("Logging in..."));
2026-05-06 20:18:22 -04:00
m_relay.login(QUrl(url), username, password);
2026-05-06 19:23:28 -04:00
}
void DragonRelayBackend::disconnectRelay() {
stopHostPoll();
m_tunnel.stop(); // triggers onTunnelDown asynchronously
2026-05-06 20:18:22 -04:00
m_relay.revokeVPN(); // fire-and-forget
2026-05-06 19:23:28 -04:00
m_hosts.clear();
2026-05-06 20:17:08 -04:00
m_hostDisplays.clear();
2026-05-06 19:23:28 -04:00
m_tunnelIP.clear();
emit hostsChanged();
emit tunnelIPChanged();
setStatus(Disconnected, QStringLiteral("Disconnected"));
}
2026-05-06 20:34:22 -04:00
void DragonRelayBackend::streamHost(const QString &ip, const QString &app, int displayIndex) {
2026-05-06 19:23:28 -04:00
if (m_status != Ready && m_status != TunnelUp) {
qWarning() << "DragonRelayBackend::streamHost called while not ready";
return;
}
qDebug() << "DragonRelayBackend::streamHost ip=" << ip
2026-05-06 20:34:22 -04:00
<< "app=" << app << "displayIndex=" << displayIndex;
QStringList args;
args << QStringLiteral("stream") << ip << app;
if (displayIndex > 0) {
2026-05-06 20:43:32 -04:00
args << QStringLiteral("--display") << QString::number(displayIndex);
}
2026-05-06 20:43:32 -04:00
QProcess::startDetached(QStringLiteral("moonlight"), args);
2026-05-06 19:23:28 -04:00
}
2026-05-06 20:17:08 -04:00
QVariantList DragonRelayBackend::displaysForHost(const QString &hostIP) const {
return m_hostDisplays.value(hostIP);
}
void DragonRelayBackend::streamHostDisplay(const QString &hostIP, int displayIndex) {
qDebug() << "Streaming host" << hostIP << "display index" << displayIndex;
const QVariantList displays = displaysForHost(hostIP);
if (displayIndex < 0 || (!displays.isEmpty() && displayIndex >= displays.size())) {
qWarning() << "DragonRelayBackend: displayIndex" << displayIndex
<< "out of bounds for host" << hostIP
<< "(has" << displays.size() << "displays)";
displayIndex = 0; // fall back to primary
}
2026-05-06 20:34:22 -04:00
streamHost(hostIP, QStringLiteral("Desktop"), displayIndex);
2026-05-06 20:17:08 -04:00
}
2026-05-06 19:23:28 -04:00
void DragonRelayBackend::refreshHosts() {
if (m_status == TunnelUp || m_status == Ready)
m_relay.fetchHosts();
}
void DragonRelayBackend::pollHosts() {
m_relay.fetchHosts();
}
void DragonRelayBackend::startHostPoll() {
m_relay.fetchHosts();
2026-05-06 19:23:28 -04:00
m_hostPollTimer.start();
}
void DragonRelayBackend::stopHostPoll() {
m_hostPollTimer.stop();
}
void DragonRelayBackend::onLoginDone(bool ok, const QString &err) {
if (!ok) {
setStatus(Error, QStringLiteral("Login failed: ") + err);
return;
}
setStatus(Connecting, QStringLiteral("Provisioning VPN peer..."));
2026-05-06 20:18:22 -04:00
m_relay.provisionVPN();
2026-05-06 19:23:28 -04:00
}
void DragonRelayBackend::onVPNPeerProvisioned(const RelayVPNConf &conf) {
setStatus(Connecting, QStringLiteral("Starting WireGuard tunnel..."));
2026-05-06 19:23:28 -04:00
WireGuardConfig cfg = WireGuardConfig::fromConf(conf.conf);
if (!cfg.isValid()) {
setStatus(Error, QStringLiteral(
"Bad VPN config: missing PrivateKey, Address, PublicKey, or Endpoint"));
2026-05-06 20:18:22 -04:00
m_relay.revokeVPN();
2026-05-06 19:23:28 -04:00
return;
}
if (!m_tunnel.start(cfg)) {
// tunnel.start() emits tunnelError() synchronously on failure,
// which routes through onTunnelError().
}
2026-05-06 19:23:28 -04:00
}
void DragonRelayBackend::onVPNError(const QString &err) {
setStatus(Error, QStringLiteral("VPN error: ") + err);
}
void DragonRelayBackend::onHostsFetched(const QList<RelayHost> &hosts) {
m_hosts.clear();
2026-05-06 20:17:08 -04:00
m_hostDisplays.clear();
2026-05-06 19:23:28 -04:00
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);
2026-05-06 20:17:08 -04:00
m_hostDisplays[h.ip] = h.displays;
2026-05-06 19:23:28 -04:00
}
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());
2026-05-06 19:23:28 -04:00
setStatus(next, text);
}
}
void DragonRelayBackend::onHostsError(const QString &err) {
qWarning() << "DragonRelayBackend: hosts fetch error:" << err;
// Don't downgrade to Error state - tunnel is still up.
2026-05-06 19:23:28 -04:00
}
void DragonRelayBackend::onTunnelUp(const QString &localIP) {
if (m_tunnelIP == localIP && (m_status == TunnelUp || m_status == Ready))
return; // ignore duplicate signals (first-packet path + 1.5 s fallback timer)
2026-05-06 19:23:28 -04:00
m_tunnelIP = localIP;
emit tunnelIPChanged();
setStatus(TunnelUp,
QStringLiteral("Tunnel up (%1) - fetching hosts...").arg(localIP));
2026-05-06 19:23:28 -04:00
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();
2026-05-06 20:18:22 -04:00
m_relay.revokeVPN();
2026-05-06 19:23:28 -04:00
setStatus(Error, QStringLiteral("Tunnel error: ") + err);
}