Compare commits

..

2 Commits

Author SHA1 Message Date
vladimir.kuznetsov
5f2898062b chore: bump version 2025-02-25 22:28:16 +07:00
vladimir.kuznetsov
ff1dea2fdd bugfix: fixed mobile controllers initialization 2025-02-25 22:27:20 +07:00
48 changed files with 668 additions and 1693 deletions

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.8.4.3
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 2080)
set(APP_ANDROID_VERSION_CODE 2078)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -6,11 +6,11 @@
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский
[AmneziaVPN](https://amnezia.org) — это open source VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
### [Сайт](https://amnezia.org) | [Зеркало сайта](https://storage.googleapis.com/amnezia/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/amnezia/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
> [!TIP]
> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/amnezia/amnezia.org).
@@ -30,7 +30,7 @@
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
- Поддерживает платформы: Windows, macOS, Linux, Android, iOS.
- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS.
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
## Ссылки
@@ -38,10 +38,10 @@
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
## Технологии
@@ -80,8 +80,8 @@ git submodule update --init --recursive
Проверьте папку deploy для скриптов сборки.
### Как собрать iOS-приложение из исходного кода на MacOS
1. Убедитесь, что у вас установлен Xcode версии 14 или выше.
2. Для генерации проекта Xcode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
1. Убедитесь, что у вас установлен XCode версии 14 или выше.
2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
- MacOS
- iOS
- Модуль совместимости с Qt 5
@@ -117,7 +117,7 @@ $QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
export PATH=$(PATH):/path/to/GOPATH/bin
```
6. Откройте проект в Xcode. Теперь вы можете тестировать, архивировать или публиковать приложение.
6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение.
Если сборка завершится с ошибкой:
```

View File

@@ -31,6 +31,10 @@ add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
if(IOS)
set(PACKAGES ${PACKAGES} Multimedia)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets)
endif()
@@ -44,6 +48,10 @@ set(LIBS ${LIBS}
Qt6::Core5Compat Qt6::Concurrent
)
if(IOS)
set(LIBS ${LIBS} Qt6::Multimedia)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
endif()

View File

