dragonmoonlight/app/vpn/relayclient.cpp

265 lines
8.6 KiB
C++
Raw Permalink 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))
2026-05-06 19:01:40 -04:00
{
}
RelayClient::~RelayClient() = default;
QUrl RelayClient::apiUrl(const QString &path) const
{
QUrl u = m_relayUrl;
QString base = u.path();
while (base.endsWith(QLatin1Char('/'))) base.chop(1);
u.setPath(base + path);
2026-05-06 19:01:40 -04:00
return u;
}
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);
}
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"));
2026-05-06 19:01:40 -04:00
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"));
2026-05-06 19:01:40 -04:00
emit loginFailed(err);
return;
}
m_jwt = token;
m_username = username;
persistJwt();
emit loginSucceeded();
}
void RelayClient::provisionVPN(const QString &deviceName)
{
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"));
2026-05-06 19:01:40 -04:00
emit vpnProvisionFailed(err);
return;
}
m_vpnPeerId = json[QStringLiteral("id")].toInt(-1);
m_settings->setValue(QStringLiteral("vpn/peer_id"), m_vpnPeerId);
m_settings->setValue(QStringLiteral("vpn/relay_url"), m_relayUrl.toString());
2026-05-06 19:01:40 -04:00
2026-05-06 20:19:06 -04:00
RelayVPNConf vpnConf;
vpnConf.conf = conf;
emit vpnProvisioned(vpnConf);
2026-05-06 19:01:40 -04:00
}
void RelayClient::revokeVPN()
{
if (m_vpnPeerId < 0) {
m_vpnPeerId = m_settings->value(QStringLiteral("vpn/peer_id"), -1).toInt();
}
if (m_vpnPeerId < 0) {
emit vpnRevoked();
2026-05-06 19:01:40 -04:00
return;
}
const QString path = QStringLiteral("/api/vpn/peer/")
+ QString::number(m_vpnPeerId);
2026-05-06 19:01:40 -04:00
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();
}
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
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();
2026-05-06 20:16:28 -04:00
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();
2026-05-06 20:16:28 -04:00
h.displays.append(displayMap);
}
if (h.displays.isEmpty()) {
qDebug() << "Host" << h.name
<< "has no display data in relay response (older Artemis?)";
}
2026-05-06 19:01:40 -04:00
if (!h.ip.isEmpty())
hosts << h;
}
emit hostsReady(hosts);
}
void RelayClient::persistJwt()
{
m_settings->setValue(QStringLiteral("auth/relay_url"), m_relayUrl.toString());
m_settings->setValue(QStringLiteral("auth/username"), m_username);
}
void RelayClient::loadJwt()
{
// Intentionally empty - we re-auth on each launch for security.
2026-05-06 19:01:40 -04:00
}
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);
// TODO: replace with QKeychain so the password isn't stored in plaintext.
2026-05-06 19:01:40 -04:00
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); // length of "servers/"
2026-05-06 19:01:40 -04:00
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();
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);
}