// relayclient.cpp #include "relayclient.h" #include #include #include #include #include #include #include #include RelayClient::RelayClient(QObject *parent) : QObject(parent) , m_nam(new QNetworkAccessManager(this)) , m_settings(new QSettings(QStringLiteral("WildDragon"), QStringLiteral("DragonMoonlight"), this)) { } RelayClient::~RelayClient() = default; // ─── URL helpers ────────────────────────────────────────────────────────────── QUrl RelayClient::apiUrl(const QString &path) const { QUrl u = m_relayUrl; u.setPath(u.path() + path); return u; } // ─── Auth-header injection ──────────────────────────────────────────────── QNetworkReply *RelayClient::authedGet(const QString &path) { QNetworkRequest req(apiUrl(path)); req.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + m_jwt.toUtf8()); return m_nam->get(req); } QNetworkReply *RelayClient::authedPost(const QString &path, const QByteArray &body) { QNetworkRequest req(apiUrl(path)); req.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + m_jwt.toUtf8()); req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json")); return m_nam->post(req, body); } QNetworkReply *RelayClient::authedDelete(const QString &path) { QNetworkRequest req(apiUrl(path)); req.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + m_jwt.toUtf8()); return m_nam->deleteResource(req); } // ─── login() ───────────────────────────────────────────────────────────────── void RelayClient::login(const QUrl &relayUrl, const QString &username, const QString &password) { m_relayUrl = relayUrl; m_username = username; m_jwt.clear(); QNetworkRequest req(apiUrl(QStringLiteral("/api/auth/login"))); req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json")); QJsonObject body; body[QStringLiteral("username")] = username; body[QStringLiteral("password")] = password; const QByteArray payload = QJsonDocument(body).toJson(QJsonDocument::Compact); QNetworkReply *reply = m_nam->post(req, payload); connect(reply, &QNetworkReply::finished, this, [this, reply, username, password]() { reply->deleteLater(); onLoginReply(reply, username, password); }); } void RelayClient::onLoginReply(QNetworkReply *reply, const QString &username, const QString & /*password*/) { if (reply->error() != QNetworkReply::NoError) { emit loginFailed(reply->errorString()); return; } const QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); const QString token = json[QStringLiteral("token")].toString(); if (token.isEmpty()) { const QString err = json[QStringLiteral("error")].toString(QStringLiteral("Unknown error")); emit loginFailed(err); return; } m_jwt = token; m_username = username; persistJwt(); emit loginSucceeded(); } // ─── provisionVPN() ─────────────────────────────────────────────────────────── void RelayClient::provisionVPN(const QString &deviceName) { // Use the machine's host name if the caller didn't provide one. const QString device = deviceName.isEmpty() ? QSysInfo::machineHostName() : deviceName; QJsonObject body; body[QStringLiteral("device_name")] = device; const QByteArray payload = QJsonDocument(body).toJson(QJsonDocument::Compact); QNetworkReply *reply = authedPost(QStringLiteral("/api/vpn/peer"), payload); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); onProvisionReply(reply); }); } void RelayClient::onProvisionReply(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { emit vpnProvisionFailed(reply->errorString()); return; } const QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); const QString conf = json[QStringLiteral("conf")].toString(); if (conf.isEmpty()) { const QString err = json[QStringLiteral("error")].toString(QStringLiteral("No conf in response")); emit vpnProvisionFailed(err); return; } m_vpnPeerId = json[QStringLiteral("id")].toInt(-1); // Persist peer ID so we can revoke it even after a restart. m_settings->setValue(QStringLiteral("vpn/peer_id"), m_vpnPeerId); m_settings->setValue(QStringLiteral("vpn/relay_url"), m_relayUrl.toString()); RelayVPNConf vpnConf; vpnConf.conf = conf; emit vpnProvisioned(vpnConf); } // ─── revokeVPN() ───────────────────────────────────────────────────────────── void RelayClient::revokeVPN() { if (m_vpnPeerId < 0) { // Try to load from settings (post-restart cleanup). m_vpnPeerId = m_settings->value(QStringLiteral("vpn/peer_id"), -1).toInt(); } if (m_vpnPeerId < 0) { emit vpnRevoked(); // nothing to do return; } const QString path = QStringLiteral("/api/vpn/peer/") + QString::number(m_vpnPeerId); QNetworkReply *reply = authedDelete(path); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); onRevokeReply(reply); }); } void RelayClient::onRevokeReply(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) qWarning() << "[RelayClient] revokeVPN error:" << reply->errorString(); m_vpnPeerId = -1; m_settings->remove(QStringLiteral("vpn/peer_id")); emit vpnRevoked(); } // ─── fetchHosts() ───────────────────────────────────────────────────────────── void RelayClient::fetchHosts() { QNetworkReply *reply = authedGet(QStringLiteral("/api/hosts")); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); onHostsReply(reply); }); } void RelayClient::onHostsReply(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { emit hostsFetchFailed(reply->errorString()); return; } QList hosts; const QJsonArray arr = QJsonDocument::fromJson(reply->readAll()).array(); for (const QJsonValue &v : arr) { const QJsonObject obj = v.toObject(); RelayHost h; h.name = obj[QStringLiteral("name")].toString(); h.ip = obj[QStringLiteral("ip")].toString(); h.port = obj[QStringLiteral("port")].toInt(47984); // Parse displays array if present const QJsonArray displaysArr = obj[QStringLiteral("displays")].toArray(); for (const QJsonValue &dv : displaysArr) { const QJsonObject displayObj = dv.toObject(); QVariantMap displayMap; displayMap[QStringLiteral("name")] = displayObj[QStringLiteral("name")].toString(); displayMap[QStringLiteral("friendlyName")] = displayObj[QStringLiteral("friendlyName")].toString(); displayMap[QStringLiteral("width")] = displayObj[QStringLiteral("width")].toInt(); displayMap[QStringLiteral("height")] = displayObj[QStringLiteral("height")].toInt(); displayMap[QStringLiteral("isPrimary")] = displayObj[QStringLiteral("isPrimary")].toBool(); h.displays.append(displayMap); } if (h.displays.isEmpty()) { qWarning() << "Host" << h.name << "has no display data in relay response"; } if (!h.ip.isEmpty()) hosts << h; } emit hostsReady(hosts); } // ─── Persistence ────────────────────────────────────────────────────────────── void RelayClient::persistJwt() { // NOTE: QSettings on macOS stores in ~/Library/Preferences. // The JWT has a limited lifetime — don't rely on it across sessions; // the app will re-authenticate on next launch using saved credentials. m_settings->setValue(QStringLiteral("auth/relay_url"), m_relayUrl.toString()); m_settings->setValue(QStringLiteral("auth/username"), m_username); // We do NOT persist the JWT itself or the password. } void RelayClient::loadJwt() { // Intentionally empty — re-auth on each launch for security. } void RelayClient::saveServer(const QString &label, const QUrl &url, const QString &username, const QString &password) { m_settings->beginGroup(QStringLiteral("servers/") + label); m_settings->setValue(QStringLiteral("url"), url.toString()); m_settings->setValue(QStringLiteral("username"), username); // password stored in the system keychain would be better; plain QSettings // as a baseline — replace with QKeychain in production. m_settings->setValue(QStringLiteral("password"), password); m_settings->endGroup(); } QList RelayClient::savedServers() const { QList list; const QStringList groups = m_settings->childGroups(); for (const QString &g : groups) { if (!g.startsWith(QStringLiteral("servers/"))) continue; const QString label = g.mid(8); m_settings->beginGroup(g); SavedServer s; s.label = label; s.url = QUrl(m_settings->value(QStringLiteral("url")).toString()); s.username = m_settings->value(QStringLiteral("username")).toString(); s.password = m_settings->value(QStringLiteral("password")).toString()); m_settings->endGroup(); list << s; } return list; } void RelayClient::removeServer(const QString &label) { m_settings->remove(QStringLiteral("servers/") + label); }