@@ -3,7 +3,6 @@
#include <QDebug>
#include <QJsonDocument>
#include <QProcess>
#include <QRegularExpression>
#include <QString>
#include <QTemporaryDir>
#include <QTemporaryFile>
@@ -20,17 +19,13 @@
#include "settings.h"
#include "utilities.h"
WireguardConfigurator::WireguardConfigurator(std::shared_ptr<Settings> settings,
const QSharedPointer<ServerController> &serverController, bool isAwg,
QObject *parent)
WireguardConfigurator::WireguardConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController,
bool isAwg, QObject *parent)
: ConfiguratorBase(settings, serverController, parent), m_isAwg(isAwg)
{
m_serverConfigPath =
m_isAwg ? amnezia::protocols::awg::serverConfigPath : amnezia::protocols::wireguard::serverConfigPath;
m_serverPublicKeyPath =
m_isAwg ? amnezia::protocols::awg::serverPublicKeyPath : amnezia::protocols::wireguard::serverPublicKeyPath;
m_serverPskKeyPath =
m_isAwg ? amnezia::protocols::awg::serverPskKeyPath : amnezia::protocols::wireguard::serverPskKeyPath;
m_serverConfigPath = m_isAwg ? amnezia::protocols::awg::serverConfigPath : amnezia::protocols::wireguard::serverConfigPath;
m_serverPublicKeyPath = m_isAwg ? amnezia::protocols::awg::serverPublicKeyPath : amnezia::protocols::wireguard::serverPublicKeyPath;
m_serverPskKeyPath = m_isAwg ? amnezia::protocols::awg::serverPskKeyPath : amnezia::protocols::wireguard::serverPskKeyPath;
m_configTemplate = m_isAwg ? ProtocolScriptType::awg_template : ProtocolScriptType::wireguard_template;
m_protocolName = m_isAwg ? config_key::awg : config_key::wireguard;
@@ -68,31 +63,9 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::genClientKeys()
return connData;
}
QList<QHostAddress> WireguardConfigurator::getIpsFromConf(const QString &input)
{
QRegularExpression regex("AllowedIPs = (\\d+\\.\\d+\\.\\d+\\.\\d+)");
QRegularExpressionMatchIterator matchIterator = regex.globalMatch(input);
QList<QHostAddress> ips;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
const QString address_string { match.captured(1) };
const QHostAddress address { address_string };
if (address.isNull()) {
qWarning() << "Couldn't recognize the ip address: " << address_string;
} else {
ips << address;
}
}
return ips;
}
WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardConfig(const ServerCredentials &credentials,
DockerContainer container,
const QJsonObject &containerConfig,
ErrorCode &errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
WireguardConfigurator::ConnectionData connData = WireguardConfigurator::genClientKeys();
connData.host = credentials.hostName;
@@ -103,45 +76,65 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
return connData;
}
QString getIpsScript = QString("cat %1 | grep AllowedIPs").arg(m_serverConfigPath);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
// Get list of already created clients (only IP addresses)
QString nextIpNumber;
{
QString script = QString("cat %1 | grep AllowedIPs").arg(m_serverConfigPath);
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
errorCode = m_serverController->runContainerScript(credentials, container, getIpsScript, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return connData;
}
auto ips = getIpsFromConf(stdOut);
errorCode = m_serverController->runContainerScript(credentials, container, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return connData;
}
QHostAddress nextIp = [&] {
QHostAddress result;
QHostAddress lastIp;
if (ips.empty()) {
lastIp.setAddress(containerConfig.value(m_protocolName)
.toObject()
.value(config_key::subnet_address)
.toString(protocols::wireguard::defaultSubnetAddress));
stdOut.replace("AllowedIPs = ", "");
stdOut.replace("/32", "");
QStringList ips = stdOut.split("\n", Qt::SkipEmptyParts);
// remove extra IPs from each line for case when user manually edited the wg0.conf
// and added there more IPs for route his itnernal networks, like:
// ...
// AllowedIPs = 10.8.1.6/32, 192.168.1.0/24, 192.168.2.0/24, ...
// ...
// without this code - next IP would be 1 if last item in 'ips' has format above
QStringList vpnIps;
for (const auto &ip : ips) {
vpnIps.append(ip.split(",", Qt::SkipEmptyParts).first().trimmed());
}
ips = vpnIps;
// Calc next IP address
if (ips.isEmpty()) {
nextIpNumber = "2";
} else {
lastIp = ips.last();
int next = ips.last().split(".").last().toInt() + 1;
if (next > 254) {
errorCode = ErrorCode::AddressPoolError;
return connData;
}
nextIpNumber = QString::number(next);
}
quint8 lastOctet = static_cast<quint8>(lastIp.toIPv4Address());
switch (lastOctet) {
case 254: result.setAddress(lastIp.toIPv4Address() + 3); break;
case 255: result.setAddress(lastIp.toIPv4Address() + 2); break;
default: result.setAddress(lastIp.toIPv4Address() + 1); break;
}
QString subnetIp = containerConfig.value(m_protocolName).toObject().value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress);
{
QStringList l = subnetIp.split(".", Qt::SkipEmptyParts);
if (l.isEmpty()) {
errorCode = ErrorCode::AddressPoolError;
return connData;
}
l.removeLast();
l.append(nextIpNumber);
return result;
}();
connData.clientIP = nextIp.toString();
connData.clientIP = l.join(".");
}
// Get keys
connData.serverPubKey =
m_serverController->getTextFileFromContainer(container, credentials, m_serverPublicKeyPath, errorCode);
connData.serverPubKey = m_serverController->getTextFileFromContainer(container, credentials, m_serverPublicKeyPath, errorCode);
connData.serverPubKey.replace("\n", "");
if (errorCode != ErrorCode::NoError) {
return connData;
@@ -168,12 +161,10 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
return connData;
}
QString script = QString("sudo docker exec -i $CONTAINER_NAME bash -c 'wg syncconf wg0 <(wg-quick strip %1)'")
.arg(m_serverConfigPath);
QString script = QString("sudo docker exec -i $CONTAINER_NAME bash -c 'wg syncconf wg0 <(wg-quick strip %1)'").arg(m_serverConfigPath);
errorCode = m_serverController->runScript(
credentials,
m_serverController->replaceVars(script, m_serverController->genVarsForScript(credentials, container)));
credentials, m_serverController->replaceVars(script, m_serverController->genVarsForScript(credentials, container)));
return connData;
}
@@ -182,8 +173,8 @@ QString WireguardConfigurator::createConfig(const ServerCredentials &credentials
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString scriptData = amnezia::scriptData(m_configTemplate, container);
QString config = m_serverController->replaceVars(
scriptData, m_serverController->genVarsForScript(credentials, container, containerConfig));
QString config =
m_serverController->replaceVars(scriptData, m_serverController->genVarsForScript(credentials, container, containerConfig));
ConnectionData connData = prepareWireguardConfig(credentials, container, containerConfig, errorCode);
if (errorCode != ErrorCode::NoError) {
@@ -217,16 +208,16 @@ QString WireguardConfigurator::createConfig(const ServerCredentials &credentials
return QJsonDocument(jConfig).toJson();
}
QString WireguardConfigurator::processConfigWithLocalSettings(const QPair<QString, QString> &dns,
const bool isApiConfig, QString &protocolConfigString)
QString WireguardConfigurator::processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString)
{
processConfigWithDnsSettings(dns, protocolConfigString);
return protocolConfigString;
}
QString WireguardConfigurator::processConfigWithExportSettings(const QPair<QString, QString> &dns,
const bool isApiConfig, QString &protocolConfigString)
QString WireguardConfigurator::processConfigWithExportSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString)
{
processConfigWithDnsSettings(dns, protocolConfigString);

View File

@@ -1,7 +1,6 @@
#ifndef WIREGUARD_CONFIGURATOR_H
#define WIREGUARD_CONFIGURATOR_H
#include <QHostAddress>
#include <QObject>
#include <QProcessEnvironment>
@@ -13,8 +12,8 @@ class WireguardConfigurator : public ConfiguratorBase
{
Q_OBJECT
public:
WireguardConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController,
bool isAwg, QObject *parent = nullptr);
WireguardConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, bool isAwg,
QObject *parent = nullptr);
struct ConnectionData
{
@@ -27,18 +26,15 @@ public:
QString port;
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode);
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode);
QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString);
QString processConfigWithExportSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString);
QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig, QString &protocolConfigString);
QString processConfigWithExportSettings(const QPair<QString, QString> &dns, const bool isApiConfig, QString &protocolConfigString);
static ConnectionData genClientKeys();
private:
QList<QHostAddress> getIpsFromConf(const QString &input);
ConnectionData prepareWireguardConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode);

