Compare commits

..

43 Commits

Author SHA1 Message Date
vladimir.kuznetsov
7013a2f1d7 chore: update link to submodule 2025-05-12 19:34:23 +08:00
vladimir.kuznetsov
49d9b7d7e2 chore: update link to submodule 2025-04-23 13:47:17 +08:00
lunardunno
7169480999 feature: error handling for cgroup (#1486)
* Error for cgroup mountpoint

Added handling of message: cgroup mountpoint does not exist.

* Case for error cgroup

Added case and case description for: Cgroup Mountpoint Does Not Exist

* Case for Runc

Added error handling for Runc, which does not work in cgroup v2.
Changed numbering of new errors.

* stdErr handling fot run_container

Enabling stdErr handling fot run_container.sh

* change for stdErr handling

* Another place to handle the error 211

Another place to handle the error: ServerRuncNotWorkOnCgroupsV2

* test_1

* test 2

* test 3

* Moving error handling

Moving error handling to the right place in the controller.

* Polishing

* Еext correction

Сorrection of description text.
2025-04-23 12:12:23 +07:00
Mikhail Kiselev
c44ce0d77c fix: add missing include (#1541) 2025-04-19 23:21:10 +07:00
Nethius
7fd71a8408 feature: retrieving support info from gateway (#1483)
* feature: retrieving support info from gateway

* feature: added "external-premium" service-type

* chore: fixed external premium visability
2025-04-16 09:58:44 +07:00
DarthSidious007
68db721089 add S3 deploy (#1530) 2025-04-16 09:35:53 +07:00
MrMirDan
a180e12bdf chore: updated ru translation (#1531) 2025-04-12 22:04:34 +07:00
Yaroslav
f3a4a1b1be feat: improve post uninstall script for macos to properly remove application and its components (#1521) 2025-04-11 23:09:12 +07:00
Nethius
6977a8ecbc chore: bump version and update translation files (#1526) 2025-04-11 12:59:06 +07:00
Nethius
d00f64e6ad feature: added export logs button on start page (#1525) 2025-04-11 12:29:28 +07:00
Mykola Baibuz
d5b3da6ba3 Use older Ubuntu version for build job (#1523) 2025-04-11 08:57:56 +07:00
aiamnezia
c245318339 bugfix: empty split tunneling list (#1520)
* Disable split tunneling with empty list

* Fix bug with Amnezia DNS in split tunneling list

* update ubuntu version for linux deploy pipeline

* Fix deploy script
2025-04-10 14:24:33 +07:00
Nethius
b3b0fec2e1 feature: additional logs for proxy bypass (#1518) 2025-04-09 10:47:33 +07:00
Nethius
9d571a4c71 feature: new mirrors support (#1519) 2025-04-08 12:07:31 +07:00
pokamest
f283858490 Merge pull request #1517 from amnezia-vpn/chore/update-go-version
Update go version in actions to 1.24
2025-04-07 21:53:05 +01:00
pokamest
76fe203767 Update go version in actions to 1.24 2025-04-07 18:05:08 +01:00
pokamest
b9a47f2f50 Merge pull request #1516 from amnezia-vpn/feature/openvpn-warning
feature: warning when importing openvpn configurations
2025-04-07 17:59:37 +01:00
vladimir.kuznetsov
27cb17c640 chore: clear warning text before extract 2025-04-07 23:35:24 +08:00
vladimir.kuznetsov
ef8fb89eb3 feature: warning when importing openvpn configurations 2025-04-07 23:30:11 +08:00
Nethius
f1b045f8a8 fixed selecting the default button on PageSetupWizardEasy (#1502) 2025-03-30 12:53:26 +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
63 changed files with 8419 additions and 3397 deletions

View File

@@ -10,7 +10,7 @@ env:
jobs:
Build-Linux-Ubuntu:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.6.2
@@ -190,7 +190,7 @@ jobs:
- name: 'Install go'
uses: actions/setup-go@v5
with:
go-version: '1.22.1'
go-version: '1.24'
cache: false
- name: 'Setup gomobile'

View File

@@ -1,64 +1,41 @@
name: 'Upload a new version'
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
RELEASE_VERSION:
description: 'Release version (e.g. 1.2.3.4)'
required: true
type: string
jobs:
upload:
Upload-S3:
runs-on: ubuntu-latest
name: upload
steps:
- name: Checkout CMakeLists.txt
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
ref: ${{ inputs.RELEASE_VERSION }}
sparse-checkout: |
CMakeLists.txt
deploy/deploy_s3.sh
sparse-checkout-cone-mode: false
- name: Verify git tag
run: |
GIT_TAG=${{ github.ref_name }}
TAG_NAME=${{ inputs.RELEASE_VERSION }}
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
if [[ "$GIT_TAG" == "$CMAKE_TAG" ]]; then
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are the same. Continuing..."
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
else
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are not the same! Cancelling..."
echo "::error::Mismatch: Git tag ($TAG_NAME) != CMakeLists.txt version ($CMAKE_TAG). Exiting with error..."
exit 1
fi
- name: Download artifacts from the "${{ github.ref_name }}" tag
uses: robinraju/release-downloader@v1.8
- name: Setup Rclone
uses: AnimMouse/setup-rclone@v1
with:
tag: ${{ github.ref_name }}
fileName: "AmneziaVPN_(Linux_|)${{ github.ref_name }}*"
out-file-path: ${{ github.ref_name }}
rclone_config: ${{ secrets.RCLONE_CONFIG }}
- name: Upload beta version
uses: jakejarvis/s3-sync-action@master
if: contains(github.event.base_ref, 'dev')
with:
args: --include "AmneziaVPN*" --delete
env:
AWS_S3_BUCKET: updates
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: https://${{ vars.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com
SOURCE_DIR: ${{ github.ref_name }}
DEST_DIR: beta/${{ github.ref_name }}
- name: Upload stable version
uses: jakejarvis/s3-sync-action@master
if: contains(github.event.base_ref, 'master')
with:
args: --include "AmneziaVPN*" --delete
env:
AWS_S3_BUCKET: updates
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: https://${{ vars.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com
SOURCE_DIR: ${{ github.ref_name }}
DEST_DIR: stable/${{ github.ref_name }}
- name: Send dist to S3
run: bash deploy/deploy_s3.sh ${{ inputs.RELEASE_VERSION }}

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.8.4.1
project(${PROJECT} VERSION 4.8.6.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 2078)
set(APP_ANDROID_VERSION_CODE 2083)
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,13 +20,17 @@
#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;
@@ -63,9 +68,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 +103,45 @@ 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;
};
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;
};
errorCode = m_serverController->runContainerScript(credentials, container, script, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return connData;
}
errorCode = m_serverController->runContainerScript(credentials, container, getIpsScript, cbReadStdOut);
if (errorCode != ErrorCode::NoError) {
return connData;
}
auto ips = getIpsFromConf(stdOut);
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";
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 {
int next = ips.last().split(".").last().toInt() + 1;
if (next > 254) {
errorCode = ErrorCode::AddressPoolError;
return connData;
}
nextIpNumber = QString::number(next);
lastIp = ips.last();
}
}
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;
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;
}
l.removeLast();
l.append(nextIpNumber);
connData.clientIP = l.join(".");
}
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 +168,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 'wg syncconf wg0 <(wg-quick strip %1)'")
.arg(m_serverConfigPath);
errorCode = m_serverController->runScript(
credentials, m_serverController->replaceVars(script, m_serverController->genVarsForScript(credentials, container)));
credentials,
m_serverController->replaceVars(script, m_serverController->genVarsForScript(credentials, container)));
return connData;
}
@@ -173,8 +182,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 +217,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);

View File

@@ -10,7 +10,8 @@ namespace apiDefs
AmneziaFreeV3,
AmneziaPremiumV1,
AmneziaPremiumV2,
SelfHosted
SelfHosted,
ExternalPremium
};
enum ConfigSource {
@@ -43,6 +44,13 @@ namespace apiDefs
constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String issuedConfigs("issued_configs");
constexpr QLatin1String supportInfo("support_info");
constexpr QLatin1String email("email");
constexpr QLatin1String billingEmail("billing_email");
constexpr QLatin1String website("website");
constexpr QLatin1String websiteName("website_name");
constexpr QLatin1String telegram("telegram");
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs

View File

@@ -32,15 +32,17 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium");
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto stackType = apiConfigObject.value(apiDefs::key::stackType).toString();
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
if (serviceType == servicePremium || stackType == stackPremium) {
if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceFree || stackType == stackFree) {
} else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) {
return apiDefs::ConfigType::ExternalPremium;
}
}
default: {
@@ -66,6 +68,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::NoError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << reply->error();
return amnezia::ErrorCode::ApiConfigTimeoutError;
} else {
QString err = reply->errorString();
@@ -85,3 +88,10 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
qDebug() << "something went wrong";
return amnezia::ErrorCode::InternalError;
}
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::ExternalPremium };
return premiumTypes.contains(getConfigType(serverConfigObject));
}

View File

@@ -13,6 +13,8 @@ namespace apiUtils
bool isSubscriptionExpired(const QString &subscriptionEndDate);
bool isPremiumServer(const QJsonObject &serverConfigObject);
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);

