From 1f08d78b43f6e3321e6ccb0c9bd97863ac0f04e4 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Sun, 22 Sep 2024 22:52:59 +0100 Subject: [PATCH 001/208] wip --- client/ui/controllers/installController.cpp | 27 +--- client/utilities.cpp | 139 ++++++++++++++++---- client/utilities.h | 6 +- 3 files changed, 120 insertions(+), 52 deletions(-) mode change 100644 => 100755 client/ui/controllers/installController.cpp mode change 100644 => 100755 client/utilities.cpp mode change 100644 => 100755 client/utilities.h diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp old mode 100644 new mode 100755 index c6f17057..5a64d092 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -33,31 +33,6 @@ namespace constexpr char apiConfig[] = "api_config"; } - -#ifdef Q_OS_WINDOWS - QString getNextDriverLetter() - { - QProcess drivesProc; - drivesProc.start("wmic logicaldisk get caption"); - drivesProc.waitForFinished(); - QString drives = drivesProc.readAll(); - qDebug() << drives; - - QString letters = "CFGHIJKLMNOPQRSTUVWXYZ"; - QString letter; - for (int i = letters.size() - 1; i > 0; i--) { - letter = letters.at(i); - if (!drives.contains(letter + ":")) - break; - } - if (letter == "C:") { - // set err info - qDebug() << "Can't find free drive letter"; - return ""; - } - return letter; - } -#endif } InstallController::InstallController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, @@ -667,7 +642,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw QString hostname = serverCredentials.hostName; #ifdef Q_OS_WINDOWS - mountPath = getNextDriverLetter() + ":"; + mountPath = Utils::getNextDriverLetter() + ":"; // QString cmd = QString("net use \\\\sshfs\\%1@x.x.x.x!%2 /USER:%1 %3") // .arg(labelTftpUserNameText()) // .arg(labelTftpPortText()) diff --git a/client/utilities.cpp b/client/utilities.cpp old mode 100644 new mode 100755 index 4047365f..bcae2ed5 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -10,7 +10,62 @@ #include #include "utilities.h" -#include "version.h" + +#ifdef Q_OS_WINDOWS +QString printErrorMessage(DWORD errorCode) { + LPVOID lpMsgBuf; + + DWORD dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + + DWORD dwLanguageId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + + FormatMessageW( + dwFlags, + NULL, + errorCode, + dwLanguageId, + (LPWSTR)&lpMsgBuf, + 0, + NULL + ); + + QString errorMsg = QString::fromWCharArray((LPCWSTR)lpMsgBuf); + LocalFree(lpMsgBuf); + return errorMsg.trimmed(); +} + +QString Utils::getNextDriverLetter() +{ + DWORD drivesBitmask = GetLogicalDrives(); + if (drivesBitmask == 0) { + DWORD error = GetLastError(); + qDebug() << "GetLogicalDrives failed. Error code:" << error; + return ""; + } + + QString letters = "FGHIJKLMNOPQRSTUVWXYZ"; + QString availableLetter; + + for (int i = letters.size() - 1; i >= 0; --i) { + QChar letterChar = letters.at(i); + int driveIndex = letterChar.toLatin1() - 'A'; + + if ((drivesBitmask & (1 << driveIndex)) == 0) { + availableLetter = letterChar; + break; + } + } + + if (availableLetter.isEmpty()) { + qDebug() << "Can't find free drive letter"; + return ""; + } + + return availableLetter; +} +#endif QString Utils::getRandomString(int len) { @@ -109,30 +164,34 @@ QString Utils::usrExecutable(const QString &baseName) bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) { #ifdef Q_OS_WIN - QProcess process; - process.setReadChannel(QProcess::StandardOutput); - process.setProcessChannelMode(QProcess::MergedChannels); - process.start("wmic.exe", - QStringList() << "/OUTPUT:STDOUT" - << "PROCESS" - << "get" - << "Caption"); - process.waitForStarted(); - process.waitForFinished(); - QString processData(process.readAll()); - QStringList processList = processData.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); - foreach (const QString &rawLine, processList) { - const QString line = rawLine.simplified(); - if (line.isEmpty()) { - continue; - } + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) { + qWarning() << "Utils::processIsRunning error CreateToolhelp32Snapshot"; + return false; + } - if (line == fileName) { + PROCESSENTRY32W pe32; + pe32.dwSize = sizeof(PROCESSENTRY32W); + + if (!Process32FirstW(hSnapshot, &pe32)) { + CloseHandle(hSnapshot); + qWarning() << "Utils::processIsRunning error Process32FirstW"; + return false; + } + + do { + QString exeFile = QString::fromWCharArray(pe32.szExeFile); + + if (exeFile.compare(fileName, Qt::CaseInsensitive) == 0) { + CloseHandle(hSnapshot); return true; } - } + } while (Process32NextW(hSnapshot, &pe32)); + + CloseHandle(hSnapshot); return false; -#elif defined(Q_OS_IOS) + +#elif defined(Q_OS_IOS) || defined(Q_OS_ANDROID) return false; #else QProcess process; @@ -150,12 +209,44 @@ bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) #endif } -void Utils::killProcessByName(const QString &name) +bool Utils::killProcessByName(const QString &name) { qDebug().noquote() << "Kill process" << name; #ifdef Q_OS_WIN - QProcess::execute("taskkill", QStringList() << "/IM" << name << "/F"); -#elif defined Q_OS_IOS + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) + return false; + + PROCESSENTRY32W pe32; + pe32.dwSize = sizeof(PROCESSENTRY32W); + + bool success = false; + + if (Process32FirstW(hSnapshot, &pe32)) { + do { + QString exeFile = QString::fromWCharArray(pe32.szExeFile); + + if (exeFile.compare(name, Qt::CaseInsensitive) == 0) { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID); + if (hProcess != NULL) { + if (TerminateProcess(hProcess, 0)) { + success = true; + } else { + DWORD error = GetLastError(); + qCritical() << "Can't terminate process" << exeFile << "(PID:" << pe32.th32ProcessID << "). Error:" << printErrorMessage(error); + } + CloseHandle(hProcess); + } else { + DWORD error = GetLastError(); + qCritical() << "Can't open process for termination" << exeFile << "(PID:" << pe32.th32ProcessID << "). Error:" << printErrorMessage(error); + } + } + } while (Process32NextW(hSnapshot, &pe32)); + } + + CloseHandle(hSnapshot); + return success; +#elif defined Q_OS_IOS || defined(Q_OS_ANDROID) return; #else QProcess::execute(QString("pkill %1").arg(name)); diff --git a/client/utilities.h b/client/utilities.h old mode 100644 new mode 100755 index 9bf8c82a..b3e3b50b --- a/client/utilities.h +++ b/client/utilities.h @@ -7,7 +7,8 @@ #include #ifdef Q_OS_WIN - #include "Windows.h" +#include +#include #endif class Utils : public QObject @@ -27,7 +28,7 @@ public: static bool initializePath(const QString &path); static bool processIsRunning(const QString &fileName, const bool fullFlag = false); - static void killProcessByName(const QString &name); + static bool killProcessByName(const QString &name); static QString openVpnExecPath(); static QString wireguardExecPath(); @@ -36,6 +37,7 @@ public: #ifdef Q_OS_WIN static bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent); + static QString getNextDriverLetter(); #endif }; From 3aa8a46f6e36ea62a0d3f010ed7a05d72aa55188 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Mon, 23 Sep 2024 01:19:46 +0300 Subject: [PATCH 002/208] wip --- client/ui/controllers/connectionController.cpp | 14 +++++++------- client/utilities.cpp | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index c7f95000..db8beed1 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -34,13 +34,13 @@ 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)) -// { -// emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); -// return; -// } -// #endif +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) + { + emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); + return; + } +#endif int serverIndex = m_serversModel->getDefaultServerIndex(); QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex); diff --git a/client/utilities.cpp b/client/utilities.cpp index bcae2ed5..ed91f5fc 100755 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -247,7 +247,7 @@ bool Utils::killProcessByName(const QString &name) CloseHandle(hSnapshot); return success; #elif defined Q_OS_IOS || defined(Q_OS_ANDROID) - return; + return false; #else QProcess::execute(QString("pkill %1").arg(name)); #endif From 1542adba82a0f7f4f48e381406799c86c90d7bec Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Mon, 23 Sep 2024 00:44:25 +0100 Subject: [PATCH 003/208] Switched to secure PRNG & some pw len increased --- client/ui/controllers/installController.cpp | 4 ++-- client/utilities.cpp | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index c6f17057..0126a5b0 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -135,10 +135,10 @@ void InstallController::install(DockerContainer container, int port, TransportPr containerConfig[config_key::transportPacketMagicHeader] = transportPacketMagicHeader; } else if (container == DockerContainer::Sftp) { containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName); - containerConfig.insert(config_key::password, Utils::getRandomString(10)); + containerConfig.insert(config_key::password, Utils::getRandomString(16)); } else if (container == DockerContainer::Socks5Proxy) { containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName); - containerConfig.insert(config_key::password, Utils::getRandomString(10)); + containerConfig.insert(config_key::password, Utils::getRandomString(16)); } config.insert(config_key::container, ContainerProps::containerToString(container)); diff --git a/client/utilities.cpp b/client/utilities.cpp index 4047365f..cb50235a 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -14,14 +14,13 @@ QString Utils::getRandomString(int len) { - const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); - + const QString possibleCharacters = QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); QString randomString; + for (int i = 0; i < len; ++i) { - quint32 index = QRandomGenerator::global()->generate() % possibleCharacters.length(); - QChar nextChar = possibleCharacters.at(index); - randomString.append(nextChar); + randomString.append(possibleCharacters.at(QRandomGenerator::system()->bounded(possibleCharacters.length()))); } + return randomString; } From 2763da960f608dd2b6b27b7751f6dd63d1ba86c0 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 2 Oct 2024 13:20:16 +0800 Subject: [PATCH 004/208] chore: added clear() after extractConfigFromData() on android --- client/amnezia_application.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 2d06b443..4e25097d 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -111,10 +111,11 @@ void AmneziaApplication::init() qFatal("Android controller initialization failed"); } - connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) { - m_pageController->goToPageHome(); + connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); + data.clear(); + emit m_pageController->goToPageViewConfig(); }); m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); @@ -122,16 +123,16 @@ void AmneziaApplication::init() #ifdef Q_OS_IOS IosController::Instance()->initialize(); - connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) { - m_pageController->goToPageHome(); + connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); + emit m_pageController->goToPageViewConfig(); }); - connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) { - m_pageController->goToPageHome(); + connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) { + emit m_pageController->goToPageHome(); m_pageController->goToPageSettingsBackup(); - m_settingsController->importBackupFromOutside(filePath); + emit m_settingsController->importBackupFromOutside(filePath); }); QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); From dce08b3eccec04007e30ec8a8f2187106ebc1472 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Sun, 6 Oct 2024 13:19:06 +0800 Subject: [PATCH 005/208] added processing of auth_data section when requesting api config - fixed saving api_config section when processing backend response --- client/core/controllers/apiController.cpp | 40 ++++++++++++------- client/core/controllers/apiController.h | 2 +- .../ui/controllers/connectionController.cpp | 3 ++ client/ui/controllers/importController.cpp | 3 +- client/ui/controllers/installController.cpp | 13 +++--- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 5cdaa7ae..47197ad9 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -40,6 +40,9 @@ namespace constexpr char apiPayload[] = "api_payload"; constexpr char keyPayload[] = "key_payload"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; } const QStringList proxyStorageUrl = { "" }; @@ -94,8 +97,8 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); } else if (protocol == configKey::awg) { configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); - auto serverConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - auto containers = serverConfig.value(config_key::containers).toArray(); + auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + auto containers = newServerConfig.value(config_key::containers).toArray(); if (containers.isEmpty()) { return; // todo process error } @@ -114,25 +117,30 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); container[containerName] = containerConfig; containers.replace(0, container); - serverConfig[config_key::containers] = containers; - configStr = QString(QJsonDocument(serverConfig).toJson()); + newServerConfig[config_key::containers] = containers; + configStr = QString(QJsonDocument(newServerConfig).toJson()); } - QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1); - serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2); - serverConfig[config_key::containers] = apiConfig.value(config_key::containers); - serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName); + 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 (apiConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) { - serverConfig[config_key::configVersion] = apiConfig.value(config_key::configVersion); - serverConfig[config_key::description] = apiConfig.value(config_key::description); - serverConfig[config_key::name] = apiConfig.value(config_key::name); + if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::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 = apiConfig.value(config_key::defaultContainer).toString(); + 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); + serverConfig[configKey::apiConfig] = apiConfig; + return; } @@ -316,7 +324,8 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) } ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig) + const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, + QJsonObject &serverConfig) { #ifdef Q_OS_IOS IosController::Instance()->requestInetAccess(); @@ -339,6 +348,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } apiPayload[configKey::serviceType] = serviceType; apiPayload[configKey::uuid] = installationUuid; + apiPayload[configKey::authData] = authData; QSimpleCrypto::QBlockCipher blockCipher; QByteArray key = blockCipher.generatePrivateSalt(32); diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h index 1f811498..bcb25f96 100644 --- a/client/core/controllers/apiController.h +++ b/client/core/controllers/apiController.h @@ -21,7 +21,7 @@ public slots: ErrorCode getServicesList(QByteArray &responseBody); ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType, - const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig); + const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig); signals: void errorOccurred(ErrorCode errorCode); diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index c7f95000..a51556a1 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -51,6 +51,9 @@ void ConnectionController::openConnection() if (configVersion == ApiConfigSources::Telegram && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromTelegram(); + } else if (configVersion == ApiConfigSources::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 end_date event"; if (configVersion == ApiConfigSources::Telegram) { diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 261551ea..2b7681eb 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -39,11 +39,12 @@ namespace const QString amneziaConfigPatternUserName = "userName"; const QString amneziaConfigPatternPassword = "password"; const QString amneziaFreeConfigPattern = "api_key"; + const QString amneziaPremiumConfigPattern = "auth_data"; const QString backupPattern = "Servers/serversList"; if (config.contains(backupPattern)) { return ConfigTypes::Backup; - } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) + } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) || config.contains(amneziaPremiumConfigPattern) || (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName) && config.contains(amneziaConfigPatternPassword))) { return ConfigTypes::Amnezia; diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index c6f17057..628ea59d 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -32,6 +32,7 @@ namespace constexpr char availableCountries[] = "available_countries"; constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; } #ifdef Q_OS_WINDOWS @@ -826,7 +827,7 @@ bool InstallController::installServiceFromApi() ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), - m_apiServicesModel->getSelectedServiceProtocol(), "", serverConfig); + m_apiServicesModel->getSelectedServiceProtocol(), "", QJsonObject(), serverConfig); if (errorCode != ErrorCode::NoError) { emit installationErrorOccurred(errorCode); return false; @@ -853,19 +854,19 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + 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, 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::serviceInfo, apiConfig.value(configKey::serviceInfo)); newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode)); newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); From 71995ce420d823d5608c848ce0be902b3a2d6762 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 9 Oct 2024 13:13:08 +0800 Subject: [PATCH 006/208] bugfix: fixed path to log folder for wireguard on windows --- client/platforms/windows/windowscommons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp index c0a14dda..4c0d8176 100644 --- a/client/platforms/windows/windowscommons.cpp +++ b/client/platforms/windows/windowscommons.cpp @@ -21,7 +21,7 @@ #include "platforms/windows/windowsutils.h" constexpr const char* VPN_NAME = "AmneziaVPN"; -constexpr const char* WIREGUARD_DIR = "WireGuard"; +constexpr const char* WIREGUARD_DIR = "AmneziaWG"; constexpr const char* DATA_DIR = "Data"; namespace { From 399a8c6d287504cfe26ea3a939041da30fed211e Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 11 Oct 2024 05:58:30 +0400 Subject: [PATCH 007/208] bugfix: fixed qml warnings when loading user list on PageShare (#1119) --- client/ui/models/clientManagementModel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7d3be2cb..7445d60f 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -77,6 +77,7 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co { beginResetModel(); m_clientsTable = QJsonArray(); + endResetModel(); ErrorCode error = ErrorCode::NoError; @@ -90,10 +91,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co const QByteArray clientsTableString = serverController->getTextFileFromContainer(container, credentials, clientsTableFile, error); if (error != ErrorCode::NoError) { logger.error() << "Failed to get the clientsTable file from the server"; - endResetModel(); return error; } + beginResetModel(); m_clientsTable = QJsonDocument::fromJson(clientsTableString).array(); if (m_clientsTable.isEmpty()) { @@ -601,5 +602,6 @@ QHash ClientManagementModel::roleNames() const roles[LatestHandshakeRole] = "latestHandshake"; roles[DataReceivedRole] = "dataReceived"; roles[DataSentRole] = "dataSent"; + roles[AllowedIpsRole] = "allowedIps"; return roles; } From 694e781beb8e2d065dfb6d5a7ec1b24c1873727d Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 11 Oct 2024 05:58:53 +0400 Subject: [PATCH 008/208] bugfix: fixed path to log folder for wireguard on windows (#1137) --- client/platforms/windows/windowscommons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp index c0a14dda..4c0d8176 100644 --- a/client/platforms/windows/windowscommons.cpp +++ b/client/platforms/windows/windowscommons.cpp @@ -21,7 +21,7 @@ #include "platforms/windows/windowsutils.h" constexpr const char* VPN_NAME = "AmneziaVPN"; -constexpr const char* WIREGUARD_DIR = "WireGuard"; +constexpr const char* WIREGUARD_DIR = "AmneziaWG"; constexpr const char* DATA_DIR = "Data"; namespace { From 7b838e77a02fa01ef8a157c109201d0fdf4a4e96 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 13 Oct 2024 15:14:43 +0400 Subject: [PATCH 009/208] bugfix: removed the importErrorOccurred() signal overload, since qml does not know how to handle signal overloads (#1111) --- client/core/defs.h | 1 + client/core/errorstrings.cpp | 1 + client/ui/controllers/importController.cpp | 6 +++--- client/ui/controllers/importController.h | 1 - client/ui/controllers/installController.cpp | 2 +- client/ui/controllers/installController.h | 2 +- client/ui/qml/Pages2/PageSetupWizardViewConfig.qml | 2 +- client/ui/qml/Pages2/PageStart.qml | 4 ++++ 8 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/core/defs.h b/client/core/defs.h index ebc07f4b..802eca45 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -96,6 +96,7 @@ namespace amnezia // import and install errors ImportInvalidConfigError = 900, + ImportOpenConfigError = 901, // Android errors AndroidError = 1000, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 8c16d786..00e94995 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -50,6 +50,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break; 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(""); break; // Android errors case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 261551ea..168e3564 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -84,7 +84,7 @@ bool ImportController::extractConfigFromFile(const QString &fileName) return extractConfigFromData(data); } - emit importErrorOccurred(tr("Unable to open file"), false); + emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); return false; } @@ -188,12 +188,12 @@ bool ImportController::extractConfigFromData(QString data) if (!m_serversModel->getServersCount()) { emit restoreAppConfig(config.toUtf8()); } else { - emit importErrorOccurred(tr("Invalid configuration file"), false); + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); } break; } case ConfigTypes::Invalid: { - emit importErrorOccurred(tr("Invalid configuration file"), false); + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); break; } } diff --git a/client/ui/controllers/importController.h b/client/ui/controllers/importController.h index 61205253..05e320a5 100644 --- a/client/ui/controllers/importController.h +++ b/client/ui/controllers/importController.h @@ -54,7 +54,6 @@ public slots: signals: void importFinished(); - void importErrorOccurred(const QString &errorMessage, bool goToPageHome); void importErrorOccurred(ErrorCode errorCode, bool goToPageHome); void qrDecodingFinished(); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index c6f17057..31aa1fb1 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -768,7 +768,7 @@ bool InstallController::checkSshConnection(QSharedPointer serv } else { if (output.contains(tr("Please login as the user"))) { output.replace("\n", ""); - emit installationErrorOccurred(output); + emit wrongInstallationUser(output); return false; } } diff --git a/client/ui/controllers/installController.h b/client/ui/controllers/installController.h index 7eea216a..d7ab3553 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -75,8 +75,8 @@ signals: void removeAllContainersFinished(const QString &finishedMessage); void removeProcessedContainerFinished(const QString &finishedMessage); - void installationErrorOccurred(const QString &errorMessage); void installationErrorOccurred(ErrorCode errorCode); + void wrongInstallationUser(const QString &message); void serverAlreadyExists(int serverIndex); diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 3aac1555..92048f36 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -37,7 +37,7 @@ PageType { Connections { target: ImportController - function onImportErrorOccurred(errorMessage, goToPageHome) { + function onImportErrorOccurred(error, goToPageHome) { if (goToPageHome) { PageController.goToStartPage() } else { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index bb6663fb..640c61ef 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -123,6 +123,10 @@ PageType { } } + function onWrongInstallationUser(message) { + onInstallationErrorOccurred(message) + } + function onUpdateContainerFinished(message) { PageController.showNotificationMessage(message) PageController.closePage() From 2c9067b0de3e4a80979cbc9343220873fc54ea45 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 18 Oct 2024 14:57:20 +0800 Subject: [PATCH 010/208] bugfix: added missing text in the errors --- client/core/errorstrings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 00e94995..c5c0363b 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -50,7 +50,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break; 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(""); break; + case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break; // Android errors case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; From 60de146f031116bcc273a1f88045c838bb63b876 Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 18 Oct 2024 13:47:53 +0400 Subject: [PATCH 011/208] chore/mozilla upstream (#1136) * cherry-pick commit 5a51e292d44ec0fb07867aff0401b4c2a8fca1e8 from mozila upstream * cherry-pick commit e8ecb857dcfb804b7766a54e725b442fc6c0e661 from mozila upstream * cherry-pick commit 16269ffa600905b09678014f64951748fb0ff8ad from mozila upstream --- client/daemon/daemon.cpp | 23 ++++--------------- client/daemon/daemon.h | 1 - client/daemon/daemonlocalserverconnection.cpp | 17 ++++++++++---- client/mozilla/localsocketcontroller.cpp | 4 ++-- client/platforms/linux/daemon/linuxdaemon.h | 1 - client/platforms/macos/daemon/macosdaemon.h | 1 - .../platforms/windows/daemon/windowsdaemon.h | 1 - .../windows/daemon/windowssplittunnel.cpp | 2 +- 8 files changed, 21 insertions(+), 29 deletions(-) diff --git a/client/daemon/daemon.cpp b/client/daemon/daemon.cpp index 3e237e9c..a234860b 100644 --- a/client/daemon/daemon.cpp +++ b/client/daemon/daemon.cpp @@ -78,7 +78,7 @@ bool Daemon::activate(const InterfaceConfig& config) { return false; } - if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { + if (!dnsutils()->restoreResolvers()) { return false; } @@ -165,10 +165,6 @@ bool Daemon::activate(const InterfaceConfig& config) { } bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) { - if (!supportDnsUtils()) { - return true; - } - if ((config.m_hopType == InterfaceConfig::MultiHopExit) || (config.m_hopType == InterfaceConfig::SingleHop)) { QList resolvers; @@ -423,13 +419,8 @@ bool Daemon::deactivate(bool emitSignals) { } // Cleanup DNS - if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { - return false; - } - - if (!wgutils()->interfaceExists()) { - logger.warning() << "Wireguard interface does not exist."; - return false; + if (!dnsutils()->restoreResolvers()) { + logger.warning() << "Failed to restore DNS resolvers."; } // Cleanup peers and routing @@ -449,13 +440,9 @@ bool Daemon::deactivate(bool emitSignals) { } m_excludedAddrSet.clear(); - // Delete the interface - if (!wgutils()->deleteInterface()) { - return false; - } - m_connections.clear(); - return true; + // Delete the interface + return wgutils()->deleteInterface(); } QString Daemon::logs() { diff --git a/client/daemon/daemon.h b/client/daemon/daemon.h index d3d8c34d..3d418d70 100644 --- a/client/daemon/daemon.h +++ b/client/daemon/daemon.h @@ -69,7 +69,6 @@ class Daemon : public QObject { virtual WireguardUtils* wgutils() const = 0; virtual bool supportIPUtils() const { return false; } virtual IPUtils* iputils() { return nullptr; } - virtual bool supportDnsUtils() const { return false; } virtual DnsUtils* dnsutils() { return nullptr; } static bool parseStringList(const QJsonObject& obj, const QString& name, diff --git a/client/daemon/daemonlocalserverconnection.cpp b/client/daemon/daemonlocalserverconnection.cpp index 1a49b7e5..edbc4c9b 100644 --- a/client/daemon/daemonlocalserverconnection.cpp +++ b/client/daemon/daemonlocalserverconnection.cpp @@ -92,6 +92,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { logger.debug() << "Command received:" << type; + // It is expected that sometimes the client will request backend logs + // before the first authentication. In these cases we just return empty + // logs. + if (type == "logs") { + QJsonObject obj; + obj.insert("type", "logs"); + obj.insert("logs", ""); + write(obj); + return; + } + if (type == "activate") { InterfaceConfig config; if (!Daemon::parseConfig(obj, config)) { @@ -115,8 +126,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { if (type == "status") { QJsonObject obj = Daemon::instance()->getStatus(); obj.insert("type", "status"); - m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - m_socket->write("\n"); + write(obj); return; } @@ -124,8 +134,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { QJsonObject obj; obj.insert("type", "logs"); obj.insert("logs", Daemon::instance()->logs().replace("\n", "|")); - m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - m_socket->write("\n"); + write(obj); return; } diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 4d040288..5e9f0f97 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -34,8 +34,8 @@ LocalSocketController::LocalSocketController() { m_socket = new QLocalSocket(this); connect(m_socket, &QLocalSocket::connected, this, &LocalSocketController::daemonConnected); - connect(m_socket, &QLocalSocket::disconnected, this, - &LocalSocketController::disconnected); + connect(m_socket, &QLocalSocket::disconnected, this, + [&] { errorOccurred(QLocalSocket::PeerClosedError); }); connect(m_socket, &QLocalSocket::errorOccurred, this, &LocalSocketController::errorOccurred); connect(m_socket, &QLocalSocket::readyRead, this, diff --git a/client/platforms/linux/daemon/linuxdaemon.h b/client/platforms/linux/daemon/linuxdaemon.h index 7f5d27b7..dbac8cee 100644 --- a/client/platforms/linux/daemon/linuxdaemon.h +++ b/client/platforms/linux/daemon/linuxdaemon.h @@ -22,7 +22,6 @@ class LinuxDaemon final : public Daemon { protected: WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } bool supportIPUtils() const override { return true; } IPUtils* iputils() override { return m_iputils; } diff --git a/client/platforms/macos/daemon/macosdaemon.h b/client/platforms/macos/daemon/macosdaemon.h index a48c326c..4181648e 100644 --- a/client/platforms/macos/daemon/macosdaemon.h +++ b/client/platforms/macos/daemon/macosdaemon.h @@ -21,7 +21,6 @@ class MacOSDaemon final : public Daemon { protected: WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } bool supportIPUtils() const override { return true; } IPUtils* iputils() override { return m_iputils; } diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h index 9d051bae..7e38c41e 100644 --- a/client/platforms/windows/daemon/windowsdaemon.h +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -26,7 +26,6 @@ class WindowsDaemon final : public Daemon { protected: bool run(Op op, const InterfaceConfig& config) override; WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } private: diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp index 39941933..c4e893b2 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.cpp +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -502,7 +502,7 @@ QString WindowsSplitTunnel::convertPath(const QString& path) { // device should contain : for e.g C: return ""; } - QByteArray buffer(2048, 0xFF); + QByteArray buffer(2048, 0xFFu); auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter), (wchar_t*)buffer.data(), buffer.size() / 2); From d63bf15011f074ef498af5d91c5c46092856ada4 Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 18 Oct 2024 12:52:24 +0300 Subject: [PATCH 012/208] Android qt 6.7.3 (#1143) * Up Qt to 6.7.3 * Bump version to 4.8.2.0 * Raise the minimum Android version to 8 (API 26) * Update version code to separate versions for new and old Androids * Fix mouse not working on TVs * Refactor logging * Bump version code --- .github/workflows/deploy.yml | 2 +- CMakeLists.txt | 4 +- client/android/gradle.properties | 2 +- .../protocolApi/src/main/kotlin/Protocol.kt | 4 - .../src/org/amnezia/vpn/AmneziaActivity.kt | 73 +++++++++++++++++-- .../src/org/amnezia/vpn/AmneziaVpnService.kt | 6 +- .../src/org/amnezia/vpn/AuthActivity.kt | 2 +- .../org/amnezia/vpn/ImportConfigActivity.kt | 8 +- .../org/amnezia/vpn/ServiceNotification.kt | 12 ++- .../utils/src/main/kotlin/LibraryLoader.kt | 2 +- client/android/utils/src/main/kotlin/Log.kt | 17 +---- .../utils/src/main/kotlin/net/NetworkState.kt | 20 ++--- .../vpn/protocol/wireguard/Wireguard.kt | 2 +- client/android/xray/src/main/kotlin/Xray.kt | 4 +- client/cmake/android.cmake | 2 +- 15 files changed, 97 insertions(+), 63 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d9138516..f9fb19a5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -301,7 +301,7 @@ jobs: env: ANDROID_BUILD_PLATFORM: android-34 - QT_VERSION: 6.7.2 + QT_VERSION: 6.7.3 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} diff --git a/CMakeLists.txt b/CMakeLists.txt index fba4183c..ce5777e4 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.1.9 +project(${PROJECT} VERSION 4.8.2.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 65) +set(APP_ANDROID_VERSION_CODE 2067) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/android/gradle.properties b/client/android/gradle.properties index 5a27838c..ce651e1c 100644 --- a/client/android/gradle.properties +++ b/client/android/gradle.properties @@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false # For development copy and set local values for these parameters in local.properties #androidCompileSdkVersion=android-34 #androidBuildToolsVersion=34.0.0 -#qtMinSdkVersion=24 +#qtMinSdkVersion=26 #qtTargetSdkVersion=34 #androidNdkVersion=26.1.10909125 #qtTargetAbiList=x86_64 diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index b5c382be..6e682aa4 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -1,6 +1,5 @@ package org.amnezia.vpn.protocol -import android.annotation.SuppressLint import android.content.Context import android.net.IpPrefix import android.net.VpnService @@ -8,9 +7,6 @@ import android.net.VpnService.Builder import android.os.Build import android.system.OsConstants import androidx.annotation.RequiresApi -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipFile import kotlinx.coroutines.flow.MutableStateFlow import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index d5026425..b2c2ff71 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -21,6 +21,7 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.provider.Settings +import android.view.MotionEvent import android.view.WindowManager.LayoutParams import android.webkit.MimeTypeMap import android.widget.Toast @@ -158,7 +159,7 @@ class AmneziaActivity : QtActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d(TAG, "Create Amnezia activity: $intent") + Log.d(TAG, "Create Amnezia activity") loadLibs() window.apply { addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) @@ -200,7 +201,7 @@ class AmneziaActivity : QtActivity() { NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED ) ) { - Log.d( + Log.v( TAG, "Notification state changed: ${it?.action}, blocked = " + "${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}" ) @@ -214,7 +215,7 @@ class AmneziaActivity : QtActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Log.d(TAG, "onNewIntent: $intent") + Log.v(TAG, "onNewIntent: $intent") intent?.let(::processIntent) } @@ -403,7 +404,7 @@ class AmneziaActivity : QtActivity() { @MainThread private fun startVpn(vpnConfig: String) { getVpnProto(vpnConfig)?.let { proto -> - Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto") + Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto") if (isServiceConnected) { if (proto.serviceClass == vpnProto?.serviceClass) { vpnProto = proto @@ -516,7 +517,7 @@ class AmneziaActivity : QtActivity() { startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( onSuccess = { it?.data?.let { uri -> - Log.d(TAG, "Save file to $uri") + Log.v(TAG, "Save file to $uri") try { contentResolver.openOutputStream(uri)?.use { os -> os.bufferedWriter().use { it.write(data) } @@ -565,7 +566,7 @@ class AmneziaActivity : QtActivity() { startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( onAny = { val uri = it?.data?.toString() ?: "" - Log.d(TAG, "Open file: $uri") + Log.v(TAG, "Open file: $uri") mainScope.launch { qtInitialized.await() QtAndroidController.onFileOpened(uri) @@ -720,6 +721,66 @@ class AmneziaActivity : QtActivity() { } } + // workaround for a bug in Qt that causes the mouse click event not to be handled + // also disable right-click, as it causes the application to crash + private var lastButtonState = 0 + private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain( + downTime, + eventTime, + action, + pointerCount, + (0 until pointerCount).map { i -> + MotionEvent.PointerProperties().apply { + getPointerProperties(i, this) + } + }.toTypedArray(), + (0 until pointerCount).map { i -> + MotionEvent.PointerCoords().apply { + getPointerCoords(i, this) + } + }.toTypedArray(), + metaState, + MotionEvent.BUTTON_PRIMARY, + xPrecision, + yPrecision, + deviceId, + edgeFlags, + source, + flags + ) + + private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + lastButtonState = ev.buttonState + if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true + } + + MotionEvent.ACTION_UP -> { + when (lastButtonState) { + MotionEvent.BUTTON_SECONDARY -> return true + MotionEvent.BUTTON_PRIMARY -> { + val modEvent = ev.fixCopy() + return superDispatch(modEvent).apply { modEvent.recycle() } + } + } + } + } + return superDispatch(ev) + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + return handleMouseEvent(ev) { super.dispatchTouchEvent(it) } + } + return super.dispatchTouchEvent(ev) + } + + override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { + ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }} + return super.dispatchTrackballEvent(ev) + } + /** * Utils methods */ diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 937127ee..8d108bc3 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -300,7 +300,7 @@ open class AmneziaVpnService : VpnService() { arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED ) { it?.action?.let { action -> - Log.d(TAG, "Broadcast request received: $action") + Log.v(TAG, "Broadcast request received: $action") when (action) { ACTION_CONNECT -> connect() ACTION_DISCONNECT -> disconnect() @@ -317,7 +317,7 @@ open class AmneziaVpnService : VpnService() { ) ) { val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false) - Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state") + Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state") if (state == false) { enableNotification() } else { @@ -450,7 +450,7 @@ open class AmneziaVpnService : VpnService() { serviceNotification.isNotificationEnabled() && getSystemService()?.isInteractive != false ) { - Log.d(TAG, "Launch traffic stats update") + Log.v(TAG, "Launch traffic stats update") trafficStats.reset() startTrafficStatsUpdateJob() } diff --git a/client/android/src/org/amnezia/vpn/AuthActivity.kt b/client/android/src/org/amnezia/vpn/AuthActivity.kt index 2593315c..46401548 100644 --- a/client/android/src/org/amnezia/vpn/AuthActivity.kt +++ b/client/android/src/org/amnezia/vpn/AuthActivity.kt @@ -66,7 +66,7 @@ class AuthActivity : FragmentActivity() { object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: AuthenticationResult) { super.onAuthenticationSucceeded(result) - Log.d(TAG, "Authentication succeeded") + Log.v(TAG, "Authentication succeeded") QtAndroidController.onAuthResult(true) finish() } diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index 9faa30d0..49823a36 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d(TAG, "Create Import Config Activity: $intent") + Log.v(TAG, "Create Import Config Activity: $intent") intent?.let(::readConfig) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - Log.d(TAG, "onNewIntent: $intent") + Log.v(TAG, "onNewIntent: $intent") intent.let(::readConfig) } private fun readConfig(intent: Intent) { when (intent.action) { ACTION_SEND -> { - Log.d(TAG, "Process SEND action, type: ${intent.type}") + Log.v(TAG, "Process SEND action, type: ${intent.type}") when (intent.type) { "application/octet-stream" -> { intent.getUriCompat()?.let { uri -> @@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() { } ACTION_VIEW -> { - Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}") + Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}") when (intent.scheme) { "file", "content" -> { intent.data?.let { uri -> diff --git a/client/android/src/org/amnezia/vpn/ServiceNotification.kt b/client/android/src/org/amnezia/vpn/ServiceNotification.kt index f4707731..47e8f263 100644 --- a/client/android/src/org/amnezia/vpn/ServiceNotification.kt +++ b/client/android/src/org/amnezia/vpn/ServiceNotification.kt @@ -62,7 +62,7 @@ class ServiceNotification(private val context: Context) { fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification { val speedString = if (state == CONNECTED) zeroSpeed else null - Log.d(TAG, "Build notification: $serverName, $state") + Log.v(TAG, "Build notification: $serverName, $state") return notificationBuilder .setSmallIcon(R.drawable.ic_amnezia_round) @@ -88,17 +88,15 @@ class ServiceNotification(private val context: Context) { fun isNotificationEnabled(): Boolean { if (!context.isNotificationPermissionGranted()) return false if (!notificationManager.areNotificationsEnabled()) return false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) - ?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true - } - return true + return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let { + it.importance != NotificationManager.IMPORTANCE_NONE + } ?: true } @SuppressLint("MissingPermission") fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) { if (context.isNotificationPermissionGranted()) { - Log.d(TAG, "Update notification: $serverName, $state") + Log.v(TAG, "Update notification: $serverName, $state") notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state)) } } diff --git a/client/android/utils/src/main/kotlin/LibraryLoader.kt b/client/android/utils/src/main/kotlin/LibraryLoader.kt index f1c6465e..8def18d0 100644 --- a/client/android/utils/src/main/kotlin/LibraryLoader.kt +++ b/client/android/utils/src/main/kotlin/LibraryLoader.kt @@ -46,7 +46,7 @@ object LibraryLoader { System.loadLibrary(libraryName) return } catch (_: UnsatisfiedLinkError) { - Log.d(TAG, "Failed to load library, try to extract it from apk") + Log.w(TAG, "Failed to load library, try to extract it from apk") } var tempFile: File? = null try { diff --git a/client/android/utils/src/main/kotlin/Log.kt b/client/android/utils/src/main/kotlin/Log.kt index a656b9ea..da11c200 100644 --- a/client/android/utils/src/main/kotlin/Log.kt +++ b/client/android/utils/src/main/kotlin/Log.kt @@ -1,8 +1,6 @@ package org.amnezia.vpn.util import android.content.Context -import android.icu.text.DateFormat -import android.icu.text.SimpleDateFormat import android.os.Build import android.os.Process import java.io.File @@ -12,8 +10,6 @@ import java.nio.channels.FileChannel import java.nio.channels.FileLock import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.Date -import java.util.Locale import java.util.concurrent.locks.ReentrantLock import org.amnezia.vpn.util.Log.Priority.D import org.amnezia.vpn.util.Log.Priority.E @@ -41,11 +37,7 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024 * | | | create a report and/or terminate the process | */ object Log { - private val dateTimeFormat: Any = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) - else object : ThreadLocal() { - override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US) - } + private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) private lateinit var logDir: File private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) } @@ -143,12 +135,7 @@ object Log { } private fun formatLogMsg(tag: String, msg: String, priority: Priority): String { - val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter) - } else { - @Suppress("UNCHECKED_CAST") - (dateTimeFormat as ThreadLocal).get()?.format(Date()) - } + val date = LocalDateTime.now().format(dateTimeFormat) return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " + "$tag: $msg\n" } diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index b71bf393..1cab5535 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -42,18 +42,12 @@ class NetworkState( private val networkCallback: NetworkCallback by lazy(NONE) { object : NetworkCallback() { override fun onAvailable(network: Network) { - Log.d(TAG, "onAvailable: $network") + Log.v(TAG, "onAvailable: $network") } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - checkNetworkState(network, networkCapabilities) - } else { - handler.post { - checkNetworkState(network, networkCapabilities) - } - } + Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") + checkNetworkState(network, networkCapabilities) } private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) { @@ -73,11 +67,11 @@ class NetworkState( } override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { - Log.d(TAG, "onBlockedStatusChanged: $network, $blocked") + Log.v(TAG, "onBlockedStatusChanged: $network, $blocked") } override fun onLost(network: Network) { - Log.d(TAG, "onLost: $network") + Log.v(TAG, "onLost: $network") } } } @@ -87,7 +81,7 @@ class NetworkState( Log.d(TAG, "Bind network listener") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + } else { val numberAttempts = 300 var attemptCount = 0 while(true) { @@ -108,8 +102,6 @@ class NetworkState( } } } - } else { - connectivityManager.requestNetwork(networkRequest, networkCallback) } isListenerBound = true } diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index ac11374b..e93834f4 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -66,7 +66,7 @@ open class Wireguard : Protocol() { try { delay(1000) var log = getLogcat(time) - Log.d(TAG, "First waiting log: $log") + Log.v(TAG, "First waiting log: $log") // check that there is a connection log, // to avoid infinite connection if (!log.contains("Attaching to interface")) { diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index 6e37c9c2..08242525 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -130,8 +130,8 @@ class Xray : Protocol() { LibXray.initXray(assetsPath) val geoDir = File(assetsPath, "geo").absolutePath val configPath = File(context.cacheDir, "config.json") - Log.d(TAG, "xray.location.asset: $geoDir") - Log.d(TAG, "config: $configPath") + Log.v(TAG, "xray.location.asset: $geoDir") + Log.v(TAG, "config: $configPath") try { configPath.writeText(configJson) } catch (e: IOException) { diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index c96d9ab8..34ca5bff 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -1,6 +1,6 @@ message("Client android ${CMAKE_ANDROID_ARCH_ABI} build") -set(APP_ANDROID_MIN_SDK 24) +set(APP_ANDROID_MIN_SDK 26) set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING "The minimum API level supported by the application or library" FORCE) From 74802f30ed9b838f0ad55d5d167c96f44dd3d459 Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 18 Oct 2024 13:57:38 +0400 Subject: [PATCH 013/208] feature/proxy storage bypass (#1179) * feature: added proxy storage bypass - added encryption error handling to apiController * chore: fixed include --- client/CMakeLists.txt | 2 ++ client/core/controllers/apiController.cpp | 37 +++++++++++++++++++---- client/core/defs.h | 1 + client/core/errorstrings.cpp | 1 + client/utilities.cpp | 19 ++++++++++++ client/utilities.h | 3 ++ 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2de5db48..2ec4082c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -26,9 +26,11 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") +add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") 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) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 5cdaa7ae..96c28a81 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -12,6 +12,7 @@ #include "configurators/wireguard_configurator.h" #include "core/enums/apiEnums.h" #include "version.h" +#include "utilities.h" namespace { @@ -42,8 +43,6 @@ namespace constexpr char keyPayload[] = "key_payload"; } - const QStringList proxyStorageUrl = { "" }; - ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { if (!sslErrors.empty()) { @@ -146,6 +145,15 @@ QStringList ApiController::getProxyUrls() 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); @@ -166,11 +174,23 @@ QStringList ApiController::getProxyUrls() EVP_PKEY *privateKey = nullptr; QByteArray responseBody; try { - QByteArray key = PROD_PROXY_STORAGE_KEY; - QSimpleCrypto::QRsa rsa; - privateKey = rsa.getPrivateKeyFromByteArray(key, ""); - responseBody = rsa.decrypt(encryptedResponseBody, privateKey, RSA_PKCS1_PADDING); + 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 {}; } @@ -361,6 +381,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co QSimpleCrypto::QRsa rsa; publicKey = rsa.getPublicKeyFromByteArray(rsaKey); } catch (...) { + Utils::logException(); qCritical() << "error loading public key from environment variables"; return ErrorCode::ApiMissingAgwPublicKey; } @@ -370,7 +391,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co 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; @@ -416,7 +439,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); 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/defs.h b/client/core/defs.h index 802eca45..d00d347b 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -108,6 +108,7 @@ namespace amnezia ApiConfigTimeoutError = 1103, ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, + ApiConfigDecryptionError = 1106, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index c5c0363b..49534606 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -62,6 +62,7 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break; case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; + case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; diff --git a/client/utilities.cpp b/client/utilities.cpp index 4047365f..731781b5 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -244,3 +244,22 @@ bool Utils::signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent) } #endif + +void Utils::logException(const std::exception &e) +{ + qCritical() << e.what(); + try { + std::rethrow_if_nested(e); + } catch (const std::exception &nested) { + logException(nested); + } catch (...) {} +} + +void Utils::logException(const std::exception_ptr &eptr) +{ + try { + if (eptr) std::rethrow_exception(eptr); + } catch (const std::exception &e) { + logException(e); + } catch (...) {} +} diff --git a/client/utilities.h b/client/utilities.h index 9bf8c82a..2a17a1f4 100644 --- a/client/utilities.h +++ b/client/utilities.h @@ -34,6 +34,9 @@ public: static QString certUtilPath(); static QString tun2socksPath(); + static void logException(const std::exception &e); + static void logException(const std::exception_ptr &eptr = std::current_exception()); + #ifdef Q_OS_WIN static bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent); #endif From 5601bc4fdfcf4cd663c58df57c345bb9a6f8e6d0 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 18 Oct 2024 21:39:09 +0800 Subject: [PATCH 014/208] chore: added new env for workflows --- .github/workflows/deploy.yml | 15 +++++++++++++++ .github/workflows/tag-deploy.yml | 3 +++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f9fb19a5..64a4986d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,7 +16,10 @@ jobs: QT_VERSION: 6.6.2 QIF_VERSION: 4.7 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install Qt' @@ -83,7 +86,10 @@ jobs: QIF_VERSION: 4.7 BUILD_ARCH: 64 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Get sources' @@ -146,7 +152,10 @@ jobs: CC: cc CXX: c++ PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Setup xcode' @@ -238,7 +247,10 @@ jobs: QT_VERSION: 6.4.3 QIF_VERSION: 4.6 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Setup xcode' @@ -304,7 +316,10 @@ jobs: QT_VERSION: 6.7.3 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install desktop Qt' diff --git a/.github/workflows/tag-deploy.yml b/.github/workflows/tag-deploy.yml index dffb3ab1..2bcbd8c6 100644 --- a/.github/workflows/tag-deploy.yml +++ b/.github/workflows/tag-deploy.yml @@ -16,7 +16,10 @@ jobs: QT_VERSION: 6.4.1 QIF_VERSION: 4.5 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install desktop Qt' From 928c4f18c98e6f601b634015ff472be8ecb56fbe Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 22 Oct 2024 22:24:23 +0800 Subject: [PATCH 015/208] chore/using the global network manager --- client/CMakeLists.txt | 1 - client/core/controllers/apiController.cpp | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2ec4082c..05f9f17c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -25,7 +25,6 @@ execute_process( add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") -add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}") add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 31a561d8..a7c304f3 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -352,7 +352,6 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co QThread::msleep(10); #endif - QNetworkAccessManager manager; QNetworkRequest request; request.setTransferTimeout(7000); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -410,7 +409,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); - QNetworkReply *reply = manager.post(request, QJsonDocument(requestBody).toJson()); + QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson()); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); @@ -425,7 +424,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } for (const QString &proxyUrl : m_proxyUrls) { request.setUrl(QString("%1v1/config").arg(proxyUrl)); - reply = manager.post(request, QJsonDocument(requestBody).toJson()); + 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; }); From e31a2066c080358be07fe48ffe0b60674db335ae Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 22 Oct 2024 23:05:58 +0800 Subject: [PATCH 016/208] feature/added support tag to PageSetupWizardConfigSource --- .../Pages2/PageSetupWizardConfigSource.qml | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 7f7cf9e1..7c031997 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -49,6 +49,8 @@ PageType { HeaderType { + property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() + Layout.fillWidth: true Layout.topMargin: 24 Layout.rightMargin: 16 @@ -56,7 +58,7 @@ PageType { headerText: qsTr("Connection") - actionButtonImage: PageController.isStartPageVisible() ? "qrc:/images/controls/more-vertical.svg" : "" + actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : "" actionButtonFunction: function() { moreActionsDrawer.open() } @@ -67,18 +69,19 @@ PageType { parent: root anchors.fill: parent - expandedHeight: root.height * 0.35 + expandedHeight: root.height * 0.5 expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 + spacing: 0 HeaderType { Layout.fillWidth: true Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 headerText: qsTr("Settings") } @@ -87,9 +90,12 @@ PageType { id: switcher Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 text: qsTr("Enable logs") + visible: PageController.isStartPageVisible() checked: SettingsController.isLoggingEnabled onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { @@ -98,6 +104,28 @@ PageType { } } + LabelWithButtonType { + id: supportUuid + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Support tag") + descriptionText: SettingsController.getInstallationUuid() + + descriptionOnTop: true + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + + visible: SettingsController.getInstallationUuid() !== "" + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } } } } From 5358aaeb00cd2315d5cc82e5fe1d6ae959867c64 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Tue, 22 Oct 2024 23:14:41 +0800 Subject: [PATCH 017/208] chore/displaying route addresses when adding to split tunneling fails --- client/platforms/windows/daemon/wireguardutilswindows.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/platforms/windows/daemon/wireguardutilswindows.cpp b/client/platforms/windows/daemon/wireguardutilswindows.cpp index a68551d7..1a220235 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.cpp +++ b/client/platforms/windows/daemon/wireguardutilswindows.cpp @@ -248,7 +248,7 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { } if (result != NO_ERROR) { logger.error() << "Failed to create route to" - << logger.sensitive(prefix.toString()) + << prefix.toString() << "result:" << result; } return result == NO_ERROR; @@ -265,7 +265,7 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { } if (result != NO_ERROR) { logger.error() << "Failed to delete route to" - << logger.sensitive(prefix.toString()) + << prefix.toString() << "result:" << result; } return result == NO_ERROR; From 92b19eccf6753ce3734b6ba51acaaee6ebeb93cb Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Wed, 23 Oct 2024 00:33:22 +0800 Subject: [PATCH 018/208] bugfix/removed adding routes in vpnconnection class for awg and wg protocols --- client/vpnconnection.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index 591e396f..ac881bd7 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -56,14 +56,15 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) { #ifdef AMNEZIA_DESKTOP - QString proto = m_settings->defaultContainerName(m_settings->defaultServerIndex()); + auto container = m_settings->defaultContainer(m_settings->defaultServerIndex()); if (IpcClient::Interface()) { if (state == Vpn::ConnectionState::Connected) { IpcClient::Interface()->resetIpStack(); IpcClient::Interface()->flushDns(); - if (!m_vpnConfiguration.value(config_key::configVersion).toInt()) { + if (!m_vpnConfiguration.value(config_key::configVersion).toInt() && container != DockerContainer::Awg + && container != DockerContainer::WireGuard) { QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); From 923e358aaaf458cfa9b319064326b2ec6c308e25 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 24 Oct 2024 01:02:30 +0800 Subject: [PATCH 019/208] added a check to trigger proxy bypass --- client/core/controllers/apiController.cpp | 56 +++++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 31a561d8..1f8257ee 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -11,8 +11,8 @@ #include "amnezia_application.h" #include "configurators/wireguard_configurator.h" #include "core/enums/apiEnums.h" -#include "version.h" #include "utilities.h" +#include "version.h" namespace { @@ -65,6 +65,28 @@ namespace 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) @@ -320,24 +342,27 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) { + responseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { m_proxyUrls = getProxyUrls(); 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(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError - && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + + responseBody = reply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, responseBody, false)) { break; } - reply->deleteLater(); } } - responseBody = reply->readAll(); auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); return errorCode; @@ -419,32 +444,33 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); - if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) { - if (m_proxyUrls.isEmpty()) { - m_proxyUrls = getProxyUrls(); - } + auto encryptedResponseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { + m_proxyUrls = getProxyUrls(); 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 = 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(); - if (reply->error() != QNetworkReply::NetworkError::TimeoutError - && reply->error() != QNetworkReply::NetworkError::OperationCanceledError) { + + encryptedResponseBody = reply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, false)) { break; } - reply->deleteLater(); } } auto errorCode = checkErrors(sslErrors, reply); + reply->deleteLater(); if (errorCode) { return errorCode; } - auto encryptedResponseBody = reply->readAll(); - reply->deleteLater(); try { auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig); From d511220f8befbe076f320e94d667000f0e9843dc Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 24 Oct 2024 01:03:54 +0800 Subject: [PATCH 020/208] added a randomized proxy bypass --- client/core/controllers/apiController.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 1f8257ee..dbd621a8 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -1,5 +1,8 @@ #include "apiController.h" +#include +#include + #include #include #include @@ -346,6 +349,9 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) 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)); @@ -448,6 +454,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { 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)); From 4685d3b543b19a7bf4ac1ab81de31c20021c71a7 Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 24 Oct 2024 19:12:53 +0400 Subject: [PATCH 021/208] bugfix/api auth data saving (#1195) * bugfix: fixed authData saving * bugfix: added serviceInfo processing from api response --- client/core/controllers/apiController.cpp | 6 ++++++ client/ui/controllers/installController.cpp | 2 ++ client/ui/models/apiCountryModel.cpp | 4 ++++ client/ui/models/apiCountryModel.h | 3 ++- client/ui/models/servers_model.cpp | 2 +- client/ui/qml/Pages2/PageSettingsApiLanguageList.qml | 2 +- 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index a7cbd40a..193ac481 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -37,6 +37,7 @@ namespace 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"; @@ -163,6 +164,11 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle 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() == ApiConfigSources::AmneziaGateway) { + apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject()); + } + serverConfig[configKey::apiConfig] = apiConfig; return; diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 4ac0bc32..306e7f38 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -847,6 +847,8 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); newServerConfig.insert(configKey::apiConfig, newApiConfig); + newServerConfig.insert(configKey::authData, authData); + newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { diff --git a/client/ui/models/apiCountryModel.cpp b/client/ui/models/apiCountryModel.cpp index ae58329f..922a9d56 100644 --- a/client/ui/models/apiCountryModel.cpp +++ b/client/ui/models/apiCountryModel.cpp @@ -39,6 +39,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const case CountryNameRole: { return countryInfo.value(configKey::serverCountryName).toString(); } + case CountryImageCodeRole: { + return countryInfo.value(configKey::serverCountryCode).toString().toUpper(); + } } return QVariant(); @@ -76,5 +79,6 @@ QHash ApiCountryModel::roleNames() const QHash roles; roles[CountryNameRole] = "countryName"; roles[CountryCodeRole] = "countryCode"; + roles[CountryImageCodeRole] = "countryImageCode"; return roles; } diff --git a/client/ui/models/apiCountryModel.h b/client/ui/models/apiCountryModel.h index 8789158b..b9e243d0 100644 --- a/client/ui/models/apiCountryModel.h +++ b/client/ui/models/apiCountryModel.h @@ -11,7 +11,8 @@ class ApiCountryModel : public QAbstractListModel public: enum Roles { CountryNameRole = Qt::UserRole + 1, - CountryCodeRole + CountryCodeRole, + CountryImageCodeRole }; explicit ApiCountryModel(QObject *parent = nullptr); diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 85e5dae2..c87499a7 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -771,5 +771,5 @@ const QString ServersModel::getDefaultServerImagePathCollapsed() if (countryCode.isEmpty()) { return ""; } - return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode); + return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode.toUpper()); } diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 234e5142..120313cd 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -90,7 +90,7 @@ PageType { Layout.rightMargin: 32 Layout.alignment: Qt.AlignRight - source: "qrc:/countriesFlags/images/flagKit/" + countryCode + ".svg" + source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" } } From 5065262aac9ebd70f1c8614cb794650d2a9f14e1 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Thu, 24 Oct 2024 14:05:26 +0800 Subject: [PATCH 022/208] bugfix: fixed clientInfoDrawer recursive rearrange --- client/ui/qml/Pages2/PageShare.qml | 83 ++++++++++++++---------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 6640df36..617b1091 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -772,7 +772,8 @@ PageType { } } - anchors.fill: parent + width: root.width + height: root.height expandedContent: ColumnLayout { id: expandedContent @@ -783,8 +784,6 @@ PageType { anchors.leftMargin: 16 anchors.rightMargin: 16 - spacing: 8 - onImplicitHeightChanged: { clientInfoDrawer.expandedHeight = expandedContent.implicitHeight + 32 } @@ -797,57 +796,54 @@ PageType { } } - Header2Type { - Layout.fillWidth: true - - headerText: clientName - } - - ColumnLayout - { - id: textColumn - property string textColor: AmneziaStyle.color.mutedGray + Header2TextType { + Layout.maximumWidth: parent.width Layout.bottomMargin: 24 - ParagraphTextType { - color: textColumn.textColor - visible: creationDate - Layout.fillWidth: true + text: clientName + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight + } - text: qsTr("Creation date: %1").arg(creationDate) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: creationDate + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: latestHandshake - Layout.fillWidth: true + text: qsTr("Creation date: %1").arg(creationDate) + } - text: qsTr("Latest handshake: %1").arg(latestHandshake) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: latestHandshake + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: dataReceived - Layout.fillWidth: true + text: qsTr("Latest handshake: %1").arg(latestHandshake) + } - text: qsTr("Data received: %1").arg(dataReceived) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: dataReceived + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: dataSent - Layout.fillWidth: true + text: qsTr("Data received: %1").arg(dataReceived) + } - text: qsTr("Data sent: %1").arg(dataSent) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: dataSent + Layout.fillWidth: true - ParagraphTextType { - color: textColumn.textColor - visible: allowedIps - Layout.fillWidth: true + text: qsTr("Data sent: %1").arg(dataSent) + } - text: qsTr("Allowed IPs: %1").arg(allowedIps) - } + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: allowedIps + Layout.fillWidth: true + + text: qsTr("Allowed IPs: %1").arg(allowedIps) } Item { @@ -952,6 +948,7 @@ PageType { BasicButtonType { id: revokeButton Layout.fillWidth: true + Layout.topMargin: 8 defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite From 7261a86c48c80f7c745f0e055fbafcac52c66243 Mon Sep 17 00:00:00 2001 From: albexk Date: Thu, 24 Oct 2024 19:25:44 +0300 Subject: [PATCH 023/208] Bump version to 4.8.2.1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce5777e4..b94e7e73 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.2.0 +project(${PROJECT} VERSION 4.8.2.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 2067) +set(APP_ANDROID_VERSION_CODE 2068) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 9f3f215452639c698c6756445702320ce8a4c780 Mon Sep 17 00:00:00 2001 From: Aftershock669 Date: Thu, 24 Oct 2024 22:27:53 +0300 Subject: [PATCH 024/208] Update README - add website mirror links - remove direct platform download links - add "Testiny" sponsored badge --- README.md | 12 ++++-------- metadata/img-readme/andr.png | Bin 13621 -> 0 bytes metadata/img-readme/apl.png | Bin 14495 -> 0 bytes metadata/img-readme/download.png | Bin 0 -> 3451 bytes metadata/img-readme/lin.png | Bin 11749 -> 0 bytes metadata/img-readme/mac.png | Bin 9513 -> 0 bytes metadata/img-readme/testiny.png | Bin 0 -> 5313 bytes 7 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 metadata/img-readme/andr.png delete mode 100644 metadata/img-readme/apl.png create mode 100644 metadata/img-readme/download.png delete mode 100644 metadata/img-readme/lin.png delete mode 100644 metadata/img-readme/mac.png create mode 100644 metadata/img-readme/testiny.png diff --git a/README.md b/README.md index e4a6bf0c..eed800f5 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,17 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
- - - - - -
- + +[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads) [All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
+ ## Features @@ -37,7 +33,7 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website +- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/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 support channel (English) - [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) diff --git a/metadata/img-readme/andr.png b/metadata/img-readme/andr.png deleted file mode 100644 index a39cd52f8ac97ba1b3a4bda78cfee2f725ead50a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13621 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 KGBr|W;r|C110n?g diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png deleted file mode 100644 index 6dedfa12ea1d6605b1419bb2c079d64dac02080f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14495 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 diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6a885004aba6dd61392155d4edbe021d4a304d GIT binary patch literal 3451 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 D@^Fny literal 0 HcmV?d00001 diff --git a/metadata/img-readme/lin.png b/metadata/img-readme/lin.png deleted file mode 100644 index 352eae5a3bb23b4f197723bdcd91dc524db2c408..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11749 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 C$Vk=z diff --git a/metadata/img-readme/mac.png b/metadata/img-readme/mac.png deleted file mode 100644 index 2cbb32ae41f1b18c0bb5550671fe56d683fc404d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9513 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 diff --git a/metadata/img-readme/testiny.png b/metadata/img-readme/testiny.png new file mode 100644 index 0000000000000000000000000000000000000000..4f38a3a91c82b2e3a3409e46e7548f93c2c69f6b GIT binary patch literal 5313 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 T$;wY@00000NkvXXu0mjf(4uGS literal 0 HcmV?d00001 From af55af5e760daf00cebe1124d67e9e40d79c9bb3 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 25 Oct 2024 17:48:22 +0800 Subject: [PATCH 025/208] bugfix: fixed proxy bypass encryption check --- client/core/controllers/apiController.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index dbd621a8..075f2cc2 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -399,7 +399,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } apiPayload[configKey::serviceType] = serviceType; apiPayload[configKey::uuid] = installationUuid; - apiPayload[configKey::authData] = authData; + if (!authData.isEmpty()) { + apiPayload[configKey::authData] = authData; + } QSimpleCrypto::QBlockCipher blockCipher; QByteArray key = blockCipher.generatePrivateSalt(32); @@ -452,7 +454,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co auto encryptedResponseBody = reply->readAll(); - if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { m_proxyUrls = getProxyUrls(); std::random_device randomDevice; std::mt19937 generator(randomDevice()); @@ -468,7 +470,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co wait.exec(); encryptedResponseBody = reply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, false)) { + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { break; } } From 4f3bae4a9ae68cc13965633a18d6a072f3d7bd15 Mon Sep 17 00:00:00 2001 From: Aftershock669 Date: Fri, 25 Oct 2024 17:00:28 +0300 Subject: [PATCH 026/208] Fix / Update README --- metadata/img-readme/apl.png | Bin 0 -> 14495 bytes metadata/img-readme/win.png | Bin 11018 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 metadata/img-readme/apl.png delete mode 100644 metadata/img-readme/win.png diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png new file mode 100644 index 0000000000000000000000000000000000000000..6dedfa12ea1d6605b1419bb2c079d64dac02080f GIT binary patch literal 14495 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 literal 0 HcmV?d00001 diff --git a/metadata/img-readme/win.png b/metadata/img-readme/win.png deleted file mode 100644 index 5a35cf490cff7b584ec68df4785dd454b7edfa31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11018 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 JrI=B`{{h}5>cap4 From 9e71e64cbdc514e04878d33aa4568e8a130090de Mon Sep 17 00:00:00 2001 From: Nethius Date: Fri, 25 Oct 2024 19:19:28 +0400 Subject: [PATCH 027/208] chore: bump version to 4.8.2.3 (#1203) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b94e7e73..420a7eb2 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.2.1 +project(${PROJECT} VERSION 4.8.2.3 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) From e7b25719e44271dd445bb8a7ab3644d6dfcc1024 Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 25 Oct 2024 19:23:05 +0300 Subject: [PATCH 028/208] Chore/bump version (#1204) * chore: bump Android version code --------- Co-authored-by: Nethius --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 420a7eb2..b5e64e32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 2068) +set(APP_ANDROID_VERSION_CODE 2069) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From 1533270e4e875d52ce0989f10c43c144a32cf306 Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 2 Nov 2024 00:54:24 +0300 Subject: [PATCH 029/208] Fix connection check for AWG/WG --- .../vpn/protocol/wireguard/Wireguard.kt | 85 +++++++++++-------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index e93834f4..80cab96d 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,11 +1,12 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder -import java.io.IOException -import java.util.Locale +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED @@ -27,6 +28,8 @@ open class Wireguard : Protocol() { private var tunnelHandle: Int = -1 protected open val ifName: String = "amn0" + private lateinit var scope: CoroutineScope + private var statusJob: Job? = null override val statistics: Statistics get() { @@ -49,46 +52,17 @@ open class Wireguard : Protocol() { override fun internalInit() { if (!isInitialized) loadSharedLibrary(context, "wg-go") + if (this::scope.isInitialized) { + scope.cancel() + } + scope = CoroutineScope(Dispatchers.IO) } override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) - val startTime = System.currentTimeMillis() start(wireguardConfig, vpnBuilder, protect) - waitForConnection(startTime) - state.value = CONNECTED } - private suspend fun waitForConnection(startTime: Long) { - Log.d(TAG, "Waiting for connection") - withContext(Dispatchers.IO) { - val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0) - try { - delay(1000) - var log = getLogcat(time) - Log.v(TAG, "First waiting log: $log") - // check that there is a connection log, - // to avoid infinite connection - if (!log.contains("Attaching to interface")) { - Log.w(TAG, "Logs do not contain a connection log") - return@withContext - } - while (!log.contains("Received handshake response")) { - delay(1000) - log = getLogcat(time) - } - } catch (e: IOException) { - Log.e(TAG, "Failed to get logcat: $e") - } - } - } - - private fun getLogcat(time: String): String = - ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time) - .redirectErrorStream(true) - .start() - .inputStream.reader().readText() - protected open fun parseConfig(config: JSONObject): WireguardConfig { val configData = config.getJSONObject("wireguard_config_data") return WireguardConfig.build { @@ -178,6 +152,43 @@ open class Wireguard : Protocol() { tunnelHandle = -1 throw VpnStartException("Protect VPN interface: permission not granted or revoked") } + launchStatusJob() + } + + private fun launchStatusJob() { + Log.d(TAG, "Launch status job") + statusJob = scope.launch { + while (true) { + val lastHandshake = getLastHandshake() + Log.v(TAG, "lastHandshake=$lastHandshake") + if (lastHandshake == 0L) { + delay(1000) + continue + } + if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED + else if (lastHandshake == -1L) state.value = DISCONNECTED + statusJob = null + break + } + } + } + + private fun getLastHandshake(): Long { + if (tunnelHandle == -1) { + Log.e(TAG, "Trying to get config of a non-existent tunnel") + return -1 + } + val config = GoBackend.awgGetConfig(tunnelHandle) + if (config == null) { + Log.e(TAG, "Failed to get tunnel config") + return -2 + } + val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong() + if (lastHandshake == null) { + Log.e(TAG, "Failed to get last_handshake_time_sec") + return -2 + } + return lastHandshake } override fun stopVpn() { @@ -185,6 +196,8 @@ open class Wireguard : Protocol() { Log.w(TAG, "Tunnel already down") return } + statusJob?.cancel() + statusJob = null val handleToClose = tunnelHandle tunnelHandle = -1 GoBackend.awgTurnOff(handleToClose) From 576e2226fe2742d14b326d9609f55a19dd02dcd1 Mon Sep 17 00:00:00 2001 From: albexk Date: Sun, 3 Nov 2024 16:11:23 +0300 Subject: [PATCH 030/208] fix(android): add CHANGE_NETWORK_STATE permission for all Android versions --- client/android/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 179def86..9e44e022 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -20,7 +20,7 @@ - + From 31867993ce5a77732b95c5e1717c6d53395f59b5 Mon Sep 17 00:00:00 2001 From: Nethius Date: Wed, 6 Nov 2024 09:57:39 +0400 Subject: [PATCH 031/208] chore: minor fixes (#1235) --- client/ui/controllers/installController.cpp | 1 - client/ui/qml/Controls2/BusyIndicatorType.qml | 2 +- client/ui/qml/Controls2/CardType.qml | 2 +- client/ui/qml/Controls2/DrawerType2.qml | 2 +- client/ui/qml/Controls2/PopupType.qml | 2 +- client/ui/qml/Controls2/TopCloseButtonType.qml | 2 +- client/ui/qml/Modules/Style/AmneziaStyle.qml | 4 ++++ client/ui/qml/Pages2/PageHome.qml | 4 ++-- client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 8 ++++---- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 306e7f38..ae0804cb 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -848,7 +848,6 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, authData); - newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { diff --git a/client/ui/qml/Controls2/BusyIndicatorType.qml b/client/ui/qml/Controls2/BusyIndicatorType.qml index 55af280f..480f25c1 100644 --- a/client/ui/qml/Controls2/BusyIndicatorType.qml +++ b/client/ui/qml/Controls2/BusyIndicatorType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Controls2/CardType.qml b/client/ui/qml/Controls2/CardType.qml index 50f84dbf..f584a8fc 100644 --- a/client/ui/qml/Controls2/CardType.qml +++ b/client/ui/qml/Controls2/CardType.qml @@ -19,7 +19,7 @@ RadioButton { property string textColor: AmneziaStyle.color.midnightBlack - property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3) + property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot property string selectedBorderColor: AmneziaStyle.color.goldenApricot property string defaultBodredColor: AmneziaStyle.color.transparent property int borderWidth: 0 diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index 6647bc88..c4b584c1 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -92,7 +92,7 @@ Item { id: background anchors.fill: parent - color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack Behavior on color { PropertyAnimation { duration: 200 } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index bd4aa4fb..7a6a770e 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -24,7 +24,7 @@ Popup { Overlay.modal: Rectangle { visible: root.closeButtonVisible - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } onOpened: { diff --git a/client/ui/qml/Controls2/TopCloseButtonType.qml b/client/ui/qml/Controls2/TopCloseButtonType.qml index 1bd7fef6..3a652da6 100644 --- a/client/ui/qml/Controls2/TopCloseButtonType.qml +++ b/client/ui/qml/Controls2/TopCloseButtonType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index c0038246..1abfbe3a 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -22,5 +22,9 @@ QtObject { readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12) readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08) readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05) + readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8) + readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3) + 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) } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8074337a..5689e4d4 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -316,8 +316,8 @@ PageType { rootButtonImageColor: AmneziaStyle.color.midnightBlack rootButtonBackgroundColor: AmneziaStyle.color.paleGray - rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) - rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) + rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray + rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray rootButtonHoveredBorderColor: AmneziaStyle.color.transparent rootButtonDefaultBorderColor: AmneziaStyle.color.transparent rootButtonTextTopMargin: 8 diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index f23e36d9..2d6c1d9b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -132,8 +132,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Reload API config") @@ -172,8 +172,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Remove from application") From 23806e1defcf827cec2f7727f432329eb5c0399c Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 8 Nov 2024 11:22:16 +0300 Subject: [PATCH 032/208] chore: bump version to 4.8.2.4 (#1240) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b5e64e32..cb695631 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.2.3 +project(${PROJECT} VERSION 4.8.2.4 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 2069) +set(APP_ANDROID_VERSION_CODE 2071) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From aa871bd1c99a693cd88e8cd635bc7d1bf69f75db Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 12 Nov 2024 10:22:34 +0400 Subject: [PATCH 033/208] feature: added country selection on home page drawer (#1215) --- .../qml/Components/ShareConnectionDrawer.qml | 6 +-- client/ui/qml/Controls2/BasicButtonType.qml | 22 ++++++---- .../qml/Controls2/TextFieldWithHeaderType.qml | 2 +- client/ui/qml/Pages2/PageHome.qml | 41 ++++++++++++++----- client/ui/qml/Pages2/PageShare.qml | 2 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 2 +- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 3235ad0a..d2bf28ab 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -84,7 +84,7 @@ DrawerType2 { Layout.topMargin: 16 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" KeyNavigation.tab: copyConfigTextButton @@ -120,7 +120,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } Keys.onEnterPressed: { copyConfigTextButton.clicked() } @@ -143,7 +143,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy config string") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" KeyNavigation.tab: showSettingsButton } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 5c599013..828c32bc 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -22,9 +22,10 @@ Button { property int borderWidth: 0 property int borderFocusedWidth: 1 - property string imageSource + property string leftImageSource property string rightImageSource - property string leftImageColor: textColor + property string leftImageColor + property bool changeLeftImageSize: true property bool squareLeftSide: false @@ -127,18 +128,23 @@ Button { anchors.centerIn: parent Image { - Layout.preferredHeight: 20 - Layout.preferredWidth: 20 - - source: root.imageSource - visible: root.imageSource === "" ? false : true + id: leftImage + source: root.leftImageSource + visible: root.leftImageSource === "" ? false : true layer { - enabled: true + enabled: leftImageColor !== "" ? true : false effect: ColorOverlay { color: leftImageColor } } + + Component.onCompleted: { + if (root.changeLeftImageSize) { + leftImage.Layout.preferredHeight = 20 + leftImage.Layout.preferredWidth = 20 + } + } } ButtonTextType { diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 4ec0976b..365faa94 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -183,7 +183,7 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText - imageSource: root.buttonImageSource + leftImageSource: root.buttonImageSource anchors.top: content.top anchors.bottom: content.bottom diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 5689e4d4..8422a10f 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -98,7 +98,6 @@ PageType { pressedColor: AmneziaStyle.color.sheerWhite disabledColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.mutedGray - leftImageColor: AmneziaStyle.color.transparent borderWidth: 0 buttonTextLabel.lineHeight: 20 @@ -110,7 +109,7 @@ PageType { text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") - imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() @@ -166,6 +165,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right + spacing: 0 Component.onCompleted: { drawer.collapsedHeight = collapsed.implicitHeight @@ -267,18 +267,39 @@ PageType { RowLayout { Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44 + Layout.topMargin: 8 + Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16 spacing: 0 - Image { - Layout.rightMargin: 8 - visible: source !== "" - source: ServersModel.defaultServerImagePathCollapsed - } + BasicButtonType { + enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsed + hoverEnabled: enabled + + implicitHeight: 36 + + leftPadding: 16 + rightPadding: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.transparent + textColor: AmneziaStyle.color.mutedGray + + buttonTextLabel.lineHeight: 16 + buttonTextLabel.font.pixelSize: 13 + buttonTextLabel.font.weight: 400 - LabelTextType { - id: collapsedServerMenuDescription text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded + leftImageSource: ServersModel.defaultServerImagePathCollapsed + changeLeftImageSize: false + + rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : "" + + onClicked: { + ServersModel.processedIndex = ServersModel.defaultIndex + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 617b1091..995fa3e7 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -573,7 +573,7 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 2a565230..404ba563 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -135,7 +135,7 @@ PageType { Layout.topMargin: 40 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) From 8547de82ea940cfb3220bc6c013c9ed4eb66dd39 Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 14 Nov 2024 07:58:04 +0400 Subject: [PATCH 034/208] bump xcode-version for macos build (#1249) --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 64a4986d..0ce8d576 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -256,7 +256,7 @@ jobs: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.3.1' + xcode-version: '15.4.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v3 From e0b091b47424b87ac980c9e7e2bcd80bd6e0e476 Mon Sep 17 00:00:00 2001 From: Aftershock669 <58403826+Aftershock669@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:51:46 +0300 Subject: [PATCH 035/208] Update readme (#1267) --- README.md | 19 ++++++++++--------- metadata/img-readme/apl.png | Bin 14495 -> 0 bytes metadata/img-readme/download-alt.svg | 8 ++++++++ metadata/img-readme/download-website.svg | 8 ++++++++ metadata/img-readme/download.png | Bin 3451 -> 0 bytes metadata/img-readme/play.png | Bin 15126 -> 0 bytes 6 files changed, 26 insertions(+), 9 deletions(-) delete mode 100644 metadata/img-readme/apl.png create mode 100644 metadata/img-readme/download-alt.svg create mode 100644 metadata/img-readme/download-website.svg delete mode 100644 metadata/img-readme/download.png delete mode 100644 metadata/img-readme/play.png diff --git a/README.md b/README.md index eed800f5..27a29edf 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,21 @@ [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![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) -Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. -![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png) +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -
    +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) - - - +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). -[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads) + + [All releases](https://github.com/amnezia-vpn/amnezia-client/releases) -
    +
    @@ -33,7 +33,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation - [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 support channel (English) - [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png deleted file mode 100644 index 6dedfa12ea1d6605b1419bb2c079d64dac02080f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14495 zcmXYYWk4fM(=G1q?(Xgu+?~bUeQ{XaWpQ_1oWzE>DR} zQDg@0X;uSmhU-~t-*y(zS!;h)+p=6cl3fwC6%JhihX-dix3%@X&$obsgVWh0B6r~J z>my%eyNmT`#OFBQ?UFxE4DNh-dI<{|$zf?p>vd9Yauq8hod661zfhG9OaAvFT%oF- zor8%ol#_p_rlu;tx&G^AM^<6dB&!B>tMe(SI0OO? z8Ic;IH>o)`)>5rVvqYspbmy>{Ip=h-e1%HM<6&B|9W!NW3ZHXe-Un~|&jfflXaI*1 zDNZ(;9V3-a)UQSO@Z=Q~002O5PJFGhn76ki++xK0;%|LkNJGbI1$A|G`C%qbvtc;A zg9D`h#suBDCZ{bZc~2Z}R6L)Iwa%nYQ~d`G3pI5{Lsvz0_0)h~7f+V+QWZ85vB>|8 zKRyz%00a^eQl^hCzAYoN|HqAm(Vz8oJuQH;DbtIDo*qGNBp;_5=YPE?Ast|0nJoO8 zQ038-Q&R9}fzM7_SXpjON=s`XLyU`y?XQwj_;2FGSePz$BHP$=M&KnTrMGf%vehT6 zf78Z?Foxwo*N@N56#|`vAk~^}gbi_|wmY}znyX)5reOUzsSWZq( z2H;3gV&W5U|1LIbW#MnoCnQWrx8Jm6NO)xA&`>05mXBGlySqCSwWTFmcmku9ui_Y)3M$c4;fgQQqJg< zz8+U-q^GBei3JijIQpgKo0FXx5()|m1)?`=9W9&VYsS=uBz+--5!ZH)FJhDRY{cfK z!IATh^|ogJPcZCj2=Dhf%x^V1;H*Zk4Ye!t%MfxSV;hJx%2|Ug4hvAYe17*8t8t-x z&$9vT-wzo$znqp>*<5Zl=jMv%jj&?aR6z^xcs4({VSf}J>k{g&ObMXg;c% z2%{dNOf&4x^>sG8DZIkJLYn;<`==?i*OS>TX|0qEWmNPzRJSOC(ffOSe@`42JFyor z`ERs<#@*iN((xpadtuoTFhrsr1a!zhlatk@+coTP8GwDs+1U_`+O;9~v9aR19{ z9Hh84;svT7hp%yjQ{5t-b|`p!Xd?daU@NtT<^jGgF6?;wj$;YLh|4^`#dJ7s?mTt} zrH*>jlR}9rj%PYTbr&ET^Yd@ZN{6`(IqMa!%0 zh8c6Y|62`|#hCarB{}_@DQhPfn&_6#3k4dsMW(``Z+7S1Y4v+Dv?+*JSkH%*?#JQ(N*o z{ByH2i(iDbcecP~o&Wwkc&4jBB0U|)uWw6C1$C=8z;IPVeBf&P+iSefj~}k+SBD}%;Yl5kBk6VmVDq5Fi)8F`6X1h$Rw2^vi&5|Go zQ(>JzGOlX;wGoMl*mx|m?Kk$jHi%Yy1++8sdrlrL=bGp6a_>9Hge?!fJuwKihJ-+< ztG!SVjr-(eXOh|4+Ohz;ZHKNZGLJhoX@CmXFU-cBVT;lV8kliJ>_l-S+!zII=+-_7 zms_d<0VEjd=jTiZeBNX$P4;nD{4W;=MybZVP8WK9BqIhJ8Mwnr?jC%7hFV(TS$~|J z;{$Ph(!+{6A&hk8l3t7rmEu%P*?65}l2cM%;x^13($Z7O#cPTpcoGZ0YI! zHa?pIcX;Dq`+x-^e!v<=dU|Qdx37L((M=z?l~!ozm+^;dfcw|BVQPN%A0|c?CP`-_ zXg2WWy!<`UV4}trs+W}BX9R&) z@@Rn$Yr`IwlNn6nmCwD&XP+X^#Q%2cNUe!agP-4ef$RC&XpG~NB?a~AXKz!mJ;RQM zqj86NeQs|B;(6NA(lC_$UYDB_Yu_Ld?N>@&tr>vo2++Fgf#i{FMRu z*-QtC;OA>-6U+S~Z4JCF+hlZ^-x1fBOyUYua!yJp$IWE#V_V(jgGPv}o> z3OGNR_gg8Wh$3U%_=cNPwhi5D={!aW)UGoa@W|EKd|O_zAJi3fI>)QMXi$m1bHKR! z7wWqy%zfM7MIc3Y(j&N7zWL$bvT?>vuj2%vg1eFfA$=IIAwv zl+b`&H<58sbjP}D%kCs-Zf=FSI%Wm4=l%{UxbpgNHT$4HaRT5_d{5oLKe|H1WgXn~ zK@>9rfOmK2i`(f5G;OZ0BM55y-VqlV423=OwR;_-c((q4TuvpUq{PHX#^869g`>?n zIKVEX*%kKGwBYlsDwj}~=O|Ya@f5MLwhRghimkFBH6M+GrMI=MJsgUN>sKCMQp}_e zj-}1)vtfsmFfc&s9-1Wu=<32-93JwcSkAaiPK%Bn$15p6_iCPh_x}=s60a|5MZ(Rv z@uU1c&HNiI#~!$_0c*+Fq}$ipMFY=L96RIV*gWAP8Ef4Qu-H>aR(ukS{D!LYEfK>aN@uf6xQ+wUNSkXw!?3C`y*;WZxm%!;22>w0J%804&a`rV!6=LSKFaz zdJ90&AEaBrYvtf0l^N6%hnSX9&!A^Cyvvj@jmZu;9)8Yk3;9!uq-q+x_iY0;I!_3c z(8jo)CXhUFCMa7Y^(E?r~DlK%CV_LshmgC|EamZ}AKk!-4UBF!7-wazR?$ z%dH+rN^UM-X&q}*uZy`B9HWk|tkHL%|CH*dJB4s~a$|@AD7;BbzlXpYKeb+qph(Ej%+2GbM!|A9+%u`QLa2-OP?2lU^@b zvy1+1jirSZgs1ypw0W(Gr6Y5FkC2EnHTIJHQ+465@yt}&ItEV0oKxGiHs{$D7QO>W zYa$KNz2G9+ry9A*$$muksi>JwYqO1auQvvLxRzH8@Y^n_&3_ z1cIqs)#R5Ng&S(y13m<-^O11Yy8=abOlZVa=LFk5j!8HJNpj3S*Lxu?e3BEA$PX$Pji0zl7HHyw>9A6+#*hAQ=sICjtAv&9L?rUx}A#` zJ*9;zkpOh|dHattZ=ZYa!z)f~W_Au?y|f~Ni@@>rP?^!MhZUbs-RO`8c>K4kgo+m$ zEg9$RizgctcDe#lBlu7c3C1O0h|s%y@t3FdgBoKH{=&XMzfAR4t`~YR63+N}TVUz2kRy)hsyJO3*oNOIT!mG4HqhT-j7kX!5_Fw;eGjraVhK#SKa{Wa@vSJH8jJx z`SV%=kLVeu*0=-e(t4|Z=!3~y*aym#RH6_|kY*&S>3GKFsLS(moM96_6jspn$b5fq z*Fy}+_8jl(LnRYEL(M1efvV1f~gY`!7+nTQLC(LVk0(Xs;#_B%N%pS9NJmgzMnTpA18Gk0cNxdf; zjSj&+74g_?wGuZwe3oiUDw=`9lc4MGkA*Ssxe+NC-AK{PKu(T@U3gD849CT%5w{xf zaDZutQ!~e9m1n(kLQ9nq+mF9=_03N4+4rhCN5RBhy_uwp(e-!$c z&losH#rgdETUn3QM;CTbhmJ6GmK(_Gt?N z1>_EhTWFFy6Zv}V15rs+?C%s<;F41e$TZcW|c* zX0d5!_yft+v4!p)#h1UOr-|s_G|bClU2gY=BRxDoT_zZ0M6>Mc&gSrBYo@;18HB!jo{8qA$>&Ace!~BjVHPd{p0c9NUHbUQ1$@` z69ZoMz@mlcS!qoRD{~1flzT4kt_x|}ene>}-1?BU#kqY#cu>W`OW zW1=x5M0>_(mq!PhtIt_RZ83FM1UFK&*;eUQWkb);PNt$CKYl3jogv>pJpZua{GI?n z&Pm{LUW2IH35e!zYi5hI)hO$~&uTJVx-Fe*kmDm~I27sZ$fCF2F_%t=@ND&rlNWl^cgn~$a;NRR>_-6AgpRp=4Pi2ldaJ@T3rQV(axb;=Z=7Mj8;en-P}tu=(YLV98dL(4_F10s<~ zp!#Z!QUCwXFjp@?5h9U)2+1hXlDcip%iJnGu!6!WJc^QH zK=SG026(5}75IN<7DN>U1-_=TPFMe8_g!|V-Q4)El{6|1i=stvCqM~~x>ARWGcv!& zHH}||cFfI36dY>g@vMLUw znIZ;|T_8xCDd0h%!{dyCC68}L4NOT+ zA!9atuDaio9h=DmEL`6_9#we`Pa=v2Mhz2`MrYxx+C^#yu$rDWT3%n@U?wT1{_0l( zc9B{AnV5_;?DB#PSC7!C%%)=X(j}IY8<`r0qJlR=Y5zV@B3>lEoG0u?K-UtwvPcV@ zD`)i<`C(xBg^AC;HCBYHN_U9P2yU#Ch?*QPFO1Dn%K#65a}8%7m*#T$4oz2CIyk<= z=?w91dN8gAF$qBU>7&6}Rv1p_M-PXIh`TwJw2|Sb>y0%t`7j zJ6mDUp7>8`em=>>&I%@zlEJj7B9F%=hse zUB};UeM)!g%h?#Zzg4t#063s59+#aD`}QnQH0-`up*F;Tqg07MEZ=`-V;`Ju^?*RJ z!uxc!2`(3erP&dDQ9mLSzgRY%%^qs=ga3YagGN-8wAL`CrucYI-oPEJ`f8}wkPw7{ zLL!vXpfM75cV-LN*x881KxOfJces6l506NO_7aYjeO`yJtnC)65d1?vq(2ymkG zCnsanJ{)h&4!wu}1ZW~4+!nDV2+GUrNfV@MYQC%=Sy%#qU0!I9r;x*={_ONz^NtWW z%gb|BTK1}HTF`3aGk?G|2OZNBH4d^drlFTgqppK4?@i&VZcFdhI^~<-4py|#=*W28_iKb*UQdrh}(a8GQ zg5c)MjXKh1rw!!_^Tac39GnP$!a6brh8TV?2zUU_xJ@3^)L}nUqSs| z^Md!;wCd@&YVElo(pU1$P>Y`OKY!#aG$KI+Ug=LbL^(~vd>>&`VU&*EEd0}ovP@BD zl9j7(T56`kwVj;;JKPkD&o1WS0?GkKB%SBG@c}0rgxvE>QVZ(pHDhX8T2fZeB(=tE zI8sv5WTq1{f5Z(1JPri=tc{Fvc^cT+ZDwk+WDS3_uYgc9eeKgvx5lQq?X51=oaNt% z$;rw6zKfDFu+wk0D0ek6l;nWm3dMXuRF`i0ZxH!xPE#j8FsHb=TiTi=@K0@J4d|Jf zrS6Vq4sp$%2WU`#;7_Krn11+QTF;WH>9%gB zce;EK-03q64R+J-Jm&B^nJj~hEtcCKi*^r`K_ZL+xn4Do=X-J}dU}S&%fS7sGc_f4 z%Ri}h!jm8r82hro;7bPevZPgqEvD013L3ikV^&o4hu+~8!b{LQ znT73q>;oS*ytjj=B=3#e->FT51Gk;NC+DLzY+7&V?p10f+pRw^RK#CFV9&?>qJEE# zF65YA9?oWOf~=6QPmt7y&W6YACDQC_+}*jtzOB*}>=+n*eBNH?t0r!}Z+k8Hvu52Mv# z3t`s38$jekd>w=Sy*n14GyUwUwY7B$PR{7k_lLN=I!07T1WezZkN^!zJkdKYkHe-; zX$BPw3ki*KK2iedkB|x~TIxX+(U}O4CzsIj{&-|o92}dOn2D$=jQCIjk&%{$87V1< zi&j%1YfV;SW`0Cbl%=KA^&WRtne6sF2S*6XF=fl`?7dGRCHqkcW5{nFQ5;DH8I1LL z-60UwTOy2l_~WWryR1DH!r9EY$U#V{rhd_#7-6}Ax%=qjdT3o zr)ocU2>9x~P@@>lc;_nJ#*|}AxLJ%Ehi(=)Ir>K|AvQbx5%rp}>Wn6!QBiv>v$F~2 z@C9`yC2|wZWfCHgbK1T$t0*W$`aWF_^5O29)n)m^KikY^@fK8l+^!{*HGDGC@IDtJzQVKzEX$q$GB@$qPX*eC8UY1r6JcGVc4+EkXfz zn^~eW>`%00^pdNP5M5vM-hj{g@1H_6Ry}Ii98t>o9M~*|y%0~N^?`-)1wzQjo5H}c zHBQr++=BkW1q2I8xB{yCiwm%-7=+wD;qO))$UScBkNkd7QBX1!;y?b75PiME>k0^-hw40Ehi+&daI;W-|;cA0e zpkUbEh$X(PwVY^n+Xh&kr2L+3|2#kpExWKS+27VdUgm7TaD13S8J1P z)6(w&io-o%j$1Tjo>4i-`EwgZ+AZf1dtB({N+v-4B8L$VfSMXQ$K1-X;H*oDOmRqv zU+?2=PfsGmPe;G=6Sb+{u*3e|9&9685CPL{^C^S{qJW$?K#bk!rL$3Ro;!$tgC-$F z>_AA^!V~=b6#M~&eQt}STD!j$=sT^98O&09L92qg@Pq+xEu+wm{>pZ%)qx+kQdB5t!GoUhPP7S4t*pc1 z1=HZ+zPfZGM$gBk9N>MnAD!|g8r$6{VtU!xH5(}>Xn+}kasVV0iMF;3>`RY#Ji|Uv z6gHENq}6{aiyKl4Hn1;a$_MUE$W#>DRn(2(Kb}af5+1zIanfq}j$)AK+0dkP19sOO zKIX{fWG%&|0>St4uza2}q9n7TLOvnzTvU6R?)T6$iFR3(=>_rH#*NJDu8~4o@uVF= zgI6O)nR4Erf14?q92^`2E+^l0noUG7jSUOhO}i*rr6XWgx1Gk;^M~=C&W4POVuqu9 zx2v=EPCniqf-M;XYq>Jy&v-6OW_P->E_qpXS8)RFM!C_TE2ZO1h9Y(bY9$VJ@yg93 zFYC!cZcaJh(|kch6I)-<*E{S3?j!Jo_HUG?nKOy-40OQB3htZ6dlAdPIxCTgYnaDY z#LhCnjeIJJ*+Flh&i-qxbLK4zRj4LJrBtSk#fj;hy(Bo(xz`rgt$5wY0(JijUe#LB-dt;%>+nxLM29)a_dydO zdCpg~j`4Pt-C}=@IO9w@GpamqJu4~5!z#&UG`!-r@_1pg*ch(5vH9c&*YkECToQ>2 zP&3_$cY>e4Zg2-6@HgP#LU7K@BXJMOBF|s02(d|3b(N zZO+3YcX(xQq`nW2jIi@BBZtM7HMF$Bs+}GJ`z2?uF^GtuxWhZbp_z<2bF(hDJ0~pi zx&!WTaE{KeAn0)#t+oW-Zx%y9Fo%RAAU}@`T)akmHlKo4$c=<2mfiVATv4@yf31+g zLOy0!A`&5uA?y0jPJ(k*r=IzyeLHF|Yj~YtZpN6XG&v6qfooHf&Z6^QTr);sPMZz5 z1QGG>^MuA+BA$>9n$1tLIXi^nHKWccXY~PPY9=P$Z=L?vzK={<$Ftfh{rX&(Jwb2{ zP05$S&h5nTSya1~g9zUK!t+Dx=j#o}T+vZ6A?rX`hhz#&Pou$Q$Bq7vZde?a_Z}>> z_0?t{%yz}h{(C4pNDMa;T+E@i+~>(yB_)7n4fJ0nvKRaD9<0a9D0Lg5eCqe?gI>L6 z3w%vnj74L0*d97HG<|SZYHH+tLxW7d-KiQT8#p=!hQa1j&ah=3yX3h!)fyj|Yi1QW zrEv4Hcz~AA?h^(m#uHPkv=a~Cv{q9i3pK(zE0+!L%_li?<*=#dA&;L3jYJtbQ7bWF z=h;t`SMCmzksO@B95a2)Ehyv}A0fqQ1t<~dKrSyYz^LuEm}k&3Q^N;-FpJ^+N$9PO zZN87NM6W<b3E0e+ z6a3CXLM8AtVyg7Eux9}k&GH@5w+yl>Y8j{UQPC3!PdHfE)lW|A>#dNMOQU4UQU3Kl zV>5>R_#1n(D`K)n{v3A0-)$%@l>cSZ2yj3cz24)Dk*A6PA7&zj99|^e}g+Wwa2M<(t zaW};H#GUb7YpkuIe({dAU6~wYEcZZ+pNry$UTX7l{ zz)kEuU0&$dj68v@ zB}&E=@)oL*6EC6#-s-vV6Y=96p6K-+TJ8Go&fL%`ocO9m3(ml}Kr}Ul&*SF}w|x!G z+S0*lVw4E?1Y3veNaK%|z;BV%KXJv+mK_U4=tjlQpKmbzCMU+M>Ps*|wUD9lwY91# z#E+z_>#df;Gq$|E6$pTVt0Gx3aXP^BtBCajEuJ~Eo`!-q^Z5wkydNx8=w8W~iq1;Q zmClRJYIc~8B-(Y3R1~v8M|#XkirVxAG?JL#-XY}zYzmS^r}quSnLzT0W{b;3rhr~E zkW^Co2f%r|J2)ar63&|<7Kh0I$+LA3*$RzT!0R0B?wGZL_wC-gMi(09X3Qoh3r|8H zT}gU4n#*+jhJ{kx1|188CB>;R#VNHKn$Bn4DfKDav@5tj#foykqJ}4 zqe>r~6qgW^y`jNxQY%#}GnlVf4CWIQWAmC!_e#K8wTrN#l59qfN5P?`KMeq$&0kVI z@+9i=Thgl}RZ%{_3=E6NDcFtJ)g#?zcYLWBWa5ZCF-ZC9`;>`|x`iI^E&P2p*4Eiu zq@^t93I}Q?qk+B`SFpw6(oC0|EbnwvoaHJ*kqHG*xf^0=;r(sF*W%+?kW$bu^*(o2 z%O1q%jsvU1NdJ<}$3FKb%f)S${=i@{!09fn?6)~$PQNWiqM=2ItRj`{gndDZFW}}1 zQyQ?w--pSr0}ax>12D}gK@{U7#j#$SkIzQ>-? zY)gqoWAPV(0Ku+T_@7j0`sZ?Oi#2EeWx~cX^Fx1`YHEt&f~xc*QAi`IzTdC1^IZkh zE_i}q?*1y{0MoeDla(=WWBYmOvgF#AuxQNPyYd)cv=<$;-+C=E^zMVB#Zo_|E{o8~ z+rDqBTpuq_KR=U(Z`Yy~+h?^Ya6m5N;{7a2<(>@g_A^BgCYoRPuO;L0b=bJzIw)Mf_<9 zBZZ(Kp;9(by|lT`T3GI76MG0xM4FfZ1e!fceg}A_0u964w@rD{y zwfDAwnj3)>o-f=PD(p#>^3Hd26Mg-7LFj#d!tVF}p4GAm3Li?)po-aRQB zTG_5IhubJpz3mo(?D=XnSRkMGHCnE4AW386>LiiVmpLIG!Ad}TWl}V~!*)CRs7mf| z#v?hgXyjRde!}bn*(3D*pz>O}iR}yXslJ1OP`WTF89~mkccY#V5WIad|C`b9JunN&1Vcsw;>#}XHF?(eP^Uai}m{;-Yp zI8;njmgmDa`_)|1g`|&^?{Bl)PJ=lVW7hD#YLAqVJpMVuo4 z<;J8@47*rWk#3ax7WwPI;+g4JO6N?HCUS4=+-yOT#4L%pz2V2(BLGBO7OND96i-bl zz8@a8@*i1P7e!sO9sIb5`=>3(JbSD0)b1`CPCb z?RE7>r@%?_n2)A9lb^62kJC!pgu{d*we=Fb;30ZU;Uwj3`#btnCO77W;+e>7ELr}GcPRd)+BJvYb3FmRnnD#rOIw>Y zaWnyt6l}})9?DcrRWk&$e73)Sm(paVHcwIHTG63g>hOGG@hoK_)_&L_gOIx@#pHED6-UK5{O~rl_h|KK3>mHbL4QtPYGdtwySig_H^FneCf2RRrxw?Z;eX!fj3j*b_*gNN1EdoP@PXPyKshCMT>G*q)DF9Z4q z267=>>gS>NxuES3_Vw`s5u^bD!8cV86!tmNlA+q_>K5DS{=SbPWwj@U-(1M~>EQ)& z3fxE_sHLRN4{RZqzkMrIpH6bAoRC9{$TMwVZ%}mdSRi(?vJEF@!=Wf;{4jcf4WtUp z<}GmMUj~})h<>?#(iG+GnwqX=VI#sD!`>$1PBvHaYnatBm?K0Wk-)7uZ6E2!b9*!8 zj=Fu$O#BI(Eh>tIjKj($tEjYR+lN*JB_>8ip3+em<5x7H?a3G^Wc1X~h+`D8LxzF3 z8%ta*Vu*p8NcRs}#U$KWPT7AU$!uZvFfqva$@9@K|LLtO*7^xM^x84)J<#+~vWuWl zTyrZ9l72NJ=pPWXvvCct%!j=o?aGWi&Q*d~~>D$CvhdS0VO{Oop}sc^sKK#s4F`B@h>F!-*3S(W=T`mJ8_=|F4WX z-)+|foMxWDGE{V8V}A@nmL0^c;5~9&rkpx?lB{GA8}vl^?y*;?DqZRo76b4<-ZmIi zjS8Y!xl}8H%0bOA2-w#kS`#D^fyf7Op#*fA{UBk9JS0dN0us8|7LJ2)$+Nssax*Ix z%QUJFFDrJl%6PDJtdKX$C3*KRjEoV8}XDm z03QF+Vv=e_05Z78hd--re36B!yR2 zRs!iI=M6ws7e(RzqVpeJF%ul2rd9z74)#h#{9iQ_Q*z$C=*52>VHxyzLhtRH1%rB| zO9q!q3)mm^u*%2!lvSDBygEMC9uAY+?mG4B1@NY$QYh8Eof+RgXy^ne+Ek8ee&F|j;qu44eHIP>4 zkkGc0{=3P}&Na`qLXaW|jFk4ah`ozMCCyy$%w1qBLZ6R*h51ngQhUwB5pXBR$HoSs zUWE?y_k%Szd;CvQwl^fhRN!^B9!y+Kn%URzomEy*Ie%~eZmPNlS^J`{zW&+^=JiK7 zLJUF=!LA3$3`NIT2Zwg&1xeIYEls;$`LzEHqGV*|jCo1%e>G#d0v^egnA1!$bfT1$ zZ^v(_LWYxV)@jQqD8kaGHntaO>FGyRq*i7QNIW1lgtiLl!NI}*ENM|OH6;g8dH^z> zk>Sx1m*0#mEYb0C33>Y$AA15S}E)l^hmfBUAq z9DHyPn^;&_C=PIx;;7)YU!EGAQ=K)^R#;S&{+1rG0-8Yb#EIIfi7APRcmy~&_=$;$ eXf)@ZU$Di3x(-(Ec>f6x!Q`ZrC2Pe^LjE5cro|Hg diff --git a/metadata/img-readme/download-alt.svg b/metadata/img-readme/download-alt.svg new file mode 100644 index 00000000..f97c9c3d --- /dev/null +++ b/metadata/img-readme/download-alt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website.svg b/metadata/img-readme/download-website.svg new file mode 100644 index 00000000..d0cf8375 --- /dev/null +++ b/metadata/img-readme/download-website.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png deleted file mode 100644 index 0e6a885004aba6dd61392155d4edbe021d4a304d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3451 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 D@^Fny diff --git a/metadata/img-readme/play.png b/metadata/img-readme/play.png deleted file mode 100644 index 2fb316c8d0edd9b8767f460417bf7cbe469c682a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15126 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 b?l Date: Tue, 26 Nov 2024 11:41:17 +0700 Subject: [PATCH 036/208] bugfix: added scroll on page with services list (#1262) * added scroll on page with services list * fixed margins on PageSetupWizardApiServicesList --- client/ui/qml/Controls2/CardWithIconsType.qml | 1 + .../Pages2/PageSetupWizardApiServicesList.qml | 119 +++++++++--------- .../Pages2/PageSetupWizardConfigSource.qml | 1 - 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index fea65116..18a29b87 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -145,6 +145,7 @@ Button { cursorShape: Qt.PointingHandCursor hoverEnabled: true + enabled: root.enabled onEntered: { backgroundRect.color = root.hoveredColor diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index 85a50393..f726cd49 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -16,83 +16,82 @@ PageType { defaultActiveFocusItem: focusItem - FlickableType { - id: fl + ColumnLayout { + id: header + anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.height + anchors.left: parent.left + anchors.right: parent.right - ColumnLayout { - id: content + spacing: 0 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Item { + id: focusItem + KeyNavigation.tab: backButton + } - spacing: 0 - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - - BackButtonType { - id: backButton - Layout.topMargin: 20 + BackButtonType { + id: backButton + Layout.topMargin: 20 // KeyNavigation.tab: fileButton.rightButton - } + } - HeaderType { - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 32 + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - headerText: qsTr("VPN by Amnezia") - descriptionText: qsTr("Choose a VPN service that suits your needs.") - } + headerText: qsTr("VPN by Amnezia") + descriptionText: qsTr("Choose a VPN service that suits your needs.") + } + } - ListView { - id: containers - width: parent.width - height: containers.contentItem.height - spacing: 16 + ListView { + id: servicesListView + anchors.top: header.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 16 + spacing: 0 - currentIndex: 1 - interactive: false - model: ApiServicesModel + currentIndex: 1 + clip: true + model: ApiServicesModel - delegate: Item { - implicitWidth: containers.width - implicitHeight: delegateContent.implicitHeight + ScrollBar.vertical: ScrollBar {} - ColumnLayout { - id: delegateContent + delegate: Item { + implicitWidth: servicesListView.width + implicitHeight: delegateContent.implicitHeight - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ColumnLayout { + id: delegateContent - CardWithIconsType { - id: card + anchors.fill: parent - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 + CardWithIconsType { + id: card - headerText: name - bodyText: cardDescription - footerText: price + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - rightImageSource: "qrc:/images/controls/chevron-right.svg" + headerText: name + bodyText: cardDescription + footerText: price - onClicked: { - if (isServiceAvailable) { - ApiServicesModel.setServiceIndex(index) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) - } - } + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + enabled: isServiceAvailable + + onClicked: { + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 7c031997..f973c89c 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -47,7 +47,6 @@ PageType { KeyNavigation.tab: textKey.textField } - HeaderType { property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() From 1d721ffb9abb4c0c5a123f33edb5c066a6eee40b Mon Sep 17 00:00:00 2001 From: Anton Sosnin Date: Wed, 27 Nov 2024 04:55:23 +0200 Subject: [PATCH 037/208] SteamDeck/OS installation fix (#1270) --- deploy/data/linux/post_install.sh | 10 ++++++++++ deploy/data/linux/post_uninstall.sh | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/deploy/data/linux/post_install.sh b/deploy/data/linux/post_install.sh index b3345bac..324462d9 100755 --- a/deploy/data/linux/post_install.sh +++ b/deploy/data/linux/post_install.sh @@ -19,6 +19,11 @@ date > $LOG_FILE echo "Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + if sudo systemctl is-active --quiet $APP_NAME; then sudo systemctl stop $APP_NAME >> $LOG_FILE sudo systemctl disable $APP_NAME >> $LOG_FILE @@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE echo "user desktop creation loop ended" >> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh index 5849a90e..98090d20 100755 --- a/deploy/data/linux/post_uninstall.sh +++ b/deploy/data/linux/post_uninstall.sh @@ -13,6 +13,11 @@ date >> $LOG_FILE echo "Uninstall Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do sudo unlink $dir >> $LOG_FILE done @@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then fi +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service after uninstall status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE From 9d96b1cd13ac1e6a06a0f413ceca9d187f839c71 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 29 Nov 2024 22:10:35 +0000 Subject: [PATCH 038/208] Update Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27a29edf..8b453907 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
    USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
    USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
    -XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 - +XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
    +TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns ## Acknowledgments This project is tested with BrowserStack. From 4efaf20a1ca5e88c137b6116eeba4a6765f48804 Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 2 Dec 2024 10:46:20 +0300 Subject: [PATCH 039/208] chore: fix deploy workflow (#1280) --- .github/workflows/deploy.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ce8d576..a51c19b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -217,7 +217,11 @@ jobs: export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin" export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos" export PATH=$PATH:~/go/bin - sh deploy/build_ios.sh + sh deploy/build_ios.sh | \ + sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d' env: IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }} IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }} From 5dc16c06f158660ee011ea5cdb46005c1121d5a2 Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 3 Dec 2024 08:47:33 +0300 Subject: [PATCH 040/208] chore: increased the api request timeout (#1276) --- client/core/controllers/apiController.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 75a3f93c..c50165e7 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -50,6 +50,8 @@ namespace constexpr char authData[] = "auth_data"; } + const int requestTimeoutMsecs = 12 * 1000; // 12 secs + ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { if (!sslErrors.empty()) { @@ -177,7 +179,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle QStringList ApiController::getProxyUrls() { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; @@ -280,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c if (serverConfig.value(config_key::configVersion).toInt()) { QNetworkRequest request; - request.setTransferTimeout(7000); + 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(); @@ -336,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); @@ -390,7 +392,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint)); From 1c1e74d06f6e6738a1976d2c3af7bf9b6f1e2286 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 6 Dec 2024 12:40:04 +0000 Subject: [PATCH 041/208] ru readme --- README_RU.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 README_RU.md diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 00000000..8b453907 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,191 @@ +# Amnezia VPN +## _The best client for self-hosted VPN_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![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) + +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. + +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) + +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) + +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). + + + + +[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) + +
    + + + +## Features + +- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. +- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. +- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). +- Windows, MacOS, Linux, Android, iOS releases. +- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). + +## Links + +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [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 support channel (English) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium + +## Tech + +AmneziaVPN uses several open-source projects to work: + +- [OpenSSL](https://www.openssl.org/) +- [OpenVPN](https://openvpn.net/) +- [Shadowsocks](https://shadowsocks.org/) +- [Qt](https://www.qt.io/) +- [LibSsh](https://libssh.org) - forked from Qt Creator +- and more... + +## Checking out the source code + +Make sure to pull all submodules after checking out the repo. + +```bash +git submodule update --init --recursive +``` + +## Development + +Want to contribute? Welcome! + +### Help with translations + +Download the most actual translation files. + +Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. +Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". + +Unzip this file. +Each *.ts file contains strings for one corresponding language. + +Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. +You can do it via a web-interface or any other method you're familiar with. + +### Building sources and deployment + +Check deploy folder for build scripts. + +### How to build an iOS app from source code on MacOS + +1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. + +2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: + - MacOS + - iOS + - Qt 5 Compatibility Module + - Qt Shader Tools + - Additional Libraries: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + +3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) + +4. You also need to install go >= v1.16. If you don't have it installed already, +download go from the [official website](https://golang.org/dl/) or use Homebrew. +The latest version is recommended. Install gomobile +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Build the project +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment + + +If you get `gomobile: command not found` make sure to set PATH to the location +of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Open the XCode project. You can then run /test/archive/ship the app. + +If the build fails with the following error +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with +key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +if the above error persists on your M1 Mac, then most probably you need to install arch based CMake +``` +arch -arm64 brew install cmake +``` + +Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that +require them. In this case, simply restart the build. + +## How to build the Android app + +_Tested on Mac OS_ + +The Android app has the following requirements: +* JDK 11 +* Android platform SDK 33 +* CMake 3.25.0 + +After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. + +- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. +- Set path to JDK 11 +- Set path to Android SDK (`$ANDROID_HOME`) + +In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  +Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  +Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. + +That's it! You should be ready to compile the project from QT Creator! + +### Development flow + +After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. +If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! + +You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. + +## License + +GPL v3.0 + +## Donate + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
    +USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
    +USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
    +XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
    +TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns +## Acknowledgments + +This project is tested with BrowserStack. +We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. From ea910ba30054d5a88a9bfc62f35b04e1ece2986f Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Dec 2024 22:15:01 +0200 Subject: [PATCH 042/208] Update README_RU.md --- README_RU.md | 181 +++++++++------------------------------------------ 1 file changed, 30 insertions(+), 151 deletions(-) diff --git a/README_RU.md b/README_RU.md index 8b453907..6ebdb97f 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,182 +1,60 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ +## _Лучший клиент для создания VPN на собственном сервере_ -[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) -[![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) - -[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[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) -### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) +### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting) > [!TIP] -> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). +> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). -[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
    -## Features +## Особенности -- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. -- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. -- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. -- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). -- Windows, MacOS, Linux, Android, iOS releases. -- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). +- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN. +- Классические 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. +- Поддержка конфигурации протокола 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). -## Links +## Ссылки -- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) -- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [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 support channel (English) -- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) -- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) -- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) -- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium +- [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\) -## Tech +## Технологии -AmneziaVPN uses several open-source projects to work: +AmneziaVPN использует несколько проектов с открытым исходным кодом: - [OpenSSL](https://www.openssl.org/) - [OpenVPN](https://openvpn.net/) - [Shadowsocks](https://shadowsocks.org/) - [Qt](https://www.qt.io/) -- [LibSsh](https://libssh.org) - forked from Qt Creator -- and more... +- [LibSsh](https://libssh.org) +- и другие... -## Checking out the source code - -Make sure to pull all submodules after checking out the repo. - -```bash -git submodule update --init --recursive -``` - -## Development - -Want to contribute? Welcome! - -### Help with translations - -Download the most actual translation files. - -Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. -Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". - -Unzip this file. -Each *.ts file contains strings for one corresponding language. - -Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. -You can do it via a web-interface or any other method you're familiar with. - -### Building sources and deployment - -Check deploy folder for build scripts. - -### How to build an iOS app from source code on MacOS - -1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. - -2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: - - MacOS - - iOS - - Qt 5 Compatibility Module - - Qt Shader Tools - - Additional Libraries: - - Qt Image Formats - - Qt Multimedia - - Qt Remote Objects - -3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) - -4. You also need to install go >= v1.16. If you don't have it installed already, -download go from the [official website](https://golang.org/dl/) or use Homebrew. -The latest version is recommended. Install gomobile -```bash -export PATH=$PATH:~/go/bin -go install golang.org/x/mobile/cmd/gomobile@latest -gomobile init -``` - -5. Build the project -```bash -export QT_BIN_DIR="/Qt//ios/bin" -export QT_MACOS_ROOT_DIR="/Qt//macos" -export QT_IOS_BIN=$QT_BIN_DIR -export PATH=$PATH:~/go/bin -mkdir build-ios -$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -``` -Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment - - -If you get `gomobile: command not found` make sure to set PATH to the location -of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. -```bash -export PATH=$(PATH):/path/to/GOPATH/bin -``` - -6. Open the XCode project. You can then run /test/archive/ship the app. - -If the build fails with the following error -``` -make: *** -[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] -Error 1 -``` -Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with -key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. - -if the above error persists on your M1 Mac, then most probably you need to install arch based CMake -``` -arch -arm64 brew install cmake -``` - -Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that -require them. In this case, simply restart the build. - -## How to build the Android app - -_Tested on Mac OS_ - -The Android app has the following requirements: -* JDK 11 -* Android platform SDK 33 -* CMake 3.25.0 - -After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. - -- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. -- Set path to JDK 11 -- Set path to Android SDK (`$ANDROID_HOME`) - -In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  -Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  -Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. - -That's it! You should be ready to compile the project from QT Creator! - -### Development flow - -After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. -If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! - -You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. - -## License +## Лицензия GPL v3.0 -## Donate +## Донаты Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) @@ -185,7 +63,8 @@ USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
    USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
    XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
    TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns -## Acknowledgments -This project is tested with BrowserStack. -We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. From 569d63ef0f750f9938dfdda2fc69c9559a7be4c8 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 7 Dec 2024 15:53:40 +0200 Subject: [PATCH 043/208] Add files via upload --- metadata/img-readme/download-website-ru.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 metadata/img-readme/download-website-ru.svg diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 00000000..386ae4fe --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + From d67201ede9dc2f39b525ec55e04ac0225e551462 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:34:18 +0200 Subject: [PATCH 044/208] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b453907..8f887808 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ + +### _The best client for self-hosted VPN_ + [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![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_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md) + + [Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) From c5aa070bf4cd7ff1de931dc22a887aea3104ae92 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:49:26 +0200 Subject: [PATCH 045/208] Update README_RU.md --- README_RU.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README_RU.md b/README_RU.md index 6ebdb97f..fe9dd286 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,6 +1,11 @@ # Amnezia VPN -## _Лучший клиент для создания VPN на собственном сервере_ +### _Лучший клиент для создания VPN на собственном сервере_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![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 sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) @@ -10,8 +15,8 @@ > [!TIP] > Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). - - + + [Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) From 6ea6ab1bd983fd2be880e9e14a9184bda9b79349 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 8 Dec 2024 08:14:22 +0300 Subject: [PATCH 046/208] chore: added clang-format config files (#1293) --- .clang-format | 39 +++++++++++++++++++++++++++++++++++++++ .clang-format-ignore | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-format-ignore diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..5c459fd2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,39 @@ +BasedOnStyle: WebKit +AccessModifierOffset: '-4' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortEnumsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: None +AlwaysBreakTemplateDeclarations: 'No' +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakConstructorInitializers: BeforeColon +ColumnLimit: '120' +CommentPragmas: '"^!|^:"' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '8' +IndentPPDirectives: BeforeHash +NamespaceIndentation: All +PenaltyExcessCharacter: '10' +PointerAlignment: Right +SortIncludes: 'true' +SpaceAfterTemplateKeyword: 'false' +Standard: Auto diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 00000000..4019357f --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,20 @@ +/client/3rd +/client/3rd-prebuild +/client/android +/client/cmake +/client/core/serialization +/client/daemon +/client/fonts +/client/images +/client/ios +/client/mozilla +/client/platforms/dummy +/client/platforms/linux +/client/platforms/macos +/client/platforms/windows +/client/server_scripts +/client/translations +/deploy +/docs +/metadata +/service/src From 2db99715b1fc5a7ef1f8b800c72d6e5b3422ce1f Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Dec 2024 09:32:49 +0300 Subject: [PATCH 047/208] feature: added subscription expiration date for premium v2 (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: added subscription expiration date for premium v2 * feature: added a check for the presence of the “services” field in the response body of the getServicesList() function * feature: added prohibition to change location when connection is active * bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend --- client/core/controllers/apiController.cpp | 7 + client/core/defs.h | 1 + client/core/errorstrings.cpp | 3 +- .../ui/controllers/connectionController.cpp | 2 +- client/ui/models/apiServicesModel.cpp | 114 ++++++--- client/ui/models/apiServicesModel.h | 40 ++- client/ui/models/servers_model.cpp | 33 ++- client/ui/models/servers_model.h | 5 + .../Pages2/PageSettingsApiLanguageList.qml | 6 + .../qml/Pages2/PageSettingsApiServerInfo.qml | 7 +- .../ui/qml/Pages2/PageSettingsServerInfo.qml | 227 +++++++++--------- 11 files changed, 285 insertions(+), 160 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index c50165e7..6562632a 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -379,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); + + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + return ErrorCode::ApiServicesMissingError; + } + } + return errorCode; } diff --git a/client/core/defs.h b/client/core/defs.h index d00d347b..c0db2e12 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 49534606..70f433c6 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -63,7 +63,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; 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; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f8516f6e..f9491d4e 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -55,7 +55,7 @@ void ConnectionController::openConnection() && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by end_date event"; + qDebug() << "attempt to update api config by expires_at event"; if (configVersion == ApiConfigSources::Telegram) { emit updateApiConfigFromTelegram(); } else { diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 2a87bde3..81a10f87 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -27,6 +27,9 @@ namespace constexpr char storeEndpoint[] = "store_endpoint"; constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; } namespace serviceType @@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject service = m_services.at(index.row()).toObject(); - QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); - auto serviceType = service.value(configKey::serviceType).toString(); + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; switch (role) { case NameRole: { - return serviceInfo.value(configKey::name).toString(); + return apiServiceData.serviceInfo.name; } case CardDescriptionRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); + auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); } else if (serviceType == serviceType::amneziaFree){ QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { description += tr("

    Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); } return description; @@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case IsServiceAvailableRole: { if (serviceType == serviceType::amneziaFree) { - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { return false; } } return true; } case SpeedRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); - return tr("%1 MBit/s").arg(speed); + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); } - case WorkPeriodRole: { - auto timelimit = serviceInfo.value(configKey::timelimit).toString(); - if (timelimit == "0") { + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { return ""; } - return tr("%1 days").arg(timelimit); + return tr("%1 days").arg(timeLimit); } case RegionRole: { - return serviceInfo.value(configKey::region).toString(); + return apiServiceData.serviceInfo.region; } case FeaturesRole: { if (serviceType == serviceType::amneziaPremium) { @@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } } case PriceRole: { - auto price = serviceInfo.value(configKey::price).toString(); + auto price = apiServiceData.serviceInfo.price; if (price == "free") { return tr("Free"); } return tr("%1 $/month").arg(price); } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } } return QVariant(); @@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data) { beginResetModel(); - m_countryCode = data.value(configKey::userCountryCode).toString(); - m_services = data.value(configKey::services).toArray(); - if (m_services.isEmpty()) { - QJsonObject service; - service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo)); - service.insert(configKey::serviceType, data.value(configKey::serviceType)); + m_services.clear(); - m_services.push_back(service); + m_countryCode = data.value(configKey::userCountryCode).toString(); + auto services = data.value(configKey::services).toArray(); + + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + m_services.push_back(getApiServicesData(service.toObject())); + } } endResetModel(); @@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index) QJsonObject ApiServicesModel::getSelectedServiceInfo() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceInfo).toObject(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; } QString ApiServicesModel::getSelectedServiceType() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceType).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.type; } QString ApiServicesModel::getSelectedServiceProtocol() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceProtocol).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; } QString ApiServicesModel::getSelectedServiceName() { - auto modelIndex = index(m_selectedServiceIndex, 0); - return data(modelIndex, ApiServicesModel::Roles::NameRole).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; } QJsonArray ApiServicesModel::getSelectedServiceCountries() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::availableCountries).toArray(); + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; } QString ApiServicesModel::getCountryCode() @@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode() QString ApiServicesModel::getStoreEndpoint() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::storeEndpoint).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; } QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) @@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; - roles[WorkPeriodRole] = "workPeriod"; + roles[TimeLimitRole] = "timeLimit"; roles[RegionRole] = "region"; roles[FeaturesRole] = "features"; roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; return roles; } + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString(); + + if (serviceInfo.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 49918940..c96a49ab 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -3,6 +3,7 @@ #include #include +#include class ApiServicesModel : public QAbstractListModel { @@ -15,10 +16,11 @@ public: ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ protected: QHash roleNames() const override; private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + QString m_countryCode; - QJsonArray m_services; + QVector m_services; int m_selectedServiceIndex; }; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index c87499a7..b72b10c3 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -22,7 +22,7 @@ namespace constexpr char serviceProtocol[] = "service_protocol"; constexpr char publicKeyInfo[] = "public_key"; - constexpr char endDate[] = "end_date"; + constexpr char expiresAt[] = "expires_at"; } } @@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); - const QString endDate = publicKeyInfo.value(configKey::endDate).toString(); - if (endDate.isEmpty()) { - publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); serverConfig.insert(configKey::apiConfig, apiConfig); editServer(serverConfig, serverIndex); @@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) return false; } - auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC(); - if (endDateDateTime < QDateTime::currentDateTimeUtc()) { + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 0f18ea30..78bc22cc 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -115,6 +116,7 @@ public slots: QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); @@ -127,6 +129,9 @@ protected: signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 120313cd..600db85d 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -54,8 +54,14 @@ PageType { imageSource: "qrc:/images/controls/download.svg" checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + if (index !== ApiCountryModel.currentIndex) { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 2d6c1d9b..167e56e5 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -56,12 +56,15 @@ PageType { } LabelWithImageType { + property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable") + Layout.fillWidth: true Layout.margins: 16 imageSource: "qrc:/images/controls/history.svg" - leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period") + rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate") + : ApiServicesModel.getSelectedServiceData("workPeriod") visible: rightText !== "" } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 95ae5c8a..ffcfb441 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -25,6 +25,8 @@ PageType { property int pageSettingsApiServerInfo: 3 property int pageSettingsApiLanguageList: 4 + property var processedServer + defaultActiveFocusItem: focusItem Connections { @@ -35,8 +37,18 @@ PageType { } } + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -44,147 +56,139 @@ PageType { value: true } ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } Item { id: focusItem - KeyNavigation.tab: header + //KeyNavigation.tab: header } ColumnLayout { anchors.fill: parent - spacing: 16 + spacing: 4 - Repeater { - id: header - model: proxyServersModel + BackButtonType { + id: backButton - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() + Layout.topMargin: 20 + KeyNavigation.tab: headerContent.actionButton + + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } + + HeaderType { + id: headerContent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" + : "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) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName + } } - delegate: ColumnLayout { + KeyNavigation.tab: tabBar - property alias focusItem: backButton + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.open() + } + } + } - id: content + DrawerType2 { + id: serverNameEditDrawer - Layout.topMargin: 20 + parent: root - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton + anchors.fill: parent + expandedHeight: root.height * 0.35 - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } + onClosed: { + if (!GC.isMobile()) { + headerContent.actionButton.forceActiveFocus() + } + } + + expandedContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Connections { + target: serverNameEditDrawer + enabled: !GC.isMobile() + function onOpened() { + serverName.textField.forceActiveFocus() } } - HeaderType { - id: headerContent + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } + + TextFieldWithHeaderType { + id: serverName + Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) { - return serverDescription - } else if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar - - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + KeyNavigation.tab: saveButton } - DrawerType2 { - id: serverNameEditDrawer + BasicButtonType { + id: saveButton - parent: root + Layout.fillWidth: true - anchors.fill: parent - expandedHeight: root.height * 0.35 + text: qsTr("Save") + KeyNavigation.tab: focusItem1 - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Connections { - target: serverNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - serverName.textField.forceActiveFocus() - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return } - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } - - TextFieldWithHeaderType { - id: serverName - - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true - - KeyNavigation.tab: saveButton - } - - BasicButtonType { - id: saveButton - - Layout.fillWidth: true - - text: qsTr("Save") - KeyNavigation.tab: focusItem1 - - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } - - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } + if (serverName.textFieldText !== root.processedServer.name) { + ServersModel.setProcessedServerData("name", serverName.textFieldText); } + serverNameEditDrawer.close() } } } @@ -257,8 +261,7 @@ PageType { StackLayout { id: nestedStackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + Layout.fillWidth: true currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? From d06924c59dd8684c28b6257efe5d1a11db34b19b Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Tue, 10 Dec 2024 03:17:16 +0100 Subject: [PATCH 048/208] feature/xray user management (#972) * feature: implement client management functionality for Xray --------- Co-authored-by: aiamnezia Co-authored-by: vladimir.kuznetsov --- client/configurators/xray_configurator.cpp | 165 +++++++++- client/configurators/xray_configurator.h | 4 + client/ui/controllers/exportController.cpp | 9 +- client/ui/controllers/exportController.h | 2 +- client/ui/models/clientManagementModel.cpp | 353 +++++++++++++++++++-- client/ui/models/clientManagementModel.h | 6 + client/ui/qml/Pages2/PageShare.qml | 2 +- 7 files changed, 495 insertions(+), 46 deletions(-) diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c..514aa821 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -3,38 +3,169 @@ #include #include #include +#include +#include "logger.h" #include "containers/containers_defs.h" #include "core/controllers/serverController.h" #include "core/scripts_registry.h" +namespace { +Logger logger("XrayConfigurator"); +} + XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent) : ConfiguratorBase(settings, serverController, parent) { } -QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode &errorCode) +QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) { - QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), - m_serverController->genVarsForScript(credentials, container, containerConfig)); - - QString xrayPublicKey = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - xrayPublicKey.replace("\n", ""); - - QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode); - xrayUuid.replace("\n", ""); - - QString xrayShortId = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); - xrayShortId.replace("\n", ""); - + // Generate new UUID for client + QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Get current server config + QString currentConfig = m_serverController->getTextFileFromContainer( + container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to get server config file"; return ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + // Parse current config as JSON + QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + logger.error() << "Failed to parse server config JSON"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject serverConfig = doc.object(); + + // Validate server config structure + if (!serverConfig.contains("inbounds")) { + logger.error() << "Server config missing 'inbounds' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray inbounds = serverConfig["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Server config has empty 'inbounds' array"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Inbound missing 'settings' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Settings missing 'clients' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray clients = settings["clients"].toArray(); + + // Create configuration for new client + QJsonObject clientConfig { + {"id", clientId}, + {"flow", "xtls-rprx-vision"} + }; + + clients.append(clientConfig); + + // Update config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + serverConfig["inbounds"] = inbounds; + + // Save updated config to server + QString updatedConfig = QJsonDocument(serverConfig).toJson(); + errorCode = m_serverController->uploadTextFileToContainer( + container, + credentials, + updatedConfig, + amnezia::protocols::xray::serverConfigPath, + libssh::ScpOverwriteMode::ScpOverwriteExisting + ); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to upload updated config"; + return ""; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + errorCode = m_serverController->runScript( + credentials, + m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container)) + ); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to restart container"; + return ""; + } + + return clientId; +} + +QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) +{ + // Get client ID from prepareServerConfig + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode); + if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { + logger.error() << "Failed to prepare server config"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), + m_serverController->genVarsForScript(credentials, container, containerConfig)); + + if (config.isEmpty()) { + logger.error() << "Failed to get config template"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString xrayPublicKey = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayPublicKey.replace("\n", ""); + + QString xrayShortId = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayShortId.replace("\n", ""); + + // Validate all required variables are present + if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { + logger.error() << "Config template missing required variables:" + << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") + << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") + << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); + errorCode = ErrorCode::InternalError; + return ""; + } + + config.replace("$XRAY_CLIENT_ID", xrayClientId); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_SHORT_ID", xrayShortId); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 2acfdf71..8ed4e775 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ public: QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2690b5b1..8681406e 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index b031ea39..a2c9fcfa 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -28,7 +28,7 @@ public slots: void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7445d60f..f07eae71 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + + return appendClient(clientId, clientName, container, credentials, serverController); } ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, @@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + break; } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + ErrorCode error = ErrorCode::NoError; + + // Get server config + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + // Get client ID to remove + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + // Remove client from server config + QJsonObject configObj = serverConfig.object(); + if (!configObj.contains("inbounds")) { + logger.error() << "Missing inbounds in xray config"; + return ErrorCode::InternalError; + } + + QJsonArray inbounds = configObj["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Empty inbounds array in xray config"; + return ErrorCode::InternalError; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings"; + return ErrorCode::InternalError; + } + + QJsonArray clients = settings["clients"].toArray(); + if (clients.isEmpty()) { + logger.error() << "Empty clients array in xray config"; + return ErrorCode::InternalError; + } + + for (int i = 0; i < clients.size(); ++i) { + QJsonObject clientObj = clients[i].toObject(); + if (clientObj.contains("id") && clientObj["id"].toString() == clientId) { + clients.removeAt(i); + break; + } + } + + // Update server config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + configObj["inbounds"] = inbounds; + + // Upload updated config + error = serverController->uploadTextFileToContainer( + container, + credentials, + QJsonDocument(configObj).toJson(), + serverConfigPath + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload updated xray config"; + return error; + } + + // Remove from local table + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + // Update clients table file on server + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable") + .arg(ContainerProps::containerTypeToString(container)); + + error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file"; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + error = serverController->runScript( + credentials, + serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container)) + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to restart xray container"; + return error; + } + + return error; +} + QHash ClientManagementModel::roleNames() const { QHash roles; @@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const roles[DataSentRole] = "dataSent"; roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 60132abe..989120a9 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -40,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -64,11 +66,15 @@ private: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 995fa3e7..d6ce7848 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -92,7 +92,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray" From 367789bda28f33a11a72d61b222453c5b502299e Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 14 Dec 2024 14:29:33 +0200 Subject: [PATCH 049/208] Update README_RU.md (#1300) * Update README_RU.md --- README_RU.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/README_RU.md b/README_RU.md index fe9dd286..59518f4b 100644 --- a/README_RU.md +++ b/README_RU.md @@ -55,6 +55,112 @@ AmneziaVPN использует несколько проектов с откр - [LibSsh](https://libssh.org) - и другие... +## Проверка исходного кода +После клонирования репозитория обязательно загрузите все подмодули. + +```bash +git submodule update --init --recursive +``` + + +## Разработка +Хотите внести свой вклад? Добро пожаловать! + +### Помощь с переводами + +Загрузите самые актуальные файлы перевода. + +Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations". + +Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка. + +Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом. + +### Сборка исходного кода и деплой +Проверьте папку deploy для скриптов сборки. + +### Как собрать iOS-приложение из исходного кода на MacOS +1. Убедитесь, что у вас установлен XCode версии 14 или выше. +2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули: +- MacOS +- iOS +- Модуль совместимости с Qt 5 +- Qt Shader Tools +- Дополнительные библиотеки: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + + +3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь. +4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile: + +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Соберите проект: +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Замените и на ваши значения. + +Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile: +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение. + +Если сборка завершится с ошибкой: +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM: +``` +arch -arm64 brew install cmake +``` + + При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку. + + +## Как собрать Android-приложение +Сборка тестировалась на MacOS. Требования: +- JDK 11 +- Android SDK 33 +- CMake 3.25.0 + +Установите QT, QT Creator и Android Studio. +Настройте QT Creator: + +- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`. +- Укажите путь к JDK 11. +- Укажите путь к Android SDK (`$ANDROID_HOME`) + +Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере! + +Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake. + +Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK. + +### Разработка Android-компонентов + +После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt__Clang_-`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt__Clang_-/client/android-build` в качестве корневой. +Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения. +Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`/client/android-build/.`). + + ## Лицензия GPL v3.0 From 48f6cf904e7f41c002be3a6daf3b53535191456c Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 19 Dec 2024 10:36:20 +0300 Subject: [PATCH 050/208] chore/minor UI fixes (#1308) * chore: corrected the translation error * bugfix: fixed basic button left iamge color --- client/translations/amneziavpn_ru_RU.ts | 2 +- client/ui/qml/Controls2/BasicButtonType.qml | 2 +- client/ui/qml/Pages2/PageHome.qml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 2fb21259..c0d855b2 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -2679,7 +2679,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Where to get connection data, step-by-step instructions for buying a VPS - Где взять данные для подключения, пошаговые инстуркции по покупке VPS + Где взять данные для подключения, пошаговые инструкции по покупке VPS diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 828c32bc..ef66e0e2 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -24,7 +24,7 @@ Button { property string leftImageSource property string rightImageSource - property string leftImageColor + property string leftImageColor: textColor property bool changeLeftImageSize: true property bool squareLeftSide: false diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8422a10f..e5112575 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -110,6 +110,7 @@ PageType { text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageColor: "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() From b88ab8e432fbd2ab5002856cd83ef57181a6d7fa Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 23 Dec 2024 04:27:09 +0300 Subject: [PATCH 051/208] fix(build): fix aqtinstall (#1312) --- .github/workflows/deploy.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a51c19b2..35e740b0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -335,7 +335,8 @@ jobs: arch: 'linux_gcc_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86_64 Qt' uses: jurplel/install-qt-action@v4 @@ -346,7 +347,8 @@ jobs: arch: 'android_x86_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86 Qt' uses: jurplel/install-qt-action@v4 @@ -357,7 +359,8 @@ jobs: arch: 'android_x86' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_armv7 Qt' uses: jurplel/install-qt-action@v4 @@ -368,7 +371,8 @@ jobs: arch: 'android_armv7' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_arm64_v8a Qt' uses: jurplel/install-qt-action@v4 @@ -379,7 +383,8 @@ jobs: arch: 'android_arm64_v8a' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Grant execute permission for qt-cmake' shell: bash From 2bff37efae583a9606e5fae263dbbac907338458 Mon Sep 17 00:00:00 2001 From: Mikhail Kiselev <73298492+sund3RRR@users.noreply.github.com> Date: Sat, 28 Dec 2024 08:02:14 +0300 Subject: [PATCH 052/208] fix: segmentation violation due to missing return (#1321) --- client/utilities.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/utilities.cpp b/client/utilities.cpp index 1cc69aeb..4caa8f70 100755 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -248,7 +248,7 @@ bool Utils::killProcessByName(const QString &name) #elif defined Q_OS_IOS || defined(Q_OS_ANDROID) return false; #else - QProcess::execute(QString("pkill %1").arg(name)); + return QProcess::execute(QString("pkill %1").arg(name)) == 0; #endif } From 212e9b3a9151b2793a5cc1ccb6dfb00de9155aca Mon Sep 17 00:00:00 2001 From: Andrey Alekseenko Date: Mon, 30 Dec 2024 05:45:26 +0000 Subject: [PATCH 053/208] fix: adding second new VMess links now works (#1325) --- client/core/serialization/vmess_new.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/core/serialization/vmess_new.cpp b/client/core/serialization/vmess_new.cpp index 6f3ec3e1..68d32203 100644 --- a/client/core/serialization/vmess_new.cpp +++ b/client/core/serialization/vmess_new.cpp @@ -104,7 +104,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes server.users.first().security = "auto"; } - const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { + const auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { if (query.hasQueryItem(key)) return query.queryItemValue(key, QUrl::FullyDecoded); else From 6acaab0ffa4960acb0ee603db81037603814638b Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Tue, 31 Dec 2024 04:16:52 +0100 Subject: [PATCH 054/208] Improve navigation cpp (#1061) * add focusController class * add more key handlers * add focus navigation to qml * fixed language selector * add reverse focus change to FocusController * add default focus item * update transitions * update pages * add ListViewFocusController * fix ListView navigation * update CardType for using with focus navigation * remove useless key navigation * remove useless slots, logs, Drawer open and close * fix reverse focus move on listView * fix drawer radio buttons selection * fix drawer layout and focus move * fix PageSetupWizardProtocolSettings focus move * fix back navigation on default focus item * fix crashes after ListView navigation * fix protocol settings focus move * fix focus on users on page share * clean up page share * fix server rename * fix page share default server selection * refactor about page for correct focus move * fix focus move on list views with header and-or footer * minor fixes * fix server list back button handler * fix spawn signals on switch * fix share details drawer * fix drawer open close usage * refactor listViewFocusController * refactor focusController to make the logic more straightforward * fix focus on notification * update config page for scrolling with tab * fix crash on return with esc key * fix focus navigation in dynamic delegate of list view * fix focus move on qr code on share page * refactor page logging settings for focus navigation * update popup * Bump version * Add mandatory requirement for android.software.leanback. * Fix importing files on TVs * fix: add separate method for reading files to fix file reading on Android TV * fix(android): add CHANGE_NETWORK_STATE permission for all Android versions * Fix connection check for AWG/WG * chore: minor fixes (#1235) * fix: add a workaround to open files on Android TV due to lack of SAF * fix: change the banner format for TV * refactor: make TvFilePicker activity more sustainable * fix: add the touch emulation method for Android TV * fix: null uri processing * fix: add the touch emulation method for Android TV * fix: hide UI elements that use file saving * chore: bump version code * add `ScrollBarType` * update initial config page * refactor credentials setup page to handle the focus navigation * add `setDelegateIndex` method to `listViewFocusController` * fix focus behavior on new page/popup * make minor fixes and clean up * fix: get rid of the assign function call * Scrollbar is on if the content is larger than a screen * Fix selection in language change list * Update select language list * update logging settings page * fix checked item in lists * fix split tunneling settings * make unchangable properties readonly * refactor SwitcherType * fix hide/unhide password * `PageShare` readonly properties * Fix list view focus moving on `PageShare` * remove manual focus control on `PageShare` * format `ListViewFocusController` * format `FocusController` * add `focusControl` with utility functions for focus control * refactor `listViewFocusController` acoording to `focusControl` * refactor `focusConroller` according to `focusControl` * add `printSectionName` method to `listViewController` * remove arrow from `Close application` item * fix focus movement in `ServersListView` * `Restore from backup` is visible only on start screen * `I have nothing` is visible only on start screen * fix back button on `SelectLanguageDrawer` * rename `focusControl` to `qmlUtils` * fix `CMakeLists.txt` * fix `ScrollBarType` * fix `PageSetupWizardApiServicesList` * fix focus movement on dynamic delegates in listView * refactor `PageSetupWizardProtocols` * remove comments and clean up * fix `ListViewWithLabelsType` * fix `PageProtocolCloakSettings` * fix `PageSettingsAppSplitTunneling` * fix `PageDevMenu` * remove debug output from `FocusController` * remove debug output from `ListViewFocusController` * remove debug output from `focusControl` * `focusControl` => `FocusControl` --------- Co-authored-by: albexk Co-authored-by: Nethius --- CMakeLists.txt | 4 +- client/CMakeLists.txt | 2 + client/amnezia_application.cpp | 3 + client/amnezia_application.h | 2 + client/android/AndroidManifest.xml | 9 +- .../res/mipmap-anydpi-v26/ic_banner.xml | 5 - client/android/res/mipmap-hdpi/ic_banner.png | Bin 0 -> 15410 bytes client/android/res/mipmap-mdpi/ic_banner.png | Bin 0 -> 10138 bytes .../res/mipmap-xhdpi/ic_banner_foreground.png | Bin 12414 -> 0 bytes client/android/res/values-ru/strings.xml | 2 + .../res/values/ic_banner_background.xml | 4 - client/android/res/values/strings.xml | 2 + .../src/org/amnezia/vpn/AmneziaActivity.kt | 204 +++++- .../src/org/amnezia/vpn/TvFilePicker.kt | 45 ++ .../platforms/android/android_controller.cpp | 28 +- client/platforms/android/android_controller.h | 4 + client/resources.qrc | 384 +++++------ client/ui/controllers/focusController.cpp | 210 ++++++ client/ui/controllers/focusController.h | 57 ++ client/ui/controllers/importController.cpp | 22 +- .../controllers/listViewFocusController.cpp | 309 +++++++++ .../ui/controllers/listViewFocusController.h | 70 ++ client/ui/controllers/pageController.cpp | 18 +- client/ui/controllers/pageController.h | 7 +- client/ui/controllers/settingsController.cpp | 8 +- client/ui/controllers/sitesController.cpp | 6 +- client/ui/controllers/systemController.cpp | 34 +- client/ui/controllers/systemController.h | 6 +- client/ui/qml/Components/ConnectButton.qml | 26 + .../ConnectionTypeSelectionDrawer.qml | 23 +- .../qml/Components/HomeContainersListView.qml | 58 +- .../Components/HomeSplitTunnelingDrawer.qml | 31 +- .../ui/qml/Components/InstalledAppsDrawer.qml | 13 +- client/ui/qml/Components/QuestionDrawer.qml | 19 +- .../qml/Components/SelectLanguageDrawer.qml | 230 +++---- client/ui/qml/Components/ServersListView.qml | 126 ++++ .../Components/SettingsContainersListView.qml | 37 +- .../qml/Components/ShareConnectionDrawer.qml | 110 +-- .../qml/Components/TransportProtoSelector.qml | 2 - client/ui/qml/Controls2/BackButtonType.qml | 8 +- client/ui/qml/Controls2/BasicButtonType.qml | 29 +- client/ui/qml/Controls2/CardType.qml | 31 +- client/ui/qml/Controls2/CardWithIconsType.qml | 30 +- client/ui/qml/Controls2/DrawerType2.qml | 193 ++++-- client/ui/qml/Controls2/DropDownType.qml | 136 ++-- client/ui/qml/Controls2/FlickableType.qml | 5 +- client/ui/qml/Controls2/HeaderType.qml | 2 - .../qml/Controls2/HorizontalRadioButton.qml | 26 + client/ui/qml/Controls2/ImageButtonType.qml | 35 +- .../ui/qml/Controls2/LabelWithButtonType.qml | 26 + .../qml/Controls2/ListViewWithLabelsType.qml | 6 +- .../Controls2/ListViewWithRadioButtonType.qml | 193 +++--- client/ui/qml/Controls2/PageType.qml | 43 +- client/ui/qml/Controls2/PopupType.qml | 22 +- client/ui/qml/Controls2/ScrollBarType.qml | 11 + client/ui/qml/Controls2/SwitcherType.qml | 43 +- client/ui/qml/Controls2/TabButtonType.qml | 27 +- .../ui/qml/Controls2/TabImageButtonType.qml | 29 +- .../qml/Controls2/TextAreaWithFooterType.qml | 3 - .../qml/Controls2/TextFieldWithHeaderType.qml | 24 +- .../ui/qml/Controls2/VerticalRadioButton.qml | 27 +- client/ui/qml/Pages2/PageDevMenu.qml | 52 +- client/ui/qml/Pages2/PageHome.qml | 279 ++------ .../Pages2/PageProtocolAwgClientSettings.qml | 416 ++++++----- .../ui/qml/Pages2/PageProtocolAwgSettings.qml | 644 +++++++++--------- .../qml/Pages2/PageProtocolCloakSettings.qml | 22 +- .../Pages2/PageProtocolOpenVpnSettings.qml | 45 +- client/ui/qml/Pages2/PageProtocolRaw.qml | 45 +- .../PageProtocolShadowSocksSettings.qml | 19 +- .../PageProtocolWireGuardClientSettings.qml | 2 - .../Pages2/PageProtocolWireGuardSettings.qml | 39 +- .../qml/Pages2/PageProtocolXraySettings.qml | 19 - .../ui/qml/Pages2/PageServiceDnsSettings.qml | 10 - .../ui/qml/Pages2/PageServiceSftpSettings.qml | 21 - .../Pages2/PageServiceSocksProxySettings.qml | 41 +- .../Pages2/PageServiceTorWebsiteSettings.qml | 10 - client/ui/qml/Pages2/PageSettings.qml | 25 +- client/ui/qml/Pages2/PageSettingsAbout.qml | 227 +++--- .../Pages2/PageSettingsApiLanguageList.qml | 128 ++-- .../qml/Pages2/PageSettingsApiServerInfo.qml | 14 - .../Pages2/PageSettingsAppSplitTunneling.qml | 39 +- .../ui/qml/Pages2/PageSettingsApplication.qml | 38 +- client/ui/qml/Pages2/PageSettingsBackup.qml | 17 +- .../ui/qml/Pages2/PageSettingsConnection.qml | 41 +- client/ui/qml/Pages2/PageSettingsDns.qml | 21 +- client/ui/qml/Pages2/PageSettingsLogging.qml | 220 +++--- .../ui/qml/Pages2/PageSettingsServerData.qml | 20 - .../ui/qml/Pages2/PageSettingsServerInfo.qml | 105 +-- .../qml/Pages2/PageSettingsServerProtocol.qml | 219 +++--- .../Pages2/PageSettingsServerProtocols.qml | 76 +-- .../qml/Pages2/PageSettingsServerServices.qml | 72 +- .../ui/qml/Pages2/PageSettingsServersList.qml | 123 ++-- .../qml/Pages2/PageSettingsSplitTunneling.qml | 248 ++----- .../Pages2/PageSetupWizardApiServiceInfo.qml | 8 - .../Pages2/PageSetupWizardApiServicesList.qml | 23 +- .../Pages2/PageSetupWizardConfigSource.qml | 255 ++++--- .../qml/Pages2/PageSetupWizardCredentials.qml | 228 +++++-- client/ui/qml/Pages2/PageSetupWizardEasy.qml | 15 +- .../PageSetupWizardProtocolSettings.qml | 78 +-- .../qml/Pages2/PageSetupWizardProtocols.qml | 147 ++-- client/ui/qml/Pages2/PageSetupWizardStart.qml | 9 - .../ui/qml/Pages2/PageSetupWizardTextKey.qml | 12 - .../qml/Pages2/PageSetupWizardViewConfig.qml | 14 +- client/ui/qml/Pages2/PageShare.qml | 269 ++------ client/ui/qml/Pages2/PageShareFullAccess.qml | 22 +- client/ui/qml/Pages2/PageStart.qml | 73 +- client/ui/qml/main2.qml | 59 +- client/utils/qmlUtils.cpp | 128 ++++ client/utils/qmlUtils.h | 30 + 109 files changed, 4036 insertions(+), 3700 deletions(-) delete mode 100644 client/android/res/mipmap-anydpi-v26/ic_banner.xml create mode 100644 client/android/res/mipmap-hdpi/ic_banner.png create mode 100644 client/android/res/mipmap-mdpi/ic_banner.png delete mode 100644 client/android/res/mipmap-xhdpi/ic_banner_foreground.png delete mode 100644 client/android/res/values/ic_banner_background.xml create mode 100644 client/android/src/org/amnezia/vpn/TvFilePicker.kt create mode 100644 client/ui/controllers/focusController.cpp create mode 100644 client/ui/controllers/focusController.h create mode 100644 client/ui/controllers/listViewFocusController.cpp create mode 100644 client/ui/controllers/listViewFocusController.h create mode 100644 client/ui/qml/Components/ServersListView.qml create mode 100644 client/ui/qml/Controls2/ScrollBarType.qml create mode 100644 client/utils/qmlUtils.cpp create mode 100644 client/utils/qmlUtils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cb695631..98f3be14 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.2.4 +project(${PROJECT} VERSION 4.8.3.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 2071) +set(APP_ANDROID_VERSION_CODE 2072) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 05f9f17c..3ef92385 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -146,6 +146,7 @@ set(HEADERS ${HEADERS} ${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 @@ -197,6 +198,7 @@ set(SOURCES ${SOURCES} ${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 diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 4e25097d..aeed439b 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -404,6 +404,9 @@ void AmneziaApplication::initControllers() 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()); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 64566216..cfeac0d1 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -19,6 +19,7 @@ #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" @@ -124,6 +125,7 @@ private: #endif QScopedPointer m_connectionController; + QScopedPointer m_focusController; QScopedPointer m_pageController; QScopedPointer m_installController; QScopedPointer m_importController; diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 9e44e022..96f60f53 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -11,7 +11,7 @@ - + - +