View File

@@ -93,9 +93,6 @@ 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()
@@ -135,8 +132,7 @@ 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_apiDevicesModel, 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_apiServicesModel, m_settings));

View File

@@ -25,9 +25,8 @@
#include "ui/models/protocols/ikev2ConfigModel.h"
#endif
#include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/api/apiDevicesModel.h"
#include "ui/models/api/apiServicesModel.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/appSplitTunnelingModel.h"
#include "ui/models/clientManagementModel.h"
#include "ui/models/protocols/awgConfigModel.h"
@@ -118,7 +117,6 @@ private:
QSharedPointer<ApiServicesModel> m_apiServicesModel;
QSharedPointer<ApiCountryModel> m_apiCountryModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel;
QScopedPointer<ShadowSocksConfigModel> m_shadowSocksConfigModel;

View File

@@ -26,10 +26,6 @@ namespace
constexpr char apiPayload[] = "api_payload";
constexpr char keyPayload[] = "key_payload";
}
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found.");
}
GatewayController::GatewayController(const QString &gatewayEndpoint, bool isDevEnvironment, int requestTimeoutMsecs, QObject *parent)
@@ -152,7 +148,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
QByteArray encryptedResponseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, false)) {
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
@@ -161,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<QSslError> &nestedSslErrors) {
encryptedResponseBody = nestedReply->readAll();
reply = nestedReply;
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
sslErrors = nestedSslErrors;
return false;
reply = nestedReply;
return true;
}
return true;
return false;
};
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
@@ -198,16 +194,16 @@ QStringList GatewayController::getProxyUrls()
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList proxyStorageUrls;
QStringList proxyStorageUrl;
if (m_isDevEnvironment) {
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", ");
proxyStorageUrl = QStringList { DEV_S3_ENDPOINT };
} else {
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", ");
proxyStorageUrl = QStringList { PROD_S3_ENDPOINT };
}
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
for (const auto &proxyStorageUrl : proxyStorageUrls) {
for (const auto &proxyStorageUrl : proxyStorageUrl) {
request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request);
@@ -216,45 +212,45 @@ QStringList GatewayController::getProxyUrls()
wait.exec();
if (reply->error() == QNetworkReply::NetworkError::NoError) {
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" << encryptedResponseBody;
continue;
}
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
} else {
reply->deleteLater();
break;
}
reply->deleteLater();
}
return {};
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" << encryptedResponseBody;
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,
@@ -266,15 +262,6 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
} else if (responseBody.contains("html")) {
qDebug() << "The response contains an html tag";
return true;
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
} else {
return true;
}
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
@@ -309,7 +296,7 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
if (replyProcessingFunction(reply, sslErrors)) {
if (!replyProcessingFunction(reply, sslErrors)) {
break;
}
}

