feat/Implement QR code generation and scanning

This commit is contained in:
dranik
2026-05-07 14:34:40 +03:00
parent 009ca981d5
commit 2f6714e278
19 changed files with 864 additions and 0 deletions
@@ -0,0 +1,203 @@
#include "pairingController.h"
#include <QJsonDocument>
#include <QSysInfo>
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "version.h"
using namespace amnezia;
namespace
{
constexpr auto kGenerateQrEndpoint = "%1api/v1/generate_qr";
constexpr auto kScanQrEndpoint = "%1api/v1/scan_qr";
bool isLocalGatewayHost(const QString &gatewayUrl)
{
return gatewayUrl.contains(QStringLiteral("127.0.0.1"), Qt::CaseInsensitive)
|| gatewayUrl.contains(QStringLiteral("localhost"), Qt::CaseInsensitive);
}
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
{
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
if (apiStatus != ErrorCode::NoError) {
return apiStatus;
}
const QString config = obj.value(apiDefs::key::config).toString();
if (!config.isEmpty()) {
outPayload.config = config;
outPayload.serviceInfo = obj.value(apiDefs::key::serviceInfo).toObject();
outPayload.supportedProtocols = obj.value(apiDefs::key::supportedProtocols).toArray();
return ErrorCode::NoError;
}
if (obj.contains(QStringLiteral("detail"))) {
return ErrorCode::ApiConfigEmptyError;
}
const QString msg = obj.value(QStringLiteral("message")).toString();
if (msg.contains(QStringLiteral("timeout"), Qt::CaseInsensitive)) {
return ErrorCode::ApiConfigTimeoutError;
}
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingRateLimitedError;
}
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingServiceUnavailableError;
}
if (!msg.isEmpty()) {
return ErrorCode::ApiConfigDownloadError;
}
return ErrorCode::ApiConfigEmptyError;
}
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
{
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
if (apiStatus != ErrorCode::NoError) {
return apiStatus;
}
if (obj.value(QStringLiteral("message")).toString() == QLatin1String("OK")) {
return ErrorCode::NoError;
}
if (obj.contains(QStringLiteral("detail"))) {
return ErrorCode::ApiPairingForbiddenError;
}
const QString msg = obj.value(QStringLiteral("message")).toString();
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
return ErrorCode::ApiNotFoundError;
}
if (msg.contains(QStringLiteral("Conflict"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("already"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingConflictError;
}
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingRateLimitedError;
}
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingServiceUnavailableError;
}
if (!msg.isEmpty()) {
return ErrorCode::ApiConfigDownloadError;
}
return ErrorCode::ApiConfigEmptyError;
}
ErrorCode interpretGenerateQrJson(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
{
return applyGatewayOrOpenApiGenerateError(obj, outPayload);
}
ErrorCode interpretScanQrJson(const QJsonObject &obj)
{
return applyGatewayOrOpenApiScanError(obj);
}
} // namespace
ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload)
{
outPayload = QrPairingConfigPayload {};
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretGenerateQrJson(obj, outPayload);
}
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody)
{
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretScanQrJson(obj);
}
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
: m_appSettingsRepository(appSettingsRepository)
{
}
int PairingController::pairingLongPollTimeoutMsecs() const
{
const QString endpoint = m_appSettingsRepository->getGatewayEndpoint();
if (isLocalGatewayHost(endpoint)) {
return 120 * 1000;
}
return 30 * 1000;
}
QJsonObject PairingController::buildGenerateQrPayload(const QString &qrUuid) const
{
QJsonObject o;
o[apiDefs::key::qrUuid] = qrUuid;
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
o[apiDefs::key::appVersion] = QString(APP_VERSION);
o[apiDefs::key::osVersion] = QSysInfo::productType();
return o;
}
QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey) const
{
QJsonObject auth;
auth[apiDefs::key::apiKey] = apiKey;
QJsonObject o;
o[apiDefs::key::qrUuid] = qrUuid;
o[apiDefs::key::config] = vpnConfig;
o[apiDefs::key::serviceInfo] = serviceInfo;
o[apiDefs::key::supportedProtocols] = supportedProtocols;
o[apiDefs::key::authData] = auth;
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
o[apiDefs::key::appVersion] = QString(APP_VERSION);
o[apiDefs::key::osVersion] = QSysInfo::productType();
return o;
}
ErrorCode PairingController::startPairing(const QString &qrUuid, QrPairingConfigPayload &outPayload)
{
outPayload = QrPairingConfigPayload {};
if (qrUuid.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
pairingLongPollTimeoutMsecs(), m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const ErrorCode transportError = gatewayController.post(QString::fromLatin1(kGenerateQrEndpoint), buildGenerateQrPayload(qrUuid), responseBody);
if (transportError != ErrorCode::NoError) {
return transportError;
}
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretGenerateQrJson(obj, outPayload);
}
ErrorCode PairingController::completePairing(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey)
{
if (qrUuid.isEmpty() || vpnConfig.isEmpty() || apiKey.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const ErrorCode transportError =
gatewayController.post(QString::fromLatin1(kScanQrEndpoint),
buildScanQrPayload(qrUuid, vpnConfig, serviceInfo, supportedProtocols, apiKey), responseBody);
if (transportError != ErrorCode::NoError) {
return transportError;
}
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretScanQrJson(obj);
}
@@ -0,0 +1,45 @@
#ifndef PAIRINGCONTROLLER_H
#define PAIRINGCONTROLLER_H
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "core/utils/errorCodes.h"
class SecureAppSettingsRepository;
/**
* Core API for QR pairing against Amnezia gateway (POST /api/v1/generate_qr, /api/v1/scan_qr).
* Phase 1: transport via GatewayController, error mapping incl. gateway `http_status` wrapper and OpenAPI-style bodies.
*/
class PairingController
{
public:
struct QrPairingConfigPayload
{
QString config;
QJsonObject serviceInfo;
QJsonArray supportedProtocols;
};
explicit PairingController(SecureAppSettingsRepository *appSettingsRepository);
int pairingLongPollTimeoutMsecs() const;
QJsonObject buildGenerateQrPayload(const QString &qrUuid) const;
QJsonObject buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey) const;
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody);
amnezia::ErrorCode startPairing(const QString &qrUuid, QrPairingConfigPayload &outPayload);
amnezia::ErrorCode completePairing(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey);
private:
SecureAppSettingsRepository *m_appSettingsRepository;
};
#endif // PAIRINGCONTROLLER_H
@@ -145,6 +145,7 @@ void CoreController::initCoreControllers()
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
m_pairingController = new PairingController(m_appSettingsRepository);
m_newsController = new NewsController(m_appSettingsRepository, m_serversController);
m_updateController = new UpdateController(m_appSettingsRepository, this);
@@ -211,6 +212,9 @@ void CoreController::initControllers()
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
m_pairingUiController = new PairingUiController(m_pairingController, m_serversController, m_subscriptionController, m_appSettingsRepository, this);
setQmlContextProperty("PairingUiController", m_pairingUiController);
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
+4
View File
@@ -10,6 +10,8 @@
#endif
#include "ui/controllers/api/subscriptionUiController.h"
#include "ui/controllers/api/pairingUiController.h"
#include "core/controllers/api/pairingController.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/controllers/appSplitTunnelingUiController.h"
#include "ui/controllers/allowedDnsUiController.h"
@@ -164,6 +166,7 @@ private:
UpdateUiController* m_updateUiController;
SubscriptionUiController* m_subscriptionUiController;
PairingUiController* m_pairingUiController;
ApiNewsUiController* m_apiNewsUiController;
ServicesCatalogUiController* m_servicesCatalogUiController;
@@ -175,6 +178,7 @@ private:
AllowedDnsController* m_allowedDnsController;
ServicesCatalogController* m_servicesCatalogController;
SubscriptionController* m_subscriptionController;
PairingController* m_pairingController;
NewsController* m_newsController;
UpdateController* m_updateController;
InstallController* m_installController;
@@ -20,6 +20,7 @@
#include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/importUiController.h"
#include "ui/controllers/api/subscriptionUiController.h"
#include "ui/controllers/api/pairingUiController.h"
#include "ui/controllers/updateUiController.h"
#include "ui/models/serversModel.h"
#include "core/controllers/serversController.h"
@@ -97,6 +98,9 @@ void CoreSignalHandlers::initErrorMessagesHandler()
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
connect(m_coreController->m_pairingUiController, &PairingUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
connect(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
}
@@ -15,6 +15,8 @@
#include "QBlockCipher.h"
#include "QRsa.h"
#include "embedded_agw_public_keys.h"
#include "amneziaApplication.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiKeys.h"