Compare commits

...

38 Commits

Author SHA1 Message Date
lunardunno
d61e2bc8f6 revert change: Linux versions of stdOut 2025-04-01 03:11:51 +04:00
lunardunno
9e1298550f Linux version to stdOut
Print the Linux version to stdOut for subsequent checking by the server controller.
2025-04-01 03:04:55 +04:00
vladimir.kuznetsov
805bc5fb61 Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD 2025-03-25 22:43:34 +07:00
Anton Sosnin
050066132b Fix iOS initial translation loading (#1477) 2025-03-24 14:35:22 +07:00
Nethius
2a6e6a1e24 chore: bump version (#1485) 2025-03-21 14:12:56 +07:00
Nethius
92689d084c feature/old api proxy (#1484)
* feature: proxy old api requests through gateway

* chore: bump version
2025-03-21 10:25:44 +07:00
lunardunno
00f314039d Patch for user checking. (#1481)
* Direct use of the $HOME variable.

* Sudo check witch variable $HOME.

Direct use of the $HOME variable.

* Changing for Error 208

Changing description and title for error 208

* Revert "Changing for Error 208"

This reverts commit f45624c023.

* Changing for Error 207

Changing description and title for Error 207
2025-03-20 10:24:37 +07:00
lunardunno
fcb75e837d chore: correcting version (#1480)
* Сorrecting version

Correction: return to the correct version

* Correction for SH
2025-03-19 21:51:49 +07:00
Yaroslav
9fbea76b74 There's a common issue of building iOS apps on Qt 6.8 because of new introduced ffmpeg dependency in multimedia Qt package (#1414)
ref: https://community.esri.com/t5/qt-maps-sdk-questions/build-failure-on-ios-with-qt-6-8/m-p/1548701#M5339
2025-03-14 20:40:27 +07:00
lunardunno
b3ff120bcf Checking server user permissions to use sudo (#1442)
* Username if whoami returns an error

Сommand to use home directory name if whoami returns error or is missing for prepare_host.sh.

* Update check_user_in_sudo.sh

Сommand to use home directory name if whoami returns error or is missing for check_user_in_sudo.sh.
Checking server user permissions to use sudo using a package manager or using uname.
Сhecking and redefining the system language.
Checking requirements for sudo users or root in script.

* Cases have been changed and added.

Changed description of the “Server User Not In Sudo” case.
Corrected the name and description of the "ServerPacketManagerError" case. Packet to Package.
Adding a "SudoPackageIsNotPreinstalled" case.
Adding a "ServerUserNotAllowedInSudoers" case.
Adding a "ServerUserPasswordRequired" case.

* Serves errors have been changed and added.

Corrected the name of the "ServerPacketManagerError" error to "ServerPackageManagerError".
Adding a "SudoPackageIsNotPreinstalled" error.
Adding a "ServerUserNotAllowedInSudoers" error.
Adding a "ServerUserPasswordRequired" error.

* Return ServerPacketManagerError

Return to the name "ServerPacketManagerError".

* Added errors handling 

Added new errors' handling to serverController.cpp.
Permission checks are also performed for the root user.

* Update translations

Updating translations for two existing server errors.

* Myanmar translation update

* Update for my_MM.ts

* checking for not allowed

Checking for "not allowed" in stdOut

* Removed "not allowed"

Removed check for "not allowed" in stdOut

* Removed nested launch

Removed nested launch via sudo

* Returned nested launch

Returned nested launch via sudo

* All checks with sudo

Both checks with sudo always run.

* Moved removing timestamp sudo

Removing the sudo timestamp is done every time.

* Checking the user directory

Checking the accessibility of the user's home directory

* Polishing

Изменение порядка обработки ошибок.

* changing detection order 

change the order of detection of inconsistencies:
1. sudo not preinstalled. (if user != root)
2. user not in sudo or wheel group. (if user != root)
3. user's directory is not accessible. (for all)
4. user not allowed in sudoers. (for all)
5. user password required. (for all)

* Packet to Package

* chore: bump version (#1463)

* fix for sh (#1462)

Fix for servers where sh is used as default shell.

* Username if whoami returns an error

Сommand to use home directory name if whoami returns error or is missing for prepare_host.sh.

* Update check_user_in_sudo.sh

Сommand to use home directory name if whoami returns error or is missing for check_user_in_sudo.sh.
Checking server user permissions to use sudo using a package manager or using uname.
Сhecking and redefining the system language.
Checking requirements for sudo users or root in script.

* Cases have been changed and added.

Changed description of the “Server User Not In Sudo” case.
Corrected the name and description of the "ServerPacketManagerError" case. Packet to Package.
Adding a "SudoPackageIsNotPreinstalled" case.
Adding a "ServerUserNotAllowedInSudoers" case.
Adding a "ServerUserPasswordRequired" case.

* Serves errors have been changed and added.

Corrected the name of the "ServerPacketManagerError" error to "ServerPackageManagerError".
Adding a "SudoPackageIsNotPreinstalled" error.
Adding a "ServerUserNotAllowedInSudoers" error.
Adding a "ServerUserPasswordRequired" error.

* Return ServerPacketManagerError

Return to the name "ServerPacketManagerError".

* Update translations

Updating translations for two existing server errors.

* Added errors handling 

Added new errors' handling to serverController.cpp.
Permission checks are also performed for the root user.

* Myanmar translation update

* Update for my_MM.ts

* checking for not allowed

Checking for "not allowed" in stdOut

* Removed "not allowed"

Removed check for "not allowed" in stdOut

* Removed nested launch

Removed nested launch via sudo

* Returned nested launch

Returned nested launch via sudo

* All checks with sudo

Both checks with sudo always run.

* Moved removing timestamp sudo

Removing the sudo timestamp is done every time.

* Checking the user directory

Checking the accessibility of the user's home directory

* Polishing

Изменение порядка обработки ошибок.

* changing detection order 

change the order of detection of inconsistencies:
1. sudo not preinstalled. (if user != root)
2. user not in sudo or wheel group. (if user != root)
3. user's directory is not accessible. (for all)
4. user not allowed in sudoers. (for all)
5. user password required. (for all)

* Undoing unintended changes

Undoing unintended changes.

* Undoing unintended change

Undoing unintended change.

* not allowed to use sudo

The user is not allowed to use sudo on this server.

* Capital letters in the error

Capital letters in the error description.

---------

Co-authored-by: albexk <albexk@proton.me>
2025-03-14 20:39:58 +07:00
paldeflex
9dea98f020 chore: README typo fixes (#1467) 2025-03-10 23:22:09 +07:00
Mykola Baibuz
c4701d4e7a Update XRay for Desktops (#1459)
version 25.3.6
2025-03-10 15:11:26 +07:00
Nethius
48903ca3a1 chore: fixed proxyStorageUrl typo (#1466) 2025-03-09 13:36:21 +07:00
Nethius
0c9fd4aef4 feature: added multiply proxy storage support (#1465) 2025-03-09 13:07:08 +07:00
lunardunno
b2af2e46ac fix for sh (#1462)
Fix for servers where sh is used as default shell.
2025-03-09 12:34:00 +07:00
albexk
efc76a0683 chore: bump version (#1463) 2025-03-09 10:30:43 +07:00
Nethius
c4a553c166 chore: error body processing (#1458) 2025-03-07 10:39:12 +07:00
Cyril Anisimov
69a00b0252 feature: remove the limit of ip addresses = 254 (#1438) 2025-03-06 21:43:47 +07:00
KsZnak
4257c08b43 Update amneziavpn_ru_RU.ts (#1457) 2025-03-06 21:38:42 +07:00
Mykola Baibuz
c9e5b92f79 Remove unneeded flushDns (#1443) 2025-03-05 13:21:39 +07:00
Mykola Baibuz
99818c2ad8 Fixes for native OpenVPN config import (#1444)
* Remote address in OpenVPN config can be host name

* Protocol parameter in OpenVPN config is not mandatory
2025-03-05 13:20:46 +07:00
shiroow
99e3afabad chore: update eng text (#1456)
chore: update eng text
2025-03-05 10:11:31 +07:00
Yaroslav
d3339a7f3a fix: iOS/iPadOS crashes on a start of the app because of there's no keyFrame set (#1448)
So setting one if it's not set.
2025-03-04 18:13:04 +07:00
Nethius
678bfffe49 chore: minor ui fixes (#1446)
* chore: minor ui fixes

* chore: update ru translation file

* bugfix: fixed config update by ttl for gateway configs

* bugfix: fixed proxy bypassing

* chore: minor ui fixes

* chore: update ru translation file

* chore: bump version
2025-03-04 13:33:35 +07:00
Nethius
728b48044c Merge pull request #1440 from amnezia-vpn/feature/subscription-settings-page
feature/subscription settings page
2025-02-28 22:17:43 +07:00
Nethius
7ccbfa48bc bugfix: fixed mobile controllers initialization (#1436)
* bugfix: fixed mobile controllers initialization

* chore: bump version
2025-02-25 22:29:58 +07:00
vladimir.kuznetsov
3241782098 Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD 2024-05-20 10:49:42 +02:00
pokamest
b19009b669 Merge branch 'dev' into update_server_scripts 2024-03-18 21:43:55 +00:00
pokamest
26218b22ee Merge branch 'dev' into update_server_scripts 2024-03-03 15:11:14 +00:00
pokamest
3eeeb5094e Merge pull request #628 from amnezia-vpn/feature/new-awg-docker-container
Feature/new awg docker container
2024-03-03 07:09:56 -08:00
vladimir.kuznetsov
f309a358c3 Merge branch 'update_server_scripts' of github.com:amnezia-vpn/amnezia-client into feature/new-awg-docker-container 2024-02-24 14:41:08 +05:00
vladimir.kuznetsov
be0ec37738 Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD 2024-02-24 14:40:14 +05:00
vladimir.kuznetsov
179c6093ce Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into feature/new-awg-docker-container 2024-02-24 14:37:26 +05:00
vladimir.kuznetsov
10933ce466 added backward compatibility for the old awg container 2024-02-24 14:34:47 +05:00
vladimir.kuznetsov
cd9cdd24ec Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into feature/new-awg-docker-container 2024-02-22 15:49:41 +05:00
vladimir.kuznetsov
144ed3c988 updated the paths to awg files and interfaces to match the new docker container 2024-02-21 19:06:16 +05:00
vladimir.kuznetsov
e046b6df04 Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into update_server_scripts 2024-02-21 15:00:54 +05:00
root
548959752c rename interface and config file name to awg0
change base docker image to amneziavpn/amneziawg-go:latest
2024-02-15 11:07:09 +07:00
55 changed files with 1819 additions and 717 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#include "coreController.h"
#include <QDirIterator>
#include <QTranslator>
#if defined(Q_OS_ANDROID)
@@ -20,6 +21,9 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initControllers();
initSignalHandlers();
initAndroidController();
initAppleController();
initNotificationHandler();
auto locale = m_settings->getAppLanguage();
@@ -90,6 +94,9 @@ void CoreController::initModels()
m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this));
m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get());
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
}
void CoreController::initControllers()
@@ -129,7 +136,8 @@ void CoreController::initControllers()
m_systemController.reset(new SystemController(m_settings));
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_settings));
m_apiSettingsController.reset(
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
@@ -231,7 +239,23 @@ void CoreController::updateTranslator(const QLocale &locale)
QCoreApplication::removeTranslator(m_translator.get());
}
QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm";
QStringList availableTranslations;
QDirIterator it(":/translations", QStringList("amneziavpn_*.qm"), QDir::Files);
while (it.hasNext()) {
availableTranslations << it.next();
}
// This code allow to load translation for the language only, without country code
const QString lang = locale.name().split("_").first();
const QString translationFilePrefix = QString(":/translations/amneziavpn_") + lang;
QString strFileName = QString(":/translations/amneziavpn_%1.qm").arg(locale.name());
for (const QString &translation : availableTranslations) {
if (translation.contains(translationFilePrefix)) {
strFileName = translation;
break;
}
}
if (m_translator->load(strFileName)) {
if (QCoreApplication::installTranslator(m_translator.get())) {
m_settings->setAppLanguage(locale);

View File

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

View File

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

View File

@@ -709,7 +709,7 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
QString transportProto = containerConfig.value(config_key::transport_proto).toString(defaultTransportProto);
// TODO reimplement with netstat
QString script = QString("which lsof &>/dev/null || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port);
QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port);
for (auto &port : fixedPorts) {
script = script.append("|:%1").arg(port);
}
@@ -757,10 +757,6 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, DockerContainer container)
{
if (credentials.userName == "root") {
return ErrorCode::NoError;
}
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
@@ -774,8 +770,16 @@ ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, D
const QString scriptData = amnezia::scriptData(SharedScriptType::check_user_in_sudo);
ErrorCode error = runScript(credentials, replaceVars(scriptData, genVarsForScript(credentials)), cbReadStdOut, cbReadStdErr);
if (!stdOut.contains("sudo"))
if (credentials.userName != "root" && stdOut.contains("sudo:") && !stdOut.contains("uname:") && stdOut.contains("not found"))
return ErrorCode::ServerSudoPackageIsNotPreinstalled;
if (credentials.userName != "root" && !stdOut.contains("sudo") && !stdOut.contains("wheel"))
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required"))
return ErrorCode::ServerUserPasswordRequired;
return error;
}
@@ -839,3 +843,24 @@ ErrorCode ServerController::getDecryptedPrivateKey(const ServerCredentials &cred
auto error = m_sshClient.getDecryptedPrivateKey(credentials, decryptedPrivateKey, callback);
return error;
}
bool ServerController::isNewAwgContainer(const ServerCredentials &credentials)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
QString script = QString("sudo docker exec -i $CONTAINER_NAME bash -c 'type awg'");
runScript(credentials, replaceVars(script, genVarsForScript(credentials, DockerContainer::Awg)), cbReadStdOut, cbReadStdErr);
return stdOut.contains("/usr/bin/awg");
}

View File

@@ -57,6 +57,8 @@ public:
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey,
const std::function<QString()> &callback);
bool isNewAwgContainer(const ServerCredentials &credentials);
private:
ErrorCode installDockerWorker(const ServerCredentials &credentials, DockerContainer container);
ErrorCode prepareHostWorker(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config = QJsonObject());

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 522 B

View File

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

View File

@@ -186,6 +186,9 @@ namespace amnezia
constexpr char serverPublicKeyPath[] = "/opt/amnezia/wireguard/wireguard_server_public_key.key";
constexpr char serverPskKeyPath[] = "/opt/amnezia/wireguard/wireguard_psk.key";
constexpr char interfaceName[] = "wg0";
constexpr char wgBinaryName[] = "wg";
constexpr char wgQuickBinaryName[] = "wg-quick";
}
namespace sftp
@@ -203,7 +206,7 @@ namespace amnezia
constexpr char defaultMtu[] = "1376";
#endif
constexpr char serverConfigPath[] = "/opt/amnezia/awg/wg0.conf";
constexpr char serverConfigPath[] = "/opt/amnezia/awg/awg0.conf";
constexpr char serverPublicKeyPath[] = "/opt/amnezia/awg/wireguard_server_public_key.key";
constexpr char serverPskKeyPath[] = "/opt/amnezia/awg/wireguard_psk.key";
@@ -216,6 +219,10 @@ namespace amnezia
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
constexpr char interfaceName[] = "awg0";
constexpr char wgBinaryName[] = "awg";
constexpr char wgQuickBinaryName[] = "awg-quick";
}
namespace socks5Proxy

View File

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

View File

@@ -1,4 +1,4 @@
FROM amneziavpn/amnezia-wg:latest
FROM amneziavpn/amneziawg-go:latest
LABEL maintainer="AmneziaVPN"

View File

@@ -1,15 +1,15 @@
mkdir -p /opt/amnezia/awg
cd /opt/amnezia/awg
WIREGUARD_SERVER_PRIVATE_KEY=$(wg genkey)
WIREGUARD_SERVER_PRIVATE_KEY=$(awg genkey)
echo $WIREGUARD_SERVER_PRIVATE_KEY > /opt/amnezia/awg/wireguard_server_private_key.key
WIREGUARD_SERVER_PUBLIC_KEY=$(echo $WIREGUARD_SERVER_PRIVATE_KEY | wg pubkey)
WIREGUARD_SERVER_PUBLIC_KEY=$(echo $WIREGUARD_SERVER_PRIVATE_KEY | awg pubkey)
echo $WIREGUARD_SERVER_PUBLIC_KEY > /opt/amnezia/awg/wireguard_server_public_key.key
WIREGUARD_PSK=$(wg genpsk)
WIREGUARD_PSK=$(awg genpsk)
echo $WIREGUARD_PSK > /opt/amnezia/awg/wireguard_psk.key
cat > /opt/amnezia/awg/wg0.conf <<EOF
cat > /opt/amnezia/awg/awg0.conf <<EOF
[Interface]
PrivateKey = $WIREGUARD_SERVER_PRIVATE_KEY
Address = $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR

View File

@@ -6,19 +6,19 @@ echo "Container startup"
#ifconfig eth0:0 $SERVER_IP_ADDRESS netmask 255.255.255.255 up
# kill daemons in case of restart
wg-quick down /opt/amnezia/awg/wg0.conf
awg-quick down /opt/amnezia/awg/awg0.conf
# start daemons if configured
if [ -f /opt/amnezia/awg/wg0.conf ]; then (wg-quick up /opt/amnezia/awg/wg0.conf); fi
if [ -f /opt/amnezia/awg/awg0.conf ]; then (awg-quick up /opt/amnezia/awg/awg0.conf); fi
# Allow traffic on the TUN interface.
iptables -A INPUT -i wg0 -j ACCEPT
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A OUTPUT -o wg0 -j ACCEPT
iptables -A INPUT -i awg0 -j ACCEPT
iptables -A FORWARD -i awg0 -j ACCEPT
iptables -A OUTPUT -o awg0 -j ACCEPT
# Allow forwarding traffic only from the VPN.
iptables -A FORWARD -i wg0 -o eth0 -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -i wg0 -o eth1 -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -i awg0 -o eth0 -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -i awg0 -o eth1 -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#include "apiConfigsController.h"
#include <QEventLoop>
#include <QClipboard>
#include <QEventLoop>
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
@@ -19,7 +19,7 @@ namespace
constexpr char cloak[] = "cloak";
constexpr char awg[] = "awg";
constexpr char apiEdnpoint[] = "api_endpoint";
constexpr char apiEndpoint[] = "api_endpoint";
constexpr char accessToken[] = "api_key";
constexpr char certificate[] = "certificate";
constexpr char publicKey[] = "public_key";
@@ -269,54 +269,37 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
{
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkRequest request;
request.setTransferTimeout(apiDefs::requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint);
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs);
QString protocol = serverConfig.value(configKey::protocol).toString();
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
ApiPayloadData apiPayloadData = generateApiPayloadData(protocol);
QString serviceProtocol = serverConfig.value(configKey::protocol).toString();
ApiPayloadData apiPayloadData = generateApiPayloadData(serviceProtocol);
QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::uuid] = installationUuid;
QJsonObject apiPayload = fillApiPayload(serviceProtocol, apiPayloadData);
apiPayload[configKey::uuid] = installationUuid;
apiPayload[configKey::accessToken] = serverConfig.value(configKey::accessToken).toString();
apiPayload[configKey::apiEndpoint] = serverConfig.value(configKey::apiEndpoint).toString();
QByteArray requestBody = QJsonDocument(apiPayload).toJson();
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody);
QNetworkReply *reply = amnApp->networkManager()->post(request, requestBody);
if (errorCode == ErrorCode::NoError) {
fillServerConfig(serviceProtocol, apiPayloadData, responseBody, serverConfig);
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
if (errorCode != ErrorCode::NoError) {
reply->deleteLater();
emit errorOccurred(errorCode);
return false;
}
auto apiResponseBody = reply->readAll();
reply->deleteLater();
fillServerConfig(protocol, apiPayloadData, apiResponseBody, serverConfig);
m_serversModel->editServer(serverConfig, serverIndex);
emit updateServerFromApiFinished();
return true;
} else {
emit errorOccurred(errorCode);
return false;
}
return true;
}
bool ApiConfigsController::deactivateDevice()
@@ -354,6 +337,43 @@ bool ApiConfigsController::deactivateDevice()
return true;
}
bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs);
auto serverIndex = m_serversModel->getProcessedServerIndex();
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
return true;
}
QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString();
ApiPayloadData apiPayloadData = generateApiPayloadData(protocol);
QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode);
apiPayload[configKey::serverCountryCode] = serverCountryCode;
apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType);
apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData);
apiPayload[configKey::uuid] = uuid;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode);
return false;
}
if (uuid == m_settings->getInstallationUuid(true)) {
serverConfigObject.remove(config_key::containers);
m_serversModel->editServer(serverConfigObject, serverIndex);
}
return true;
}
bool ApiConfigsController::isConfigValid()
{
int serverIndex = m_serversModel->getDefaultServerIndex();
@@ -369,7 +389,7 @@ bool ApiConfigsController::isConfigValid()
return updateServiceFromGateway(serverIndex, "", "");
} else if (configSource && m_serversModel->isApiKeyExpired(serverIndex)) {
qDebug() << "attempt to update api config by expires_at event";
if (configSource == apiDefs::ConfigSource::Telegram) {
if (configSource == apiDefs::ConfigSource::AmneziaGateway) {
return updateServiceFromGateway(serverIndex, "", "");
} else {
m_serversModel->removeApiConfig(serverIndex);

View File

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

View File

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

View File

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

View File

@@ -27,8 +27,6 @@ namespace
ConfigTypes checkConfigFormat(const QString &config)
{
const QString openVpnConfigPatternCli = "client";
const QString openVpnConfigPatternProto1 = "proto tcp";
const QString openVpnConfigPatternProto2 = "proto udp";
const QString openVpnConfigPatternDriver1 = "dev tun";
const QString openVpnConfigPatternDriver2 = "dev tap";
@@ -53,14 +51,13 @@ namespace
|| (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName)
&& config.contains(amneziaConfigPatternPassword))) {
return ConfigTypes::Amnezia;
} else if (config.contains(openVpnConfigPatternCli)
&& (config.contains(openVpnConfigPatternProto1) || config.contains(openVpnConfigPatternProto2))
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
return ConfigTypes::OpenVpn;
} else if (config.contains(wireguardConfigPatternSectionInterface) && config.contains(wireguardConfigPatternSectionPeer)) {
return ConfigTypes::WireGuard;
} else if ((config.contains(xrayConfigPatternInbound)) && (config.contains(xrayConfigPatternOutbound))) {
return ConfigTypes::Xray;
} else if (config.contains(openVpnConfigPatternCli)
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
return ConfigTypes::OpenVpn;
}
return ConfigTypes::Invalid;
}
@@ -345,7 +342,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data)
arr.push_back(containers);
QString hostName;
const static QRegularExpression hostNameRegExp("remote (.*) [0-9]*");
const static QRegularExpression hostNameRegExp("remote\\s+([^\\s]+)");
QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data);
if (hostNameMatch.hasMatch()) {
hostName = hostNameMatch.captured(1);

View File

@@ -370,8 +370,17 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
containerConfig.insert(config_key::transport_proto, transportProto);
if (protocol == Proto::Awg) {
QString serverConfigPath;
if (container == DockerContainer::Awg) {
if (serverController->isNewAwgContainer(credentials)) {
serverConfigPath = amnezia::protocols::awg::serverConfigPath;
} else {
serverConfigPath = "/opt/amnezia/awg/wg0.conf";
}
}
QString serverConfig = serverController->getTextFileFromContainer(container, credentials,
protocols::awg::serverConfigPath, errorCode);
serverConfigPath, errorCode);
QMap<QString, QString> serverConfigMap;
auto serverConfigLines = serverConfig.split("\n");

View File

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

View File

@@ -44,7 +44,6 @@ void SitesController::addSite(QString hostname)
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
}
QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection);
};
const auto &resolveCallback = [this, processSite](const QHostInfo &hostInfo) {
@@ -75,7 +74,6 @@ void SitesController::removeSite(int index)
QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection);
emit finished(tr("Site removed: %1").arg(hostname));
}
@@ -124,7 +122,6 @@ void SitesController::importSites(const QString &fileName, bool replaceExisting)
m_sitesModel->addSites(sites, replaceExisting);
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, ips));
QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection);
emit finished(tr("Import completed"));
}