View File

@@ -757,6 +757,10 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, DockerContainer container)
{
if (credentials.userName == "root") {
return ErrorCode::NoError;
}
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
@@ -770,16 +774,8 @@ ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, D
const QString scriptData = amnezia::scriptData(SharedScriptType::check_user_in_sudo);
ErrorCode error = runScript(credentials, replaceVars(scriptData, genVarsForScript(credentials)), cbReadStdOut, cbReadStdErr);
if (credentials.userName != "root" && stdOut.contains("sudo:") && !stdOut.contains("uname:") && stdOut.contains("not found"))
return ErrorCode::SudoPackageIsNotPreinstalled;
if (credentials.userName != "root" && !stdOut.contains("sudo") && !stdOut.contains("wheel"))
if (!stdOut.contains("sudo"))
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required"))
return ErrorCode::ServerUserPasswordRequired;
return error;
}

View File

@@ -54,10 +54,6 @@ namespace amnezia
ServerCancelInstallation = 204,
ServerUserNotInSudo = 205,
ServerPacketManagerError = 206,
SudoPackageIsNotPreinstalled = 207,
ServerUserDirectoryNotAccessible = 208,
ServerUserNotAllowedInSudoers = 209,
ServerUserPasswordRequired = 210,
// Ssh connection errors
SshRequestDeniedError = 300,

View File

@@ -20,12 +20,8 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ErrorCode::ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user is not a member of the sudo group"); break;
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Package manager error"); break;
case(ErrorCode::SudoPackageIsNotPreinstalled): errorMessage = QObject::tr("The sudo package is not pre-installed"); break;
case(ErrorCode::ServerUserDirectoryNotAccessible): errorMessage = QObject::tr("The server user's home directory is not accessible"); break;
case(ErrorCode::ServerUserNotAllowedInSudoers): errorMessage = QObject::tr("Action not allowed in sudoers"); break;
case(ErrorCode::ServerUserPasswordRequired): errorMessage = QObject::tr("The user's password is required"); break;
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 3H4C2.89543 3 2 3.89543 2 5V15C2 16.1046 2.89543 17 4 17H20C21.1046 17 22 16.1046 22 15V5C22 3.89543 21.1046 3 20 3Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 21H16" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17V21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 522 B

View File

@@ -14,15 +14,10 @@ extension UIApplication {
var keyWindows: [UIWindow] {
connectedScenes
.compactMap {
guard let windowScene = $0 as? UIWindowScene else { return nil }
if #available(iOS 15.0, *) {
guard let keywindow = windowScene.keyWindow else {
windowScene.windows.first?.makeKey()
return windowScene.windows.first
}
return keywindow
($0 as? UIWindowScene)?.keyWindow
} else {
return windowScene.windows.first { $0.isKeyWindow }
($0 as? UIWindowScene)?.windows.first { $0.isKeyWindow }
}
}
}

View File

@@ -229,8 +229,6 @@
<file>ui/qml/Pages2/PageSettingsApiSupport.qml</file>
<file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file>
<file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevices.qml</file>
<file>images/controls/monitor.svg</file>
</qresource>
<qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file>

View File

@@ -1,12 +1,2 @@
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); opt="--version";\
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\
else pm="uname"; opt="-a";\
fi;\
CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\
echo $LANG | grep -qE '^(en_US.UTF-8|C.UTF-8|C)$' || export LC_ALL=C;\
sudo -K;\
if [ "$CUR_USER" = "root" ] || ( groups "$CUR_USER" | grep -E '\<(sudo|wheel)\>' ); then \
sudo -nu $CUR_USER $pm $opt > /dev/null; sudo -n $pm $opt > /dev/null;\
fi
CUR_USER=$(whoami);\
groups $CUR_USER