View File

@@ -1,5 +1,6 @@
#include "coreController.h"
#include <QDirIterator>
#include <QTranslator>
#if defined(Q_OS_ANDROID)
@@ -93,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()
@@ -132,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));
@@ -234,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,62 +216,77 @@ 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;
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
qDebug() << "go to the next storage endpoint";
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,
const QByteArray &iv, const QByteArray &salt)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "Timeout occurred";
qDebug() << "timeout occurred";
qDebug() << reply->error();
return true;
} else if (responseBody.contains("html")) {
qDebug() << "The response contains an html tag";
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 {
qDebug() << reply->error();
return true;
}
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
qDebug() << reply->error();
return true;
} else if (checkEncryption) {
try {
QSimpleCrypto::QBlockCipher blockCipher;
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
} catch (...) {
qDebug() << "Failed to decrypt the data";
qDebug() << "failed to decrypt the data";
return true;
}
}
@@ -288,7 +307,7 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
QByteArray responseBody;
for (const QString &proxyUrl : proxyUrls) {
qDebug() << "Go to the next endpoint";
qDebug() << "go to the next proxy endpoint";
reply->deleteLater(); // delete the previous reply
reply = requestFunction(endpoint.arg(proxyUrl));
@@ -296,7 +315,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

@@ -439,15 +439,22 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
stdOut += data + "\n";
return ErrorCode::NoError;
};
auto cbReadStdErr = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
errorCode =
ErrorCode error =
runScript(credentials,
replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)),
cbReadStdOut);
if (errorCode)
return errorCode;
cbReadStdOut, cbReadStdErr);
if (stdOut.contains("doesn't work on cgroups v2"))
return ErrorCode::ServerDockerOnCgroupsV2;
if (stdOut.contains("cgroup mountpoint does not exist"))
return ErrorCode::ServerCgroupMountpoint;
return errorCode;
return error;
}
ErrorCode ServerController::runContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config)
@@ -709,7 +716,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 +764,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 +777,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;
}