View File

@@ -48,8 +48,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
case ServiceDescriptionRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) {
return tr("Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to 200 "
"Mb/s");
return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. "
"Speeds up to 200 Mbps");
} else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and "
"more. YouTube is not included in the free plan.");
@@ -58,6 +58,19 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2;
}
case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
QJsonObject issuedConfigObject = m_issuedConfigsInfo.at(i).toObject();
auto lastDownloaded = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::lastDownloaded).toString());
auto workerLastUpdated = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString());
if (lastDownloaded < workerLastUpdated) {
return true;
}
}
return false;
}
}
return QVariant();
@@ -124,6 +137,7 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[ConnectedDevicesRole] = "connectedDevices";
roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsComponentVisibleRole] = "isComponentVisible";
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
return roles;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
#include "apiDevicesModel.h"
#include <QJsonObject>
#include "core/api/apiDefs.h"
#include "logger.h"
namespace
{
Logger logger("ApiDevicesModel");
constexpr QLatin1String gatewayAccount("gateway_account");
}
ApiDevicesModel::ApiDevicesModel(std::shared_ptr<Settings> settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent)
{
}
int ApiDevicesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_issuedConfigs.size();
}
QVariant ApiDevicesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
return QVariant();
IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.at(index.row());
switch (role) {
case OsVersionRole: {
return issuedConfigInfo.osVersion;
}
case SupportTagRole: {
return issuedConfigInfo.installationUuid;
}
case CountryCodeRole: {
return issuedConfigInfo.countryCode;
}
case LastUpdateRole: {
return QDateTime::fromString(issuedConfigInfo.lastDownloaded, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
}
case IsCurrentDeviceRole: {
return issuedConfigInfo.installationUuid == m_settings->getInstallationUuid(false);
}
}
return QVariant();
}
void ApiDevicesModel::updateModel(const QJsonArray &issuedConfigs)
{
beginResetModel();
m_issuedConfigs.clear();
for (int i = 0; i < issuedConfigs.size(); i++) {
IssuedConfigInfo issuedConfigInfo;
QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject();
if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != gatewayAccount) {
continue;
}
issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString();
issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString();
issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString();
issuedConfigInfo.sourceType = issuedConfigObject.value(apiDefs::key::sourceType).toString();
issuedConfigInfo.osVersion = issuedConfigObject.value(apiDefs::key::osVersion).toString();
issuedConfigInfo.countryName = issuedConfigObject.value(apiDefs::key::serverCountryName).toString();
issuedConfigInfo.countryCode = issuedConfigObject.value(apiDefs::key::serverCountryCode).toString();
m_issuedConfigs.push_back(issuedConfigInfo);
}
endResetModel();
}
QHash<int, QByteArray> ApiDevicesModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[OsVersionRole] = "osVersion";
roles[SupportTagRole] = "supportTag";
roles[CountryCodeRole] = "countryCode";
roles[LastUpdateRole] = "lastUpdate";
roles[IsCurrentDeviceRole] = "isCurrentDevice";
return roles;
}