View File

@@ -1,4 +1,4 @@
CUR_USER=$(whoami 2>/dev/null || echo ~ | sed 's/.*\///');\
CUR_USER=$(whoami);\
sudo mkdir -p $DOCKERFILE_FOLDER;\
sudo chown $CUR_USER $DOCKERFILE_FOLDER;\
if ! sudo docker network ls | grep -q amnezia-dns-net; then sudo docker network create \

View File

@@ -3334,8 +3334,8 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation>المستخدم ليس عضوًا في مجموعة sudo</translation>
<source>The user does not have permission to use sudo</source>
<translation>ليس لدي المستخدم الصلحيات لأستخدام sudo</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>
@@ -3399,7 +3399,7 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<source>Server error: Packet manager error</source>
<translation>خطأ في الخادم: خطأ في مدير الحزم</translation>
</message>
<message>

View File

@@ -3468,8 +3468,8 @@ It&apos;s okay as long as it&apos;s from someone you trust.</source>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation>کاربر عضو گروه sudo نیست</translation>
<source>The user does not have permission to use sudo</source>
<translation>The user does not have permission to use sudo</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>
@@ -3590,8 +3590,8 @@ It&apos;s okay as long as it&apos;s from someone you trust.</source>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<translation>خطای سرور: خطای مدیر بسته</translation>
<source>Server error: Packet manager error</source>
<translation>Server error: Packet manager error</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="34"/>

View File

@@ -3434,13 +3434,13 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation> sudo </translation>
<source>The user does not have permission to use sudo</source>
<translation> sudo ि </translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<translation> ि: ि</translation>
<source>Server error: Packet manager error</source>
<translation> ि: ि</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>

View File

@@ -3330,8 +3330,8 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation> sudo </translation>
<source>The user does not have permission to use sudo</source>
<translation> sudo ကက</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>
@@ -3395,8 +3395,8 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<translation> - Package manager </translation>
<source>Server error: Packet manager error</source>
<translation> မှု: Packet Manager </translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="34"/>

File diff suppressed because it is too large Load Diff

View File

@@ -3700,13 +3700,13 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation>Користувач не входить до групи sudo</translation>
<source>The user does not have permission to use sudo</source>
<translation>The user does not have permission to use sudo</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<translation>Помилка сервера: Помилка менеджера пакетів</translation>
<source>Server error: Packet manager error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>

View File

@@ -3433,8 +3433,8 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation>صارف sudo گروپ کا رکن نہیں ہے</translation>
<source>The user does not have permission to use sudo</source>
<translation>صارف کو sudo استعمال کرنے کی اجازت نہیں ہے</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>
@@ -3498,7 +3498,7 @@ Already installed containers were found on the server. All installed containers
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<source>Server error: Packet manager error</source>
<translation>سرور خطا: پیکیج منیجر خطا</translation>
</message>
<message>

View File

@@ -3675,13 +3675,13 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="22"/>
<source>The user is not a member of the sudo group</source>
<translation> sudo </translation>
<source>The user does not have permission to use sudo</source>
<translation>root权限</translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="23"/>
<source>Server error: Package manager error</source>
<translation></translation>
<source>Server error: Packet manager error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="26"/>

View File

@@ -1,7 +1,7 @@
#include "apiConfigsController.h"
#include <QClipboard>
#include <QEventLoop>
#include <QClipboard>
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
@@ -251,7 +251,6 @@ 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) {
@@ -355,43 +354,6 @@ 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();
@@ -407,7 +369,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::AmneziaGateway) {
if (configSource == apiDefs::ConfigSource::Telegram) {
return updateServiceFromGateway(serverIndex, "", "");
} else {
m_serversModel->removeApiConfig(serverIndex);

View File

@@ -31,7 +31,6 @@ public slots:
bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex);
bool deactivateDevice();
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
bool isConfigValid();

View File

@@ -25,13 +25,11 @@ namespace
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel,
const QSharedPointer<ApiCountryModel> &apiCountryModel,
const QSharedPointer<ApiDevicesModel> &apiDevicesModel,
const std::shared_ptr<Settings> &settings, QObject *parent)
: QObject(parent),
m_serversModel(serversModel),
m_apiAccountInfoModel(apiAccountInfoModel),
m_apiCountryModel(apiCountryModel),
m_apiDevicesModel(apiDevicesModel),
m_settings(settings)
{
}
@@ -75,7 +73,6 @@ bool ApiSettingsController::getAccountInfo(bool reload)
if (reload) {
updateApiCountryModel();
updateApiDevicesModel();
}
return true;
@@ -86,8 +83,3 @@ void ApiSettingsController::updateApiCountryModel()
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
m_apiCountryModel->updateIssuedConfigsInfo(m_apiAccountInfoModel->getIssuedConfigsInfo());
}
void ApiSettingsController::updateApiDevicesModel()
{
m_apiDevicesModel->updateModel(m_apiAccountInfoModel->getIssuedConfigsInfo());
}