View File

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

View File

@@ -20,8 +20,14 @@ 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;
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;

View File

@@ -6,6 +6,7 @@
#define INTERFACECONFIG_H
#include <QList>
#include <QMap>
#include <QString>
#include "ipaddress.h"

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

@@ -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,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 \

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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()
@@ -327,7 +310,7 @@ bool ApiConfigsController::deactivateDevice()
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
if (!apiUtils::isPremiumServer(serverConfigObject)) {
return true;
}
@@ -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::isPremiumServer(serverConfigObject)) {
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)
{
}
@@ -60,7 +62,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
QByteArray responseBody;
if (apiUtils::getConfigType(serverConfig) == apiDefs::ConfigType::AmneziaPremiumV2) {
if (apiUtils::isPremiumServer(serverConfig)) {
ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
@@ -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;
}
@@ -97,6 +94,8 @@ bool ImportController::extractConfigFromFile(const QString &fileName)
bool ImportController::extractConfigFromData(QString data)
{
m_maliciousWarningText.clear();
QString config = data;
QString prefix;
QString errormsg;
@@ -345,7 +344,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);
@@ -661,6 +660,7 @@ void ImportController::checkForMaliciousStrings(const QJsonObject &serverConfig)
if ((containerName == ContainerProps::containerToString(DockerContainer::OpenVpn))
|| (containerName == ContainerProps::containerToString(DockerContainer::Cloak))
|| (containerName == ContainerProps::containerToString(DockerContainer::ShadowSocks))) {
QString protocolConfig =
containerConfig[ProtocolProps::protoToString(Proto::OpenVpn)].toObject()[config_key::last_config].toString();
QString protocolConfigJson = QJsonDocument::fromJson(protocolConfig.toUtf8()).object()[config_key::config].toString();
@@ -682,8 +682,11 @@ void ImportController::checkForMaliciousStrings(const QJsonObject &serverConfig)
}
}
m_maliciousWarningText = tr("This configuration contains an OpenVPN setup. OpenVPN configurations can include malicious "
"scripts, so only add it if you fully trust the provider of this config. ");
if (maliciousStrings.size() >= dangerousTagsMaxCount) {
m_maliciousWarningText = tr("In the imported configuration, potentially dangerous lines were found:");
m_maliciousWarningText.push_back(tr("<br>In the imported configuration, potentially dangerous lines were found:"));
for (const auto &string : maliciousStrings) {
m_maliciousWarningText.push_back(QString("<br><i>%1</i>").arg(string));
}

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,15 +48,32 @@ 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.");
} else {
return "";
}
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2;
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium;
}
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;
}
}
@@ -80,6 +97,8 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
m_accountInfoData = accountInfoData;
m_supportInfo = accountInfoObject.value(apiDefs::key::supportInfo).toObject();
endResetModel();
}
@@ -108,12 +127,27 @@ QJsonArray ApiAccountInfoModel::getIssuedConfigsInfo()
QString ApiAccountInfoModel::getTelegramBotLink()
{
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
return tr("amnezia_free_support_bot");
} else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) {
return tr("amnezia_premium_support_bot");
}
return "";
return m_supportInfo.value(apiDefs::key::telegram).toString();
}
QString ApiAccountInfoModel::getEmailLink()
{
return m_supportInfo.value(apiDefs::key::email).toString();
}
QString ApiAccountInfoModel::getBillingEmailLink()
{
return m_supportInfo.value(apiDefs::key::billingEmail).toString();
}
QString ApiAccountInfoModel::getSiteLink()
{
return m_supportInfo.value(apiDefs::key::websiteName).toString();
}
QString ApiAccountInfoModel::getFullSiteLink()
{
return m_supportInfo.value(apiDefs::key::website).toString();
}
QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
@@ -124,6 +158,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);
@@ -32,7 +33,12 @@ public slots:
QJsonArray getAvailableCountries();
QJsonArray getIssuedConfigsInfo();
QString getTelegramBotLink();
QString getEmailLink();
QString getBillingEmailLink();
QString getSiteLink();
QString getFullSiteLink();
protected:
QHash<int, QByteArray> roleNames() const override;
@@ -50,6 +56,7 @@ private:
AccountInfoData m_accountInfoData;
QJsonArray m_availableCountries;
QJsonArray m_issuedConfigsInfo;
QJsonObject m_supportInfo;
};
#endif // APIACCOUNTINFOMODEL_H

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

