From db3164223a037d7bd4da7864f3d09eeabfd3fa5c Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 12 Feb 2025 12:43:11 +0700 Subject: [PATCH] feature: added share vpn key to subscription settings page --- client/CMakeLists.txt | 1 + client/core/api/apiDefs.h | 2 + client/core/defs.h | 3 -- client/core/qrCodeUtils.cpp | 35 ++++++++++++++ client/core/qrCodeUtils.h | 17 +++++++ .../controllers/api/apiConfigsController.cpp | 30 ++++++++++-- .../ui/controllers/api/apiConfigsController.h | 11 +++++ client/ui/controllers/exportController.cpp | 46 ++++--------------- client/ui/controllers/exportController.h | 3 -- client/ui/controllers/importController.cpp | 19 ++++++-- .../qml/Components/ShareConnectionDrawer.qml | 12 +++-- .../qml/Pages2/PageSettingsApiServerInfo.qml | 20 ++++++++ 12 files changed, 147 insertions(+), 52 deletions(-) create mode 100644 client/core/qrCodeUtils.cpp create mode 100644 client/core/qrCodeUtils.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 1ed3df08a..24edfb073 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -59,6 +59,7 @@ endif() qt_standard_project_setup() qt_add_executable(${PROJECT} MANUAL_FINALIZATION core/api/apiDefs.h + core/qrCodeUtils.h core/qrCodeUtils.cpp ) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h index 0431839c6..2df4c8337 100644 --- a/client/core/api/apiDefs.h +++ b/client/core/api/apiDefs.h @@ -24,6 +24,8 @@ namespace apiDefs constexpr QLatin1String apiConfig("api_config"); constexpr QLatin1String stackType("stack_type"); + + constexpr QLatin1String vpnKey("vpn_key"); } const int requestTimeoutMsecs = 12 * 1000; // 12 secs diff --git a/client/core/defs.h b/client/core/defs.h index c0db2e127..4ea0e6130 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -6,9 +6,6 @@ namespace amnezia { - - constexpr const qint16 qrMagicCode = 1984; - struct ServerCredentials { QString hostName; diff --git a/client/core/qrCodeUtils.cpp b/client/core/qrCodeUtils.cpp new file mode 100644 index 000000000..02f43ff07 --- /dev/null +++ b/client/core/qrCodeUtils.cpp @@ -0,0 +1,35 @@ +#include "qrCodeUtils.h" + +#include +#include + +QList qrCodeUtuls::generateQrCodeImageSeries(const QByteArray &data) +{ + double k = 850; + + quint8 chunksCount = std::ceil(data.size() / k); + QList chunks; + for (int i = 0; i < data.size(); i = i + k) { + QByteArray chunk; + QDataStream s(&chunk, QIODevice::WriteOnly); + s << qrCodeUtuls::qrMagicCode << chunksCount << (quint8)std::round(i / k) << data.mid(i, k); + + QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(ba, qrcodegen::QrCode::Ecc::LOW); + QString svg = QString::fromStdString(toSvgString(qr, 1)); + chunks.append(svgToBase64(svg)); + } + + return chunks; +} + +QString qrCodeUtuls::svgToBase64(const QString &image) +{ + return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); +} + +qrcodegen::QrCode qrCodeUtuls::generateQrCode(const QByteArray &data) +{ + return qrcodegen::QrCode::encodeText(data, qrcodegen::QrCode::Ecc::LOW); +} diff --git a/client/core/qrCodeUtils.h b/client/core/qrCodeUtils.h new file mode 100644 index 000000000..f5f207c15 --- /dev/null +++ b/client/core/qrCodeUtils.h @@ -0,0 +1,17 @@ +#ifndef QRCODEUTILS_H +#define QRCODEUTILS_H + +#include + +#include "qrcodegen.hpp" + +namespace qrCodeUtuls +{ + constexpr const qint16 qrMagicCode = 1984; + + QList generateQrCodeImageSeries(const QByteArray &data); + qrcodegen::QrCode generateQrCode(const QByteArray &data); + QString svgToBase64(const QString &image); +}; + +#endif // QRCODEUTILS_H diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index f791c7e81..0858ecff2 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -1,10 +1,11 @@ -#include "ApiConfigsController.h" +#include "apiConfigsController.h" #include "configurators/wireguard_configurator.h" #include "core/api/apiDefs.h" #include "core/controllers/gatewayController.h" -#include "version.h" +#include "core/qrCodeUtils.h" #include "ui/controllers/systemController.h" +#include "version.h" namespace { @@ -43,7 +44,7 @@ namespace } ApiConfigsController::ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, - QObject *parent) + QObject *parent) : QObject(parent), m_serversModel(serversModel), m_settings(settings) { } @@ -84,6 +85,19 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, return true; } +void ApiConfigsController::prepareVpnKeyExport() +{ + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); + + auto qr = qrCodeUtuls::generateQrCode(vpnKey.toUtf8()); + m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + + emit vpnKeyExportReady(); +} + ApiConfigsController::ApiPayloadData ApiConfigsController::generateApiPayloadData(const QString &protocol) { ApiConfigsController::ApiPayloadData apiPayload; @@ -111,3 +125,13 @@ QJsonObject ApiConfigsController::fillApiPayload(const QString &protocol, const return obj; } + +QList ApiConfigsController::getQrCodes() +{ + return m_qrCodes; +} + +int ApiConfigsController::getQrCodesCount() +{ + return m_qrCodes.size(); +} diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index 861bc176c..f9bf977f4 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -13,11 +13,17 @@ class ApiConfigsController : public QObject ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, QObject *parent = nullptr); + Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) + Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady) + public slots: bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); + // bool exportVpnKey(const QString &fileName); + void prepareVpnKeyExport(); signals: void errorOccurred(ErrorCode errorCode); + void vpnKeyExportReady(); private: struct ApiPayloadData @@ -31,6 +37,11 @@ public slots: ApiPayloadData generateApiPayloadData(const QString &protocol); QJsonObject fillApiPayload(const QString &protocol, const ApiPayloadData &apiPayloadData); + QList getQrCodes(); + int getQrCodesCount(); + + QList m_qrCodes; + QSharedPointer m_serversModel; std::shared_ptr m_settings; }; diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 8681406e9..516d1f4d0 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -9,8 +9,8 @@ #include #include "core/controllers/vpnConfigurationController.h" +#include "core/qrCodeUtils.h" #include "systemController.h" -#include "qrcodegen.hpp" ExportController::ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const QSharedPointer &clientManagementModel, @@ -50,7 +50,7 @@ void ExportController::generateFullAccessConfig() compressedConfig = qCompress(compressedConfig, 8); m_config = QString("vpn://%1").arg(QString(compressedConfig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))); - m_qrCodes = generateQrCodeImageSeries(compressedConfig); + m_qrCodes = qrCodeUtuls::generateQrCodeImageSeries(compressedConfig); emit exportConfigChanged(); } @@ -92,7 +92,7 @@ void ExportController::generateConnectionConfig(const QString &clientName) compressedConfig = qCompress(compressedConfig, 8); m_config = QString("vpn://%1").arg(QString(compressedConfig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))); - m_qrCodes = generateQrCodeImageSeries(compressedConfig); + m_qrCodes = qrCodeUtuls::generateQrCodeImageSeries(compressedConfig); emit exportConfigChanged(); } @@ -149,7 +149,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName) m_config.append(line + "\n"); } - m_qrCodes = generateQrCodeImageSeries(m_config.toUtf8()); + m_qrCodes = qrCodeUtuls::generateQrCodeImageSeries(m_config.toUtf8()); emit exportConfigChanged(); } @@ -167,8 +167,8 @@ void ExportController::generateWireGuardConfig(const QString &clientName) m_config.append(line + "\n"); } - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW); - m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtuls::generateQrCode(m_config.toUtf8()); + m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -187,8 +187,8 @@ void ExportController::generateAwgConfig(const QString &clientName) m_config.append(line + "\n"); } - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW); - m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtuls::generateQrCode(m_config.toUtf8()); + m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -221,8 +221,8 @@ void ExportController::generateShadowSocksConfig() m_nativeConfigString = "ss://" + m_nativeConfigString.toUtf8().toBase64(); - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_nativeConfigString.toUtf8(), qrcodegen::QrCode::Ecc::LOW); - m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtuls::generateQrCode(m_nativeConfigString.toUtf8()); + m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -312,32 +312,6 @@ void ExportController::renameClient(const int row, const QString &clientName, co } } -QList ExportController::generateQrCodeImageSeries(const QByteArray &data) -{ - double k = 850; - - quint8 chunksCount = std::ceil(data.size() / k); - QList chunks; - for (int i = 0; i < data.size(); i = i + k) { - QByteArray chunk; - QDataStream s(&chunk, QIODevice::WriteOnly); - s << amnezia::qrMagicCode << chunksCount << (quint8)std::round(i / k) << data.mid(i, k); - - QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(ba, qrcodegen::QrCode::Ecc::LOW); - QString svg = QString::fromStdString(toSvgString(qr, 1)); - chunks.append(svgToBase64(svg)); - } - - return chunks; -} - -QString ExportController::svgToBase64(const QString &image) -{ - return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); -} - int ExportController::getQrCodesCount() { return m_qrCodes.size(); diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index a2c9fcfaf..5fb3e6b33 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -50,9 +50,6 @@ public slots: void saveFile(const QString &fileName, const QString &data); private: - QList generateQrCodeImageSeries(const QByteArray &data); - QString svgToBase64(const QString &image); - int getQrCodesCount(); void clearPreviousConfig(); diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 28bbc9f6b..c79b32882 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -7,6 +7,8 @@ #include #include +#include "core/api/apiDefs.h" +#include "core/api/apiUtils.h" #include "core/errorstrings.h" #include "core/serialization/serialization.h" #include "systemController.h" @@ -45,7 +47,8 @@ namespace if (config.contains(backupPattern)) { return ConfigTypes::Backup; - } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) || config.contains(amneziaPremiumConfigPattern) + } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) + || config.contains(amneziaPremiumConfigPattern) || (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName) && config.contains(amneziaConfigPatternPassword))) { return ConfigTypes::Amnezia; @@ -149,8 +152,8 @@ bool ImportController::extractConfigFromData(QString data) m_configType = checkConfigFormat(config); if (m_configType == ConfigTypes::Invalid) { - data.replace("vpn://", ""); - QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + config.replace("vpn://", ""); + QByteArray ba = QByteArray::fromBase64(config.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); QByteArray ba_uncompressed = qUncompress(ba); if (!ba_uncompressed.isEmpty()) { ba = ba_uncompressed; @@ -180,6 +183,13 @@ bool ImportController::extractConfigFromData(QString data) } case ConfigTypes::Amnezia: { m_config = QJsonDocument::fromJson(config.toUtf8()).object(); + + if (apiUtils::isServerFromApi(m_config)) { + auto apiConfig = m_config.value(apiDefs::key::apiConfig).toObject(); + apiConfig[apiDefs::key::vpnKey] = data; + m_config[apiDefs::key::apiConfig] = apiConfig; + } + processAmneziaConfig(m_config); if (!m_config.empty()) { checkForMaliciousStrings(m_config); @@ -680,7 +690,8 @@ void ImportController::processAmneziaConfig(QJsonObject &config) } QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object(); - jsonConfig[config_key::mtu] = dockerContainer == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; + jsonConfig[config_key::mtu] = + dockerContainer == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; containerConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index f98944f07..9a9f1aa16 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -22,7 +22,11 @@ DrawerType2 { property string headerText property string configContentHeaderText - property string contentVisible + property string shareButtonText: qsTr("Share") + property string copyButtonText: qsTr("Copy") + property bool showSettingsButtonVisible: true + + property bool contentVisible property string configExtension: ".vpn" property string configCaption: qsTr("Save AmneziaVPN config") @@ -80,7 +84,7 @@ DrawerType2 { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("Share") + text: root.shareButtonText leftImageSource: "qrc:/images/controls/share-2.svg" clickedFunc: function() { @@ -116,7 +120,7 @@ DrawerType2 { textColor: AmneziaStyle.color.paleGray borderWidth: 1 - text: qsTr("Copy") + text: root.copyButtonText leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } @@ -153,6 +157,8 @@ DrawerType2 { Layout.leftMargin: 16 Layout.rightMargin: 16 + visible: root.showSettingsButtonVisible + defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite pressedColor: AmneziaStyle.color.sheerWhite diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 659df1684..303ba36cd 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -160,6 +160,20 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { + shareConnectionDrawer.headerText = qsTr("Amnezia Premium subscription key") + + shareConnectionDrawer.openTriggered() + shareConnectionDrawer.contentVisible = false + shareConnectionDrawer.showSettingsButtonVisible = false; + shareConnectionDrawer.shareButtonText = qsTr("Save VPN key to file") + shareConnectionDrawer.copyButtonText = qsTr("Copy VPN key") + + + PageController.showBusyIndicator(true) + + ApiConfigsController.prepareVpnKeyExport() + + PageController.showBusyIndicator(false) } } @@ -292,4 +306,10 @@ PageType { } } } + + ShareConnectionDrawer { + id: shareConnectionDrawer + + anchors.fill: parent + } }