View File

@@ -5,7 +5,6 @@
#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
@@ -13,14 +12,13 @@ class ApiSettingsController : public QObject
Q_OBJECT
public:
ApiSettingsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel,
const QSharedPointer<ApiCountryModel> &apiCountryModel, const QSharedPointer<ApiDevicesModel> &apiDevicesModel,
const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
const QSharedPointer<ApiCountryModel> &apiCountryModel, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
~ApiSettingsController();
public slots:
bool getAccountInfo(bool reload);
void updateApiCountryModel();
void updateApiDevicesModel();
signals:
void errorOccurred(ErrorCode errorCode);
@@ -29,7 +27,6 @@ private:
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
QSharedPointer<ApiCountryModel> m_apiCountryModel;
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
std::shared_ptr<Settings> m_settings;
};

View File

@@ -27,6 +27,8 @@ 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";
@@ -51,13 +53,14 @@ 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;
}
@@ -342,7 +345,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data)
arr.push_back(containers);
QString hostName;
const static QRegularExpression hostNameRegExp("remote\\s+([^\\s]+)");
const static QRegularExpression hostNameRegExp("remote (.*) [0-9]*");
QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data);
if (hostNameMatch.hasMatch()) {
hostName = hostNameMatch.captured(1);

View File

@@ -36,7 +36,6 @@ namespace PageLoader
PageSettingsApiSupport,
PageSettingsApiInstructions,
PageSettingsApiNativeConfigs,
PageSettingsApiDevices,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,

View File

@@ -44,6 +44,7 @@ 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) {
@@ -74,6 +75,7 @@ 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));
}
@@ -122,6 +124,7 @@ 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"));
}

View File

@@ -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 seamless work, downloading large files, and watching videos. Access all websites and online resources. "
"Speeds up to 200 Mbps");
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.");
@@ -58,19 +58,6 @@ 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();
@@ -137,7 +124,6 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[ConnectedDevicesRole] = "connectedDevices";
roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsComponentVisibleRole] = "isComponentVisible";
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
return roles;
}

View File

@@ -17,8 +17,7 @@ public:
ConnectedDevicesRole,
ServiceDescriptionRole,
EndDateRole,
IsComponentVisibleRole,
HasExpiredWorkerRole
IsComponentVisibleRole
};
explicit ApiAccountInfoModel(QObject *parent = nullptr);

View File

@@ -44,9 +44,6 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
case IsIssuedRole: {
return isIssued;
}
case IsWorkerExpiredRole: {
return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated;
}
}
return QVariant();
@@ -117,6 +114,5 @@ QHash<int, QByteArray> ApiCountryModel::roleNames() const
roles[CountryCodeRole] = "countryCode";
roles[CountryImageCodeRole] = "countryImageCode";
roles[IsIssuedRole] = "isIssued";
roles[IsWorkerExpiredRole] = "isWorkerExpired";
return roles;
}

View File

@@ -14,8 +14,7 @@ public:
CountryNameRole = Qt::UserRole + 1,
CountryCodeRole,
CountryImageCodeRole,
IsIssuedRole,
IsWorkerExpiredRole
IsIssuedRole
};
explicit ApiCountryModel(QObject *parent = nullptr);

View File

