mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
feat/Implement QR code generation and scanning
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user