@@ -1,13 +1,11 @@
#include "languageModel.h"
LanguageModel::LanguageModel(std::shared_ptr<Settings> settings, QObject *parent)
: m_settings(settings), QAbstractListModel(parent)
LanguageModel::LanguageModel(std::shared_ptr<Settings> settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent)
{
QMetaEnum metaEnum = QMetaEnum::fromType<LanguageSettings::AvailableLanguageEnum>();
for (int i = 0; i < metaEnum.keyCount(); i++) {
m_availableLanguages.push_back(
LanguageModelData {getLocalLanguageName(static_cast<LanguageSettings::AvailableLanguageEnum>(i)),
static_cast<LanguageSettings::AvailableLanguageEnum>(i) });
m_availableLanguages.push_back(LanguageModelData { getLocalLanguageName(static_cast<LanguageSettings::AvailableLanguageEnum>(i)),
static_cast<LanguageSettings::AvailableLanguageEnum>(i) });
}
}
@@ -50,8 +48,7 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan
case LanguageSettings::AvailableLanguageEnum::Burmese: strLanguage = "မြန်မာဘာသာ"; break;
case LanguageSettings::AvailableLanguageEnum::Urdu: strLanguage = "اُرْدُوْ"; break;
case LanguageSettings::AvailableLanguageEnum::Hindi: strLanguage = "हिन्दी"; break;
default:
break;
default: break;
}
return strLanguage;
@@ -104,11 +101,12 @@ QString LanguageModel::getCurrentLanguageName()
return m_availableLanguages[getCurrentLanguageIndex()].name;
}
QString LanguageModel::getCurrentSiteUrl()
QString LanguageModel::getCurrentSiteUrl(const QString &path)
{
auto language = static_cast<LanguageSettings::AvailableLanguageEnum>(getCurrentLanguageIndex());
switch (language) {
case LanguageSettings::AvailableLanguageEnum::Russian: return "https://storage.googleapis.com/amnezia/amnezia.org";
default: return "https://amnezia.org";
case LanguageSettings::AvailableLanguageEnum::Russian:
return "https://storage.googleapis.com/amnezia/amnezia.org" + (path.isEmpty() ? "" : (QString("?m-path=/%1").arg(path)));
default: return QString("https://amnezia.org") + (path.isEmpty() ? "" : (QString("/%1").arg(path)));
}
}