@@ -1,90 +0,0 @@
#include "apiDevicesModel.h"
#include <QJsonObject>
#include "core/api/apiDefs.h"
#include "logger.h"
namespace
{
Logger logger("ApiDevicesModel");
constexpr QLatin1String gatewayAccount("gateway_account");
}
ApiDevicesModel::ApiDevicesModel(std::shared_ptr<Settings> 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<int>(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<int, QByteArray> ApiDevicesModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[OsVersionRole] = "osVersion";
roles[SupportTagRole] = "supportTag";
roles[CountryCodeRole] = "countryCode";
roles[LastUpdateRole] = "lastUpdate";
roles[IsCurrentDeviceRole] = "isCurrentDevice";
return roles;
}

View File

@@ -1,52 +0,0 @@
#ifndef APIDEVICESMODEL_H
#define APIDEVICESMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QVector>
#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> 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<int, QByteArray> roleNames() const override;
private:
struct IssuedConfigInfo
{
QString installationUuid;
QString workerLastUpdated;
QString lastDownloaded;
QString sourceType;
QString osVersion;
QString countryName;
QString countryCode;
};
QVector<IssuedConfigInfo> m_issuedConfigs;
std::shared_ptr<Settings> m_settings;
};
#endif // APIDEVICESMODEL_H

View File

@@ -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 classic VPN for seamless work, downloading large files, and watching videos. "
"Access all websites and online resources. Speeds up to %1 Mbps.")
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.")
.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 classic VPN for for seamless work, downloading large files, and watching videos. "
"Access all websites and online resources.");
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.");
} 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.");
}

View File

@@ -54,7 +54,7 @@ Rectangle {
Layout.rightMargin: 10
Layout.leftMargin: 10
text: qsTr("Amnezia Premium - for access to all websites and online resources")
text: qsTr("Amnezia Premium - for access to any website")
color: AmneziaStyle.color.pearlGray
lineHeight: 18

View File

@@ -27,6 +27,5 @@ 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)
}
}

View File

@@ -81,7 +81,7 @@ PageType {
actionButtonImage: "qrc:/images/controls/settings.svg"
headerText: root.processedServer.name
descriptionText: qsTr("Location for connection")
descriptionText: qsTr("Locations for connection")
actionButtonFunction: function() {
PageController.showBusyIndicator(true)

View File

@@ -1,105 +0,0 @@
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("Active Devices")
descriptionText: qsTr("Manage currently 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() {
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")
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)
}
}

View File

