From 7c8ae9c3114e4fc597f8dc81ea1d704dba8ed157 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 31 Jan 2025 10:06:35 +0700 Subject: [PATCH 01/36] refactoring: moved api info pages from ServerInfo --- client/resources.qrc | 3 +- client/ui/controllers/pageController.h | 2 + .../ui/qml/Components/RenameServerDrawer.qml | 55 ++++++++++++ client/ui/qml/Components/ServersListView.qml | 13 ++- client/ui/qml/Pages2/PageHome.qml | 12 ++- ... => PageSettingsApiAvailableCountries.qml} | 72 +++++++++++++++- .../qml/Pages2/PageSettingsApiServerInfo.qml | 79 ++++++++++++++++- .../ui/qml/Pages2/PageSettingsServerInfo.qml | 85 ++----------------- .../ui/qml/Pages2/PageSettingsServersList.qml | 12 ++- 9 files changed, 245 insertions(+), 88 deletions(-) create mode 100644 client/ui/qml/Components/RenameServerDrawer.qml rename client/ui/qml/Pages2/{PageSettingsApiLanguageList.qml => PageSettingsApiAvailableCountries.qml} (63%) diff --git a/client/resources.qrc b/client/resources.qrc index ff03a6e7..c5563618 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -192,7 +192,7 @@ ui/qml/Pages2/PageServiceTorWebsiteSettings.qml ui/qml/Pages2/PageSettings.qml ui/qml/Pages2/PageSettingsAbout.qml - ui/qml/Pages2/PageSettingsApiLanguageList.qml + ui/qml/Pages2/PageSettingsApiAvailableCountries.qml ui/qml/Pages2/PageSettingsApiServerInfo.qml ui/qml/Pages2/PageSettingsApplication.qml ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -224,6 +224,7 @@ ui/qml/Pages2/PageShare.qml ui/qml/Pages2/PageShareFullAccess.qml ui/qml/Pages2/PageStart.qml + ui/qml/Components/RenameServerDrawer.qml images/flagKit/ZW.svg diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index ffbdd3a1..428cf4f8 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -31,6 +31,8 @@ namespace PageLoader PageSettingsLogging, PageSettingsSplitTunneling, PageSettingsAppSplitTunneling, + PageSettingsApiServerInfo, + PageSettingsApiAvailableCountries, PageServiceSftpSettings, PageServiceTorWebsiteSettings, diff --git a/client/ui/qml/Components/RenameServerDrawer.qml b/client/ui/qml/Components/RenameServerDrawer.qml new file mode 100644 index 00000000..d65b9bba --- /dev/null +++ b/client/ui/qml/Components/RenameServerDrawer.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" + +import "../Config" + +DrawerType2 { + property string serverNameText + + id: root + objectName: "serverNameEditDrawer" + + expandedStateContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + TextFieldWithHeaderType { + id: serverName + + Layout.fillWidth: true + headerText: qsTr("Server name") + textField.text: root.serverNameText + textField.maximumLength: 30 + checkEmptyText: true + } + + BasicButtonType { + id: saveButton + + Layout.fillWidth: true + + text: qsTr("Save") + + clickedFunc: function() { + if (serverName.textField.text === "") { + return + } + + if (serverName.textField.text !== root.serverNameText) { + ServersModel.setProcessedServerData("name", serverName.textField.text); + } + root.closeTriggered() + } + } + } +} diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index dc5c5a33..8d591d86 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -110,7 +110,18 @@ ListView { onClicked: function() { ServersModel.processedIndex = index - PageController.goToPage(PageEnum.PageSettingsServerInfo) + + if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { + PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) + + } else { + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } else { + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } + drawer.closeTriggered() } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index ae29b80c..fc5f5326 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -297,7 +297,17 @@ PageType { onClicked: { ServersModel.processedIndex = ServersModel.defaultIndex - PageController.goToPage(PageEnum.PageSettingsServerInfo) + + if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { + PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) + + } else { + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } else { + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } } diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml similarity index 63% rename from client/ui/qml/Pages2/PageSettingsApiLanguageList.qml rename to client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index 30968b38..853a44a3 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -3,6 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs +import SortFilterProxyModel 0.2 + import PageEnum 1.0 import Style 1.0 @@ -15,22 +17,86 @@ import "../Components" PageType { id: root + property var processedServer + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + + SortFilterProxyModel { + id: proxyServersModel + objectName: "proxyServersModel" + + sourceModel: ServersModel + filters: [ + ValueFilter { + roleName: "isCurrentlyProcessed" + value: true + } + ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } + } + ListView { id: menuContent property bool isFocusable: true - width: parent.width - height: parent.height + anchors.fill: parent + + ScrollBar.vertical: ScrollBarType {} clip: true - interactive: true + reuseItems: true + snapMode: ListView.SnapToItem + model: ApiCountryModel + currentIndex: 0 + ButtonGroup { id: containersRadioButtonGroup } + header: ColumnLayout { + width: menuContent.width + + spacing: 4 + + BackButtonType { + id: backButton + objectName: "backButton" + + Layout.topMargin: 20 + } + + HeaderType { + id: headerContent + objectName: "headerContent" + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 10 + + actionButtonImage: "qrc:/images/controls/settings.svg" + + headerText: root.processedServer.name + descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") + + actionButtonFunction: function() { + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } + } + delegate: ColumnLayout { id: content diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 1fc17218..4346e617 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -3,6 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs +import SortFilterProxyModel 0.2 + import PageEnum 1.0 import Style 1.0 @@ -54,12 +56,40 @@ PageType { readonly property string objectImageSource: "qrc:/images/controls/gauge.svg" } + property var processedServer + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + + SortFilterProxyModel { + id: proxyServersModel + objectName: "proxyServersModel" + + sourceModel: ServersModel + filters: [ + ValueFilter { + roleName: "isCurrentlyProcessed" + value: true + } + ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } + } + ListView { id: listView - anchors.fill: parent property bool isFocusable: true + anchors.fill: parent + Keys.onTabPressed: { FocusController.nextKeyTabItem() } @@ -86,9 +116,54 @@ PageType { ScrollBar.vertical: ScrollBarType {} - model: labelsModel clip: true reuseItems: true + snapMode: ListView.SnapToItem + + model: labelsModel + + header: ColumnLayout { + width: listView.width + + spacing: 4 + + BackButtonType { + id: backButton + objectName: "backButton" + + Layout.topMargin: 20 + } + + HeaderType { + id: headerContent + objectName: "headerContent" + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 10 + + actionButtonImage: "qrc:/images/controls/edit-3.svg" + + headerText: root.processedServer.name + descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") + + actionButtonFunction: function() { + serverNameEditDrawer.openTriggered() + } + } + + RenameServerDrawer { + id: serverNameEditDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.35 + + serverNameText: root.processedServer.name + } + } delegate: ColumnLayout { width: listView.width diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 9abb6ae2..d350ebef 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -22,8 +22,6 @@ PageType { readonly property int pageSettingsServerProtocols: 0 readonly property int pageSettingsServerServices: 1 readonly property int pageSettingsServerData: 2 - readonly property int pageSettingsApiServerInfo: 3 - readonly property int pageSettingsApiLanguageList: 4 property var processedServer @@ -71,15 +69,6 @@ PageType { BackButtonType { id: backButton objectName: "backButton" - - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - root.processedServer.isCountrySelectionAvailable) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } - } } HeaderType { @@ -91,18 +80,11 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 10 - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" - : "qrc:/images/controls/edit-3.svg" + actionButtonImage: "qrc:/images/controls/edit-3.svg" headerText: root.processedServer.name descriptionText: { - if (root.processedServer.isServerFromGatewayApi) { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate") - } else { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } - } else if (root.processedServer.isServerFromTelegramApi) { + if (root.processedServer.isServerFromTelegramApi) { return root.processedServer.serverDescription } else if (root.processedServer.hasWriteAccess) { return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName @@ -112,60 +94,19 @@ PageType { } actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.openTriggered() - } + serverNameEditDrawer.openTriggered() } } - DrawerType2 { + RenameServerDrawer { id: serverNameEditDrawer - objectName: "serverNameEditDrawer" parent: root anchors.fill: parent expandedHeight: root.height * 0.35 - expandedStateContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - TextFieldWithHeaderType { - id: serverName - - Layout.fillWidth: true - headerText: qsTr("Server name") - textField.text: root.processedServer.name - textField.maximumLength: 30 - checkEmptyText: true - } - - BasicButtonType { - id: saveButton - - Layout.fillWidth: true - - text: qsTr("Save") - - clickedFunc: function() { - if (serverName.textField.text === "") { - return - } - - if (serverName.textField.text !== root.processedServer.name) { - ServersModel.setProcessedServerData("name", serverName.textField.text); - } - serverNameEditDrawer.closeTriggered() - } - } - } + serverNameText: root.processedServer.name } TabBar { @@ -181,8 +122,6 @@ PageType { color: AmneziaStyle.color.transparent } - visible: !ServersModel.getProcessedServerData("isServerFromGatewayApi") - TabButtonType { id: protocolsTab @@ -221,9 +160,7 @@ PageType { Layout.fillWidth: true - currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? - (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? - root.pageSettingsApiLanguageList : root.pageSettingsApiServerInfo) : tabBar.currentIndex + currentIndex: tabBar.currentIndex PageSettingsServerProtocols { id: protocolsPage @@ -239,16 +176,6 @@ PageType { id: dataPage stackView: root.stackView } - - PageSettingsApiServerInfo { - id: apiInfoPage - stackView: root.stackView - } - - PageSettingsApiLanguageList { - id: apiLanguageListPage - stackView: root.stackView - } } } } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index 17337a48..b13f4947 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -93,7 +93,17 @@ PageType { clickedFunction: function() { ServersModel.processedIndex = index - PageController.goToPage(PageEnum.PageSettingsServerInfo) + + if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { + PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) + + } else { + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } else { + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } From 3f55f6a629bcd67fb47966fabe09f17c5c493174 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 31 Jan 2025 14:33:12 +0700 Subject: [PATCH 02/36] refactoring: moved gateway interaction functions to a separate class --- client/CMakeLists.txt | 3 +- client/core/controllers/apiController.cpp | 267 +--------------- client/core/controllers/gatewayController.cpp | 300 ++++++++++++++++++ client/core/controllers/gatewayController.h | 35 ++ client/core/networkUtilities.cpp | 20 ++ client/core/networkUtilities.h | 5 + 6 files changed, 369 insertions(+), 261 deletions(-) create mode 100644 client/core/controllers/gatewayController.cpp create mode 100644 client/core/controllers/gatewayController.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 3ef92385..4b7540f0 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -57,7 +57,8 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) endif() qt_standard_project_setup() -qt_add_executable(${PROJECT} MANUAL_FINALIZATION) +qt_add_executable(${PROJECT} MANUAL_FINALIZATION + core/controllers/gatewayController.h core/controllers/gatewayController.cpp) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 6562632a..52ec86c2 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -1,21 +1,15 @@ #include "apiController.h" -#include -#include - #include #include #include #include -#include "QBlockCipher.h" -#include "QRsa.h" - #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" #include "core/enums/apiEnums.h" -#include "utilities.h" #include "version.h" +#include "gatewayController.h" namespace { @@ -51,48 +45,6 @@ namespace } const int requestTimeoutMsecs = 12 * 1000; // 12 secs - - ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) - { - if (!sslErrors.empty()) { - qDebug().noquote() << sslErrors; - return ErrorCode::ApiConfigSslError; - } else if (reply->error() == QNetworkReply::NoError) { - return ErrorCode::NoError; - } else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - return ErrorCode::ApiConfigTimeoutError; - } else { - QString err = reply->errorString(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - return ErrorCode::ApiConfigDownloadError; - } - } - - bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", - const QByteArray &iv = "", const QByteArray &salt = "") - { - if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - qDebug() << "Timeout occurred"; - return true; - } else if (responseBody.contains("html")) { - qDebug() << "The response contains an html tag"; - return true; - } else if (checkEncryption) { - try { - QSimpleCrypto::QBlockCipher blockCipher; - static_cast(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt)); - } catch (...) { - qDebug() << "Failed to decrypt the data"; - return true; - } - } - return false; - } } ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent) @@ -176,75 +128,6 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle return; } -QStringList ApiController::getProxyUrls() -{ - QNetworkRequest request; - request.setTransferTimeout(requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QEventLoop wait; - QList sslErrors; - QNetworkReply *reply; - - QStringList proxyStorageUrl; - if (m_isDevEnvironment) { - proxyStorageUrl = QStringList { DEV_S3_ENDPOINT }; - } else { - proxyStorageUrl = QStringList { PROD_S3_ENDPOINT }; - } - - QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; - - for (const auto &proxyStorageUrl : proxyStorageUrl) { - request.setUrl(proxyStorageUrl); - reply = amnApp->manager()->get(request); - - connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(); - - if (reply->error() == QNetworkReply::NetworkError::NoError) { - break; - } - reply->deleteLater(); - } - - auto encryptedResponseBody = reply->readAll(); - reply->deleteLater(); - - EVP_PKEY *privateKey = nullptr; - QByteArray responseBody; - try { - if (!m_isDevEnvironment) { - QCryptographicHash hash(QCryptographicHash::Sha512); - hash.addData(key); - QByteArray hashResult = hash.result().toHex(); - - QByteArray key = QByteArray::fromHex(hashResult.left(64)); - QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); - - QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); - - QSimpleCrypto::QBlockCipher blockCipher; - responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); - } else { - responseBody = encryptedResponseBody; - } - } catch (...) { - Utils::logException(); - qCritical() << "error loading private key from environment variables or decrypting payload"; - return {}; - } - - auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); - - QStringList endpoints; - for (const auto &endpoint : endpointsArray) { - endpoints.push_back(endpoint.toString()); - } - return endpoints; -} - ApiController::ApiPayloadData ApiController::generateApiPayloadData(const QString &protocol) { ApiController::ApiPayloadData apiPayload; @@ -332,54 +215,8 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c ErrorCode ApiController::getServicesList(QByteArray &responseBody) { -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - QNetworkRequest request; - request.setTransferTimeout(requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); - - QNetworkReply *reply; - reply = amnApp->manager()->get(request); - - QEventLoop wait; - QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - - QList sslErrors; - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(); - - responseBody = reply->readAll(); - - if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { - m_proxyUrls = getProxyUrls(); - std::random_device randomDevice; - std::mt19937 generator(randomDevice()); - std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator); - for (const QString &proxyUrl : m_proxyUrls) { - qDebug() << "Go to the next endpoint"; - request.setUrl(QString("%1v1/services").arg(proxyUrl)); - reply->deleteLater(); // delete the previous reply - reply = amnApp->manager()->get(request); - - QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(); - - responseBody = reply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, responseBody, false)) { - break; - } - } - } - - auto errorCode = checkErrors(sslErrors, reply); - reply->deleteLater(); - + GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); + ErrorCode errorCode = gatewayController.get("%1v1/services", responseBody); if (errorCode == ErrorCode::NoError) { if (!responseBody.contains("services")) { return ErrorCode::ApiServicesMissingError; @@ -393,16 +230,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig) { -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - QNetworkRequest request; - request.setTransferTimeout(requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint)); + GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); @@ -417,92 +245,11 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co apiPayload[configKey::authData] = authData; } - QSimpleCrypto::QBlockCipher blockCipher; - QByteArray key = blockCipher.generatePrivateSalt(32); - QByteArray iv = blockCipher.generatePrivateSalt(32); - QByteArray salt = blockCipher.generatePrivateSalt(8); + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody); - QJsonObject keyPayload; - keyPayload[configKey::aesKey] = QString(key.toBase64()); - keyPayload[configKey::aesIv] = QString(iv.toBase64()); - keyPayload[configKey::aesSalt] = QString(salt.toBase64()); - - QByteArray encryptedKeyPayload; - QByteArray encryptedApiPayload; - try { - QSimpleCrypto::QRsa rsa; - - EVP_PKEY *publicKey = nullptr; - try { - QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; - QSimpleCrypto::QRsa rsa; - publicKey = rsa.getPublicKeyFromByteArray(rsaKey); - } catch (...) { - Utils::logException(); - qCritical() << "error loading public key from environment variables"; - return ErrorCode::ApiMissingAgwPublicKey; - } - - encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); - EVP_PKEY_free(publicKey); - - encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); - } catch (...) { // todo change error handling in QSimpleCrypto? - Utils::logException(); - qCritical() << "error when encrypting the request body"; - return ErrorCode::ApiConfigDecryptionError; - } - - QJsonObject requestBody; - requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); - requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - - QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); - - QEventLoop wait; - connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - - QList sslErrors; - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(); - - auto encryptedResponseBody = reply->readAll(); - - if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { - m_proxyUrls = getProxyUrls(); - std::random_device randomDevice; - std::mt19937 generator(randomDevice()); - std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator); - for (const QString &proxyUrl : m_proxyUrls) { - qDebug() << "Go to the next endpoint"; - request.setUrl(QString("%1v1/config").arg(proxyUrl)); - reply->deleteLater(); // delete the previous reply - reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); - - QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); - connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(); - - encryptedResponseBody = reply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { - break; - } - } - } - - auto errorCode = checkErrors(sslErrors, reply); - reply->deleteLater(); - if (errorCode) { - return errorCode; - } - - try { - auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); + if (errorCode == ErrorCode::NoError) { fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig); - } catch (...) { // todo change error handling in QSimpleCrypto? - Utils::logException(); - qCritical() << "error when decrypting the request body"; - return ErrorCode::ApiConfigDecryptionError; } return errorCode; diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp new file mode 100644 index 00000000..44a3d5d1 --- /dev/null +++ b/client/core/controllers/gatewayController.cpp @@ -0,0 +1,300 @@ +#include "gatewayController.h" + +#include +#include +#include +#include + +#include "QBlockCipher.h" +#include "QRsa.h" + +#include "amnezia_application.h" +#include "core/networkUtilities.h" +#include "utilities.h" + +namespace +{ + namespace configKey + { + constexpr char aesKey[] = "aes_key"; + constexpr char aesIv[] = "aes_iv"; + constexpr char aesSalt[] = "aes_salt"; + + constexpr char apiPayload[] = "api_payload"; + constexpr char keyPayload[] = "key_payload"; + } +} + +GatewayController::GatewayController(const QString &gatewayEndpoint, bool isDevEnvironment, int requestTimeoutMsecs, QObject *parent) + : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment), m_requestTimeoutMsecs(requestTimeoutMsecs) +{ +} + +ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody) +{ +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + request.setUrl(QString(endpoint).arg(m_gatewayEndpoint)); + + QNetworkReply *reply; + reply = amnApp->manager()->get(request); + + QEventLoop wait; + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + + QList sslErrors; + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + responseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { + auto requestFunction = [&request, &responseBody](const QString &url) { + request.setUrl(url); + return amnApp->manager()->get(request); + }; + + auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply, + const QList &nestedSslErrors) { + responseBody = nestedReply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) { + sslErrors = nestedSslErrors; + reply = nestedReply; + return true; + } + return false; + }; + + bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); + } + + auto errorCode = NetworkUtilities::checkNetworkReplyErrors(sslErrors, reply); + reply->deleteLater(); + + return errorCode; +} + +ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +{ +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + request.setUrl(endpoint.arg(m_gatewayEndpoint)); + + QSimpleCrypto::QBlockCipher blockCipher; + QByteArray key = blockCipher.generatePrivateSalt(32); + QByteArray iv = blockCipher.generatePrivateSalt(32); + QByteArray salt = blockCipher.generatePrivateSalt(8); + + QJsonObject keyPayload; + keyPayload[configKey::aesKey] = QString(key.toBase64()); + keyPayload[configKey::aesIv] = QString(iv.toBase64()); + keyPayload[configKey::aesSalt] = QString(salt.toBase64()); + + QByteArray encryptedKeyPayload; + QByteArray encryptedApiPayload; + try { + QSimpleCrypto::QRsa rsa; + + EVP_PKEY *publicKey = nullptr; + try { + QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + QSimpleCrypto::QRsa rsa; + publicKey = rsa.getPublicKeyFromByteArray(rsaKey); + } catch (...) { + Utils::logException(); + qCritical() << "error loading public key from environment variables"; + return ErrorCode::ApiMissingAgwPublicKey; + } + + encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); + EVP_PKEY_free(publicKey); + + encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); + } catch (...) { // todo change error handling in QSimpleCrypto? + Utils::logException(); + qCritical() << "error when encrypting the request body"; + return ErrorCode::ApiConfigDecryptionError; + } + + QJsonObject requestBody; + requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); + requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); + + QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); + + QEventLoop wait; + connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + + QList sslErrors; + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + QByteArray encryptedResponseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, false)) { + auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { + request.setUrl(url); + return amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); + }; + + auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, + this](QNetworkReply *nestedReply, const QList &nestedSslErrors) { + encryptedResponseBody = nestedReply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { + sslErrors = nestedSslErrors; + reply = nestedReply; + return true; + } + return false; + }; + + bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); + } + + auto errorCode = NetworkUtilities::checkNetworkReplyErrors(sslErrors, reply); + reply->deleteLater(); + if (errorCode) { + return errorCode; + } + + try { + responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); + return ErrorCode::NoError; + } catch (...) { // todo change error handling in QSimpleCrypto? + Utils::logException(); + qCritical() << "error when decrypting the request body"; + return ErrorCode::ApiConfigDecryptionError; + } +} + +QStringList GatewayController::getProxyUrls() +{ + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QEventLoop wait; + QList sslErrors; + QNetworkReply *reply; + + QStringList proxyStorageUrl; + if (m_isDevEnvironment) { + proxyStorageUrl = QStringList { DEV_S3_ENDPOINT }; + } else { + proxyStorageUrl = QStringList { PROD_S3_ENDPOINT }; + } + + QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + + for (const auto &proxyStorageUrl : proxyStorageUrl) { + request.setUrl(proxyStorageUrl); + reply = amnApp->manager()->get(request); + + connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + if (reply->error() == QNetworkReply::NetworkError::NoError) { + break; + } + reply->deleteLater(); + } + + auto encryptedResponseBody = reply->readAll(); + reply->deleteLater(); + + EVP_PKEY *privateKey = nullptr; + QByteArray responseBody; + try { + if (!m_isDevEnvironment) { + QCryptographicHash hash(QCryptographicHash::Sha512); + hash.addData(key); + QByteArray hashResult = hash.result().toHex(); + + QByteArray key = QByteArray::fromHex(hashResult.left(64)); + QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); + + QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); + + QSimpleCrypto::QBlockCipher blockCipher; + responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); + } else { + responseBody = encryptedResponseBody; + } + } catch (...) { + Utils::logException(); + qCritical() << "error loading private key from environment variables or decrypting payload"; + return {}; + } + + auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); + + QStringList endpoints; + for (const auto &endpoint : endpointsArray) { + endpoints.push_back(endpoint.toString()); + } + return endpoints; +} + +bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, + const QByteArray &iv, const QByteArray &salt) +{ + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + qDebug() << "Timeout occurred"; + return true; + } else if (responseBody.contains("html")) { + qDebug() << "The response contains an html tag"; + return true; + } else if (checkEncryption) { + try { + QSimpleCrypto::QBlockCipher blockCipher; + static_cast(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt)); + } catch (...) { + qDebug() << "Failed to decrypt the data"; + return true; + } + } + return false; +} + +void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply, + std::function requestFunction, + std::function &sslErrors)> replyProcessingFunction) +{ + QStringList proxyUrls = getProxyUrls(); + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); + + QEventLoop wait; + QList sslErrors; + QByteArray responseBody; + + for (const QString &proxyUrl : proxyUrls) { + qDebug() << "Go to the next endpoint"; + reply->deleteLater(); // delete the previous reply + reply = requestFunction(endpoint.arg(proxyUrl)); + + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + if (!replyProcessingFunction(reply, sslErrors)) { + break; + } + } +} diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h new file mode 100644 index 00000000..45d989f0 --- /dev/null +++ b/client/core/controllers/gatewayController.h @@ -0,0 +1,35 @@ +#ifndef GATEWAYCONTROLLER_H +#define GATEWAYCONTROLLER_H + +#include +#include + +#include "core/defs.h" + +#ifdef Q_OS_IOS + #include "platforms/ios/ios_controller.h" +#endif + +class GatewayController : public QObject +{ + Q_OBJECT + +public: + explicit GatewayController(const QString &gatewayEndpoint, bool isDevEnvironment, int requestTimeoutMsecs, QObject *parent = nullptr); + + amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody); + amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); + +private: + QStringList getProxyUrls(); + bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", + const QByteArray &iv = "", const QByteArray &salt = ""); + void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function requestFunction, + std::function &sslErrors)> replyProcessingFunction); + + int m_requestTimeoutMsecs; + QString m_gatewayEndpoint; + bool m_isDevEnvironment = false; +}; + +#endif // GATEWAYCONTROLLER_H diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index a5825f0d..7d98e6a1 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -107,6 +107,26 @@ QStringList NetworkUtilities::summarizeRoutes(const QStringList &ips, const QStr return QStringList(); } +amnezia::ErrorCode NetworkUtilities::checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply) +{ + if (!sslErrors.empty()) { + qDebug().noquote() << sslErrors; + return amnezia::ErrorCode::ApiConfigSslError; + } else if (reply->error() == QNetworkReply::NoError) { + return amnezia::ErrorCode::NoError; + } else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + return amnezia::ErrorCode::ApiConfigTimeoutError; + } else { + QString err = reply->errorString(); + qDebug() << QString::fromUtf8(reply->readAll()); + qDebug() << reply->error(); + qDebug() << err; + qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + return amnezia::ErrorCode::ApiConfigDownloadError; + } +} + QString NetworkUtilities::getIPAddress(const QString &host) { QHostAddress address(host); diff --git a/client/core/networkUtilities.h b/client/core/networkUtilities.h index 3057b852..805ce9e5 100644 --- a/client/core/networkUtilities.h +++ b/client/core/networkUtilities.h @@ -5,6 +5,9 @@ #include #include #include +#include + +#include "core/defs.h" class NetworkUtilities : public QObject @@ -31,6 +34,8 @@ public: static QStringList summarizeRoutes(const QStringList &ips, const QString cidr); + static amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply); + }; #endif // NETWORKUTILITIES_H From b183a3b232c2c5161b5d838ce51230b42f256c74 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 6 Feb 2025 15:26:47 +0700 Subject: [PATCH 03/36] feature: added pages for subscription settings feature --- client/CMakeLists.txt | 176 +--------------- client/amnezia_application.cpp | 6 + client/amnezia_application.h | 7 +- client/cmake/sources.cmake | 189 ++++++++++++++++++ client/core/api/apiUtils.cpp | 10 + client/core/api/apiUtils.h | 11 + client/core/controllers/apiController.cpp | 44 +++- client/core/controllers/apiController.h | 7 +- client/resources.qrc | 3 + .../controllers/api/apiSettingsController.cpp | 57 ++++++ .../controllers/api/apiSettingsController.h | 27 +++ .../ui/controllers/api/importController.cpp | 2 + client/ui/controllers/api/importController.h | 24 +++ client/ui/controllers/installController.cpp | 1 + client/ui/controllers/pageController.h | 6 +- client/ui/models/api/apiAccountInfoModel.cpp | 105 ++++++++++ client/ui/models/api/apiAccountInfoModel.h | 55 +++++ client/ui/qml/Components/ServersListView.qml | 3 +- client/ui/qml/Controls2/ListViewType.qml | 38 ++++ client/ui/qml/Pages2/PageHome.qml | 3 +- client/ui/qml/Pages2/PageSettingsAbout.qml | 3 +- .../Pages2/PageSettingsApiInstructions.qml | 87 ++++++++ .../qml/Pages2/PageSettingsApiServerInfo.qml | 145 ++++++-------- .../ui/qml/Pages2/PageSettingsApiSupport.qml | 107 ++++++++++ .../ui/qml/Pages2/PageSettingsServerData.qml | 10 - .../ui/qml/Pages2/PageSettingsServersList.qml | 7 +- client/ui/qml/Pages2/PageStart.qml | 10 + 27 files changed, 856 insertions(+), 287 deletions(-) create mode 100644 client/cmake/sources.cmake create mode 100644 client/core/api/apiUtils.cpp create mode 100644 client/core/api/apiUtils.h create mode 100644 client/ui/controllers/api/apiSettingsController.cpp create mode 100644 client/ui/controllers/api/apiSettingsController.h create mode 100644 client/ui/controllers/api/importController.cpp create mode 100644 client/ui/controllers/api/importController.h create mode 100644 client/ui/models/api/apiAccountInfoModel.cpp create mode 100644 client/ui/models/api/apiAccountInfoModel.h create mode 100644 client/ui/qml/Controls2/ListViewType.qml create mode 100644 client/ui/qml/Pages2/PageSettingsApiInstructions.qml create mode 100644 client/ui/qml/Pages2/PageSettingsApiSupport.qml diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 4b7540f0..294b9646 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -57,8 +57,7 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) endif() qt_standard_project_setup() -qt_add_executable(${PROJECT} MANUAL_FINALIZATION - core/controllers/gatewayController.h core/controllers/gatewayController.cpp) +qt_add_executable(${PROJECT} MANUAL_FINALIZATION) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) @@ -111,8 +110,8 @@ if(IS_CI) endif() endif() - include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake) include_directories( ${CMAKE_CURRENT_LIST_DIR}/../ipc @@ -121,167 +120,22 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) -configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) - -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/migrations.h - ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc.h - ${CMAKE_CURRENT_LIST_DIR}/amnezia_application.h - ${CMAKE_CURRENT_LIST_DIR}/containers/containers_defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.h - ${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.h - ${CMAKE_CURRENT_LIST_DIR}/core/server_defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/apiController.h - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.h - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h - ${CMAKE_CURRENT_LIST_DIR}/ui/pages.h - ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h - ${CMAKE_CURRENT_BINARY_DIR}/version.h - ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.h - ${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.h - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h - ${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h - ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h - ${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.h -) - -# Mozilla headres -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/mozilla/models/server.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/ipaddress.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/leakdetector.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/controllerimpl.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/localsocketcontroller.h -) - include_directories(mozilla) include_directories(mozilla/shared) include_directories(mozilla/models) -if(NOT IOS) - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.h - ) -endif() - -if(NOT ANDROID) - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h - ) -endif() - -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/migrations.cpp - ${CMAKE_CURRENT_LIST_DIR}/amnezia_application.cpp - ${CMAKE_CURRENT_LIST_DIR}/containers/containers_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/server_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/apiController.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/outbound.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/inbound.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/ss.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/ssd.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vless.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp - ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp - ${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.cpp -) - -# Mozilla sources -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/mozilla/models/server.cpp - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/ipaddress.cpp - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/leakdetector.cpp - ${CMAKE_CURRENT_LIST_DIR}/mozilla/localsocketcontroller.cpp -) +configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(${PROJECT} PRIVATE "MZ_DEBUG") endif() -if(NOT IOS) - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.cpp - ) -endif() - -if(NOT ANDROID) - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp - ) -endif() - -file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.h) -file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.cpp) - -file(GLOB_RECURSE PAGE_LOGIC_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/pages_logic/*.h) -file(GLOB_RECURSE PAGE_LOGIC_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/pages_logic/*.cpp) - -file(GLOB CONFIGURATORS_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/configurators/*.h) -file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/configurators/*.cpp) - -file(GLOB UI_MODELS_H CONFIGURE_DEPENDS - ${CMAKE_CURRENT_LIST_DIR}/ui/models/*.h - ${CMAKE_CURRENT_LIST_DIR}/ui/models/protocols/*.h - ${CMAKE_CURRENT_LIST_DIR}/ui/models/services/*.h -) -file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS - ${CMAKE_CURRENT_LIST_DIR}/ui/models/*.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/models/protocols/*.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/models/services/*.cpp -) - -file(GLOB UI_CONTROLLERS_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/controllers/*.h) -file(GLOB UI_CONTROLLERS_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/controllers/*.cpp) - -set(HEADERS ${HEADERS} - ${COMMON_FILES_H} - ${PAGE_LOGIC_H} - ${CONFIGURATORS_H} - ${UI_MODELS_H} - ${UI_CONTROLLERS_H} -) -set(SOURCES ${SOURCES} - ${COMMON_FILES_CPP} - ${PAGE_LOGIC_CPP} - ${CONFIGURATORS_CPP} - ${UI_MODELS_CPP} - ${UI_CONTROLLERS_CPP} -) - if(WIN32) configure_file( ${CMAKE_CURRENT_LIST_DIR}/platforms/windows/amneziavpn.rc.in ${CMAKE_CURRENT_BINARY_DIR}/amneziavpn.rc ) - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/protocols/ikev2_vpn_protocol_windows.h - ) - - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/protocols/ikev2_vpn_protocol_windows.cpp - ) - - set(RESOURCES ${RESOURCES} - ${CMAKE_CURRENT_BINARY_DIR}/amneziavpn.rc - ) - set(LIBS ${LIBS} user32 rasapi32 @@ -325,30 +179,6 @@ endif() if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) message("Client desktop build") add_compile_definitions(AMNEZIA_DESKTOP) - - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/core/ipcclient.h - ${CMAKE_CURRENT_LIST_DIR}/core/privileged_process.h - ${CMAKE_CURRENT_LIST_DIR}/ui/systemtray_notificationhandler.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnovercloakprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/shadowsocksvpnprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/wireguardprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/xrayprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/awgprotocol.h - ) - - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/core/ipcclient.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/privileged_process.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/systemtray_notificationhandler.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnovercloakprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/shadowsocksvpnprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/wireguardprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/xrayprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/awgprotocol.cpp - ) endif() if(ANDROID) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index aeed439b..2bd0b991 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -378,6 +378,9 @@ void AmneziaApplication::initModels() }); connect(m_serversModel.get(), &ServersModel::updateApiServicesModel, this, [this]() { m_apiServicesModel->updateModel(m_serversModel->getProcessedServerData("apiConfig").toJsonObject()); }); + + m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this)); + m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get()); } void AmneziaApplication::initControllers() @@ -463,4 +466,7 @@ void AmneziaApplication::initControllers() m_systemController.reset(new SystemController(m_settings)); m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + + m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_settings)); + m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); } diff --git a/client/amnezia_application.h b/client/amnezia_application.h index cfeac0d1..dcadb77b 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -25,6 +25,8 @@ #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" #include "ui/controllers/appSplitTunnelingController.h" +// #include "ui/controllers/api/importController.h" +#include "ui/controllers/api/apiSettingsController.h" #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" #include "ui/models/protocols/cloakConfigModel.h" @@ -48,6 +50,7 @@ #include "ui/models/appSplitTunnelingModel.h" #include "ui/models/apiServicesModel.h" #include "ui/models/apiCountryModel.h" +#include "ui/models/api/apiAccountInfoModel.h" #define amnApp (static_cast(QCoreApplication::instance())) @@ -104,6 +107,7 @@ private: QSharedPointer m_clientManagementModel; QSharedPointer m_apiServicesModel; QSharedPointer m_apiCountryModel; + QSharedPointer m_apiAccountInfoModel; QScopedPointer m_openVpnConfigModel; QScopedPointer m_shadowSocksConfigModel; @@ -114,7 +118,6 @@ private: #ifdef Q_OS_WINDOWS QScopedPointer m_ikev2ConfigModel; #endif - QScopedPointer m_sftpConfigModel; QScopedPointer m_socks5ConfigModel; @@ -135,6 +138,8 @@ private: QScopedPointer m_systemController; QScopedPointer m_appSplitTunnelingController; + QScopedPointer m_apiSettingsController; + QNetworkAccessManager *m_nam; QMetaObject::Connection m_reloadConfigErrorOccurredConnection; diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake new file mode 100644 index 00000000..ace83685 --- /dev/null +++ b/client/cmake/sources.cmake @@ -0,0 +1,189 @@ +set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) + +set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/migrations.h + ${CLIENT_ROOT_DIR}/../ipc/ipc.h + ${CLIENT_ROOT_DIR}/amnezia_application.h + ${CLIENT_ROOT_DIR}/containers/containers_defs.h + ${CLIENT_ROOT_DIR}/core/defs.h + ${CLIENT_ROOT_DIR}/core/errorstrings.h + ${CLIENT_ROOT_DIR}/core/scripts_registry.h + ${CLIENT_ROOT_DIR}/core/server_defs.h + ${CLIENT_ROOT_DIR}/core/controllers/apiController.h + ${CLIENT_ROOT_DIR}/core/controllers/gatewayController.h + ${CLIENT_ROOT_DIR}/core/controllers/serverController.h + ${CLIENT_ROOT_DIR}/core/controllers/vpnConfigurationController.h + ${CLIENT_ROOT_DIR}/protocols/protocols_defs.h + ${CLIENT_ROOT_DIR}/protocols/qml_register_protocols.h + ${CLIENT_ROOT_DIR}/ui/pages.h + ${CLIENT_ROOT_DIR}/ui/qautostart.h + ${CLIENT_ROOT_DIR}/protocols/vpnprotocol.h + ${CMAKE_CURRENT_BINARY_DIR}/version.h + ${CLIENT_ROOT_DIR}/core/sshclient.h + ${CLIENT_ROOT_DIR}/core/networkUtilities.h + ${CLIENT_ROOT_DIR}/core/serialization/serialization.h + ${CLIENT_ROOT_DIR}/core/serialization/transfer.h + ${CLIENT_ROOT_DIR}/core/enums/apiEnums.h + ${CLIENT_ROOT_DIR}/../common/logger/logger.h + ${CLIENT_ROOT_DIR}/utils/qmlUtils.h + ${CLIENT_ROOT_DIR}/core/api/apiUtils.h +) + +# Mozilla headres +set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/mozilla/models/server.h + ${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.h + ${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.h + ${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h + ${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h +) + +if(NOT IOS) + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h + ) +endif() + +if(NOT ANDROID) + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/ui/notificationhandler.h + ) +endif() + +set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/migrations.cpp + ${CLIENT_ROOT_DIR}/amnezia_application.cpp + ${CLIENT_ROOT_DIR}/containers/containers_defs.cpp + ${CLIENT_ROOT_DIR}/core/errorstrings.cpp + ${CLIENT_ROOT_DIR}/core/scripts_registry.cpp + ${CLIENT_ROOT_DIR}/core/server_defs.cpp + ${CLIENT_ROOT_DIR}/core/controllers/apiController.cpp + ${CLIENT_ROOT_DIR}/core/controllers/gatewayController.cpp + ${CLIENT_ROOT_DIR}/core/controllers/serverController.cpp + ${CLIENT_ROOT_DIR}/core/controllers/vpnConfigurationController.cpp + ${CLIENT_ROOT_DIR}/protocols/protocols_defs.cpp + ${CLIENT_ROOT_DIR}/ui/qautostart.cpp + ${CLIENT_ROOT_DIR}/protocols/vpnprotocol.cpp + ${CLIENT_ROOT_DIR}/core/sshclient.cpp + ${CLIENT_ROOT_DIR}/core/networkUtilities.cpp + ${CLIENT_ROOT_DIR}/core/serialization/outbound.cpp + ${CLIENT_ROOT_DIR}/core/serialization/inbound.cpp + ${CLIENT_ROOT_DIR}/core/serialization/ss.cpp + ${CLIENT_ROOT_DIR}/core/serialization/ssd.cpp + ${CLIENT_ROOT_DIR}/core/serialization/vless.cpp + ${CLIENT_ROOT_DIR}/core/serialization/trojan.cpp + ${CLIENT_ROOT_DIR}/core/serialization/vmess.cpp + ${CLIENT_ROOT_DIR}/core/serialization/vmess_new.cpp + ${CLIENT_ROOT_DIR}/../common/logger/logger.cpp + ${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp + ${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp +) + +# Mozilla sources +set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/mozilla/models/server.cpp + ${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.cpp + ${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp + ${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp +) + +if(NOT IOS) + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp + ) +endif() + +if(NOT ANDROID) + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp + ) +endif() + +file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/*.h) +file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/*.cpp) + +file(GLOB_RECURSE PAGE_LOGIC_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/pages_logic/*.h) +file(GLOB_RECURSE PAGE_LOGIC_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/pages_logic/*.cpp) + +file(GLOB CONFIGURATORS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/configurators/*.h) +file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/configurators/*.cpp) + +file(GLOB UI_MODELS_H CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/models/*.h + ${CLIENT_ROOT_DIR}/ui/models/protocols/*.h + ${CLIENT_ROOT_DIR}/ui/models/services/*.h + ${CLIENT_ROOT_DIR}/ui/models/api/*.h +) +file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/models/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/services/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/api/*.cpp +) + +file(GLOB UI_CONTROLLERS_H CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/controllers/*.h + ${CLIENT_ROOT_DIR}/ui/controllers/api/*.h +) +file(GLOB UI_CONTROLLERS_CPP CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/controllers/*.cpp + ${CLIENT_ROOT_DIR}/ui/controllers/api/*.cpp +) + +set(HEADERS ${HEADERS} + ${COMMON_FILES_H} + ${PAGE_LOGIC_H} + ${CONFIGURATORS_H} + ${UI_MODELS_H} + ${UI_CONTROLLERS_H} +) +set(SOURCES ${SOURCES} + ${COMMON_FILES_CPP} + ${PAGE_LOGIC_CPP} + ${CONFIGURATORS_CPP} + ${UI_MODELS_CPP} + ${UI_CONTROLLERS_CPP} +) + +if(WIN32) + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/protocols/ikev2_vpn_protocol_windows.h + ) + + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/protocols/ikev2_vpn_protocol_windows.cpp + ) + + set(RESOURCES ${RESOURCES} + ${CMAKE_CURRENT_BINARY_DIR}/amneziavpn.rc + ) +endif() + +if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) + message("Client desktop build") + add_compile_definitions(AMNEZIA_DESKTOP) + + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/core/ipcclient.h + ${CLIENT_ROOT_DIR}/core/privileged_process.h + ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h + ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h + ${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h + ${CLIENT_ROOT_DIR}/protocols/shadowsocksvpnprotocol.h + ${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.h + ${CLIENT_ROOT_DIR}/protocols/xrayprotocol.h + ${CLIENT_ROOT_DIR}/protocols/awgprotocol.h + ) + + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/core/ipcclient.cpp + ${CLIENT_ROOT_DIR}/core/privileged_process.cpp + ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp + ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/shadowsocksvpnprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/xrayprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/awgprotocol.cpp + ) +endif() diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp new file mode 100644 index 00000000..eed34f65 --- /dev/null +++ b/client/core/api/apiUtils.cpp @@ -0,0 +1,10 @@ +#include "apiUtils.h" + +#include + +bool ApiUtils::isSubscriptionExpired(const QString &subscriptionEndDate) +{ + QDateTime now = QDateTime::currentDateTime(); + QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs); + return endDate < now; +} diff --git a/client/core/api/apiUtils.h b/client/core/api/apiUtils.h new file mode 100644 index 00000000..dc863c22 --- /dev/null +++ b/client/core/api/apiUtils.h @@ -0,0 +1,11 @@ +#ifndef APIUTILS_H +#define APIUTILS_H + +#include + +namespace ApiUtils +{ + bool isSubscriptionExpired(const QString &subscriptionEndDate); +} + +#endif // APIUTILS_H diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 52ec86c2..4e43f148 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -8,8 +8,8 @@ #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" #include "core/enums/apiEnums.h" -#include "version.h" #include "gatewayController.h" +#include "version.h" namespace { @@ -213,10 +213,29 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c } } +ErrorCode ApiController::getAccountInfo(const QString &userCountryCode, const QString &serviceType, const QJsonObject &authData, + QByteArray &responseBody) +{ + GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); + + QJsonObject apiPayload; + apiPayload[configKey::userCountryCode] = userCountryCode; + apiPayload[configKey::serviceType] = serviceType; + apiPayload[configKey::authData] = authData; + + ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody); + + return errorCode; +} + ErrorCode ApiController::getServicesList(QByteArray &responseBody) { GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); - ErrorCode errorCode = gatewayController.get("%1v1/services", responseBody); + + QJsonObject apiPayload; + apiPayload[configKey::osVersion] = QSysInfo::productType(); + + ErrorCode errorCode = gatewayController.post(QString("%1v1/services"), apiPayload, responseBody); if (errorCode == ErrorCode::NoError) { if (!responseBody.contains("services")) { return ErrorCode::ApiServicesMissingError; @@ -254,3 +273,24 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co return errorCode; } + +ErrorCode ApiController::getNativeConfig(const QString &userCountryCode, const QString &serviceType, const QString &protocol, + const QString &serverCountryCode, const QJsonObject &authData, QString &nativeConfig) +{ + GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); + + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = userCountryCode; + apiPayload[configKey::serverCountryCode] = serverCountryCode; + apiPayload[configKey::serviceType] = serviceType; + apiPayload[configKey::authData] = authData; + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/country_config"), apiPayload, responseBody); + + nativeConfig = responseBody; + + return errorCode; +} diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index bcb25f96..9fb1f49d 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -19,9 +19,14 @@ public: public slots: void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); + ErrorCode getAccountInfo(const QString &userCountryCode, const QString &serviceType, const QJsonObject &authData, + QByteArray &responseBody); ErrorCode getServicesList(QByteArray &responseBody); ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig); + const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, + QJsonObject &serverConfig); + ErrorCode getNativeConfig(const QString &userCountryCode, const QString &serviceType, const QString &protocol, + const QString &serverCountryCode, const QJsonObject &authData, QString &nativeConfig); signals: void errorOccurred(ErrorCode errorCode); diff --git a/client/resources.qrc b/client/resources.qrc index c5563618..308accda 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -225,6 +225,9 @@ ui/qml/Pages2/PageShareFullAccess.qml ui/qml/Pages2/PageStart.qml ui/qml/Components/RenameServerDrawer.qml + ui/qml/Controls2/ListViewType.qml + ui/qml/Pages2/PageSettingsApiSupport.qml + ui/qml/Pages2/PageSettingsApiInstructions.qml images/flagKit/ZW.svg diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp new file mode 100644 index 00000000..d1025432 --- /dev/null +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -0,0 +1,57 @@ +#include "apiSettingsController.h" + +#include "core/controllers/gatewayController.h" + +namespace +{ + namespace configKey + { + constexpr char userCountryCode[] = "user_country_code"; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceInfo[] = "service_info"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; + } + + const int requestTimeoutMsecs = 12 * 1000; // 12 secs +} + +ApiSettingsController::ApiSettingsController(const QSharedPointer &serversModel, + const QSharedPointer &apiAccountInfoModel, const std::shared_ptr &settings, + QObject *parent) + : QObject(parent), m_serversModel(serversModel), m_apiAccountInfoModel(apiAccountInfoModel), m_settings(settings) +{ +} + +ApiSettingsController::~ApiSettingsController() +{ +} + +bool ApiSettingsController::getAccountInfo() +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs); + + auto processedIndex = m_serversModel->getProcessedServerIndex(); + auto serverConfig = m_serversModel->getServerConfig(processedIndex); + auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + auto authData = serverConfig.value(configKey::authData).toObject(); + + QJsonObject apiPayload; + apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); + apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString(); + apiPayload[configKey::authData] = authData; + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError) { + // emit errorOccured(errorCode); + return false; + } + + QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object(); + m_apiAccountInfoModel->updateModel(accountInfo, serverConfig); + + return true; +} diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h new file mode 100644 index 00000000..cad0e9a1 --- /dev/null +++ b/client/ui/controllers/api/apiSettingsController.h @@ -0,0 +1,27 @@ +#ifndef APISETTINGSCONTROLLER_H +#define APISETTINGSCONTROLLER_H + +#include + +#include "ui/models/api/apiAccountInfoModel.h" +#include "ui/models/servers_model.h" + +class ApiSettingsController : public QObject +{ + Q_OBJECT +public: + ApiSettingsController(const QSharedPointer &serversModel, const QSharedPointer &apiAccountInfoModel, + const std::shared_ptr &settings, QObject *parent = nullptr); + ~ApiSettingsController(); + +public slots: + bool getAccountInfo(); + +private: + QSharedPointer m_serversModel; + QSharedPointer m_apiAccountInfoModel; + + std::shared_ptr m_settings; +}; + +#endif // APISETTINGSCONTROLLER_H diff --git a/client/ui/controllers/api/importController.cpp b/client/ui/controllers/api/importController.cpp new file mode 100644 index 00000000..e352326f --- /dev/null +++ b/client/ui/controllers/api/importController.cpp @@ -0,0 +1,2 @@ +#include "importController.h" + diff --git a/client/ui/controllers/api/importController.h b/client/ui/controllers/api/importController.h new file mode 100644 index 00000000..f4580360 --- /dev/null +++ b/client/ui/controllers/api/importController.h @@ -0,0 +1,24 @@ +#ifndef IMPORTCONTROLLER_H +#define IMPORTCONTROLLER_H + +#include + +#include "ui/models/api/apiAccountInfoModel.h" +#include "ui/models/servers_model.h" + +// namespace api +// { +// class ImportController : public QObject +// { +// Q_OBJECT +// public: +// ImportController(const QSharedPointer &serversModel, QSharedPointer &accountInfoModel); +// ~ImportController(); + +// private: +// QSharedPointer m_serversModel; +// QSharedPointer m_accountInfoModel; +// }; +// } + +#endif // IMPORTCONTROLLER_H diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index ae0804cb..26e433e0 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -832,6 +832,7 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin auto authData = serverConfig.value(configKey::authData).toObject(); QJsonObject newServerConfig; + ErrorCode errorCode = apiController.getConfigForService( m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(), apiConfig.value(configKey::serviceType).toString(), apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode, diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 428cf4f8..6e1efda5 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -33,6 +33,8 @@ namespace PageLoader PageSettingsAppSplitTunneling, PageSettingsApiServerInfo, PageSettingsApiAvailableCountries, + PageSettingsApiSupport, + PageSettingsApiInstructions, PageServiceSftpSettings, PageServiceTorWebsiteSettings, @@ -55,7 +57,7 @@ namespace PageLoader PageProtocolOpenVpnSettings, PageProtocolShadowSocksSettings, PageProtocolCloakSettings, - PageProtocolXraySettings, + PageProtocolXraySettings, PageProtocolWireGuardSettings, PageProtocolAwgSettings, PageProtocolIKev2Settings, @@ -106,7 +108,7 @@ public slots: int incrementDrawerDepth(); int decrementDrawerDepth(); - private slots: +private slots: void onShowErrorMessage(amnezia::ErrorCode errorCode); signals: diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp new file mode 100644 index 00000000..7679f0ff --- /dev/null +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -0,0 +1,105 @@ +#include "apiAccountInfoModel.h" + +#include + +#include "core/api/apiUtils.h" +#include "logger.h" + +namespace +{ + Logger logger("AccountInfoModel"); + + namespace configKey + { + constexpr char availableCountries[] = "available_countries"; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serverCountryName[] = "server_country_name"; + constexpr char lastUpdated[] = "last_updated"; + constexpr char activeDeviceCount[] = "active_device_count"; + constexpr char maxDeviceCount[] = "max_device_count"; + constexpr char subscriptionEndDate[] = "subscription_end_date"; + } +} + +ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +int ApiAccountInfoModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) + return QVariant(); + + switch (role) { + case SubscriptionStatusRole: { + return ApiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active"); + } + case EndDateRole: { + return QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } + case ConnectedDevicesRole: { + return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount); + } + // case ServiceDescriptionRole: { + // return apiServiceData.serviceInfo.name; + // } + } + + return QVariant(); +} + +void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig) +{ + beginResetModel(); + + AccountInfoData accountInfoData; + + auto availableCountries = accountInfoObject.value(configKey::availableCountries).toArray(); + for (const auto &country : availableCountries) { + auto countryObject = country.toObject(); + CountryInfo countryInfo; + countryInfo.serverCountryCode = countryObject.value(configKey::serverCountryCode).toString(); + countryInfo.serverCountryName = countryObject.value(configKey::serverCountryName).toString(); + countryInfo.lastUpdated = countryObject.value(configKey::lastUpdated).toString(); + + accountInfoData.AvailableCountries.push_back(countryInfo); + } + + accountInfoData.activeDeviceCount = accountInfoObject.value(configKey::activeDeviceCount).toInt(); + accountInfoData.maxDeviceCount = accountInfoObject.value(configKey::maxDeviceCount).toInt(); + accountInfoData.subscriptionEndDate = accountInfoObject.value(configKey::subscriptionEndDate).toString(); + + m_accountInfoData = accountInfoData; + + endResetModel(); +} + +QVariant ApiAccountInfoModel::data(const QString &roleString) +{ + QModelIndex modelIndex = index(0); + auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return data(modelIndex, it.key()); + } + } + + return {}; +} + +QHash ApiAccountInfoModel::roleNames() const +{ + QHash roles; + roles[SubscriptionStatusRole] = "subscriptionStatus"; + roles[EndDateRole] = "endDate"; + roles[ConnectedDevicesRole] = "connectedDevices"; + roles[ServiceDescriptionRole] = "serviceDescription"; + + return roles; +} diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h new file mode 100644 index 00000000..f7cabb69 --- /dev/null +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -0,0 +1,55 @@ +#ifndef APIACCOUNTINFOMODEL_H +#define APIACCOUNTINFOMODEL_H + +#include +#include +#include + +class ApiAccountInfoModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + SubscriptionStatusRole = Qt::UserRole + 1, + ConnectedDevicesRole, + ServiceDescriptionRole, + EndDateRole + }; + + explicit ApiAccountInfoModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig); + QVariant data(const QString &roleString); + +protected: + QHash roleNames() const override; + +private: + struct CountryInfo + { + QString serverCountryCode; + QString serverCountryName; + QString lastUpdated; + }; + + struct AccountInfoData + { + QString subscriptionEndDate; + int activeDeviceCount; + int maxDeviceCount; + + QString vpnKey; + + QVector AvailableCountries; + }; + + AccountInfoData m_accountInfoData; +}; + +#endif // APIACCOUNTINFOMODEL_H diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index 8d591d86..870b83b7 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -112,9 +112,10 @@ ListView { ServersModel.processedIndex = index if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + ApiSettingsController.getAccountInfo() + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) - } else { PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } diff --git a/client/ui/qml/Controls2/ListViewType.qml b/client/ui/qml/Controls2/ListViewType.qml new file mode 100644 index 00000000..0de43d77 --- /dev/null +++ b/client/ui/qml/Controls2/ListViewType.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls + +ListView { + id: root + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + ScrollBar.vertical: ScrollBarType {} + + clip: true + reuseItems: true + snapMode: ListView.SnapToItem +} diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index fc5f5326..38a87e72 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -299,9 +299,10 @@ PageType { ServersModel.processedIndex = ServersModel.defaultIndex if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + ApiSettingsController.getAccountInfo() + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) - } else { PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index 2160692b..37327313 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -47,8 +47,7 @@ PageType { readonly property string description: qsTr("For reviews and bug reports") readonly property string imageSource: "qrc:/images/controls/mail.svg" readonly property var handler: function() { - GC.copyToClipBoard(title) - PageController.showNotificationMessage(qsTr("Copied")) + Qt.openUrlExternally(qsTr("mailto:support@amnezia.org")) } } diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml new file mode 100644 index 00000000..9106808a --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + QtObject { + id: windows + + readonly property string title: qsTr("Windows") + readonly property string imageSource: "qrc:/images/controls/external-link.svg" + readonly property var handler: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) + } + } + + QtObject { + id: linux + + readonly property string title: qsTr("Windows") + readonly property string imageSource: "qrc:/images/controls/external-link.svg" + readonly property var handler: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) + } + } + + property list instructionsModel: [ + windows, + linux + ] + + ListViewType { + id: listView + + anchors.fill: parent + + model: instructionsModel + + header: ColumnLayout { + width: listView.width + + BackButtonType { + id: backButton + } + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: "Support" + descriptionText: qsTr("Our technical support specialists are ready to help you at any time") + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + id: telegramButton + Layout.fillWidth: true + Layout.topMargin: 6 + + text: title + leftImageSource: imageSource + + clickedFunction: handler + } + + DividerType {} + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 4346e617..350f2ea2 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -18,28 +18,19 @@ PageType { id: root property list labelsModel: [ - regionObject, - priceObject, + statusObject, endDateObject, - speedObject + deviceCountObject ] QtObject { - id: regionObject + id: statusObject - readonly property string title: qsTr("For the region") - readonly property string contentKey: "region" + readonly property string title: qsTr("Subscription status") + readonly property string contentKey: "subscriptionStatus" readonly property string objectImageSource: "qrc:/images/controls/map-pin.svg" } - QtObject { - id: priceObject - - readonly property string title: qsTr("Price") - readonly property string contentKey: "price" - readonly property string objectImageSource: "qrc:/images/controls/tag.svg" - } - QtObject { id: endDateObject @@ -49,10 +40,10 @@ PageType { } QtObject { - id: speedObject + id: deviceCountObject - readonly property string title: qsTr("Speed") - readonly property string contentKey: "speed" + readonly property string title: qsTr("Connected devices") + readonly property string contentKey: "connectedDevices" readonly property string objectImageSource: "qrc:/images/controls/gauge.svg" } @@ -83,43 +74,11 @@ PageType { } } - ListView { + ListViewType { id: listView - property bool isFocusable: true - anchors.fill: parent - Keys.onTabPressed: { - FocusController.nextKeyTabItem() - } - - Keys.onBacktabPressed: { - FocusController.previousKeyTabItem() - } - - Keys.onUpPressed: { - FocusController.nextKeyUpItem() - } - - Keys.onDownPressed: { - FocusController.nextKeyDownItem() - } - - Keys.onLeftPressed: { - FocusController.nextKeyLeftItem() - } - - Keys.onRightPressed: { - FocusController.nextKeyRightItem() - } - - ScrollBar.vertical: ScrollBarType {} - - clip: true - reuseItems: true - snapMode: ListView.SnapToItem - model: labelsModel header: ColumnLayout { @@ -175,7 +134,7 @@ PageType { imageSource: objectImageSource leftText: title - rightText: ApiServicesModel.getSelectedServiceData(contentKey) + rightText: ApiAccountInfoModel.data(contentKey) visible: rightText !== "" } @@ -185,53 +144,61 @@ PageType { width: listView.width spacing: 0 - ParagraphTextType { - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - - onLinkActivated: function(link) { - Qt.openUrlExternally(link) - } - textFormat: Text.RichText - text: { - var text = ApiServicesModel.getSelectedServiceData("features") - if (text === undefined) { - return "" - } - return text.replace("%1", LanguageModel.getCurrentSiteUrl()) - } - - visible: text !== "" - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } - } - LabelWithButtonType { - id: supportUuid Layout.fillWidth: true + Layout.topMargin: 32 - text: qsTr("Support tag") - descriptionText: SettingsController.getInstallationUuid() + visible: false - descriptionOnTop: true - - rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: AmneziaStyle.color.paleGray + text: qsTr("Subscription key") + rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - GC.copyToClipBoard(descriptionText) - PageController.showNotificationMessage(qsTr("Copied")) - if (!GC.isMobile()) { - this.rightButton.forceActiveFocus() - } } } + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Configuration files") + + descriptionText: qsTr("To connect the router") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Support") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsApiSupport) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("How to connect on another devicey") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsApiInstructions) + } + } + + DividerType {} + BasicButtonType { id: resetButton Layout.alignment: Qt.AlignHCenter diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml new file mode 100644 index 00000000..fafd1c02 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + ColumnLayout { + id: backButtonLayout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + } + + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: "Support" + descriptionText: qsTr("Our technical support specialists are ready to help you at any time") + } + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Telegram") + descriptionText: qsTr("@amnezia_premium_support_bot") + rightImageSource: "qrc:/images/controls/external-link.svg" + + clickedFunction: function() { + Qt.openUrlExternally(qsTr("https://t.me/amnezia_premium_support_bot")) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Mail") + descriptionText: qsTr("support@amnezia.org") + rightImageSource: "qrc:/images/controls/external-link.svg" + + clickedFunction: function() { + Qt.openUrlExternally(qsTr("mailto:support@amnezia.org")) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Site") + descriptionText: qsTr("amnezia.org") + rightImageSource: "qrc:/images/controls/external-link.svg" + + clickedFunction: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) + } + } + + DividerType {} + + LabelWithButtonType { + id: supportUuid + Layout.fillWidth: true + + text: qsTr("Support tag") + descriptionText: SettingsController.getInstallationUuid() + + descriptionOnTop: true + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsServerData.qml b/client/ui/qml/Pages2/PageSettingsServerData.qml index cd736d39..977e669e 100644 --- a/client/ui/qml/Pages2/PageSettingsServerData.qml +++ b/client/ui/qml/Pages2/PageSettingsServerData.qml @@ -36,16 +36,6 @@ PageType { PageController.showErrorMessage(message) } - function onRemoveProcessedServerFinished(finishedMessage) { - if (!ServersModel.getServersCount()) { - PageController.goToPageHome() - } else { - PageController.goToStartPage() - PageController.goToPage(PageEnum.PageSettingsServersList) - } - PageController.showNotificationMessage(finishedMessage) - } - function onRebootProcessedServerFinished(finishedMessage) { PageController.showNotificationMessage(finishedMessage) } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index b13f4947..31d9f72a 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -95,12 +95,9 @@ PageType { ServersModel.processedIndex = index if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) + ApiSettingsController.getAccountInfo() - } else { - PageController.goToPage(PageEnum.PageSettingsApiServerInfo) - } + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } else { PageController.goToPage(PageEnum.PageSettingsServerInfo) } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 71009e28..b5a61e83 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -154,6 +154,16 @@ PageType { PageController.goToPageHome() PageController.showNotificationMessage(message) } + + function onRemoveProcessedServerFinished(finishedMessage) { + if (!ServersModel.getServersCount()) { + PageController.goToPageHome() + } else { + PageController.goToStartPage() + PageController.goToPage(PageEnum.PageSettingsServersList) + } + PageController.showNotificationMessage(finishedMessage) + } } Connections { From 42d3d9b98a8f633093f3e27c9ebab1cd6af2316d Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 7 Feb 2025 22:22:14 +0700 Subject: [PATCH 04/36] feature: added page for export api native configs --- client/CMakeLists.txt | 4 +- client/amnezia_application.cpp | 16 ++- client/amnezia_application.h | 3 +- client/cmake/sources.cmake | 1 - client/core/api/apiDefs.h | 32 ++++++ client/core/api/apiUtils.cpp | 38 ++++++- client/core/api/apiUtils.h | 8 +- client/core/controllers/apiController.cpp | 25 +---- client/core/controllers/apiController.h | 2 - client/core/enums/apiEnums.h | 9 -- client/resources.qrc | 1 + .../controllers/api/apiConfigsController.cpp | 106 ++++++++++++++++++ .../ui/controllers/api/apiConfigsController.h | 35 ++++++ .../controllers/api/apiSettingsController.cpp | 16 ++- .../controllers/api/apiSettingsController.h | 6 +- .../ui/controllers/api/importController.cpp | 2 - client/ui/controllers/api/importController.h | 24 ---- .../ui/controllers/connectionController.cpp | 8 +- client/ui/controllers/pageController.h | 1 + client/ui/models/api/apiAccountInfoModel.cpp | 45 +++++--- client/ui/models/api/apiAccountInfoModel.h | 16 +-- client/ui/models/servers_model.cpp | 6 +- client/ui/qml/Components/ServersListView.qml | 6 +- client/ui/qml/Pages2/PageHome.qml | 6 +- .../PageSettingsApiAvailableCountries.qml | 4 + .../Pages2/PageSettingsApiInstructions.qml | 66 ++++++++--- .../Pages2/PageSettingsApiNativeConfigs.qml | 87 ++++++++++++++ .../qml/Pages2/PageSettingsApiServerInfo.qml | 13 ++- .../ui/qml/Pages2/PageSettingsApiSupport.qml | 2 +- .../ui/qml/Pages2/PageSettingsServersList.qml | 2 + 30 files changed, 461 insertions(+), 129 deletions(-) create mode 100644 client/core/api/apiDefs.h delete mode 100644 client/core/enums/apiEnums.h create mode 100644 client/ui/controllers/api/apiConfigsController.cpp create mode 100644 client/ui/controllers/api/apiConfigsController.h delete mode 100644 client/ui/controllers/api/importController.cpp delete mode 100644 client/ui/controllers/api/importController.h create mode 100644 client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 294b9646..1ed3df08 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -57,7 +57,9 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) endif() qt_standard_project_setup() -qt_add_executable(${PROJECT} MANUAL_FINALIZATION) +qt_add_executable(${PROJECT} MANUAL_FINALIZATION + core/api/apiDefs.h +) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 2bd0b991..7e72defe 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -10,8 +12,6 @@ #include #include #include -#include -#include #include "logger.h" #include "ui/models/installedAppsModel.h" @@ -282,16 +282,17 @@ bool AmneziaApplication::parseCommands() } #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) -void AmneziaApplication::startLocalServer() { +void AmneziaApplication::startLocalServer() +{ const QString serverName("AmneziaVPNInstance"); QLocalServer::removeServer(serverName); - QLocalServer* server = new QLocalServer(this); + QLocalServer *server = new QLocalServer(this); server->listen(serverName); QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() { if (server) { - QLocalSocket* clientConnection = server->nextPendingConnection(); + QLocalSocket *clientConnection = server->nextPendingConnection(); clientConnection->deleteLater(); } emit m_pageController->raiseMainWindow(); @@ -467,6 +468,9 @@ void AmneziaApplication::initControllers() m_systemController.reset(new SystemController(m_settings)); m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); - m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_settings)); + m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_settings)); m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); + + m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_settings)); + m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); } diff --git a/client/amnezia_application.h b/client/amnezia_application.h index dcadb77b..3b9d06dd 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -25,7 +25,7 @@ #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" #include "ui/controllers/appSplitTunnelingController.h" -// #include "ui/controllers/api/importController.h" +#include "ui/controllers/api/apiConfigsController.h" #include "ui/controllers/api/apiSettingsController.h" #include "ui/models/containers_model.h" #include "ui/models/languageModel.h" @@ -139,6 +139,7 @@ private: QScopedPointer m_appSplitTunnelingController; QScopedPointer m_apiSettingsController; + QScopedPointer m_apiConfigsController; QNetworkAccessManager *m_nam; diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index ace83685..1d7c4080 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -23,7 +23,6 @@ set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/networkUtilities.h ${CLIENT_ROOT_DIR}/core/serialization/serialization.h ${CLIENT_ROOT_DIR}/core/serialization/transfer.h - ${CLIENT_ROOT_DIR}/core/enums/apiEnums.h ${CLIENT_ROOT_DIR}/../common/logger/logger.h ${CLIENT_ROOT_DIR}/utils/qmlUtils.h ${CLIENT_ROOT_DIR}/core/api/apiUtils.h diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h new file mode 100644 index 00000000..0431839c --- /dev/null +++ b/client/core/api/apiDefs.h @@ -0,0 +1,32 @@ +#ifndef APIDEFS_H +#define APIDEFS_H + +#include + +namespace apiDefs +{ + enum ConfigType { + AmneziaFreeV2 = 1, + AmneziaFreeV3, + AmneziaPremiumV1, + AmneziaPremiumV2, + SelfHosted + }; + + enum ConfigSource { + Telegram = 1, + AmneziaGateway + }; + + namespace key + { + constexpr QLatin1String configVersion("config_version"); + + constexpr QLatin1String apiConfig("api_config"); + constexpr QLatin1String stackType("stack_type"); + } + + const int requestTimeoutMsecs = 12 * 1000; // 12 secs +} + +#endif // APIDEFS_H diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp index eed34f65..d90ac1dc 100644 --- a/client/core/api/apiUtils.cpp +++ b/client/core/api/apiUtils.cpp @@ -1,10 +1,46 @@ #include "apiUtils.h" #include +#include -bool ApiUtils::isSubscriptionExpired(const QString &subscriptionEndDate) +bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate) { QDateTime now = QDateTime::currentDateTime(); QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs); return endDate < now; } + +bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject) +{ + auto configVersion = serverConfigObject.value(apiDefs::key::configVersion).toInt(); + switch (configVersion) { + case apiDefs::ConfigSource::Telegram: return true; + case apiDefs::ConfigSource::AmneziaGateway: return true; + default: return false; + } +} + +apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObject) +{ + auto configVersion = serverConfigObject.value(apiDefs::key::configVersion).toInt(); + switch (configVersion) { + case apiDefs::ConfigSource::Telegram: { + }; + case apiDefs::ConfigSource::AmneziaGateway: { + constexpr QLatin1String premium("prem"); + constexpr QLatin1String free("free"); + + auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); + auto stackType = apiConfigObject.value(apiDefs::key::stackType).toString(); + + if (stackType == premium) { + return apiDefs::ConfigType::AmneziaPremiumV2; + } else if (stackType == free) { + return apiDefs::ConfigType::AmneziaFreeV3; + } + } + default: { + return apiDefs::ConfigType::SelfHosted; + } + }; +} diff --git a/client/core/api/apiUtils.h b/client/core/api/apiUtils.h index dc863c22..2c9496bd 100644 --- a/client/core/api/apiUtils.h +++ b/client/core/api/apiUtils.h @@ -3,9 +3,15 @@ #include -namespace ApiUtils +#include "apiDefs.h" + +namespace apiUtils { + bool isServerFromApi(const QJsonObject &serverConfigObject); + bool isSubscriptionExpired(const QString &subscriptionEndDate); + + apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); } #endif // APIUTILS_H diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 4e43f148..34fb30a0 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -7,7 +7,7 @@ #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" -#include "core/enums/apiEnums.h" +#include "core/api/apiDefs.h" #include "gatewayController.h" #include "version.h" @@ -106,7 +106,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle serverConfig[config_key::containers] = newServerConfig.value(config_key::containers); serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName); - if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { + if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion); serverConfig[config_key::description] = newServerConfig.value(config_key::description); serverConfig[config_key::name] = newServerConfig.value(config_key::name); @@ -119,7 +119,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap()); auto apiConfig = QJsonObject::fromVariantMap(map); - if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { + if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject()); } @@ -274,23 +274,4 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co return errorCode; } -ErrorCode ApiController::getNativeConfig(const QString &userCountryCode, const QString &serviceType, const QString &protocol, - const QString &serverCountryCode, const QJsonObject &authData, QString &nativeConfig) -{ - GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); - ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); - - QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); - apiPayload[configKey::userCountryCode] = userCountryCode; - apiPayload[configKey::serverCountryCode] = serverCountryCode; - apiPayload[configKey::serviceType] = serviceType; - apiPayload[configKey::authData] = authData; - - QByteArray responseBody; - ErrorCode errorCode = gatewayController.post(QString("%1v1/country_config"), apiPayload, responseBody); - - nativeConfig = responseBody; - - return errorCode; -} diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index 9fb1f49d..ab34bde4 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -25,8 +25,6 @@ public slots: ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig); - ErrorCode getNativeConfig(const QString &userCountryCode, const QString &serviceType, const QString &protocol, - const QString &serverCountryCode, const QJsonObject &authData, QString &nativeConfig); signals: void errorOccurred(ErrorCode errorCode); diff --git a/client/core/enums/apiEnums.h b/client/core/enums/apiEnums.h deleted file mode 100644 index 1f050007..00000000 --- a/client/core/enums/apiEnums.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef APIENUMS_H -#define APIENUMS_H - -enum ApiConfigSources { - Telegram = 1, - AmneziaGateway -}; - -#endif // APIENUMS_H diff --git a/client/resources.qrc b/client/resources.qrc index 308accda..cd44ca60 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -228,6 +228,7 @@ ui/qml/Controls2/ListViewType.qml ui/qml/Pages2/PageSettingsApiSupport.qml ui/qml/Pages2/PageSettingsApiInstructions.qml + ui/qml/Pages2/PageSettingsApiNativeConfigs.qml images/flagKit/ZW.svg diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp new file mode 100644 index 00000000..62ccebb8 --- /dev/null +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -0,0 +1,106 @@ +#include "ApiConfigsController.h" + +#include "configurators/wireguard_configurator.h" +#include "core/api/apiDefs.h" +#include "core/controllers/gatewayController.h" +#include "version.h" +#include "ui/controllers/systemController.h" + +namespace +{ + namespace configKey + { + constexpr char cloak[] = "cloak"; + constexpr char awg[] = "awg"; + + constexpr char apiEdnpoint[] = "api_endpoint"; + constexpr char accessToken[] = "api_key"; + constexpr char certificate[] = "certificate"; + constexpr char publicKey[] = "public_key"; + + constexpr char uuid[] = "installation_uuid"; + constexpr char osVersion[] = "os_version"; + constexpr char appVersion[] = "app_version"; + + constexpr char userCountryCode[] = "user_country_code"; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceInfo[] = "service_info"; + constexpr char serviceProtocol[] = "service_protocol"; + + constexpr char aesKey[] = "aes_key"; + constexpr char aesIv[] = "aes_iv"; + constexpr char aesSalt[] = "aes_salt"; + + constexpr char apiPayload[] = "api_payload"; + constexpr char keyPayload[] = "key_payload"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; + + constexpr char config[] = "config"; + } +} + +ApiConfigsController::ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, + QObject *parent) + : QObject(parent), m_serversModel(serversModel), m_settings(settings) +{ +} + +void ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = apiConfigObject.value(configKey::serverCountryCode); + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + apiPayload[configKey::uuid] = m_settings->getInstallationUuid(true); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/native_config"), apiPayload, responseBody); + // // if (errorCode != ErrorCode::NoError) { + + // // } + + QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); + QString nativeConfig = jsonConfig.value(configKey::config).toString(); + + SystemController::saveFile(fileName, nativeConfig); +} + +ApiConfigsController::ApiPayloadData ApiConfigsController::generateApiPayloadData(const QString &protocol) +{ + ApiConfigsController::ApiPayloadData apiPayload; + if (protocol == configKey::cloak) { + apiPayload.certRequest = OpenVpnConfigurator::createCertRequest(); + } else if (protocol == configKey::awg) { + auto connData = WireguardConfigurator::genClientKeys(); + apiPayload.wireGuardClientPubKey = connData.clientPubKey; + apiPayload.wireGuardClientPrivKey = connData.clientPrivKey; + } + return apiPayload; +} + +QJsonObject ApiConfigsController::fillApiPayload(const QString &protocol, const ApiPayloadData &apiPayloadData) +{ + QJsonObject obj; + if (protocol == configKey::cloak) { + obj[configKey::certificate] = apiPayloadData.certRequest.request; + } else if (protocol == configKey::awg) { + obj[configKey::publicKey] = apiPayloadData.wireGuardClientPubKey; + } + + obj[configKey::osVersion] = QSysInfo::productType(); + obj[configKey::appVersion] = QString(APP_VERSION); + + return obj; +} diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h new file mode 100644 index 00000000..8f3249a7 --- /dev/null +++ b/client/ui/controllers/api/apiConfigsController.h @@ -0,0 +1,35 @@ +#ifndef APICONFIGSCONTROLLER_H +#define APICONFIGSCONTROLLER_H + +#include + +#include "configurators/openvpn_configurator.h" +#include "ui/models/servers_model.h" + +class ApiConfigsController : public QObject +{ + Q_OBJECT +public: + ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, + QObject *parent = nullptr); + +public slots: + void exportNativeConfig(const QString &serverCountryCode, const QString &fileName); + +private: + struct ApiPayloadData + { + OpenVpnConfigurator::ConnectionData certRequest; + + QString wireGuardClientPrivKey; + QString wireGuardClientPubKey; + }; + + ApiPayloadData generateApiPayloadData(const QString &protocol); + QJsonObject fillApiPayload(const QString &protocol, const ApiPayloadData &apiPayloadData); + + QSharedPointer m_serversModel; + std::shared_ptr m_settings; +}; + +#endif // APICONFIGSCONTROLLER_H diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index d1025432..00620d07 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -19,9 +19,14 @@ namespace } ApiSettingsController::ApiSettingsController(const QSharedPointer &serversModel, - const QSharedPointer &apiAccountInfoModel, const std::shared_ptr &settings, - QObject *parent) - : QObject(parent), m_serversModel(serversModel), m_apiAccountInfoModel(apiAccountInfoModel), m_settings(settings) + const QSharedPointer &apiAccountInfoModel, + const QSharedPointer &apiCountryModel, + const std::shared_ptr &settings, QObject *parent) + : QObject(parent), + m_serversModel(serversModel), + m_apiAccountInfoModel(apiAccountInfoModel), + m_apiCountryModel(apiCountryModel), + m_settings(settings) { } @@ -55,3 +60,8 @@ bool ApiSettingsController::getAccountInfo() return true; } + +void ApiSettingsController::updateApiCountryModel() +{ + m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); +} diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h index cad0e9a1..32aa1673 100644 --- a/client/ui/controllers/api/apiSettingsController.h +++ b/client/ui/controllers/api/apiSettingsController.h @@ -4,6 +4,7 @@ #include #include "ui/models/api/apiAccountInfoModel.h" +#include "ui/models/apiCountryModel.h" #include "ui/models/servers_model.h" class ApiSettingsController : public QObject @@ -11,15 +12,18 @@ class ApiSettingsController : public QObject Q_OBJECT public: ApiSettingsController(const QSharedPointer &serversModel, const QSharedPointer &apiAccountInfoModel, - const std::shared_ptr &settings, QObject *parent = nullptr); + const QSharedPointer &apiCountryModel, const std::shared_ptr &settings, + QObject *parent = nullptr); ~ApiSettingsController(); public slots: bool getAccountInfo(); + void updateApiCountryModel(); private: QSharedPointer m_serversModel; QSharedPointer m_apiAccountInfoModel; + QSharedPointer m_apiCountryModel; std::shared_ptr m_settings; }; diff --git a/client/ui/controllers/api/importController.cpp b/client/ui/controllers/api/importController.cpp deleted file mode 100644 index e352326f..00000000 --- a/client/ui/controllers/api/importController.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#include "importController.h" - diff --git a/client/ui/controllers/api/importController.h b/client/ui/controllers/api/importController.h deleted file mode 100644 index f4580360..00000000 --- a/client/ui/controllers/api/importController.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef IMPORTCONTROLLER_H -#define IMPORTCONTROLLER_H - -#include - -#include "ui/models/api/apiAccountInfoModel.h" -#include "ui/models/servers_model.h" - -// namespace api -// { -// class ImportController : public QObject -// { -// Q_OBJECT -// public: -// ImportController(const QSharedPointer &serversModel, QSharedPointer &accountInfoModel); -// ~ImportController(); - -// private: -// QSharedPointer m_serversModel; -// QSharedPointer m_accountInfoModel; -// }; -// } - -#endif // IMPORTCONTROLLER_H diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f9491d4e..9cd386c5 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -8,7 +8,7 @@ #include #include "core/controllers/vpnConfigurationController.h" -#include "core/enums/apiEnums.h" +#include "core/api/apiDefs.h" #include "version.h" ConnectionController::ConnectionController(const QSharedPointer &serversModel, @@ -48,15 +48,15 @@ void ConnectionController::openConnection() emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing); - if (configVersion == ApiConfigSources::Telegram + if (configVersion == apiDefs::ConfigSource::Telegram && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromTelegram(); - } else if (configVersion == ApiConfigSources::AmneziaGateway + } else if (configVersion == apiDefs::ConfigSource::AmneziaGateway && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { qDebug() << "attempt to update api config by expires_at event"; - if (configVersion == ApiConfigSources::Telegram) { + if (configVersion == apiDefs::ConfigSource::Telegram) { emit updateApiConfigFromTelegram(); } else { emit updateApiConfigFromGateway(); diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 6e1efda5..9ec93cc0 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -35,6 +35,7 @@ namespace PageLoader PageSettingsApiAvailableCountries, PageSettingsApiSupport, PageSettingsApiInstructions, + PageSettingsApiNativeConfigs, PageServiceSftpSettings, PageServiceTorWebsiteSettings, diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index 7679f0ff..36923491 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -12,9 +12,9 @@ namespace namespace configKey { constexpr char availableCountries[] = "available_countries"; - constexpr char serverCountryCode[] = "server_country_code"; - constexpr char serverCountryName[] = "server_country_name"; - constexpr char lastUpdated[] = "last_updated"; + // constexpr char serverCountryCode[] = "server_country_code"; + // constexpr char serverCountryName[] = "server_country_name"; + // constexpr char lastUpdated[] = "last_updated"; constexpr char activeDeviceCount[] = "active_device_count"; constexpr char maxDeviceCount[] = "max_device_count"; constexpr char subscriptionEndDate[] = "subscription_end_date"; @@ -38,7 +38,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const switch (role) { case SubscriptionStatusRole: { - return ApiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active"); + return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active"); } case EndDateRole: { return QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); @@ -46,9 +46,15 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const case ConnectedDevicesRole: { return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount); } - // case ServiceDescriptionRole: { - // return apiServiceData.serviceInfo.name; - // } + case ServiceDescriptionRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { + return tr("Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to 200 " + "Mb/s"); + } else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and " + "more. YouTube is not included in the free plan."); + } + } } return QVariant(); @@ -60,21 +66,23 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons AccountInfoData accountInfoData; - auto availableCountries = accountInfoObject.value(configKey::availableCountries).toArray(); - for (const auto &country : availableCountries) { - auto countryObject = country.toObject(); - CountryInfo countryInfo; - countryInfo.serverCountryCode = countryObject.value(configKey::serverCountryCode).toString(); - countryInfo.serverCountryName = countryObject.value(configKey::serverCountryName).toString(); - countryInfo.lastUpdated = countryObject.value(configKey::lastUpdated).toString(); + m_availableCountries = accountInfoObject.value(configKey::availableCountries).toArray(); + // for (const auto &country : availableCountries) { + // auto countryObject = country.toObject(); + // CountryInfo countryInfo; + // countryInfo.serverCountryCode = countryObject.value(configKey::serverCountryCode).toString(); + // countryInfo.serverCountryName = countryObject.value(configKey::serverCountryName).toString(); + // countryInfo.lastUpdated = countryObject.value(configKey::lastUpdated).toString(); - accountInfoData.AvailableCountries.push_back(countryInfo); - } + // accountInfoData.AvailableCountries.push_back(countryInfo); + // } accountInfoData.activeDeviceCount = accountInfoObject.value(configKey::activeDeviceCount).toInt(); accountInfoData.maxDeviceCount = accountInfoObject.value(configKey::maxDeviceCount).toInt(); accountInfoData.subscriptionEndDate = accountInfoObject.value(configKey::subscriptionEndDate).toString(); + accountInfoData.configType = apiUtils::getConfigType(serverConfig); + m_accountInfoData = accountInfoData; endResetModel(); @@ -93,6 +101,11 @@ QVariant ApiAccountInfoModel::data(const QString &roleString) return {}; } +QJsonArray ApiAccountInfoModel::getAvailableCountries() +{ + return m_availableCountries; +} + QHash ApiAccountInfoModel::roleNames() const { QHash roles; diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index f7cabb69..66d283da 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -5,6 +5,8 @@ #include #include +#include "core/api/apiDefs.h" + class ApiAccountInfoModel : public QAbstractListModel { Q_OBJECT @@ -27,29 +29,23 @@ public slots: void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig); QVariant data(const QString &roleString); + QJsonArray getAvailableCountries(); + protected: QHash roleNames() const override; private: - struct CountryInfo - { - QString serverCountryCode; - QString serverCountryName; - QString lastUpdated; - }; - struct AccountInfoData { QString subscriptionEndDate; int activeDeviceCount; int maxDeviceCount; - QString vpnKey; - - QVector AvailableCountries; + apiDefs::ConfigType configType; }; AccountInfoData m_accountInfoData; + QJsonArray m_availableCountries; }; #endif // APIACCOUNTINFOMODEL_H diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index b72b10c3..f77eb6fc 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -1,7 +1,7 @@ #include "servers_model.h" +#include "core/api/apiDefs.h" #include "core/controllers/serverController.h" -#include "core/enums/apiEnums.h" #include "core/networkUtilities.h" #ifdef Q_OS_IOS @@ -132,10 +132,10 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const return serverHasInstalledContainers(index.row()); } case IsServerFromTelegramApiRole: { - return server.value(config_key::configVersion).toInt() == ApiConfigSources::Telegram; + return server.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::Telegram; } case IsServerFromGatewayApiRole: { - return server.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway; + return server.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway; } case ApiConfigRole: { return apiConfig; diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index 870b83b7..845586ba 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -112,11 +112,13 @@ ListView { ServersModel.processedIndex = index if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - ApiSettingsController.getAccountInfo() - if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { + PageController.showBusyIndicator(true) + ApiSettingsController.getAccountInfo() + PageController.showBusyIndicator(false) + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } } else { diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 38a87e72..8b97efd1 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -299,11 +299,13 @@ PageType { ServersModel.processedIndex = ServersModel.defaultIndex if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - ApiSettingsController.getAccountInfo() - if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { + PageController.showBusyIndicator(true) + ApiSettingsController.getAccountInfo() + PageController.showBusyIndicator(false) + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } } else { diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index 853a44a3..9f00f9f4 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -92,6 +92,10 @@ PageType { descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") actionButtonFunction: function() { + PageController.showBusyIndicator(true) + ApiSettingsController.getAccountInfo() + PageController.showBusyIndicator(false) + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } } diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml index 9106808a..d1c8231e 100644 --- a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -20,31 +20,67 @@ PageType { id: windows readonly property string title: qsTr("Windows") - readonly property string imageSource: "qrc:/images/controls/external-link.svg" - readonly property var handler: function() { - Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) - } + readonly property string link: qsTr("") + } + + QtObject { + id: macos + + readonly property string title: qsTr("macOS") + readonly property string link: qsTr("") + } + + QtObject { + id: android + + readonly property string title: qsTr("Android") + readonly property string link: qsTr("") + } + + QtObject { + id: androidTv + + readonly property string title: qsTr("AndroidTV") + readonly property string link: qsTr("") + } + + QtObject { + id: ios + + readonly property string title: qsTr("iOS") + readonly property string link: qsTr("") } QtObject { id: linux - readonly property string title: qsTr("Windows") - readonly property string imageSource: "qrc:/images/controls/external-link.svg" - readonly property var handler: function() { - Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) - } + readonly property string title: qsTr("Linux") + readonly property string link: qsTr("") + } + + QtObject { + id: routers + + readonly property string title: qsTr("Routers") + readonly property string link: qsTr("") } property list instructionsModel: [ windows, - linux + macos, + android, + androidTv, + ios, + linux, + routers ] ListViewType { id: listView anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 24 model: instructionsModel @@ -62,8 +98,8 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: "Support" - descriptionText: qsTr("Our technical support specialists are ready to help you at any time") + headerText: qsTr("How to connect on another device") + descriptionText: qsTr("Instructions on the Amnezia website") } } @@ -76,9 +112,11 @@ PageType { Layout.topMargin: 6 text: title - leftImageSource: imageSource + rightImageSource: "qrc:/images/controls/external-link.svg" - clickedFunction: handler + clickedFunction: function() { + Qt.openUrlExternally(link) + } } DividerType {} diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml new file mode 100644 index 00000000..0cac82b8 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtCore + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property string configExtension: ".conf" + property string configCaption: qsTr("Save AmneziaVPN config") + + ListViewType { + id: listView + + anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 24 + + model: ApiCountryModel + + header: ColumnLayout { + width: listView.width + + BackButtonType { + id: backButton + } + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Configuration files") + descriptionText: qsTr("To connect a router or AmneziaWG application") + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + id: telegramButton + Layout.fillWidth: true + Layout.topMargin: 6 + + text: countryName + leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" + rightImageSource: "qrc:/images/controls/download.svg" + + clickedFunction: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = countryCode + configExtension + } else { + fileName = SystemController.getFileName(configCaption, + qsTr("Config files (*" + configExtension + ")"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode, + true, + configExtension) + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + ApiConfigsController.exportNativeConfig(countryCode, fileName) + PageController.showBusyIndicator(false) + } + } + } + + DividerType {} + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 350f2ea2..0e3e7261 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -105,7 +105,7 @@ PageType { actionButtonImage: "qrc:/images/controls/edit-3.svg" headerText: root.processedServer.name - descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") + descriptionText: ApiAccountInfoModel.data("serviceDescription") actionButtonFunction: function() { serverNameEditDrawer.openTriggered() @@ -145,6 +145,8 @@ PageType { spacing: 0 LabelWithButtonType { + id: vpnKey + Layout.fillWidth: true Layout.topMargin: 32 @@ -157,17 +159,22 @@ PageType { } } - DividerType {} + DividerType { + visible: false + } LabelWithButtonType { Layout.fillWidth: true + Layout.topMargin: vpnKey.visible ? 0 : 32 text: qsTr("Configuration files") - descriptionText: qsTr("To connect the router") + descriptionText: qsTr("To connect a router or AmneziaWG application") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { + ApiSettingsController.updateApiCountryModel() + PageController.goToPage(PageEnum.PageSettingsApiNativeConfigs) } } diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml index fafd1c02..e547359e 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSupport.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -37,7 +37,7 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: "Support" + headerText: qsTr("Support") descriptionText: qsTr("Our technical support specialists are ready to help you at any time") } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index 31d9f72a..bdd2366f 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -95,7 +95,9 @@ PageType { ServersModel.processedIndex = index if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + PageController.showBusyIndicator(true) ApiSettingsController.getAccountInfo() + PageController.showBusyIndicator(false) PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } else { From 07baf0ed65f771fbf308329b242a93135d5dca0f Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Mon, 10 Feb 2025 15:10:59 +0700 Subject: [PATCH 05/36] feature: added error handling and minor ui fixes --- client/core/controllers/apiController.cpp | 15 -------- client/core/controllers/apiController.h | 3 -- .../controllers/api/apiConfigsController.cpp | 15 ++++++-- .../ui/controllers/api/apiConfigsController.h | 7 +++- .../controllers/api/apiSettingsController.cpp | 12 ++++-- .../controllers/api/apiSettingsController.h | 3 ++ client/ui/models/api/apiAccountInfoModel.cpp | 37 +++++++++++++------ client/ui/models/api/apiAccountInfoModel.h | 4 +- client/ui/qml/Components/ServersListView.qml | 5 ++- client/ui/qml/Pages2/PageHome.qml | 5 ++- .../PageSettingsApiAvailableCountries.qml | 7 +++- .../Pages2/PageSettingsApiNativeConfigs.qml | 6 ++- .../qml/Pages2/PageSettingsApiServerInfo.qml | 16 ++++++-- .../ui/qml/Pages2/PageSettingsApiSupport.qml | 6 ++- .../ui/qml/Pages2/PageSettingsServersList.qml | 5 ++- client/ui/qml/Pages2/PageStart.qml | 8 ++++ 16 files changed, 101 insertions(+), 53 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 34fb30a0..fab91408 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -213,21 +213,6 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c } } -ErrorCode ApiController::getAccountInfo(const QString &userCountryCode, const QString &serviceType, const QJsonObject &authData, - QByteArray &responseBody) -{ - GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); - - QJsonObject apiPayload; - apiPayload[configKey::userCountryCode] = userCountryCode; - apiPayload[configKey::serviceType] = serviceType; - apiPayload[configKey::authData] = authData; - - ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody); - - return errorCode; -} - ErrorCode ApiController::getServicesList(QByteArray &responseBody) { GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index ab34bde4..367c89e4 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -18,9 +18,6 @@ public: public slots: void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); - - ErrorCode getAccountInfo(const QString &userCountryCode, const QString &serviceType, const QJsonObject &authData, - QByteArray &responseBody); ErrorCode getServicesList(QByteArray &responseBody); ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 62ccebb8..f791c7e8 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -48,8 +48,13 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer &s { } -void ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName) +bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName) { + if (fileName.isEmpty()) { + emit errorOccurred(ErrorCode::PermissionsError); + return false; + } + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); @@ -67,14 +72,16 @@ void ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, QByteArray responseBody; ErrorCode errorCode = gatewayController.post(QString("%1v1/native_config"), apiPayload, responseBody); - // // if (errorCode != ErrorCode::NoError) { - - // // } + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); QString nativeConfig = jsonConfig.value(configKey::config).toString(); SystemController::saveFile(fileName, nativeConfig); + return true; } ApiConfigsController::ApiPayloadData ApiConfigsController::generateApiPayloadData(const QString &protocol) diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index 8f3249a7..861bc176 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -11,10 +11,13 @@ class ApiConfigsController : public QObject Q_OBJECT public: ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, - QObject *parent = nullptr); + QObject *parent = nullptr); public slots: - void exportNativeConfig(const QString &serverCountryCode, const QString &fileName); + bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); + +signals: + void errorOccurred(ErrorCode errorCode); private: struct ApiPayloadData diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 00620d07..0bebf19e 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -1,5 +1,6 @@ #include "apiSettingsController.h" +#include "core/api/apiUtils.h" #include "core/controllers/gatewayController.h" namespace @@ -49,10 +50,13 @@ bool ApiSettingsController::getAccountInfo() apiPayload[configKey::authData] = authData; QByteArray responseBody; - ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody); - if (errorCode != ErrorCode::NoError) { - // emit errorOccured(errorCode); - return false; + + if (apiUtils::getConfigType(serverConfig) == apiDefs::ConfigType::AmneziaPremiumV2) { + ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } } QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object(); diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h index 32aa1673..ae9d9126 100644 --- a/client/ui/controllers/api/apiSettingsController.h +++ b/client/ui/controllers/api/apiSettingsController.h @@ -20,6 +20,9 @@ public slots: bool getAccountInfo(); void updateApiCountryModel(); +signals: + void errorOccurred(ErrorCode errorCode); + private: QSharedPointer m_serversModel; QSharedPointer m_apiAccountInfoModel; diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index 36923491..254097bc 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -12,9 +12,6 @@ namespace namespace configKey { constexpr char availableCountries[] = "available_countries"; - // constexpr char serverCountryCode[] = "server_country_code"; - // constexpr char serverCountryName[] = "server_country_name"; - // constexpr char lastUpdated[] = "last_updated"; constexpr char activeDeviceCount[] = "active_device_count"; constexpr char maxDeviceCount[] = "max_device_count"; constexpr char subscriptionEndDate[] = "subscription_end_date"; @@ -38,12 +35,23 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const switch (role) { case SubscriptionStatusRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return tr("Active"); + } + return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active"); } case EndDateRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return ""; + } + return QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); } case ConnectedDevicesRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return ""; + } return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount); } case ServiceDescriptionRole: { @@ -55,6 +63,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const "more. YouTube is not included in the free plan."); } } + case IsComponentVisibleRole: { + return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2; + } } return QVariant(); @@ -67,15 +78,6 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons AccountInfoData accountInfoData; m_availableCountries = accountInfoObject.value(configKey::availableCountries).toArray(); - // for (const auto &country : availableCountries) { - // auto countryObject = country.toObject(); - // CountryInfo countryInfo; - // countryInfo.serverCountryCode = countryObject.value(configKey::serverCountryCode).toString(); - // countryInfo.serverCountryName = countryObject.value(configKey::serverCountryName).toString(); - // countryInfo.lastUpdated = countryObject.value(configKey::lastUpdated).toString(); - - // accountInfoData.AvailableCountries.push_back(countryInfo); - // } accountInfoData.activeDeviceCount = accountInfoObject.value(configKey::activeDeviceCount).toInt(); accountInfoData.maxDeviceCount = accountInfoObject.value(configKey::maxDeviceCount).toInt(); @@ -106,6 +108,16 @@ QJsonArray ApiAccountInfoModel::getAvailableCountries() return m_availableCountries; } +QString ApiAccountInfoModel::getTelegramBotLink() +{ + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return tr("amnezia_free_support_bot"); + } else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { + return tr("amnezia_premium_support_bot"); + } + return ""; +} + QHash ApiAccountInfoModel::roleNames() const { QHash roles; @@ -113,6 +125,7 @@ QHash ApiAccountInfoModel::roleNames() const roles[EndDateRole] = "endDate"; roles[ConnectedDevicesRole] = "connectedDevices"; roles[ServiceDescriptionRole] = "serviceDescription"; + roles[IsComponentVisibleRole] = "isComponentVisible"; return roles; } diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index 66d283da..9dfdc508 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -16,7 +16,8 @@ public: SubscriptionStatusRole = Qt::UserRole + 1, ConnectedDevicesRole, ServiceDescriptionRole, - EndDateRole + EndDateRole, + IsComponentVisibleRole }; explicit ApiAccountInfoModel(QObject *parent = nullptr); @@ -30,6 +31,7 @@ public slots: QVariant data(const QString &roleString); QJsonArray getAvailableCountries(); + QString getTelegramBotLink(); protected: QHash roleNames() const override; diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index 845586ba..3a6792ac 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -116,8 +116,11 @@ ListView { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { PageController.showBusyIndicator(true) - ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo() PageController.showBusyIndicator(false) + if (!result) { + return + } PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8b97efd1..d189044d 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -303,8 +303,11 @@ PageType { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { PageController.showBusyIndicator(true) - ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo() PageController.showBusyIndicator(false) + if (!result) { + return + } PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index 9f00f9f4..41735b14 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -89,12 +89,15 @@ PageType { actionButtonImage: "qrc:/images/controls/settings.svg" headerText: root.processedServer.name - descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") + descriptionText: qsTr("Locations for connection") actionButtonFunction: function() { PageController.showBusyIndicator(true) - ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo() PageController.showBusyIndicator(false) + if (!result) { + return + } PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index 0cac82b8..4e897e44 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -75,8 +75,12 @@ PageType { } if (fileName !== "") { PageController.showBusyIndicator(true) - ApiConfigsController.exportNativeConfig(countryCode, fileName) + let result = ApiConfigsController.exportNativeConfig(countryCode, fileName) PageController.showBusyIndicator(false) + + if (result) { + PageController.showNotificationMessage(qsTr("Config file saved")) + } } } } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 0e3e7261..659df168 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -141,16 +141,20 @@ PageType { } footer: ColumnLayout { + id: footer + width: listView.width spacing: 0 + readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible") + LabelWithButtonType { id: vpnKey Layout.fillWidth: true Layout.topMargin: 32 - visible: false + visible: footer.isVisibleForAmneziaFree text: qsTr("Subscription key") rightImageSource: "qrc:/images/controls/chevron-right.svg" @@ -160,12 +164,13 @@ PageType { } DividerType { - visible: false + visible: footer.isVisibleForAmneziaFree } LabelWithButtonType { Layout.fillWidth: true - Layout.topMargin: vpnKey.visible ? 0 : 32 + + visible: footer.isVisibleForAmneziaFree text: qsTr("Configuration files") @@ -178,10 +183,13 @@ PageType { } } - DividerType {} + DividerType { + visible: footer.isVisibleForAmneziaFree + } LabelWithButtonType { Layout.fillWidth: true + Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32 text: qsTr("Support") rightImageSource: "qrc:/images/controls/chevron-right.svg" diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml index e547359e..64921a61 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSupport.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -44,12 +44,14 @@ PageType { LabelWithButtonType { Layout.fillWidth: true + readonly property string telegramBotLink: ApiAccountInfoModel.getTelegramBotLink() + text: qsTr("Telegram") - descriptionText: qsTr("@amnezia_premium_support_bot") + descriptionText: "@" + telegramBotLink rightImageSource: "qrc:/images/controls/external-link.svg" clickedFunction: function() { - Qt.openUrlExternally(qsTr("https://t.me/amnezia_premium_support_bot")) + Qt.openUrlExternally("https://t.me/" + telegramBotLink) } } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index bdd2366f..fcfcd114 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -96,8 +96,11 @@ PageType { if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { PageController.showBusyIndicator(true) - ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo() PageController.showBusyIndicator(false) + if (!result) { + return + } PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } else { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index b5a61e83..6081bbc8 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -224,6 +224,14 @@ PageType { } } + Connections { + target: ApiSettingsController + + function onErrorOccurred(error) { + PageController.showErrorMessage(error) + } + } + StackViewType { id: tabBarStackView objectName: "tabBarStackView" From db3164223a037d7bd4da7864f3d09eeabfd3fa5c Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 12 Feb 2025 12:43:11 +0700 Subject: [PATCH 06/36] 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 1ed3df08..24edfb07 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 0431839c..2df4c833 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 c0db2e12..4ea0e613 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 00000000..02f43ff0 --- /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 00000000..f5f207c1 --- /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 f791c7e8..0858ecff 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 861bc176..f9bf977f 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -13,11 +13,17 @@ public: 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 @@ private: 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 8681406e..516d1f4d 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 a2c9fcfa..5fb3e6b3 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -50,9 +50,6 @@ signals: 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 28bbc9f6..c79b3288 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 f98944f0..9a9f1aa1 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 659df168..303ba36c 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 + } } From 915c8f46c54f127157ce80c030863f650b9853aa Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Sat, 8 Feb 2025 16:34:04 +0100 Subject: [PATCH 07/36] add timeouts in ipc client init --- client/core/ipcclient.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index b44da1bf..8a316df9 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -1,5 +1,8 @@ #include "ipcclient.h" #include +#include +#include + IpcClient *IpcClient::m_instance = nullptr; @@ -44,6 +47,12 @@ bool IpcClient::init(IpcClient *instance) Instance()->m_ClientNode.addClientSideConnection(Instance()->m_localSocket.data()); Instance()->m_ipcClient.reset(Instance()->m_ClientNode.acquire()); + std::this_thread::sleep_for(std::chrono::seconds(2)); //< wait until client is ready + + if (!Instance()->m_ipcClient) { + qFatal() << "IpcClient is not ready!"; + } + Instance()->m_ipcClient->waitForSource(1000); if (!Instance()->m_ipcClient->isReplicaValid()) { @@ -51,6 +60,12 @@ bool IpcClient::init(IpcClient *instance) } Instance()->m_Tun2SocksClient.reset(Instance()->m_ClientNode.acquire()); + std::this_thread::sleep_for(std::chrono::seconds(5)); //< wait until client is ready + + if (!Instance()->m_Tun2SocksClient) { + qFatal() << "IpcClient::m_Tun2SocksClient is not ready!"; + } + Instance()->m_Tun2SocksClient->waitForSource(1000); if (!Instance()->m_Tun2SocksClient->isReplicaValid()) { From 9398e0e6950e635a6737ac8091b73e0b37a8f681 Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Sat, 8 Feb 2025 20:30:29 +0100 Subject: [PATCH 08/36] apply timeouts only for Windows --- client/core/ipcclient.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index 8a316df9..cfd6af12 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -47,7 +47,9 @@ bool IpcClient::init(IpcClient *instance) Instance()->m_ClientNode.addClientSideConnection(Instance()->m_localSocket.data()); Instance()->m_ipcClient.reset(Instance()->m_ClientNode.acquire()); +#ifdef Q_OS_WIN std::this_thread::sleep_for(std::chrono::seconds(2)); //< wait until client is ready +#endif if (!Instance()->m_ipcClient) { qFatal() << "IpcClient is not ready!"; @@ -60,8 +62,11 @@ bool IpcClient::init(IpcClient *instance) } Instance()->m_Tun2SocksClient.reset(Instance()->m_ClientNode.acquire()); + +#ifdef Q_OS_WIN std::this_thread::sleep_for(std::chrono::seconds(5)); //< wait until client is ready - +#endif + if (!Instance()->m_Tun2SocksClient) { qFatal() << "IpcClient::m_Tun2SocksClient is not ready!"; } From eb83086d5cd16a423c2c3abde9ceafb4a20ab2fc Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Sat, 8 Feb 2025 20:30:54 +0100 Subject: [PATCH 09/36] apply format to file --- client/core/ipcclient.cpp | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index cfd6af12..7b13ba07 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -3,17 +3,16 @@ #include #include - IpcClient *IpcClient::m_instance = nullptr; IpcClient::IpcClient(QObject *parent) : QObject(parent) { - } IpcClient::~IpcClient() { - if (m_localSocket) m_localSocket->close(); + if (m_localSocket) + m_localSocket->close(); } bool IpcClient::isSocketConnected() const @@ -28,13 +27,15 @@ IpcClient *IpcClient::Instance() QSharedPointer IpcClient::Interface() { - if (!Instance()) return nullptr; + if (!Instance()) + return nullptr; return Instance()->m_ipcClient; } QSharedPointer IpcClient::InterfaceTun2Socks() { - if (!Instance()) return nullptr; + if (!Instance()) + return nullptr; return Instance()->m_Tun2SocksClient; } @@ -78,9 +79,8 @@ bool IpcClient::init(IpcClient *instance) } }); - connect(Instance()->m_localSocket, &QLocalSocket::disconnected, [instance](){ - instance->m_isSocketConnected = false; - }); + connect(Instance()->m_localSocket, &QLocalSocket::disconnected, + [instance]() { instance->m_isSocketConnected = false; }); Instance()->m_localSocket->connectToServer(amnezia::getIpcServiceUrl()); Instance()->m_localSocket->waitForConnected(); @@ -97,7 +97,7 @@ bool IpcClient::init(IpcClient *instance) QSharedPointer IpcClient::CreatePrivilegedProcess() { - if (! Instance()->m_ipcClient || ! Instance()->m_ipcClient->isReplicaValid()) { + if (!Instance()->m_ipcClient || !Instance()->m_ipcClient->isReplicaValid()) { qWarning() << "IpcClient::createPrivilegedProcess : IpcClient IpcClient replica is not valid"; return nullptr; } @@ -120,18 +120,15 @@ QSharedPointer IpcClient::CreatePrivilegedProcess() pd->ipcProcess.reset(priv); if (!pd->ipcProcess) { qWarning() << "Acquire PrivilegedProcess failed"; - } - else { + } else { pd->ipcProcess->waitForSource(1000); if (!pd->ipcProcess->isReplicaValid()) { qWarning() << "PrivilegedProcess replica is not connected!"; } - QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(), [pd](){ - pd->replicaNode->deleteLater(); - }); + QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(), + [pd]() { pd->replicaNode->deleteLater(); }); } - }); pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid)); pd->localSocket->waitForConnected(); @@ -139,5 +136,3 @@ QSharedPointer IpcClient::CreatePrivilegedProcess() auto processReplica = QSharedPointer(pd->ipcProcess); return processReplica; } - - From e9250afd2b908229749063e495c171b6d422a833 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 15 Feb 2025 11:50:42 +0700 Subject: [PATCH 10/36] refactoring: simplified the validity check of the config before connection - improved project structure --- client/CMakeLists.txt | 5 +- client/amnezia_application.cpp | 290 +-------------- client/amnezia_application.h | 97 +---- client/cmake/sources.cmake | 7 +- client/core/api/apiDefs.h | 2 +- client/core/api/apiUtils.cpp | 5 + client/core/api/apiUtils.h | 1 + client/core/controllers/apiController.cpp | 262 -------------- client/core/controllers/apiController.h | 50 --- client/core/controllers/coreController.cpp | 332 ++++++++++++++++++ client/core/controllers/coreController.h | 133 +++++++ .../vpnConfigurationController.cpp | 6 +- .../controllers/vpnConfigurationController.h | 5 +- client/core/defs.h | 2 + client/core/errorstrings.cpp | 2 + .../controllers/api/apiConfigsController.cpp | 294 +++++++++++++++- .../ui/controllers/api/apiConfigsController.h | 22 +- .../ui/controllers/connectionController.cpp | 136 +------ client/ui/controllers/connectionController.h | 12 +- client/ui/controllers/installController.cpp | 148 +++----- client/ui/controllers/installController.h | 15 +- .../ui/models/{ => api}/apiServicesModel.cpp | 0 client/ui/models/{ => api}/apiServicesModel.h | 0 client/ui/models/servers_model.cpp | 2 +- client/ui/models/servers_model.h | 2 +- .../PageSettingsApiAvailableCountries.qml | 2 +- .../qml/Pages2/PageSettingsApiServerInfo.qml | 2 +- .../Pages2/PageSetupWizardApiServiceInfo.qml | 2 +- .../Pages2/PageSetupWizardConfigSource.qml | 2 +- client/ui/qml/Pages2/PageShare.qml | 2 +- client/ui/qml/Pages2/PageStart.qml | 70 ++-- 31 files changed, 941 insertions(+), 969 deletions(-) delete mode 100644 client/core/controllers/apiController.cpp delete mode 100644 client/core/controllers/apiController.h create mode 100644 client/core/controllers/coreController.cpp create mode 100644 client/core/controllers/coreController.h rename client/ui/models/{ => api}/apiServicesModel.cpp (100%) rename client/ui/models/{ => api}/apiServicesModel.h (100%) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 24edfb07..294b9646 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -57,10 +57,7 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) endif() qt_standard_project_setup() -qt_add_executable(${PROJECT} MANUAL_FINALIZATION - core/api/apiDefs.h - core/qrCodeUtils.h core/qrCodeUtils.cpp -) +qt_add_executable(${PROJECT} MANUAL_FINALIZATION) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 7e72defe..8dea8c0a 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -14,22 +14,14 @@ #include #include "logger.h" +#include "ui/controllers/pageController.h" #include "ui/models/installedAppsModel.h" #include "version.h" #include "platforms/ios/QRCodeReaderBase.h" -#if defined(Q_OS_ANDROID) - #include "core/installedAppsImageProvider.h" - #include "platforms/android/android_controller.h" -#endif #include "protocols/qml_register_protocols.h" -#if defined(Q_OS_IOS) - #include "platforms/ios/ios_controller.h" - #include -#endif - AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) { setQuitOnLastWindowClosed(false); @@ -84,79 +76,12 @@ void AmneziaApplication::init() m_vpnConnection->moveToThread(&m_vpnConnectionThread); m_vpnConnectionThread.start(); - initModels(); - loadTranslator(); - initControllers(); - -#ifdef Q_OS_ANDROID - if (!AndroidController::initLogging()) { - qFatal("Android logging initialization failed"); - } - AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); - connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); - - AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled()); - connect(m_settings.get(), &Settings::screenshotsEnabledChanged, AndroidController::instance(), &AndroidController::setScreenshotsEnabled); - - connect(m_settings.get(), &Settings::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer); - - connect(m_settings.get(), &Settings::settingsCleared, []() { AndroidController::instance()->resetLastServer(-1); }); - - connect(AndroidController::instance(), &AndroidController::initConnectionState, this, [this](Vpn::ConnectionState state) { - m_connectionController->onConnectionStateChanged(state); - if (m_vpnConnection) - m_vpnConnection->restoreConnection(); - }); - if (!AndroidController::instance()->initialize()) { - qFatal("Android controller initialization failed"); - } - - connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) { - emit m_pageController->goToPageHome(); - m_importController->extractConfigFromData(data); - data.clear(); - emit m_pageController->goToPageViewConfig(); - }); - - m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); -#endif - -#ifdef Q_OS_IOS - IosController::Instance()->initialize(); - connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { - emit m_pageController->goToPageHome(); - m_importController->extractConfigFromData(data); - emit m_pageController->goToPageViewConfig(); - }); - - connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) { - emit m_pageController->goToPageHome(); - m_pageController->goToPageSettingsBackup(); - emit m_settingsController->importBackupFromOutside(filePath); - }); - - QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); - - connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); }); -#endif - -#ifndef Q_OS_ANDROID - m_notificationHandler.reset(NotificationHandler::create(nullptr)); - - connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), - &NotificationHandler::setConnectionState); - - connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), &PageController::raiseMainWindow); - connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(), - static_cast(&ConnectionController::openConnection)); - connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), - &ConnectionController::closeConnection); - connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); -#endif + m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine)); m_engine->addImportPath("qrc:/ui/qml/Modules/"); m_engine->load(url); - m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); + + m_coreController->setQmlRoot(); bool enabled = m_settings->isSaveLogs(); #ifndef Q_OS_ANDROID @@ -168,13 +93,13 @@ void AmneziaApplication::init() #endif Logger::setServiceLogsEnabled(enabled); -#ifdef Q_OS_WIN +#ifdef Q_OS_WIN //TODO if (m_parser.isSet("a")) - m_pageController->showOnStartup(); + m_coreController->pageController()->showOnStartup(); else - emit m_pageController->raiseMainWindow(); + emit m_coreController->pageController()->raiseMainWindow(); #else - m_pageController->showOnStartup(); + m_coreController->pageController()->showOnStartup(); #endif // Android TextArea clipboard workaround @@ -231,33 +156,6 @@ void AmneziaApplication::loadFonts() QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf"); } -void AmneziaApplication::loadTranslator() -{ - auto locale = m_settings->getAppLanguage(); - m_translator.reset(new QTranslator()); - updateTranslator(locale); -} - -void AmneziaApplication::updateTranslator(const QLocale &locale) -{ - if (!m_translator->isEmpty()) { - QCoreApplication::removeTranslator(m_translator.get()); - } - - QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm"; - if (m_translator->load(strFileName)) { - if (QCoreApplication::installTranslator(m_translator.get())) { - m_settings->setAppLanguage(locale); - } - } else { - m_settings->setAppLanguage(QLocale::English); - } - - m_engine->retranslate(); - - emit translationsUpdated(); -} - bool AmneziaApplication::parseCommands() { m_parser.setApplicationDescription(APPLICATION_NAME); @@ -295,7 +193,7 @@ void AmneziaApplication::startLocalServer() QLocalSocket *clientConnection = server->nextPendingConnection(); clientConnection->deleteLater(); } - emit m_pageController->raiseMainWindow(); + emit m_coreController->pageController()->raiseMainWindow(); //TODO }); } #endif @@ -304,173 +202,3 @@ QQmlApplicationEngine *AmneziaApplication::qmlEngine() const { return m_engine; } - -void AmneziaApplication::initModels() -{ - m_containersModel.reset(new ContainersModel(this)); - m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); - - m_defaultServerContainersModel.reset(new ContainersModel(this)); - m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get()); - - m_serversModel.reset(new ServersModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); - connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); - connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), - &ContainersModel::updateModel); - m_serversModel->resetModel(); - - m_languageModel.reset(new LanguageModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); - connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &AmneziaApplication::updateTranslator); - connect(this, &AmneziaApplication::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated); - - m_sitesModel.reset(new SitesModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); - - m_appSplitTunnelingModel.reset(new AppSplitTunnelingModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("AppSplitTunnelingModel", m_appSplitTunnelingModel.get()); - - m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); - - m_openVpnConfigModel.reset(new OpenVpnConfigModel(this)); - m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get()); - - m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this)); - m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get()); - - m_cloakConfigModel.reset(new CloakConfigModel(this)); - m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get()); - - m_wireGuardConfigModel.reset(new WireGuardConfigModel(this)); - m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get()); - - m_awgConfigModel.reset(new AwgConfigModel(this)); - m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get()); - - m_xrayConfigModel.reset(new XrayConfigModel(this)); - m_engine->rootContext()->setContextProperty("XrayConfigModel", m_xrayConfigModel.get()); - -#ifdef Q_OS_WINDOWS - m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this)); - m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get()); -#endif - - m_sftpConfigModel.reset(new SftpConfigModel(this)); - m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); - - m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this)); - m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get()); - - m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); - connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(), - &ServersModel::clearCachedProfile); - - m_apiServicesModel.reset(new ApiServicesModel(this)); - m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get()); - - m_apiCountryModel.reset(new ApiCountryModel(this)); - m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get()); - connect(m_serversModel.get(), &ServersModel::updateApiLanguageModel, this, [this]() { - m_apiCountryModel->updateModel(m_serversModel->getProcessedServerData("apiAvailableCountries").toJsonArray(), - m_serversModel->getProcessedServerData("apiServerCountryCode").toString()); - }); - connect(m_serversModel.get(), &ServersModel::updateApiServicesModel, this, - [this]() { m_apiServicesModel->updateModel(m_serversModel->getProcessedServerData("apiConfig").toJsonObject()); }); - - m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this)); - m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get()); -} - -void AmneziaApplication::initControllers() -{ - m_connectionController.reset( - new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings)); - m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); - - connect(m_connectionController.get(), qOverload(&ConnectionController::connectionErrorOccurred), this, - [this](const QString &errorMessage) { - emit m_pageController->showErrorMessage(errorMessage); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - }); - - connect(m_connectionController.get(), qOverload(&ConnectionController::connectionErrorOccurred), this, - [this](ErrorCode errorCode) { - emit m_pageController->showErrorMessage(errorCode); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - }); - - connect(m_connectionController.get(), &ConnectionController::connectButtonClicked, m_connectionController.get(), - &ConnectionController::toggleConnection, Qt::QueuedConnection); - - m_pageController.reset(new PageController(m_serversModel, m_settings)); - m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); - - m_focusController.reset(new FocusController(m_engine, this)); - m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get()); - - m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, - m_apiServicesModel, m_settings)); - m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); - connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), - &PageController::showPassphraseRequestDrawer); - connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(), - &InstallController::setEncryptedPassphrase); - connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(), - &ConnectionController::onCurrentContainerUpdated); - - connect(m_installController.get(), &InstallController::updateServerFromApiFinished, this, [this]() { - disconnect(m_reloadConfigErrorOccurredConnection); - emit m_connectionController->configFromApiUpdated(); - }); - - connect(m_connectionController.get(), &ConnectionController::updateApiConfigFromGateway, this, [this]() { - m_reloadConfigErrorOccurredConnection = connect( - m_installController.get(), qOverload(&InstallController::installationErrorOccurred), this, - [this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); }, - static_cast(Qt::AutoConnection || Qt::SingleShotConnection)); - m_installController->updateServiceFromApi(m_serversModel->getDefaultServerIndex(), "", ""); - }); - - connect(m_connectionController.get(), &ConnectionController::updateApiConfigFromTelegram, this, [this]() { - m_reloadConfigErrorOccurredConnection = connect( - m_installController.get(), qOverload(&InstallController::installationErrorOccurred), this, - [this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); }, - static_cast(Qt::AutoConnection || Qt::SingleShotConnection)); - m_serversModel->removeApiConfig(m_serversModel->getDefaultServerIndex()); - m_installController->updateServiceFromTelegram(m_serversModel->getDefaultServerIndex()); - }); - - connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(), &ConnectionController::onTranslationsUpdated); - - m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); - m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); - - m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, m_settings)); - m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); - - m_settingsController.reset( - new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); - m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); - if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { - QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); - } - connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled, m_serversModel.get(), &ServersModel::toggleAmneziaDns); - - m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); - m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); - - m_appSplitTunnelingController.reset(new AppSplitTunnelingController(m_settings, m_appSplitTunnelingModel)); - m_engine->rootContext()->setContextProperty("AppSplitTunnelingController", m_appSplitTunnelingController.get()); - - m_systemController.reset(new SystemController(m_settings)); - m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); - - m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_settings)); - m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); - - m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_settings)); - m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); -} diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 3b9d06dd..b967f160 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -12,46 +12,10 @@ #include #endif +#include "core/controllers/coreController.h" #include "settings.h" #include "vpnconnection.h" -#include "ui/controllers/connectionController.h" -#include "ui/controllers/exportController.h" -#include "ui/controllers/importController.h" -#include "ui/controllers/installController.h" -#include "ui/controllers/focusController.h" -#include "ui/controllers/pageController.h" -#include "ui/controllers/settingsController.h" -#include "ui/controllers/sitesController.h" -#include "ui/controllers/systemController.h" -#include "ui/controllers/appSplitTunnelingController.h" -#include "ui/controllers/api/apiConfigsController.h" -#include "ui/controllers/api/apiSettingsController.h" -#include "ui/models/containers_model.h" -#include "ui/models/languageModel.h" -#include "ui/models/protocols/cloakConfigModel.h" -#ifndef Q_OS_ANDROID - #include "ui/notificationhandler.h" -#endif -#ifdef Q_OS_WINDOWS - #include "ui/models/protocols/ikev2ConfigModel.h" -#endif -#include "ui/models/protocols/awgConfigModel.h" -#include "ui/models/protocols/openvpnConfigModel.h" -#include "ui/models/protocols/shadowsocksConfigModel.h" -#include "ui/models/protocols/wireguardConfigModel.h" -#include "ui/models/protocols/xrayConfigModel.h" -#include "ui/models/protocols_model.h" -#include "ui/models/servers_model.h" -#include "ui/models/services/sftpConfigModel.h" -#include "ui/models/services/socks5ProxyConfigModel.h" -#include "ui/models/sites_model.h" -#include "ui/models/clientManagementModel.h" -#include "ui/models/appSplitTunnelingModel.h" -#include "ui/models/apiServicesModel.h" -#include "ui/models/apiCountryModel.h" -#include "ui/models/api/apiAccountInfoModel.h" - #define amnApp (static_cast(QCoreApplication::instance())) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) @@ -70,8 +34,6 @@ public: void init(); void registerTypes(); void loadFonts(); - void loadTranslator(); - void updateTranslator(const QLocale &locale); bool parseCommands(); #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) @@ -79,71 +41,26 @@ public: #endif QQmlApplicationEngine *qmlEngine() const; - QNetworkAccessManager *manager() { return m_nam; } - -signals: - void translationsUpdated(); + QNetworkAccessManager *manager() + { + return m_nam; + } private: - void initModels(); - void initControllers(); - QQmlApplicationEngine *m_engine {}; std::shared_ptr m_settings; + QScopedPointer m_coreController; + QSharedPointer m_containerProps; QSharedPointer m_protocolProps; - QSharedPointer m_translator; QCommandLineParser m_parser; - QSharedPointer m_containersModel; - QSharedPointer m_defaultServerContainersModel; - QSharedPointer m_serversModel; - QSharedPointer m_languageModel; - QSharedPointer m_protocolsModel; - QSharedPointer m_sitesModel; - QSharedPointer m_appSplitTunnelingModel; - QSharedPointer m_clientManagementModel; - QSharedPointer m_apiServicesModel; - QSharedPointer m_apiCountryModel; - QSharedPointer m_apiAccountInfoModel; - - QScopedPointer m_openVpnConfigModel; - QScopedPointer m_shadowSocksConfigModel; - QScopedPointer m_cloakConfigModel; - QScopedPointer m_xrayConfigModel; - QScopedPointer m_wireGuardConfigModel; - QScopedPointer m_awgConfigModel; -#ifdef Q_OS_WINDOWS - QScopedPointer m_ikev2ConfigModel; -#endif - QScopedPointer m_sftpConfigModel; - QScopedPointer m_socks5ConfigModel; - QSharedPointer m_vpnConnection; QThread m_vpnConnectionThread; -#ifndef Q_OS_ANDROID - QScopedPointer m_notificationHandler; -#endif - - QScopedPointer m_connectionController; - QScopedPointer m_focusController; - QScopedPointer m_pageController; - QScopedPointer m_installController; - QScopedPointer m_importController; - QScopedPointer m_exportController; - QScopedPointer m_settingsController; - QScopedPointer m_sitesController; - QScopedPointer m_systemController; - QScopedPointer m_appSplitTunnelingController; - - QScopedPointer m_apiSettingsController; - QScopedPointer m_apiConfigsController; QNetworkAccessManager *m_nam; - - QMetaObject::Connection m_reloadConfigErrorOccurredConnection; }; #endif // AMNEZIA_APPLICATION_H diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 1d7c4080..c3af531a 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -9,7 +9,9 @@ set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/errorstrings.h ${CLIENT_ROOT_DIR}/core/scripts_registry.h ${CLIENT_ROOT_DIR}/core/server_defs.h - ${CLIENT_ROOT_DIR}/core/controllers/apiController.h + ${CLIENT_ROOT_DIR}/core/api/apiDefs.h + ${CLIENT_ROOT_DIR}/core/qrCodeUtils.h + ${CLIENT_ROOT_DIR}/core/controllers/coreController.h ${CLIENT_ROOT_DIR}/core/controllers/gatewayController.h ${CLIENT_ROOT_DIR}/core/controllers/serverController.h ${CLIENT_ROOT_DIR}/core/controllers/vpnConfigurationController.h @@ -56,7 +58,8 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/errorstrings.cpp ${CLIENT_ROOT_DIR}/core/scripts_registry.cpp ${CLIENT_ROOT_DIR}/core/server_defs.cpp - ${CLIENT_ROOT_DIR}/core/controllers/apiController.cpp + ${CLIENT_ROOT_DIR}/core/qrCodeUtils.cpp + ${CLIENT_ROOT_DIR}/core/controllers/coreController.cpp ${CLIENT_ROOT_DIR}/core/controllers/gatewayController.cpp ${CLIENT_ROOT_DIR}/core/controllers/serverController.cpp ${CLIENT_ROOT_DIR}/core/controllers/vpnConfigurationController.cpp diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h index 2df4c833..b01832ce 100644 --- a/client/core/api/apiDefs.h +++ b/client/core/api/apiDefs.h @@ -6,7 +6,7 @@ namespace apiDefs { enum ConfigType { - AmneziaFreeV2 = 1, + AmneziaFreeV2 = 0, AmneziaFreeV3, AmneziaPremiumV1, AmneziaPremiumV2, diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp index d90ac1dc..7c58e0e1 100644 --- a/client/core/api/apiUtils.cpp +++ b/client/core/api/apiUtils.cpp @@ -44,3 +44,8 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec } }; } + +apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigObject) +{ + return static_cast(serverConfigObject.value(apiDefs::key::configVersion).toInt()); +} diff --git a/client/core/api/apiUtils.h b/client/core/api/apiUtils.h index 2c9496bd..bb122736 100644 --- a/client/core/api/apiUtils.h +++ b/client/core/api/apiUtils.h @@ -12,6 +12,7 @@ namespace apiUtils bool isSubscriptionExpired(const QString &subscriptionEndDate); apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); + apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); } #endif // APIUTILS_H diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp deleted file mode 100644 index fab91408..00000000 --- a/client/core/controllers/apiController.cpp +++ /dev/null @@ -1,262 +0,0 @@ -#include "apiController.h" - -#include -#include -#include -#include - -#include "amnezia_application.h" -#include "configurators/wireguard_configurator.h" -#include "core/api/apiDefs.h" -#include "gatewayController.h" -#include "version.h" - -namespace -{ - namespace configKey - { - constexpr char cloak[] = "cloak"; - constexpr char awg[] = "awg"; - - constexpr char apiEdnpoint[] = "api_endpoint"; - constexpr char accessToken[] = "api_key"; - constexpr char certificate[] = "certificate"; - constexpr char publicKey[] = "public_key"; - constexpr char protocol[] = "protocol"; - - constexpr char uuid[] = "installation_uuid"; - constexpr char osVersion[] = "os_version"; - constexpr char appVersion[] = "app_version"; - - constexpr char userCountryCode[] = "user_country_code"; - constexpr char serverCountryCode[] = "server_country_code"; - constexpr char serviceType[] = "service_type"; - constexpr char serviceInfo[] = "service_info"; - - constexpr char aesKey[] = "aes_key"; - constexpr char aesIv[] = "aes_iv"; - constexpr char aesSalt[] = "aes_salt"; - - constexpr char apiPayload[] = "api_payload"; - constexpr char keyPayload[] = "key_payload"; - - constexpr char apiConfig[] = "api_config"; - constexpr char authData[] = "auth_data"; - } - - const int requestTimeoutMsecs = 12 * 1000; // 12 secs -} - -ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent) - : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment) -{ -} - -void ApiController::fillServerConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, - const QByteArray &apiResponseBody, QJsonObject &serverConfig) -{ - QString data = QJsonDocument::fromJson(apiResponseBody).object().value(config_key::config).toString(); - - data.replace("vpn://", ""); - QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - - if (ba.isEmpty()) { - emit errorOccurred(ErrorCode::ApiConfigEmptyError); - return; - } - - QByteArray ba_uncompressed = qUncompress(ba); - if (!ba_uncompressed.isEmpty()) { - ba = ba_uncompressed; - } - - QString configStr = ba; - if (protocol == configKey::cloak) { - configStr.replace("", "\n"); - configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); - } else if (protocol == configKey::awg) { - configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); - auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - auto containers = newServerConfig.value(config_key::containers).toArray(); - if (containers.isEmpty()) { - return; // todo process error - } - auto container = containers.at(0).toObject(); - QString containerName = ContainerProps::containerTypeToString(DockerContainer::Awg); - auto containerConfig = container.value(containerName).toObject(); - auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object(); - containerConfig[config_key::junkPacketCount] = protocolConfig.value(config_key::junkPacketCount); - containerConfig[config_key::junkPacketMinSize] = protocolConfig.value(config_key::junkPacketMinSize); - containerConfig[config_key::junkPacketMaxSize] = protocolConfig.value(config_key::junkPacketMaxSize); - containerConfig[config_key::initPacketJunkSize] = protocolConfig.value(config_key::initPacketJunkSize); - containerConfig[config_key::responsePacketJunkSize] = protocolConfig.value(config_key::responsePacketJunkSize); - containerConfig[config_key::initPacketMagicHeader] = protocolConfig.value(config_key::initPacketMagicHeader); - containerConfig[config_key::responsePacketMagicHeader] = protocolConfig.value(config_key::responsePacketMagicHeader); - containerConfig[config_key::underloadPacketMagicHeader] = protocolConfig.value(config_key::underloadPacketMagicHeader); - containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); - container[containerName] = containerConfig; - containers.replace(0, container); - newServerConfig[config_key::containers] = containers; - configStr = QString(QJsonDocument(newServerConfig).toJson()); - } - - QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1); - serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2); - serverConfig[config_key::containers] = newServerConfig.value(config_key::containers); - serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName); - - if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { - serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion); - serverConfig[config_key::description] = newServerConfig.value(config_key::description); - serverConfig[config_key::name] = newServerConfig.value(config_key::name); - } - - auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString(); - serverConfig[config_key::defaultContainer] = defaultContainer; - - QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap(); - map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap()); - auto apiConfig = QJsonObject::fromVariantMap(map); - - if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { - apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject()); - } - - serverConfig[configKey::apiConfig] = apiConfig; - - return; -} - -ApiController::ApiPayloadData ApiController::generateApiPayloadData(const QString &protocol) -{ - ApiController::ApiPayloadData apiPayload; - if (protocol == configKey::cloak) { - apiPayload.certRequest = OpenVpnConfigurator::createCertRequest(); - } else if (protocol == configKey::awg) { - auto connData = WireguardConfigurator::genClientKeys(); - apiPayload.wireGuardClientPubKey = connData.clientPubKey; - apiPayload.wireGuardClientPrivKey = connData.clientPrivKey; - } - return apiPayload; -} - -QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData) -{ - QJsonObject obj; - if (protocol == configKey::cloak) { - obj[configKey::certificate] = apiPayloadData.certRequest.request; - } else if (protocol == configKey::awg) { - obj[configKey::publicKey] = apiPayloadData.wireGuardClientPubKey; - } - - obj[configKey::osVersion] = QSysInfo::productType(); - obj[configKey::appVersion] = QString(APP_VERSION); - - return obj; -} - -void ApiController::updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig) -{ -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - if (serverConfig.value(config_key::configVersion).toInt()) { - QNetworkRequest request; - request.setTransferTimeout(requestTimeoutMsecs); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); - QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); - request.setUrl(endpoint); - - QString protocol = serverConfig.value(configKey::protocol).toString(); - - ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); - - QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); - apiPayload[configKey::uuid] = installationUuid; - - QByteArray requestBody = QJsonDocument(apiPayload).toJson(); - - QNetworkReply *reply = amnApp->manager()->post(request, requestBody); - - QObject::connect(reply, &QNetworkReply::finished, [this, reply, protocol, apiPayloadData, serverIndex, serverConfig]() mutable { - if (reply->error() == QNetworkReply::NoError) { - auto apiResponseBody = reply->readAll(); - fillServerConfig(protocol, apiPayloadData, apiResponseBody, serverConfig); - emit finished(serverConfig, serverIndex); - } else { - if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - emit errorOccurred(ErrorCode::ApiConfigTimeoutError); - } else { - QString err = reply->errorString(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - emit errorOccurred(ErrorCode::ApiConfigDownloadError); - } - } - - reply->deleteLater(); - }); - - QObject::connect(reply, &QNetworkReply::errorOccurred, - [this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; }); - connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList &errors) { - qDebug().noquote() << errors; - emit errorOccurred(ErrorCode::ApiConfigSslError); - }); - } -} - -ErrorCode ApiController::getServicesList(QByteArray &responseBody) -{ - GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); - - QJsonObject apiPayload; - apiPayload[configKey::osVersion] = QSysInfo::productType(); - - ErrorCode errorCode = gatewayController.post(QString("%1v1/services"), apiPayload, responseBody); - if (errorCode == ErrorCode::NoError) { - if (!responseBody.contains("services")) { - return ErrorCode::ApiServicesMissingError; - } - } - - return errorCode; -} - -ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, - QJsonObject &serverConfig) -{ - GatewayController gatewayController(m_gatewayEndpoint, m_isDevEnvironment, requestTimeoutMsecs); - - ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); - - QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); - apiPayload[configKey::userCountryCode] = userCountryCode; - if (!serverCountryCode.isEmpty()) { - apiPayload[configKey::serverCountryCode] = serverCountryCode; - } - apiPayload[configKey::serviceType] = serviceType; - apiPayload[configKey::uuid] = installationUuid; - if (!authData.isEmpty()) { - apiPayload[configKey::authData] = authData; - } - - QByteArray responseBody; - ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody); - - if (errorCode == ErrorCode::NoError) { - fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig); - } - - return errorCode; -} - - diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h deleted file mode 100644 index 367c89e4..00000000 --- a/client/core/controllers/apiController.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef APICONTROLLER_H -#define APICONTROLLER_H - -#include - -#include "configurators/openvpn_configurator.h" - -#ifdef Q_OS_IOS - #include "platforms/ios/ios_controller.h" -#endif - -class ApiController : public QObject -{ - Q_OBJECT - -public: - explicit ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent = nullptr); - -public slots: - void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); - ErrorCode getServicesList(QByteArray &responseBody); - ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, - QJsonObject &serverConfig); - -signals: - void errorOccurred(ErrorCode errorCode); - void finished(const QJsonObject &config, const int serverIndex); - -private: - struct ApiPayloadData - { - OpenVpnConfigurator::ConnectionData certRequest; - - QString wireGuardClientPrivKey; - QString wireGuardClientPubKey; - }; - - ApiPayloadData generateApiPayloadData(const QString &protocol); - QJsonObject fillApiPayload(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData); - void fillServerConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, const QByteArray &apiResponseBody, - QJsonObject &serverConfig); - QStringList getProxyUrls(); - - QString m_gatewayEndpoint; - QStringList m_proxyUrls; - bool m_isDevEnvironment = false; -}; - -#endif // APICONTROLLER_H diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp new file mode 100644 index 00000000..551eb61f --- /dev/null +++ b/client/core/controllers/coreController.cpp @@ -0,0 +1,332 @@ +#include "coreController.h" + +#if defined(Q_OS_ANDROID) + #include "core/installedAppsImageProvider.h" + #include "platforms/android/android_controller.h" +#endif + +#if defined(Q_OS_IOS) + #include "platforms/ios/ios_controller.h" + #include +#endif + +CoreController::CoreController(const QSharedPointer &vpnConnection, const std::shared_ptr &settings, + QQmlApplicationEngine *engine, QObject *parent) + : QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine) +{ + initModels(); + initControllers(); + initSignalHandlers(); + + initNotificationHandler(); + + auto locale = m_settings->getAppLanguage(); + m_translator.reset(new QTranslator()); + updateTranslator(locale); +} + +void CoreController::initModels() +{ + m_containersModel.reset(new ContainersModel(this)); + m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); + + m_defaultServerContainersModel.reset(new ContainersModel(this)); + m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get()); + + m_serversModel.reset(new ServersModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); + + m_languageModel.reset(new LanguageModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); + + m_sitesModel.reset(new SitesModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); + + m_appSplitTunnelingModel.reset(new AppSplitTunnelingModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("AppSplitTunnelingModel", m_appSplitTunnelingModel.get()); + + m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); + + m_openVpnConfigModel.reset(new OpenVpnConfigModel(this)); + m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get()); + + m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this)); + m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get()); + + m_cloakConfigModel.reset(new CloakConfigModel(this)); + m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get()); + + m_wireGuardConfigModel.reset(new WireGuardConfigModel(this)); + m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get()); + + m_awgConfigModel.reset(new AwgConfigModel(this)); + m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get()); + + m_xrayConfigModel.reset(new XrayConfigModel(this)); + m_engine->rootContext()->setContextProperty("XrayConfigModel", m_xrayConfigModel.get()); + +#ifdef Q_OS_WINDOWS + m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this)); + m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get()); +#endif + + m_sftpConfigModel.reset(new SftpConfigModel(this)); + m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); + + m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this)); + m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get()); + + m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); + + m_apiServicesModel.reset(new ApiServicesModel(this)); + m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get()); + + m_apiCountryModel.reset(new ApiCountryModel(this)); + m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get()); + + m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this)); + m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get()); +} + +void CoreController::initControllers() +{ + m_connectionController.reset( + new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings)); + m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); + + m_pageController.reset(new PageController(m_serversModel, m_settings)); + m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); + + m_focusController.reset(new FocusController(m_engine, this)); + m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get()); + + m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, m_settings)); + m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); + + connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(), + &ConnectionController::onCurrentContainerUpdated); // TODO remove this + + m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); + m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); + + m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, m_settings)); + m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); + + m_settingsController.reset( + new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); + m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); + + m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); + m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); + + m_appSplitTunnelingController.reset(new AppSplitTunnelingController(m_settings, m_appSplitTunnelingModel)); + m_engine->rootContext()->setContextProperty("AppSplitTunnelingController", m_appSplitTunnelingController.get()); + + m_systemController.reset(new SystemController(m_settings)); + m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + + m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_settings)); + m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); + + m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); + m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); +} + +void CoreController::initAndroidController() +{ +#ifdef Q_OS_ANDROID + if (!AndroidController::initLogging()) { + qFatal("Android logging initialization failed"); + } + AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); + connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); + + AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled()); + connect(m_settings.get(), &Settings::screenshotsEnabledChanged, AndroidController::instance(), &AndroidController::setScreenshotsEnabled); + + connect(m_settings.get(), &Settings::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer); + + connect(m_settings.get(), &Settings::settingsCleared, []() { AndroidController::instance()->resetLastServer(-1); }); + + connect(AndroidController::instance(), &AndroidController::initConnectionState, this, [this](Vpn::ConnectionState state) { + m_connectionController->onConnectionStateChanged(state); + if (m_vpnConnection) + m_vpnConnection->restoreConnection(); + }); + if (!AndroidController::instance()->initialize()) { + qFatal("Android controller initialization failed"); + } + + connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); + m_importController->extractConfigFromData(data); + data.clear(); + emit m_pageController->goToPageViewConfig(); + }); + + m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); +#endif +} + +void CoreController::initAppleController() +{ +#ifdef Q_OS_IOS + IosController::Instance()->initialize(); + connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); + m_importController->extractConfigFromData(data); + emit m_pageController->goToPageViewConfig(); + }); + + connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) { + emit m_pageController->goToPageHome(); + m_pageController->goToPageSettingsBackup(); + emit m_settingsController->importBackupFromOutside(filePath); + }); + + QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); + + connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); }); +#endif +} + +void CoreController::initSignalHandlers() +{ + initApiCountryModelUpdateHandler(); + initContainerModelUpdateHandler(); + initAdminConfigRevokedHandler(); + initConnectionErrorOccurredHandler(); + initPassphraseRequestHandler(); + initTranslationsUpdatedHandler(); + initAutoConnectHandler(); + initAmneziaDnsToggledHandler(); + initPrepareConfigHandler(); +} + +void CoreController::initNotificationHandler() +{ +#ifndef Q_OS_ANDROID + m_notificationHandler.reset(NotificationHandler::create(nullptr)); + + connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), + &NotificationHandler::setConnectionState); + + connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), &PageController::raiseMainWindow); + connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(), + static_cast(&ConnectionController::openConnection)); + connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), + &ConnectionController::closeConnection); + connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); +#endif +} + +void CoreController::updateTranslator(const QLocale &locale) +{ + if (!m_translator->isEmpty()) { + QCoreApplication::removeTranslator(m_translator.get()); + } + + QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm"; + if (m_translator->load(strFileName)) { + if (QCoreApplication::installTranslator(m_translator.get())) { + m_settings->setAppLanguage(locale); + } + } else { + m_settings->setAppLanguage(QLocale::English); + } + + m_engine->retranslate(); + + emit translationsUpdated(); +} + +void CoreController::setQmlRoot() +{ + m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); +} + +void CoreController::initApiCountryModelUpdateHandler() +{ + // TODO + connect(m_serversModel.get(), &ServersModel::updateApiCountryModel, this, [this]() { + m_apiCountryModel->updateModel(m_serversModel->getProcessedServerData("apiAvailableCountries").toJsonArray(), + m_serversModel->getProcessedServerData("apiServerCountryCode").toString()); + }); + connect(m_serversModel.get(), &ServersModel::updateApiServicesModel, this, + [this]() { m_apiServicesModel->updateModel(m_serversModel->getProcessedServerData("apiConfig").toJsonObject()); }); +} + +void CoreController::initContainerModelUpdateHandler() +{ + connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); + connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), + &ContainersModel::updateModel); + m_serversModel->resetModel(); +} + +void CoreController::initAdminConfigRevokedHandler() +{ + connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(), + &ServersModel::clearCachedProfile); +} + +void CoreController::initConnectionErrorOccurredHandler() +{ + connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](ErrorCode errorCode) { + emit m_pageController->showErrorMessage(errorCode); + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + }); +} + +void CoreController::initPassphraseRequestHandler() +{ + connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), + &PageController::showPassphraseRequestDrawer); + connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(), + &InstallController::setEncryptedPassphrase); +} + +void CoreController::initTranslationsUpdatedHandler() +{ + connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &CoreController::updateTranslator); + connect(this, &CoreController::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated); + connect(this, &CoreController::translationsUpdated, m_connectionController.get(), &ConnectionController::onTranslationsUpdated); +} + +void CoreController::initAutoConnectHandler() +{ + if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { + QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); + } +} + +void CoreController::initAmneziaDnsToggledHandler() +{ + connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled, m_serversModel.get(), &ServersModel::toggleAmneziaDns); +} + +void CoreController::initPrepareConfigHandler() +{ + connect(m_connectionController.get(), &ConnectionController::prepareConfig, this, [this]() { + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing); + + if (!m_apiConfigsController->isConfigValid()) { + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + return; + } + + if (!m_installController->isConfigValid()) { + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + return; + } + + m_connectionController->openConnection(); + }); +} + +QSharedPointer CoreController::pageController() const +{ + return m_pageController; +} diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h new file mode 100644 index 00000000..c67c9ccc --- /dev/null +++ b/client/core/controllers/coreController.h @@ -0,0 +1,133 @@ +#ifndef CORECONTROLLER_H +#define CORECONTROLLER_H + +#include +#include +#include + +#include "ui/controllers/api/apiConfigsController.h" +#include "ui/controllers/api/apiSettingsController.h" +#include "ui/controllers/appSplitTunnelingController.h" +#include "ui/controllers/connectionController.h" +#include "ui/controllers/exportController.h" +#include "ui/controllers/focusController.h" +#include "ui/controllers/importController.h" +#include "ui/controllers/installController.h" +#include "ui/controllers/pageController.h" +#include "ui/controllers/settingsController.h" +#include "ui/controllers/sitesController.h" +#include "ui/controllers/systemController.h" + +#include "ui/models/containers_model.h" +#include "ui/models/languageModel.h" +#include "ui/models/protocols/cloakConfigModel.h" +#ifdef Q_OS_WINDOWS + #include "ui/models/protocols/ikev2ConfigModel.h" +#endif +#include "ui/models/api/apiAccountInfoModel.h" +#include "ui/models/api/apiServicesModel.h" +#include "ui/models/apiCountryModel.h" +#include "ui/models/appSplitTunnelingModel.h" +#include "ui/models/clientManagementModel.h" +#include "ui/models/protocols/awgConfigModel.h" +#include "ui/models/protocols/openvpnConfigModel.h" +#include "ui/models/protocols/shadowsocksConfigModel.h" +#include "ui/models/protocols/wireguardConfigModel.h" +#include "ui/models/protocols/xrayConfigModel.h" +#include "ui/models/protocols_model.h" +#include "ui/models/servers_model.h" +#include "ui/models/services/sftpConfigModel.h" +#include "ui/models/services/socks5ProxyConfigModel.h" +#include "ui/models/sites_model.h" + +#ifndef Q_OS_ANDROID + #include "ui/notificationhandler.h" +#endif + +class CoreController : public QObject +{ + Q_OBJECT + +public: + explicit CoreController(const QSharedPointer &vpnConnection, const std::shared_ptr &settings, + QQmlApplicationEngine *engine, QObject *parent = nullptr); + + QSharedPointer pageController() const; + void setQmlRoot(); + +signals: + void translationsUpdated(); + +private: + void initModels(); + void initControllers(); + void initAndroidController(); + void initAppleController(); + void initSignalHandlers(); + + void initNotificationHandler(); + + void updateTranslator(const QLocale &locale); + + void initApiCountryModelUpdateHandler(); + void initContainerModelUpdateHandler(); + void initAdminConfigRevokedHandler(); + void initConnectionErrorOccurredHandler(); + void initPassphraseRequestHandler(); + void initTranslationsUpdatedHandler(); + void initAutoConnectHandler(); + void initAmneziaDnsToggledHandler(); + void initPrepareConfigHandler(); + + QQmlApplicationEngine *m_engine {}; // TODO use parent child system here? + std::shared_ptr m_settings; + QSharedPointer m_vpnConnection; + QSharedPointer m_translator; + +#ifndef Q_OS_ANDROID + QScopedPointer m_notificationHandler; +#endif + + QMetaObject::Connection m_reloadConfigErrorOccurredConnection; + + QScopedPointer m_connectionController; + QScopedPointer m_focusController; + QSharedPointer m_pageController; // TODO + QScopedPointer m_installController; + QScopedPointer m_importController; + QScopedPointer m_exportController; + QScopedPointer m_settingsController; + QScopedPointer m_sitesController; + QScopedPointer m_systemController; + QScopedPointer m_appSplitTunnelingController; + + QScopedPointer m_apiSettingsController; + QScopedPointer m_apiConfigsController; + + QSharedPointer m_containersModel; + QSharedPointer m_defaultServerContainersModel; + QSharedPointer m_serversModel; + QSharedPointer m_languageModel; + QSharedPointer m_protocolsModel; + QSharedPointer m_sitesModel; + QSharedPointer m_appSplitTunnelingModel; + QSharedPointer m_clientManagementModel; + + QSharedPointer m_apiServicesModel; + QSharedPointer m_apiCountryModel; + QSharedPointer m_apiAccountInfoModel; + + QScopedPointer m_openVpnConfigModel; + QScopedPointer m_shadowSocksConfigModel; + QScopedPointer m_cloakConfigModel; + QScopedPointer m_xrayConfigModel; + QScopedPointer m_wireGuardConfigModel; + QScopedPointer m_awgConfigModel; +#ifdef Q_OS_WINDOWS + QScopedPointer m_ikev2ConfigModel; +#endif + QScopedPointer m_sftpConfigModel; + QScopedPointer m_socks5ConfigModel; +}; + +#endif // CORECONTROLLER_H diff --git a/client/core/controllers/vpnConfigurationController.cpp b/client/core/controllers/vpnConfigurationController.cpp index 52f42c42..61287972 100644 --- a/client/core/controllers/vpnConfigurationController.cpp +++ b/client/core/controllers/vpnConfigurationController.cpp @@ -77,8 +77,7 @@ ErrorCode VpnConfigurationsController::createProtocolConfigString(const bool isA } QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair &dns, const QJsonObject &serverConfig, - const QJsonObject &containerConfig, const DockerContainer container, - ErrorCode &errorCode) + const QJsonObject &containerConfig, const DockerContainer container) { QJsonObject vpnConfiguration {}; @@ -103,7 +102,8 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair &settings, QSharedPointer serverController, QObject *parent = nullptr); + explicit VpnConfigurationsController(const std::shared_ptr &settings, QSharedPointer serverController, + QObject *parent = nullptr); public slots: ErrorCode createProtocolConfigForContainer(const ServerCredentials &credentials, const DockerContainer container, @@ -21,7 +22,7 @@ public slots: const DockerContainer container, const QJsonObject &containerConfig, const Proto protocol, QString &protocolConfigString); QJsonObject createVpnConfiguration(const QPair &dns, const QJsonObject &serverConfig, - const QJsonObject &containerConfig, const DockerContainer container, ErrorCode &errorCode); + const QJsonObject &containerConfig, const DockerContainer container); static void updateContainerConfigAfterInstallation(const DockerContainer container, QJsonObject &containerConfig, const QString &stdOut); signals: diff --git a/client/core/defs.h b/client/core/defs.h index 4ea0e613..c7fde0ee 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -44,6 +44,7 @@ namespace amnezia InternalError = 101, NotImplementedError = 102, AmneziaServiceNotRunning = 103, + NotSupportedOnThisPlatform = 104, // Server errors ServerCheckFailed = 200, @@ -94,6 +95,7 @@ namespace amnezia // import and install errors ImportInvalidConfigError = 900, ImportOpenConfigError = 901, + NoInstalledContainersError = 902, // Android errors AndroidError = 1000, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 70f433c6..976915c2 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -12,6 +12,7 @@ QString errorString(ErrorCode code) { case(ErrorCode::UnknownError): errorMessage = QObject::tr("Unknown error"); break; case(ErrorCode::NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break; case(ErrorCode::AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break; + case(ErrorCode::NotSupportedOnThisPlatform): errorMessage = QObject::tr("The selected protocol is not supported on the current platform"); break; // Server errors case(ErrorCode::ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break; @@ -51,6 +52,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break; case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break; + case(ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break; // Android errors case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 0858ecff..ee133253 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -1,8 +1,13 @@ #include "apiConfigsController.h" +#include + +#include "amnezia_application.h" #include "configurators/wireguard_configurator.h" #include "core/api/apiDefs.h" +#include "core/api/apiUtils.h" #include "core/controllers/gatewayController.h" +#include "core/networkUtilities.h" #include "core/qrCodeUtils.h" #include "ui/controllers/systemController.h" #include "version.h" @@ -18,6 +23,7 @@ namespace constexpr char accessToken[] = "api_key"; constexpr char certificate[] = "certificate"; constexpr char publicKey[] = "public_key"; + constexpr char protocol[] = "protocol"; constexpr char uuid[] = "installation_uuid"; constexpr char osVersion[] = "os_version"; @@ -43,9 +49,10 @@ namespace } } -ApiConfigsController::ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, - QObject *parent) - : QObject(parent), m_serversModel(serversModel), m_settings(settings) +ApiConfigsController::ApiConfigsController(const QSharedPointer &serversModel, + const QSharedPointer &apiServicesModel, + const std::shared_ptr &settings, QObject *parent) + : QObject(parent), m_serversModel(serversModel), m_apiServicesModel(apiServicesModel), m_settings(settings) { } @@ -98,6 +105,211 @@ void ApiConfigsController::prepareVpnKeyExport() emit vpnKeyExportReady(); } +bool ApiConfigsController::fillAvailableServices() +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + QJsonObject apiPayload; + apiPayload[configKey::osVersion] = QSysInfo::productType(); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/services"), apiPayload, responseBody); + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + errorCode = ErrorCode::ApiServicesMissingError; + } + } + + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } + + QJsonObject data = QJsonDocument::fromJson(responseBody).object(); + m_apiServicesModel->updateModel(data); + return true; +} + +bool ApiConfigsController::importServiceFromGateway() +{ + if (m_serversModel->isServerFromApiAlreadyExists(m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), + m_apiServicesModel->getSelectedServiceProtocol())) { + emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded); + return false; + } + + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto installationUuid = m_settings->getInstallationUuid(true); + auto userCountryCode = m_apiServicesModel->getCountryCode(); + auto serviceType = m_apiServicesModel->getSelectedServiceType(); + auto serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol(); + + ApiPayloadData apiPayloadData = generateApiPayloadData(serviceProtocol); + + QJsonObject apiPayload = fillApiPayload(serviceProtocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = userCountryCode; + apiPayload[configKey::serviceType] = serviceType; + apiPayload[configKey::uuid] = installationUuid; + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody); + + QJsonObject serverConfig; + if (errorCode == ErrorCode::NoError) { + fillServerConfig(serviceProtocol, apiPayloadData, responseBody, serverConfig); + + QJsonObject apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + apiConfig.insert(configKey::userCountryCode, m_apiServicesModel->getCountryCode()); + apiConfig.insert(configKey::serviceType, m_apiServicesModel->getSelectedServiceType()); + apiConfig.insert(configKey::serviceProtocol, m_apiServicesModel->getSelectedServiceProtocol()); + + serverConfig.insert(configKey::apiConfig, apiConfig); + + m_serversModel->addServer(serverConfig); + emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName())); + return true; + } else { + emit errorOccurred(errorCode); + return false; + } +} + +bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, + bool reloadServiceConfig) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfig = m_serversModel->getServerConfig(serverIndex); + auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + auto authData = serverConfig.value(configKey::authData).toObject(); + + auto installationUuid = m_settings->getInstallationUuid(true); + auto userCountryCode = apiConfig.value(configKey::userCountryCode).toString(); + auto serviceType = apiConfig.value(configKey::serviceType).toString(); + auto serviceProtocol = apiConfig.value(configKey::serviceProtocol).toString(); + + ApiPayloadData apiPayloadData = generateApiPayloadData(serviceProtocol); + + QJsonObject apiPayload = fillApiPayload(serviceProtocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = userCountryCode; + apiPayload[configKey::serviceType] = serviceType; + apiPayload[configKey::uuid] = installationUuid; + + if (!newCountryCode.isEmpty()) { + apiPayload[configKey::serverCountryCode] = newCountryCode; + } + if (!authData.isEmpty()) { + apiPayload[configKey::authData] = authData; + } + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody); + + QJsonObject newServerConfig; + if (errorCode == ErrorCode::NoError) { + fillServerConfig(serviceProtocol, apiPayloadData, responseBody, newServerConfig); + + QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject(); + newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode)); + newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); + newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); + + newServerConfig.insert(configKey::apiConfig, newApiConfig); + newServerConfig.insert(configKey::authData, authData); + + m_serversModel->editServer(newServerConfig, serverIndex); + if (reloadServiceConfig) { + emit reloadServerFromApiFinished(tr("API config reloaded")); + } else if (newCountryName.isEmpty()) { + emit updateServerFromApiFinished(); + } else { + emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName)); + } + return true; + } else { + emit errorOccurred(errorCode); + return false; + } +} + +bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) +{ + auto serverConfig = m_serversModel->getServerConfig(serverIndex); + auto installationUuid = m_settings->getInstallationUuid(true); + +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + if (serverConfig.value(config_key::configVersion).toInt()) { + QNetworkRequest request; + request.setTransferTimeout(apiDefs::requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); + QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); + request.setUrl(endpoint); + + QString protocol = serverConfig.value(configKey::protocol).toString(); + + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::uuid] = installationUuid; + + QByteArray requestBody = QJsonDocument(apiPayload).toJson(); + + QNetworkReply *reply = amnApp->manager()->post(request, requestBody); + + QEventLoop wait; + connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + + QList sslErrors; + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + auto errorCode = NetworkUtilities::checkNetworkReplyErrors(sslErrors, reply); + if (errorCode != ErrorCode::NoError) { + reply->deleteLater(); + emit errorOccurred(errorCode); + return false; + } + + auto apiResponseBody = reply->readAll(); + reply->deleteLater(); + fillServerConfig(protocol, apiPayloadData, apiResponseBody, serverConfig); + m_serversModel->editServer(serverConfig, serverIndex); + emit updateServerFromApiFinished(); + } + return true; +} + +bool ApiConfigsController::isConfigValid() +{ + int serverIndex = m_serversModel->getDefaultServerIndex(); + QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex); + auto configSource = apiUtils::getConfigSource(serverConfigObject); + + if (configSource == apiDefs::ConfigSource::Telegram + && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + m_serversModel->removeApiConfig(serverIndex); + return updateServiceFromTelegram(serverIndex); + } else if (configSource == apiDefs::ConfigSource::AmneziaGateway + && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + return updateServiceFromGateway(serverIndex, "", ""); + } else if (configSource && m_serversModel->isApiKeyExpired(serverIndex)) { + qDebug() << "attempt to update api config by expires_at event"; + if (configSource == apiDefs::ConfigSource::Telegram) { + return updateServiceFromGateway(serverIndex, "", ""); + } else { + m_serversModel->removeApiConfig(serverIndex); + return updateServiceFromTelegram(serverIndex); + } + } + return true; +} + ApiConfigsController::ApiPayloadData ApiConfigsController::generateApiPayloadData(const QString &protocol) { ApiConfigsController::ApiPayloadData apiPayload; @@ -126,6 +338,82 @@ QJsonObject ApiConfigsController::fillApiPayload(const QString &protocol, const return obj; } +void ApiConfigsController::fillServerConfig(const QString &protocol, const ApiPayloadData &apiPayloadData, + const QByteArray &apiResponseBody, QJsonObject &serverConfig) +{ + QString data = QJsonDocument::fromJson(apiResponseBody).object().value(config_key::config).toString(); + + data.replace("vpn://", ""); + QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + if (ba.isEmpty()) { + emit errorOccurred(ErrorCode::ApiConfigEmptyError); + return; + } + + QByteArray ba_uncompressed = qUncompress(ba); + if (!ba_uncompressed.isEmpty()) { + ba = ba_uncompressed; + } + + QString configStr = ba; + if (protocol == configKey::cloak) { + configStr.replace("", "\n"); + configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); + } else if (protocol == configKey::awg) { + configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); + auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + auto containers = newServerConfig.value(config_key::containers).toArray(); + if (containers.isEmpty()) { + return; // todo process error + } + auto container = containers.at(0).toObject(); + QString containerName = ContainerProps::containerTypeToString(DockerContainer::Awg); + auto containerConfig = container.value(containerName).toObject(); + auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object(); + containerConfig[config_key::junkPacketCount] = protocolConfig.value(config_key::junkPacketCount); + containerConfig[config_key::junkPacketMinSize] = protocolConfig.value(config_key::junkPacketMinSize); + containerConfig[config_key::junkPacketMaxSize] = protocolConfig.value(config_key::junkPacketMaxSize); + containerConfig[config_key::initPacketJunkSize] = protocolConfig.value(config_key::initPacketJunkSize); + containerConfig[config_key::responsePacketJunkSize] = protocolConfig.value(config_key::responsePacketJunkSize); + containerConfig[config_key::initPacketMagicHeader] = protocolConfig.value(config_key::initPacketMagicHeader); + containerConfig[config_key::responsePacketMagicHeader] = protocolConfig.value(config_key::responsePacketMagicHeader); + containerConfig[config_key::underloadPacketMagicHeader] = protocolConfig.value(config_key::underloadPacketMagicHeader); + containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); + container[containerName] = containerConfig; + containers.replace(0, container); + newServerConfig[config_key::containers] = containers; + configStr = QString(QJsonDocument(newServerConfig).toJson()); + } + + QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1); + serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2); + serverConfig[config_key::containers] = newServerConfig.value(config_key::containers); + serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName); + + if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { + serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion); + serverConfig[config_key::description] = newServerConfig.value(config_key::description); + serverConfig[config_key::name] = newServerConfig.value(config_key::name); + } + + auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString(); + serverConfig[config_key::defaultContainer] = defaultContainer; + + QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap(); + map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap()); + auto apiConfig = QJsonObject::fromVariantMap(map); + + if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { + apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject()); + } + + serverConfig[configKey::apiConfig] = apiConfig; + + return; +} + QList ApiConfigsController::getQrCodes() { return m_qrCodes; diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index f9bf977f..a8202afc 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -4,14 +4,15 @@ #include #include "configurators/openvpn_configurator.h" +#include "ui/models/api/apiServicesModel.h" #include "ui/models/servers_model.h" class ApiConfigsController : public QObject { Q_OBJECT public: - ApiConfigsController(const QSharedPointer &serversModel, const std::shared_ptr &settings, - QObject *parent = nullptr); + ApiConfigsController(const QSharedPointer &serversModel, const QSharedPointer &apiServicesModel, + const std::shared_ptr &settings, QObject *parent = nullptr); Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady) @@ -21,8 +22,22 @@ public slots: // bool exportVpnKey(const QString &fileName); void prepareVpnKeyExport(); + bool fillAvailableServices(); + bool importServiceFromGateway(); + bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, + bool reloadServiceConfig = false); + bool updateServiceFromTelegram(const int serverIndex); + + bool isConfigValid(); + signals: void errorOccurred(ErrorCode errorCode); + + void installServerFromApiFinished(const QString &message); + void changeApiCountryFinished(const QString &message); + void reloadServerFromApiFinished(const QString &message); + void updateServerFromApiFinished(); + void vpnKeyExportReady(); private: @@ -36,6 +51,8 @@ private: ApiPayloadData generateApiPayloadData(const QString &protocol); QJsonObject fillApiPayload(const QString &protocol, const ApiPayloadData &apiPayloadData); + void fillServerConfig(const QString &protocol, const ApiPayloadData &apiPayloadData, const QByteArray &apiResponseBody, + QJsonObject &serverConfig); QList getQrCodes(); int getQrCodesCount(); @@ -43,6 +60,7 @@ private: QList m_qrCodes; QSharedPointer m_serversModel; + QSharedPointer m_apiServicesModel; std::shared_ptr m_settings; }; diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index 9cd386c5..9fc60493 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -5,10 +5,8 @@ #else #include #endif -#include #include "core/controllers/vpnConfigurationController.h" -#include "core/api/apiDefs.h" #include "version.h" ConnectionController::ConnectionController(const QSharedPointer &serversModel, @@ -27,7 +25,7 @@ ConnectionController::ConnectionController(const QSharedPointer &s connect(this, &ConnectionController::connectToVpn, m_vpnConnection.get(), &VpnConnection::connectToVpn, Qt::QueuedConnection); connect(this, &ConnectionController::disconnectFromVpn, m_vpnConnection.get(), &VpnConnection::disconnectFromVpn, Qt::QueuedConnection); - connect(this, &ConnectionController::configFromApiUpdated, this, &ConnectionController::continueConnection); + connect(this, &ConnectionController::connectButtonClicked, this, &ConnectionController::toggleConnection, Qt::QueuedConnection); m_state = Vpn::ConnectionState::Disconnected; } @@ -35,8 +33,7 @@ ConnectionController::ConnectionController(const QSharedPointer &s void ConnectionController::openConnection() { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) - if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) - { + if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) { emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); return; } @@ -44,26 +41,24 @@ void ConnectionController::openConnection() int serverIndex = m_serversModel->getDefaultServerIndex(); QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex); - auto configVersion = serverConfig.value(config_key::configVersion).toInt(); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing); + DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); - if (configVersion == apiDefs::ConfigSource::Telegram - && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { - emit updateApiConfigFromTelegram(); - } else if (configVersion == apiDefs::ConfigSource::AmneziaGateway - && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { - emit updateApiConfigFromGateway(); - } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by expires_at event"; - if (configVersion == apiDefs::ConfigSource::Telegram) { - emit updateApiConfigFromTelegram(); - } else { - emit updateApiConfigFromGateway(); - } - } else { - continueConnection(); + if (!m_containersModel->isSupportedByCurrentPlatform(container)) { + emit connectionErrorOccurred(ErrorCode::NotSupportedOnThisPlatform); + return; } + + QSharedPointer serverController(new ServerController(m_settings)); + VpnConfigurationsController vpnConfigurationController(m_settings, serverController); + + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + auto dns = m_serversModel->getDnsPair(serverIndex); + + auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container); + emit connectToVpn(serverIndex, credentials, container, vpnConfiguration); } void ConnectionController::closeConnection() @@ -167,7 +162,7 @@ void ConnectionController::toggleConnection() } else if (isConnected()) { closeConnection(); } else { - openConnection(); + emit prepareConfig(); } } @@ -180,98 +175,3 @@ bool ConnectionController::isConnected() const { return m_isConnected; } - -bool ConnectionController::isProtocolConfigExists(const QJsonObject &containerConfig, const DockerContainer container) -{ - for (Proto protocol : ContainerProps::protocolsForContainer(container)) { - QString protocolConfig = - containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString(); - - if (protocolConfig.isEmpty()) { - return false; - } - } - return true; -} - -void ConnectionController::continueConnection() -{ - int serverIndex = m_serversModel->getDefaultServerIndex(); - QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex); - auto configVersion = serverConfig.value(config_key::configVersion).toInt(); - - if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { - emit noInstalledContainers(); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - return; - } - - DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); - - if (!m_containersModel->isSupportedByCurrentPlatform(container)) { - emit connectionErrorOccurred(tr("The selected protocol is not supported on the current platform")); - return; - } - - if (container == DockerContainer::None) { - emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first")); - return; - } - - QSharedPointer serverController(new ServerController(m_settings)); - VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - - QJsonObject containerConfig = m_containersModel->getContainerConfig(container); - ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); - ErrorCode errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController); - if (errorCode != ErrorCode::NoError) { - emit connectionErrorOccurred(errorCode); - return; - } - - auto dns = m_serversModel->getDnsPair(serverIndex); - - auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container, errorCode); - if (errorCode != ErrorCode::NoError) { - emit connectionErrorOccurred(tr("unable to create configuration")); - return; - } - - emit connectToVpn(serverIndex, credentials, container, vpnConfiguration); -} - -ErrorCode ConnectionController::updateProtocolConfig(const DockerContainer container, const ServerCredentials &credentials, - QJsonObject &containerConfig, QSharedPointer serverController) -{ - QFutureWatcher watcher; - - if (serverController.isNull()) { - serverController.reset(new ServerController(m_settings)); - } - - QFuture future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() { - ErrorCode errorCode = ErrorCode::NoError; - if (!isProtocolConfigExists(containerConfig, container)) { - VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - m_serversModel->updateContainerConfig(container, containerConfig); - - errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, - QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - } - return errorCode; - }); - - QEventLoop wait; - connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); - watcher.setFuture(future); - wait.exec(); - - return watcher.result(); -} diff --git a/client/ui/controllers/connectionController.h b/client/ui/controllers/connectionController.h index 25d4d74a..cabeb601 100644 --- a/client/ui/controllers/connectionController.h +++ b/client/ui/controllers/connectionController.h @@ -40,30 +40,20 @@ public slots: void onTranslationsUpdated(); - ErrorCode updateProtocolConfig(const DockerContainer container, const ServerCredentials &credentials, QJsonObject &containerConfig, - QSharedPointer serverController = nullptr); - signals: void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); void disconnectFromVpn(); void connectionStateChanged(); - void connectionErrorOccurred(const QString &errorMessage); void connectionErrorOccurred(ErrorCode errorCode); void reconnectWithUpdatedContainer(const QString &message); - void noInstalledContainers(); - void connectButtonClicked(); void preparingConfig(); - - void updateApiConfigFromGateway(); - void updateApiConfigFromTelegram(); - void configFromApiUpdated(); + void prepareConfig(); private: Vpn::ConnectionState getCurrentConnectionState(); - bool isProtocolConfigExists(const QJsonObject &containerConfig, const DockerContainer container); void continueConnection(); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 26e433e0..7a6d8d40 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -6,8 +6,8 @@ #include #include #include +#include -#include "core/controllers/apiController.h" #include "core/controllers/serverController.h" #include "core/controllers/vpnConfigurationController.h" #include "core/networkUtilities.h" @@ -15,6 +15,7 @@ #include "ui/models/protocols/awgConfigModel.h" #include "ui/models/protocols/wireguardConfigModel.h" #include "utilities.h" +#include "core/api/apiUtils.h" namespace { @@ -39,14 +40,12 @@ namespace InstallController::InstallController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const QSharedPointer &protocolsModel, const QSharedPointer &clientManagementModel, - const QSharedPointer &apiServicesModel, const std::shared_ptr &settings, - QObject *parent) + const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_serversModel(serversModel), m_containersModel(containersModel), m_protocolModel(protocolsModel), m_clientManagementModel(clientManagementModel), - m_apiServicesModel(apiServicesModel), m_settings(settings) { } @@ -773,110 +772,79 @@ void InstallController::addEmptyServer() emit installServerFinished(tr("Server added successfully")); } -bool InstallController::fillAvailableServices() +bool InstallController::isConfigValid() { - ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); + int serverIndex = m_serversModel->getDefaultServerIndex(); + QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex); - QByteArray responseBody; - ErrorCode errorCode = apiController.getServicesList(responseBody); - if (errorCode != ErrorCode::NoError) { - emit installationErrorOccurred(errorCode); + if (apiUtils::isServerFromApi(serverConfigObject)) { + return true; + } + + if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + emit noInstalledContainers(); return false; } - QJsonObject data = QJsonDocument::fromJson(responseBody).object(); - m_apiServicesModel->updateModel(data); - return true; -} + DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); -bool InstallController::installServiceFromApi() -{ - if (m_serversModel->isServerFromApiAlreadyExists(m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), - m_apiServicesModel->getSelectedServiceProtocol())) { - emit installationErrorOccurred(ErrorCode::ApiConfigAlreadyAdded); + if (container == DockerContainer::None) { + emit installationErrorOccurred(ErrorCode::NoInstalledContainersError); return false; } - ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); - QJsonObject serverConfig; + QSharedPointer serverController(new ServerController(m_settings)); + VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(), - m_apiServicesModel->getSelectedServiceType(), - m_apiServicesModel->getSelectedServiceProtocol(), "", QJsonObject(), serverConfig); - if (errorCode != ErrorCode::NoError) { - emit installationErrorOccurred(errorCode); - return false; - } + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); - auto serviceInfo = m_apiServicesModel->getSelectedServiceInfo(); - QJsonObject apiConfig = serverConfig.value(configKey::apiConfig).toObject(); - apiConfig.insert(configKey::serviceInfo, serviceInfo); - apiConfig.insert(configKey::userCountryCode, m_apiServicesModel->getCountryCode()); - apiConfig.insert(configKey::serviceType, m_apiServicesModel->getSelectedServiceType()); - apiConfig.insert(configKey::serviceProtocol, m_apiServicesModel->getSelectedServiceProtocol()); + QFutureWatcher watcher; - serverConfig.insert(configKey::apiConfig, apiConfig); + QFuture future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() { + ErrorCode errorCode = ErrorCode::NoError; - m_serversModel->addServer(serverConfig); - emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName())); - return true; -} + auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) { + for (Proto protocol : ContainerProps::protocolsForContainer(container)) { + QString protocolConfig = + containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString(); -bool InstallController::updateServiceFromApi(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, - bool reloadServiceConfig) -{ - ApiController apiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); + if (protocolConfig.isEmpty()) { + return false; + } + } + return true; + }; - auto serverConfig = m_serversModel->getServerConfig(serverIndex); - auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); - auto authData = serverConfig.value(configKey::authData).toObject(); + if (!isProtocolConfigExists(containerConfig, container)) { + VpnConfigurationsController vpnConfigurationController(m_settings, serverController); + errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + m_serversModel->updateContainerConfig(container, containerConfig); - QJsonObject newServerConfig; - - ErrorCode errorCode = apiController.getConfigForService( - m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(), - apiConfig.value(configKey::serviceType).toString(), apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode, - authData, newServerConfig); - if (errorCode != ErrorCode::NoError) { - emit installationErrorOccurred(errorCode); - return false; - } - - QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject(); - newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode)); - newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); - newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); - - newServerConfig.insert(configKey::apiConfig, newApiConfig); - newServerConfig.insert(configKey::authData, authData); - m_serversModel->editServer(newServerConfig, serverIndex); - - if (reloadServiceConfig) { - emit reloadServerFromApiFinished(tr("API config reloaded")); - } else if (newCountryName.isEmpty()) { - emit updateServerFromApiFinished(); - } else { - emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName)); - } - return true; -} - -void InstallController::updateServiceFromTelegram(const int serverIndex) -{ - ApiController *apiController = new ApiController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv()); - - auto serverConfig = m_serversModel->getServerConfig(serverIndex); - - apiController->updateServerConfigFromApi(m_settings->getInstallationUuid(true), serverIndex, serverConfig); - connect(apiController, &ApiController::finished, this, [this, apiController](const QJsonObject &config, const int serverIndex) { - m_serversModel->editServer(config, serverIndex); - emit updateServerFromApiFinished(); - apiController->deleteLater(); + errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, + QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + } + return errorCode; }); - connect(apiController, &ApiController::errorOccurred, this, [this, apiController](ErrorCode errorCode) { + + QEventLoop wait; + connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); + watcher.setFuture(future); + wait.exec(); + + ErrorCode errorCode = watcher.result(); + + if (errorCode != ErrorCode::NoError) { emit installationErrorOccurred(errorCode); - apiController->deleteLater(); - }); + return false; + } + return true; } bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig, diff --git a/client/ui/controllers/installController.h b/client/ui/controllers/installController.h index d7ab3553..8e42b5b2 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -10,7 +10,6 @@ #include "ui/models/containers_model.h" #include "ui/models/protocols_model.h" #include "ui/models/servers_model.h" -#include "ui/models/apiServicesModel.h" class InstallController : public QObject { @@ -19,7 +18,6 @@ public: explicit InstallController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const QSharedPointer &protocolsModel, const QSharedPointer &clientManagementModel, - const QSharedPointer &apiServicesModel, const std::shared_ptr &settings, QObject *parent = nullptr); ~InstallController(); @@ -52,21 +50,13 @@ public slots: void addEmptyServer(); - bool fillAvailableServices(); - bool installServiceFromApi(); - bool updateServiceFromApi(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig = false); - - void updateServiceFromTelegram(const int serverIndex); + bool isConfigValid(); signals: void installContainerFinished(const QString &finishMessage, bool isServiceInstall); void installServerFinished(const QString &finishMessage); - void installServerFromApiFinished(const QString &message); void updateContainerFinished(const QString &message); - void updateServerFromApiFinished(); - void changeApiCountryFinished(const QString &message); - void reloadServerFromApiFinished(const QString &message); void scanServerFinished(bool isInstalledContainerFound); @@ -91,6 +81,8 @@ signals: void cachedProfileCleared(const QString &message); void apiConfigRemoved(const QString &message); + void noInstalledContainers(); + private: void installServer(const DockerContainer container, const QMap &installedContainers, const ServerCredentials &serverCredentials, const QSharedPointer &serverController, @@ -108,7 +100,6 @@ private: QSharedPointer m_containersModel; QSharedPointer m_protocolModel; QSharedPointer m_clientManagementModel; - QSharedPointer m_apiServicesModel; std::shared_ptr m_settings; diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/api/apiServicesModel.cpp similarity index 100% rename from client/ui/models/apiServicesModel.cpp rename to client/ui/models/api/apiServicesModel.cpp diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/api/apiServicesModel.h similarity index 100% rename from client/ui/models/apiServicesModel.h rename to client/ui/models/api/apiServicesModel.h diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index f77eb6fc..7cde28b4 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -261,7 +261,7 @@ void ServersModel::setProcessedServerIndex(const int index) updateContainersModel(); if (data(index, IsServerFromGatewayApiRole).toBool()) { if (data(index, IsCountrySelectionAvailableRole).toBool()) { - emit updateApiLanguageModel(); + emit updateApiCountryModel(); } emit updateApiServicesModel(); } diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 78bc22cc..4b790c7a 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -140,7 +140,7 @@ signals: void defaultServerContainersUpdated(const QJsonArray &containers); void defaultServerDefaultContainerChanged(const int containerIndex); - void updateApiLanguageModel(); + void updateApiCountryModel(); void updateApiServicesModel(); private: diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index 41735b14..353547ef 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -136,7 +136,7 @@ PageType { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex ApiCountryModel.currentIndex = index - if (!InstallController.updateServiceFromApi(ServersModel.defaultIndex, countryCode, countryName)) { + if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, countryCode, countryName)) { ApiCountryModel.currentIndex = prevIndex } } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 303ba36c..e377140f 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -253,7 +253,7 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot reload API config during active connection")) } else { PageController.showBusyIndicator(true) - InstallController.updateServiceFromApi(ServersModel.processedIndex, "", "", true) + ApiConfigsController.updateServiceFromGateway(ServersModel.processedIndex, "", "", true) PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml index 8c02195e..134e73b6 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -138,7 +138,7 @@ PageType { PageController.closePage() } else { PageController.showBusyIndicator(true) - InstallController.installServiceFromApi() + ApiConfigsController.importServiceFromGateway() PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index f145127f..38a1da52 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -254,7 +254,7 @@ PageType { property bool isVisible: true property var handler: function() { PageController.showBusyIndicator(true) - var result = InstallController.fillAvailableServices() + var result = ApiConfigsController.fillAvailableServices() PageController.showBusyIndicator(false) if (result) { PageController.goToPage(PageEnum.PageSetupWizardApiServicesList) diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 27e851de..76133da8 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -46,7 +46,7 @@ PageType { shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text shareConnectionDrawer.openTriggered() - shareConnectionDrawer.contentVisible = false + shareConnectionDrawer.contentVisible = true PageController.showBusyIndicator(true) switch (type) { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 6081bbc8..bf13552c 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -132,29 +132,6 @@ PageType { PageController.showNotificationMessage(message) } - function onInstallServerFromApiFinished(message) { - PageController.showBusyIndicator(false) - if (!ConnectionController.isConnected) { - ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); - ServersModel.processedIndex = ServersModel.defaultIndex - } - - PageController.goToPageHome() - PageController.showNotificationMessage(message) - } - - function onChangeApiCountryFinished(message) { - PageController.showBusyIndicator(false) - - PageController.goToPageHome() - PageController.showNotificationMessage(message) - } - - function onReloadServerFromApiFinished(message) { - PageController.goToPageHome() - PageController.showNotificationMessage(message) - } - function onRemoveProcessedServerFinished(finishedMessage) { if (!ServersModel.getServersCount()) { PageController.goToPageHome() @@ -164,6 +141,14 @@ PageType { } PageController.showNotificationMessage(finishedMessage) } + + function onNoInstalledContainers() { + PageController.setTriggeredByConnectButton(true) + + ServersModel.processedIndex = ServersModel.getDefaultServerIndex() + InstallController.setShouldCreateServer(false) + PageController.goToPage(PageEnum.PageSetupWizardEasy) + } } Connections { @@ -175,14 +160,6 @@ PageType { PageController.showNotificationMessage(message) PageController.closePage() } - - function onNoInstalledContainers() { - PageController.setTriggeredByConnectButton(true) - - ServersModel.processedIndex = ServersModel.getDefaultServerIndex() - InstallController.setShouldCreateServer(false) - PageController.goToPage(PageEnum.PageSetupWizardEasy) - } } Connections { @@ -232,6 +209,37 @@ PageType { } } + Connections { + target: ApiConfigsController + + function onErrorOccurred(error) { + PageController.showErrorMessage(error) + } + + function onInstallServerFromApiFinished(message) { + PageController.showBusyIndicator(false) + if (!ConnectionController.isConnected) { + ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); + ServersModel.processedIndex = ServersModel.defaultIndex + } + + PageController.goToPageHome() + PageController.showNotificationMessage(message) + } + + function onChangeApiCountryFinished(message) { + PageController.showBusyIndicator(false) + + PageController.goToPageHome() + PageController.showNotificationMessage(message) + } + + function onReloadServerFromApiFinished(message) { + PageController.goToPageHome() + PageController.showNotificationMessage(message) + } + } + StackViewType { id: tabBarStackView objectName: "tabBarStackView" From 52c12940c4fdb0b8d4bd2beb4c4e9fc016cc1b53 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 15 Feb 2025 13:57:44 +0700 Subject: [PATCH 11/36] bugfix: fixed visability of share drawer --- client/ui/qml/Components/ShareConnectionDrawer.qml | 4 ---- client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 1 - client/ui/qml/Pages2/PageShare.qml | 1 - client/ui/qml/Pages2/PageShareFullAccess.qml | 1 - 4 files changed, 7 deletions(-) diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 9a9f1aa1..a7910ebc 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -26,8 +26,6 @@ DrawerType2 { property string copyButtonText: qsTr("Copy") property bool showSettingsButtonVisible: true - property bool contentVisible - property string configExtension: ".vpn" property string configCaption: qsTr("Save AmneziaVPN config") property string configFileName: "amnezia_config" @@ -75,8 +73,6 @@ DrawerType2 { header: ColumnLayout { width: listView.width - visible: root.contentVisible - BasicButtonType { id: shareButton Layout.fillWidth: true diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index e377140f..05012b46 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -163,7 +163,6 @@ PageType { 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") diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 76133da8..af208544 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -46,7 +46,6 @@ PageType { shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text shareConnectionDrawer.openTriggered() - shareConnectionDrawer.contentVisible = true PageController.showBusyIndicator(true) switch (type) { diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index af409d72..70fd6292 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -141,7 +141,6 @@ PageType { shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text shareConnectionDrawer.openTriggered() - shareConnectionDrawer.contentVisible = true PageController.showBusyIndicator(false) } From a1ca994c8be71574f938800b70201f4f2b2f4ac5 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 15 Feb 2025 13:58:48 +0700 Subject: [PATCH 12/36] feature: added 409 error handling from server response --- client/core/api/apiUtils.cpp | 29 +++++++++++++++++++ client/core/api/apiUtils.h | 4 +++ client/core/controllers/gatewayController.cpp | 6 ++-- client/core/defs.h | 1 + client/core/errorstrings.cpp | 1 + client/core/networkUtilities.cpp | 20 ------------- client/core/networkUtilities.h | 5 ---- .../controllers/api/apiConfigsController.cpp | 2 +- 8 files changed, 39 insertions(+), 29 deletions(-) diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp index 7c58e0e1..088fdba1 100644 --- a/client/core/api/apiUtils.cpp +++ b/client/core/api/apiUtils.cpp @@ -49,3 +49,32 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO { return static_cast(serverConfigObject.value(apiDefs::key::configVersion).toInt()); } + +amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply) +{ + const int httpStatusCodeConflict = 409; + + if (!sslErrors.empty()) { + qDebug().noquote() << sslErrors; + return amnezia::ErrorCode::ApiConfigSslError; + } else if (reply->error() == QNetworkReply::NoError) { + return amnezia::ErrorCode::NoError; + } else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + return amnezia::ErrorCode::ApiConfigTimeoutError; + } else { + QString err = reply->errorString(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << QString::fromUtf8(reply->readAll()); + qDebug() << reply->error(); + qDebug() << err; + qDebug() << httpStatusCode; + if (httpStatusCode == httpStatusCodeConflict) { + return amnezia::ErrorCode::ApiConfigLimitError; + } + return amnezia::ErrorCode::ApiConfigDownloadError; + } + + qDebug() << "something went wrong"; + return amnezia::ErrorCode::InternalError; +} diff --git a/client/core/api/apiUtils.h b/client/core/api/apiUtils.h index bb122736..82ac315b 100644 --- a/client/core/api/apiUtils.h +++ b/client/core/api/apiUtils.h @@ -1,9 +1,11 @@ #ifndef APIUTILS_H #define APIUTILS_H +#include #include #include "apiDefs.h" +#include "core/defs.h" namespace apiUtils { @@ -13,6 +15,8 @@ namespace apiUtils apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); + + amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply); } #endif // APIUTILS_H diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 44a3d5d1..268561ae 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -9,7 +9,7 @@ #include "QRsa.h" #include "amnezia_application.h" -#include "core/networkUtilities.h" +#include "core/api/apiUtils.h" #include "utilities.h" namespace @@ -75,7 +75,7 @@ ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBo bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); } - auto errorCode = NetworkUtilities::checkNetworkReplyErrors(sslErrors, reply); + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); reply->deleteLater(); return errorCode; @@ -165,7 +165,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); } - auto errorCode = NetworkUtilities::checkNetworkReplyErrors(sslErrors, reply); + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); reply->deleteLater(); if (errorCode) { return errorCode; diff --git a/client/core/defs.h b/client/core/defs.h index c7fde0ee..330ebc48 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, ApiServicesMissingError = 1107, + ApiConfigLimitError = 1108, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 976915c2..c45b1eaf 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -66,6 +66,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + case (ErrorCode::ApiConfigLimitError): errorMessage = QObject::tr("The limit of allowed configurations per subscription has been exceeded"); break; // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index 7d98e6a1..a5825f0d 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -107,26 +107,6 @@ QStringList NetworkUtilities::summarizeRoutes(const QStringList &ips, const QStr return QStringList(); } -amnezia::ErrorCode NetworkUtilities::checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply) -{ - if (!sslErrors.empty()) { - qDebug().noquote() << sslErrors; - return amnezia::ErrorCode::ApiConfigSslError; - } else if (reply->error() == QNetworkReply::NoError) { - return amnezia::ErrorCode::NoError; - } else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - return amnezia::ErrorCode::ApiConfigTimeoutError; - } else { - QString err = reply->errorString(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - return amnezia::ErrorCode::ApiConfigDownloadError; - } -} - QString NetworkUtilities::getIPAddress(const QString &host) { QHostAddress address(host); diff --git a/client/core/networkUtilities.h b/client/core/networkUtilities.h index 805ce9e5..3b64b547 100644 --- a/client/core/networkUtilities.h +++ b/client/core/networkUtilities.h @@ -7,8 +7,6 @@ #include #include -#include "core/defs.h" - class NetworkUtilities : public QObject { @@ -33,9 +31,6 @@ public: static QString ipAddressFromIpWithSubnet(const QString ip); static QStringList summarizeRoutes(const QStringList &ips, const QString cidr); - - static amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply); - }; #endif // NETWORKUTILITIES_H diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index ee133253..7085d9a2 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -269,7 +269,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - auto errorCode = NetworkUtilities::checkNetworkReplyErrors(sslErrors, reply); + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); if (errorCode != ErrorCode::NoError) { reply->deleteLater(); emit errorOccurred(errorCode); From c128ba981ccb0106979da9d73f704dba2d704c89 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 15 Feb 2025 15:29:53 +0700 Subject: [PATCH 13/36] chore: fixed android build --- client/core/controllers/coreController.cpp | 2 ++ client/core/controllers/coreController.h | 2 +- client/core/controllers/gatewayController.cpp | 3 +++ client/core/qrCodeUtils.cpp | 8 ++++---- client/core/qrCodeUtils.h | 2 +- .../controllers/api/apiConfigsController.cpp | 4 ++-- .../ui/controllers/api/apiSettingsController.h | 2 +- client/ui/controllers/exportController.cpp | 18 +++++++++--------- client/ui/controllers/importController.cpp | 3 ++- client/ui/models/{ => api}/apiCountryModel.cpp | 4 ++-- client/ui/models/{ => api}/apiCountryModel.h | 2 +- 11 files changed, 28 insertions(+), 22 deletions(-) rename client/ui/models/{ => api}/apiCountryModel.cpp (93%) rename client/ui/models/{ => api}/apiCountryModel.h (91%) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 551eb61f..a077d809 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -1,5 +1,7 @@ #include "coreController.h" +#include + #if defined(Q_OS_ANDROID) #include "core/installedAppsImageProvider.h" #include "platforms/android/android_controller.h" diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index c67c9ccc..1b725825 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -26,7 +26,7 @@ #endif #include "ui/models/api/apiAccountInfoModel.h" #include "ui/models/api/apiServicesModel.h" -#include "ui/models/apiCountryModel.h" +#include "ui/models/api/apiCountryModel.h" #include "ui/models/appSplitTunnelingModel.h" #include "ui/models/clientManagementModel.h" #include "ui/models/protocols/awgConfigModel.h" diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 268561ae..d7a4b018 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -1,5 +1,8 @@ #include "gatewayController.h" +#include +#include + #include #include #include diff --git a/client/core/qrCodeUtils.cpp b/client/core/qrCodeUtils.cpp index 02f43ff0..a18af172 100644 --- a/client/core/qrCodeUtils.cpp +++ b/client/core/qrCodeUtils.cpp @@ -3,7 +3,7 @@ #include #include -QList qrCodeUtuls::generateQrCodeImageSeries(const QByteArray &data) +QList qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data) { double k = 850; @@ -12,7 +12,7 @@ QList qrCodeUtuls::generateQrCodeImageSeries(const QByteArray &data) 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); + s << qrCodeUtils::qrMagicCode << chunksCount << (quint8)std::round(i / k) << data.mid(i, k); QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); @@ -24,12 +24,12 @@ QList qrCodeUtuls::generateQrCodeImageSeries(const QByteArray &data) return chunks; } -QString qrCodeUtuls::svgToBase64(const QString &image) +QString qrCodeUtils::svgToBase64(const QString &image) { return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); } -qrcodegen::QrCode qrCodeUtuls::generateQrCode(const QByteArray &data) +qrcodegen::QrCode qrCodeUtils::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 index f5f207c1..cda0723b 100644 --- a/client/core/qrCodeUtils.h +++ b/client/core/qrCodeUtils.h @@ -5,7 +5,7 @@ #include "qrcodegen.hpp" -namespace qrCodeUtuls +namespace qrCodeUtils { constexpr const qint16 qrMagicCode = 1984; diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 7085d9a2..33894e79 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -99,8 +99,8 @@ void ApiConfigsController::prepareVpnKeyExport() auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); - auto qr = qrCodeUtuls::generateQrCode(vpnKey.toUtf8()); - m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(vpnKey.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit vpnKeyExportReady(); } diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h index ae9d9126..5374e899 100644 --- a/client/ui/controllers/api/apiSettingsController.h +++ b/client/ui/controllers/api/apiSettingsController.h @@ -4,7 +4,7 @@ #include #include "ui/models/api/apiAccountInfoModel.h" -#include "ui/models/apiCountryModel.h" +#include "ui/models/api/apiCountryModel.h" #include "ui/models/servers_model.h" class ApiSettingsController : public QObject diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 516d1f4d..b47111ae 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -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 = qrCodeUtuls::generateQrCodeImageSeries(compressedConfig); + m_qrCodes = qrCodeUtils::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 = qrCodeUtuls::generateQrCodeImageSeries(compressedConfig); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(compressedConfig); emit exportConfigChanged(); } @@ -149,7 +149,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName) m_config.append(line + "\n"); } - m_qrCodes = qrCodeUtuls::generateQrCodeImageSeries(m_config.toUtf8()); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8()); emit exportConfigChanged(); } @@ -167,8 +167,8 @@ void ExportController::generateWireGuardConfig(const QString &clientName) m_config.append(line + "\n"); } - auto qr = qrCodeUtuls::generateQrCode(m_config.toUtf8()); - m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -187,8 +187,8 @@ void ExportController::generateAwgConfig(const QString &clientName) m_config.append(line + "\n"); } - auto qr = qrCodeUtuls::generateQrCode(m_config.toUtf8()); - m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -221,8 +221,8 @@ void ExportController::generateShadowSocksConfig() m_nativeConfigString = "ss://" + m_nativeConfigString.toUtf8().toBase64(); - auto qr = qrCodeUtuls::generateQrCode(m_nativeConfigString.toUtf8()); - m_qrCodes << qrCodeUtuls::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(m_nativeConfigString.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index c79b3288..178efc16 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -10,6 +10,7 @@ #include "core/api/apiDefs.h" #include "core/api/apiUtils.h" #include "core/errorstrings.h" +#include "core/qrCodeUtils.h" #include "core/serialization/serialization.h" #include "systemController.h" #include "utilities.h" @@ -579,7 +580,7 @@ bool ImportController::parseQrCodeChunk(const QString &code) qint16 magic; s >> magic; - if (magic == amnezia::qrMagicCode) { + if (magic == qrCodeUtils::qrMagicCode) { quint8 chunksCount; s >> chunksCount; if (m_totalQrCodeChunksCount != chunksCount) { diff --git a/client/ui/models/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp similarity index 93% rename from client/ui/models/apiCountryModel.cpp rename to client/ui/models/api/apiCountryModel.cpp index 922a9d56..0043efd2 100644 --- a/client/ui/models/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -47,11 +47,11 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const return QVariant(); } -void ApiCountryModel::updateModel(const QJsonArray &data, const QString ¤tCountryCode) +void ApiCountryModel::updateModel(const QJsonArray &countries, const QString ¤tCountryCode) { beginResetModel(); - m_countries = data; + m_countries = countries; for (int i = 0; i < m_countries.size(); i++) { if (m_countries.at(i).toObject().value(configKey::serverCountryCode).toString() == currentCountryCode) { m_currentIndex = i; diff --git a/client/ui/models/apiCountryModel.h b/client/ui/models/api/apiCountryModel.h similarity index 91% rename from client/ui/models/apiCountryModel.h rename to client/ui/models/api/apiCountryModel.h index b9e243d0..c935ebce 100644 --- a/client/ui/models/apiCountryModel.h +++ b/client/ui/models/api/apiCountryModel.h @@ -24,7 +24,7 @@ public: Q_PROPERTY(int currentIndex READ getCurrentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) public slots: - void updateModel(const QJsonArray &data, const QString ¤tCountryCode); + void updateModel(const QJsonArray &countries, const QString ¤tCountryCode); int getCurrentIndex(); void setCurrentIndex(const int i); From a5254ac238a6d1293191165f9fea0bfeb7f91104 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 19 Feb 2025 14:56:53 +0700 Subject: [PATCH 14/36] chore: fixed qr code display --- .../controllers/api/apiConfigsController.cpp | 4 ++-- .../qml/Components/ShareConnectionDrawer.qml | 23 +++++++++++-------- .../qml/Pages2/PageSettingsApiServerInfo.qml | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 33894e79..0861471a 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -99,8 +99,7 @@ void ApiConfigsController::prepareVpnKeyExport() auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); - auto qr = qrCodeUtils::generateQrCode(vpnKey.toUtf8()); - m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(vpnKey.toUtf8()); emit vpnKeyExportReady(); } @@ -214,6 +213,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode)); newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); + newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey)); newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, authData); diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index a7910ebc..dd59180b 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -24,7 +24,7 @@ DrawerType2 { property string configContentHeaderText property string shareButtonText: qsTr("Share") property string copyButtonText: qsTr("Copy") - property bool showSettingsButtonVisible: true + property bool isSelfHostedConfig: true property string configExtension: ".vpn" property string configCaption: qsTr("Save AmneziaVPN config") @@ -153,7 +153,7 @@ DrawerType2 { Layout.leftMargin: 16 Layout.rightMargin: 16 - visible: root.showSettingsButtonVisible + visible: root.isSelfHostedConfig defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite @@ -285,6 +285,8 @@ DrawerType2 { delegate: ColumnLayout { width: listView.width + property bool isQrCodeVisible: root.isSelfHostedConfig ? ExportController.qrCodesCount > 0 : ApiConfigsController.qrCodesCount > 0 + Rectangle { id: qrCodeContainer @@ -294,7 +296,7 @@ DrawerType2 { Layout.leftMargin: 16 Layout.rightMargin: 16 - visible: ExportController.qrCodesCount > 0 + visible: isQrCodeVisible color: "white" @@ -302,7 +304,8 @@ DrawerType2 { anchors.fill: parent smooth: false - source: ExportController.qrCodesCount ? ExportController.qrCodes[0] : "" + source: root.isSelfHostedConfig ? (isQrCodeVisible ? ExportController.qrCodes[0] : "") : + (isQrCodeVisible ? ApiConfigsController.qrCodes[0] : "") property bool isFocusable: true @@ -333,15 +336,17 @@ DrawerType2 { Timer { property int index: 0 interval: 1000 - running: ExportController.qrCodesCount > 0 + running: isQrCodeVisible repeat: true onTriggered: { - if (ExportController.qrCodesCount > 0) { + if (isQrCodeVisible) { index++ - if (index >= ExportController.qrCodesCount) { + let qrCodesCount = root.isSelfHostedConfig ? ExportController.qrCodesCount : ApiConfigsController.qrCodesCount + if (index >= qrCodesCount) { index = 0 } - parent.source = ExportController.qrCodes[index] + + parent.source = root.isSelfHostedConfig ? ExportController.qrCodes[index] : ApiConfigsController.qrCodes[index] } } } @@ -359,7 +364,7 @@ DrawerType2 { Layout.leftMargin: 16 Layout.rightMargin: 16 - visible: ExportController.qrCodesCount > 0 + visible: isQrCodeVisible horizontalAlignment: Text.AlignHCenter text: qsTr("To read the QR code in the Amnezia app, select \"Add server\" → \"I have data to connect\" → \"QR code, key or settings file\"") diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 05012b46..75c1e893 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -163,7 +163,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Amnezia Premium subscription key") shareConnectionDrawer.openTriggered() - shareConnectionDrawer.showSettingsButtonVisible = false; + shareConnectionDrawer.isSelfHostedConfig = false; shareConnectionDrawer.shareButtonText = qsTr("Save VPN key to file") shareConnectionDrawer.copyButtonText = qsTr("Copy VPN key") From 35e0e146e616484fa592705894337f71fb5fb745 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Wed, 19 Feb 2025 14:34:26 +0200 Subject: [PATCH 15/36] Rewrite timeouts using waitForSource --- client/core/ipcclient.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index 7b13ba07..da08ea04 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -1,7 +1,5 @@ #include "ipcclient.h" #include -#include -#include IpcClient *IpcClient::m_instance = nullptr; @@ -46,14 +44,12 @@ bool IpcClient::init(IpcClient *instance) Instance()->m_localSocket = new QLocalSocket(Instance()); connect(Instance()->m_localSocket.data(), &QLocalSocket::connected, &Instance()->m_ClientNode, []() { Instance()->m_ClientNode.addClientSideConnection(Instance()->m_localSocket.data()); - - Instance()->m_ipcClient.reset(Instance()->m_ClientNode.acquire()); -#ifdef Q_OS_WIN - std::this_thread::sleep_for(std::chrono::seconds(2)); //< wait until client is ready -#endif + auto cliNode = Instance()->m_ClientNode.acquire(); + cliNode->waitForSource(5000); + Instance()->m_ipcClient.reset(cliNode); if (!Instance()->m_ipcClient) { - qFatal() << "IpcClient is not ready!"; + qWarning() << "IpcClient is not ready!"; } Instance()->m_ipcClient->waitForSource(1000); @@ -64,12 +60,12 @@ bool IpcClient::init(IpcClient *instance) Instance()->m_Tun2SocksClient.reset(Instance()->m_ClientNode.acquire()); -#ifdef Q_OS_WIN - std::this_thread::sleep_for(std::chrono::seconds(5)); //< wait until client is ready -#endif + auto t2sNode = Instance()->m_ClientNode.acquire(); + t2sNode->waitForSource(5000); + Instance()->m_ipcClient.reset(t2sNode); if (!Instance()->m_Tun2SocksClient) { - qFatal() << "IpcClient::m_Tun2SocksClient is not ready!"; + qWarning() << "IpcClient::m_Tun2SocksClient is not ready!"; } Instance()->m_Tun2SocksClient->waitForSource(1000); From eda24765e70e4561b470bd2a8352be6010687a55 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 19 Feb 2025 20:27:15 +0700 Subject: [PATCH 16/36] feature: added error messages handler --- client/core/controllers/coreController.cpp | 22 +++++++++++-------- client/core/controllers/coreController.h | 3 ++- .../PageSettingsApiAvailableCountries.qml | 1 + client/ui/qml/Pages2/PageStart.qml | 7 ------ 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index a077d809..3d71ce80 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -196,10 +196,11 @@ void CoreController::initAppleController() void CoreController::initSignalHandlers() { + initErrorMessagesHandler(); + initApiCountryModelUpdateHandler(); initContainerModelUpdateHandler(); initAdminConfigRevokedHandler(); - initConnectionErrorOccurredHandler(); initPassphraseRequestHandler(); initTranslationsUpdatedHandler(); initAutoConnectHandler(); @@ -244,6 +245,17 @@ void CoreController::updateTranslator(const QLocale &locale) emit translationsUpdated(); } +void CoreController::initErrorMessagesHandler() +{ + connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](ErrorCode errorCode) { + emit m_pageController->showErrorMessage(errorCode); + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + }); + + connect(m_apiConfigsController.get(), &ApiConfigsController::errorOccurred, m_pageController.get(), + qOverload(&PageController::showErrorMessage)); +} + void CoreController::setQmlRoot() { m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); @@ -274,14 +286,6 @@ void CoreController::initAdminConfigRevokedHandler() &ServersModel::clearCachedProfile); } -void CoreController::initConnectionErrorOccurredHandler() -{ - connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](ErrorCode errorCode) { - emit m_pageController->showErrorMessage(errorCode); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - }); -} - void CoreController::initPassphraseRequestHandler() { connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 1b725825..029044c4 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -69,10 +69,11 @@ private: void updateTranslator(const QLocale &locale); + void initErrorMessagesHandler(); + void initApiCountryModelUpdateHandler(); void initContainerModelUpdateHandler(); void initAdminConfigRevokedHandler(); - void initConnectionErrorOccurredHandler(); void initPassphraseRequestHandler(); void initTranslationsUpdatedHandler(); void initAutoConnectHandler(); diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index 353547ef..dfbc70b4 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -139,6 +139,7 @@ PageType { if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, countryCode, countryName)) { ApiCountryModel.currentIndex = prevIndex } + PageController.showBusyIndicator(false) } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index bf13552c..0a21497d 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -212,12 +212,7 @@ PageType { Connections { target: ApiConfigsController - function onErrorOccurred(error) { - PageController.showErrorMessage(error) - } - function onInstallServerFromApiFinished(message) { - PageController.showBusyIndicator(false) if (!ConnectionController.isConnected) { ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); ServersModel.processedIndex = ServersModel.defaultIndex @@ -228,8 +223,6 @@ PageType { } function onChangeApiCountryFinished(message) { - PageController.showBusyIndicator(false) - PageController.goToPageHome() PageController.showNotificationMessage(message) } From c2b17c128d6c9efa23f46161e0aa5aea0ff81c32 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 19 Feb 2025 22:58:04 +0700 Subject: [PATCH 17/36] feature: added issued configs info parsing --- client/core/api/apiDefs.h | 16 ++++++ .../controllers/api/apiSettingsController.cpp | 1 + client/ui/models/api/apiAccountInfoModel.cpp | 22 ++++---- client/ui/models/api/apiAccountInfoModel.h | 2 + client/ui/models/api/apiCountryModel.cpp | 51 ++++++++++++++----- client/ui/models/api/apiCountryModel.h | 23 ++++++++- 6 files changed, 87 insertions(+), 28 deletions(-) diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h index b01832ce..2892f90b 100644 --- a/client/core/api/apiDefs.h +++ b/client/core/api/apiDefs.h @@ -26,6 +26,22 @@ namespace apiDefs constexpr QLatin1String stackType("stack_type"); constexpr QLatin1String vpnKey("vpn_key"); + + constexpr QLatin1String installationUuid("installation_uuid"); + constexpr QLatin1String workerLastUpdated("worker_last_updated"); + constexpr QLatin1String lastDownloaded("last_downloaded"); + constexpr QLatin1String sourceType("source_type"); + + constexpr QLatin1String serverCountryCode("server_country_code"); + constexpr QLatin1String serverCountryName("server_country_name"); + + constexpr QLatin1String osVersion("os_version"); + + constexpr QLatin1String availableCountries("available_countries"); + constexpr QLatin1String activeDeviceCount("active_device_count"); + constexpr QLatin1String maxDeviceCount("max_device_count"); + constexpr QLatin1String subscriptionEndDate("subscription_end_date"); + constexpr QLatin1String issuedConfigs("issued_configs"); } const int requestTimeoutMsecs = 12 * 1000; // 12 secs diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 0bebf19e..5f436436 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -68,4 +68,5 @@ bool ApiSettingsController::getAccountInfo() void ApiSettingsController::updateApiCountryModel() { m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); + m_apiCountryModel->updateIssuedConfigsInfo(m_apiAccountInfoModel->getIssuedConfigsInfo()); } diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index 254097bc..055c2347 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -8,14 +8,6 @@ namespace { Logger logger("AccountInfoModel"); - - namespace configKey - { - constexpr char availableCountries[] = "available_countries"; - constexpr char activeDeviceCount[] = "active_device_count"; - constexpr char maxDeviceCount[] = "max_device_count"; - constexpr char subscriptionEndDate[] = "subscription_end_date"; - } } ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent) @@ -77,11 +69,12 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons AccountInfoData accountInfoData; - m_availableCountries = accountInfoObject.value(configKey::availableCountries).toArray(); + m_availableCountries = accountInfoObject.value(apiDefs::key::availableCountries).toArray(); + m_issuedConfigsInfo = accountInfoObject.value(apiDefs::key::issuedConfigs).toArray(); - accountInfoData.activeDeviceCount = accountInfoObject.value(configKey::activeDeviceCount).toInt(); - accountInfoData.maxDeviceCount = accountInfoObject.value(configKey::maxDeviceCount).toInt(); - accountInfoData.subscriptionEndDate = accountInfoObject.value(configKey::subscriptionEndDate).toString(); + accountInfoData.activeDeviceCount = accountInfoObject.value(apiDefs::key::activeDeviceCount).toInt(); + accountInfoData.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt(); + accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString(); accountInfoData.configType = apiUtils::getConfigType(serverConfig); @@ -108,6 +101,11 @@ QJsonArray ApiAccountInfoModel::getAvailableCountries() return m_availableCountries; } +QJsonArray ApiAccountInfoModel::getIssuedConfigsInfo() +{ + return m_issuedConfigsInfo; +} + QString ApiAccountInfoModel::getTelegramBotLink() { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index 9dfdc508..e50ea5f5 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -31,6 +31,7 @@ public slots: QVariant data(const QString &roleString); QJsonArray getAvailableCountries(); + QJsonArray getIssuedConfigsInfo(); QString getTelegramBotLink(); protected: @@ -48,6 +49,7 @@ private: AccountInfoData m_accountInfoData; QJsonArray m_availableCountries; + QJsonArray m_issuedConfigsInfo; }; #endif // APIACCOUNTINFOMODEL_H diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp index 0043efd2..d8d74a41 100644 --- a/client/ui/models/api/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -2,17 +2,12 @@ #include +#include "core/api/apiDefs.h" #include "logger.h" namespace { Logger logger("ApiCountryModel"); - - namespace configKey - { - constexpr char serverCountryCode[] = "server_country_code"; - constexpr char serverCountryName[] = "server_country_name"; - } } ApiCountryModel::ApiCountryModel(QObject *parent) : QAbstractListModel(parent) @@ -30,17 +25,19 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject countryInfo = m_countries.at(index.row()).toObject(); + CountryInfo countryInfo = m_countries.at(index.row()); + IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode); + bool isIssued = !issuedConfigInfo.lastDownloaded.isEmpty(); switch (role) { case CountryCodeRole: { - return countryInfo.value(configKey::serverCountryCode).toString(); + return countryInfo.countryCode; } case CountryNameRole: { - return countryInfo.value(configKey::serverCountryName).toString(); + return countryInfo.countryName; } case CountryImageCodeRole: { - return countryInfo.value(configKey::serverCountryCode).toString().toUpper(); + return countryInfo.countryCode.toUpper(); } } @@ -51,13 +48,39 @@ void ApiCountryModel::updateModel(const QJsonArray &countries, const QString &cu { beginResetModel(); - m_countries = countries; - for (int i = 0; i < m_countries.size(); i++) { - if (m_countries.at(i).toObject().value(configKey::serverCountryCode).toString() == currentCountryCode) { + m_countries.clear(); + for (int i = 0; i < countries.size(); i++) { + CountryInfo countryInfo; + QJsonObject countryObject = countries.at(i).toObject(); + + countryInfo.countryName = countryObject.value(apiDefs::key::serverCountryName).toString(); + countryInfo.countryCode = countryObject.value(apiDefs::key::serverCountryCode).toString(); + + if (countryInfo.countryCode == currentCountryCode) { m_currentIndex = i; emit currentIndexChanged(m_currentIndex); - break; } + m_countries.push_back(countryInfo); + } + + endResetModel(); +} + +void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs) +{ + beginResetModel(); + + for (int i = 0; i < issuedConfigs.size(); i++) { + IssuedConfigInfo issuedConfigInfo; + QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject(); + + issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString(); + issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString(); + issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString(); + issuedConfigInfo.sourceType = issuedConfigObject.value(apiDefs::key::sourceType).toString(); + issuedConfigInfo.osVersion = issuedConfigObject.value(apiDefs::key::osVersion).toString(); + + m_issuedConfigs.insert(issuedConfigObject.value(apiDefs::key::serverCountryCode).toString(), issuedConfigInfo); } endResetModel(); diff --git a/client/ui/models/api/apiCountryModel.h b/client/ui/models/api/apiCountryModel.h index c935ebce..e57ec0dd 100644 --- a/client/ui/models/api/apiCountryModel.h +++ b/client/ui/models/api/apiCountryModel.h @@ -2,6 +2,7 @@ #define APICOUNTRYMODEL_H #include +#include #include class ApiCountryModel : public QAbstractListModel @@ -12,7 +13,8 @@ public: enum Roles { CountryNameRole = Qt::UserRole + 1, CountryCodeRole, - CountryImageCodeRole + CountryImageCodeRole, + IsIssuedRole }; explicit ApiCountryModel(QObject *parent = nullptr); @@ -25,6 +27,7 @@ public: public slots: void updateModel(const QJsonArray &countries, const QString ¤tCountryCode); + void updateIssuedConfigsInfo(const QJsonArray &issuedConfigs); int getCurrentIndex(); void setCurrentIndex(const int i); @@ -36,7 +39,23 @@ protected: QHash roleNames() const override; private: - QJsonArray m_countries; + struct IssuedConfigInfo + { + QString installationUuid; + QString workerLastUpdated; + QString lastDownloaded; + QString sourceType; + QString osVersion; + }; + + struct CountryInfo + { + QString countryName; + QString countryCode; + }; + + QVector m_countries; + QHash m_issuedConfigs; int m_currentIndex; }; From 95121c06e2bb109e45b36f6c06138f79dfbddb25 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 20 Feb 2025 13:44:19 +0700 Subject: [PATCH 18/36] feature: added functionality to revoke api configs --- .../controllers/api/apiConfigsController.cpp | 53 +++++- .../ui/controllers/api/apiConfigsController.h | 2 + client/ui/models/api/apiCountryModel.cpp | 4 + .../Pages2/PageSettingsApiNativeConfigs.qml | 158 +++++++++++++++--- .../qml/Pages2/PageSettingsApiServerInfo.qml | 42 ++++- 5 files changed, 234 insertions(+), 25 deletions(-) diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 0861471a..cd3c9f1b 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -73,7 +73,7 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); - apiPayload[configKey::serverCountryCode] = apiConfigObject.value(configKey::serverCountryCode); + apiPayload[configKey::serverCountryCode] = serverCountryCode; apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); apiPayload[configKey::uuid] = m_settings->getInstallationUuid(true); @@ -92,6 +92,31 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, return true; } +bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = serverCountryCode; + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_native_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } + return true; +} + void ApiConfigsController::prepareVpnKeyExport() { auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); @@ -285,6 +310,32 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) return true; } +bool ApiConfigsController::deactivateDevice() +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = apiConfigObject.value(configKey::serverCountryCode); + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + apiPayload[configKey::uuid] = m_settings->getInstallationUuid(true); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } + return true; +} + bool ApiConfigsController::isConfigValid() { int serverIndex = m_serversModel->getDefaultServerIndex(); diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index a8202afc..f8354942 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -19,6 +19,7 @@ public: public slots: bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); + bool revokeNativeConfig(const QString &serverCountryCode); // bool exportVpnKey(const QString &fileName); void prepareVpnKeyExport(); @@ -27,6 +28,7 @@ public slots: bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig = false); bool updateServiceFromTelegram(const int serverIndex); + bool deactivateDevice(); bool isConfigValid(); diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp index d8d74a41..a43184b4 100644 --- a/client/ui/models/api/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -39,6 +39,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const case CountryImageCodeRole: { return countryInfo.countryCode.toUpper(); } + case IsIssuedRole: { + return isIssued; + } } return QVariant(); @@ -103,5 +106,6 @@ QHash ApiCountryModel::roleNames() const roles[CountryNameRole] = "countryName"; roles[CountryCodeRole] = "countryCode"; roles[CountryImageCodeRole] = "countryImageCode"; + roles[IsIssuedRole] = "isIssued"; return roles; } diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index 4e897e44..b2ab2c05 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -60,32 +60,152 @@ PageType { text: countryName leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" - rightImageSource: "qrc:/images/controls/download.svg" + rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg" clickedFunction: function() { - var fileName = "" - if (GC.isMobile()) { - fileName = countryCode + configExtension - } else { - fileName = SystemController.getFileName(configCaption, - qsTr("Config files (*" + configExtension + ")"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode, - true, - configExtension) - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - let result = ApiConfigsController.exportNativeConfig(countryCode, fileName) - PageController.showBusyIndicator(false) - - if (result) { - PageController.showNotificationMessage(qsTr("Config file saved")) - } + if (isIssued) { + moreOptionsDrawer.countryName = countryName + moreOptionsDrawer.countryCode = countryCode + moreOptionsDrawer.openTriggered() } + issueConfig(countryCode) } } DividerType {} } } + + DrawerType2 { + id: moreOptionsDrawer + + property string countryName + property string countryCode + + anchors.fill: parent + expandedHeight: parent.height * 0.4375 + + expandedStateContent: Item { + implicitHeight: moreOptionsDrawer.expandedHeight + + BackButtonType { + id: moreOptionsDrawerBackButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + + backButtonFunction: function() { + moreOptionsDrawer.closeTriggered() + } + } + + FlickableType { + anchors.top: moreOptionsDrawerBackButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + contentHeight: moreOptionsDrawerContent.height + + ColumnLayout { + id: moreOptionsDrawerContent + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Header2Type { + Layout.fillWidth: true + Layout.margins: 16 + + headerText: qsTr("Configuration file ") + moreOptionsDrawer.countryName + } + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Create a new") + descriptionText: qsTr("The previously created one will stop working") + + clickedFunction: function() { + showQuestion(true, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Revoke the current configuration file") + + clickedFunction: function() { + showQuestion(false, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) + } + } + + DividerType {} + } + } + } + } + + function issueConfig(countryCode) { + var fileName = "" + if (GC.isMobile()) { + fileName = countryCode + configExtension + } else { + fileName = SystemController.getFileName(configCaption, + qsTr("Config files (*" + configExtension + ")"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode, + true, + configExtension) + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + let result = ApiConfigsController.exportNativeConfig(countryCode, fileName) + if (result) { + ApiSettingsController.getAccountInfo() + } + + PageController.showBusyIndicator(false) + + if (result) { + PageController.showNotificationMessage(qsTr("Config file saved")) + } + } + } + + function revokeConfig(countryCode) { + PageController.showBusyIndicator(true) + let result = ApiConfigsController.revokeNativeConfig(countryCode) + if (result) { + ApiSettingsController.getAccountInfo() + } + PageController.showBusyIndicator(false) + + if (result) { + PageController.showNotificationMessage(qsTr("The config has been revoked")) + } + } + + function showQuestion(isConfigIssue, countryCode, countryName) { + var headerText = qsTr("Revoke the actual %1 configuration file?").arg(countryName) + var descriptionText = qsTr("The previously created file will no longer be valid. It will not be possible to connect using it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (isConfigIssue) { + issueConfig(countryCode) + } else { + revokeConfig(countryCode) + } + } + var noButtonFunction = function() { + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 75c1e893..000edba7 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -257,10 +257,45 @@ PageType { } } var noButtonFunction = function() { - if (!GC.isMobile()) { - removeButton.forceActiveFocus() + } + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } + + BasicButtonType { + id: revokeButton + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 24 + Layout.bottomMargin: 16 + Layout.leftMargin: 8 + implicitHeight: 32 + + defaultColor: "transparent" + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + textColor: AmneziaStyle.color.vibrantRed + + text: qsTr("Deactivate the subscription on this device") + + clickedFunc: function() { + var headerText = qsTr("Deactivate the subscription on this device?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("The next time the “Connect” button is pressed, the device will be activated again")) + } else { + PageController.showBusyIndicator(true) + if (ApiConfigsController.deactivateDevice()) { + ApiSettingsController.getAccountInfo() + } + PageController.showBusyIndicator(false) } } + var noButtonFunction = function() { + } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } @@ -295,9 +330,6 @@ PageType { } } var noButtonFunction = function() { - if (!GC.isMobile()) { - removeButton.forceActiveFocus() - } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) From 5f6cd282d320fd52236984370372f28f4b45b47d Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 21 Feb 2025 14:14:22 +0700 Subject: [PATCH 19/36] chore: added links to instructions --- client/ui/qml/Pages2/PageSettingsApiInstructions.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml index d1c8231e..b73d8e16 100644 --- a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -20,21 +20,21 @@ PageType { id: windows readonly property string title: qsTr("Windows") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#windows") } QtObject { id: macos readonly property string title: qsTr("macOS") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#macos") } QtObject { id: android readonly property string title: qsTr("Android") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#android") } QtObject { @@ -48,21 +48,21 @@ PageType { id: ios readonly property string title: qsTr("iOS") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#ios") } QtObject { id: linux readonly property string title: qsTr("Linux") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#linux") } QtObject { id: routers readonly property string title: qsTr("Routers") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#routers") } property list instructionsModel: [ From 48980c486efd348e50a7b48d1987f0bbba1fbb48 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 21 Feb 2025 14:15:03 +0700 Subject: [PATCH 20/36] chore: fixed qr code with vpnkey processing --- client/ui/controllers/importController.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 178efc16..1bba0e8a 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -155,9 +155,9 @@ bool ImportController::extractConfigFromData(QString data) if (m_configType == ConfigTypes::Invalid) { 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; + QByteArray baUncompressed = qUncompress(ba); + if (!baUncompressed.isEmpty()) { + ba = baUncompressed; } config = ba; @@ -228,6 +228,21 @@ bool ImportController::extractConfigFromQr(const QByteArray &data) return true; } + m_configType = checkConfigFormat(data); + if (m_configType == ConfigTypes::Invalid) { + QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + QByteArray baUncompressed = qUncompress(ba); + + if (!baUncompressed.isEmpty()) { + ba = baUncompressed; + } + + if (!ba.isEmpty()) { + m_config = QJsonDocument::fromJson(ba).object(); + return true; + } + } + return false; } From 8afe50cd87975cf7b1a5771c4c40bc73cae35cc3 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 21 Feb 2025 14:15:23 +0700 Subject: [PATCH 21/36] chore: fixed native config post processing --- client/ui/controllers/api/apiConfigsController.cpp | 3 +++ client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index cd3c9f1b..8c6d53c2 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -87,6 +87,7 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); QString nativeConfig = jsonConfig.value(configKey::config).toString(); + nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); SystemController::saveFile(fileName, nativeConfig); return true; @@ -124,6 +125,8 @@ void ApiConfigsController::prepareVpnKeyExport() auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); + vpnKey.replace("vpn://", ""); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(vpnKey.toUtf8()); emit vpnKeyExportReady(); diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 000edba7..befe7370 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -217,7 +217,7 @@ PageType { LabelWithButtonType { Layout.fillWidth: true - text: qsTr("How to connect on another devicey") + text: qsTr("How to connect on another device") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { @@ -266,11 +266,12 @@ PageType { BasicButtonType { id: revokeButton Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 24 Layout.bottomMargin: 16 Layout.leftMargin: 8 implicitHeight: 32 + visible: footer.isVisibleForAmneziaFree + defaultColor: "transparent" hoveredColor: AmneziaStyle.color.translucentWhite pressedColor: AmneziaStyle.color.sheerWhite From 6a424e98588b882ac9d246cdfe5972b180f1a87f Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 21 Feb 2025 14:16:40 +0700 Subject: [PATCH 22/36] chore: added link to android tv instruction --- client/ui/qml/Pages2/PageSettingsApiInstructions.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml index b73d8e16..70a0df74 100644 --- a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -41,7 +41,7 @@ PageType { id: androidTv readonly property string title: qsTr("AndroidTV") - readonly property string link: qsTr("") + readonly property string link: qsTr("https://docs.amnezia.org/ru/documentation/instructions/android_tv_connect/") } QtObject { From 46536bc60ab15fd8ed4c84d2d63ad53cce34d751 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Fri, 21 Feb 2025 09:31:10 +0200 Subject: [PATCH 23/36] change node to IpcProcessTun2SocksReplica --- client/core/ipcclient.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index da08ea04..69edcd15 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -58,11 +58,9 @@ bool IpcClient::init(IpcClient *instance) qWarning() << "IpcClient replica is not connected!"; } - Instance()->m_Tun2SocksClient.reset(Instance()->m_ClientNode.acquire()); - - auto t2sNode = Instance()->m_ClientNode.acquire(); + auto t2sNode = Instance()->m_ClientNode.acquire(); t2sNode->waitForSource(5000); - Instance()->m_ipcClient.reset(t2sNode); + Instance()->m_Tun2SocksClient.reset(t2sNode); if (!Instance()->m_Tun2SocksClient) { qWarning() << "IpcClient::m_Tun2SocksClient is not ready!"; From d19017f87b991479e2f312404500937bf68087b1 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sat, 22 Feb 2025 14:42:09 +0700 Subject: [PATCH 24/36] chore: minor ui fixes --- client/amnezia_application.cpp | 10 ++++++++++ client/amnezia_application.h | 7 +++---- client/core/api/apiDefs.h | 1 + client/core/api/apiUtils.cpp | 12 ++++++++---- client/core/controllers/gatewayController.cpp | 12 +++++++----- .../ui/controllers/api/apiConfigsController.cpp | 17 ++++++++++++++--- .../ui/controllers/api/apiConfigsController.h | 4 ++++ .../controllers/api/apiSettingsController.cpp | 15 ++++++++++++++- .../ui/controllers/api/apiSettingsController.h | 2 +- client/ui/models/api/apiCountryModel.cpp | 9 ++++++++- client/ui/qml/Components/ServersListView.qml | 2 +- client/ui/qml/Pages2/PageHome.qml | 2 +- .../PageSettingsApiAvailableCountries.qml | 2 +- .../qml/Pages2/PageSettingsApiNativeConfigs.qml | 10 +++++----- .../ui/qml/Pages2/PageSettingsApiServerInfo.qml | 12 +++++++++++- .../ui/qml/Pages2/PageSettingsServersList.qml | 2 +- 16 files changed, 90 insertions(+), 29 deletions(-) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 8dea8c0a..f32d525a 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -202,3 +202,13 @@ QQmlApplicationEngine *AmneziaApplication::qmlEngine() const { return m_engine; } + +QNetworkAccessManager *AmneziaApplication::networkManager() +{ + return m_nam; +} + +QClipboard *AmneziaApplication::getClipboard() +{ + return this->clipboard(); +} diff --git a/client/amnezia_application.h b/client/amnezia_application.h index b967f160..ea5f6f52 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -11,6 +11,7 @@ #else #include #endif +#include #include "core/controllers/coreController.h" #include "settings.h" @@ -41,10 +42,8 @@ public: #endif QQmlApplicationEngine *qmlEngine() const; - QNetworkAccessManager *manager() - { - return m_nam; - } + QNetworkAccessManager *networkManager(); + QClipboard *getClipboard(); private: QQmlApplicationEngine *m_engine {}; diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h index 2892f90b..41dd80ba 100644 --- a/client/core/api/apiDefs.h +++ b/client/core/api/apiDefs.h @@ -24,6 +24,7 @@ namespace apiDefs constexpr QLatin1String apiConfig("api_config"); constexpr QLatin1String stackType("stack_type"); + constexpr QLatin1String serviceType("service_type"); constexpr QLatin1String vpnKey("vpn_key"); diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp index 088fdba1..6166c512 100644 --- a/client/core/api/apiUtils.cpp +++ b/client/core/api/apiUtils.cpp @@ -27,15 +27,19 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec case apiDefs::ConfigSource::Telegram: { }; case apiDefs::ConfigSource::AmneziaGateway: { - constexpr QLatin1String premium("prem"); - constexpr QLatin1String free("free"); + constexpr QLatin1String stackPremium("prem"); + constexpr QLatin1String stackFree("free"); + + constexpr QLatin1String servicePremium("amnezia-premium"); + constexpr QLatin1String serviceFree("amnezia-free"); auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); auto stackType = apiConfigObject.value(apiDefs::key::stackType).toString(); + auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString(); - if (stackType == premium) { + if (serviceType == servicePremium || stackType == stackPremium) { return apiDefs::ConfigType::AmneziaPremiumV2; - } else if (stackType == free) { + } else if (serviceType == serviceFree || stackType == stackFree) { return apiDefs::ConfigType::AmneziaFreeV3; } } diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index d7a4b018..41fe61f8 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -47,7 +47,7 @@ ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBo request.setUrl(QString(endpoint).arg(m_gatewayEndpoint)); QNetworkReply *reply; - reply = amnApp->manager()->get(request); + reply = amnApp->networkManager()->get(request); QEventLoop wait; QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -61,7 +61,7 @@ ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBo if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { auto requestFunction = [&request, &responseBody](const QString &url) { request.setUrl(url); - return amnApp->manager()->get(request); + return amnApp->networkManager()->get(request); }; auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply, @@ -137,7 +137,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); + QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -151,7 +151,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, false)) { auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { request.setUrl(url); - return amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); + return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); }; auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, @@ -205,7 +205,7 @@ QStringList GatewayController::getProxyUrls() for (const auto &proxyStorageUrl : proxyStorageUrl) { request.setUrl(proxyStorageUrl); - reply = amnApp->manager()->get(request); + reply = amnApp->networkManager()->get(request); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); @@ -256,6 +256,8 @@ QStringList GatewayController::getProxyUrls() bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt) { + qDebug() << reply->error(); + qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { qDebug() << "Timeout occurred"; return true; diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 8c6d53c2..0fe304c2 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -1,13 +1,13 @@ #include "apiConfigsController.h" #include +#include #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" #include "core/api/apiDefs.h" #include "core/api/apiUtils.h" #include "core/controllers/gatewayController.h" -#include "core/networkUtilities.h" #include "core/qrCodeUtils.h" #include "ui/controllers/systemController.h" #include "version.h" @@ -76,7 +76,6 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, apiPayload[configKey::serverCountryCode] = serverCountryCode; apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); - apiPayload[configKey::uuid] = m_settings->getInstallationUuid(true); QByteArray responseBody; ErrorCode errorCode = gatewayController.post(QString("%1v1/native_config"), apiPayload, responseBody); @@ -124,6 +123,7 @@ void ApiConfigsController::prepareVpnKeyExport() auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); + m_vpnKey = vpnKey; vpnKey.replace("vpn://", ""); @@ -132,6 +132,12 @@ void ApiConfigsController::prepareVpnKeyExport() emit vpnKeyExportReady(); } +void ApiConfigsController::copyVpnKeyToClipboard() +{ + auto clipboard = amnApp->getClipboard(); + clipboard->setText(m_vpnKey); +} + bool ApiConfigsController::fillAvailableServices() { GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); @@ -288,7 +294,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) QByteArray requestBody = QJsonDocument(apiPayload).toJson(); - QNetworkReply *reply = amnApp->manager()->post(request, requestBody); + QNetworkReply *reply = amnApp->networkManager()->post(request, requestBody); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -477,3 +483,8 @@ int ApiConfigsController::getQrCodesCount() { return m_qrCodes.size(); } + +QString ApiConfigsController::getVpnKey() +{ + return m_vpnKey; +} diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index f8354942..26b02978 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -16,12 +16,14 @@ public: Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady) + Q_PROPERTY(QString vpnKey READ getVpnKey NOTIFY vpnKeyExportReady) public slots: bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); bool revokeNativeConfig(const QString &serverCountryCode); // bool exportVpnKey(const QString &fileName); void prepareVpnKeyExport(); + void copyVpnKeyToClipboard(); bool fillAvailableServices(); bool importServiceFromGateway(); @@ -58,8 +60,10 @@ private: QList getQrCodes(); int getQrCodesCount(); + QString getVpnKey(); QList m_qrCodes; + QString m_vpnKey; QSharedPointer m_serversModel; QSharedPointer m_apiServicesModel; diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 5f436436..53b6e5a8 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -1,5 +1,8 @@ #include "apiSettingsController.h" +#include +#include + #include "core/api/apiUtils.h" #include "core/controllers/gatewayController.h" @@ -35,8 +38,14 @@ ApiSettingsController::~ApiSettingsController() { } -bool ApiSettingsController::getAccountInfo() +bool ApiSettingsController::getAccountInfo(bool reload) { + if (reload) { + QEventLoop wait; + QTimer::singleShot(1000, &wait, &QEventLoop::quit); + wait.exec(); + } + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs); auto processedIndex = m_serversModel->getProcessedServerIndex(); @@ -62,6 +71,10 @@ bool ApiSettingsController::getAccountInfo() QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object(); m_apiAccountInfoModel->updateModel(accountInfo, serverConfig); + if (reload) { + updateApiCountryModel(); + } + return true; } diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h index 5374e899..e12e232a 100644 --- a/client/ui/controllers/api/apiSettingsController.h +++ b/client/ui/controllers/api/apiSettingsController.h @@ -17,7 +17,7 @@ public: ~ApiSettingsController(); public slots: - bool getAccountInfo(); + bool getAccountInfo(bool reload); void updateApiCountryModel(); signals: diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp index a43184b4..4ded6fed 100644 --- a/client/ui/models/api/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -8,6 +8,8 @@ namespace { Logger logger("ApiCountryModel"); + + constexpr QLatin1String countryConfig("country_config"); } ApiCountryModel::ApiCountryModel(QObject *parent) : QAbstractListModel(parent) @@ -27,7 +29,7 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const CountryInfo countryInfo = m_countries.at(index.row()); IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode); - bool isIssued = !issuedConfigInfo.lastDownloaded.isEmpty(); + bool isIssued = issuedConfigInfo.sourceType == countryConfig; switch (role) { case CountryCodeRole: { @@ -73,10 +75,15 @@ void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs) { beginResetModel(); + m_issuedConfigs.clear(); for (int i = 0; i < issuedConfigs.size(); i++) { IssuedConfigInfo issuedConfigInfo; QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject(); + if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != countryConfig) { + continue; + } + issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString(); issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString(); issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString(); diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index 3a6792ac..d0567a8c 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -116,7 +116,7 @@ ListView { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { PageController.showBusyIndicator(true) - let result = ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo(false) PageController.showBusyIndicator(false) if (!result) { return diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index d189044d..f7233a89 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -303,7 +303,7 @@ PageType { PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) } else { PageController.showBusyIndicator(true) - let result = ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo(false) PageController.showBusyIndicator(false) if (!result) { return diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index dfbc70b4..f70b646b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -93,7 +93,7 @@ PageType { actionButtonFunction: function() { PageController.showBusyIndicator(true) - let result = ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo(false) PageController.showBusyIndicator(false) if (!result) { return diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index b2ab2c05..7fa2b8f4 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -54,7 +54,6 @@ PageType { width: listView.width LabelWithButtonType { - id: telegramButton Layout.fillWidth: true Layout.topMargin: 6 @@ -67,8 +66,9 @@ PageType { moreOptionsDrawer.countryName = countryName moreOptionsDrawer.countryCode = countryCode moreOptionsDrawer.openTriggered() + } else { + issueConfig(countryCode) } - issueConfig(countryCode) } } @@ -166,11 +166,10 @@ PageType { PageController.showBusyIndicator(true) let result = ApiConfigsController.exportNativeConfig(countryCode, fileName) if (result) { - ApiSettingsController.getAccountInfo() + ApiSettingsController.getAccountInfo(true) } PageController.showBusyIndicator(false) - if (result) { PageController.showNotificationMessage(qsTr("Config file saved")) } @@ -181,7 +180,7 @@ PageType { PageController.showBusyIndicator(true) let result = ApiConfigsController.revokeNativeConfig(countryCode) if (result) { - ApiSettingsController.getAccountInfo() + ApiSettingsController.getAccountInfo(true) } PageController.showBusyIndicator(false) @@ -202,6 +201,7 @@ PageType { } else { revokeConfig(countryCode) } + moreOptionsDrawer.closeTriggered() } var noButtonFunction = function() { } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index befe7370..7080ad26 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -128,7 +128,17 @@ PageType { width: listView.width spacing: 0 + Connections { + target: ApiAccountInfoModel + + function onModelReset() { + delegateItem.rightText = ApiAccountInfoModel.data(contentKey) + } + } + LabelWithImageType { + id: delegateItem + Layout.fillWidth: true Layout.margins: 16 @@ -290,7 +300,7 @@ PageType { } else { PageController.showBusyIndicator(true) if (ApiConfigsController.deactivateDevice()) { - ApiSettingsController.getAccountInfo() + ApiSettingsController.getAccountInfo(true) } PageController.showBusyIndicator(false) } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index fcfcd114..554b6cbb 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -96,7 +96,7 @@ PageType { if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { PageController.showBusyIndicator(true) - let result = ApiSettingsController.getAccountInfo() + let result = ApiSettingsController.getAccountInfo(false) PageController.showBusyIndicator(false) if (!result) { return From 0bca78eca99dfca0f7daea871d88c3b98c9dd8e8 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Sat, 22 Feb 2025 18:59:31 -0800 Subject: [PATCH 25/36] Update Windows OpenSSL (#1426) * Update Windows OpenSSL to 3.0.16 and add shared library for QSslSocket plugin * chore: update link to submodule 3rd-prebuild --------- Co-authored-by: vladimir.kuznetsov --- client/3rd-prebuilt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index b714700a..e555c78b 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit b714700addf0c6e773367b13f2211f63110171a1 +Subproject commit e555c78bcf44070d5c88bcca54480732c9164f18 From 19fcddfdaf141129f812133c98432b09ae178eaa Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sun, 23 Feb 2025 14:26:04 +0700 Subject: [PATCH 26/36] chore: added 404 handling for revoke configs - added revoke before remove api server for premium v2 --- client/core/api/apiUtils.cpp | 3 +++ client/core/defs.h | 1 + client/core/errorstrings.cpp | 1 + .../ui/controllers/api/apiConfigsController.cpp | 15 ++++++++++++--- .../ui/qml/Pages2/PageSettingsApiServerInfo.qml | 9 ++++++--- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp index 6166c512..9f518b52 100644 --- a/client/core/api/apiUtils.cpp +++ b/client/core/api/apiUtils.cpp @@ -57,6 +57,7 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply) { const int httpStatusCodeConflict = 409; + const int httpStatusCodeNotFound = 404; if (!sslErrors.empty()) { qDebug().noquote() << sslErrors; @@ -75,6 +76,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &ssl qDebug() << httpStatusCode; if (httpStatusCode == httpStatusCodeConflict) { return amnezia::ErrorCode::ApiConfigLimitError; + } else if (httpStatusCode == httpStatusCodeNotFound) { + return amnezia::ErrorCode::ApiNotFoundError; } return amnezia::ErrorCode::ApiConfigDownloadError; } diff --git a/client/core/defs.h b/client/core/defs.h index 330ebc48..6c85c65d 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -110,6 +110,7 @@ namespace amnezia ApiConfigDecryptionError = 1106, ApiServicesMissingError = 1107, ApiConfigLimitError = 1108, + ApiNotFoundError = 1109, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index c45b1eaf..2b9182cf 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -67,6 +67,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; case (ErrorCode::ApiConfigLimitError): errorMessage = QObject::tr("The limit of allowed configurations per subscription has been exceeded"); break; + case (ErrorCode::ApiNotFoundError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break; // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 0fe304c2..27813f9c 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -110,7 +110,7 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode) QByteArray responseBody; ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_native_config"), apiPayload, responseBody); - if (errorCode != ErrorCode::NoError) { + if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { emit errorOccurred(errorCode); return false; } @@ -323,9 +323,14 @@ bool ApiConfigsController::deactivateDevice() { GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); - auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto serverIndex = m_serversModel->getProcessedServerIndex(); + auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) { + return true; + } + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); @@ -338,10 +343,14 @@ bool ApiConfigsController::deactivateDevice() QByteArray responseBody; ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody); - if (errorCode != ErrorCode::NoError) { + if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { emit errorOccurred(errorCode); return false; } + + serverConfigObject.remove(config_key::containers); + m_serversModel->editServer(serverConfigObject, serverIndex); + return true; } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 7080ad26..44ed6f4b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -164,7 +164,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 32 - visible: footer.isVisibleForAmneziaFree + visible: false //footer.isVisibleForAmneziaFree text: qsTr("Subscription key") rightImageSource: "qrc:/images/controls/chevron-right.svg" @@ -187,11 +187,12 @@ PageType { } DividerType { - visible: footer.isVisibleForAmneziaFree + visible: false //footer.isVisibleForAmneziaFree } LabelWithButtonType { Layout.fillWidth: true + Layout.topMargin: 32 visible: footer.isVisibleForAmneziaFree @@ -336,7 +337,9 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) } else { PageController.showBusyIndicator(true) - InstallController.removeProcessedServer() + if (ApiConfigsController.deactivateDevice()) { + InstallController.removeProcessedServer() + } PageController.showBusyIndicator(false) } } From 2b1ec9c693257dfbf0a5f40e5bd0c3b66a1014fd Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sun, 23 Feb 2025 14:39:18 +0700 Subject: [PATCH 27/36] chore: added log to see proxy decrypt errors --- client/core/controllers/gatewayController.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 41fe61f8..61c3d594 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -240,7 +240,7 @@ QStringList GatewayController::getProxyUrls() } } catch (...) { Utils::logException(); - qCritical() << "error loading private key from environment variables or decrypting payload"; + qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; return {}; } @@ -256,8 +256,6 @@ QStringList GatewayController::getProxyUrls() bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt) { - qDebug() << reply->error(); - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { qDebug() << "Timeout occurred"; return true; From abd7fdd19cf6fbb94d3a5f46ae5ab3a948fcbb84 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Mon, 24 Feb 2025 13:39:03 +0700 Subject: [PATCH 28/36] chore: minor ui fix --- .../qml/Pages2/PageSettingsApiAvailableCountries.qml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index f70b646b..43fbb160 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -44,19 +44,11 @@ PageType { } } - ListView { + ListViewType { id: menuContent - property bool isFocusable: true - anchors.fill: parent - ScrollBar.vertical: ScrollBarType {} - - clip: true - reuseItems: true - snapMode: ListView.SnapToItem - model: ApiCountryModel currentIndex: 0 From c28e1b468a03c671d6e8f1fb20ddca838eaa913b Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Mon, 24 Feb 2025 13:41:50 +0700 Subject: [PATCH 29/36] chore: bump version --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22141c9d..946162f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.3.3 +project(${PROJECT} VERSION 4.8.4.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2076) +set(APP_ANDROID_VERSION_CODE 2077) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 7ccbfa48bcedf3f84509bfc0b81eb1fc52741ced Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 25 Feb 2025 18:29:58 +0300 Subject: [PATCH 30/36] bugfix: fixed mobile controllers initialization (#1436) * bugfix: fixed mobile controllers initialization * chore: bump version --- CMakeLists.txt | 4 ++-- client/core/controllers/coreController.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 946162f8..3989f3b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.4.0 +project(${PROJECT} VERSION 4.8.4.1 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2077) +set(APP_ANDROID_VERSION_CODE 2078) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 3d71ce80..8d63974c 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -20,6 +20,9 @@ CoreController::CoreController(const QSharedPointer &vpnConnectio initControllers(); initSignalHandlers(); + initAndroidController(); + initAppleController(); + initNotificationHandler(); auto locale = m_settings->getAppLanguage(); From 728b48044c30f855375378bc485f506ac9f59d6b Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 28 Feb 2025 18:17:43 +0300 Subject: [PATCH 31/36] Merge pull request #1440 from amnezia-vpn/feature/subscription-settings-page feature/subscription settings page --- CMakeLists.txt | 4 +- client/core/controllers/coreController.cpp | 6 +- client/core/controllers/coreController.h | 4 +- client/core/controllers/gatewayController.cpp | 2 +- client/images/controls/monitor.svg | 5 + client/resources.qrc | 2 + .../controllers/api/apiConfigsController.cpp | 40 ++++- .../ui/controllers/api/apiConfigsController.h | 1 + .../controllers/api/apiSettingsController.cpp | 8 + .../controllers/api/apiSettingsController.h | 7 +- client/ui/controllers/pageController.h | 1 + client/ui/models/api/apiAccountInfoModel.cpp | 14 ++ client/ui/models/api/apiAccountInfoModel.h | 3 +- client/ui/models/api/apiCountryModel.cpp | 4 + client/ui/models/api/apiCountryModel.h | 3 +- client/ui/models/api/apiDevicesModel.cpp | 90 +++++++++++ client/ui/models/api/apiDevicesModel.h | 52 +++++++ client/ui/qml/Modules/Style/AmneziaStyle.qml | 1 + .../ui/qml/Pages2/PageSettingsApiDevices.qml | 100 ++++++++++++ .../Pages2/PageSettingsApiInstructions.qml | 1 - .../Pages2/PageSettingsApiNativeConfigs.qml | 3 + .../qml/Pages2/PageSettingsApiServerInfo.qml | 50 +++++- .../ui/qml/Pages2/PageSettingsApiSupport.qml | 146 ++++++++++-------- 23 files changed, 466 insertions(+), 81 deletions(-) create mode 100644 client/images/controls/monitor.svg create mode 100644 client/ui/models/api/apiDevicesModel.cpp create mode 100644 client/ui/models/api/apiDevicesModel.h create mode 100644 client/ui/qml/Pages2/PageSettingsApiDevices.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 3989f3b1..7c602249 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.4.1 +project(${PROJECT} VERSION 4.8.4.2 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2078) +set(APP_ANDROID_VERSION_CODE 2079) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 8d63974c..82232c99 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -93,6 +93,9 @@ void CoreController::initModels() m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this)); m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get()); + + m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get()); } void CoreController::initControllers() @@ -132,7 +135,8 @@ void CoreController::initControllers() m_systemController.reset(new SystemController(m_settings)); m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); - m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_settings)); + m_apiSettingsController.reset( + new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings)); m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 029044c4..700504af 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -25,8 +25,9 @@ #include "ui/models/protocols/ikev2ConfigModel.h" #endif #include "ui/models/api/apiAccountInfoModel.h" -#include "ui/models/api/apiServicesModel.h" #include "ui/models/api/apiCountryModel.h" +#include "ui/models/api/apiDevicesModel.h" +#include "ui/models/api/apiServicesModel.h" #include "ui/models/appSplitTunnelingModel.h" #include "ui/models/clientManagementModel.h" #include "ui/models/protocols/awgConfigModel.h" @@ -117,6 +118,7 @@ private: QSharedPointer m_apiServicesModel; QSharedPointer m_apiCountryModel; QSharedPointer m_apiAccountInfoModel; + QSharedPointer m_apiDevicesModel; QScopedPointer m_openVpnConfigModel; QScopedPointer m_shadowSocksConfigModel; diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 61c3d594..15776328 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -148,7 +148,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api QByteArray encryptedResponseBody = reply->readAll(); - if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, false)) { + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { request.setUrl(url); return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); diff --git a/client/images/controls/monitor.svg b/client/images/controls/monitor.svg new file mode 100644 index 00000000..1cdf57c2 --- /dev/null +++ b/client/images/controls/monitor.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/resources.qrc b/client/resources.qrc index cd44ca60..16071da0 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -229,6 +229,8 @@ ui/qml/Pages2/PageSettingsApiSupport.qml ui/qml/Pages2/PageSettingsApiInstructions.qml ui/qml/Pages2/PageSettingsApiNativeConfigs.qml + ui/qml/Pages2/PageSettingsApiDevices.qml + images/controls/monitor.svg images/flagKit/ZW.svg diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 27813f9c..d3c8747d 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -1,7 +1,7 @@ #include "apiConfigsController.h" -#include #include +#include #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" @@ -251,6 +251,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, authData); + // newServerConfig.insert( m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { @@ -354,6 +355,43 @@ bool ApiConfigsController::deactivateDevice() return true; } +bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverIndex = m_serversModel->getProcessedServerIndex(); + auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) { + return true; + } + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = serverCountryCode; + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + apiPayload[configKey::uuid] = uuid; + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { + emit errorOccurred(errorCode); + return false; + } + + if (uuid == m_settings->getInstallationUuid(true)) { + serverConfigObject.remove(config_key::containers); + m_serversModel->editServer(serverConfigObject, serverIndex); + } + + return true; +} + bool ApiConfigsController::isConfigValid() { int serverIndex = m_serversModel->getDefaultServerIndex(); diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h index 26b02978..2fe981e4 100644 --- a/client/ui/controllers/api/apiConfigsController.h +++ b/client/ui/controllers/api/apiConfigsController.h @@ -31,6 +31,7 @@ public slots: bool reloadServiceConfig = false); bool updateServiceFromTelegram(const int serverIndex); bool deactivateDevice(); + bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode); bool isConfigValid(); diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp index 53b6e5a8..737bfd1a 100644 --- a/client/ui/controllers/api/apiSettingsController.cpp +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -25,11 +25,13 @@ namespace ApiSettingsController::ApiSettingsController(const QSharedPointer &serversModel, const QSharedPointer &apiAccountInfoModel, const QSharedPointer &apiCountryModel, + const QSharedPointer &apiDevicesModel, const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_serversModel(serversModel), m_apiAccountInfoModel(apiAccountInfoModel), m_apiCountryModel(apiCountryModel), + m_apiDevicesModel(apiDevicesModel), m_settings(settings) { } @@ -73,6 +75,7 @@ bool ApiSettingsController::getAccountInfo(bool reload) if (reload) { updateApiCountryModel(); + updateApiDevicesModel(); } return true; @@ -83,3 +86,8 @@ void ApiSettingsController::updateApiCountryModel() m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); m_apiCountryModel->updateIssuedConfigsInfo(m_apiAccountInfoModel->getIssuedConfigsInfo()); } + +void ApiSettingsController::updateApiDevicesModel() +{ + m_apiDevicesModel->updateModel(m_apiAccountInfoModel->getIssuedConfigsInfo()); +} diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h index e12e232a..afe9a570 100644 --- a/client/ui/controllers/api/apiSettingsController.h +++ b/client/ui/controllers/api/apiSettingsController.h @@ -5,6 +5,7 @@ #include "ui/models/api/apiAccountInfoModel.h" #include "ui/models/api/apiCountryModel.h" +#include "ui/models/api/apiDevicesModel.h" #include "ui/models/servers_model.h" class ApiSettingsController : public QObject @@ -12,13 +13,14 @@ class ApiSettingsController : public QObject Q_OBJECT public: ApiSettingsController(const QSharedPointer &serversModel, const QSharedPointer &apiAccountInfoModel, - const QSharedPointer &apiCountryModel, const std::shared_ptr &settings, - QObject *parent = nullptr); + const QSharedPointer &apiCountryModel, const QSharedPointer &apiDevicesModel, + const std::shared_ptr &settings, QObject *parent = nullptr); ~ApiSettingsController(); public slots: bool getAccountInfo(bool reload); void updateApiCountryModel(); + void updateApiDevicesModel(); signals: void errorOccurred(ErrorCode errorCode); @@ -27,6 +29,7 @@ private: QSharedPointer m_serversModel; QSharedPointer m_apiAccountInfoModel; QSharedPointer m_apiCountryModel; + QSharedPointer m_apiDevicesModel; std::shared_ptr m_settings; }; diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 9ec93cc0..60621414 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -36,6 +36,7 @@ namespace PageLoader PageSettingsApiSupport, PageSettingsApiInstructions, PageSettingsApiNativeConfigs, + PageSettingsApiDevices, PageServiceSftpSettings, PageServiceTorWebsiteSettings, diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index 055c2347..cdb9dfe8 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -58,6 +58,19 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const case IsComponentVisibleRole: { return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2; } + case HasExpiredWorkerRole: { + for (int i = 0; i < m_issuedConfigsInfo.size(); i++) { + QJsonObject issuedConfigObject = m_issuedConfigsInfo.at(i).toObject(); + + auto lastDownloaded = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::lastDownloaded).toString()); + auto workerLastUpdated = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString()); + + if (lastDownloaded < workerLastUpdated) { + return true; + } + } + return false; + } } return QVariant(); @@ -124,6 +137,7 @@ QHash ApiAccountInfoModel::roleNames() const roles[ConnectedDevicesRole] = "connectedDevices"; roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsComponentVisibleRole] = "isComponentVisible"; + roles[HasExpiredWorkerRole] = "hasExpiredWorker"; return roles; } diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index e50ea5f5..44eb7ee6 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -17,7 +17,8 @@ public: ConnectedDevicesRole, ServiceDescriptionRole, EndDateRole, - IsComponentVisibleRole + IsComponentVisibleRole, + HasExpiredWorkerRole }; explicit ApiAccountInfoModel(QObject *parent = nullptr); diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp index 4ded6fed..12f4658e 100644 --- a/client/ui/models/api/apiCountryModel.cpp +++ b/client/ui/models/api/apiCountryModel.cpp @@ -44,6 +44,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const case IsIssuedRole: { return isIssued; } + case IsWorkerExpiredRole: { + return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated; + } } return QVariant(); @@ -114,5 +117,6 @@ QHash ApiCountryModel::roleNames() const roles[CountryCodeRole] = "countryCode"; roles[CountryImageCodeRole] = "countryImageCode"; roles[IsIssuedRole] = "isIssued"; + roles[IsWorkerExpiredRole] = "isWorkerExpired"; return roles; } diff --git a/client/ui/models/api/apiCountryModel.h b/client/ui/models/api/apiCountryModel.h index e57ec0dd..08ac3685 100644 --- a/client/ui/models/api/apiCountryModel.h +++ b/client/ui/models/api/apiCountryModel.h @@ -14,7 +14,8 @@ public: CountryNameRole = Qt::UserRole + 1, CountryCodeRole, CountryImageCodeRole, - IsIssuedRole + IsIssuedRole, + IsWorkerExpiredRole }; explicit ApiCountryModel(QObject *parent = nullptr); diff --git a/client/ui/models/api/apiDevicesModel.cpp b/client/ui/models/api/apiDevicesModel.cpp new file mode 100644 index 00000000..6c0d60d0 --- /dev/null +++ b/client/ui/models/api/apiDevicesModel.cpp @@ -0,0 +1,90 @@ +#include "apiDevicesModel.h" + +#include + +#include "core/api/apiDefs.h" +#include "logger.h" + +namespace +{ + Logger logger("ApiDevicesModel"); + + constexpr QLatin1String gatewayAccount("gateway_account"); +} + +ApiDevicesModel::ApiDevicesModel(std::shared_ptr settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent) +{ +} + +int ApiDevicesModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_issuedConfigs.size(); +} + +QVariant ApiDevicesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) + return QVariant(); + + IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.at(index.row()); + + switch (role) { + case OsVersionRole: { + return issuedConfigInfo.osVersion; + } + case SupportTagRole: { + return issuedConfigInfo.installationUuid; + } + case CountryCodeRole: { + return issuedConfigInfo.countryCode; + } + case LastUpdateRole: { + return QDateTime::fromString(issuedConfigInfo.lastDownloaded, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } + case IsCurrentDeviceRole: { + return issuedConfigInfo.installationUuid == m_settings->getInstallationUuid(false); + } + } + + return QVariant(); +} + +void ApiDevicesModel::updateModel(const QJsonArray &issuedConfigs) +{ + beginResetModel(); + + m_issuedConfigs.clear(); + for (int i = 0; i < issuedConfigs.size(); i++) { + IssuedConfigInfo issuedConfigInfo; + QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject(); + + if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != gatewayAccount) { + continue; + } + + issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString(); + issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString(); + issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString(); + issuedConfigInfo.sourceType = issuedConfigObject.value(apiDefs::key::sourceType).toString(); + issuedConfigInfo.osVersion = issuedConfigObject.value(apiDefs::key::osVersion).toString(); + + issuedConfigInfo.countryName = issuedConfigObject.value(apiDefs::key::serverCountryName).toString(); + issuedConfigInfo.countryCode = issuedConfigObject.value(apiDefs::key::serverCountryCode).toString(); + + m_issuedConfigs.push_back(issuedConfigInfo); + } + + endResetModel(); +} + +QHash ApiDevicesModel::roleNames() const +{ + QHash roles; + roles[OsVersionRole] = "osVersion"; + roles[SupportTagRole] = "supportTag"; + roles[CountryCodeRole] = "countryCode"; + roles[LastUpdateRole] = "lastUpdate"; + roles[IsCurrentDeviceRole] = "isCurrentDevice"; + return roles; +} diff --git a/client/ui/models/api/apiDevicesModel.h b/client/ui/models/api/apiDevicesModel.h new file mode 100644 index 00000000..e6a59dba --- /dev/null +++ b/client/ui/models/api/apiDevicesModel.h @@ -0,0 +1,52 @@ +#ifndef APIDEVICESMODEL_H +#define APIDEVICESMODEL_H + +#include +#include +#include + +#include "settings.h" + +class ApiDevicesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + OsVersionRole = Qt::UserRole + 1, + SupportTagRole, + CountryCodeRole, + LastUpdateRole, + IsCurrentDeviceRole + }; + + explicit ApiDevicesModel(std::shared_ptr settings, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void updateModel(const QJsonArray &issuedConfigs); + +protected: + QHash roleNames() const override; + +private: + struct IssuedConfigInfo + { + QString installationUuid; + QString workerLastUpdated; + QString lastDownloaded; + QString sourceType; + QString osVersion; + + QString countryName; + QString countryCode; + }; + + QVector m_issuedConfigs; + + std::shared_ptr m_settings; +}; +#endif // APIDEVICESMODEL_H diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index f54fefce..4e2e80f0 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -27,5 +27,6 @@ QtObject { readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8) readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) readonly property color pearlGray: '#EAEAEC' + readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26) } } diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml new file mode 100644 index 00000000..5cc21d07 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -0,0 +1,100 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtCore + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + ListViewType { + id: listView + + anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 24 + + model: ApiDevicesModel + + header: ColumnLayout { + width: listView.width + + BackButtonType { + id: backButton + } + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Connected devices") + descriptionText: qsTr("To manage connected devices") + } + + WarningType { + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.fillWidth: true + + textString: qsTr("You can find the identifier on the Support tab or, for older versions of the app, " + + "by tapping '+' and then the three dots at the top of the page.") + + iconPath: "qrc:/images/controls/alert-circle.svg" + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 6 + + text: osVersion + (isCurrentDevice ? qsTr(" (current device)") : "") + descriptionText: qsTr("Support tag: ") + "\n" + supportTag + "\n" + qsTr("Last updated: ") + lastUpdate + rightImageSource: "qrc:/images/controls/trash.svg" + + clickedFunction: function() { + var headerText = qsTr("Deactivate the subscription on selected device") + var descriptionText = qsTr("The next time the “Connect” button is pressed, the device will be activated again") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + Qt.callLater(deactivateExternalDevice, supportTag, countryCode) + } + var noButtonFunction = function() { + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } + + DividerType {} + } + } + + function deactivateExternalDevice(supportTag, countryCode) { + PageController.showBusyIndicator(true) + if (ApiConfigsController.deactivateExternalDevice(supportTag, countryCode)) { + ApiSettingsController.getAccountInfo(true) + } + PageController.showBusyIndicator(false) + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml index 70a0df74..3651407b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -107,7 +107,6 @@ PageType { width: listView.width LabelWithButtonType { - id: telegramButton Layout.fillWidth: true Layout.topMargin: 6 diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index 7fa2b8f4..a1cc1fb8 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -58,6 +58,9 @@ PageType { Layout.topMargin: 6 text: countryName + descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : "" + descriptionColor: AmneziaStyle.color.vibrantRed + leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg" diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 44ed6f4b..7d089639 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -28,7 +28,7 @@ PageType { readonly property string title: qsTr("Subscription status") readonly property string contentKey: "subscriptionStatus" - readonly property string objectImageSource: "qrc:/images/controls/map-pin.svg" + readonly property string objectImageSource: "qrc:/images/controls/info.svg" } QtObject { @@ -44,7 +44,7 @@ PageType { readonly property string title: qsTr("Connected devices") readonly property string contentKey: "connectedDevices" - readonly property string objectImageSource: "qrc:/images/controls/gauge.svg" + readonly property string objectImageSource: "qrc:/images/controls/monitor.svg" } property var processedServer @@ -158,11 +158,28 @@ PageType { readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible") + WarningType { + id: warning + + Layout.topMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.fillWidth: true + + backGroundColor: AmneziaStyle.color.translucentRichBrown + + textString: qsTr("Configurations have been updated for some countries. Download and install the updated configuration files") + + iconPath: "qrc:/images/controls/alert-circle.svg" + + visible: ApiAccountInfoModel.data("hasExpiredWorker") + } + LabelWithButtonType { id: vpnKey Layout.fillWidth: true - Layout.topMargin: 32 + Layout.topMargin: warning.visible ? 16 : 32 visible: false //footer.isVisibleForAmneziaFree @@ -192,7 +209,7 @@ PageType { LabelWithButtonType { Layout.fillWidth: true - Layout.topMargin: 32 + Layout.topMargin: warning.visible ? 16 : 32 visible: footer.isVisibleForAmneziaFree @@ -211,6 +228,26 @@ PageType { visible: footer.isVisibleForAmneziaFree } + LabelWithButtonType { + Layout.fillWidth: true + + visible: footer.isVisibleForAmneziaFree + + text: qsTr("Connected devices") + + descriptionText: qsTr("To manage connected devices") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ApiSettingsController.updateApiDevicesModel() + PageController.goToPage(PageEnum.PageSettingsApiDevices) + } + } + + DividerType { + visible: footer.isVisibleForAmneziaFree + } + LabelWithButtonType { Layout.fillWidth: true Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32 @@ -292,12 +329,13 @@ PageType { clickedFunc: function() { var headerText = qsTr("Deactivate the subscription on this device?") + var descriptionText = qsTr("The next time the “Connect” button is pressed, the device will be activated again") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { - PageController.showNotificationMessage(qsTr("The next time the “Connect” button is pressed, the device will be activated again")) + PageController.showNotificationMessage(qsTr("Cannot deactivate subscription during active connection")) } else { PageController.showBusyIndicator(true) if (ApiConfigsController.deactivateDevice()) { @@ -309,7 +347,7 @@ PageType { var noButtonFunction = function() { } - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml index 64921a61..424e10c5 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSupport.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -16,92 +16,110 @@ import "../Components" PageType { id: root - ColumnLayout { - id: backButtonLayout + QtObject { + id: telegram - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + readonly property string title: qsTr("Telegram") + readonly property string description: "@" + ApiAccountInfoModel.getTelegramBotLink() + readonly property string link: "https://t.me/" + ApiAccountInfoModel.getTelegramBotLink() + } + QtObject { + id: techSupport + + readonly property string title: qsTr("For technical support") + readonly property string description: qsTr("support@amnezia.org") + readonly property string link: "mailto:support@amnezia.org" + } + + QtObject { + id: paymentSupport + + readonly property string title: qsTr("For payment issues") + readonly property string description: qsTr("help@vpnpay.io") + readonly property string link: "mailto:help@vpnpay.io" + } + + QtObject { + id: site + + readonly property string title: qsTr("Site") + readonly property string description: qsTr("amnezia.org") + readonly property string link: LanguageModel.getCurrentSiteUrl() + } + + property list supportModel: [ + telegram, + techSupport, + paymentSupport, + site + ] + + ListViewType { + id: listView + + anchors.fill: parent anchors.topMargin: 20 + anchors.bottomMargin: 24 - BackButtonType { - id: backButton - } + model: supportModel + header: ColumnLayout { + width: listView.width - HeaderType { - id: header + BackButtonType { + id: backButton + } - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 + HeaderType { + id: header - headerText: qsTr("Support") - descriptionText: qsTr("Our technical support specialists are ready to help you at any time") - } + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 - LabelWithButtonType { - Layout.fillWidth: true - - readonly property string telegramBotLink: ApiAccountInfoModel.getTelegramBotLink() - - text: qsTr("Telegram") - descriptionText: "@" + telegramBotLink - rightImageSource: "qrc:/images/controls/external-link.svg" - - clickedFunction: function() { - Qt.openUrlExternally("https://t.me/" + telegramBotLink) + headerText: qsTr("Support") + descriptionText: qsTr("Our technical support specialists are ready to help you at any time") } } - DividerType {} + delegate: ColumnLayout { + width: listView.width - LabelWithButtonType { - Layout.fillWidth: true - - text: qsTr("Mail") - descriptionText: qsTr("support@amnezia.org") - rightImageSource: "qrc:/images/controls/external-link.svg" - - clickedFunction: function() { - Qt.openUrlExternally(qsTr("mailto:support@amnezia.org")) + LabelWithButtonType { + Layout.fillWidth: true + text: title + descriptionText: description + rightImageSource: "qrc:/images/controls/external-link.svg" + clickedFunction: function() { + Qt.openUrlExternally(link) + } } + DividerType {} } - DividerType {} - LabelWithButtonType { - Layout.fillWidth: true + footer: ColumnLayout { + width: listView.width - text: qsTr("Site") - descriptionText: qsTr("amnezia.org") - rightImageSource: "qrc:/images/controls/external-link.svg" + LabelWithButtonType { + id: supportUuid + Layout.fillWidth: true - clickedFunction: function() { - Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) - } - } + text: qsTr("Support tag") + descriptionText: SettingsController.getInstallationUuid() - DividerType {} + descriptionOnTop: true - LabelWithButtonType { - id: supportUuid - Layout.fillWidth: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray - text: qsTr("Support tag") - descriptionText: SettingsController.getInstallationUuid() - - descriptionOnTop: true - - rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: AmneziaStyle.color.paleGray - - clickedFunction: function() { - GC.copyToClipBoard(descriptionText) - PageController.showNotificationMessage(qsTr("Copied")) - if (!GC.isMobile()) { - this.rightButton.forceActiveFocus() + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } } } } From 678bfffe492ed89911a9dcacfb125e82aa1ce1be Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 4 Mar 2025 13:33:35 +0700 Subject: [PATCH 32/36] chore: minor ui fixes (#1446) * chore: minor ui fixes * chore: update ru translation file * bugfix: fixed config update by ttl for gateway configs * bugfix: fixed proxy bypassing * chore: minor ui fixes * chore: update ru translation file * chore: bump version --- CMakeLists.txt | 4 +- client/core/controllers/gatewayController.cpp | 74 +- client/translations/amneziavpn_ru_RU.ts | 1215 ++++++++++++----- .../controllers/api/apiConfigsController.cpp | 2 +- client/ui/models/api/apiAccountInfoModel.cpp | 4 +- .../PageSettingsApiAvailableCountries.qml | 2 +- .../ui/qml/Pages2/PageSettingsApiDevices.qml | 13 +- .../Pages2/PageSettingsApiInstructions.qml | 2 +- .../Pages2/PageSettingsApiNativeConfigs.qml | 18 +- .../qml/Pages2/PageSettingsApiServerInfo.qml | 22 +- .../ui/qml/Pages2/PageSettingsApiSupport.qml | 8 +- 11 files changed, 963 insertions(+), 401 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c602249..4692f28f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.4.2 +project(${PROJECT} VERSION 4.8.4.3 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2079) +set(APP_ANDROID_VERSION_CODE 2080) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 15776328..ba4a8d36 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -157,12 +157,12 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, this](QNetworkReply *nestedReply, const QList &nestedSslErrors) { encryptedResponseBody = nestedReply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { + reply = nestedReply; + if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { sslErrors = nestedSslErrors; - reply = nestedReply; - return true; + return false; } - return false; + return true; }; bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); @@ -212,45 +212,45 @@ QStringList GatewayController::getProxyUrls() wait.exec(); if (reply->error() == QNetworkReply::NetworkError::NoError) { - break; - } - reply->deleteLater(); - } + auto encryptedResponseBody = reply->readAll(); + reply->deleteLater(); - auto encryptedResponseBody = reply->readAll(); - reply->deleteLater(); + EVP_PKEY *privateKey = nullptr; + QByteArray responseBody; + try { + if (!m_isDevEnvironment) { + QCryptographicHash hash(QCryptographicHash::Sha512); + hash.addData(key); + QByteArray hashResult = hash.result().toHex(); - EVP_PKEY *privateKey = nullptr; - QByteArray responseBody; - try { - if (!m_isDevEnvironment) { - QCryptographicHash hash(QCryptographicHash::Sha512); - hash.addData(key); - QByteArray hashResult = hash.result().toHex(); + QByteArray key = QByteArray::fromHex(hashResult.left(64)); + QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); - QByteArray key = QByteArray::fromHex(hashResult.left(64)); - QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); + QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); - QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); + QSimpleCrypto::QBlockCipher blockCipher; + responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); + } else { + responseBody = encryptedResponseBody; + } + } catch (...) { + Utils::logException(); + qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; + continue; + } - QSimpleCrypto::QBlockCipher blockCipher; - responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); + auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); + + QStringList endpoints; + for (const auto &endpoint : endpointsArray) { + endpoints.push_back(endpoint.toString()); + } + return endpoints; } else { - responseBody = encryptedResponseBody; + reply->deleteLater(); } - } catch (...) { - Utils::logException(); - qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; - return {}; } - - auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); - - QStringList endpoints; - for (const auto &endpoint : endpointsArray) { - endpoints.push_back(endpoint.toString()); - } - return endpoints; + return {}; } bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, @@ -262,7 +262,7 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray } else if (responseBody.contains("html")) { qDebug() << "The response contains an html tag"; return true; - } else if (checkEncryption) { + } else if (reply->error() == QNetworkReply::NetworkError::NoError && checkEncryption) { try { QSimpleCrypto::QBlockCipher blockCipher; static_cast(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt)); @@ -296,7 +296,7 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (!replyProcessingFunction(reply, sslErrors)) { + if (replyProcessingFunction(reply, sslErrors)) { break; } } diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 1b4d441f..1aa62f89 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -4,60 +4,129 @@ AdLabel - + Amnezia Premium - for access to any website Amnezia Premium - для доступа к любым сайтам + + ApiAccountInfoModel + + + + Active + + + + + Inactive + + + + + %1 out of %2 + + + + + Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to 200 Mbps + + + + + Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and more. YouTube is not included in the free plan. + + + + + amnezia_free_support_bot + + + + + amnezia_premium_support_bot + + + + + ApiConfigsController + + + %1 installed successfully. + %1 успешно установлен. + + + + API config reloaded + Конфигурация API перезагружена + + + + Successfully changed the country of connection to %1 + Изменение страны подключения на %1 + + ApiServicesModel - Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с + Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с - VPN to access blocked sites in regions with high levels of Internet censorship. - VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. + VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. - + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> <p><a style="color: #EB5757;">Недоступно в вашем регионе. Если у вас включен VPN, отключите его, вернитесь на предыдущий экран и попробуйте снова.</a> - Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - Amnezia Premium — классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры. + Amnezia Premium — классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры. - Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры + Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры - + + Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. Works for any sites with no restrictions. Speed up to %1 MBit/s. Unlimited traffic. + + + + + + AmneziaFree provides free unlimited access to a basic set of web sites, such as Facebook, Instagram, Twitter (X), Discord, Telegram, and others. YouTube is not included in the free plan. + + + + + Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. Works for any sites with no restrictions. + + + + %1 MBit/s - + %1 days %1 дней - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> Через VPN будут открываться только популярные сайты, заблокированные в вашем регионе, такие как Instagram, Facebook, Twitter и другие. Остальные сайты будут открываться с вашего реального IP-адреса, <a href="%1/free" style="color: #FBB26A;">подробности на сайте.</a> - + Free Бесплатно - + %1 $/month %1 $/месяц @@ -96,62 +165,59 @@ ConnectionController - VPN Protocols is not installed. Please install VPN container at first - VPN-протоколы не установлены. + VPN-протоколы не установлены. Пожалуйста, установите протокол - + Connecting... Подключение... - + Connected Подключено - + Preparing... Подготовка... - + Settings updated successfully, reconnnection... Настройки успешно обновлены, переподключение... - + Settings updated successfully Настройки успешно обновлены - The selected protocol is not supported on the current platform - Выбранный протокол не поддерживается на данном устройстве + Выбранный протокол не поддерживается на данном устройстве - unable to create configuration - не удалось создать конфигурацию + не удалось создать конфигурацию - + Reconnecting... Переподключение... - - - - + + + + Connect Подключиться - + Disconnecting... Отключение... @@ -274,12 +340,12 @@ Can't be disabled for current server Неверный файл конфигурации - + Scanned %1 of %2. Отсканировано %1 из %2. - + In the imported configuration, potentially dangerous lines were found: В импортированной конфигурации были обнаружены потенциально опасные строки: @@ -287,88 +353,85 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 успешно установлен. - + %1 is already installed on the server. %1 уже установлен на сервер. - + Added containers that were already installed on the server Добавлены сервисы и протоколы, которые были ранее установлены на сервер - + Already installed containers were found on the server. All installed containers have been added to the application На сервере обнаружены установленные протоколы и сервисы. Все они были добавлены в приложение - + Settings updated successfully Настройки успешно обновлены - + Server '%1' was rebooted Сервер '%1' был перезагружен - + Server '%1' was removed Сервер '%1' был удален - + All containers from server '%1' have been removed Все протоколы и сервисы были удалены с сервера '%1' - + %1 has been removed from the server '%2' %1 был удален с сервера '%2' - + Api config removed Конфигурация API удалена - + %1 cached profile cleared %1 закэшированный профиль очищен - + Please login as the user Пожалуйста, войдите в систему от имени пользователя - + Server added successfully Сервер успешно добавлен - %1 installed successfully. - %1 успешно установлен. + %1 успешно установлен. - API config reloaded - Конфигурация API перезагружена + Конфигурация API перезагружена - Successfully changed the country of connection to %1 - Изменение страны подключения на %1 + Изменение страны подключения на %1 @@ -480,12 +543,12 @@ Already installed containers were found on the server. All installed containers Раздельное туннелирование выключено - + VPN protocol VPN-протокол - + Servers Серверы @@ -1386,22 +1449,22 @@ Already installed containers were found on the server. All installed containers Приложение - + Backup Резервное копирование - + About AmneziaVPN Об AmneziaVPN - + Dev console - + Close application Закрыть приложение @@ -1409,7 +1472,7 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Поддержите Amnezia @@ -1418,12 +1481,12 @@ Already installed containers were found on the server. All installed containers Показать другие способы на GitHub - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia — это бесплатное приложение с открытым исходным кодом. Поддержите разработчиков, если оно вам нравится. - + Contacts Контакты @@ -1457,32 +1520,36 @@ Already installed containers were found on the server. All installed containers Для отзывов и сообщений об ошибках - Copied - Скопировано + Скопировано - + + mailto:support@amnezia.org + + + + GitHub GitHub - + Discover the source code - + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт - + Visit official website @@ -1491,109 +1558,480 @@ Already installed containers were found on the server. All installed containers https://amnezia.org - + Software version: %1 Версия ПО: %1 - + Check for updates Проверить обновления - + Privacy Policy Политика конфиденциальности + + PageSettingsApiAvailableCountries + + + Location for connection + + + + + Unable change server location while there is an active connection + Невозможно изменить локацию во время активного соединения + + + + PageSettingsApiDevices + + + Active devices + + + + + Manage currently connected devices + + + + + You can find the identifier on the Support tab or, for older versions of the app, by tapping '+' and then the three dots at the top of the page. + + + + + (current device) + + + + + Support tag: + + + + + Last updated: + + + + + Cannot unlink device during active connection + + + + + Are you sure you want to unlink this device? + + + + + This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect. + + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + PageSettingsApiInstructions + + + Windows + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#windows + + + + + macOS + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#macos + + + + + Android + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#android + + + + + AndroidTV + + + + + https://docs.amnezia.org/ru/documentation/instructions/android_tv_connect/ + + + + + iOS + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#ios + + + + + Linux + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#linux + + + + + Routers + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#routers + + + + + How to connect on another device + + + + + Setup guides on the Amnezia website + + + PageSettingsApiLanguageList - Unable change server location while there is an active connection - Невозможно изменить локацию во время активного соединения + Невозможно изменить локацию во время активного соединения + + + + PageSettingsApiNativeConfigs + + + Save AmneziaVPN config + Сохранить конфигурацию AmneziaVPN + + + + Configuration files + + + + + For router setup or the AmneziaWG app + + + + + The configuration needs to be reissued + + + + + configuration file + + + + + Generate a new configuration file + + + + + The previously created one will stop working + + + + + Revoke the current configuration file + + + + + Config file saved + + + + + The config has been revoked + + + + + Generate a new %1 configuration file? + + + + + Revoke the current %1 configuration file? + + + + + Your previous configuration file will no longer work, and it will not be possible to connect using it + + + + + Download + + + + + Continue + Продолжить + + + + Cancel + Отменить PageSettingsApiServerInfo - For the region - Для региона + Для региона - Price - Цена + Цена - Work period - Период работы + Период работы - + Valid until - Speed - Скорость + Скорость - - Support tag + Copied + Скопировано + + + + Subscription status - - Copied - Скопировано + + Active connections + - + + Configurations have been updated for some countries. Download and install the updated configuration files + + + + + Subscription key + + + + + Amnezia Premium subscription key + + + + + Save VPN key to file + + + + + Copy VPN key + + + + + Configuration files + + + + + Manage configuration files + + + + + Active devices + + + + + Manage currently connected devices + + + + + Support + + + + + How to connect on another device + + + + Reload API config Перезагрузить конфигурацию API - + Reload API config? Перезагрузить конфигурацию API? - - + + + Continue Продолжить - - + + + Cancel Отменить - + Cannot reload API config during active connection Невозможно перзагрузить API конфигурацию при активном соединении - + + Unlink this device + + + + + Are you sure you want to unlink this device? + + + + + This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect. + + + + + Cannot unlink device during active connection + + + + Remove from application Удалить из приложения - + Remove from application? Удалить из приложения? - + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения + + PageSettingsApiSupport + + + Telegram + + + + + Email Support + + + + + support@amnezia.org + + + + + Email Billing & Orders + + + + + help@vpnpay.io + + + + + Website + Веб-сайт + + + + amnezia.org + + + + + Support + + + + + Our technical support specialists are available to assist you at any time + + + + + Support tag + + + + + Copied + Скопировано + + PageSettingsAppSplitTunneling @@ -2009,20 +2447,20 @@ Already installed containers were found on the server. All installed containers Открыть папку с логами - - + + Save Сохранить - - + + Logs files (*.log) Файлы логов (*.log) - - + + Logs file saved Файл с логами сохранен @@ -2056,32 +2494,32 @@ Already installed containers were found on the server. All installed containers Логи очищены - + Client logs - + AmneziaVPN logs - + Open logs folder Открыть папку с логами - + Export logs Сохранить логи - + Service logs - + AmneziaVPN-service logs @@ -2121,93 +2559,93 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue Продолжить - - - - + + + + Cancel Отменить - + Check the server for previously installed Amnezia services Проверить сервер на наличие ранее установленных сервисов Amnezia - + Add them to the application if they were not displayed Добавить их в приложение, если они не отображаются - + Reboot server Перезагрузить сервер - + Do you want to reboot the server? Вы уверены, что хотите перезагрузить сервер? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? Процесс перезагрузки может занять около 30 секунд. Вы уверены, что хотите продолжить? - + Cannot reboot server during active connection Невозможно перезагрузить сервер во время активного соединения - + Do you want to remove the server from application? Вы уверены, что хотите удалить сервер из приложения? - + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения - + Do you want to clear server from Amnezia software? Вы хотите очистить сервер от всех сервисов Amnezia? - + All users whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Cannot clear server from Amnezia software during active connection Невозможно очистить сервер от сервисов Amnezia во время активного соединения - + Reset API config Сбросить конфигурацию API - + Do you want to reset API config? Вы хотите сбросить конфигурацию API? - + Cannot reset API config during active connection Невозможно сбросить конфигурацию API во время активного соединения - + Remove server from application Удалить сервер из приложения @@ -2216,12 +2654,12 @@ Already installed containers were found on the server. All installed containers Удалить сервер? - + All installed AmneziaVPN services will still remain on the server. Все установленные сервисы и протоколы Amnezia останутся на сервере. - + Clear server from Amnezia software Очистить сервер от протоколов и сервисов Amnezia @@ -2237,32 +2675,29 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - Subscription is valid until - Подписка заканчивается через + Подписка заканчивается через - Server name - Имя сервера + Имя сервера - Save - Сохранить + Сохранить - + Protocols Протоколы - + Services Сервисы - + Management Управление @@ -2394,17 +2829,17 @@ Already installed containers were found on the server. All installed containers Режим - + Remove Удалить - + Continue Продолжить - + Cancel Отменить @@ -2413,7 +2848,7 @@ Already installed containers were found on the server. All installed containers Сайт или IP - + Import / Export Sites Импорт/экспорт сайтов @@ -2428,12 +2863,12 @@ Already installed containers were found on the server. All installed containers Невозможно изменить настройки раздельного туннелирования во время активного соединения - + website or IP веб-сайт или IP - + Import Импорт @@ -2449,29 +2884,29 @@ Already installed containers were found on the server. All installed containers - - + + Sites files (*.json) Файлы сайтов (*.json) - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим @@ -2545,7 +2980,7 @@ It's okay as long as it's from someone you trust. Что у вас есть? - + File with connection settings Файл с настройками подключения @@ -2604,80 +3039,65 @@ It's okay as long as it's from someone you trust. Другие варианты подключения - + Site Amnezia Сайт Amnezia - + VPN by Amnezia VPN от Amnezia - + Connect to classic paid and free VPN services from Amnezia Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server Настроить VPN на собственном сервере - + Restore from backup Восстановить из резервной копии - + - + Open backup file Открыть резервную копию - + Backup files (*.backup) Файлы резервных копий (*.backup) - - - - - - + Open config file Открыть файл с конфигурацией - + QR code QR-код - - - - - - + I have nothing У меня ничего нет - - - - - Key as text Ключ в виде текста @@ -2690,7 +3110,7 @@ It's okay as long as it's from someone you trust. Подключение к серверу - + Server IP address [:port] IP-адрес[:порт] сервера @@ -2703,7 +3123,7 @@ It's okay as long as it's from someone you trust. Password / SSH private key - + Continue Продолжить @@ -2713,7 +3133,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам - + Enter the address in the format 255.255.255.255:88 Введите адрес в формате 255.255.255.255:88 @@ -2727,47 +3147,47 @@ and will not be shared or disclosed to the Amnezia or any third parties Настроить ваш сервер - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username Имя пользователя SSH - + Password or SSH private key Пароль или закрытый ключ SSH - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам - + How to run your VPN server Как создать VPN на собственном сервере - + Where to get connection data, step-by-step instructions for buying a VPS Где взять данные для подключения, пошаговые инструкции по покупке VPS - + Ip address cannot be empty Поле с IP-адресом не может быть пустым - + Login cannot be empty Поле с логином не может быть пустым - + Password/private key cannot be empty Поле с паролем/закрытым ключом не может быть пустым @@ -2775,17 +3195,26 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - What is the level of internet control in your region? - Какой уровень контроля над интернетом в вашем регионе? + Какой уровень контроля над интернетом в вашем регионе? + + + + Choose Installation Type + + Manual + + + + Choose a VPN protocol Выберите VPN-протокол - + Skip setup Пропустить настройку @@ -2798,7 +3227,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Выбрать VPN-протокол - + Continue Продолжить @@ -3005,12 +3434,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + OpenVPN native format Оригинальный формат OpenVPN - + WireGuard native format Оригинальный формат WireGuard @@ -3019,7 +3448,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN-Доступ - + Connection Соединение @@ -3032,8 +3461,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. + - Server Сервер @@ -3057,113 +3486,113 @@ and will not be shared or disclosed to the Amnezia or any third parties Файл с настройками подключения к - + Save OpenVPN config Сохранить конфигурацию OpenVPN - + Save WireGuard config Сохранить конфигурацию WireGuard - + Save AmneziaWG config Сохранить конфигурацию AmneziaWG - + Save Shadowsocks config Сохранить конфигурацию Shadowsocks - + Save Cloak config Сохранить конфигурацию Cloak - + Save XRay config Сохранить конфигурацию XRay - + For the AmneziaVPN app Для приложения AmneziaVPN - + AmneziaWG native format Оригинальный формат AmneziaWG - + Shadowsocks native format Оригинальный формат Shadowsocks - + Cloak native format Оригинальный формат Cloak - + XRay native format Оригинальный формат XRay - + Share VPN Access Поделиться VPN - + Share full access to the server and VPN Поделиться полным доступом к серверу и VPN - + Use for your own devices, or share with those you trust to manage the server. Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете. - - + + Users Пользователи - + User name Имя пользователя - + Search Поиск - + Creation date: %1 Дата создания: %1 - + Latest handshake: %1 Последнее рукопожатие: %1 - + Data received: %1 Получено данных: %1 - + Data sent: %1 Отправлено данных: %1 - + Allowed IPs: %1 @@ -3172,42 +3601,42 @@ and will not be shared or disclosed to the Amnezia or any third parties Дата создания: - + Rename Переименовать - + Client name Имя клиента - + Save Сохранить - + Revoke Отозвать - + Revoke the config for a user - %1? Отозвать конфигурацию для пользователя - %1? - + The user will no longer be able to connect to your server. Пользователь больше не сможет подключаться к вашему серверу. - + Continue Продолжить - + Cancel Отменить @@ -3216,25 +3645,25 @@ and will not be shared or disclosed to the Amnezia or any third parties Полный доступ - + Share VPN access without the ability to manage the server Поделиться доступом к VPN без возможности управления сервером + - Protocol Протокол + - Connection format Формат подключения - - + + Share Поделиться @@ -3298,17 +3727,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageStart - + Logging was disabled after 14 days, log files were deleted Логирование было отключено по прошествии 14 дней, файлы логов были удалены. - + Settings restored from backup file Настройки восстановлены из бэкап файла - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. Логирование включено. Обратите внимание, что через 14 дней оно будет автоматически отключено, а все файлы логов будут удалены. @@ -3578,72 +4007,77 @@ and will not be shared or disclosed to the Amnezia or any third parties Фоновая служба не запущена - + + The selected protocol is not supported on the current platform + Выбранный протокол не поддерживается на данном устройстве + + + Server check failed Проверка сервера завершилась неудачей - + Server port already used. Check for another software Порт сервера уже используется. Проверьте наличие другого ПО - + Server error: Docker container missing Ошибка сервера: отсутствует Docker-контейнер - + Server error: Docker failed Ошибка сервера: сбой в работе Docker - + Installation canceled by user Установка отменена пользователем - + The user does not have permission to use sudo У пользователя нет прав на использование sudo - + Server error: Packet manager error Ошибка сервера: ошибка менеджера пакетов - + SSH request was denied SSH-запрос был отклонён - + SSH request was interrupted SSH-запрос был прерван - + SSH internal error Внутренняя ошибка SSH - + Invalid private key or invalid passphrase entered Введен неверный закрытый ключ или неверная парольная фраза - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types Выбранный формат закрытого ключа не поддерживается, используйте типы ключей openssh ED25519 или PEM - + Timeout connecting to server Тайм-аут подключения к серверу - + SCP error: Generic failure Ошибка SCP: общий сбой @@ -3700,22 +4134,23 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp error: No media was in remote drive - + The config does not contain any containers and credentials for connecting to the server Конфигурация не содержит каких-либо контейнеров и учетных данных для подключения к серверу - + + Error when retrieving configuration from API Ошибка при получении конфигурации из API - + This config has already been added to the application Данная конфигурация уже была добавлена в приложение - + ErrorCode: %1. Код ошибки: %1. @@ -3724,127 +4159,139 @@ and will not be shared or disclosed to the Amnezia or any third parties Failed to save config to disk - + OpenVPN config missing Отсутствует конфигурация OpenVPN - + OpenVPN management server error Серверная ошибка управлением OpenVPN - + OpenVPN executable missing Отсутствует исполняемый файл OpenVPN - + Shadowsocks (ss-local) executable missing Отсутствует исполняемый файл Shadowsocks (ss-local) - + Cloak (ck-client) executable missing Отсутствует исполняемый файл Cloak (ck-client) - + Amnezia helper service error Ошибка вспомогательной службы Amnezia - + OpenSSL failed Ошибка OpenSSL - + Can't connect: another VPN connection is active Невозможно подключиться: активно другое VPN-соединение - + Can't setup OpenVPN TAP network adapter Невозможно настроить сетевой адаптер OpenVPN TAP - + VPN pool error: no available addresses Ошибка пула VPN: нет доступных адресов - + Unable to open config file Не удалось открыть файл конфигурации - + + VPN Protocols is not installed. + Please install VPN container at first + VPN-протоколы не установлены. + Пожалуйста, установите протокол + + + VPN connection error Ошибка VPN-соединения - + In the response from the server, an empty config was received В ответе от сервера была получена пустая конфигурация - + SSL error occurred Произошла ошибка SSL - + Server response timeout on api request Тайм-аут ответа сервера на запрос API - + Missing AGW public key - + Failed to decrypt response payload - + Missing list of available services + The limit of allowed configurations per subscription has been exceeded + + + + QFile error: The file could not be opened Ошибка QFile: не удалось открыть файл - + QFile error: An error occurred when reading from the file Ошибка QFile: произошла ошибка при чтении из файла - + QFile error: The file could not be accessed Ошибка QFile: не удалось получить доступ к файлу - + QFile error: An unspecified error occurred Ошибка QFile: произошла неизвестная ошибка - + QFile error: A fatal error occurred Ошибка QFile: произошла фатальная ошибка - + QFile error: The operation was aborted Ошибка QFile: операция была прервана - + Internal error Внутренняя ошибка @@ -3854,27 +4301,24 @@ and will not be shared or disclosed to the Amnezia or any third parties IPsec - Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - Shadowsocks маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. + Shadowsocks маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. - OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak — это OpenVPN с маскировкой VPN-трафика под обычный веб-трафик и защитой от обнаружения активным зондированием. Подходит для регионов с самым высоким уровнем цензуры. + OpenVPN over Cloak — это OpenVPN с маскировкой VPN-трафика под обычный веб-трафик и защитой от обнаружения активным зондированием. Подходит для регионов с самым высоким уровнем цензуры. - + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. IKEv2/IPsec — современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. Он имеет встроенную поддержку в последних версиях Android и iOS. - + Create a file vault on your server to securely store and transfer files. Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. - This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -3893,7 +4337,7 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - Это связка протокола OpenVPN и плагина Cloak, разработанная специально для защиты от блокировки. + Это связка протокола OpenVPN и плагина Cloak, разработанная специально для защиты от блокировки. OpenVPN обеспечивает безопасное VPN-соединение, шифруя весь интернет-трафик между клиентом и сервером. @@ -3912,7 +4356,6 @@ Cloak изменяет метаданные пакетов таким образ * Работает по сетевому протоколу TCP, использует порт 443 - A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -3922,7 +4365,7 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - Относительно новый и популярный VPN-протокол с простой архитектурой. + Относительно новый и популярный VPN-протокол с простой архитектурой. WireGuard обеспечивает стабильное VPN-соединение и высокую производительность на всех устройствах. Он использует строго заданные настройки шифрования. WireGuard по сравнению с OpenVPN имеет меньшую задержку и лучшую пропускную способность при передаче данных. WireGuard очень уязвим для блокировки из-за характерных сигнатур пакетов. В отличие от некоторых других VPN-протоколов, использующих методы обфускации, последовательные сигнатуры пакетов WireGuard легче идентифицируются и, следовательно, могут блокироваться современными Deep Packet Inspection (DPI) системами и другими инструментами для сетевого мониторинга. @@ -3933,18 +4376,17 @@ WireGuard очень уязвим для блокировки из-за хара * Работает по сетевому протоколу UDP - The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - Протокол REALITY, новаторская разработка создателей XRay, специально спроектирован для противодействия самой строгой цензуре с помощью нового способа обхода блокировок. + Протокол REALITY, новаторская разработка создателей XRay, специально спроектирован для противодействия самой строгой цензуре с помощью нового способа обхода блокировок. Он уникальным образом идентифицирует цензоров на этапе TLS-рукопожатия, беспрепятственно работая в качестве прокси для реальных клиентов и перенаправляя цензоров на реальные сайты, такие как google.com, тем самым предъявляя подлинный TLS-сертификат и данные. REALITY отличается от аналогичных технологий благодаря способности без специальной настройки маскировать веб-трафик так, как будто он поступает со случайных легитимных сайтов. В отличие от более старых протоколов, таких как VMess, VLESS и транспорт XTLS-Vision, технология распознавания "друг или враг" на этапе TLS-рукопожатия повышает безопасность и обходит обнаружение сложными системами DPI-анализа, которые используют методы активного зондирования. Это делает REALITY эффективным решением для поддержания свободы интернета в регионах с жесткой цензурой. - + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -3965,7 +4407,7 @@ While it offers a blend of security, stability, and speed, it's essential t * Работает по сетевому протоколу UDP, использует порты 500 и 4500 - + DNS Service Сервис DNS @@ -3976,7 +4418,7 @@ While it offers a blend of security, stability, and speed, it's essential t - + Website in Tor network Веб-сайт в сети Tor @@ -3991,36 +4433,123 @@ While it offers a blend of security, stability, and speed, it's essential t OpenVPN — самый популярный VPN-протокол с гибкой настройкой. Имеет собственный протокол безопасности с SSL/TLS для обмена ключами. - + + Shadowsocks masks VPN traffic, making it resemble normal web traffic, but it may still be detected by certain analysis systems. + + + + + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. It is very resistant to detection, but offers low speed. + + + + + WireGuard - popular VPN protocol with high performance, high speed and low power consumption. + + + + + AmneziaWG is a special protocol from Amnezia based on WireGuard. It provides high connection speed and ensures stable operation even in the most challenging network conditions. + + + + + XRay with REALITY masks VPN traffic as web traffic and protects against active probing. It is highly resistant to detection and offers high speed. + + + + + OpenVPN stands as one of the most popular and time-tested VPN protocols available. +It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. + +* Available in the AmneziaVPN across all platforms +* Normal power consumption on mobile devices +* Flexible customisation to suit user needs to work with different operating systems and devices +* Recognised by DPI systems and therefore susceptible to blocking +* Can operate over both TCP and UDP network protocols. + + + + + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against detection. + +OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. + +Cloak protects OpenVPN from detection. + +Cloak can modify packet metadata so that it completely masks VPN traffic as normal web traffic, and also protects the VPN from detection by Active Probing. This makes it very resistant to being detected + +Immediately after receiving the first data packet, Cloak authenticates the incoming connection. If authentication fails, the plugin masks the server as a fake website and your VPN becomes invisible to analysis systems. + +* Available in the AmneziaVPN across all platforms +* High power consumption on mobile devices +* Flexible settings +* Not recognised by detection systems +* Works over TCP network protocol, 443 port. + + + + + + A relatively new popular VPN protocol with a simplified architecture. +WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. +WireGuard is very susceptible to detection and blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Easily recognised by DPI analysis systems, susceptible to blocking +* Works over UDP network protocol. + + + + + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. +While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. +This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Not recognised by traffic analysis systems +* Works over UDP network protocol. + + + + + The REALITY protocol, a pioneering development by the creators of XRay, is designed to provide the highest level of protection against detection through its innovative approach to security and privacy. +It uniquely identifies attackers during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting attackers to genuine websites, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security. This makes REALITY a robust solution for maintaining internet freedom. + + + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. - WireGuard — новый популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. Рекомендуется для регионов с низким уровнем цензуры. + WireGuard — новый популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. Рекомендуется для регионов с низким уровнем цензуры. - AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. - AmneziaWG — специальный протокол от Amnezia, основанный на протоколе WireGuard. Он такой же быстрый, как WireGuard, но очень устойчив к блокировкам. Рекомендуется для регионов с высоким уровнем цензуры. + AmneziaWG — специальный протокол от Amnezia, основанный на протоколе WireGuard. Он такой же быстрый, как WireGuard, но очень устойчив к блокировкам. Рекомендуется для регионов с высоким уровнем цензуры. - XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - XRay with REALITY подойдет для стран с самым высоким уровнем цензуры. Маскировка трафика под веб-трафик на уровне TLS и защита от обнаружения методами активного зондирования. + XRay with REALITY подойдет для стран с самым высоким уровнем цензуры. Маскировка трафика под веб-трафик на уровне TLS и защита от обнаружения методами активного зондирования. IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. IKEv2/IPsec — современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. - + Deploy a WordPress site on the Tor network in two clicks. Разверните сайт на WordPress в сети Tor в два клика. - + Replace the current DNS server with your own. This will increase your privacy level. Замените текущий DNS-сервер на свой собственный. Это повысит уровень вашей конфиденциальности. - OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -4029,7 +4558,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Flexible customisation to suit user needs to work with different operating systems and devices * Recognised by DPI analysis systems and therefore susceptible to blocking * Can operate over both TCP and UDP network protocols. - OpenVPN — один из самых популярных и проверенных временем VPN-протоколов. + OpenVPN — один из самых популярных и проверенных временем VPN-протоколов. В нем используется уникальный протокол безопасности, опирающийся на SSL/TLS для шифрования и обмена ключами. Кроме того, OpenVPN поддерживает множество методов аутентификации, что делает его универсальным и адаптируемым к широкому спектру устройств и операционных систем. Благодаря открытому исходному коду OpenVPN подвергается тщательному анализу со стороны мирового сообщества, что постоянно повышает его безопасность. Оптимальное соотношение производительности, безопасности и совместимости делает OpenVPN лучшим выбором как для частных лиц, так и для компаний, заботящихся о конфиденциальности. * Доступен в AmneziaVPN на всех платформах @@ -4039,7 +4568,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Может работать по сетевым протоколам TCP и UDP - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -4064,7 +4593,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for В отличие от более старых протоколов, таких как VMess, VLESS и XTLS-Vision, технология распознавания "друг или враг" на этапе TLS-рукопожатия повышает безопасность и обходит обнаружение сложными системами DPI-анализа, которые используют методы активного зондирования. Это делает REALITY эффективным решением для поддержания свободы интернета в регионах с жесткой цензурой. - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -4141,7 +4670,6 @@ WireGuard очень восприимчив к блокированию из-з * Работает по сетевому протоколу UDP. - A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -4151,7 +4679,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin * Minimum number of settings * Not recognised by DPI analysis systems, resistant to blocking * Works over UDP network protocol. - AmneziaWG — усовершенствованная версия популярного VPN-протокола WireGuard. AmneziaWG опирается на фундамент, заложенный WireGuard, сохраняя упрощенную архитектуру и высокую производительность на различных устройствах. + AmneziaWG — усовершенствованная версия популярного VPN-протокола WireGuard. AmneziaWG опирается на фундамент, заложенный WireGuard, сохраняя упрощенную архитектуру и высокую производительность на различных устройствах. Хотя WireGuard известен своей эффективностью, у него были проблемы с обнаружением из-за характерных сигнатур пакетов. AmneziaWG решает эту проблему за счет использования более совершенных методов обфускации, благодаря чему его трафик сливается с обычным интернет-трафиком. Таким образом, AmneziaWG сохраняет высокую производительность оригинального протокола, добавляя при этом дополнительный уровень скрытности, что делает его отличным выбором для тех, кому нужно быстрое и незаметное VPN-соединение. @@ -4242,7 +4770,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin - + SOCKS5 proxy server Прокси-сервер SOCKS5 @@ -4378,6 +4906,19 @@ This means that AmneziaWG keeps the fast performance of the original while addin Невозможно найти разделитель-двоеточие между именем хоста и портом + + RenameServerDrawer + + + Server name + Имя сервера + + + + Save + Сохранить + + SelectLanguageDrawer @@ -4428,24 +4969,24 @@ This means that AmneziaWG keeps the fast performance of the original while addin ShareConnectionDrawer - - + + Save AmneziaVPN config Сохранить конфигурацию AmneziaVPN - + Share Поделиться - + Copy Скопировать - - + + Copied Скопировано @@ -4455,12 +4996,12 @@ This means that AmneziaWG keeps the fast performance of the original while addin Скопировать строку конфигурации - + Show connection settings Показать настройки подключения - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код" @@ -4544,7 +5085,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin TextFieldWithHeaderType - + The field can't be empty Поле не может быть пустым @@ -4552,7 +5093,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Мбит/с @@ -4603,28 +5144,24 @@ This means that AmneziaWG keeps the fast performance of the original while addin amnezia::ContainerProps - Low - Низкий + Низкий - High - Высокий + Высокий Extreme Экстремальный - I just want to increase the level of my privacy. - Я просто хочу повысить уровень своей приватности. + Я просто хочу повысить уровень своей приватности. - I want to bypass censorship. This option recommended in most cases. - Я хочу обойти блокировки. Этот вариант рекомендуется в большинстве случаев. + Я хочу обойти блокировки. Этот вариант рекомендуется в большинстве случаев. Most VPN protocols are blocked. Recommended if other options are not working. @@ -4646,6 +5183,16 @@ This means that AmneziaWG keeps the fast performance of the original while addin I just want to increase the level of privacy Хочу просто повысить уровень приватности + + + Automatic + + + + + AmneziaWG protocol will be installed. It provides high connection speed and ensures stable operation even in the most challenging network conditions. + + main2 diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index d3c8747d..b8696201 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -407,7 +407,7 @@ bool ApiConfigsController::isConfigValid() return updateServiceFromGateway(serverIndex, "", ""); } else if (configSource && m_serversModel->isApiKeyExpired(serverIndex)) { qDebug() << "attempt to update api config by expires_at event"; - if (configSource == apiDefs::ConfigSource::Telegram) { + if (configSource == apiDefs::ConfigSource::AmneziaGateway) { return updateServiceFromGateway(serverIndex, "", ""); } else { m_serversModel->removeApiConfig(serverIndex); diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index cdb9dfe8..191582a5 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -48,8 +48,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const } case ServiceDescriptionRole: { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { - return tr("Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to 200 " - "Mb/s"); + return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. " + "Speeds up to 200 Mbps"); } else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and " "more. YouTube is not included in the free plan."); diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index 43fbb160..6e67ef1f 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -81,7 +81,7 @@ PageType { actionButtonImage: "qrc:/images/controls/settings.svg" headerText: root.processedServer.name - descriptionText: qsTr("Locations for connection") + descriptionText: qsTr("Location for connection") actionButtonFunction: function() { PageController.showBusyIndicator(true) diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index 5cc21d07..07df07a7 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -42,8 +42,8 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Connected devices") - descriptionText: qsTr("To manage connected devices") + headerText: qsTr("Active devices") + descriptionText: qsTr("Manage currently connected devices") } WarningType { @@ -71,8 +71,13 @@ PageType { rightImageSource: "qrc:/images/controls/trash.svg" clickedFunction: function() { - var headerText = qsTr("Deactivate the subscription on selected device") - var descriptionText = qsTr("The next time the “Connect” button is pressed, the device will be activated again") + if (isCurrentDevice && ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) + return + } + + var headerText = qsTr("Are you sure you want to unlink this device?") + var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect.") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml index 3651407b..7961594b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -99,7 +99,7 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("How to connect on another device") - descriptionText: qsTr("Instructions on the Amnezia website") + descriptionText: qsTr("Setup guides on the Amnezia website") } } diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index a1cc1fb8..f7acdf76 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -46,7 +46,7 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Configuration files") - descriptionText: qsTr("To connect a router or AmneziaWG application") + descriptionText: qsTr("For router setup or the AmneziaWG app") } } @@ -123,13 +123,13 @@ PageType { Layout.fillWidth: true Layout.margins: 16 - headerText: qsTr("Configuration file ") + moreOptionsDrawer.countryName + headerText: moreOptionsDrawer.countryName + qsTr(" configuration file") } LabelWithButtonType { Layout.fillWidth: true - text: qsTr("Create a new") + text: qsTr("Generate a new configuration file") descriptionText: qsTr("The previously created one will stop working") clickedFunction: function() { @@ -193,9 +193,15 @@ PageType { } function showQuestion(isConfigIssue, countryCode, countryName) { - var headerText = qsTr("Revoke the actual %1 configuration file?").arg(countryName) - var descriptionText = qsTr("The previously created file will no longer be valid. It will not be possible to connect using it.") - var yesButtonText = qsTr("Continue") + var headerText + if (isConfigIssue) { + headerText = qsTr("Generate a new %1 configuration file?").arg(countryName) + } else { + headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName) + } + + var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it") + var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue") var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 7d089639..c7650862 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -42,7 +42,7 @@ PageType { QtObject { id: deviceCountObject - readonly property string title: qsTr("Connected devices") + readonly property string title: qsTr("Active connections") readonly property string contentKey: "connectedDevices" readonly property string objectImageSource: "qrc:/images/controls/monitor.svg" } @@ -215,7 +215,7 @@ PageType { text: qsTr("Configuration files") - descriptionText: qsTr("To connect a router or AmneziaWG application") + descriptionText: qsTr("Manage configuration files") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { @@ -233,9 +233,9 @@ PageType { visible: footer.isVisibleForAmneziaFree - text: qsTr("Connected devices") + text: qsTr("Active devices") - descriptionText: qsTr("To manage connected devices") + descriptionText: qsTr("Manage currently connected devices") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { @@ -265,6 +265,8 @@ PageType { LabelWithButtonType { Layout.fillWidth: true + visible: footer.isVisibleForAmneziaFree + text: qsTr("How to connect on another device") rightImageSource: "qrc:/images/controls/chevron-right.svg" @@ -273,7 +275,9 @@ PageType { } } - DividerType {} + DividerType { + visible: footer.isVisibleForAmneziaFree + } BasicButtonType { id: resetButton @@ -325,17 +329,17 @@ PageType { pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed - text: qsTr("Deactivate the subscription on this device") + text: qsTr("Unlink this device") clickedFunc: function() { - var headerText = qsTr("Deactivate the subscription on this device?") - var descriptionText = qsTr("The next time the “Connect” button is pressed, the device will be activated again") + var headerText = qsTr("Are you sure you want to unlink this device?") + var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect.") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { - PageController.showNotificationMessage(qsTr("Cannot deactivate subscription during active connection")) + PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) } else { PageController.showBusyIndicator(true) if (ApiConfigsController.deactivateDevice()) { diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml index 424e10c5..3e4f0149 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSupport.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -27,7 +27,7 @@ PageType { QtObject { id: techSupport - readonly property string title: qsTr("For technical support") + readonly property string title: qsTr("Email Support") readonly property string description: qsTr("support@amnezia.org") readonly property string link: "mailto:support@amnezia.org" } @@ -35,7 +35,7 @@ PageType { QtObject { id: paymentSupport - readonly property string title: qsTr("For payment issues") + readonly property string title: qsTr("Email Billing & Orders") readonly property string description: qsTr("help@vpnpay.io") readonly property string link: "mailto:help@vpnpay.io" } @@ -43,7 +43,7 @@ PageType { QtObject { id: site - readonly property string title: qsTr("Site") + readonly property string title: qsTr("Website") readonly property string description: qsTr("amnezia.org") readonly property string link: LanguageModel.getCurrentSiteUrl() } @@ -79,7 +79,7 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Support") - descriptionText: qsTr("Our technical support specialists are ready to help you at any time") + descriptionText: qsTr("Our technical support specialists are available to assist you at any time") } } From d3339a7f3a50e9c5937cfc27404cd160037a0790 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Tue, 4 Mar 2025 12:13:04 +0100 Subject: [PATCH 33/36] fix: iOS/iPadOS crashes on a start of the app because of there's no keyFrame set (#1448) So setting one if it's not set. --- client/platforms/ios/ScreenProtection.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/platforms/ios/ScreenProtection.swift b/client/platforms/ios/ScreenProtection.swift index 1355dc13..200cf0cb 100644 --- a/client/platforms/ios/ScreenProtection.swift +++ b/client/platforms/ios/ScreenProtection.swift @@ -14,10 +14,15 @@ extension UIApplication { var keyWindows: [UIWindow] { connectedScenes .compactMap { + guard let windowScene = $0 as? UIWindowScene else { return nil } if #available(iOS 15.0, *) { - ($0 as? UIWindowScene)?.keyWindow + guard let keywindow = windowScene.keyWindow else { + windowScene.windows.first?.makeKey() + return windowScene.windows.first + } + return keywindow } else { - ($0 as? UIWindowScene)?.windows.first { $0.isKeyWindow } + return windowScene.windows.first { $0.isKeyWindow } } } } From 99e3afabadb3a3b9cd457b2ba2dd657e3b15a844 Mon Sep 17 00:00:00 2001 From: shiroow <56990402+shiroow@users.noreply.github.com> Date: Wed, 5 Mar 2025 06:11:31 +0300 Subject: [PATCH 34/36] chore: update eng text (#1456) chore: update eng text --- client/ui/models/api/apiServicesModel.cpp | 8 ++++---- client/ui/qml/Components/AdLabel.qml | 2 +- client/ui/qml/Pages2/PageSettingsApiDevices.qml | 2 +- .../ui/qml/Pages2/PageSettingsApiNativeConfigs.qml | 2 +- client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 14 +++++++------- client/ui/qml/Pages2/PageSettingsApiSupport.qml | 2 +- client/ui/qml/Pages2/PageSettingsConnection.qml | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/client/ui/models/api/apiServicesModel.cpp b/client/ui/models/api/apiServicesModel.cpp index f1880e4d..65f17758 100644 --- a/client/ui/models/api/apiServicesModel.cpp +++ b/client/ui/models/api/apiServicesModel.cpp @@ -65,8 +65,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const case CardDescriptionRole: { auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { - return tr("Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. " - "Works for any sites with no restrictions. Speed up to %1 MBit/s. Unlimited traffic.") + return tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos. " + "Access all websites and online resources. Speeds up to %1 Mbps.") .arg(speed); } else if (serviceType == serviceType::amneziaFree) { QString description = tr("AmneziaFree provides free unlimited access to a basic set of web sites, such as Facebook, Instagram, Twitter (X), Discord, Telegram, and others. YouTube is not included in the free plan."); @@ -79,8 +79,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case ServiceDescriptionRole: { if (serviceType == serviceType::amneziaPremium) { - return tr("Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. " - "Works for any sites with no restrictions."); + return tr("Amnezia Premium is classic VPN for for seamless work, downloading large files, and watching videos. " + "Access all websites and online resources."); } else { return tr("AmneziaFree provides free unlimited access to a basic set of web sites, such as Facebook, Instagram, Twitter (X), Discord, Telegram, and others. YouTube is not included in the free plan."); } diff --git a/client/ui/qml/Components/AdLabel.qml b/client/ui/qml/Components/AdLabel.qml index efbe1720..91e1a42c 100644 --- a/client/ui/qml/Components/AdLabel.qml +++ b/client/ui/qml/Components/AdLabel.qml @@ -54,7 +54,7 @@ Rectangle { Layout.rightMargin: 10 Layout.leftMargin: 10 - text: qsTr("Amnezia Premium - for access to any website") + text: qsTr("Amnezia Premium - for access to all websites and online resources") color: AmneziaStyle.color.pearlGray lineHeight: 18 diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index 07df07a7..c6a2f98c 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -42,7 +42,7 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Active devices") + headerText: qsTr("Active Devices") descriptionText: qsTr("Manage currently connected devices") } diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index f7acdf76..44b2d2fa 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -45,7 +45,7 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Configuration files") + headerText: qsTr("Configuration Files") descriptionText: qsTr("For router setup or the AmneziaWG app") } } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index c7650862..689502c1 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -26,7 +26,7 @@ PageType { QtObject { id: statusObject - readonly property string title: qsTr("Subscription status") + readonly property string title: qsTr("Subscription Status") readonly property string contentKey: "subscriptionStatus" readonly property string objectImageSource: "qrc:/images/controls/info.svg" } @@ -34,7 +34,7 @@ PageType { QtObject { id: endDateObject - readonly property string title: qsTr("Valid until") + readonly property string title: qsTr("Valid Until") readonly property string contentKey: "endDate" readonly property string objectImageSource: "qrc:/images/controls/history.svg" } @@ -42,7 +42,7 @@ PageType { QtObject { id: deviceCountObject - readonly property string title: qsTr("Active connections") + readonly property string title: qsTr("Active Connections") readonly property string contentKey: "connectedDevices" readonly property string objectImageSource: "qrc:/images/controls/monitor.svg" } @@ -183,7 +183,7 @@ PageType { visible: false //footer.isVisibleForAmneziaFree - text: qsTr("Subscription key") + text: qsTr("Subscription Key") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { @@ -191,7 +191,7 @@ PageType { shareConnectionDrawer.openTriggered() shareConnectionDrawer.isSelfHostedConfig = false; - shareConnectionDrawer.shareButtonText = qsTr("Save VPN key to file") + shareConnectionDrawer.shareButtonText = qsTr("Save VPN key as a file") shareConnectionDrawer.copyButtonText = qsTr("Copy VPN key") @@ -213,7 +213,7 @@ PageType { visible: footer.isVisibleForAmneziaFree - text: qsTr("Configuration files") + text: qsTr("Configuration Files") descriptionText: qsTr("Manage configuration files") rightImageSource: "qrc:/images/controls/chevron-right.svg" @@ -233,7 +233,7 @@ PageType { visible: footer.isVisibleForAmneziaFree - text: qsTr("Active devices") + text: qsTr("Active Devices") descriptionText: qsTr("Manage currently connected devices") rightImageSource: "qrc:/images/controls/chevron-right.svg" diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml index 3e4f0149..2ca13151 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSupport.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -27,7 +27,7 @@ PageType { QtObject { id: techSupport - readonly property string title: qsTr("Email Support") + readonly property string title: qsTr("Email") readonly property string description: qsTr("support@amnezia.org") readonly property string link: "mailto:support@amnezia.org" } diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index d3743b96..69671f27 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -140,7 +140,7 @@ PageType { } onClicked: { if (!checkable) { - PageController.showNotificationMessage(qsTr("Cannot change killSwitch settings during active connection")) + PageController.showNotificationMessage(qsTr("Cannot change KillSwitch settings during active connection")) } } } From 99818c2ad88160e7c52486fd951ee039d35d6f4d Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Tue, 4 Mar 2025 22:20:46 -0800 Subject: [PATCH 35/36] Fixes for native OpenVPN config import (#1444) * Remote address in OpenVPN config can be host name * Protocol parameter in OpenVPN config is not mandatory --- client/ui/controllers/importController.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 1bba0e8a..4ca12e21 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -27,8 +27,6 @@ namespace ConfigTypes checkConfigFormat(const QString &config) { const QString openVpnConfigPatternCli = "client"; - const QString openVpnConfigPatternProto1 = "proto tcp"; - const QString openVpnConfigPatternProto2 = "proto udp"; const QString openVpnConfigPatternDriver1 = "dev tun"; const QString openVpnConfigPatternDriver2 = "dev tap"; @@ -53,14 +51,13 @@ namespace || (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName) && config.contains(amneziaConfigPatternPassword))) { return ConfigTypes::Amnezia; - } else if (config.contains(openVpnConfigPatternCli) - && (config.contains(openVpnConfigPatternProto1) || config.contains(openVpnConfigPatternProto2)) - && (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) { - return ConfigTypes::OpenVpn; } else if (config.contains(wireguardConfigPatternSectionInterface) && config.contains(wireguardConfigPatternSectionPeer)) { return ConfigTypes::WireGuard; } else if ((config.contains(xrayConfigPatternInbound)) && (config.contains(xrayConfigPatternOutbound))) { return ConfigTypes::Xray; + } else if (config.contains(openVpnConfigPatternCli) + && (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) { + return ConfigTypes::OpenVpn; } return ConfigTypes::Invalid; } @@ -345,7 +342,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) arr.push_back(containers); QString hostName; - const static QRegularExpression hostNameRegExp("remote (.*) [0-9]*"); + const static QRegularExpression hostNameRegExp("remote\\s+([^\\s]+)"); QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data); if (hostNameMatch.hasMatch()) { hostName = hostNameMatch.captured(1); From c9e5b92f7912ba27c229645a0b1c247333d27bf1 Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Tue, 4 Mar 2025 22:21:39 -0800 Subject: [PATCH 36/36] Remove unneeded flushDns (#1443) --- client/ui/controllers/sitesController.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ui/controllers/sitesController.cpp b/client/ui/controllers/sitesController.cpp index 24ae035f..d40be458 100644 --- a/client/ui/controllers/sitesController.cpp +++ b/client/ui/controllers/sitesController.cpp @@ -44,7 +44,6 @@ void SitesController::addSite(QString hostname) QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, QStringList() << hostname)); } - QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection); }; const auto &resolveCallback = [this, processSite](const QHostInfo &hostInfo) { @@ -75,7 +74,6 @@ void SitesController::removeSite(int index) QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteRoutes", Qt::QueuedConnection, Q_ARG(QStringList, QStringList() << hostname)); - QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection); emit finished(tr("Site removed: %1").arg(hostname)); } @@ -124,7 +122,6 @@ void SitesController::importSites(const QString &fileName, bool replaceExisting) m_sitesModel->addSites(sites, replaceExisting); QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, ips)); - QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection); emit finished(tr("Import completed")); }