View File

@@ -0,0 +1,52 @@
#ifndef APIDEVICESMODEL_H
#define APIDEVICESMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QVector>
#include "settings.h"
class ApiDevicesModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
OsVersionRole = Qt::UserRole + 1,
SupportTagRole,
CountryCodeRole,
LastUpdateRole,
IsCurrentDeviceRole
};
explicit ApiDevicesModel(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(const QJsonArray &issuedConfigs);
protected:
QHash<int, QByteArray> roleNames() const override;
private:
struct IssuedConfigInfo
{
QString installationUuid;
QString workerLastUpdated;
QString lastDownloaded;
QString sourceType;
QString osVersion;
QString countryName;
QString countryCode;
};
QVector<IssuedConfigInfo> m_issuedConfigs;
std::shared_ptr<Settings> m_settings;
};
#endif // APIDEVICESMODEL_H

View File

@@ -65,8 +65,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) {
return tr("Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. "
"Works for any sites with no restrictions. Speed up to %1 MBit/s. Unlimited traffic.")
return tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos. "
"Access all websites and online resources. Speeds up to %1 Mbps.")
.arg(speed);
} else if (serviceType == serviceType::amneziaFree) {
QString description = tr("AmneziaFree provides free unlimited access to a basic set of web sites, such as Facebook, Instagram, Twitter (X), Discord, Telegram, and others. YouTube is not included in the free plan.");
@@ -79,8 +79,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case ServiceDescriptionRole: {
if (serviceType == serviceType::amneziaPremium) {
return tr("Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. "
"Works for any sites with no restrictions.");
return tr("Amnezia Premium is classic VPN for for seamless work, downloading large files, and watching videos. "
"Access all websites and online resources.");
} else {
return tr("AmneziaFree provides free unlimited access to a basic set of web sites, such as Facebook, Instagram, Twitter (X), Discord, Telegram, and others. YouTube is not included in the free plan.");
}

View File

@@ -209,7 +209,8 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta
{
ErrorCode error = ErrorCode::NoError;
const QString wireGuardConfigFile = QString("opt/amnezia/%1/wg0.conf").arg(container == DockerContainer::WireGuard ? "wireguard" : "awg");
const QString wireGuardConfigFile =
DockerContainer::WireGuard ? amnezia::protocols::wireguard::serverConfigPath : amnezia::protocols::awg::serverConfigPath;
const QString wireguardConfigString = serverController->getTextFileFromContainer(container, credentials, wireGuardConfigFile, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the wg conf file from the server";
@@ -736,8 +737,17 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont
{
ErrorCode error = ErrorCode::NoError;
const QString wireGuardConfigFile =
QString("/opt/amnezia/%1/wg0.conf").arg(container == DockerContainer::WireGuard ? "wireguard" : "awg");
QString wireGuardConfigFile;
if (container == DockerContainer::Awg) {
if (serverController->isNewAwgContainer(credentials)) {
wireGuardConfigFile = amnezia::protocols::awg::serverConfigPath;
} else {
wireGuardConfigFile = "/opt/amnezia/awg/wg0.conf";
}
} else {
wireGuardConfigFile = amnezia::protocols::wireguard::serverConfigPath;
}
const QString wireguardConfigString = serverController->getTextFileFromContainer(container, credentials, wireGuardConfigFile, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the wg conf file from the server";
@@ -780,7 +790,11 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont
return error;
}
const QString script = "sudo docker exec -i $CONTAINER_NAME bash -c 'wg syncconf wg0 <(wg-quick strip %1)'";
QString interfaceName = DockerContainer::WireGuard ? protocols::wireguard::interfaceName : protocols::awg::interfaceName;
QString wgBinaryName = DockerContainer::WireGuard ? protocols::wireguard::wgBinaryName : protocols::awg::wgBinaryName;
QString wgQuickBinaryName = DockerContainer::WireGuard ? protocols::wireguard::wgQuickBinaryName : protocols::awg::wgQuickBinaryName;
QString script = QString("sudo docker exec -i $CONTAINER_NAME bash -c '%4 syncconf %2 <(%3 strip %1)'")
.arg(wireGuardConfigFile, interfaceName, wgQuickBinaryName, wgBinaryName);
error = serverController->runScript(
credentials,
serverController->replaceVars(script.arg(wireGuardConfigFile), serverController->genVarsForScript(credentials, container)));

View File

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

View File

@@ -27,5 +27,6 @@ QtObject {
readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8)
readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
readonly property color pearlGray: '#EAEAEC'
readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26)
}
}

View File

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

View File

@@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20
anchors.bottomMargin: 24
model: ApiDevicesModel
header: ColumnLayout {
width: listView.width
BackButtonType {
id: backButton
}
HeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Active Devices")
descriptionText: qsTr("Manage currently connected devices")
}
WarningType {
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.fillWidth: true
textString: qsTr("You can find the identifier on the Support tab or, for older versions of the app, "
+ "by tapping '+' and then the three dots at the top of the page.")
iconPath: "qrc:/images/controls/alert-circle.svg"
}
}
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 6
text: osVersion + (isCurrentDevice ? qsTr(" (current device)") : "")
descriptionText: qsTr("Support tag: ") + "\n" + supportTag + "\n" + qsTr("Last updated: ") + lastUpdate
rightImageSource: "qrc:/images/controls/trash.svg"
clickedFunction: function() {
if (isCurrentDevice && ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
return
}
var headerText = qsTr("Are you sure you want to unlink this device?")
var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
Qt.callLater(deactivateExternalDevice, supportTag, countryCode)
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
DividerType {}
}
}
function deactivateExternalDevice(supportTag, countryCode) {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateExternalDevice(supportTag, countryCode)) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
}
}