@@ -99,7 +99,7 @@ PageType {
Layout.leftMargin: 16
headerText: qsTr("How to connect on another device")
descriptionText: qsTr("Setup guides on the Amnezia website")
descriptionText: qsTr("Instructions on the Amnezia website")
}
}
@@ -107,6 +107,7 @@ PageType {
width: listView.width
LabelWithButtonType {
id: telegramButton
Layout.fillWidth: true
Layout.topMargin: 6

View File

@@ -45,8 +45,8 @@ PageType {
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Configuration Files")
descriptionText: qsTr("For router setup or the AmneziaWG app")
headerText: qsTr("Configuration files")
descriptionText: qsTr("To connect a router or AmneziaWG application")
}
}
@@ -58,9 +58,6 @@ 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"
@@ -123,13 +120,13 @@ PageType {
Layout.fillWidth: true
Layout.margins: 16
headerText: moreOptionsDrawer.countryName + qsTr(" configuration file")
headerText: qsTr("Configuration file ") + moreOptionsDrawer.countryName
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Generate a new configuration file")
text: qsTr("Create a new")
descriptionText: qsTr("The previously created one will stop working")
clickedFunction: function() {
@@ -193,15 +190,9 @@ PageType {
}
function showQuestion(isConfigIssue, countryCode, countryName) {
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 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() {

View File

@@ -26,15 +26,15 @@ 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"
readonly property string objectImageSource: "qrc:/images/controls/map-pin.svg"
}
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,9 +42,9 @@ PageType {
QtObject {
id: deviceCountObject
readonly property string title: qsTr("Active Connections")
readonly property string title: qsTr("Connected devices")
readonly property string contentKey: "connectedDevices"
readonly property string objectImageSource: "qrc:/images/controls/monitor.svg"
readonly property string objectImageSource: "qrc:/images/controls/gauge.svg"
}
property var processedServer
@@ -158,32 +158,15 @@ 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: warning.visible ? 16 : 32
Layout.topMargin: 32
visible: false //footer.isVisibleForAmneziaFree
text: qsTr("Subscription Key")
text: qsTr("Subscription key")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
@@ -191,7 +174,7 @@ PageType {
shareConnectionDrawer.openTriggered()
shareConnectionDrawer.isSelfHostedConfig = false;
shareConnectionDrawer.shareButtonText = qsTr("Save VPN key as a file")
shareConnectionDrawer.shareButtonText = qsTr("Save VPN key to file")
shareConnectionDrawer.copyButtonText = qsTr("Copy VPN key")
@@ -209,13 +192,13 @@ PageType {
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: warning.visible ? 16 : 32
Layout.topMargin: 32
visible: footer.isVisibleForAmneziaFree
text: qsTr("Configuration Files")
text: qsTr("Configuration files")
descriptionText: qsTr("Manage configuration files")
descriptionText: qsTr("To connect a router or AmneziaWG application")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
@@ -228,26 +211,6 @@ PageType {
visible: footer.isVisibleForAmneziaFree
}
LabelWithButtonType {
Layout.fillWidth: true
visible: footer.isVisibleForAmneziaFree
text: qsTr("Active Devices")
descriptionText: qsTr("Manage currently 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
@@ -265,8 +228,6 @@ PageType {
LabelWithButtonType {
Layout.fillWidth: true
visible: footer.isVisibleForAmneziaFree
text: qsTr("How to connect on another device")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
@@ -275,9 +236,7 @@ PageType {
}
}
DividerType {
visible: footer.isVisibleForAmneziaFree
}
DividerType {}
BasicButtonType {
id: resetButton
@@ -329,17 +288,16 @@ PageType {
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Unlink this device")
text: qsTr("Deactivate the subscription on this device")
clickedFunc: function() {
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 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("Cannot unlink device during active connection"))
PageController.showNotificationMessage(qsTr("The next time the “Connect” button is pressed, the device will be activated again"))
} else {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) {
@@ -351,7 +309,7 @@ PageType {
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}

View File

@@ -16,110 +16,92 @@ import "../Components"
PageType {
id: root
QtObject {
id: telegram
ColumnLayout {
id: backButtonLayout
readonly property string title: qsTr("Telegram")
readonly property string description: "@" + ApiAccountInfoModel.getTelegramBotLink()
readonly property string link: "https://t.me/" + ApiAccountInfoModel.getTelegramBotLink()
}
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
QtObject {
id: techSupport
readonly property string title: qsTr("Email")
readonly property string description: qsTr("support@amnezia.org")
readonly property string link: "mailto:support@amnezia.org"
}
QtObject {
id: paymentSupport
readonly property string title: qsTr("Email Billing & Orders")
readonly property string description: qsTr("help@vpnpay.io")
readonly property string link: "mailto:help@vpnpay.io"
}
QtObject {
id: site
readonly property string title: qsTr("Website")
readonly property string description: qsTr("amnezia.org")
readonly property string link: LanguageModel.getCurrentSiteUrl()
}
property list<QtObject> supportModel: [
telegram,
techSupport,
paymentSupport,
site
]
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20
anchors.bottomMargin: 24
model: supportModel
header: ColumnLayout {
width: listView.width
BackButtonType {
id: backButton
}
HeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Support")
descriptionText: qsTr("Our technical support specialists are available to assist you at any time")
}
}
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
Layout.fillWidth: true
text: title
descriptionText: description
rightImageSource: "qrc:/images/controls/external-link.svg"
clickedFunction: function() {
Qt.openUrlExternally(link)
}
}
DividerType {}
BackButtonType {
id: backButton
}
footer: ColumnLayout {
width: listView.width
HeaderType {
id: header
LabelWithButtonType {
id: supportUuid
Layout.fillWidth: true
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Support tag")
descriptionText: SettingsController.getInstallationUuid()
headerText: qsTr("Support")
descriptionText: qsTr("Our technical support specialists are ready to help you at any time")
}
descriptionOnTop: true
LabelWithButtonType {
Layout.fillWidth: true
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: AmneziaStyle.color.paleGray
readonly property string telegramBotLink: ApiAccountInfoModel.getTelegramBotLink()
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
}
text: qsTr("Telegram")
descriptionText: "@" + telegramBotLink
rightImageSource: "qrc:/images/controls/external-link.svg"
clickedFunction: function() {
Qt.openUrlExternally("https://t.me/" + telegramBotLink)
}
}
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()
}
}
}

View File

@@ -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"))
}
}
}