279 lines
10 KiB
C++
279 lines
10 KiB
C++
// 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 ────────────────────────────────────────────────
|
|
|
|
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<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);
|
|
|
|
// 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::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();
|
|
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);
|
|
}
|