View File

@@ -59,7 +59,7 @@ public slots:
int getCurrentLanguageIndex();
int getLineHeightAppend();
QString getCurrentLanguageName();
QString getCurrentSiteUrl();
QString getCurrentSiteUrl(const QString &path = "");
signals:
void updateTranslations(const QLocale &locale);

View File

@@ -29,7 +29,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/premium")
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("premium"))
}
}
@@ -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

@@ -252,7 +252,7 @@ PageType {
text: qsTr("Privacy Policy")
clickedFunc: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/policy")
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("policy"))
}
}
}

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: ApiAccountInfoModel.getEmailLink()
readonly property string link: "mailto:" + ApiAccountInfoModel.getEmailLink()
}
QtObject {
id: paymentSupport
readonly property string title: qsTr("Email Billing & Orders")
readonly property string description: ApiAccountInfoModel.getBillingEmailLink()
readonly property string link: "mailto:" + ApiAccountInfoModel.getBillingEmailLink()
}
QtObject {
id: site
readonly property string title: qsTr("Website")
readonly property string description: ApiAccountInfoModel.getSiteLink()
readonly property string link: ApiAccountInfoModel.getFullSiteLink()
}
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"))
}
}
}

View File

@@ -3,6 +3,8 @@ import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import PageEnum 1.0
import Style 1.0
@@ -101,6 +103,34 @@ PageType {
}
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Export client logs")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
visible: PageController.isStartPageVisible()
clickedFunction: function() {
var fileName = ""
if (GC.isMobile()) {
fileName = "AmneziaVPN.log"
} else {
fileName = SystemController.getFileName(qsTr("Save"),
qsTr("Logs files (*.log)"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN",
true,
".log")
}
if (fileName !== "") {
PageController.showBusyIndicator(true)
SettingsController.exportLogsFile(fileName)
PageController.showBusyIndicator(false)
PageController.showNotificationMessage(qsTr("Logs file saved"))
}
}
}
LabelWithButtonType {
id: supportUuid
Layout.fillWidth: true

View File

@@ -175,7 +175,7 @@ PageType {
leftImageSource: "qrc:/images/controls/help-circle.svg"
onClicked: {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/starter-guide")
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("starter-guide"))
}
}
}

View File

