dragonmoonlight/app/vpn/relayclient.cpp

280 lines
10 KiB
C++
Raw Normal View History

2026-05-06 19:01:40 -04:00
// relayclient.cpp
#include "relayclient.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QSettings>
#include <QSysInfo>
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 ────────────────────────────────────────────────
2026-05-06 19:01:40 -04:00
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());
2026-05-06 20:19:06 -04:00
RelayVPNConf vpnConf;
vpnConf.conf = conf;
emit vpnProvisioned(vpnConf);
2026-05-06 19:01:40 -04:00
}
// ─── 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<RelayHost> 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);
2026-05-06 20:16:28 -04:00
// 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";
}
2026-05-06 19:01:40 -04:00
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::SavedServer> RelayClient::savedServers() const
{
QList<SavedServer> 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();
2026-05-06 20:16:28 -04:00
s.password = m_settings->value(QStringLiteral("password")).toString());
2026-05-06 19:01:40 -04:00
m_settings->endGroup();
list << s;
}
return list;
}
void RelayClient::removeServer(const QString &label)
{
m_settings->remove(QStringLiteral("servers/") + label);
}