View File

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

View File

@@ -45,8 +45,8 @@ PageType {
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Configuration files")
descriptionText: qsTr("To connect a router or AmneziaWG application")
headerText: qsTr("Configuration Files")
descriptionText: qsTr("For router setup or the AmneziaWG app")
}
}
@@ -58,6 +58,9 @@ PageType {
Layout.topMargin: 6
text: countryName
descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : ""
descriptionColor: AmneziaStyle.color.vibrantRed
leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg"
@@ -120,13 +123,13 @@ PageType {
Layout.fillWidth: true
Layout.margins: 16
headerText: qsTr("Configuration file ") + moreOptionsDrawer.countryName
headerText: moreOptionsDrawer.countryName + qsTr(" configuration file")
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Create a new")
text: qsTr("Generate a new configuration file")
descriptionText: qsTr("The previously created one will stop working")
clickedFunction: function() {
@@ -190,9 +193,15 @@ PageType {
}
function showQuestion(isConfigIssue, countryCode, countryName) {
var headerText = qsTr("Revoke the actual %1 configuration file?").arg(countryName)
var descriptionText = qsTr("The previously created file will no longer be valid. It will not be possible to connect using it.")
var yesButtonText = qsTr("Continue")
var headerText
if (isConfigIssue) {
headerText = qsTr("Generate a new %1 configuration file?").arg(countryName)
} else {
headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName)
}
var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it")
var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {

View File

@@ -26,15 +26,15 @@ PageType {
QtObject {
id: statusObject
readonly property string title: qsTr("Subscription status")
readonly property string title: qsTr("Subscription Status")
readonly property string contentKey: "subscriptionStatus"
readonly property string objectImageSource: "qrc:/images/controls/map-pin.svg"
readonly property string objectImageSource: "qrc:/images/controls/info.svg"
}
QtObject {
id: endDateObject
readonly property string title: qsTr("Valid until")
readonly property string title: qsTr("Valid Until")
readonly property string contentKey: "endDate"
readonly property string objectImageSource: "qrc:/images/controls/history.svg"
}
@@ -42,9 +42,9 @@ PageType {
QtObject {
id: deviceCountObject
readonly property string title: qsTr("Connected devices")
readonly property string title: qsTr("Active Connections")
readonly property string contentKey: "connectedDevices"
readonly property string objectImageSource: "qrc:/images/controls/gauge.svg"
readonly property string objectImageSource: "qrc:/images/controls/monitor.svg"
}
property var processedServer
@@ -158,15 +158,32 @@ PageType {
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
WarningType {
id: warning
Layout.topMargin: 32
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.fillWidth: true
backGroundColor: AmneziaStyle.color.translucentRichBrown
textString: qsTr("Configurations have been updated for some countries. Download and install the updated configuration files")
iconPath: "qrc:/images/controls/alert-circle.svg"
visible: ApiAccountInfoModel.data("hasExpiredWorker")
}
LabelWithButtonType {
id: vpnKey
Layout.fillWidth: true
Layout.topMargin: 32
Layout.topMargin: warning.visible ? 16 : 32
visible: false //footer.isVisibleForAmneziaFree
text: qsTr("Subscription key")
text: qsTr("Subscription Key")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
@@ -174,7 +191,7 @@ PageType {
shareConnectionDrawer.openTriggered()
shareConnectionDrawer.isSelfHostedConfig = false;
shareConnectionDrawer.shareButtonText = qsTr("Save VPN key to file")
shareConnectionDrawer.shareButtonText = qsTr("Save VPN key as a file")
shareConnectionDrawer.copyButtonText = qsTr("Copy VPN key")
@@ -192,13 +209,13 @@ PageType {
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 32
Layout.topMargin: warning.visible ? 16 : 32
visible: footer.isVisibleForAmneziaFree
text: qsTr("Configuration files")
text: qsTr("Configuration Files")
descriptionText: qsTr("To connect a router or AmneziaWG application")
descriptionText: qsTr("Manage configuration files")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
@@ -211,6 +228,26 @@ PageType {
visible: footer.isVisibleForAmneziaFree
}
LabelWithButtonType {
Layout.fillWidth: true
visible: footer.isVisibleForAmneziaFree
text: qsTr("Active Devices")
descriptionText: qsTr("Manage currently connected devices")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
ApiSettingsController.updateApiDevicesModel()
PageController.goToPage(PageEnum.PageSettingsApiDevices)
}
}
DividerType {
visible: footer.isVisibleForAmneziaFree
}
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32
@@ -228,6 +265,8 @@ PageType {
LabelWithButtonType {
Layout.fillWidth: true
visible: footer.isVisibleForAmneziaFree
text: qsTr("How to connect on another device")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
@@ -236,7 +275,9 @@ PageType {
}
}
DividerType {}
DividerType {
visible: footer.isVisibleForAmneziaFree
}
BasicButtonType {
id: resetButton
@@ -288,16 +329,17 @@ PageType {
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Deactivate the subscription on this device")
text: qsTr("Unlink this device")
clickedFunc: function() {
var headerText = qsTr("Deactivate the subscription on this device?")
var headerText = qsTr("Are you sure you want to unlink this device?")
var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("The next time the “Connect” button is pressed, the device will be activated again"))
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
} else {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) {
@@ -309,7 +351,7 @@ PageType {
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}

View File

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

View File

@@ -140,7 +140,7 @@ PageType {
}
onClicked: {
if (!checkable) {
PageController.showNotificationMessage(qsTr("Cannot change killSwitch settings during active connection"))
PageController.showNotificationMessage(qsTr("Cannot change KillSwitch settings during active connection"))
}
}
}