@@ -78,7 +78,7 @@ PageType {
height: containers.contentItem.height
spacing: 16
currentIndex: 1
currentIndex: 0
clip: true
interactive: false
model: proxyContainersModel

View File

@@ -351,8 +351,10 @@ void VpnConnection::appendSplitTunnelingConfig()
sitesJsonArray.append(site);
}
// Allow traffic to Amnezia DNS
if (sitesRouteMode == Settings::VpnOnlyForwardSites) {
if (sitesJsonArray.isEmpty()) {
sitesRouteMode = Settings::RouteMode::VpnAllSites;
} else if (sitesRouteMode == Settings::VpnOnlyForwardSites) {
// Allow traffic to Amnezia DNS
sitesJsonArray.append(m_vpnConfiguration.value(config_key::dns1).toString());
sitesJsonArray.append(m_vpnConfiguration.value(config_key::dns2).toString());
}
@@ -371,6 +373,10 @@ void VpnConnection::appendSplitTunnelingConfig()
for (const auto &app : apps) {
appsJsonArray.append(app.appPath.isEmpty() ? app.packageName : app.appPath);
}
if (appsJsonArray.isEmpty()) {
appsRouteMode = Settings::AppsRouteMode::VpnAllApps;
}
}
m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode);

View File

@@ -2,13 +2,33 @@
APP_NAME=AmneziaVPN
PLIST_NAME=$APP_NAME.plist
LAUNCH_DAEMONS_PLIST_NAME=/Library/LaunchDaemons/$PLIST_NAME
LAUNCH_DAEMONS_PLIST_NAME="/Library/LaunchDaemons/$PLIST_NAME"
APP_PATH="/Applications/$APP_NAME.app"
USER_APP_SUPPORT="$HOME/Library/Application Support/$APP_NAME"
SYSTEM_APP_SUPPORT="/Library/Application Support/$APP_NAME"
LOG_FOLDER="/var/log/$APP_NAME"
CACHES_FOLDER="$HOME/Library/Caches/$APP_NAME"
if launchctl list "$APP_NAME-service" &> /dev/null; then
launchctl unload $LAUNCH_DAEMONS_PLIST_NAME
rm -f $LAUNCH_DAEMONS_PLIST_NAME
# Stop the running service if it exists
if pgrep -x "${APP_NAME}-service" > /dev/null; then
sudo killall -9 "${APP_NAME}-service"
fi
rm -rf "$HOME/Library/Application Support/$APP_NAME"
rm -rf /var/log/$APP_NAME
rm -rf /Applications/$APP_NAME.app/Contents
# Unload the service if loaded and remove its plist file regardless
if launchctl list "${APP_NAME}-service" &> /dev/null; then
sudo launchctl unload "$LAUNCH_DAEMONS_PLIST_NAME"
fi
sudo rm -f "$LAUNCH_DAEMONS_PLIST_NAME"
# Remove the entire application bundle
sudo rm -rf "$APP_PATH"
# Remove Application Support folders (user and system, if they exist)
rm -rf "$USER_APP_SUPPORT"
sudo rm -rf "$SYSTEM_APP_SUPPORT"
# Remove the log folder
sudo rm -rf "$LOG_FOLDER"
# Remove any caches left behind
rm -rf "$CACHES_FOLDER"

38
deploy/deploy_s3.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/sh
set -e
VERSION=$1
if [[ $VERSION = '' ]]; then
echo '::error::VERSION does not set. Exiting with error...'
exit 1
fi
mkdir -p dist
cd dist
echo $VERSION >> VERSION
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG
if [[ $(cat CHANGELOG) = null ]]; then
echo '::error::Release does not exists. Exiting with error...'
exit 1
fi
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_arm64-v8a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_armeabi-v7a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android8+_x86_64.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_arm64-v8a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_armeabi-v7a.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_android_7_x86_64.apk
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_linux.tar.zip
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos.dmg
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_macos_old.dmg
wget -q https://github.com/amnezia-vpn/amnezia-client/releases/download/${VERSION}/AmneziaVPN_${VERSION}_x64.exe
cd ../
rclone sync ./dist/ r2:/updates/