diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..5c459fd2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,39 @@ +BasedOnStyle: WebKit +AccessModifierOffset: '-4' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortEnumsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: None +AlwaysBreakTemplateDeclarations: 'No' +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakConstructorInitializers: BeforeColon +ColumnLimit: '120' +CommentPragmas: '"^!|^:"' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '8' +IndentPPDirectives: BeforeHash +NamespaceIndentation: All +PenaltyExcessCharacter: '10' +PointerAlignment: Right +SortIncludes: 'true' +SpaceAfterTemplateKeyword: 'false' +Standard: Auto diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 00000000..4019357f --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,20 @@ +/client/3rd +/client/3rd-prebuild +/client/android +/client/cmake +/client/core/serialization +/client/daemon +/client/fonts +/client/images +/client/ios +/client/mozilla +/client/platforms/dummy +/client/platforms/linux +/client/platforms/macos +/client/platforms/windows +/client/server_scripts +/client/translations +/deploy +/docs +/metadata +/service/src diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ca8e075..35e740b0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,12 +10,16 @@ env: jobs: Build-Linux-Ubuntu: - name: 'Build-Linux-Ubuntu' runs-on: ubuntu-20.04 env: QT_VERSION: 6.6.2 QIF_VERSION: 4.7 + PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install Qt' @@ -65,16 +69,27 @@ jobs: path: deploy/AppDir retention-days: 7 + - name: 'Upload translations artifact' + uses: actions/upload-artifact@v4 + with: + name: AmneziaVPN_translations + path: client/translations + retention-days: 7 + # ------------------------------------------------------ Build-Windows: - name: Build-Windows runs-on: windows-latest env: QT_VERSION: 6.6.2 QIF_VERSION: 4.7 BUILD_ARCH: 64 + PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Get sources' @@ -130,13 +145,17 @@ jobs: # ------------------------------------------------------ Build-iOS: - name: 'Build-iOS' runs-on: macos-13 env: QT_VERSION: 6.6.2 CC: cc CXX: c++ + PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Setup xcode' @@ -171,7 +190,7 @@ jobs: - name: 'Install go' uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.22.1' cache: false - name: 'Setup gomobile' @@ -198,7 +217,11 @@ jobs: export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin" export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos" export PATH=$PATH:~/go/bin - sh deploy/build_ios.sh + sh deploy/build_ios.sh | \ + sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d' env: IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }} IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }} @@ -221,19 +244,23 @@ jobs: # ------------------------------------------------------ Build-MacOS: - name: 'Build-MacOS' runs-on: macos-latest env: # Keep compat with MacOS 10.15 aka Catalina by Qt 6.4 QT_VERSION: 6.4.3 QIF_VERSION: 4.6 + PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.3.1' + xcode-version: '15.4.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v3 @@ -286,28 +313,33 @@ jobs: # ------------------------------------------------------ Build-Android: - name: 'Build-Android' runs-on: ubuntu-latest env: ANDROID_BUILD_PLATFORM: android-34 - QT_VERSION: 6.6.2 + QT_VERSION: 6.7.3 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' + PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install desktop Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' target: 'desktop' - arch: 'gcc_64' + arch: 'linux_gcc_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86_64 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -315,10 +347,11 @@ jobs: arch: 'android_x86_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -326,10 +359,11 @@ jobs: arch: 'android_x86' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_armv7 Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -337,10 +371,11 @@ jobs: arch: 'android_armv7' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_arm64_v8a Qt' - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} host: 'linux' @@ -348,7 +383,8 @@ jobs: arch: 'android_arm64_v8a' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Grant execute permission for qt-cmake' shell: bash @@ -432,3 +468,21 @@ jobs: path: deploy/build/AmneziaVPN-release.aab compression-level: 0 retention-days: 7 + + Extra: + runs-on: ubuntu-latest + steps: + - name: Search a corresponding PR + uses: octokit/request-action@v2.x + id: pull_request + with: + route: GET /repos/${{ github.repository }}/pulls + head: ${{ github.repository_owner }}:${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Add PR link to build summary + if: ${{ fromJSON(steps.pull_request.outputs.data)[0].number != '' }} + run: | + echo "Pull request:" >> $GITHUB_STEP_SUMMARY + echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/tag-deploy.yml b/.github/workflows/tag-deploy.yml index b88390f4..2bcbd8c6 100644 --- a/.github/workflows/tag-deploy.yml +++ b/.github/workflows/tag-deploy.yml @@ -15,6 +15,11 @@ jobs: env: QT_VERSION: 6.4.1 QIF_VERSION: 4.5 + PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} + PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} + DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} + DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} steps: - name: 'Install desktop Qt' diff --git a/.gitmodules b/.gitmodules index 78d45e25..decab9b7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "client/3rd/OpenVPNAdapter"] - path = client/3rd/OpenVPNAdapter - url = https://github.com/amnezia-vpn/OpenVPNAdapter.git [submodule "client/3rd/qtkeychain"] path = client/3rd/qtkeychain url = https://github.com/frankosterfeld/qtkeychain.git @@ -13,3 +10,6 @@ [submodule "client/3rd/amneziawg-apple"] path = client/3rd/amneziawg-apple url = https://github.com/amnezia-vpn/amneziawg-apple +[submodule "client/3rd/QSimpleCrypto"] + path = client/3rd/QSimpleCrypto + url = https://github.com/amnezia-vpn/QSimpleCrypto.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dbf778d..0ccae139 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.6.0.1 +project(${PROJECT} VERSION 4.8.4.4 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 54) +set(APP_ANDROID_VERSION_CODE 2081) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/README.md b/README.md index 590b76bc..992c3ad0 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,51 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ + +### _The best client for self-hosted VPN_ + [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) -Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md) + + +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. + +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) + +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/amnezia/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) + +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/amnezia/amnezia.org ). + + + + +[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) + +
+ + ## Features -- Very easy to use - enter your IP address, SSH login, and password, and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. -- OpenVPN, Shadowsocks, WireGuard, and IKEv2 protocols support. -- Masking VPN with OpenVPN over Cloak plugin -- Split tunneling support - add any sites to the client to enable VPN only for them (only for desktops) +- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. +- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. +- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). - Windows, MacOS, Linux, Android, iOS releases. +- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). ## Links -[https://amnezia.org](https://amnezia.org) - project website -[https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit -[https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) -[https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium ## Tech @@ -44,6 +70,19 @@ git submodule update --init --recursive Want to contribute? Welcome! +### Help with translations + +Download the most actual translation files. + +Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. +Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". + +Unzip this file. +Each *.ts file contains strings for one corresponding language. + +Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. +You can do it via a web-interface or any other method you're familiar with. + ### Building sources and deployment Check deploy folder for build scripts. @@ -52,7 +91,7 @@ Check deploy folder for build scripts. 1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. -2. We use QT to generate the XCode project. We need QT version 6.6.1. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: +2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: - MacOS - iOS - Qt 5 Compatibility Module @@ -119,9 +158,11 @@ The Android app has the following requirements: * Android platform SDK 33 * CMake 3.25.0 -After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. - * set path to JDK 11 - * set path to Android SDK ($ANDROID_HOME) +After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. + +- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. +- Set path to JDK 11 +- Set path to Android SDK (`$ANDROID_HOME`) In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  @@ -142,11 +183,13 @@ GPL v3.0 ## Donate -Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u -XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 -payeer.com: P2561305 -ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn) +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) +Bitcoin: bc1qmhtgcf9637rl3kqyy22r2a8wa8laka4t9rx2mf
+USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
+USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
+XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns ## Acknowledgments This project is tested with BrowserStack. diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 00000000..44681875 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,181 @@ +# Amnezia VPN + +### _Лучший клиент для создания VPN на собственном сервере_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский +[AmneziaVPN](https://amnezia.org) — это open 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) + +> [!TIP] +> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/amnezia/amnezia.org). + + + + +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) + +
+ + + +## Особенности + +- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN. +- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2. +- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них. +- Поддерживает платформы: Windows, macOS, Linux, Android, iOS. +- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). + +## Ссылки + +- [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://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\) + +## Технологии + +AmneziaVPN использует несколько проектов с открытым исходным кодом: + +- [OpenSSL](https://www.openssl.org/) +- [OpenVPN](https://openvpn.net/) +- [Shadowsocks](https://shadowsocks.org/) +- [Qt](https://www.qt.io/) +- [LibSsh](https://libssh.org) +- и другие... + +## Проверка исходного кода +После клонирования репозитория обязательно загрузите все подмодули. + +```bash +git submodule update --init --recursive +``` + + +## Разработка +Хотите внести свой вклад? Добро пожаловать! + +### Помощь с переводами + +Загрузите самые актуальные файлы перевода. + +Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations". + +Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка. + +Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом. + +### Сборка исходного кода и деплой +Проверьте папку deploy для скриптов сборки. + +### Как собрать iOS-приложение из исходного кода на MacOS +1. Убедитесь, что у вас установлен Xcode версии 14 или выше. +2. Для генерации проекта Xcode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули: +- MacOS +- iOS +- Модуль совместимости с Qt 5 +- Qt Shader Tools +- Дополнительные библиотеки: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + + +3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь. +4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile: + +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Соберите проект: +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Замените и на ваши значения. + +Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile: +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Откройте проект в Xcode. Теперь вы можете тестировать, архивировать или публиковать приложение. + +Если сборка завершится с ошибкой: +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM: +``` +arch -arm64 brew install cmake +``` + + При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку. + + +## Как собрать Android-приложение +Сборка тестировалась на MacOS. Требования: +- JDK 11 +- Android SDK 33 +- CMake 3.25.0 + +Установите QT, QT Creator и Android Studio. +Настройте QT Creator: + +- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`. +- Укажите путь к JDK 11. +- Укажите путь к Android SDK (`$ANDROID_HOME`) + +Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере! + +Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake. + +Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK. + +### Разработка Android-компонентов + +После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt__Clang_-`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt__Clang_-/client/android-build` в качестве корневой. +Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения. +Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`/client/android-build/.`). + + +## Лицензия + +GPL v3.0 + +## Донаты + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1qmhtgcf9637rl3kqyy22r2a8wa8laka4t9rx2mf
+USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
+USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
+XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns + +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index ea49bf87..efad1a5b 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit ea49bf8796afbc5bd70a0f98f4d99c9ea4792d80 +Subproject commit efad1a5b5cb8e8ab61e49ccdca18c9090a0da8d3 diff --git a/client/3rd/OpenVPNAdapter b/client/3rd/OpenVPNAdapter deleted file mode 160000 index 7c821a8d..00000000 --- a/client/3rd/OpenVPNAdapter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7c821a8d5c1ad5ad94e0763b4f25a875b5a6fe1b diff --git a/client/3rd/QSimpleCrypto b/client/3rd/QSimpleCrypto new file mode 160000 index 00000000..c99b33f0 --- /dev/null +++ b/client/3rd/QSimpleCrypto @@ -0,0 +1 @@ +Subproject commit c99b33f0e08b7206116ddff85c22d3b97ce1e79d diff --git a/client/3rd/QSimpleCrypto/QSimpleCrypto.cmake b/client/3rd/QSimpleCrypto/QSimpleCrypto.cmake deleted file mode 100644 index 7ec5498a..00000000 --- a/client/3rd/QSimpleCrypto/QSimpleCrypto.cmake +++ /dev/null @@ -1,20 +0,0 @@ -include_directories(${CMAKE_CURRENT_LIST_DIR}) - -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/include/QAead.h - ${CMAKE_CURRENT_LIST_DIR}/include/QBlockCipher.h - ${CMAKE_CURRENT_LIST_DIR}/include/QCryptoError.h - ${CMAKE_CURRENT_LIST_DIR}/include/QRsa.h - ${CMAKE_CURRENT_LIST_DIR}/include/QSimpleCrypto_global.h - ${CMAKE_CURRENT_LIST_DIR}/include/QX509.h - ${CMAKE_CURRENT_LIST_DIR}/include/QX509Store.h -) - -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/sources/QAead.cpp - ${CMAKE_CURRENT_LIST_DIR}/sources/QBlockCipher.cpp - ${CMAKE_CURRENT_LIST_DIR}/sources/QCryptoError.cpp - ${CMAKE_CURRENT_LIST_DIR}/sources/QRsa.cpp - ${CMAKE_CURRENT_LIST_DIR}/sources/QX509.cpp - ${CMAKE_CURRENT_LIST_DIR}/sources/QX509Store.cpp -) diff --git a/client/3rd/QSimpleCrypto/QSimpleCrypto.pri b/client/3rd/QSimpleCrypto/QSimpleCrypto.pri deleted file mode 100644 index 99a1c129..00000000 --- a/client/3rd/QSimpleCrypto/QSimpleCrypto.pri +++ /dev/null @@ -1,18 +0,0 @@ -INCLUDEPATH += $$PWD - -HEADERS += \ - $$PWD/include/QAead.h \ - $$PWD/include/QBlockCipher.h \ - $$PWD/include/QCryptoError.h \ - $$PWD/include/QRsa.h \ - $$PWD/include/QSimpleCrypto_global.h \ - $$PWD/include/QX509.h \ - $$PWD/include/QX509Store.h - -SOURCES += \ - $$PWD/sources/QAead.cpp \ - $$PWD/sources/QBlockCipher.cpp \ - $$PWD/sources/QCryptoError.cpp \ - $$PWD/sources/QRsa.cpp \ - $$PWD/sources/QX509.cpp \ - $$PWD/sources/QX509Store.cpp diff --git a/client/3rd/QSimpleCrypto/include/QAead.h b/client/3rd/QSimpleCrypto/include/QAead.h deleted file mode 100644 index 11f60b31..00000000 --- a/client/3rd/QSimpleCrypto/include/QAead.h +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#ifndef QAEAD_H -#define QAEAD_H - -#include "QSimpleCrypto_global.h" - -#include - -#include - -#include -#include -#include -#include - -#include "QCryptoError.h" - -// clang-format off -namespace QSimpleCrypto -{ - class QSIMPLECRYPTO_EXPORT QAead { - public: - QAead(); - - /// - /// \brief encryptAesGcm - Function encrypts data with Gcm algorithm. - /// \param data - Data that will be encrypted. - /// \param key - AES key. - /// \param iv - Initialization vector. - /// \param tag - Authorization tag. - /// \param aad - Additional authenticated data. Must be nullptr, if not used. - /// \param cipher - Can be used with OpenSSL EVP_CIPHER (gcm) - 128, 192, 256. Example: EVP_aes_256_gcm(). - /// \return Returns encrypted data or "", if error happened. - /// - QByteArray encryptAesGcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad = "", const EVP_CIPHER* cipher = EVP_aes_256_gcm()); - - /// - /// \brief decryptAesGcm - Function decrypts data with Gcm algorithm. - /// \param data - Data that will be decrypted - /// \param key - AES key - /// \param iv - Initialization vector - /// \param tag - Authorization tag - /// \param aad - Additional authenticated data. Must be nullptr, if not used - /// \param cipher - Can be used with OpenSSL EVP_CIPHER (gcm) - 128, 192, 256. Example: EVP_aes_256_gcm() - /// \return Returns decrypted data or "", if error happened. - /// - QByteArray decryptAesGcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad = "", const EVP_CIPHER* cipher = EVP_aes_256_gcm()); - - /// - /// \brief encryptAesCcm - Function encrypts data with Ccm algorithm. - /// \param data - Data that will be encrypted. - /// \param key - AES key. - /// \param iv - Initialization vector. - /// \param tag - Authorization tag. - /// \param aad - Additional authenticated data. Must be nullptr, if not used. - /// \param cipher - Can be used with OpenSSL EVP_CIPHER (ccm) - 128, 192, 256. Example: EVP_aes_256_ccm(). - /// \return Returns encrypted data or "", if error happened. - /// - QByteArray encryptAesCcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad = "", const EVP_CIPHER* cipher = EVP_aes_256_ccm()); - - /// - /// \brief decryptAesCcm - Function decrypts data with Ccm algorithm. - /// \param data - Data that will be decrypted. - /// \param key - AES key. - /// \param iv - Initialization vector. - /// \param tag - Authorization tag. - /// \param aad - Additional authenticated data. Must be nullptr, if not used. - /// \param cipher - Can be used with OpenSSL EVP_CIPHER (ccm) - 128, 192, 256. Example: EVP_aes_256_ccm(). - /// \return Returns decrypted data or "", if error happened. - /// - QByteArray decryptAesCcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad = "", const EVP_CIPHER* cipher = EVP_aes_256_ccm()); - - /// - /// \brief error - Error handler class. - /// - QCryptoError error; - }; -} // namespace QSimpleCrypto - -#endif // QAEAD_H diff --git a/client/3rd/QSimpleCrypto/include/QBlockCipher.h b/client/3rd/QSimpleCrypto/include/QBlockCipher.h deleted file mode 100644 index e7b83a88..00000000 --- a/client/3rd/QSimpleCrypto/include/QBlockCipher.h +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#ifndef QBLOCKCIPHER_H -#define QBLOCKCIPHER_H - -#include "QSimpleCrypto_global.h" - -#include - -#include - -#include -#include -#include -#include - -#include "QCryptoError.h" - -// clang-format off -namespace QSimpleCrypto -{ - class QSIMPLECRYPTO_EXPORT QBlockCipher { - - #define Aes128Rounds 10 - #define Aes192Rounds 12 - #define Aes256Rounds 14 - - public: - QBlockCipher(); - - /// - /// \brief generateRandomBytes - Function generates random bytes by size. - /// \param size - Size of generated bytes. - /// \return Returns random bytes. - /// - QByteArray generateRandomBytes(const int& size); - QByteArray generateSecureRandomBytes(const int& size); - - /// - /// \brief encryptAesBlockCipher - Function encrypts data with Aes Block Cipher algorithm. - /// \param data - Data that will be encrypted. - /// \param key - AES key. - /// \param iv - Initialization vector. - /// \param password - Encryption password. - /// \param salt - Random delta. - /// \param rounds - Transformation rounds. - /// \param chiper - Can be used with OpenSSL EVP_CIPHER (ecb, cbc, cfb, ofb, ctr) - 128, 192, 256. Example: EVP_aes_256_cbc(). - /// \param md - Hash algroitm (OpenSSL EVP_MD). Example: EVP_sha512(). - /// \return Returns decrypted data or "", if error happened. - /// - QByteArray encryptAesBlockCipher(QByteArray data, QByteArray key, - QByteArray iv = "", const int& rounds = Aes256Rounds, - const EVP_CIPHER* cipher = EVP_aes_256_cbc(), const EVP_MD* md = EVP_sha512()); - - /// - /// \brief decryptAesBlockCipher - Function decrypts data with Aes Block Cipher algorithm. - /// \param data - Data that will be decrypted. - /// \param key - AES key. - /// \param iv - Initialization vector. - /// \param password - Decryption password. - /// \param salt - Random delta. - /// \param rounds - Transformation rounds. - /// \param chiper - Can be used with OpenSSL EVP_CIPHER (ecb, cbc, cfb, ofb, ctr) - 128, 192, 256. Example: EVP_aes_256_cbc(). - /// \param md - Hash algroitm (OpenSSL EVP_MD). Example: EVP_sha512(). - /// \return Returns decrypted data or "", if error happened. - /// - QByteArray decryptAesBlockCipher(QByteArray data, QByteArray key, - QByteArray iv = "", const int& rounds = Aes256Rounds, - const EVP_CIPHER* cipher = EVP_aes_256_cbc(), const EVP_MD* md = EVP_sha512()); - - /// - /// \brief error - Error handler class. - /// - QCryptoError error; - }; -} // namespace QSimpleCrypto - -#endif // QBLOCKCIPHER_H diff --git a/client/3rd/QSimpleCrypto/include/QCryptoError.h b/client/3rd/QSimpleCrypto/include/QCryptoError.h deleted file mode 100644 index fc059654..00000000 --- a/client/3rd/QSimpleCrypto/include/QCryptoError.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef QCRYPTOERROR_H -#define QCRYPTOERROR_H - -#include - -#include "QSimpleCrypto_global.h" - -/// TODO: Add Special error code for each error. - -// clang-format off -namespace QSimpleCrypto -{ - class QSIMPLECRYPTO_EXPORT QCryptoError : public QObject { - Q_OBJECT - - public: - explicit QCryptoError(QObject* parent = nullptr); - - /// - /// \brief setError - Sets error information - /// \param errorCode - Error code. - /// \param errorSummary - Error summary. - /// - inline void setError(const quint8 errorCode, const QString& errorSummary) - { - m_currentErrorCode = errorCode; - m_errorSummary = errorSummary; - } - - /// - /// \brief lastError - Returns last error. - /// \return Returns eror ID and error Text. - /// - inline QPair lastError() const - { - return QPair(m_currentErrorCode, m_errorSummary); - } - - private: - quint8 m_currentErrorCode; - QString m_errorSummary; - }; -} - -#endif // QCRYPTOERROR_H diff --git a/client/3rd/QSimpleCrypto/include/QRsa.h b/client/3rd/QSimpleCrypto/include/QRsa.h deleted file mode 100644 index 45eb3169..00000000 --- a/client/3rd/QSimpleCrypto/include/QRsa.h +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#ifndef QRSA_H -#define QRSA_H - -#include "QSimpleCrypto_global.h" - -#include -#include - -#include - -#include -#include -#include - -#include "QCryptoError.h" - -// clang-format off -namespace QSimpleCrypto -{ - class QSIMPLECRYPTO_EXPORT QRsa { - - #define PublicEncrypt 0 - #define PrivateEncrypt 1 - #define PublicDecrypt 2 - #define PrivateDecrypt 3 - - public: - QRsa(); - - /// - /// \brief generateRsaKeys - Function generate Rsa Keys and returns them in OpenSSL structure. - /// \param bits - RSA key size. - /// \param rsaBigNumber - The exponent is an odd number, typically 3, 17 or 65537. - /// \return Returns 'OpenSSL RSA structure' or 'nullptr', if error happened. Returned value must be cleaned up with 'RSA_free()' to avoid memory leak. - /// - RSA* generateRsaKeys(const int& bits, const int& rsaBigNumber); - - /// - /// \brief savePublicKey - Saves to file RSA public key. - /// \param rsa - OpenSSL RSA structure. - /// \param publicKeyFileName - Public key file name. - /// - void savePublicKey(RSA *rsa, const QByteArray& publicKeyFileName); - - /// - /// \brief savePrivateKey - Saves to file RSA private key. - /// \param rsa - OpenSSL RSA structure. - /// \param privateKeyFileName - Private key file name. - /// \param password - Private key password. - /// \param cipher - Can be used with 'OpenSSL EVP_CIPHER' (ecb, cbc, cfb, ofb, ctr) - 128, 192, 256. Example: EVP_aes_256_cbc(). - /// - void savePrivateKey(RSA* rsa, const QByteArray& privateKeyFileName, QByteArray password = "", const EVP_CIPHER* cipher = nullptr); - - /// - /// \brief getPublicKeyFromFile - Gets RSA public key from a file. - /// \param filePath - File path to public key file. - /// \return Returns 'OpenSSL EVP_PKEY structure' or 'nullptr', if error happened. Returned value must be cleaned up with 'EVP_PKEY_free()' to avoid memory leak. - /// - EVP_PKEY* getPublicKeyFromFile(const QByteArray& filePath); - - /// - /// \brief getPrivateKeyFromFile - Gets RSA private key from a file. - /// \param filePath - File path to private key file. - /// \param password - Private key password. - /// \return - Returns 'OpenSSL EVP_PKEY structure' or 'nullptr', if error happened. Returned value must be cleaned up with 'EVP_PKEY_free()' to avoid memory leak. - /// - EVP_PKEY* getPrivateKeyFromFile(const QByteArray& filePath, const QByteArray& password = ""); - - /// - /// \brief encrypt - Encrypt data with RSA algorithm. - /// \param plaintext - Text that must be encrypted. - /// \param rsa - OpenSSL RSA structure. - /// \param encryptType - Public or private encrypt type. (PUBLIC_ENCRYPT, PRIVATE_ENCRYPT). - /// \param padding - OpenSSL RSA padding can be used with: 'RSA_PKCS1_PADDING', 'RSA_NO_PADDING' and etc. - /// \return Returns encrypted data or "", if error happened. - /// - QByteArray encrypt(QByteArray plainText, RSA* rsa, const int& encryptType = PublicEncrypt, const int& padding = RSA_PKCS1_PADDING); - - /// - /// \brief decrypt - Decrypt data with RSA algorithm. - /// \param cipherText - Text that must be decrypted. - /// \param rsa - OpenSSL RSA structure. - /// \param decryptType - Public or private type. (PUBLIC_DECRYPT, PRIVATE_DECRYPT). - /// \param padding - RSA padding can be used with: 'RSA_PKCS1_PADDING', 'RSA_NO_PADDING' and etc. - /// \return - Returns decrypted data or "", if error happened. - /// - QByteArray decrypt(QByteArray cipherText, RSA* rsa, const int& decryptType = PrivateDecrypt, const int& padding = RSA_PKCS1_PADDING); - - /// - /// \brief error - Error handler class. - /// - QCryptoError error; - }; -} // namespace QSimpleCrypto - -#endif // QRSA_H diff --git a/client/3rd/QSimpleCrypto/include/QSimpleCrypto_global.h b/client/3rd/QSimpleCrypto/include/QSimpleCrypto_global.h deleted file mode 100644 index fdd6c020..00000000 --- a/client/3rd/QSimpleCrypto/include/QSimpleCrypto_global.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef QSIMPLECRYPTO_GLOBAL_H -#define QSIMPLECRYPTO_GLOBAL_H - -#include -#include - -#define QSIMPLECRYPTO_EXPORT - -#endif // QSIMPLECRYPTO_GLOBAL_H diff --git a/client/3rd/QSimpleCrypto/include/QX509.h b/client/3rd/QSimpleCrypto/include/QX509.h deleted file mode 100644 index c31cb9e4..00000000 --- a/client/3rd/QSimpleCrypto/include/QX509.h +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#ifndef QX509_H -#define QX509_H - -#include "QSimpleCrypto_global.h" - -#include -#include - -#include - -#include -#include -#include -#include -#include - -#include "QCryptoError.h" - -// clang-format off -namespace QSimpleCrypto -{ - class QSIMPLECRYPTO_EXPORT QX509 { - - #define oneYear 31536000L - #define x509LastVersion 2 - - public: - QX509(); - - /// - /// \brief loadCertificateFromFile - Function load X509 from file and returns OpenSSL structure. - /// \param fileName - File path to certificate. - /// \return Returns OpenSSL X509 structure or nullptr, if error happened. Returned value must be cleaned up with 'X509_free' to avoid memory leak. - /// - X509* loadCertificateFromFile(const QByteArray& fileName); - - /// - /// \brief signCertificate - Function signs X509 certificate and returns signed X509 OpenSSL structure. - /// \param endCertificate - Certificate that will be signed - /// \param caCertificate - CA certificate that will sign end certificate - /// \param caPrivateKey - CA certificate private key - /// \param fileName - With that name certificate will be saved. Leave "", if don't need to save it - /// \return Returns OpenSSL X509 structure or nullptr, if error happened. - /// - X509* signCertificate(X509* endCertificate, X509* caCertificate, EVP_PKEY* caPrivateKey, const QByteArray& fileName = ""); - - /// - /// \brief verifyCertificate - Function verifies X509 certificate and returns verified X509 OpenSSL structure. - /// \param x509 - OpenSSL X509. That certificate will be verified. - /// \param store - Trusted certificate must be added to X509_Store with 'addCertificateToStore(X509_STORE* ctx, X509* x509)'. - /// \return Returns OpenSSL X509 structure or nullptr, if error happened - /// - X509* verifyCertificate(X509* x509, X509_STORE* store); - - /// - /// \brief generateSelfSignedCertificate - Function generatesand returns self signed X509. - /// \param rsa - OpenSSL RSA. - /// \param additionalData - Certificate information. - /// \param certificateFileName - With that name certificate will be saved. Leave "", if don't need to save it. - /// \param md - OpenSSL EVP_MD structure. Example: EVP_sha512(). - /// \param serialNumber - X509 certificate serial number. - /// \param version - X509 certificate version. - /// \param notBefore - X509 start date. - /// \param notAfter - X509 end date. - /// \return Returns OpenSSL X509 structure or nullptr, if error happened. Returned value must be cleaned up with 'X509_free' to avoid memory leak. - /// - X509* generateSelfSignedCertificate(RSA* rsa, const QMap& additionalData, - const QByteArray& certificateFileName = "", const EVP_MD* md = EVP_sha512(), - const long& serialNumber = 1, const long& version = x509LastVersion, - const long& notBefore = 0, const long& notAfter = oneYear); - - /// - /// \brief error - Error handler class. - /// - QCryptoError error; - }; -} // namespace QSimpleCrypto - -#endif // QX509_H diff --git a/client/3rd/QSimpleCrypto/include/QX509Store.h b/client/3rd/QSimpleCrypto/include/QX509Store.h deleted file mode 100644 index 8cd8ca82..00000000 --- a/client/3rd/QSimpleCrypto/include/QX509Store.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#ifndef QX509STORE_H -#define QX509STORE_H - -#include "QSimpleCrypto_global.h" - -#include -#include -#include - -#include - -#include -#include -#include - -#include "QCryptoError.h" - -// clang-format off -namespace QSimpleCrypto -{ - class QSIMPLECRYPTO_EXPORT QX509Store { - public: - QX509Store(); - - /// - /// \brief addCertificateToStore - /// \param store - OpenSSL X509_STORE. - /// \param x509 - OpenSSL X509. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool addCertificateToStore(X509_STORE* store, X509* x509); - - /// - /// \brief addLookup - /// \param store - OpenSSL X509_STORE. - /// \param method - OpenSSL X509_LOOKUP_METHOD. Example: X509_LOOKUP_file. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool addLookup(X509_STORE* store, X509_LOOKUP_METHOD* method); - - /// - /// \brief setCertificateDepth - /// \param store - OpenSSL X509_STORE. - /// \param depth - That is the maximum number of untrusted CA certificates that can appear in a chain. Example: 0. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool setDepth(X509_STORE* store, const int& depth); - - /// - /// \brief setFlag - /// \param store - OpenSSL X509_STORE. - /// \param flag - The verification flags consists of zero or more of the following flags ored together. Example: X509_V_FLAG_CRL_CHECK. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool setFlag(X509_STORE* store, const unsigned long& flag); - - /// - /// \brief setFlag - /// \param store - OpenSSL X509_STORE. - /// \param purpose - Verification purpose in param to purpose. Example: X509_PURPOSE_ANY. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool setPurpose(X509_STORE* store, const int& purpose); - - /// - /// \brief setTrust - /// \param store - OpenSSL X509_STORE. - /// \param trust - Trust Level. Example: X509_TRUST_SSL_SERVER. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool setTrust(X509_STORE* store, const int& trust); - - /// - /// \brief setDefaultPaths - /// \param store - OpenSSL X509_STORE. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool setDefaultPaths(X509_STORE* store); - - /// - /// \brief loadLocations - /// \param store - OpenSSL X509_STORE. - /// \param fileName - File name. Example: "caCertificate.pem". - /// \param dirPath - Path to file. Example: "path/To/File". - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool loadLocations(X509_STORE* store, const QByteArray& fileName, const QByteArray& dirPath); - - /// - /// \brief loadLocations - /// \param store - OpenSSL X509_STORE. - /// \param file - Qt QFile that will be loaded. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool loadLocations(X509_STORE* store, const QFile& file); - - /// - /// \brief loadLocations - /// \param store - OpenSSL X509_STORE. - /// \param fileInfo - Qt QFileInfo. - /// \return Returns 'true' on success and 'false', if error happened. - /// - bool loadLocations(X509_STORE* store, const QFileInfo& fileInfo); - - /// - /// \brief error - Error handler class. - /// - QCryptoError error; - }; -} - -#endif // QX509STORE_H diff --git a/client/3rd/QSimpleCrypto/sources/QAead.cpp b/client/3rd/QSimpleCrypto/sources/QAead.cpp deleted file mode 100644 index 968c8841..00000000 --- a/client/3rd/QSimpleCrypto/sources/QAead.cpp +++ /dev/null @@ -1,364 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#include "include/QAead.h" - -QSimpleCrypto::QAead::QAead() -{ -} - -/// -/// \brief QSimpleCrypto::QAEAD::encryptAesGcm - Function encrypts data with Gcm algorithm. -/// \param data - Data that will be encrypted. -/// \param key - AES key. -/// \param iv - Initialization vector. -/// \param tag - Authorization tag. -/// \param aad - Additional authenticated data. Must be nullptr, if not used. -/// \param cipher - Can be used with OpenSSL EVP_CIPHER (gcm) - 128, 192, 256. Example: EVP_aes_256_gcm(). -/// \return Returns encrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QAead::encryptAesGcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad, const EVP_CIPHER* cipher) -{ - try { - /* Initialize EVP_CIPHER_CTX */ - std::unique_ptr encryptionCipher { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; - if (encryptionCipher == nullptr) { - throw std::runtime_error("Couldn't initialize \'encryptionCipher\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set data length */ - int plainTextLength = data.size(); - int cipherTextLength = 0; - - /* Initialize cipherText. Here encrypted data will be stored */ - std::unique_ptr cipherText { new unsigned char[plainTextLength]() }; - if (cipherText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'ciphertext'."); - } - - /* Initialize encryption operation. */ - if (!EVP_EncryptInit_ex(encryptionCipher.get(), cipher, nullptr, reinterpret_cast(key.data()), reinterpret_cast(iv.data()))) { - throw std::runtime_error("Couldn't initialize encryption operation. EVP_EncryptInit_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set IV length if default 12 bytes (96 bits) is not appropriate */ - if (!EVP_CIPHER_CTX_ctrl(encryptionCipher.get(), EVP_CTRL_GCM_SET_IVLEN, iv.length(), nullptr)) { - throw std::runtime_error("Couldn't set IV length. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - -// /* Check if aad need to be used */ -// if (aad.length() > 0) { -// /* Provide any AAD data. This can be called zero or more times as required */ -// if (!EVP_EncryptUpdate(encryptionCipher.get(), nullptr, &cipherTextLength, reinterpret_cast(aad.data()), aad.length())) { -// throw std::runtime_error("Couldn't provide aad data. EVP_EncryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); -// } -// } - - /* - * Provide the message to be encrypted, and obtain the encrypted output. - * EVP_EncryptUpdate can be called multiple times if necessary - */ - if (!EVP_EncryptUpdate(encryptionCipher.get(), cipherText.get(), &cipherTextLength, reinterpret_cast(data.data()), plainTextLength)) { - throw std::runtime_error("Couldn't provide message to be encrypted. EVP_EncryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* - * Finalize the encryption. Normally cipher text bytes may be written at - * this stage, but this does not occur in GCM mode - */ - if (!EVP_EncryptFinal_ex(encryptionCipher.get(), cipherText.get(), &plainTextLength)) { - throw std::runtime_error("Couldn't finalize encryption. EVP_EncryptFinal_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - -// /* Get tag */ -// if (!EVP_CIPHER_CTX_ctrl(encryptionCipher.get(), EVP_CTRL_GCM_GET_TAG, tag->length(), reinterpret_cast(tag->data()))) { -// throw std::runtime_error("Couldn't get tag. EVP_CIPHER_CTX_ctrl(. Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); -// } - - /* Finilize data to be readable with qt */ - QByteArray encryptedData = QByteArray(reinterpret_cast(cipherText.get()), cipherTextLength); - - return encryptedData; - - } catch (std::exception& exception) { - QSimpleCrypto::QAead::error.setError(1, exception.what()); - return QByteArray(); - } catch (...) { - QSimpleCrypto::QAead::error.setError(2, "Unknown error!"); - return QByteArray(); - } - - return QByteArray(); -} - -/// -/// \brief QSimpleCrypto::QAEAD::decryptAesGcm - Function decrypts data with Gcm algorithm. -/// \param data - Data that will be decrypted -/// \param key - AES key -/// \param iv - Initialization vector -/// \param tag - Authorization tag -/// \param aad - Additional authenticated data. Must be nullptr, if not used -/// \param cipher - Can be used with OpenSSL EVP_CIPHER (gcm) - 128, 192, 256. Example: EVP_aes_256_gcm() -/// \return Returns decrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QAead::decryptAesGcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad, const EVP_CIPHER* cipher) -{ - try { - /* Initialize EVP_CIPHER_CTX */ - std::unique_ptr decryptionCipher { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; - if (decryptionCipher.get() == nullptr) { - throw std::runtime_error("Couldn't initialize \'decryptionCipher\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set data length */ - int cipherTextLength = data.size(); - int plainTextLength = 0; - - /* Initialize plainText. Here decrypted data will be stored */ - std::unique_ptr plainText { new unsigned char[cipherTextLength]() }; - if (plainText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'plaintext'."); - } - - /* Initialize decryption operation. */ - if (!EVP_DecryptInit_ex(decryptionCipher.get(), cipher, nullptr, reinterpret_cast(key.data()), reinterpret_cast(iv.data()))) { - throw std::runtime_error("Couldn't initialize decryption operation. EVP_DecryptInit_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set IV length. Not necessary if this is 12 bytes (96 bits) */ - if (!EVP_CIPHER_CTX_ctrl(decryptionCipher.get(), EVP_CTRL_GCM_SET_IVLEN, iv.length(), nullptr)) { - throw std::runtime_error("Couldn't set IV length. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - -// /* Check if aad need to be used */ -// if (aad.length() > 0) { -// /* Provide any AAD data. This can be called zero or more times as required */ -// if (!EVP_DecryptUpdate(decryptionCipher.get(), nullptr, &plainTextLength, reinterpret_cast(aad.data()), aad.length())) { -// throw std::runtime_error("Couldn't provide aad data. EVP_DecryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); -// } -// } - - /* - * Provide the message to be decrypted, and obtain the plain text output. - * EVP_DecryptUpdate can be called multiple times if necessary - */ - if (!EVP_DecryptUpdate(decryptionCipher.get(), plainText.get(), &plainTextLength, reinterpret_cast(data.data()), cipherTextLength)) { - throw std::runtime_error("Couldn't provide message to be decrypted. EVP_DecryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - -// /* Set expected tag value. Works in OpenSSL 1.0.1d and later */ -// if (!EVP_CIPHER_CTX_ctrl(decryptionCipher.get(), EVP_CTRL_GCM_SET_TAG, tag->length(), reinterpret_cast(tag->data()))) { -// throw std::runtime_error("Coldn't set tag. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); -// } - - /* - * Finalize the decryption. A positive return value indicates success, - * anything else is a failure - the plain text is not trustworthy. - */ - if (!EVP_DecryptFinal_ex(decryptionCipher.get(), plainText.get(), &cipherTextLength)) { - throw std::runtime_error("Couldn't finalize decryption. EVP_DecryptFinal_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Finilize data to be readable with qt */ - QByteArray decryptedData = QByteArray(reinterpret_cast(plainText.get()), plainTextLength); - - return decryptedData; - - } catch (std::exception& exception) { - QSimpleCrypto::QAead::error.setError(1, exception.what()); - return QByteArray(); - } catch (...) { - QSimpleCrypto::QAead::error.setError(2, "Unknown error!"); - return QByteArray(); - } - - return QByteArray(); -} - -/// -/// \brief QSimpleCrypto::QAEAD::encryptAesCcm - Function encrypts data with Ccm algorithm. -/// \param data - Data that will be encrypted. -/// \param key - AES key. -/// \param iv - Initialization vector. -/// \param tag - Authorization tag. -/// \param aad - Additional authenticated data. Must be nullptr, if not used. -/// \param cipher - Can be used with OpenSSL EVP_CIPHER (ccm) - 128, 192, 256. Example: EVP_aes_256_ccm(). -/// \return Returns encrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QAead::encryptAesCcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad, const EVP_CIPHER* cipher) -{ - try { - /* Initialize EVP_CIPHER_CTX */ - std::unique_ptr encryptionCipher { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; - if (encryptionCipher == nullptr) { - throw std::runtime_error("Couldn't initialize \'encryptionCipher\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set data length */ - int plainTextLength = data.size(); - int cipherTextLength = 0; - - /* Initialize cipherText. Here encrypted data will be stored */ - std::unique_ptr cipherText { new unsigned char[plainTextLength]() }; - if (cipherText.get() == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'ciphertext'."); - } - - /* Initialize encryption operation. */ - if (!EVP_EncryptInit_ex(encryptionCipher.get(), cipher, nullptr, reinterpret_cast(key.data()), reinterpret_cast(iv.data()))) { - throw std::runtime_error("Couldn't initialize encryption operation. EVP_EncryptInit_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set IV length if default 12 bytes (96 bits) is not appropriate */ - if (!EVP_CIPHER_CTX_ctrl(encryptionCipher.get(), EVP_CTRL_CCM_SET_IVLEN, iv.length(), nullptr)) { - throw std::runtime_error("Couldn't set IV length. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set tag length */ - if (!EVP_CIPHER_CTX_ctrl(encryptionCipher.get(), EVP_CTRL_CCM_SET_TAG, tag->length(), nullptr)) { - throw std::runtime_error("Coldn't set tag. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Check if aad need to be used */ - if (aad.length() > 0) { - /* Provide the total plain text length */ - if (!EVP_EncryptUpdate(encryptionCipher.get(), nullptr, &cipherTextLength, nullptr, plainTextLength)) { - throw std::runtime_error("Couldn't provide total plaintext length. EVP_EncryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Provide any AAD data. This can be called zero or more times as required */ - if (!EVP_EncryptUpdate(encryptionCipher.get(), nullptr, &cipherTextLength, reinterpret_cast(aad.data()), aad.length())) { - throw std::runtime_error("Couldn't provide aad data. EVP_EncryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } - - /* - * Provide the message to be encrypted, and obtain the encrypted output. - * EVP_EncryptUpdate can be called multiple times if necessary - */ - if (!EVP_EncryptUpdate(encryptionCipher.get(), cipherText.get(), &cipherTextLength, reinterpret_cast(data.data()), plainTextLength)) { - throw std::runtime_error("Couldn't provide message to be encrypted. EVP_EncryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* - * Finalize the encryption. Normally ciphertext bytes may be written at - * this stage, but this does not occur in GCM mode - */ - if (!EVP_EncryptFinal_ex(encryptionCipher.get(), cipherText.get(), &plainTextLength)) { - throw std::runtime_error("Couldn't finalize encryption. EVP_EncryptFinal_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Get tag */ - if (!EVP_CIPHER_CTX_ctrl(encryptionCipher.get(), EVP_CTRL_CCM_GET_TAG, tag->length(), reinterpret_cast(tag->data()))) { - throw std::runtime_error("Couldn't get tag. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Finilize data to be readable with qt */ - QByteArray encryptedData = QByteArray(reinterpret_cast(cipherText.get()), cipherTextLength); - - return encryptedData; - - } catch (std::exception& exception) { - QSimpleCrypto::QAead::error.setError(1, exception.what()); - return QByteArray(); - } catch (...) { - QSimpleCrypto::QAead::error.setError(2, "Unknown error!"); - return QByteArray(); - } - - return QByteArray(); -} - -/// -/// \brief QSimpleCrypto::QAEAD::decryptAesCcm - Function decrypts data with Ccm algorithm. -/// \param data - Data that will be decrypted. -/// \param key - AES key. -/// \param iv - Initialization vector. -/// \param tag - Authorization tag. -/// \param aad - Additional authenticated data. Must be nullptr, if not used. -/// \param cipher - Can be used with OpenSSL EVP_CIPHER (ccm) - 128, 192, 256. Example: EVP_aes_256_ccm(). -/// \return Returns decrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QAead::decryptAesCcm(QByteArray data, QByteArray key, QByteArray iv, QByteArray* tag, QByteArray aad, const EVP_CIPHER* cipher) -{ - try { - /* Initialize EVP_CIPHER_CTX */ - std::unique_ptr decryptionCipher { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; - if (decryptionCipher.get() == nullptr) { - throw std::runtime_error("Couldn't initialize \'decryptionCipher\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set data length */ - int cipherTextLength = data.size(); - int plainTextLength = 0; - - /* Initialize plainText. Here decrypted data will be stored */ - std::unique_ptr plainText { new unsigned char[cipherTextLength]() }; - if (plainText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'plaintext'."); - } - - /* Initialize decryption operation. */ - if (!EVP_DecryptInit_ex(decryptionCipher.get(), cipher, nullptr, reinterpret_cast(key.data()), reinterpret_cast(iv.data()))) { - throw std::runtime_error("Couldn't initialize decryption operation. EVP_DecryptInit_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set IV length. Not necessary if this is 12 bytes (96 bits) */ - if (!EVP_CIPHER_CTX_ctrl(decryptionCipher.get(), EVP_CTRL_CCM_SET_IVLEN, iv.length(), nullptr)) { - throw std::runtime_error("Couldn't set IV length. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set expected tag value. Works in OpenSSL 1.0.1d and later */ - if (!EVP_CIPHER_CTX_ctrl(decryptionCipher.get(), EVP_CTRL_CCM_SET_TAG, tag->length(), reinterpret_cast(tag->data()))) { - throw std::runtime_error("Coldn't set tag. EVP_CIPHER_CTX_ctrl(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Check if aad need to be used */ - if (aad.length() > 0) { - /* Provide the total ciphertext length */ - if (!EVP_DecryptUpdate(decryptionCipher.get(), nullptr, &plainTextLength, nullptr, cipherTextLength)) { - throw std::runtime_error("Couldn't provide total plaintext length. EVP_DecryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Provide any AAD data. This can be called zero or more times as required */ - if (!EVP_DecryptUpdate(decryptionCipher.get(), nullptr, &plainTextLength, reinterpret_cast(aad.data()), aad.length())) { - throw std::runtime_error("Couldn't provide aad data. EVP_DecryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } - - /* - * Provide the message to be decrypted, and obtain the plaintext output. - * EVP_DecryptUpdate can be called multiple times if necessary - */ - if (!EVP_DecryptUpdate(decryptionCipher.get(), plainText.get(), &plainTextLength, reinterpret_cast(data.data()), cipherTextLength)) { - throw std::runtime_error("Couldn't provide message to be decrypted. EVP_DecryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* - * Finalize the decryption. A positive return value indicates success, - * anything else is a failure - the plaintext is not trustworthy. - */ - if (!EVP_DecryptFinal_ex(decryptionCipher.get(), plainText.get(), &cipherTextLength)) { - throw std::runtime_error("Couldn't finalize decryption. EVP_DecryptFinal_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Finilize data to be readable with qt */ - QByteArray decryptedData = QByteArray(reinterpret_cast(plainText.get()), plainTextLength); - - return decryptedData; - - } catch (std::exception& exception) { - QSimpleCrypto::QAead::error.setError(1, exception.what()); - return QByteArray(); - } catch (...) { - QSimpleCrypto::QAead::error.setError(2, "Unknown error!"); - return QByteArray(); - } - - return QByteArray(); -} diff --git a/client/3rd/QSimpleCrypto/sources/QBlockCipher.cpp b/client/3rd/QSimpleCrypto/sources/QBlockCipher.cpp deleted file mode 100644 index 8b86ab98..00000000 --- a/client/3rd/QSimpleCrypto/sources/QBlockCipher.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#include "include/QBlockCipher.h" - -QSimpleCrypto::QBlockCipher::QBlockCipher() -{ -} - -/// -/// \brief QSimpleCrypto::QBlockCipher::generateRandomBytes - Function generates random bytes by size. -/// \param size - Size of generated bytes. -/// \return Returns random bytes. -/// -QByteArray QSimpleCrypto::QBlockCipher::generateRandomBytes(const int& size) -{ - unsigned char arr[sizeof(size)]; - RAND_bytes(arr, sizeof(size)); - - QByteArray buffer = QByteArray(reinterpret_cast(arr), size); - return buffer; -} - -QByteArray QSimpleCrypto::QBlockCipher::generateSecureRandomBytes(const int &size) -{ - unsigned char arr[sizeof(size)]; - RAND_priv_bytes(arr, sizeof(size)); - - QByteArray buffer = QByteArray(reinterpret_cast(arr), size); - return buffer; -} - -/// -/// \brief QSimpleCrypto::QBlockCipher::encryptAesBlockCipher - Function encrypts data with Aes Block Cipher algorithm. -/// \param data - Data that will be encrypted. -/// \param key - AES key. -/// \param iv - Initialization vector. -/// \param password - Encryption password. -/// \param salt - Random delta. -/// \param rounds - Transformation rounds. -/// \param chiper - Can be used with OpenSSL EVP_CIPHER (ecb, cbc, cfb, ofb, ctr) - 128, 192, 256. Example: EVP_aes_256_cbc(). -/// \param md - Hash algroitm (OpenSSL EVP_MD). Example: EVP_sha512(). -/// \return Returns decrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QBlockCipher::encryptAesBlockCipher(QByteArray data, QByteArray key, - QByteArray iv, - const int& rounds, const EVP_CIPHER* cipher, const EVP_MD* md) -{ - try { - /* Initialize EVP_CIPHER_CTX */ - std::unique_ptr encryptionCipher { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; - if (encryptionCipher == nullptr) { - throw std::runtime_error("Couldn't initialize \'encryptionCipher\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Reinterpret values for multi use */ - unsigned char* m_key = reinterpret_cast(key.data()); - unsigned char* m_iv = reinterpret_cast(iv.data()); - - /* Set data length */ - int cipherTextLength(data.size() + AES_BLOCK_SIZE); - int finalLength = 0; - - /* Initialize cipcherText. Here encrypted data will be stored */ - std::unique_ptr cipherText { new unsigned char[cipherTextLength]() }; - if (cipherText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'cipherText'."); - } - - // Bug here -// /* Start encryption with password based encryption routine */ -// if (!EVP_BytesToKey(cipher, md, reinterpret_cast(salt.data()), reinterpret_cast(password.data()), password.length(), rounds, m_key, m_iv)) { -// throw std::runtime_error("Couldn't start encryption routine. EVP_BytesToKey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); -// } - - /* Initialize encryption operation. */ - if (!EVP_EncryptInit_ex(encryptionCipher.get(), cipher, nullptr, m_key, m_iv)) { - throw std::runtime_error("Couldn't initialize encryption operation. EVP_EncryptInit_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* - * Provide the message to be encrypted, and obtain the encrypted output. - * EVP_EncryptUpdate can be called multiple times if necessary - */ - if (!EVP_EncryptUpdate(encryptionCipher.get(), cipherText.get(), &cipherTextLength, reinterpret_cast(data.data()), data.size())) { - throw std::runtime_error("Couldn't provide message to be encrypted. EVP_EncryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Finalize the encryption. Normally ciphertext bytes may be written at this stage */ - if (!EVP_EncryptFinal(encryptionCipher.get(), cipherText.get() + cipherTextLength, &finalLength)) { - throw std::runtime_error("Couldn't finalize encryption. EVP_EncryptFinal(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Finilize data to be readable with qt */ - QByteArray encryptedData = QByteArray(reinterpret_cast(cipherText.get()), cipherTextLength + finalLength); - - return encryptedData; - - } catch (std::exception& exception) { - QSimpleCrypto::QBlockCipher::error.setError(1, exception.what()); - return QByteArray(); - } catch (...) { - QSimpleCrypto::QBlockCipher::error.setError(2, "Unknown error!"); - return QByteArray(); - } - - return QByteArray(); -} - -/// -/// \brief QSimpleCrypto::QBlockCipher::encryptAesBlockCipher - Function decrypts data with Aes Block Cipher algorithm. -/// \param data - Data that will be decrypted. -/// \param key - AES key. -/// \param iv - Initialization vector. -/// \param password - Decryption password. -/// \param salt - Random delta. -/// \param rounds - Transformation rounds. -/// \param chiper - Can be used with OpenSSL EVP_CIPHER (ecb, cbc, cfb, ofb, ctr) - 128, 192, 256. Example: EVP_aes_256_cbc(). -/// \param md - Hash algroitm (OpenSSL EVP_MD). Example: EVP_sha512(). -/// \return Returns decrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QBlockCipher::decryptAesBlockCipher(QByteArray data, QByteArray key, - QByteArray iv, - const int& rounds, const EVP_CIPHER* cipher, const EVP_MD* md) -{ - try { - /* Initialize EVP_CIPHER_CTX */ - std::unique_ptr decryptionCipher { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; - if (decryptionCipher == nullptr) { - throw std::runtime_error("Couldn't initialize \'decryptionCipher\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Reinterpret values for multi use */ - unsigned char* m_key = reinterpret_cast(key.data()); - unsigned char* m_iv = reinterpret_cast(iv.data()); - - /* Set data length */ - int plainTextLength(data.size()); - int finalLength = 0; - - /* Initialize plainText. Here decrypted data will be stored */ - std::unique_ptr plainText { new unsigned char[plainTextLength + AES_BLOCK_SIZE]() }; - if (plainText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for \'plainText\'. EVP_CIPHER_CTX_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - // Bug here -// /* Start encryption with password based encryption routine */ -// if (!EVP_BytesToKey(cipher, md, reinterpret_cast(salt.data()), reinterpret_cast(password.data()), password.length(), rounds, m_key, m_iv)) { -// throw std::runtime_error("Couldn't start decryption routine. EVP_BytesToKey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); -// } - - /* Initialize decryption operation. */ - if (!EVP_DecryptInit_ex(decryptionCipher.get(), cipher, nullptr, m_key, m_iv)) { - throw std::runtime_error("Couldn't initialize decryption operation. EVP_DecryptInit_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* - * Provide the message to be decrypted, and obtain the plaintext output. - * EVP_DecryptUpdate can be called multiple times if necessary - */ - if (!EVP_DecryptUpdate(decryptionCipher.get(), plainText.get(), &plainTextLength, reinterpret_cast(data.data()), data.size())) { - throw std::runtime_error("Couldn't provide message to be decrypted. EVP_DecryptUpdate(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* - * Finalize the decryption. A positive return value indicates success, - * anything else is a failure - the plaintext is not trustworthy. - */ - if (!EVP_DecryptFinal(decryptionCipher.get(), plainText.get() + plainTextLength, &finalLength)) { - throw std::runtime_error("Couldn't finalize decryption. EVP_DecryptFinal. Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Finilize data to be readable with qt */ - QByteArray decryptedData = QByteArray(reinterpret_cast(plainText.get()), plainTextLength + finalLength); - - return decryptedData; - - } catch (std::exception& exception) { - QSimpleCrypto::QBlockCipher::error.setError(1, exception.what()); - return QByteArray(exception.what()); - } catch (...) { - QSimpleCrypto::QBlockCipher::error.setError(2, "Unknown error!"); - return QByteArray(); - } - - return QByteArray(); -} diff --git a/client/3rd/QSimpleCrypto/sources/QCryptoError.cpp b/client/3rd/QSimpleCrypto/sources/QCryptoError.cpp deleted file mode 100644 index 234f55d7..00000000 --- a/client/3rd/QSimpleCrypto/sources/QCryptoError.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "include/QCryptoError.h" - -QSimpleCrypto::QCryptoError::QCryptoError(QObject* parent) - : QObject(parent) -{ -} diff --git a/client/3rd/QSimpleCrypto/sources/QRsa.cpp b/client/3rd/QSimpleCrypto/sources/QRsa.cpp deleted file mode 100644 index 544d6746..00000000 --- a/client/3rd/QSimpleCrypto/sources/QRsa.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#include "include/QRsa.h" - -QSimpleCrypto::QRsa::QRsa() -{ -} - -/// -/// \brief QSimpleCrypto::QRSA::generateRsaKeys - Function generate Rsa Keys and returns them in OpenSSL structure. -/// \param bits - RSA key size. -/// \param rsaBigNumber - The exponent is an odd number, typically 3, 17 or 65537. -/// \return Returns 'OpenSSL RSA structure' or 'nullptr', if error happened. Returned value must be cleaned up with 'RSA_free()' to avoid memory leak. -/// -RSA* QSimpleCrypto::QRsa::generateRsaKeys(const int& bits, const int& rsaBigNumber) -{ - try { - /* Initialize big number */ - std::unique_ptr bigNumber { BN_new(), BN_free }; - if (bigNumber == nullptr) { - throw std::runtime_error("Couldn't initialize \'bigNumber\'. BN_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return nullptr; - } - - /* Set big number */ - if (!BN_set_word(bigNumber.get(), rsaBigNumber)) { - throw std::runtime_error("Couldn't set bigNumber. BN_set_word(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Initialize RSA */ - RSA* rsa = nullptr; - if (!(rsa = RSA_new())) { - throw std::runtime_error("Couldn't initialize x509. X509_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Generate key pair and store it in RSA */ - if (!RSA_generate_key_ex(rsa, bits, bigNumber.get(), nullptr)) { - throw std::runtime_error("Couldn't generate RSA. RSA_generate_key_ex(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - return rsa; - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return nullptr; - } -} - -/// -/// \brief QSimpleCrypto::QRSA::savePublicKey - Saves to file RSA public key. -/// \param rsa - OpenSSL RSA structure. -/// \param publicKeyFileName - Public key file name. -/// -void QSimpleCrypto::QRsa::savePublicKey(RSA* rsa, const QByteArray& publicKeyFileName) -{ - try { - /* Initialize BIO */ - std::unique_ptr bioPublicKey { BIO_new_file(publicKeyFileName.data(), "w+"), BIO_free_all }; - if (bioPublicKey == nullptr) { - throw std::runtime_error("Couldn't initialize \'bioPublicKey\'. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write public key on file */ - if (!PEM_write_bio_RSA_PUBKEY(bioPublicKey.get(), rsa)) { - throw std::runtime_error("Couldn't save public key. PEM_write_bio_RSAPublicKey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return; - } -} - -/// -/// \brief QSimpleCrypto::QRSA::savePrivateKey - Saves to file RSA private key. -/// \param rsa - OpenSSL RSA structure. -/// \param privateKeyFileName - Private key file name. -/// \param password - Private key password. -/// \param cipher - Can be used with 'OpenSSL EVP_CIPHER' (ecb, cbc, cfb, ofb, ctr) - 128, 192, 256. Example: EVP_aes_256_cbc(). -/// -void QSimpleCrypto::QRsa::savePrivateKey(RSA* rsa, const QByteArray& privateKeyFileName, QByteArray password, const EVP_CIPHER* cipher) -{ - try { - /* Initialize BIO */ - std::unique_ptr bioPrivateKey { BIO_new_file(privateKeyFileName.data(), "w+"), BIO_free_all }; - if (bioPrivateKey == nullptr) { - throw std::runtime_error("Couldn't initialize bioPrivateKey. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write private key to file */ - if (!PEM_write_bio_RSAPrivateKey(bioPrivateKey.get(), rsa, cipher, reinterpret_cast(password.data()), password.size(), nullptr, nullptr)) { - throw std::runtime_error("Couldn't save private key. PEM_write_bio_RSAPrivateKey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return; - } -} - -/// -/// \brief QSimpleCrypto::QRSA::getPublicKeyFromFile - Gets RSA public key from a file. -/// \param filePath - File path to public key file. -/// \return Returns 'OpenSSL EVP_PKEY structure' or 'nullptr', if error happened. Returned value must be cleaned up with 'EVP_PKEY_free()' to avoid memory leak. -/// -EVP_PKEY* QSimpleCrypto::QRsa::getPublicKeyFromFile(const QByteArray& filePath) -{ - try { - /* Initialize BIO */ - std::unique_ptr bioPublicKey { BIO_new_file(filePath.data(), "r"), BIO_free_all }; - if (bioPublicKey == nullptr) { - throw std::runtime_error("Couldn't initialize bioPublicKey. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Initialize EVP_PKEY */ - EVP_PKEY* keyStore = nullptr; - if (!(keyStore = EVP_PKEY_new())) { - throw std::runtime_error("Couldn't initialize keyStore. EVP_PKEY_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write private key to file */ - if (!PEM_read_bio_PUBKEY(bioPublicKey.get(), &keyStore, nullptr, nullptr)) { - throw std::runtime_error("Couldn't read private key. PEM_read_bio_PrivateKey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - return keyStore; - - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return nullptr; - } -} - -/// -/// \brief QSimpleCrypto::QRSA::getPrivateKeyFromFile - Gets RSA private key from a file. -/// \param filePath - File path to private key file. -/// \param password - Private key password. -/// \return - Returns 'OpenSSL EVP_PKEY structure' or 'nullptr', if error happened. Returned value must be cleaned up with 'EVP_PKEY_free()' to avoid memory leak. -/// -EVP_PKEY* QSimpleCrypto::QRsa::getPrivateKeyFromFile(const QByteArray& filePath, const QByteArray& password) -{ - try { - /* Initialize BIO */ - std::unique_ptr bioPrivateKey { BIO_new_file(filePath.data(), "r"), BIO_free_all }; - if (bioPrivateKey == nullptr) { - throw std::runtime_error("Couldn't initialize bioPrivateKey. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Initialize EVP_PKEY */ - EVP_PKEY* keyStore = nullptr; - if (!(keyStore = EVP_PKEY_new())) { - throw std::runtime_error("Couldn't initialize keyStore. EVP_PKEY_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write private key to file */ - if (!PEM_read_bio_PrivateKey(bioPrivateKey.get(), &keyStore, nullptr, (void*)password.data())) { - throw std::runtime_error("Couldn't read private key. PEM_read_bio_PrivateKey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - return keyStore; - - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return nullptr; - } -} - -/// -/// \brief QSimpleCrypto::QRSA::encrypt - Encrypt data with RSA algorithm. -/// \param plaintext - Text that must be encrypted. -/// \param rsa - OpenSSL RSA structure. -/// \param encryptType - Public or private encrypt type. (PUBLIC_ENCRYPT, PRIVATE_ENCRYPT). -/// \param padding - OpenSSL RSA padding can be used with: 'RSA_PKCS1_PADDING', 'RSA_NO_PADDING' and etc. -/// \return Returns encrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QRsa::encrypt(QByteArray plainText, RSA* rsa, const int& encryptType, const int& padding) -{ - try { - /* Initialize array. Here encrypted data will be saved */ - std::unique_ptr cipherText { new unsigned char[RSA_size(rsa)]() }; - if (cipherText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'cipherText'."); - } - - /* Result of encryption operation */ - short int result = 0; - - /* Execute encryption operation */ - if (encryptType == PublicDecrypt) { - result = RSA_public_encrypt(plainText.size(), reinterpret_cast(plainText.data()), cipherText.get(), rsa, padding); - } else if (encryptType == PrivateDecrypt) { - result = RSA_private_encrypt(plainText.size(), reinterpret_cast(plainText.data()), cipherText.get(), rsa, padding); - } - - /* Check for result */ - if (result <= -1) { - throw std::runtime_error("Couldn't encrypt data. Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Get encrypted data */ - const QByteArray& encryptedData = QByteArray(reinterpret_cast(cipherText.get()), RSA_size(rsa)); - - return encryptedData; - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return ""; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return ""; - } -} - -/// -/// \brief QSimpleCrypto::QRSA::decrypt - Decrypt data with RSA algorithm. -/// \param cipherText - Text that must be decrypted. -/// \param rsa - OpenSSL RSA structure. -/// \param decryptType - Public or private type. (PUBLIC_DECRYPT, PRIVATE_DECRYPT). -/// \param padding - RSA padding can be used with: 'RSA_PKCS1_PADDING', 'RSA_NO_PADDING' and etc. -/// \return - Returns decrypted data or "", if error happened. -/// -QByteArray QSimpleCrypto::QRsa::decrypt(QByteArray cipherText, RSA* rsa, const int& decryptType, const int& padding) -{ - try { - /* Initialize array. Here decrypted data will be saved */ - std::unique_ptr plainText { new unsigned char[cipherText.size()]() }; - if (plainText == nullptr) { - throw std::runtime_error("Couldn't allocate memory for 'plainText'."); - } - - /* Result of decryption operation */ - short int result = 0; - - /* Execute decryption operation */ - if (decryptType == PublicDecrypt) { - result = RSA_public_decrypt(RSA_size(rsa), reinterpret_cast(cipherText.data()), plainText.get(), rsa, padding); - } else if (decryptType == PrivateDecrypt) { - result = RSA_private_decrypt(RSA_size(rsa), reinterpret_cast(cipherText.data()), plainText.get(), rsa, padding); - } - - /* Check for result */ - if (result <= -1) { - throw std::runtime_error("Couldn't decrypt data. Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Get decrypted data */ - const QByteArray& decryptedData = QByteArray(reinterpret_cast(plainText.get())); - - return decryptedData; - } catch (std::exception& exception) { - QSimpleCrypto::QRsa::error.setError(1, exception.what()); - return ""; - } catch (...) { - QSimpleCrypto::QRsa::error.setError(2, "Unknown error!"); - return ""; - } -} diff --git a/client/3rd/QSimpleCrypto/sources/QX509.cpp b/client/3rd/QSimpleCrypto/sources/QX509.cpp deleted file mode 100644 index ac4fd270..00000000 --- a/client/3rd/QSimpleCrypto/sources/QX509.cpp +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#include "include/QX509.h" - -QSimpleCrypto::QX509::QX509() -{ -} - -/// -/// \brief QSimpleCrypto::QX509::loadCertificateFromFile - Function load X509 from file and returns OpenSSL structure. -/// \param fileName - File path to certificate. -/// \return Returns OpenSSL X509 structure or nullptr, if error happened. Returned value must be cleaned up with 'X509_free' to avoid memory leak. -/// -X509* QSimpleCrypto::QX509::loadCertificateFromFile(const QByteArray& fileName) -{ - try { - /* Initialize X509 */ - X509* x509 = nullptr; - if (!(x509 = X509_new())) { - throw std::runtime_error("Couldn't initialize X509. X509_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Initialize BIO */ - std::unique_ptr certFile { BIO_new_file(fileName.data(), "r+"), BIO_free_all }; - if (certFile == nullptr) { - throw std::runtime_error("Couldn't initialize certFile. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Read file */ - if (!PEM_read_bio_X509(certFile.get(), &x509, nullptr, nullptr)) { - throw std::runtime_error("Couldn't read certificate file from disk. PEM_read_bio_X509(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - return x509; - } catch (std::exception& exception) { - QSimpleCrypto::QX509::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QX509::error.setError(2, "Unknown error!"); - return nullptr; - } -} - -/// -/// \brief QSimpleCrypto::QX509::signCertificate - Function signs X509 certificate and returns signed X509 OpenSSL structure. -/// \param endCertificate - Certificate that will be signed -/// \param caCertificate - CA certificate that will sign end certificate -/// \param caPrivateKey - CA certificate private key -/// \param fileName - With that name certificate will be saved. Leave "", if don't need to save it -/// \return Returns OpenSSL X509 structure or nullptr, if error happened. -/// -X509* QSimpleCrypto::QX509::signCertificate(X509* endCertificate, X509* caCertificate, EVP_PKEY* caPrivateKey, const QByteArray& fileName) -{ - try { - /* Set issuer to CA's subject. */ - if (!X509_set_issuer_name(endCertificate, X509_get_subject_name(caCertificate))) { - throw std::runtime_error("Couldn't set issuer name for X509. X509_set_issuer_name(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Sign the certificate with key. */ - if (!X509_sign(endCertificate, caPrivateKey, EVP_sha256())) { - throw std::runtime_error("Couldn't sign X509. X509_sign(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write certificate file on disk. If needed */ - if (!fileName.isEmpty()) { - /* Initialize BIO */ - std::unique_ptr certFile { BIO_new_file(fileName.data(), "w+"), BIO_free_all }; - if (certFile == nullptr) { - throw std::runtime_error("Couldn't initialize certFile. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write file on disk */ - if (!PEM_write_bio_X509(certFile.get(), endCertificate)) { - throw std::runtime_error("Couldn't write certificate file on disk. PEM_write_bio_X509(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } - - return endCertificate; - } catch (std::exception& exception) { - QSimpleCrypto::QX509::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QX509::error.setError(2, "Unknown error!"); - return nullptr; - } -} - -/// -/// \brief QSimpleCrypto::QX509::verifyCertificate - Function verifies X509 certificate and returns verified X509 OpenSSL structure. -/// \param x509 - OpenSSL X509. That certificate will be verified. -/// \param store - Trusted certificate must be added to X509_Store with 'addCertificateToStore(X509_STORE* ctx, X509* x509)'. -/// \return Returns OpenSSL X509 structure or nullptr, if error happened -/// -X509* QSimpleCrypto::QX509::verifyCertificate(X509* x509, X509_STORE* store) -{ - try { - /* Initialize X509_STORE_CTX */ - std::unique_ptr ctx { X509_STORE_CTX_new(), X509_STORE_CTX_free }; - if (ctx == nullptr) { - throw std::runtime_error("Couldn't initialize keyStore. EVP_PKEY_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set up CTX for a subsequent verification operation */ - if (!X509_STORE_CTX_init(ctx.get(), store, x509, nullptr)) { - throw std::runtime_error("Couldn't initialize X509_STORE_CTX. X509_STORE_CTX_init(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Verify X509 */ - if (!X509_verify_cert(ctx.get())) { - throw std::runtime_error("Couldn't verify cert. X509_verify_cert(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - return x509; - } catch (std::exception& exception) { - QSimpleCrypto::QX509::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QX509::error.setError(2, "Unknown error!"); - return nullptr; - } -} - -/// -/// \brief QSimpleCrypto::QX509::generateSelfSignedCertificate - Function generatesand returns self signed X509. -/// \param rsa - OpenSSL RSA. -/// \param additionalData - Certificate information. -/// \param certificateFileName - With that name certificate will be saved. Leave "", if don't need to save it. -/// \param md - OpenSSL EVP_MD structure. Example: EVP_sha512(). -/// \param serialNumber - X509 certificate serial number. -/// \param version - X509 certificate version. -/// \param notBefore - X509 start date. -/// \param notAfter - X509 end date. -/// \return Returns OpenSSL X509 structure or nullptr, if error happened. Returned value must be cleaned up with 'X509_free' to avoid memory leak. -/// -X509* QSimpleCrypto::QX509::generateSelfSignedCertificate(RSA* rsa, const QMap& additionalData, - const QByteArray& certificateFileName, const EVP_MD* md, - const long& serialNumber, const long& version, - const long& notBefore, const long& notAfter) -{ - try { - /* Initialize X509 */ - X509* x509 = nullptr; - if (!(x509 = X509_new())) { - throw std::runtime_error("Couldn't initialize X509. X509_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Initialize EVP_PKEY */ - std::unique_ptr keyStore { EVP_PKEY_new(), EVP_PKEY_free }; - if (keyStore == nullptr) { - throw std::runtime_error("Couldn't initialize keyStore. EVP_PKEY_new(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Sign rsa key */ - if (!EVP_PKEY_assign_RSA(keyStore.get(), rsa)) { - throw std::runtime_error("Couldn't assign rsa. EVP_PKEY_assign_RSA(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set certificate serial number. */ - if (!ASN1_INTEGER_set(X509_get_serialNumber(x509), serialNumber)) { - throw std::runtime_error("Couldn't set serial number. ASN1_INTEGER_set(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set certificate version */ - if (!X509_set_version(x509, version)) { - throw std::runtime_error("Couldn't set version. X509_set_version(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Set certificate creation and expiration date */ - X509_gmtime_adj(X509_get_notBefore(x509), notBefore); - X509_gmtime_adj(X509_get_notAfter(x509), notAfter); - - /* Set certificate public key */ - if (!X509_set_pubkey(x509, keyStore.get())) { - throw std::runtime_error("Couldn't set public key. X509_set_pubkey(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Initialize X509_NAME */ - X509_NAME* x509Name = X509_get_subject_name(x509); - if (x509Name == nullptr) { - throw std::runtime_error("Couldn't initialize X509_NAME. X509_NAME(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Add additional data to certificate */ - QMapIterator certificateInformationList(additionalData); - while (certificateInformationList.hasNext()) { - /* Read next item in list */ - certificateInformationList.next(); - - /* Set additional data */ - if (!X509_NAME_add_entry_by_txt(x509Name, certificateInformationList.key().data(), MBSTRING_UTF8, reinterpret_cast(certificateInformationList.value().data()), -1, -1, 0)) { - throw std::runtime_error("Couldn't set additional information. X509_NAME_add_entry_by_txt(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } - - /* Set certificate info */ - if (!X509_set_issuer_name(x509, x509Name)) { - throw std::runtime_error("Couldn't set issuer name. X509_set_issuer_name(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Sign certificate */ - if (!X509_sign(x509, keyStore.get(), md)) { - throw std::runtime_error("Couldn't sign X509. X509_sign(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write certificate file on disk. If needed */ - if (!certificateFileName.isEmpty()) { - /* Initialize BIO */ - std::unique_ptr certFile { BIO_new_file(certificateFileName.data(), "w+"), BIO_free_all }; - if (certFile == nullptr) { - throw std::runtime_error("Couldn't initialize certFile. BIO_new_file(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - - /* Write file on disk */ - if (!PEM_write_bio_X509(certFile.get(), x509)) { - throw std::runtime_error("Couldn't write certificate file on disk. PEM_write_bio_X509(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - } - } - - return x509; - } catch (std::exception& exception) { - QSimpleCrypto::QX509::error.setError(1, exception.what()); - return nullptr; - } catch (...) { - QSimpleCrypto::QX509::error.setError(2, "Unknown error!"); - return nullptr; - } -} diff --git a/client/3rd/QSimpleCrypto/sources/QX509Store.cpp b/client/3rd/QSimpleCrypto/sources/QX509Store.cpp deleted file mode 100644 index bbbec1a8..00000000 --- a/client/3rd/QSimpleCrypto/sources/QX509Store.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright 2021 BrutalWizard (https://github.com/bru74lw1z4rd). All Rights Reserved. - * - * Licensed under the Apache License 2.0 (the "License"). You may not use - * this file except in compliance with the License. You can obtain a copy - * in the file LICENSE in the source distribution -**/ - -#include "include/QX509Store.h" - -QSimpleCrypto::QX509Store::QX509Store() -{ -} - -/// -/// \brief QSimpleCrypto::QX509::addCertificateToStore -/// \param store - OpenSSL X509_STORE. -/// \param x509 - OpenSSL X509. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::addCertificateToStore(X509_STORE* store, X509* x509) -{ - if (!X509_STORE_add_cert(store, x509)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't add certificate to X509_STORE. X509_STORE_add_cert(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::addLookup -/// \param store - OpenSSL X509_STORE. -/// \param method - OpenSSL X509_LOOKUP_METHOD. Example: X509_LOOKUP_file. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::addLookup(X509_STORE* store, X509_LOOKUP_METHOD* method) -{ - if (!X509_STORE_add_lookup(store, method)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't add lookup to X509_STORE. X509_STORE_add_lookup(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::setCertificateDepth -/// \param store - OpenSSL X509_STORE. -/// \param depth - That is the maximum number of untrusted CA certificates that can appear in a chain. Example: 0. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::setDepth(X509_STORE* store, const int& depth) -{ - if (!X509_STORE_set_depth(store, depth)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't set depth for X509_STORE. X509_STORE_set_depth(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::setFlag -/// \param store - OpenSSL X509_STORE. -/// \param flag - The verification flags consists of zero or more of the following flags ored together. Example: X509_V_FLAG_CRL_CHECK. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::setFlag(X509_STORE* store, const unsigned long& flag) -{ - if (!X509_STORE_set_flags(store, flag)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't set flag for X509_STORE. X509_STORE_set_flags(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::setFlag -/// \param store - OpenSSL X509_STORE. -/// \param purpose - Verification purpose in param to purpose. Example: X509_PURPOSE_ANY. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::setPurpose(X509_STORE* store, const int& purpose) -{ - if (!X509_STORE_set_purpose(store, purpose)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't set purpose for X509_STORE. X509_STORE_set_purpose(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::setTrust -/// \param store - OpenSSL X509_STORE. -/// \param trust - Trust Level. Example: X509_TRUST_SSL_SERVER. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::setTrust(X509_STORE* store, const int& trust) -{ - if (!X509_STORE_set_trust(store, trust)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't set trust for X509_STORE. X509_STORE_set_trust(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::setDefaultPaths -/// \param store - OpenSSL X509_STORE. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::setDefaultPaths(X509_STORE* store) -{ - if (!X509_STORE_set_default_paths(store)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't set default paths for X509_STORE. X509_STORE_set_default_paths(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::loadLocations -/// \param store - OpenSSL X509_STORE. -/// \param fileName - File name. Example: "caCertificate.pem". -/// \param dirPath - Path to file. Example: "path/To/File". -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::loadLocations(X509_STORE* store, const QByteArray& fileName, const QByteArray& dirPath) -{ - if (!X509_STORE_load_locations(store, fileName, dirPath)) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't load locations for X509_STORE. X509_STORE_load_locations(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::loadLocations -/// \param store - OpenSSL X509_STORE. -/// \param file - Qt QFile that will be loaded. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::loadLocations(X509_STORE* store, const QFile& file) -{ - /* Initialize QFileInfo to read information about file */ - QFileInfo info(file); - - if (!X509_STORE_load_locations(store, info.fileName().toLocal8Bit(), info.absoluteDir().path().toLocal8Bit())) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't load locations for X509_STORE. X509_STORE_load_locations(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} - -/// -/// \brief QSimpleCrypto::QX509Store::loadLocations -/// \param store - OpenSSL X509_STORE. -/// \param fileInfo - Qt QFileInfo. -/// \return Returns 'true' on success and 'false', if error happened. -/// -bool QSimpleCrypto::QX509Store::loadLocations(X509_STORE* store, const QFileInfo& fileInfo) -{ - if (!X509_STORE_load_locations(store, fileInfo.fileName().toLocal8Bit(), fileInfo.absoluteDir().path().toLocal8Bit())) { - QSimpleCrypto::QX509Store::error.setError(1, "Couldn't load locations for X509_STORE. X509_STORE_load_locations(). Error: " + QByteArray(ERR_error_string(ERR_get_error(), nullptr))); - return false; - } - - return true; -} diff --git a/client/3rd/SingleApplication/singleapplication.cmake b/client/3rd/SingleApplication/singleapplication.cmake deleted file mode 100644 index 78abfa8a..00000000 --- a/client/3rd/SingleApplication/singleapplication.cmake +++ /dev/null @@ -1,25 +0,0 @@ -include_directories(${CMAKE_CURRENT_LIST_DIR}) - -find_package(Qt6 REQUIRED COMPONENTS - Core Network -) -set(LIBS ${LIBS} Qt6::Core Qt6::Network) - - -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/singleapplication.h - ${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.h -) - -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/singleapplication.cpp - ${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.cpp -) - -if(WIN32) - if(MSVC) - set(LIBS ${LIBS} Advapi32.lib) - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - set(LIBS ${LIBS} advapi32) - endif() -endif() diff --git a/client/3rd/SingleApplication/singleapplication.cpp b/client/3rd/SingleApplication/singleapplication.cpp deleted file mode 100644 index 7e153a00..00000000 --- a/client/3rd/SingleApplication/singleapplication.cpp +++ /dev/null @@ -1,274 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2020 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#include -#include -#include - -#include "singleapplication.h" -#include "singleapplication_p.h" - -/** - * @brief Constructor. Checks and fires up LocalServer or closes the program - * if another instance already exists - * @param argc - * @param argv - * @param allowSecondary Whether to enable secondary instance support - * @param options Optional flags to toggle specific behaviour - * @param timeout Maximum time blocking functions are allowed during app load - */ -SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData ) - : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) -{ - Q_D( SingleApplication ); - -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - // On Android and iOS since the library is not supported fallback to - // standard QApplication behaviour by simply returning at this point. - qWarning() << "SingleApplication is not supported on Android and iOS systems."; - return; -#endif - - // Store the current mode of the program - d->options = options; - - // Add any unique user data - if ( ! userData.isEmpty() ) - d->addAppData( userData ); - - // Generating an application ID used for identifying the shared memory - // block and QLocalServer - d->genBlockServerName(); - - // To mitigate QSharedMemory issues with large amount of processes - // attempting to attach at the same time - SingleApplicationPrivate::randomSleep(); - -#ifdef Q_OS_UNIX - // By explicitly attaching it and then deleting it we make sure that the - // memory is deleted even after the process has crashed on Unix. - d->memory = new QSharedMemory( d->blockServerName ); - d->memory->attach(); - delete d->memory; -#endif - // Guarantee thread safe behaviour with a shared memory block. - d->memory = new QSharedMemory( d->blockServerName ); - - // Create a shared memory block - if( d->memory->create( sizeof( InstancesInfo ) )){ - // Initialize the shared memory block - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory block after create."; - abortSafely(); - } - d->initializeMemoryBlock(); - } else { - if( d->memory->error() == QSharedMemory::AlreadyExists ){ - // Attempt to attach to the memory segment - if( ! d->memory->attach() ){ - qCritical() << "SingleApplication: Unable to attach to shared memory block."; - abortSafely(); - } - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory block after attach."; - abortSafely(); - } - } else { - qCritical() << "SingleApplication: Unable to create block."; - abortSafely(); - } - } - - auto *inst = static_cast( d->memory->data() ); - QElapsedTimer time; - time.start(); - - // Make sure the shared memory block is initialised and in consistent state - while( true ){ - // If the shared memory block's checksum is valid continue - if( d->blockChecksum() == inst->checksum ) break; - - // If more than 5s have elapsed, assume the primary instance crashed and - // assume it's position - if( time.elapsed() > 5000 ){ - qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure."; - d->initializeMemoryBlock(); - } - - // Otherwise wait for a random period and try again. The random sleep here - // limits the probability of a collision between two racing apps and - // allows the app to initialise faster - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory for random wait."; - qDebug() << d->memory->errorString(); - } - SingleApplicationPrivate::randomSleep(); - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory after random wait."; - abortSafely(); - } - } - - if( inst->primary == false ){ - d->startPrimary(); - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory after primary start."; - qDebug() << d->memory->errorString(); - } - return; - } - - // Check if another instance can be started - if( allowSecondary ){ - d->startSecondary(); - if( d->options & Mode::SecondaryNotification ){ - d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); - } - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory after secondary start."; - qDebug() << d->memory->errorString(); - } - return; - } - - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; - qDebug() << d->memory->errorString(); - } - - d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); - - delete d; - - ::exit( EXIT_SUCCESS ); -} - -SingleApplication::~SingleApplication() -{ - Q_D( SingleApplication ); - delete d; -} - -/** - * Checks if the current application instance is primary. - * @return Returns true if the instance is primary, false otherwise. - */ -bool SingleApplication::isPrimary() const -{ - Q_D( const SingleApplication ); - return d->server != nullptr; -} - -/** - * Checks if the current application instance is secondary. - * @return Returns true if the instance is secondary, false otherwise. - */ -bool SingleApplication::isSecondary() const -{ - Q_D( const SingleApplication ); - return d->server == nullptr; -} - -/** - * Allows you to identify an instance by returning unique consecutive instance - * ids. It is reset when the first (primary) instance of your app starts and - * only incremented afterwards. - * @return Returns a unique instance id. - */ -quint32 SingleApplication::instanceId() const -{ - Q_D( const SingleApplication ); - return d->instanceNumber; -} - -/** - * Returns the OS PID (Process Identifier) of the process running the primary - * instance. Especially useful when SingleApplication is coupled with OS. - * specific APIs. - * @return Returns the primary instance PID. - */ -qint64 SingleApplication::primaryPid() const -{ - Q_D( const SingleApplication ); - return d->primaryPid(); -} - -/** - * Returns the username the primary instance is running as. - * @return Returns the username the primary instance is running as. - */ -QString SingleApplication::primaryUser() const -{ - Q_D( const SingleApplication ); - return d->primaryUser(); -} - -/** - * Returns the username the current instance is running as. - * @return Returns the username the current instance is running as. - */ -QString SingleApplication::currentUser() const -{ - return SingleApplicationPrivate::getUsername(); -} - -/** - * Sends message to the Primary Instance. - * @param message The message to send. - * @param timeout the maximum timeout in milliseconds for blocking functions. - * @return true if the message was sent successfuly, false otherwise. - */ -bool SingleApplication::sendMessage( const QByteArray &message, int timeout ) -{ - Q_D( SingleApplication ); - - // Nobody to connect to - if( isPrimary() ) return false; - - // Make sure the socket is connected - if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) ) - return false; - - d->socket->write( message ); - bool dataWritten = d->socket->waitForBytesWritten( timeout ); - d->socket->flush(); - return dataWritten; -} - -/** - * Cleans up the shared memory block and exits with a failure. - * This function halts program execution. - */ -void SingleApplication::abortSafely() -{ - Q_D( SingleApplication ); - - qCritical() << "SingleApplication: " << d->memory->error() << d->memory->errorString(); - delete d; - ::exit( EXIT_FAILURE ); -} - -QStringList SingleApplication::userData() const -{ - Q_D( const SingleApplication ); - return d->appData(); -} diff --git a/client/3rd/SingleApplication/singleapplication.h b/client/3rd/SingleApplication/singleapplication.h deleted file mode 100644 index 400c88ac..00000000 --- a/client/3rd/SingleApplication/singleapplication.h +++ /dev/null @@ -1,154 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2018 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#ifndef SINGLE_APPLICATION_H -#define SINGLE_APPLICATION_H - -#include -#include - -#ifndef QAPPLICATION_CLASS - #define QAPPLICATION_CLASS QApplication -#endif - -#include QT_STRINGIFY(QAPPLICATION_CLASS) - -class SingleApplicationPrivate; - -/** - * @brief The SingleApplication class handles multiple instances of the same - * Application - * @see QCoreApplication - */ -class SingleApplication : public QAPPLICATION_CLASS -{ - Q_OBJECT - - using app_t = QAPPLICATION_CLASS; - -public: - /** - * @brief Mode of operation of SingleApplication. - * Whether the block should be user-wide or system-wide and whether the - * primary instance should be notified when a secondary instance had been - * started. - * @note Operating system can restrict the shared memory blocks to the same - * user, in which case the User/System modes will have no effect and the - * block will be user wide. - * @enum - */ - enum Mode { - User = 1 << 0, - System = 1 << 1, - SecondaryNotification = 1 << 2, - ExcludeAppVersion = 1 << 3, - ExcludeAppPath = 1 << 4 - }; - Q_DECLARE_FLAGS(Options, Mode) - - /** - * @brief Intitializes a SingleApplication instance with argc command line - * arguments in argv - * @arg {int &} argc - Number of arguments in argv - * @arg {const char *[]} argv - Supplied command line arguments - * @arg {bool} allowSecondary - Whether to start the instance as secondary - * if there is already a primary instance. - * @arg {Mode} mode - Whether for the SingleApplication block to be applied - * User wide or System wide. - * @arg {int} timeout - Timeout to wait in milliseconds. - * @note argc and argv may be changed as Qt removes arguments that it - * recognizes - * @note Mode::SecondaryNotification only works if set on both the primary - * instance and the secondary instance. - * @note The timeout is just a hint for the maximum time of blocking - * operations. It does not guarantee that the SingleApplication - * initialisation will be completed in given time, though is a good hint. - * Usually 4*timeout would be the worst case (fail) scenario. - * @see See the corresponding QAPPLICATION_CLASS constructor for reference - */ - explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, const QString &userData = {} ); - ~SingleApplication() override; - - /** - * @brief Returns if the instance is the primary instance - * @returns {bool} - */ - bool isPrimary() const; - - /** - * @brief Returns if the instance is a secondary instance - * @returns {bool} - */ - bool isSecondary() const; - - /** - * @brief Returns a unique identifier for the current instance - * @returns {qint32} - */ - quint32 instanceId() const; - - /** - * @brief Returns the process ID (PID) of the primary instance - * @returns {qint64} - */ - qint64 primaryPid() const; - - /** - * @brief Returns the username of the user running the primary instance - * @returns {QString} - */ - QString primaryUser() const; - - /** - * @brief Returns the username of the current user - * @returns {QString} - */ - QString currentUser() const; - - /** - * @brief Sends a message to the primary instance. Returns true on success. - * @param {int} timeout - Timeout for connecting - * @returns {bool} - * @note sendMessage() will return false if invoked from the primary - * instance. - */ - bool sendMessage( const QByteArray &message, int timeout = 100 ); - - /** - * @brief Get the set user data. - * @returns {QStringList} - */ - QStringList userData() const; - -Q_SIGNALS: - void instanceStarted(); - void receivedMessage( quint32 instanceId, QByteArray message ); - -private: - SingleApplicationPrivate *d_ptr; - Q_DECLARE_PRIVATE(SingleApplication) - void abortSafely(); -}; - -Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) - -#endif // SINGLE_APPLICATION_H diff --git a/client/3rd/SingleApplication/singleapplication.pri b/client/3rd/SingleApplication/singleapplication.pri deleted file mode 100644 index 80283fc4..00000000 --- a/client/3rd/SingleApplication/singleapplication.pri +++ /dev/null @@ -1,15 +0,0 @@ -QT += core network -CONFIG += c++11 - -HEADERS += \ - $$PWD/singleapplication.h \ - $$PWD/singleapplication_p.h -SOURCES += $$PWD/singleapplication.cpp \ - $$PWD/singleapplication_p.cpp - -INCLUDEPATH += $$PWD - -win32 { - msvc:LIBS += Advapi32.lib - gcc:LIBS += -ladvapi32 -} diff --git a/client/3rd/SingleApplication/singleapplication_p.cpp b/client/3rd/SingleApplication/singleapplication_p.cpp deleted file mode 100644 index e65bd955..00000000 --- a/client/3rd/SingleApplication/singleapplication_p.cpp +++ /dev/null @@ -1,486 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2020 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// -// W A R N I N G !!! -// ----------------- -// -// This file is not part of the SingleApplication API. It is used purely as an -// implementation detail. This header file may change from version to -// version without notice, or may even be removed. -// - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) -#include -#else -#include -#endif - -#include "singleapplication.h" -#include "singleapplication_p.h" - -#ifdef Q_OS_UNIX - #include - #include - #include -#endif - -#ifdef Q_OS_WIN - #ifndef NOMINMAX - #define NOMINMAX 1 - #endif - #include - #include -#endif - -SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) - : q_ptr( q_ptr ) -{ - server = nullptr; - socket = nullptr; - memory = nullptr; - instanceNumber = 0; -} - -SingleApplicationPrivate::~SingleApplicationPrivate() -{ - if( socket != nullptr ){ - socket->close(); - delete socket; - } - - if( memory != nullptr ){ - memory->lock(); - auto *inst = static_cast(memory->data()); - if( server != nullptr ){ - server->close(); - delete server; - inst->primary = false; - inst->primaryPid = -1; - inst->primaryUser[0] = '\0'; - inst->checksum = blockChecksum(); - } - memory->unlock(); - - delete memory; - } -} - -QString SingleApplicationPrivate::getUsername() -{ -#ifdef Q_OS_WIN - wchar_t username[UNLEN + 1]; - // Specifies size of the buffer on input - DWORD usernameLength = UNLEN + 1; - if( GetUserNameW( username, &usernameLength ) ) - return QString::fromWCharArray( username ); -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - return QString::fromLocal8Bit( qgetenv( "USERNAME" ) ); -#else - return qEnvironmentVariable( "USERNAME" ); -#endif -#endif -#ifdef Q_OS_UNIX - QString username; - uid_t uid = geteuid(); - struct passwd *pw = getpwuid( uid ); - if( pw ) - username = QString::fromLocal8Bit( pw->pw_name ); - if ( username.isEmpty() ){ -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - username = QString::fromLocal8Bit( qgetenv( "USER" ) ); -#else - username = qEnvironmentVariable( "USER" ); -#endif - } - return username; -#endif -} - -void SingleApplicationPrivate::genBlockServerName() -{ - QCryptographicHash appData( QCryptographicHash::Sha256 ); - appData.addData( "SingleApplication", 17 ); - appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); - appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); - appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); - - if ( ! appDataList.isEmpty() ) - appData.addData( appDataList.join( "" ).toUtf8() ); - - if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){ - appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); - } - - if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){ -#ifdef Q_OS_WIN - appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); -#else - appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); -#endif - } - - // User level block requires a user specific data in the hash - if( options & SingleApplication::Mode::User ){ - appData.addData( getUsername().toUtf8() ); - } - - // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with - // server naming requirements. - blockServerName = appData.result().toBase64().replace("/", "_"); -} - -void SingleApplicationPrivate::initializeMemoryBlock() const -{ - auto *inst = static_cast( memory->data() ); - inst->primary = false; - inst->secondary = 0; - inst->primaryPid = -1; - inst->primaryUser[0] = '\0'; - inst->checksum = blockChecksum(); -} - -void SingleApplicationPrivate::startPrimary() -{ - // Reset the number of connections - auto *inst = static_cast ( memory->data() ); - - inst->primary = true; - inst->primaryPid = QCoreApplication::applicationPid(); - qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) ); - inst->checksum = blockChecksum(); - instanceNumber = 0; - // Successful creation means that no main process exists - // So we start a QLocalServer to listen for connections - QLocalServer::removeServer( blockServerName ); - server = new QLocalServer(); - - // Restrict access to the socket according to the - // SingleApplication::Mode::User flag on User level or no restrictions - if( options & SingleApplication::Mode::User ){ - server->setSocketOptions( QLocalServer::UserAccessOption ); - } else { - server->setSocketOptions( QLocalServer::WorldAccessOption ); - } - - server->listen( blockServerName ); - QObject::connect( - server, - &QLocalServer::newConnection, - this, - &SingleApplicationPrivate::slotConnectionEstablished - ); -} - -void SingleApplicationPrivate::startSecondary() -{ - auto *inst = static_cast ( memory->data() ); - - inst->secondary += 1; - inst->checksum = blockChecksum(); - instanceNumber = inst->secondary; -} - -bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) -{ - QElapsedTimer time; - time.start(); - - // Connect to the Local Server of the Primary Instance if not already - // connected. - if( socket == nullptr ){ - socket = new QLocalSocket(); - } - - if( socket->state() == QLocalSocket::ConnectedState ) return true; - - if( socket->state() != QLocalSocket::ConnectedState ){ - - while( true ){ - randomSleep(); - - if( socket->state() != QLocalSocket::ConnectingState ) - socket->connectToServer( blockServerName ); - - if( socket->state() == QLocalSocket::ConnectingState ){ - socket->waitForConnected( static_cast(msecs - time.elapsed()) ); - } - - // If connected break out of the loop - if( socket->state() == QLocalSocket::ConnectedState ) break; - - // If elapsed time since start is longer than the method timeout return - if( time.elapsed() >= msecs ) return false; - } - } - - // Initialisation message according to the SingleApplication protocol - QByteArray initMsg; - QDataStream writeStream(&initMsg, QIODevice::WriteOnly); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - writeStream.setVersion(QDataStream::Qt_5_6); -#endif - - writeStream << blockServerName.toLatin1(); - writeStream << static_cast(connectionType); - writeStream << instanceNumber; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum(QByteArray(initMsg, static_cast(initMsg.length()))); -#else - quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); -#endif - writeStream << checksum; - - // The header indicates the message length that follows - QByteArray header; - QDataStream headerStream(&header, QIODevice::WriteOnly); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion(QDataStream::Qt_5_6); -#endif - headerStream << static_cast ( initMsg.length() ); - - socket->write( header ); - socket->write( initMsg ); - bool result = socket->waitForBytesWritten( static_cast(msecs - time.elapsed()) ); - socket->flush(); - return result; -} - -quint16 SingleApplicationPrivate::blockChecksum() const -{ -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum(QByteArray(static_cast(memory->constData()), offsetof(InstancesInfo, checksum))); -#else - quint16 checksum = qChecksum(static_cast(memory->constData()), offsetof(InstancesInfo, checksum)); -#endif - return checksum; -} - -qint64 SingleApplicationPrivate::primaryPid() const -{ - qint64 pid; - - memory->lock(); - auto *inst = static_cast( memory->data() ); - pid = inst->primaryPid; - memory->unlock(); - - return pid; -} - -QString SingleApplicationPrivate::primaryUser() const -{ - QByteArray username; - - memory->lock(); - auto *inst = static_cast( memory->data() ); - username = inst->primaryUser; - memory->unlock(); - - return QString::fromUtf8( username ); -} - -/** - * @brief Executed when a connection has been made to the LocalServer - */ -void SingleApplicationPrivate::slotConnectionEstablished() -{ - QLocalSocket *nextConnSocket = server->nextPendingConnection(); - connectionMap.insert(nextConnSocket, ConnectionInfo()); - - QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, - [nextConnSocket, this](){ - auto &info = connectionMap[nextConnSocket]; - Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); - } - ); - - QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); - - QObject::connect(nextConnSocket, &QLocalSocket::destroyed, - [nextConnSocket, this](){ - connectionMap.remove(nextConnSocket); - } - ); - - QObject::connect(nextConnSocket, &QLocalSocket::readyRead, - [nextConnSocket, this](){ - auto &info = connectionMap[nextConnSocket]; - switch(info.stage){ - case StageHeader: - readInitMessageHeader(nextConnSocket); - break; - case StageBody: - readInitMessageBody(nextConnSocket); - break; - case StageConnected: - Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId ); - break; - default: - break; - }; - } - ); -} - -void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) -{ - if (!connectionMap.contains( sock )){ - return; - } - - if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){ - return; - } - - QDataStream headerStream( sock ); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion( QDataStream::Qt_5_6 ); -#endif - - // Read the header to know the message length - quint64 msgLen = 0; - headerStream >> msgLen; - ConnectionInfo &info = connectionMap[sock]; - info.stage = StageBody; - info.msgLen = msgLen; - - if ( sock->bytesAvailable() >= (qint64) msgLen ){ - readInitMessageBody( sock ); - } -} - -void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) -{ - Q_Q(SingleApplication); - - if (!connectionMap.contains( sock )){ - return; - } - - ConnectionInfo &info = connectionMap[sock]; - if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ - return; - } - - // Read the message body - QByteArray msgBytes = sock->read(info.msgLen); - QDataStream readStream(msgBytes); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - readStream.setVersion( QDataStream::Qt_5_6 ); -#endif - - // server name - QByteArray latin1Name; - readStream >> latin1Name; - - // connection type - ConnectionType connectionType = InvalidConnection; - quint8 connTypeVal = InvalidConnection; - readStream >> connTypeVal; - connectionType = static_cast ( connTypeVal ); - - // instance id - quint32 instanceId = 0; - readStream >> instanceId; - - // checksum - quint16 msgChecksum = 0; - readStream >> msgChecksum; - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast(msgBytes.length() - sizeof(quint16)))); -#else - const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16))); -#endif - - bool isValid = readStream.status() == QDataStream::Ok && - QLatin1String(latin1Name) == blockServerName && - msgChecksum == actualChecksum; - - if( !isValid ){ - sock->close(); - return; - } - - info.instanceId = instanceId; - info.stage = StageConnected; - - if( connectionType == NewInstance || - ( connectionType == SecondaryInstance && - options & SingleApplication::Mode::SecondaryNotification ) ) - { - Q_EMIT q->instanceStarted(); - } - - if (sock->bytesAvailable() > 0){ - Q_EMIT this->slotDataAvailable( sock, instanceId ); - } -} - -void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) -{ - Q_Q(SingleApplication); - Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); -} - -void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) -{ - if( closedSocket->bytesAvailable() > 0 ) - Q_EMIT slotDataAvailable( closedSocket, instanceId ); -} - -void SingleApplicationPrivate::randomSleep() -{ -#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 ) - QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u )); -#else - qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits::max() ); - QThread::msleep( 8 + static_cast ( static_cast ( qrand() ) / RAND_MAX * 10 )); -#endif -} - -void SingleApplicationPrivate::addAppData(const QString &data) -{ - appDataList.push_back(data); -} - -QStringList SingleApplicationPrivate::appData() const -{ - return appDataList; -} diff --git a/client/3rd/SingleApplication/singleapplication_p.h b/client/3rd/SingleApplication/singleapplication_p.h deleted file mode 100644 index c49a46dd..00000000 --- a/client/3rd/SingleApplication/singleapplication_p.h +++ /dev/null @@ -1,104 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) Itay Grudev 2015 - 2020 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// -// W A R N I N G !!! -// ----------------- -// -// This file is not part of the SingleApplication API. It is used purely as an -// implementation detail. This header file may change from version to -// version without notice, or may even be removed. -// - -#ifndef SINGLEAPPLICATION_P_H -#define SINGLEAPPLICATION_P_H - -#include -#include -#include -#include "singleapplication.h" - -struct InstancesInfo { - bool primary; - quint32 secondary; - qint64 primaryPid; - char primaryUser[128]; - quint16 checksum; // Must be the last field -}; - -struct ConnectionInfo { - qint64 msgLen = 0; - quint32 instanceId = 0; - quint8 stage = 0; -}; - -class SingleApplicationPrivate : public QObject { -Q_OBJECT -public: - enum ConnectionType : quint8 { - InvalidConnection = 0, - NewInstance = 1, - SecondaryInstance = 2, - Reconnect = 3 - }; - enum ConnectionStage : quint8 { - StageHeader = 0, - StageBody = 1, - StageConnected = 2, - }; - Q_DECLARE_PUBLIC(SingleApplication) - - SingleApplicationPrivate( SingleApplication *q_ptr ); - ~SingleApplicationPrivate() override; - - static QString getUsername(); - void genBlockServerName(); - void initializeMemoryBlock() const; - void startPrimary(); - void startSecondary(); - bool connectToPrimary( int msecs, ConnectionType connectionType ); - quint16 blockChecksum() const; - qint64 primaryPid() const; - QString primaryUser() const; - void readInitMessageHeader(QLocalSocket *socket); - void readInitMessageBody(QLocalSocket *socket); - static void randomSleep(); - void addAppData(const QString &data); - QStringList appData() const; - - SingleApplication *q_ptr; - QSharedMemory *memory; - QLocalSocket *socket; - QLocalServer *server; - quint32 instanceNumber; - QString blockServerName; - SingleApplication::Options options; - QMap connectionMap; - QStringList appDataList; - -public Q_SLOTS: - void slotConnectionEstablished(); - void slotDataAvailable( QLocalSocket*, quint32 ); - void slotClientConnectionClosed( QLocalSocket*, quint32 ); -}; - -#endif // SINGLEAPPLICATION_P_H diff --git a/client/3rd/amneziawg-apple b/client/3rd/amneziawg-apple index 0829e99e..76e7db55 160000 --- a/client/3rd/amneziawg-apple +++ b/client/3rd/amneziawg-apple @@ -1 +1 @@ -Subproject commit 0829e99ea9f4508fd1d4742546b62145d17587bb +Subproject commit 76e7db556a6d7e2582f9481df91db188a46c009c diff --git a/client/3rd/qtkeychain b/client/3rd/qtkeychain index 74776e2a..7460df6a 160000 --- a/client/3rd/qtkeychain +++ b/client/3rd/qtkeychain @@ -1 +1 @@ -Subproject commit 74776e2a3e2d98d19943e0968901c5b5e04cc1bd +Subproject commit 7460df6a978669290de5b56c2d98b199b61c3f88 diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 1fc28b82..72adaf25 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -24,6 +24,13 @@ execute_process( add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") +add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") +add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") + +add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") +add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}") +add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}") + if(IOS) set(PACKAGES ${PACKAGES} Multimedia) endif() @@ -34,7 +41,7 @@ endif() find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES}) -set(LIBS ${LIBS} +set(LIBS ${LIBS} Qt6::Core Qt6::Gui Qt6::Network Qt6::Xml Qt6::RemoteObjects Qt6::Quick Qt6::Svg Qt6::QuickControls2 @@ -55,6 +62,7 @@ qt_add_executable(${PROJECT} MANUAL_FINALIZATION) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep) qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep) + qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep) endif() qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc) @@ -88,11 +96,6 @@ configure_file(${CMAKE_CURRENT_LIST_DIR}/translations/translations.qrc.in ${CMAK qt6_add_resources(QRC ${I18NQRC} ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc) # -- i18n end -if(IOS) - execute_process(COMMAND bash ${CMAKE_CURRENT_LIST_DIR}/ios/scripts/openvpn.sh args - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) -endif() - set(IS_CI ${CI}) if(IS_CI) message("Detected CI env") @@ -102,169 +105,29 @@ if(IS_CI) endif() endif() - include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake) include_directories( ${CMAKE_CURRENT_LIST_DIR}/../ipc + ${CMAKE_CURRENT_LIST_DIR}/../common/logger ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) -configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) - -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/migrations.h - ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc.h - ${CMAKE_CURRENT_LIST_DIR}/amnezia_application.h - ${CMAKE_CURRENT_LIST_DIR}/containers/containers_defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.h - ${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.h - ${CMAKE_CURRENT_LIST_DIR}/core/server_defs.h - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/apiController.h - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.h - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h - ${CMAKE_CURRENT_LIST_DIR}/ui/pages.h - ${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h - ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h - ${CMAKE_CURRENT_BINARY_DIR}/version.h - ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.h - ${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.h - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h -) - -# Mozilla headres -set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/mozilla/models/server.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/ipaddress.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/leakdetector.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/controllerimpl.h - ${CMAKE_CURRENT_LIST_DIR}/mozilla/localsocketcontroller.h -) - include_directories(mozilla) include_directories(mozilla/shared) include_directories(mozilla/models) -if(NOT IOS) - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.h - ) -endif() - -if(NOT ANDROID) - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h - ) -endif() - -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/migrations.cpp - ${CMAKE_CURRENT_LIST_DIR}/amnezia_application.cpp - ${CMAKE_CURRENT_LIST_DIR}/containers/containers_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/scripts_registry.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/server_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/apiController.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/outbound.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/inbound.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/ss.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/ssd.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vless.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp -) - -# Mozilla sources -set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/mozilla/models/server.cpp - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/ipaddress.cpp - ${CMAKE_CURRENT_LIST_DIR}/mozilla/shared/leakdetector.cpp - ${CMAKE_CURRENT_LIST_DIR}/mozilla/localsocketcontroller.cpp -) +configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(${PROJECT} PRIVATE "MZ_DEBUG") endif() -if(NOT IOS) - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.cpp - ) -endif() - -if(NOT ANDROID) - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp - ) -endif() - -file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.h) -file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.cpp) - -file(GLOB_RECURSE PAGE_LOGIC_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/pages_logic/*.h) -file(GLOB_RECURSE PAGE_LOGIC_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/pages_logic/*.cpp) - -file(GLOB CONFIGURATORS_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/configurators/*.h) -file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/configurators/*.cpp) - -file(GLOB UI_MODELS_H CONFIGURE_DEPENDS - ${CMAKE_CURRENT_LIST_DIR}/ui/models/*.h - ${CMAKE_CURRENT_LIST_DIR}/ui/models/protocols/*.h - ${CMAKE_CURRENT_LIST_DIR}/ui/models/services/*.h -) -file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS - ${CMAKE_CURRENT_LIST_DIR}/ui/models/*.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/models/protocols/*.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/models/services/*.cpp -) - -file(GLOB UI_CONTROLLERS_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/controllers/*.h) -file(GLOB UI_CONTROLLERS_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/ui/controllers/*.cpp) - -set(HEADERS ${HEADERS} - ${COMMON_FILES_H} - ${PAGE_LOGIC_H} - ${CONFIGURATORS_H} - ${UI_MODELS_H} - ${UI_CONTROLLERS_H} -) -set(SOURCES ${SOURCES} - ${COMMON_FILES_CPP} - ${PAGE_LOGIC_CPP} - ${CONFIGURATORS_CPP} - ${UI_MODELS_CPP} - ${UI_CONTROLLERS_CPP} -) - if(WIN32) configure_file( - ${CMAKE_CURRENT_LIST_DIR}/platforms/windows/amneziavpn.rc.in - ${CMAKE_CURRENT_BINARY_DIR}/amneziavpn.rc - ) - - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/protocols/ikev2_vpn_protocol_windows.h - ) - - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/protocols/ikev2_vpn_protocol_windows.cpp - ) - - set(RESOURCES ${RESOURCES} + ${CMAKE_CURRENT_LIST_DIR}/platforms/windows/amneziavpn.rc.in ${CMAKE_CURRENT_BINARY_DIR}/amneziavpn.rc ) @@ -311,30 +174,6 @@ endif() if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) message("Client desktop build") add_compile_definitions(AMNEZIA_DESKTOP) - - set(HEADERS ${HEADERS} - ${CMAKE_CURRENT_LIST_DIR}/core/ipcclient.h - ${CMAKE_CURRENT_LIST_DIR}/core/privileged_process.h - ${CMAKE_CURRENT_LIST_DIR}/ui/systemtray_notificationhandler.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnovercloakprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/shadowsocksvpnprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/wireguardprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/xrayprotocol.h - ${CMAKE_CURRENT_LIST_DIR}/protocols/awgprotocol.h - ) - - set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/core/ipcclient.cpp - ${CMAKE_CURRENT_LIST_DIR}/core/privileged_process.cpp - ${CMAKE_CURRENT_LIST_DIR}/ui/systemtray_notificationhandler.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/openvpnovercloakprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/shadowsocksvpnprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/wireguardprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/xrayprotocol.cpp - ${CMAKE_CURRENT_LIST_DIR}/protocols/awgprotocol.cpp - ) endif() if(ANDROID) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index eb1eab45..f32d525a 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -2,39 +2,27 @@ #include #include +#include +#include #include +#include #include #include #include #include #include #include -#include #include "logger.h" +#include "ui/controllers/pageController.h" #include "ui/models/installedAppsModel.h" #include "version.h" #include "platforms/ios/QRCodeReaderBase.h" -#if defined(Q_OS_ANDROID) - #include "core/installedAppsImageProvider.h" - #include "platforms/android/android_controller.h" -#endif #include "protocols/qml_register_protocols.h" -#if defined(Q_OS_IOS) - #include "platforms/ios/ios_controller.h" - #include -#endif - -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv) -#else -AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options, int timeout, - const QString &userData) - : SingleApplication(argc, argv, allowSecondary, options, timeout, userData) -#endif { setQuitOnLastWindowClosed(false); @@ -88,103 +76,30 @@ void AmneziaApplication::init() m_vpnConnection->moveToThread(&m_vpnConnectionThread); m_vpnConnectionThread.start(); - initModels(); - loadTranslator(); - initControllers(); - -#ifdef Q_OS_ANDROID - if (!AndroidController::initLogging()) { - qFatal("Android logging initialization failed"); - } - AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); - connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); - - AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled()); - connect(m_settings.get(), &Settings::screenshotsEnabledChanged, AndroidController::instance(), &AndroidController::setScreenshotsEnabled); - - connect(m_settings.get(), &Settings::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer); - - connect(m_settings.get(), &Settings::settingsCleared, []() { AndroidController::instance()->resetLastServer(-1); }); - - connect(AndroidController::instance(), &AndroidController::initConnectionState, this, [this](Vpn::ConnectionState state) { - m_connectionController->onConnectionStateChanged(state); - if (m_vpnConnection) - m_vpnConnection->restoreConnection(); - }); - if (!AndroidController::instance()->initialize()) { - qFatal("Android controller initialization failed"); - } - - connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) { - m_pageController->replaceStartPage(); - m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); - }); - - m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); -#endif - -#ifdef Q_OS_IOS - IosController::Instance()->initialize(); - connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) { - m_pageController->replaceStartPage(); - m_importController->extractConfigFromData(data); - m_pageController->goToPageViewConfig(); - }); - - connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) { - m_pageController->replaceStartPage(); - m_pageController->goToPageSettingsBackup(); - m_settingsController->importBackupFromOutside(filePath); - }); - - QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); - - connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); }); -#endif - -#ifndef Q_OS_ANDROID - m_notificationHandler.reset(NotificationHandler::create(nullptr)); - - connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), - &NotificationHandler::setConnectionState); - - connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), &PageController::raiseMainWindow); - connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(), - static_cast(&ConnectionController::openConnection)); - connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), - &ConnectionController::closeConnection); - connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); -#endif + m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine)); + m_engine->addImportPath("qrc:/ui/qml/Modules/"); m_engine->load(url); - m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); + m_coreController->setQmlRoot(); + + bool enabled = m_settings->isSaveLogs(); #ifndef Q_OS_ANDROID - if (m_settings->isSaveLogs()) { - if (!Logger::init()) { + if (enabled) { + if (!Logger::init(false)) { qWarning() << "Initialization of debug subsystem failed"; } } #endif + Logger::setServiceLogsEnabled(enabled); -#ifdef Q_OS_WIN +#ifdef Q_OS_WIN //TODO if (m_parser.isSet("a")) - m_pageController->showOnStartup(); + m_coreController->pageController()->showOnStartup(); else - emit m_pageController->raiseMainWindow(); + emit m_coreController->pageController()->raiseMainWindow(); #else - m_pageController->showOnStartup(); -#endif - - // TODO - fix -#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) - if (isPrimary()) { - QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() { - qDebug() << "Secondary instance started, showing this window instead"; - emit m_pageController->raiseMainWindow(); - }); - } + m_coreController->pageController()->showOnStartup(); #endif // Android TextArea clipboard workaround @@ -241,33 +156,6 @@ void AmneziaApplication::loadFonts() QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf"); } -void AmneziaApplication::loadTranslator() -{ - auto locale = m_settings->getAppLanguage(); - m_translator.reset(new QTranslator()); - updateTranslator(locale); -} - -void AmneziaApplication::updateTranslator(const QLocale &locale) -{ - if (!m_translator->isEmpty()) { - QCoreApplication::removeTranslator(m_translator.get()); - } - - QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm"; - if (m_translator->load(strFileName)) { - if (QCoreApplication::installTranslator(m_translator.get())) { - m_settings->setAppLanguage(locale); - } - } else { - m_settings->setAppLanguage(QLocale::English); - } - - m_engine->retranslate(); - - emit translationsUpdated(); -} - bool AmneziaApplication::parseCommands() { m_parser.setApplicationDescription(APPLICATION_NAME); @@ -291,128 +179,36 @@ bool AmneziaApplication::parseCommands() return true; } +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +void AmneziaApplication::startLocalServer() +{ + const QString serverName("AmneziaVPNInstance"); + QLocalServer::removeServer(serverName); + + QLocalServer *server = new QLocalServer(this); + server->listen(serverName); + + QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() { + if (server) { + QLocalSocket *clientConnection = server->nextPendingConnection(); + clientConnection->deleteLater(); + } + emit m_coreController->pageController()->raiseMainWindow(); //TODO + }); +} +#endif + QQmlApplicationEngine *AmneziaApplication::qmlEngine() const { return m_engine; } -void AmneziaApplication::initModels() +QNetworkAccessManager *AmneziaApplication::networkManager() { - m_containersModel.reset(new ContainersModel(this)); - m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); - - m_defaultServerContainersModel.reset(new ContainersModel(this)); - m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get()); - - m_serversModel.reset(new ServersModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); - connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); - connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), - &ContainersModel::updateModel); - m_serversModel->resetModel(); - - m_languageModel.reset(new LanguageModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); - connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &AmneziaApplication::updateTranslator); - connect(this, &AmneziaApplication::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated); - - m_sitesModel.reset(new SitesModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); - - m_appSplitTunnelingModel.reset(new AppSplitTunnelingModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("AppSplitTunnelingModel", m_appSplitTunnelingModel.get()); - - m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); - - m_openVpnConfigModel.reset(new OpenVpnConfigModel(this)); - m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get()); - - m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this)); - m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get()); - - m_cloakConfigModel.reset(new CloakConfigModel(this)); - m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get()); - - m_wireGuardConfigModel.reset(new WireGuardConfigModel(this)); - m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get()); - - m_awgConfigModel.reset(new AwgConfigModel(this)); - m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get()); - - m_xrayConfigModel.reset(new XrayConfigModel(this)); - m_engine->rootContext()->setContextProperty("XrayConfigModel", m_xrayConfigModel.get()); - -#ifdef Q_OS_WINDOWS - m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this)); - m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get()); -#endif - - m_sftpConfigModel.reset(new SftpConfigModel(this)); - m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); - - m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this)); - m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get()); - - m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); - m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); - connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(), - &ServersModel::clearCachedProfile); + return m_nam; } -void AmneziaApplication::initControllers() +QClipboard *AmneziaApplication::getClipboard() { - m_connectionController.reset( - new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings)); - m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); - - connect(m_connectionController.get(), qOverload(&ConnectionController::connectionErrorOccurred), this, [this](const QString &errorMessage) { - emit m_pageController->showErrorMessage(errorMessage); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - }); - - connect(m_connectionController.get(), qOverload(&ConnectionController::connectionErrorOccurred), this, [this](ErrorCode errorCode) { - emit m_pageController->showErrorMessage(errorCode); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - }); - - connect(m_connectionController.get(), &ConnectionController::connectButtonClicked, m_connectionController.get(), - &ConnectionController::toggleConnection, Qt::QueuedConnection); - - connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(), &ConnectionController::onTranslationsUpdated); - - m_pageController.reset(new PageController(m_serversModel, m_settings)); - m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); - - m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, m_settings)); - m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); - connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), - &PageController::showPassphraseRequestDrawer); - connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(), - &InstallController::setEncryptedPassphrase); - connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(), - &ConnectionController::onCurrentContainerUpdated); - - m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); - m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); - - m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, m_settings)); - m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); - - m_settingsController.reset( - new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); - m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); - if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { - QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); - } - connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled, m_serversModel.get(), &ServersModel::toggleAmneziaDns); - - m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); - m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); - - m_appSplitTunnelingController.reset(new AppSplitTunnelingController(m_settings, m_appSplitTunnelingModel)); - m_engine->rootContext()->setContextProperty("AppSplitTunnelingController", m_appSplitTunnelingController.get()); - - m_systemController.reset(new SystemController(m_settings)); - m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + return this->clipboard(); } diff --git a/client/amnezia_application.h b/client/amnezia_application.h index b15d55d7..ea5f6f52 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -11,127 +11,53 @@ #else #include #endif +#include +#include "core/controllers/coreController.h" #include "settings.h" #include "vpnconnection.h" -#include "ui/controllers/connectionController.h" -#include "ui/controllers/exportController.h" -#include "ui/controllers/importController.h" -#include "ui/controllers/installController.h" -#include "ui/controllers/pageController.h" -#include "ui/controllers/settingsController.h" -#include "ui/controllers/sitesController.h" -#include "ui/controllers/systemController.h" -#include "ui/controllers/appSplitTunnelingController.h" -#include "ui/models/containers_model.h" -#include "ui/models/languageModel.h" -#include "ui/models/protocols/cloakConfigModel.h" -#ifndef Q_OS_ANDROID - #include "ui/notificationhandler.h" -#endif -#ifdef Q_OS_WINDOWS - #include "ui/models/protocols/ikev2ConfigModel.h" -#endif -#include "ui/models/protocols/awgConfigModel.h" -#include "ui/models/protocols/openvpnConfigModel.h" -#include "ui/models/protocols/shadowsocksConfigModel.h" -#include "ui/models/protocols/wireguardConfigModel.h" -#include "ui/models/protocols/xrayConfigModel.h" -#include "ui/models/protocols_model.h" -#include "ui/models/servers_model.h" -#include "ui/models/services/sftpConfigModel.h" -#include "ui/models/services/socks5ProxyConfigModel.h" -#include "ui/models/sites_model.h" -#include "ui/models/clientManagementModel.h" -#include "ui/models/appSplitTunnelingModel.h" - #define amnApp (static_cast(QCoreApplication::instance())) #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #define AMNEZIA_BASE_CLASS QGuiApplication #else - #define AMNEZIA_BASE_CLASS SingleApplication - #define QAPPLICATION_CLASS QApplication - #include "singleapplication.h" + #define AMNEZIA_BASE_CLASS QApplication #endif class AmneziaApplication : public AMNEZIA_BASE_CLASS { Q_OBJECT public: -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) AmneziaApplication(int &argc, char *argv[]); -#else - AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false, - SingleApplication::Options options = SingleApplication::User, int timeout = 1000, - const QString &userData = {}); -#endif virtual ~AmneziaApplication(); void init(); void registerTypes(); void loadFonts(); - void loadTranslator(); - void updateTranslator(const QLocale &locale); bool parseCommands(); - QQmlApplicationEngine *qmlEngine() const; - QNetworkAccessManager *manager() { return m_nam; } +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + void startLocalServer(); +#endif -signals: - void translationsUpdated(); + QQmlApplicationEngine *qmlEngine() const; + QNetworkAccessManager *networkManager(); + QClipboard *getClipboard(); private: - void initModels(); - void initControllers(); - QQmlApplicationEngine *m_engine {}; std::shared_ptr m_settings; + QScopedPointer m_coreController; + QSharedPointer m_containerProps; QSharedPointer m_protocolProps; - QSharedPointer m_translator; QCommandLineParser m_parser; - QSharedPointer m_containersModel; - QSharedPointer m_defaultServerContainersModel; - QSharedPointer m_serversModel; - QSharedPointer m_languageModel; - QSharedPointer m_protocolsModel; - QSharedPointer m_sitesModel; - QSharedPointer m_appSplitTunnelingModel; - QSharedPointer m_clientManagementModel; - - QScopedPointer m_openVpnConfigModel; - QScopedPointer m_shadowSocksConfigModel; - QScopedPointer m_cloakConfigModel; - QScopedPointer m_xrayConfigModel; - QScopedPointer m_wireGuardConfigModel; - QScopedPointer m_awgConfigModel; -#ifdef Q_OS_WINDOWS - QScopedPointer m_ikev2ConfigModel; -#endif - - QScopedPointer m_sftpConfigModel; - QScopedPointer m_socks5ConfigModel; - QSharedPointer m_vpnConnection; QThread m_vpnConnectionThread; -#ifndef Q_OS_ANDROID - QScopedPointer m_notificationHandler; -#endif - - QScopedPointer m_connectionController; - QScopedPointer m_pageController; - QScopedPointer m_installController; - QScopedPointer m_importController; - QScopedPointer m_exportController; - QScopedPointer m_settingsController; - QScopedPointer m_sitesController; - QScopedPointer m_systemController; - QScopedPointer m_appSplitTunnelingController; QNetworkAccessManager *m_nam; }; diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index f1d2682b..b28f754b 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -3,7 +3,6 @@ @@ -11,6 +10,9 @@ + + + @@ -18,7 +20,7 @@ - + @@ -31,9 +33,11 @@ android:label="-- %%INSERT_APP_NAME%% --" android:icon="@mipmap/icon" android:roundIcon="@mipmap/icon_round" + android:banner="@mipmap/ic_banner" android:theme="@style/NoActionBar" android:fullBackupContent="@xml/backup_content" android:dataExtractionRules="@xml/data_extraction_rules" + android:hasFragileUserData="false" tools:targetApi="s"> + @@ -62,9 +67,6 @@ android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --" /> - + + + + Unit): AwgConfig = Builder().apply(block).build() - } -} diff --git a/client/android/build.gradle b/client/android/build.gradle index d768000e..5044727b 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -3,3 +3,6 @@ // android.bundle.enableUncompressedNativeLibs is deprecated // disable adding gradle property android.bundle.enableUncompressedNativeLibs by androiddeployqt useLegacyPackaging + +// package name for androiddeployqt +namespace = "org.amnezia.vpn" diff --git a/client/android/build.gradle.kts b/client/android/build.gradle.kts index 7c4cec5a..3c742621 100644 --- a/client/android/build.gradle.kts +++ b/client/android/build.gradle.kts @@ -115,9 +115,11 @@ dependencies { implementation(project(":xray")) implementation(libs.androidx.core) implementation(libs.androidx.activity) + implementation(libs.androidx.fragment) implementation(libs.kotlinx.coroutines) implementation(libs.kotlinx.serialization.protobuf) implementation(libs.bundles.androidx.camera) implementation(libs.google.mlkit) implementation(libs.androidx.datastore) + implementation(libs.androidx.biometric) } diff --git a/client/android/cloak/src/main/kotlin/Cloak.kt b/client/android/cloak/src/main/kotlin/Cloak.kt index 5a549130..d408fb19 100644 --- a/client/android/cloak/src/main/kotlin/Cloak.kt +++ b/client/android/cloak/src/main/kotlin/Cloak.kt @@ -3,40 +3,16 @@ package org.amnezia.vpn.protocol.cloak import android.util.Base64 import net.openvpn.ovpn3.ClientAPI_Config import org.amnezia.vpn.protocol.openvpn.OpenVpn +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.json.JSONObject -/** - * Config Example: - * { - * "protocol": "cloak", - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "splitTunnelSites": [ - * ], - * "splitTunnelType": 0, - * "openvpn_config_data": { - * "config": "openVpnConfig" - * } - * "cloak_config_data": { - * "BrowserSig": "chrome", - * "EncryptionMethod": "aes-gcm", - * "NumConn": 1, - * "ProxyMethod": "openvpn", - * "PublicKey": "PublicKey=", - * "RemoteHost": "100.100.100.0", - * "RemotePort": "443", - * "ServerName": "servername", - * "StreamTimeout": 300, - * "Transport": "direct", - * "UID": "UID=" - * } - * } - */ - class Cloak : OpenVpn() { + override fun internalInit() { + super.internalInit() + if (!isInitialized) loadSharedLibrary(context, "ck-ovpn-plugin") + } + override fun parseConfig(config: JSONObject): ClientAPI_Config { val openVpnConfig = ClientAPI_Config() diff --git a/client/android/gradle.properties b/client/android/gradle.properties index 5a27838c..ce651e1c 100644 --- a/client/android/gradle.properties +++ b/client/android/gradle.properties @@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false # For development copy and set local values for these parameters in local.properties #androidCompileSdkVersion=android-34 #androidBuildToolsVersion=34.0.0 -#qtMinSdkVersion=24 +#qtMinSdkVersion=26 #qtTargetSdkVersion=34 #androidNdkVersion=26.1.10909125 #qtTargetAbiList=x86_64 diff --git a/client/android/gradle/libs.versions.toml b/client/android/gradle/libs.versions.toml index a5466422..c6fa1907 100644 --- a/client/android/gradle/libs.versions.toml +++ b/client/android/gradle/libs.versions.toml @@ -1,24 +1,28 @@ [versions] -agp = "8.2.0" -kotlin = "1.9.20" -androidx-core = "1.12.0" -androidx-activity = "1.8.1" -androidx-annotation = "1.7.0" -androidx-camera = "1.3.0" +agp = "8.5.2" +kotlin = "1.9.24" +androidx-core = "1.13.1" +androidx-activity = "1.9.1" +androidx-annotation = "1.8.2" +androidx-biometric = "1.2.0-alpha05" +androidx-camera = "1.3.4" +androidx-fragment = "1.8.2" androidx-security-crypto = "1.1.0-alpha06" -androidx-datastore = "1.1.0-beta01" -kotlinx-coroutines = "1.7.3" +androidx-datastore = "1.1.1" +kotlinx-coroutines = "1.8.1" kotlinx-serialization = "1.6.3" -google-mlkit = "17.2.0" +google-mlkit = "17.3.0" [libraries] androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } +androidx-biometric = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometric" } androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" } androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" } androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" } androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" } +androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } diff --git a/client/android/gradle/wrapper/gradle-wrapper.jar b/client/android/gradle/wrapper/gradle-wrapper.jar index d64cd491..a4b76b95 100644 Binary files a/client/android/gradle/wrapper/gradle-wrapper.jar and b/client/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties index 1af9e093..e1adfb49 100644 --- a/client/android/gradle/wrapper/gradle-wrapper.properties +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/client/android/gradlew b/client/android/gradlew index 1aa94a42..f5feea6d 100755 --- a/client/android/gradlew +++ b/client/android/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/client/android/gradlew.bat b/client/android/gradlew.bat index 93e3f59f..9d21a218 100644 --- a/client/android/gradlew.bat +++ b/client/android/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt index abe46245..22fe35cd 100644 --- a/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt +++ b/client/android/openvpn/src/main/kotlin/org/amnezia/vpn/protocol/openvpn/OpenVpn.kt @@ -11,28 +11,12 @@ import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.VpnStartException +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.getLocalNetworks import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject -/** - * Config Example: - * { - * "protocol": "openvpn", - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "splitTunnelSites": [ - * ], - * "splitTunnelType": 0, - * "openvpn_config_data": { - * "config": "openVpnConfig" - * } - * } - */ - open class OpenVpn : Protocol() { private var openVpnClient: OpenVpnClient? = null @@ -51,14 +35,17 @@ open class OpenVpn : Protocol() { } override fun internalInit() { - if (!isInitialized) loadSharedLibrary(context, "ovpn3") + if (!isInitialized) { + loadSharedLibrary(context, "ovpn3") + loadSharedLibrary(context, "ovpnutil") + } if (this::scope.isInitialized) { scope.cancel() } scope = CoroutineScope(Dispatchers.IO) } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val configBuilder = OpenVpnConfig.Builder() openVpnClient = OpenVpnClient( diff --git a/client/android/protocolApi/src/main/kotlin/Exceptions.kt b/client/android/protocolApi/src/main/kotlin/Exceptions.kt index 739a327c..b80648b0 100644 --- a/client/android/protocolApi/src/main/kotlin/Exceptions.kt +++ b/client/android/protocolApi/src/main/kotlin/Exceptions.kt @@ -2,7 +2,6 @@ package org.amnezia.vpn.protocol sealed class ProtocolException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) -class LoadLibraryException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) class BadConfigException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) class VpnStartException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause) diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index a475a2fc..6e682aa4 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -1,6 +1,5 @@ package org.amnezia.vpn.protocol -import android.annotation.SuppressLint import android.content.Context import android.net.IpPrefix import android.net.VpnService @@ -8,9 +7,6 @@ import android.net.VpnService.Builder import android.os.Build import android.system.OsConstants import androidx.annotation.RequiresApi -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipFile import kotlinx.coroutines.flow.MutableStateFlow import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork @@ -42,7 +38,7 @@ abstract class Protocol { protected abstract fun internalInit() - abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) + abstract suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) abstract fun stopVpn() @@ -158,60 +154,6 @@ abstract class Protocol { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) vpnBuilder.setMetered(false) } - - companion object { - private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean { - Log.d(TAG, "Extracting library: $libraryName") - val apks = hashSetOf() - context.applicationInfo.run { - sourceDir?.let { apks += it } - splitSourceDirs?.let { apks += it } - } - for (abi in Build.SUPPORTED_ABIS) { - for (apk in apks) { - ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile -> - val mappedName = System.mapLibraryName(libraryName) - val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator) - val zipEntry = zipFile.getEntry(libraryZipPath) - zipEntry?.let { - Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}") - FileOutputStream(destination).use { outStream -> - zipFile.getInputStream(zipEntry).use { inStream -> - inStream.copyTo(outStream, 32 * 1024) - outStream.fd.sync() - } - } - } - return true - } - } - } - return false - } - - @SuppressLint("UnsafeDynamicallyLoadedCode") - fun loadSharedLibrary(context: Context, libraryName: String) { - Log.d(TAG, "Loading library: $libraryName") - try { - System.loadLibrary(libraryName) - return - } catch (_: UnsatisfiedLinkError) { - Log.d(TAG, "Failed to load library, try to extract it from apk") - } - var tempFile: File? = null - try { - tempFile = File.createTempFile("lib", ".so", context.codeCacheDir) - if (extractLibrary(context, libraryName, tempFile)) { - System.load(tempFile.absolutePath) - return - } - } catch (e: Exception) { - throw LoadLibraryException("Failed to load library apk: $libraryName", e) - } finally { - tempFile?.delete() - } - } - } } private fun VpnService.Builder.addAddress(addr: InetNetwork) = addAddress(addr.address, addr.mask) diff --git a/client/android/qt/build.gradle.kts b/client/android/qt/build.gradle.kts index 139adf4f..6b1d3fd1 100644 --- a/client/android/qt/build.gradle.kts +++ b/client/android/qt/build.gradle.kts @@ -21,5 +21,5 @@ android { } dependencies { - implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) + api(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar")))) } diff --git a/client/android/res/mipmap-hdpi/ic_banner.png b/client/android/res/mipmap-hdpi/ic_banner.png new file mode 100644 index 00000000..a444777f Binary files /dev/null and b/client/android/res/mipmap-hdpi/ic_banner.png differ diff --git a/client/android/res/mipmap-mdpi/ic_banner.png b/client/android/res/mipmap-mdpi/ic_banner.png new file mode 100644 index 00000000..b9ad1db7 Binary files /dev/null and b/client/android/res/mipmap-mdpi/ic_banner.png differ diff --git a/client/android/res/mipmap-xhdpi/ic_banner.png b/client/android/res/mipmap-xhdpi/ic_banner.png new file mode 100644 index 00000000..f5029b23 Binary files /dev/null and b/client/android/res/mipmap-xhdpi/ic_banner.png differ diff --git a/client/android/res/values-ru/strings.xml b/client/android/res/values-ru/strings.xml index 8bdabfc0..5e35bba5 100644 --- a/client/android/res/values-ru/strings.xml +++ b/client/android/res/values-ru/strings.xml @@ -23,4 +23,6 @@ Настройки уведомлений Для показа уведомлений необходимо включить уведомления в системных настройках Открыть настройки уведомлений + + Пожалуйста, установите приложение для просмотра файлов \ No newline at end of file diff --git a/client/android/res/values/libs.xml b/client/android/res/values/libs.xml index fe63866f..3ccf1d80 100644 --- a/client/android/res/values/libs.xml +++ b/client/android/res/values/libs.xml @@ -3,7 +3,6 @@ - diff --git a/client/android/res/values/strings.xml b/client/android/res/values/strings.xml index 5251403b..bf8d76d1 100644 --- a/client/android/res/values/strings.xml +++ b/client/android/res/values/strings.xml @@ -23,4 +23,6 @@ Notification settings To show notifications, you must enable notifications in the system settings Open notification settings + + Please install a file management utility to browse files \ No newline at end of file diff --git a/client/android/res/values/styles.xml b/client/android/res/values/styles.xml index 9f4201f8..bc67beb9 100644 --- a/client/android/res/values/styles.xml +++ b/client/android/res/values/styles.xml @@ -1,6 +1,9 @@ + #FF0E0E11 diff --git a/client/android/settings.gradle.kts b/client/android/settings.gradle.kts index 5cfc8314..68426ec8 100644 --- a/client/android/settings.gradle.kts +++ b/client/android/settings.gradle.kts @@ -22,7 +22,7 @@ dependencyResolutionManagement { includeBuild("./gradle/plugins") plugins { - id("com.android.settings") version "8.2.0" + id("com.android.settings") version "8.5.2" id("settings-property-delegate") } diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 202fe2e6..c6db5e29 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -1,8 +1,10 @@ package org.amnezia.vpn import android.Manifest +import android.annotation.SuppressLint import android.app.AlertDialog import android.app.NotificationManager +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Intent @@ -11,6 +13,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY import android.content.ServiceConnection import android.content.pm.PackageManager import android.graphics.Bitmap +import android.net.Uri import android.net.VpnService import android.os.Build import android.os.Bundle @@ -19,7 +22,13 @@ import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.os.ParcelFileDescriptor +import android.os.SystemClock +import android.provider.OpenableColumns import android.provider.Settings +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import android.view.WindowManager.LayoutParams import android.webkit.MimeTypeMap import android.widget.Toast @@ -28,6 +37,7 @@ import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import java.io.IOException import kotlin.LazyThreadSafetyMode.NONE +import kotlin.coroutines.CoroutineContext import kotlin.text.RegexOption.IGNORE_CASE import AppListProvider import kotlinx.coroutines.CompletableDeferred @@ -42,6 +52,7 @@ import kotlinx.coroutines.withContext import org.amnezia.vpn.protocol.getStatistics import org.amnezia.vpn.protocol.getStatus import org.amnezia.vpn.qt.QtAndroidController +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Prefs import org.json.JSONException @@ -68,6 +79,7 @@ class AmneziaActivity : QtActivity() { private var isInBoundState = false private var notificationStateReceiver: BroadcastReceiver? = null private lateinit var vpnServiceMessenger: IpcMessenger + private var pfd: ParcelFileDescriptor? = null private val actionResultHandlers = mutableMapOf() private val permissionRequestHandlers = mutableMapOf() @@ -156,7 +168,12 @@ class AmneziaActivity : QtActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d(TAG, "Create Amnezia activity: $intent") + Log.d(TAG, "Create Amnezia activity") + loadLibs() + window.apply { + addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + statusBarColor = getColor(R.color.black) + } mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) val proto = mainScope.async(Dispatchers.IO) { VpnStateStore.getVpnState().vpnProto @@ -174,6 +191,17 @@ class AmneziaActivity : QtActivity() { runBlocking { vpnProto = proto.await() } } + private fun loadLibs() { + listOf( + "rsapss", + "crypto_3", + "ssl_3", + "ssh" + ).forEach { + loadSharedLibrary(this.applicationContext, it) + } + } + private fun registerBroadcastReceivers() { notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { registerBroadcastReceiver( @@ -182,7 +210,7 @@ class AmneziaActivity : QtActivity() { NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED ) ) { - Log.d( + Log.v( TAG, "Notification state changed: ${it?.action}, blocked = " + "${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}" ) @@ -196,7 +224,7 @@ class AmneziaActivity : QtActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Log.d(TAG, "onNewIntent: $intent") + Log.v(TAG, "onNewIntent: $intent") intent?.let(::processIntent) } @@ -230,7 +258,10 @@ class AmneziaActivity : QtActivity() { override fun onStop() { Log.d(TAG, "Stop Amnezia activity") doUnbindService() - QtAndroidController.onServiceDisconnected() + mainScope.launch { + qtInitialized.await() + QtAndroidController.onServiceDisconnected() + } super.onStop() } @@ -382,9 +413,10 @@ class AmneziaActivity : QtActivity() { @MainThread private fun startVpn(vpnConfig: String) { getVpnProto(vpnConfig)?.let { proto -> - Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto") + Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto") if (isServiceConnected) { - if (proto == vpnProto) { + if (proto.serviceClass == vpnProto?.serviceClass) { + vpnProto = proto connectToVpn(vpnConfig) return } @@ -491,21 +523,25 @@ class AmneziaActivity : QtActivity() { type = "text/*" putExtra(Intent.EXTRA_TITLE, fileName) }.also { - startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( - onSuccess = { - it?.data?.let { uri -> - Log.d(TAG, "Save file to $uri") - try { - contentResolver.openOutputStream(uri)?.use { os -> - os.bufferedWriter().use { it.write(data) } + try { + startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( + onSuccess = { + it?.data?.let { uri -> + Log.v(TAG, "Save file to $uri") + try { + contentResolver.openOutputStream(uri)?.use { os -> + os.bufferedWriter().use { it.write(data) } + } + } catch (e: IOException) { + Log.e(TAG, "Failed to save file $uri: $e") + // todo: send error to Qt } - } catch (e: IOException) { - Log.e(TAG, "Failed to save file $uri: $e") - // todo: send error to Qt } } - } - )) + )) + } catch (_: ActivityNotFoundException) { + Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show() + } } } } @@ -514,49 +550,122 @@ class AmneziaActivity : QtActivity() { fun openFile(filter: String?) { Log.v(TAG, "Open file with filter: $filter") mainScope.launch { - val mimeTypes = if (!filter.isNullOrEmpty()) { - val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) - val mime = MimeTypeMap.getSingleton() - extensionRegex.findAll(filter).map { - it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*" - }.toSet() - } else emptySet() + val intent = if (!isOnTv()) { + val mimeTypes = if (!filter.isNullOrEmpty()) { + val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) + val mime = MimeTypeMap.getSingleton() + extensionRegex.findAll(filter).map { + it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*" + }.toSet() + } else emptySet() - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - Log.v(TAG, "File mimyType filter: $mimeTypes") - if ("*/*" in mimeTypes) { - type = "*/*" - } else { - when (mimeTypes.size) { - 1 -> type = mimeTypes.first() + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + Log.v(TAG, "File mimyType filter: $mimeTypes") + if ("*/*" in mimeTypes) { + type = "*/*" + } else { + when (mimeTypes.size) { + 1 -> type = mimeTypes.first() - in 2..Int.MAX_VALUE -> { - type = "*/*" - putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + in 2..Int.MAX_VALUE -> { + type = "*/*" + putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + } + + else -> type = "*/*" } - - else -> type = "*/*" } } - }.also { - startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( - onSuccess = { - val uri = it?.data?.toString() ?: "" - Log.d(TAG, "Open file: $uri") + } else { + Intent(this@AmneziaActivity, TvFilePicker::class.java) + } + + try { + startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler( + onAny = { + if (isOnTv() && it?.hasExtra("activityNotFound") == true) { + showNoFileBrowserAlertDialog() + } + val uri = it?.data?.apply { + grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION) + }?.toString() ?: "" + Log.v(TAG, "Open file: $uri") mainScope.launch { qtInitialized.await() QtAndroidController.onFileOpened(uri) } } )) + } catch (_: ActivityNotFoundException) { + showNoFileBrowserAlertDialog() + mainScope.launch { + qtInitialized.await() + QtAndroidController.onFileOpened("") + } + } + } + } + + private fun showNoFileBrowserAlertDialog() { + AlertDialog.Builder(this) + .setMessage(R.string.tvNoFileBrowser) + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect"))) + } catch (_: Throwable) {} + } + .show() + } + + @Suppress("unused") + fun getFd(fileName: String): Int { + Log.v(TAG, "Get fd for $fileName") + return blockingCall { + try { + pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r") + pfd?.fd ?: -1 + } catch (e: Exception) { + Log.e(TAG, "Failed to get fd: $e") + -1 } } } @Suppress("unused") + fun closeFd() { + Log.v(TAG, "Close fd") + mainScope.launch { + pfd?.close() + pfd = null + } + } + + @Suppress("unused") + fun getFileName(uri: String): String { + Log.v(TAG, "Get file name for uri: $uri") + return blockingCall { + try { + contentResolver.query(Uri.parse(uri), arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor -> + if (cursor.moveToFirst() && !cursor.isNull(0)) { + return@blockingCall cursor.getString(0) ?: "" + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to get file name: $e") + } + "" + } + } + + @Suppress("unused") + @SuppressLint("UnsupportedChromeOsCameraSystemFeature") fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) + @Suppress("unused") + fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + @Suppress("unused") fun startQrCodeReader() { Log.v(TAG, "Start camera") @@ -601,6 +710,14 @@ class AmneziaActivity : QtActivity() { } } + @Suppress("unused") + fun setNavigationBarColor(color: Int) { + Log.v(TAG, "Change navigation bar color: ${"#%08X".format(color)}") + mainScope.launch { + window.navigationBarColor = color + } + } + @Suppress("unused") fun minimizeApp() { Log.v(TAG, "Minimize application") @@ -675,9 +792,132 @@ class AmneziaActivity : QtActivity() { .show() } + @Suppress("unused") + fun requestAuthentication() { + Log.v(TAG, "Request authentication") + mainScope.launch { + qtInitialized.await() + Intent(this@AmneziaActivity, AuthActivity::class.java).also { + startActivity(it) + } + } + } + + // method to workaround Qt's problem with calling the keyboard on TVs + @Suppress("unused") + fun sendTouch(x: Float, y: Float) { + Log.v(TAG, "Send touch: $x, $y") + blockingCall { + findQtWindow(window.decorView)?.let { + Log.v(TAG, "Send touch to $it") + it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN)) + it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP)) + } + } + } + + private fun findQtWindow(view: View): View? { + Log.v(TAG, "findQtWindow: process $view") + if (view::class.simpleName == "QtWindow") return view + else if (view is ViewGroup) { + for (i in 0 until view.childCount) { + val result = findQtWindow(view.getChildAt(i)) + if (result != null) return result + } + return null + } else return null + } + + private fun createEvent(x: Float, y: Float, eventTime: Long, action: Int): MotionEvent = + MotionEvent.obtain( + eventTime, + eventTime, + action, + 1, + arrayOf(MotionEvent.PointerProperties().apply { + id = 0 + toolType = MotionEvent.TOOL_TYPE_FINGER + }), + arrayOf(MotionEvent.PointerCoords().apply { + this.x = x + this.y = y + pressure = 1f + size = 1f + }), + 0, 0, 1.0f, 1.0f, 0, 0, 0,0 + ) + + // workaround for a bug in Qt that causes the mouse click event not to be handled + // also disable right-click, as it causes the application to crash + private var lastButtonState = 0 + private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain( + downTime, + eventTime, + action, + pointerCount, + (0 until pointerCount).map { i -> + MotionEvent.PointerProperties().apply { + getPointerProperties(i, this) + } + }.toTypedArray(), + (0 until pointerCount).map { i -> + MotionEvent.PointerCoords().apply { + getPointerCoords(i, this) + } + }.toTypedArray(), + metaState, + MotionEvent.BUTTON_PRIMARY, + xPrecision, + yPrecision, + deviceId, + edgeFlags, + source, + flags + ) + + private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + lastButtonState = ev.buttonState + if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true + } + + MotionEvent.ACTION_UP -> { + when (lastButtonState) { + MotionEvent.BUTTON_SECONDARY -> return true + MotionEvent.BUTTON_PRIMARY -> { + val modEvent = ev.fixCopy() + return superDispatch(modEvent).apply { modEvent.recycle() } + } + } + } + } + return superDispatch(ev) + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + Log.v(TAG, "dispatchTouch: $ev") + if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + return handleMouseEvent(ev) { super.dispatchTouchEvent(it) } + } + return super.dispatchTouchEvent(ev) + } + + override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { + ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }} + return super.dispatchTrackballEvent(ev) + } + /** * Utils methods */ + private fun blockingCall( + context: CoroutineContext = Dispatchers.Main.immediate, + block: suspend () -> T + ) = runBlocking { + mainScope.async(context) { block() }.await() + } + companion object { private fun actionCodeToString(actionCode: Int): String = when (actionCode) { diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index b30f1503..8d108bc3 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -22,6 +22,7 @@ import androidx.annotation.MainThread import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import java.net.UnknownHostException import java.util.concurrent.ConcurrentHashMap import kotlin.LazyThreadSafetyMode.NONE import kotlinx.coroutines.CoroutineExceptionHandler @@ -31,6 +32,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.drop @@ -39,7 +41,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import org.amnezia.vpn.protocol.BadConfigException -import org.amnezia.vpn.protocol.LoadLibraryException import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.CONNECTING import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED @@ -49,6 +50,7 @@ import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN import org.amnezia.vpn.protocol.VpnException import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.putStatus +import org.amnezia.vpn.util.LoadLibraryException import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.net.NetworkState @@ -111,6 +113,10 @@ open class AmneziaVpnService : VpnService() { get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME } private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> + connectionJob?.cancel() + connectionJob = null + disconnectionJob?.cancel() + disconnectionJob = null protocolState.value = DISCONNECTED when (e) { is IllegalArgumentException, @@ -122,6 +128,8 @@ open class AmneziaVpnService : VpnService() { is LoadLibraryException -> onError("${e.message}. Caused: ${e.cause?.message}") + is UnknownHostException -> onError("Unknown host") + else -> throw e } } @@ -292,7 +300,7 @@ open class AmneziaVpnService : VpnService() { arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED ) { it?.action?.let { action -> - Log.d(TAG, "Broadcast request received: $action") + Log.v(TAG, "Broadcast request received: $action") when (action) { ACTION_CONNECT -> connect() ACTION_DISCONNECT -> disconnect() @@ -309,7 +317,7 @@ open class AmneziaVpnService : VpnService() { ) ) { val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false) - Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state") + Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state") if (state == false) { enableNotification() } else { @@ -442,7 +450,7 @@ open class AmneziaVpnService : VpnService() { serviceNotification.isNotificationEnabled() && getSystemService()?.isInteractive != false ) { - Log.d(TAG, "Launch traffic stats update") + Log.v(TAG, "Launch traffic stats update") trafficStats.reset() startTrafficStatsUpdateJob() } @@ -531,7 +539,7 @@ open class AmneziaVpnService : VpnService() { protocolState.value = DISCONNECTING disconnectionJob = connectionScope.launch { - connectionJob?.join() + connectionJob?.cancelAndJoin() connectionJob = null vpnProto?.protocol?.stopVpn() diff --git a/client/android/src/org/amnezia/vpn/AuthActivity.kt b/client/android/src/org/amnezia/vpn/AuthActivity.kt new file mode 100644 index 00000000..46401548 --- /dev/null +++ b/client/android/src/org/amnezia/vpn/AuthActivity.kt @@ -0,0 +1,97 @@ +package org.amnezia.vpn + +import android.os.Build +import android.os.Bundle +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.AuthenticationResult +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import org.amnezia.vpn.qt.QtAndroidController +import org.amnezia.vpn.util.Log + +private const val TAG = "AuthActivity" + +private const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL + +class AuthActivity : FragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val biometricManager = BiometricManager.from(applicationContext) + when (biometricManager.canAuthenticate(AUTHENTICATORS)) { + BiometricManager.BIOMETRIC_SUCCESS -> { + showBiometricPrompt(biometricManager) + return + } + + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { + Log.w(TAG, "Unknown biometric status") + showBiometricPrompt(biometricManager) + return + } + + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { + Log.e(TAG, "The specified options are incompatible with the current Android " + + "version ${Build.VERSION.SDK_INT}") + } + + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> { + Log.w(TAG, "The hardware is unavailable") + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + Log.w(TAG, "No biometric or device credential is enrolled") + } + + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { + Log.w(TAG, "There is no suitable hardware") + } + + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { + Log.w(TAG, "A security vulnerability has been discovered with one or " + + "more hardware sensors") + } + } + QtAndroidController.onAuthResult(true) + finish() + } + + private fun showBiometricPrompt(biometricManager: BiometricManager) { + val executor = ContextCompat.getMainExecutor(applicationContext) + val biometricPrompt = BiometricPrompt(this, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.v(TAG, "Authentication succeeded") + QtAndroidController.onAuthResult(true) + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.w(TAG, "Authentication failed") + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Log.e(TAG, "Authentication error $errorCode: $errString") + QtAndroidController.onAuthResult(false) + finish() + } + }) + + + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(AUTHENTICATORS) + .setTitle("AmneziaVPN") + .setSubtitle(biometricManager.getStrings(AUTHENTICATORS)?.promptMessage) + .build() + + biometricPrompt.authenticate(promptInfo) + } +} diff --git a/client/android/src/org/amnezia/vpn/AuthHelper.java b/client/android/src/org/amnezia/vpn/AuthHelper.java deleted file mode 100644 index 940d03c2..00000000 --- a/client/android/src/org/amnezia/vpn/AuthHelper.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.amnezia.vpn; - -import android.content.Context; -import android.app.KeyguardManager; -import android.content.Intent; -import org.qtproject.qt.android.bindings.QtActivity; - - -import static android.content.Context.KEYGUARD_SERVICE; - -public class AuthHelper extends QtActivity { - - static final String TAG = "AuthHelper"; - - public static Intent getAuthIntent(Context context) { - KeyguardManager mKeyguardManager = (KeyguardManager)context.getSystemService(KEYGUARD_SERVICE); - if (mKeyguardManager.isDeviceSecure()) { - return mKeyguardManager.createConfirmDeviceCredentialIntent(null, null); - } else { - return null; - } - } - -} diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index cae7ab75..49823a36 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d(TAG, "Create Import Config Activity: $intent") + Log.v(TAG, "Create Import Config Activity: $intent") intent?.let(::readConfig) } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - Log.d(TAG, "onNewIntent: $intent") - intent?.let(::readConfig) + Log.v(TAG, "onNewIntent: $intent") + intent.let(::readConfig) } private fun readConfig(intent: Intent) { when (intent.action) { ACTION_SEND -> { - Log.d(TAG, "Process SEND action, type: ${intent.type}") + Log.v(TAG, "Process SEND action, type: ${intent.type}") when (intent.type) { "application/octet-stream" -> { intent.getUriCompat()?.let { uri -> @@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() { } ACTION_VIEW -> { - Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}") + Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}") when (intent.scheme) { "file", "content" -> { intent.data?.let { uri -> diff --git a/client/android/src/org/amnezia/vpn/ServiceNotification.kt b/client/android/src/org/amnezia/vpn/ServiceNotification.kt index f4707731..47e8f263 100644 --- a/client/android/src/org/amnezia/vpn/ServiceNotification.kt +++ b/client/android/src/org/amnezia/vpn/ServiceNotification.kt @@ -62,7 +62,7 @@ class ServiceNotification(private val context: Context) { fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification { val speedString = if (state == CONNECTED) zeroSpeed else null - Log.d(TAG, "Build notification: $serverName, $state") + Log.v(TAG, "Build notification: $serverName, $state") return notificationBuilder .setSmallIcon(R.drawable.ic_amnezia_round) @@ -88,17 +88,15 @@ class ServiceNotification(private val context: Context) { fun isNotificationEnabled(): Boolean { if (!context.isNotificationPermissionGranted()) return false if (!notificationManager.areNotificationsEnabled()) return false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) - ?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true - } - return true + return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let { + it.importance != NotificationManager.IMPORTANCE_NONE + } ?: true } @SuppressLint("MissingPermission") fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) { if (context.isNotificationPermissionGranted()) { - Log.d(TAG, "Update notification: $serverName, $state") + Log.v(TAG, "Update notification: $serverName, $state") notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state)) } } diff --git a/client/android/src/org/amnezia/vpn/TvFilePicker.kt b/client/android/src/org/amnezia/vpn/TvFilePicker.kt new file mode 100644 index 00000000..1ac275eb --- /dev/null +++ b/client/android/src/org/amnezia/vpn/TvFilePicker.kt @@ -0,0 +1,45 @@ +package org.amnezia.vpn + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import org.amnezia.vpn.util.Log + +private const val TAG = "TvFilePicker" + +class TvFilePicker : ComponentActivity() { + + private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { + setResult(RESULT_OK, Intent().apply { data = it }) + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.v(TAG, "onCreate") + getFile() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + Log.v(TAG, "onNewIntent") + getFile() + } + + private fun getFile() { + try { + Log.v(TAG, "getFile") + fileChooseResultLauncher.launch("*/*") + } catch (_: ActivityNotFoundException) { + Log.w(TAG, "Activity not found") + setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) }) + finish() + } catch (e: Exception) { + Log.e(TAG, "Failed to get file: $e") + setResult(RESULT_CANCELED) + finish() + } + } +} diff --git a/client/android/src/org/amnezia/vpn/VpnProto.kt b/client/android/src/org/amnezia/vpn/VpnProto.kt index 508ce226..658e06ac 100644 --- a/client/android/src/org/amnezia/vpn/VpnProto.kt +++ b/client/android/src/org/amnezia/vpn/VpnProto.kt @@ -49,7 +49,15 @@ enum class VpnProto( "org.amnezia.vpn:amneziaXrayService", XrayService::class.java ) { - override fun createProtocol(): Protocol = Xray() + override fun createProtocol(): Protocol = Xray.instance + }, + + SSXRAY( + "SSXRay", + "org.amnezia.vpn:amneziaXrayService", + XrayService::class.java + ) { + override fun createProtocol(): Protocol = Xray.instance }; private var _protocol: Protocol? = null diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index e382b080..4af138a2 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -25,5 +25,7 @@ object QtAndroidController { external fun onConfigImported(data: String) + external fun onAuthResult(result: Boolean) + external fun decodeQrCode(data: String): Boolean } \ No newline at end of file diff --git a/client/android/utils/src/main/kotlin/JsonExt.kt b/client/android/utils/src/main/kotlin/JsonExt.kt new file mode 100644 index 00000000..45c5bacd --- /dev/null +++ b/client/android/utils/src/main/kotlin/JsonExt.kt @@ -0,0 +1,9 @@ +package org.amnezia.vpn.util + +import org.json.JSONArray +import org.json.JSONObject + +inline fun JSONArray.asSequence(): Sequence = + (0..() + context.applicationInfo.run { + sourceDir?.let { apks += it } + splitSourceDirs?.let { apks += it } + } + for (abi in Build.SUPPORTED_ABIS) { + for (apk in apks) { + ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile -> + val mappedName = System.mapLibraryName(libraryName) + val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator) + val zipEntry = zipFile.getEntry(libraryZipPath) + zipEntry?.let { + Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}") + FileOutputStream(destination).use { outStream -> + zipFile.getInputStream(zipEntry).use { inStream -> + inStream.copyTo(outStream, 32 * 1024) + outStream.fd.sync() + } + } + } + return true + } + } + } + return false + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + fun loadSharedLibrary(context: Context, libraryName: String) { + Log.d(TAG, "Loading library: $libraryName") + try { + System.loadLibrary(libraryName) + return + } catch (_: UnsatisfiedLinkError) { + Log.w(TAG, "Failed to load library, try to extract it from apk") + } + var tempFile: File? = null + try { + tempFile = File.createTempFile("lib", ".so", context.codeCacheDir) + if (extractLibrary(context, libraryName, tempFile)) { + System.load(tempFile.absolutePath) + return + } + } catch (e: Exception) { + throw LoadLibraryException("Failed to load library apk: $libraryName", e) + } finally { + tempFile?.delete() + } + } +} + +class LoadLibraryException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) diff --git a/client/android/utils/src/main/kotlin/Log.kt b/client/android/utils/src/main/kotlin/Log.kt index a656b9ea..da11c200 100644 --- a/client/android/utils/src/main/kotlin/Log.kt +++ b/client/android/utils/src/main/kotlin/Log.kt @@ -1,8 +1,6 @@ package org.amnezia.vpn.util import android.content.Context -import android.icu.text.DateFormat -import android.icu.text.SimpleDateFormat import android.os.Build import android.os.Process import java.io.File @@ -12,8 +10,6 @@ import java.nio.channels.FileChannel import java.nio.channels.FileLock import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.Date -import java.util.Locale import java.util.concurrent.locks.ReentrantLock import org.amnezia.vpn.util.Log.Priority.D import org.amnezia.vpn.util.Log.Priority.E @@ -41,11 +37,7 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024 * | | | create a report and/or terminate the process | */ object Log { - private val dateTimeFormat: Any = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) - else object : ThreadLocal() { - override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US) - } + private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN) private lateinit var logDir: File private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) } @@ -143,12 +135,7 @@ object Log { } private fun formatLogMsg(tag: String, msg: String, priority: Priority): String { - val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter) - } else { - @Suppress("UNCHECKED_CAST") - (dateTimeFormat as ThreadLocal).get()?.format(Date()) - } + val date = LocalDateTime.now().format(dateTimeFormat) return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " + "$tag: $msg\n" } diff --git a/client/android/utils/src/main/kotlin/net/NetworkState.kt b/client/android/utils/src/main/kotlin/net/NetworkState.kt index 3cff8c04..1cab5535 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkState.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkState.kt @@ -42,18 +42,12 @@ class NetworkState( private val networkCallback: NetworkCallback by lazy(NONE) { object : NetworkCallback() { override fun onAvailable(network: Network) { - Log.d(TAG, "onAvailable: $network") + Log.v(TAG, "onAvailable: $network") } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - checkNetworkState(network, networkCapabilities) - } else { - handler.post { - checkNetworkState(network, networkCapabilities) - } - } + Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities") + checkNetworkState(network, networkCapabilities) } private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) { @@ -73,11 +67,11 @@ class NetworkState( } override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { - Log.d(TAG, "onBlockedStatusChanged: $network, $blocked") + Log.v(TAG, "onBlockedStatusChanged: $network, $blocked") } override fun onLost(network: Network) { - Log.d(TAG, "onLost: $network") + Log.v(TAG, "onLost: $network") } } } @@ -87,21 +81,27 @@ class NetworkState( Log.d(TAG, "Bind network listener") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - connectivityManager.requestNetwork(networkRequest, networkCallback, handler) - } catch (e: SecurityException) { - Log.e(TAG, "Failed to bind network listener: $e") - // Android 11 bug: https://issuetracker.google.com/issues/175055271 - if (e.message?.startsWith("Package android does not belong to") == true) { - delay(1000) + } else { + val numberAttempts = 300 + var attemptCount = 0 + while(true) { + try { connectivityManager.requestNetwork(networkRequest, networkCallback, handler) - } else { - throw e + break + } catch (e: SecurityException) { + Log.e(TAG, "Failed to bind network listener: $e") + // Android 11 bug: https://issuetracker.google.com/issues/175055271 + if (e.message?.startsWith("Package android does not belong to") == true) { + if (++attemptCount > numberAttempts) { + throw e + } + delay(1000) + continue + } else { + throw e + } } } - } else { - connectivityManager.requestNetwork(networkRequest, networkCallback) } isListenerBound = true } diff --git a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt index b75748be..784aa352 100644 --- a/client/android/utils/src/main/kotlin/net/NetworkUtils.kt +++ b/client/android/utils/src/main/kotlin/net/NetworkUtils.kt @@ -35,7 +35,7 @@ fun getLocalNetworks(context: Context, ipv6: Boolean): List { return emptyList() } -fun parseInetAddress(address: String): InetAddress = parseNumericAddressCompat(address) +fun parseInetAddress(address: String): InetAddress = InetAddress.getByName(address) private val parseNumericAddressCompat: (String) -> InetAddress = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -60,7 +60,7 @@ private val parseNumericAddressCompat: (String) -> InetAddress = internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6 .replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2") -internal val InetAddress.ip: String +val InetAddress.ip: String get() = if (this is Inet4Address) { hostAddress!! } else { diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 690510eb..80cab96d 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,60 +1,35 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder -import java.util.TreeMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.VpnStartException +import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary import org.amnezia.vpn.util.Log +import org.amnezia.vpn.util.asSequence import org.amnezia.vpn.util.net.InetEndpoint import org.amnezia.vpn.util.net.InetNetwork import org.amnezia.vpn.util.net.parseInetAddress +import org.amnezia.vpn.util.optStringOrNull import org.json.JSONObject -/** - * Config example: - * { - * "protocol": "wireguard", - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "splitTunnelSites": [ - * ], - * "splitTunnelType": 0, - * "wireguard_config_data": { - * "client_ip": "10.8.1.1", - * "hostName": "100.100.100.0", - * "port": 12345, - * "client_pub_key": "clientPublicKeyBase64", - * "client_priv_key": "privateKeyBase64", - * "psk_key": "presharedKeyBase64", - * "server_pub_key": "publicKeyBase64", - * "config": "[Interface] - * Address = 10.8.1.1/32 - * DNS = 1.1.1.1, 1.0.0.1 - * PrivateKey = privateKeyBase64 - * - * [Peer] - * PublicKey = publicKeyBase64 - * PresharedKey = presharedKeyBase64 - * AllowedIPs = 0.0.0.0/0, ::/0 - * Endpoint = 100.100.100.0:12345 - * PersistentKeepalive = 25 - * " - * } - * } - */ - private const val TAG = "Wireguard" open class Wireguard : Protocol() { private var tunnelHandle: Int = -1 protected open val ifName: String = "amn0" + private lateinit var scope: CoroutineScope + private var statusJob: Job? = null override val statistics: Statistics get() { @@ -77,69 +52,78 @@ open class Wireguard : Protocol() { override fun internalInit() { if (!isInitialized) loadSharedLibrary(context, "wg-go") + if (this::scope.isInitialized) { + scope.cancel() + } + scope = CoroutineScope(Dispatchers.IO) } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) start(wireguardConfig, vpnBuilder, protect) - state.value = CONNECTED } protected open fun parseConfig(config: JSONObject): WireguardConfig { - val configDataJson = config.getJSONObject("wireguard_config_data") - val configData = parseConfigData(configDataJson.getString("config")) + val configData = config.getJSONObject("wireguard_config_data") return WireguardConfig.build { - configWireguard(configData, configDataJson) + configWireguard(config, configData) configSplitTunneling(config) configAppSplitTunneling(config) } } - protected fun WireguardConfig.Builder.configWireguard(configData: Map, configDataJson: JSONObject) { - configData["Address"]?.split(",")?.map { address -> + protected fun WireguardConfig.Builder.configWireguard(config: JSONObject, configData: JSONObject) { + configData.getString("client_ip").split(",").map { address -> InetNetwork.parse(address.trim()) - }?.forEach(::addAddress) + }.forEach(::addAddress) - configData["DNS"]?.split(",")?.map { dns -> - parseInetAddress(dns.trim()) - }?.forEach(::addDnsServer) + config.optStringOrNull("dns1")?.let { dns -> + addDnsServer(parseInetAddress(dns.trim())) + } + + config.optStringOrNull("dns2")?.let { dns -> + addDnsServer(parseInetAddress(dns.trim())) + } val defRoutes = hashSetOf( InetNetwork("0.0.0.0", 0), InetNetwork("::", 0) ) val routes = hashSetOf() - configData["AllowedIPs"]?.split(",")?.map { route -> + configData.getJSONArray("allowed_ips").asSequence().map { route -> InetNetwork.parse(route.trim()) - }?.forEach(routes::add) + }.forEach(routes::add) // if the allowed IPs list contains at least one non-default route, disable global split tunneling if (routes.any { it !in defRoutes }) disableSplitTunneling() addRoutes(routes) - configDataJson.optString("mtu").let { mtu -> - if (mtu.isNotEmpty()) { - setMtu(mtu.toInt()) - } else { - configData["MTU"]?.let { setMtu(it.toInt()) } - } + configData.optStringOrNull("mtu")?.let { setMtu(it.toInt()) } + + val host = configData.getString("hostName").let { parseInetAddress(it.trim()) } + val port = configData.getInt("port") + setEndpoint(InetEndpoint(host, port)) + + if (configData.optBoolean("isObfuscationEnabled")) { + setUseProtocolExtension(true) + configExtensionParameters(configData) } - configData["Endpoint"]?.let { setEndpoint(InetEndpoint.parse(it)) } - configData["PersistentKeepalive"]?.let { setPersistentKeepalive(it.toInt()) } - configData["PrivateKey"]?.let { setPrivateKeyHex(it.base64ToHex()) } - configData["PublicKey"]?.let { setPublicKeyHex(it.base64ToHex()) } - configData["PresharedKey"]?.let { setPreSharedKeyHex(it.base64ToHex()) } + configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) } + configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) } + configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) } + configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) } } - protected fun parseConfigData(data: String): Map { - val parsedData = TreeMap(String.CASE_INSENSITIVE_ORDER) - data.lineSequence() - .filter { it.isNotEmpty() && !it.startsWith('[') } - .forEach { line -> - val attr = line.split("=", limit = 2) - parsedData[attr.first().trim()] = attr.last().trim() - } - return parsedData + protected fun WireguardConfig.Builder.configExtensionParameters(configData: JSONObject) { + configData.optStringOrNull("Jc")?.let { setJc(it.toInt()) } + configData.optStringOrNull("Jmin")?.let { setJmin(it.toInt()) } + configData.optStringOrNull("Jmax")?.let { setJmax(it.toInt()) } + configData.optStringOrNull("S1")?.let { setS1(it.toInt()) } + configData.optStringOrNull("S2")?.let { setS2(it.toInt()) } + configData.optStringOrNull("H1")?.let { setH1(it.toLong()) } + configData.optStringOrNull("H2")?.let { setH2(it.toLong()) } + configData.optStringOrNull("H3")?.let { setH3(it.toLong()) } + configData.optStringOrNull("H4")?.let { setH4(it.toLong()) } } private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) { @@ -168,6 +152,43 @@ open class Wireguard : Protocol() { tunnelHandle = -1 throw VpnStartException("Protect VPN interface: permission not granted or revoked") } + launchStatusJob() + } + + private fun launchStatusJob() { + Log.d(TAG, "Launch status job") + statusJob = scope.launch { + while (true) { + val lastHandshake = getLastHandshake() + Log.v(TAG, "lastHandshake=$lastHandshake") + if (lastHandshake == 0L) { + delay(1000) + continue + } + if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED + else if (lastHandshake == -1L) state.value = DISCONNECTED + statusJob = null + break + } + } + } + + private fun getLastHandshake(): Long { + if (tunnelHandle == -1) { + Log.e(TAG, "Trying to get config of a non-existent tunnel") + return -1 + } + val config = GoBackend.awgGetConfig(tunnelHandle) + if (config == null) { + Log.e(TAG, "Failed to get tunnel config") + return -2 + } + val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong() + if (lastHandshake == null) { + Log.e(TAG, "Failed to get last_handshake_time_sec") + return -2 + } + return lastHandshake } override fun stopVpn() { @@ -175,6 +196,8 @@ open class Wireguard : Protocol() { Log.w(TAG, "Tunnel already down") return } + statusJob?.cancel() + statusJob = null val handleToClose = tunnelHandle tunnelHandle = -1 GoBackend.awgTurnOff(handleToClose) diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt index 09269f54..7ae3d43b 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt @@ -1,6 +1,7 @@ package org.amnezia.vpn.protocol.wireguard import android.util.Base64 +import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.ProtocolConfig import org.amnezia.vpn.util.net.InetEndpoint @@ -12,7 +13,17 @@ open class WireguardConfig protected constructor( val persistentKeepalive: Int, val publicKeyHex: String, val preSharedKeyHex: String?, - val privateKeyHex: String + val privateKeyHex: String, + val useProtocolExtension: Boolean, + val jc: Int?, + val jmin: Int?, + val jmax: Int?, + val s1: Int?, + val s2: Int?, + val h1: Long?, + val h2: Long?, + val h3: Long?, + val h4: Long? ) : ProtocolConfig(protocolConfigBuilder) { protected constructor(builder: Builder) : this( @@ -21,7 +32,17 @@ open class WireguardConfig protected constructor( builder.persistentKeepalive, builder.publicKeyHex, builder.preSharedKeyHex, - builder.privateKeyHex + builder.privateKeyHex, + builder.useProtocolExtension, + builder.jc, + builder.jmin, + builder.jmax, + builder.s1, + builder.s2, + builder.h1, + builder.h2, + builder.h3, + builder.h4 ) fun toWgUserspaceString(): String = with(StringBuilder()) { @@ -33,6 +54,30 @@ open class WireguardConfig protected constructor( open fun appendDeviceLine(sb: StringBuilder) = with(sb) { appendLine("private_key=$privateKeyHex") + if (useProtocolExtension) { + validateProtocolExtensionParameters() + appendLine("jc=$jc") + appendLine("jmin=$jmin") + appendLine("jmax=$jmax") + appendLine("s1=$s1") + appendLine("s2=$s2") + appendLine("h1=$h1") + appendLine("h2=$h2") + appendLine("h3=$h3") + appendLine("h4=$h4") + } + } + + private fun validateProtocolExtensionParameters() { + if (jc == null) throw BadConfigException("Parameter jc is undefined") + if (jmin == null) throw BadConfigException("Parameter jmin is undefined") + if (jmax == null) throw BadConfigException("Parameter jmax is undefined") + if (s1 == null) throw BadConfigException("Parameter s1 is undefined") + if (s2 == null) throw BadConfigException("Parameter s2 is undefined") + if (h1 == null) throw BadConfigException("Parameter h1 is undefined") + if (h2 == null) throw BadConfigException("Parameter h2 is undefined") + if (h3 == null) throw BadConfigException("Parameter h3 is undefined") + if (h4 == null) throw BadConfigException("Parameter h4 is undefined") } open fun appendPeerLine(sb: StringBuilder) = with(sb) { @@ -65,6 +110,18 @@ open class WireguardConfig protected constructor( override var mtu: Int = WIREGUARD_DEFAULT_MTU + internal var useProtocolExtension: Boolean = false + + internal var jc: Int? = null + internal var jmin: Int? = null + internal var jmax: Int? = null + internal var s1: Int? = null + internal var s2: Int? = null + internal var h1: Long? = null + internal var h2: Long? = null + internal var h3: Long? = null + internal var h4: Long? = null + fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint } fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive } @@ -75,6 +132,18 @@ open class WireguardConfig protected constructor( fun setPrivateKeyHex(privateKeyHex: String) = apply { this.privateKeyHex = privateKeyHex } + fun setUseProtocolExtension(useProtocolExtension: Boolean) = apply { this.useProtocolExtension = useProtocolExtension } + + fun setJc(jc: Int) = apply { this.jc = jc } + fun setJmin(jmin: Int) = apply { this.jmin = jmin } + fun setJmax(jmax: Int) = apply { this.jmax = jmax } + fun setS1(s1: Int) = apply { this.s1 = s1 } + fun setS2(s2: Int) = apply { this.s2 = s2 } + fun setH1(h1: Long) = apply { this.h1 = h1 } + fun setH2(h2: Long) = apply { this.h2 = h2 } + fun setH3(h3: Long) = apply { this.h3 = h3 } + fun setH4(h4: Long) = apply { this.h4 = h4 } + override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) } } diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index b4d0b51f..08242525 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -5,6 +5,7 @@ import android.net.VpnService.Builder import java.io.File import java.io.IOException import go.Seq +import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED @@ -16,72 +17,10 @@ import org.amnezia.vpn.protocol.xray.libXray.Logger import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.net.InetNetwork +import org.amnezia.vpn.util.net.ip import org.amnezia.vpn.util.net.parseInetAddress import org.json.JSONObject -/** - * Config example: - * { - * "appSplitTunnelType": 0, - * "config_version": 0, - * "description": "Server 1", - * "dns1": "1.1.1.1", - * "dns2": "1.0.0.1", - * "hostName": "100.100.100.0", - * "protocol": "xray", - * "splitTunnelApps": [], - * "splitTunnelSites": [], - * "splitTunnelType": 0, - * "xray_config_data": { - * "inbounds": [ - * { - * "listen": "127.0.0.1", - * "port": 8080, - * "protocol": "socks", - * "settings": { - * "udp": true - * } - * } - * ], - * "log": { - * "loglevel": "error" - * }, - * "outbounds": [ - * { - * "protocol": "vless", - * "settings": { - * "vnext": [ - * { - * "address": "100.100.100.0", - * "port": 443, - * "users": [ - * { - * "encryption": "none", - * "flow": "xtls-rprx-vision", - * "id": "id" - * } - * ] - * } - * ] - * }, - * "streamSettings": { - * "network": "tcp", - * "realitySettings": { - * "fingerprint": "chrome", - * "publicKey": "publicKey", - * "serverName": "google.com", - * "shortId": "id", - * "spiderX": "" - * }, - * "security": "reality" - * } - * } - * ] - * } - * } - * - */ - private const val TAG = "Xray" private const val LIBXRAY_TAG = "libXray" @@ -108,25 +47,30 @@ class Xray : Protocol() { } } - override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { if (isRunning) { Log.w(TAG, "XRay already running") return } - val xrayJsonConfig = config.getJSONObject("xray_config_data") + val xrayJsonConfig = config.optJSONObject("xray_config_data") + ?: config.optJSONObject("ssxray_config_data") + ?: throw BadConfigException("config_data not found") val xrayConfig = parseConfig(config, xrayJsonConfig) - // for debug - // xrayJsonConfig.getJSONObject("log").put("loglevel", "debug") - xrayJsonConfig.getJSONObject("log").put("loglevel", "warning") - // disable access log - xrayJsonConfig.getJSONObject("log").put("access", "none") + (xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) }) + .put("loglevel", "warning") + .put("access", "none") // disable access log - // replace socks address - // (xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject).put("listen", "::1") + var xrayJsonConfigString = xrayJsonConfig.toString() + config.getString("hostName").let { hostName -> + val ipAddress = parseInetAddress(hostName).ip + if (hostName != ipAddress) { + xrayJsonConfigString = xrayJsonConfigString.replace(hostName, ipAddress) + } + } - start(xrayConfig, xrayJsonConfig.toString(), vpnBuilder, protect) + start(xrayConfig, xrayJsonConfigString, vpnBuilder, protect) state.value = CONNECTED isRunning = true } @@ -186,8 +130,8 @@ class Xray : Protocol() { LibXray.initXray(assetsPath) val geoDir = File(assetsPath, "geo").absolutePath val configPath = File(context.cacheDir, "config.json") - Log.d(TAG, "xray.location.asset: $geoDir") - Log.d(TAG, "config: $configPath") + Log.v(TAG, "xray.location.asset: $geoDir") + Log.v(TAG, "config: $configPath") try { configPath.writeText(configJson) } catch (e: IOException) { @@ -228,6 +172,10 @@ class Xray : Protocol() { throw VpnStartException("Failed to start tun2socks: $err") } } + + companion object { + val instance: Xray by lazy { Xray() } + } } private fun String?.isNotNullOrBlank(block: (String) -> Unit) { diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index ec544764..2b5036c5 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -2,15 +2,11 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}") -if(NOT IOS AND NOT ANDROID) - include(${CLIENT_ROOT_DIR}/3rd/SingleApplication/singleapplication.cmake) -endif() - add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel) set(LIBS ${LIBS} SortFilterProxyModel) +include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake) include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake) -include(${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/QSimpleCrypto.cmake) set(LIBSSH_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/") set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/") @@ -83,13 +79,12 @@ set(BUILD_WITH_QT6 ON) add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain) set(LIBS ${LIBS} qt6keychain) - include_directories( ${OPENSSL_INCLUDE_DIR} ${LIBSSH_INCLUDE_DIR}/include ${LIBSSH_ROOT_DIR}/include ${CLIENT_ROOT_DIR}/3rd/libssh/include - ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/include + ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include ${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include diff --git a/client/cmake/QSimpleCrypto.cmake b/client/cmake/QSimpleCrypto.cmake new file mode 100644 index 00000000..ec43cb83 --- /dev/null +++ b/client/cmake/QSimpleCrypto.cmake @@ -0,0 +1,21 @@ +set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) +set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src) + +include_directories(${QSIMPLECRYPTO_DIR}) + +set(HEADERS ${HEADERS} + ${QSIMPLECRYPTO_DIR}/include/QAead.h + ${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h + ${QSIMPLECRYPTO_DIR}/include/QRsa.h + ${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h + ${QSIMPLECRYPTO_DIR}/include/QX509.h + ${QSIMPLECRYPTO_DIR}/include/QX509Store.h +) + +set(SOURCES ${SOURCES} + ${QSIMPLECRYPTO_DIR}/sources/QAead.cpp + ${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp + ${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp + ${QSIMPLECRYPTO_DIR}/sources/QX509.cpp + ${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp +) diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 13c357bd..34ca5bff 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -1,6 +1,6 @@ message("Client android ${CMAKE_ANDROID_ARCH_ABI} build") -set(APP_ANDROID_MIN_SDK 24) +set(APP_ANDROID_MIN_SDK 26) set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING "The minimum API level supported by the application or library" FORCE) @@ -27,7 +27,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.h ) @@ -35,7 +34,6 @@ set(HEADERS ${HEADERS} set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp ) diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 5fda3506..58192237 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -76,11 +76,7 @@ set_target_properties(${PROJECT} PROPERTIES XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks" XCODE_EMBED_APP_EXTENSIONS networkextension - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution" - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development" - XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual - XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "match AppStore org.amnezia.AmneziaVPN" - XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "match Development org.amnezia.AmneziaVPN" + XCODE_ATTRIBUTE_CODE_SIGN_STYLE Automatic ) set_target_properties(${PROJECT} PROPERTIES XCODE_ATTRIBUTE_SWIFT_VERSION "5.0" @@ -126,9 +122,9 @@ add_subdirectory(ios/networkextension) add_dependencies(${PROJECT} networkextension) set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS - "${CMAKE_CURRENT_SOURCE_DIR}/3rd/OpenVPNAdapter/build/Release-iphoneos/OpenVPNAdapter.framework" + "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework" ) -set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd/OpenVPNAdapter/build/Release-iphoneos) -target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd/OpenVPNAdapter/build/Release-iphoneos/OpenVPNAdapter.framework") +set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/) +target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework") diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake new file mode 100644 index 00000000..c3af531a --- /dev/null +++ b/client/cmake/sources.cmake @@ -0,0 +1,191 @@ +set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..) + +set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/migrations.h + ${CLIENT_ROOT_DIR}/../ipc/ipc.h + ${CLIENT_ROOT_DIR}/amnezia_application.h + ${CLIENT_ROOT_DIR}/containers/containers_defs.h + ${CLIENT_ROOT_DIR}/core/defs.h + ${CLIENT_ROOT_DIR}/core/errorstrings.h + ${CLIENT_ROOT_DIR}/core/scripts_registry.h + ${CLIENT_ROOT_DIR}/core/server_defs.h + ${CLIENT_ROOT_DIR}/core/api/apiDefs.h + ${CLIENT_ROOT_DIR}/core/qrCodeUtils.h + ${CLIENT_ROOT_DIR}/core/controllers/coreController.h + ${CLIENT_ROOT_DIR}/core/controllers/gatewayController.h + ${CLIENT_ROOT_DIR}/core/controllers/serverController.h + ${CLIENT_ROOT_DIR}/core/controllers/vpnConfigurationController.h + ${CLIENT_ROOT_DIR}/protocols/protocols_defs.h + ${CLIENT_ROOT_DIR}/protocols/qml_register_protocols.h + ${CLIENT_ROOT_DIR}/ui/pages.h + ${CLIENT_ROOT_DIR}/ui/qautostart.h + ${CLIENT_ROOT_DIR}/protocols/vpnprotocol.h + ${CMAKE_CURRENT_BINARY_DIR}/version.h + ${CLIENT_ROOT_DIR}/core/sshclient.h + ${CLIENT_ROOT_DIR}/core/networkUtilities.h + ${CLIENT_ROOT_DIR}/core/serialization/serialization.h + ${CLIENT_ROOT_DIR}/core/serialization/transfer.h + ${CLIENT_ROOT_DIR}/../common/logger/logger.h + ${CLIENT_ROOT_DIR}/utils/qmlUtils.h + ${CLIENT_ROOT_DIR}/core/api/apiUtils.h +) + +# Mozilla headres +set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/mozilla/models/server.h + ${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.h + ${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.h + ${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h + ${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h +) + +if(NOT IOS) + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h + ) +endif() + +if(NOT ANDROID) + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/ui/notificationhandler.h + ) +endif() + +set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/migrations.cpp + ${CLIENT_ROOT_DIR}/amnezia_application.cpp + ${CLIENT_ROOT_DIR}/containers/containers_defs.cpp + ${CLIENT_ROOT_DIR}/core/errorstrings.cpp + ${CLIENT_ROOT_DIR}/core/scripts_registry.cpp + ${CLIENT_ROOT_DIR}/core/server_defs.cpp + ${CLIENT_ROOT_DIR}/core/qrCodeUtils.cpp + ${CLIENT_ROOT_DIR}/core/controllers/coreController.cpp + ${CLIENT_ROOT_DIR}/core/controllers/gatewayController.cpp + ${CLIENT_ROOT_DIR}/core/controllers/serverController.cpp + ${CLIENT_ROOT_DIR}/core/controllers/vpnConfigurationController.cpp + ${CLIENT_ROOT_DIR}/protocols/protocols_defs.cpp + ${CLIENT_ROOT_DIR}/ui/qautostart.cpp + ${CLIENT_ROOT_DIR}/protocols/vpnprotocol.cpp + ${CLIENT_ROOT_DIR}/core/sshclient.cpp + ${CLIENT_ROOT_DIR}/core/networkUtilities.cpp + ${CLIENT_ROOT_DIR}/core/serialization/outbound.cpp + ${CLIENT_ROOT_DIR}/core/serialization/inbound.cpp + ${CLIENT_ROOT_DIR}/core/serialization/ss.cpp + ${CLIENT_ROOT_DIR}/core/serialization/ssd.cpp + ${CLIENT_ROOT_DIR}/core/serialization/vless.cpp + ${CLIENT_ROOT_DIR}/core/serialization/trojan.cpp + ${CLIENT_ROOT_DIR}/core/serialization/vmess.cpp + ${CLIENT_ROOT_DIR}/core/serialization/vmess_new.cpp + ${CLIENT_ROOT_DIR}/../common/logger/logger.cpp + ${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp + ${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp +) + +# Mozilla sources +set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/mozilla/models/server.cpp + ${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.cpp + ${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp + ${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp +) + +if(NOT IOS) + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp + ) +endif() + +if(NOT ANDROID) + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/ui/notificationhandler.cpp + ) +endif() + +file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/*.h) +file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/*.cpp) + +file(GLOB_RECURSE PAGE_LOGIC_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/pages_logic/*.h) +file(GLOB_RECURSE PAGE_LOGIC_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/pages_logic/*.cpp) + +file(GLOB CONFIGURATORS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/configurators/*.h) +file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/configurators/*.cpp) + +file(GLOB UI_MODELS_H CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/models/*.h + ${CLIENT_ROOT_DIR}/ui/models/protocols/*.h + ${CLIENT_ROOT_DIR}/ui/models/services/*.h + ${CLIENT_ROOT_DIR}/ui/models/api/*.h +) +file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/models/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/services/*.cpp + ${CLIENT_ROOT_DIR}/ui/models/api/*.cpp +) + +file(GLOB UI_CONTROLLERS_H CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/controllers/*.h + ${CLIENT_ROOT_DIR}/ui/controllers/api/*.h +) +file(GLOB UI_CONTROLLERS_CPP CONFIGURE_DEPENDS + ${CLIENT_ROOT_DIR}/ui/controllers/*.cpp + ${CLIENT_ROOT_DIR}/ui/controllers/api/*.cpp +) + +set(HEADERS ${HEADERS} + ${COMMON_FILES_H} + ${PAGE_LOGIC_H} + ${CONFIGURATORS_H} + ${UI_MODELS_H} + ${UI_CONTROLLERS_H} +) +set(SOURCES ${SOURCES} + ${COMMON_FILES_CPP} + ${PAGE_LOGIC_CPP} + ${CONFIGURATORS_CPP} + ${UI_MODELS_CPP} + ${UI_CONTROLLERS_CPP} +) + +if(WIN32) + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/protocols/ikev2_vpn_protocol_windows.h + ) + + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/protocols/ikev2_vpn_protocol_windows.cpp + ) + + set(RESOURCES ${RESOURCES} + ${CMAKE_CURRENT_BINARY_DIR}/amneziavpn.rc + ) +endif() + +if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) + message("Client desktop build") + add_compile_definitions(AMNEZIA_DESKTOP) + + set(HEADERS ${HEADERS} + ${CLIENT_ROOT_DIR}/core/ipcclient.h + ${CLIENT_ROOT_DIR}/core/privileged_process.h + ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h + ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h + ${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h + ${CLIENT_ROOT_DIR}/protocols/shadowsocksvpnprotocol.h + ${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.h + ${CLIENT_ROOT_DIR}/protocols/xrayprotocol.h + ${CLIENT_ROOT_DIR}/protocols/awgprotocol.h + ) + + set(SOURCES ${SOURCES} + ${CLIENT_ROOT_DIR}/core/ipcclient.cpp + ${CLIENT_ROOT_DIR}/core/privileged_process.cpp + ${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp + ${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/shadowsocksvpnprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/xrayprotocol.cpp + ${CLIENT_ROOT_DIR}/protocols/awgprotocol.cpp + ) +endif() diff --git a/client/configurators/openvpn_configurator.cpp b/client/configurators/openvpn_configurator.cpp index c4bdf860..fafb7c2b 100644 --- a/client/configurators/openvpn_configurator.cpp +++ b/client/configurators/openvpn_configurator.cpp @@ -119,18 +119,21 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPairisSitesSplitTunnelingEnabled()) { config.append("\nredirect-gateway def1 ipv6 bypass-dhcp\n"); + +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) // Prevent ipv6 leak config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); +#endif config.append("block-ipv6\n"); } else if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { // no redirect-gateway } else if (m_settings->routeMode() == Settings::VpnAllExceptSites) { -#ifndef Q_OS_ANDROID +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n"); -#endif // Prevent ipv6 leak config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n"); +#endif config.append("block-ipv6\n"); } } diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index f7faaa52..69699998 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -19,13 +20,17 @@ #include "settings.h" #include "utilities.h" -WireguardConfigurator::WireguardConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, - bool isAwg, QObject *parent) +WireguardConfigurator::WireguardConfigurator(std::shared_ptr settings, + const QSharedPointer &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 WireguardConfigurator::getIpsFromConf(const QString &input) +{ + QRegularExpression regex("AllowedIPs = (\\d+\\.\\d+\\.\\d+\\.\\d+)"); + QRegularExpressionMatchIterator matchIterator = regex.globalMatch(input); + + QList 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,53 +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); - - // 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(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(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; @@ -149,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; } @@ -161,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) { @@ -187,21 +208,25 @@ QString WireguardConfigurator::createConfig(const ServerCredentials &credentials jConfig[config_key::server_pub_key] = connData.serverPubKey; jConfig[config_key::mtu] = wireguarConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); + jConfig[config_key::persistent_keep_alive] = "25"; + QJsonArray allowedIps { "0.0.0.0/0", "::/0" }; + jConfig[config_key::allowed_ips] = allowedIps; + jConfig[config_key::clientId] = connData.clientPubKey; return QJsonDocument(jConfig).toJson(); } -QString WireguardConfigurator::processConfigWithLocalSettings(const QPair &dns, const bool isApiConfig, - QString &protocolConfigString) +QString WireguardConfigurator::processConfigWithLocalSettings(const QPair &dns, + const bool isApiConfig, QString &protocolConfigString) { processConfigWithDnsSettings(dns, protocolConfigString); return protocolConfigString; } -QString WireguardConfigurator::processConfigWithExportSettings(const QPair &dns, const bool isApiConfig, - QString &protocolConfigString) +QString WireguardConfigurator::processConfigWithExportSettings(const QPair &dns, + const bool isApiConfig, QString &protocolConfigString) { processConfigWithDnsSettings(dns, protocolConfigString); diff --git a/client/configurators/wireguard_configurator.h b/client/configurators/wireguard_configurator.h index 22e8a8be..a4302e3e 100644 --- a/client/configurators/wireguard_configurator.h +++ b/client/configurators/wireguard_configurator.h @@ -1,6 +1,7 @@ #ifndef WIREGUARD_CONFIGURATOR_H #define WIREGUARD_CONFIGURATOR_H +#include #include #include @@ -12,8 +13,8 @@ class WireguardConfigurator : public ConfiguratorBase { Q_OBJECT public: - WireguardConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, bool isAwg, - QObject *parent = nullptr); + WireguardConfigurator(std::shared_ptr settings, const QSharedPointer &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 &dns, const bool isApiConfig, QString &protocolConfigString); - QString processConfigWithExportSettings(const QPair &dns, const bool isApiConfig, QString &protocolConfigString); + QString processConfigWithLocalSettings(const QPair &dns, const bool isApiConfig, + QString &protocolConfigString); + QString processConfigWithExportSettings(const QPair &dns, const bool isApiConfig, + QString &protocolConfigString); static ConnectionData genClientKeys(); private: + QList getIpsFromConf(const QString &input); ConnectionData prepareWireguardConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c..514aa821 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -3,38 +3,169 @@ #include #include #include +#include +#include "logger.h" #include "containers/containers_defs.h" #include "core/controllers/serverController.h" #include "core/scripts_registry.h" +namespace { +Logger logger("XrayConfigurator"); +} + XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent) : ConfiguratorBase(settings, serverController, parent) { } -QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode &errorCode) +QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) { - QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), - m_serverController->genVarsForScript(credentials, container, containerConfig)); - - QString xrayPublicKey = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - xrayPublicKey.replace("\n", ""); - - QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode); - xrayUuid.replace("\n", ""); - - QString xrayShortId = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); - xrayShortId.replace("\n", ""); - + // Generate new UUID for client + QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Get current server config + QString currentConfig = m_serverController->getTextFileFromContainer( + container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to get server config file"; return ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + // Parse current config as JSON + QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + logger.error() << "Failed to parse server config JSON"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject serverConfig = doc.object(); + + // Validate server config structure + if (!serverConfig.contains("inbounds")) { + logger.error() << "Server config missing 'inbounds' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray inbounds = serverConfig["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Server config has empty 'inbounds' array"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Inbound missing 'settings' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Settings missing 'clients' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray clients = settings["clients"].toArray(); + + // Create configuration for new client + QJsonObject clientConfig { + {"id", clientId}, + {"flow", "xtls-rprx-vision"} + }; + + clients.append(clientConfig); + + // Update config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + serverConfig["inbounds"] = inbounds; + + // Save updated config to server + QString updatedConfig = QJsonDocument(serverConfig).toJson(); + errorCode = m_serverController->uploadTextFileToContainer( + container, + credentials, + updatedConfig, + amnezia::protocols::xray::serverConfigPath, + libssh::ScpOverwriteMode::ScpOverwriteExisting + ); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to upload updated config"; + return ""; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + errorCode = m_serverController->runScript( + credentials, + m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container)) + ); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to restart container"; + return ""; + } + + return clientId; +} + +QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) +{ + // Get client ID from prepareServerConfig + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode); + if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { + logger.error() << "Failed to prepare server config"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), + m_serverController->genVarsForScript(credentials, container, containerConfig)); + + if (config.isEmpty()) { + logger.error() << "Failed to get config template"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString xrayPublicKey = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayPublicKey.replace("\n", ""); + + QString xrayShortId = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayShortId.replace("\n", ""); + + // Validate all required variables are present + if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { + logger.error() << "Config template missing required variables:" + << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") + << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") + << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); + errorCode = ErrorCode::InternalError; + return ""; + } + + config.replace("$XRAY_CLIENT_ID", xrayClientId); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_SHORT_ID", xrayShortId); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 2acfdf71..8ed4e775 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ public: QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp index 2f2f8367..52b148c0 100644 --- a/client/containers/containers_defs.cpp +++ b/client/containers/containers_defs.cpp @@ -90,13 +90,13 @@ QMap ContainerProps::containerHumanNames() { return { { DockerContainer::None, "Not installed" }, { DockerContainer::OpenVpn, "OpenVPN" }, - { DockerContainer::ShadowSocks, "ShadowSocks" }, + { DockerContainer::ShadowSocks, "OpenVPN over SS" }, { DockerContainer::Cloak, "OpenVPN over Cloak" }, { DockerContainer::WireGuard, "WireGuard" }, { DockerContainer::Awg, "AmneziaWG" }, { DockerContainer::Xray, "XRay" }, { DockerContainer::Ipsec, QObject::tr("IPsec") }, - { DockerContainer::SSXray, "ShadowSocks"}, + { DockerContainer::SSXray, "Shadowsocks"}, { DockerContainer::TorWebSite, QObject::tr("Website in Tor network") }, { DockerContainer::Dns, QObject::tr("AmneziaDNS") }, @@ -110,22 +110,19 @@ QMap ContainerProps::containerDescriptions() QObject::tr("OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its " "own security protocol with SSL/TLS for key exchange.") }, { DockerContainer::ShadowSocks, - QObject::tr("Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it " - "may be recognized by analysis systems in some highly censored regions.") }, + QObject::tr("Shadowsocks masks VPN traffic, making it resemble normal web traffic, but it may still be detected by certain analysis systems.") }, { DockerContainer::Cloak, QObject::tr("OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against " - "active-probing detection. Ideal for bypassing blocking in regions with the highest levels " - "of censorship.") }, + "active-probing detection. It is very resistant to detection, but offers low speed.") }, { DockerContainer::WireGuard, - QObject::tr("WireGuard - New popular VPN protocol with high performance, high speed and low power " - "consumption. Recommended for regions with low levels of censorship.") }, + QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power " + "consumption.") }, { DockerContainer::Awg, - QObject::tr("AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, " - "but very resistant to blockages. " - "Recommended for regions with high levels of censorship.") }, + QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. " + "It provides high connection speed and ensures stable operation even in the most challenging network conditions.") }, { DockerContainer::Xray, - QObject::tr("XRay with REALITY - Suitable for countries with the highest level of internet censorship. " - "Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods.") }, + QObject::tr("XRay with REALITY masks VPN traffic as web traffic and protects against active probing. " + "It is highly resistant to detection and offers high speed.") }, { DockerContainer::Ipsec, QObject::tr("IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after " "signal loss. It has native support on the latest versions of Android and iOS.") }, @@ -144,20 +141,20 @@ QMap ContainerProps::containerDetailedDescriptions() return { { DockerContainer::OpenVpn, QObject::tr( - "OpenVPN stands as one of the most popular and time-tested VPN protocols available.\n" - "It employs its unique security protocol, " - "leveraging the strength of SSL/TLS for encryption and key exchange. " - "Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, " - "catering to a wide range of devices and operating systems. " - "Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, " - "which continually reinforces its security. " - "With a strong balance of performance, security, and compatibility, " - "OpenVPN remains a top choice for privacy-conscious individuals and businesses alike.\n\n" - "* Available in the AmneziaVPN across all platforms\n" - "* Normal power consumption on mobile devices\n" - "* Flexible customisation to suit user needs to work with different operating systems and devices\n" - "* Recognised by DPI analysis systems and therefore susceptible to blocking\n" - "* Can operate over both TCP and UDP network protocols.") }, + "OpenVPN stands as one of the most popular and time-tested VPN protocols available.\n" + "It employs its unique security protocol, " + "leveraging the strength of SSL/TLS for encryption and key exchange. " + "Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, " + "catering to a wide range of devices and operating systems. " + "Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, " + "which continually reinforces its security. " + "With a strong balance of performance, security, and compatibility, " + "OpenVPN remains a top choice for privacy-conscious individuals and businesses alike.\n\n" + "* Available in the AmneziaVPN across all platforms\n" + "* Normal power consumption on mobile devices\n" + "* Flexible customisation to suit user needs to work with different operating systems and devices\n" + "* Recognised by DPI systems and therefore susceptible to blocking\n" + "* Can operate over both TCP and UDP network protocols.") }, { DockerContainer::ShadowSocks, QObject::tr("Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. " "Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection." @@ -169,28 +166,26 @@ QMap ContainerProps::containerDetailedDescriptions() "* Works over TCP network protocol.") }, { DockerContainer::Cloak, QObject::tr("This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for " - "protecting against blocking.\n\n" + "protecting against detection.\n\n" "OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client " "and the server.\n\n" - "Cloak protects OpenVPN from detection and blocking. \n\n" + "Cloak protects OpenVPN from detection. \n\n" "Cloak can modify packet metadata so that it completely masks VPN traffic as normal web traffic, " "and also protects the VPN from detection by Active Probing. This makes it very resistant to " "being detected\n\n" "Immediately after receiving the first data packet, Cloak authenticates the incoming connection. " "If authentication fails, the plugin masks the server as a fake website and your VPN becomes " "invisible to analysis systems.\n\n" - "If there is a extreme level of Internet censorship in your region, we advise you to use only " - "OpenVPN over Cloak from the first connection\n\n" "* Available in the AmneziaVPN across all platforms\n" "* High power consumption on mobile devices\n" "* Flexible settings\n" - "* Not recognised by DPI analysis systems\n" + "* Not recognised by detection systems\n" "* Works over TCP network protocol, 443 port.\n") }, { DockerContainer::WireGuard, QObject::tr("A relatively new popular VPN protocol with a simplified architecture.\n" "WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption " "settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput.\n" - "WireGuard is very susceptible to blocking due to its distinct packet signatures. " + "WireGuard is very susceptible to detection and blocking due to its distinct packet signatures. " "Unlike some other VPN protocols that employ obfuscation techniques, " "the consistent signature patterns of WireGuard packets can be more easily identified and " "thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools.\n\n" @@ -213,18 +208,18 @@ QMap ContainerProps::containerDetailedDescriptions() "* Available in the AmneziaVPN across all platforms\n" "* Low power consumption\n" "* Minimum number of settings\n" - "* Not recognised by DPI analysis systems, resistant to blocking\n" + "* Not recognised by traffic analysis systems\n" "* Works over UDP network protocol.") }, { DockerContainer::Xray, - QObject::tr("The REALITY protocol, a pioneering development by the creators of XRay, " - "is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion.\n" - "It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, " - "thus presenting an authentic TLS certificate and data. \n" - "This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, " - "legitimate sites without the need for specific configurations. \n" - "Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, " - "REALITY's innovative \"friend or foe\" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. " - "This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship.") + QObject::tr("The REALITY protocol, a pioneering development by the creators of XRay, " + "is designed to provide the highest level of protection against detection through its innovative approach to security and privacy.\n" + "It uniquely identifies attackers during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting attackers to genuine websites, " + "thus presenting an authentic TLS certificate and data. \n" + "This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, " + "legitimate sites without the need for specific configurations. \n" + "Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, " + "REALITY's innovative \"friend or foe\" recognition at the TLS handshake enhances security. " + "This makes REALITY a robust solution for maintaining internet freedom.") }, { DockerContainer::Ipsec, QObject::tr("IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol.\n" @@ -285,8 +280,9 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::WireGuard: return true; case DockerContainer::OpenVpn: return true; case DockerContainer::Awg: return true; - case DockerContainer::Cloak: - return true; + case DockerContainer::Xray: return true; + case DockerContainer::Cloak: return true; + case DockerContainer::SSXray: return true; // case DockerContainer::ShadowSocks: return true; default: return false; } @@ -305,6 +301,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::Awg: return true; case DockerContainer::Cloak: return true; case DockerContainer::Xray: return true; + case DockerContainer::SSXray: return true; default: return false; } @@ -330,9 +327,7 @@ QStringList ContainerProps::fixedPortsForContainer(DockerContainer c) bool ContainerProps::isEasySetupContainer(DockerContainer container) { switch (container) { - case DockerContainer::WireGuard: return true; case DockerContainer::Awg: return true; - // case DockerContainer::Cloak: return true; default: return false; } } @@ -340,9 +335,7 @@ bool ContainerProps::isEasySetupContainer(DockerContainer container) QString ContainerProps::easySetupHeader(DockerContainer container) { switch (container) { - case DockerContainer::WireGuard: return tr("Low"); - case DockerContainer::Awg: return tr("High"); - // case DockerContainer::Cloak: return tr("Extreme"); + case DockerContainer::Awg: return tr("Automatic"); default: return ""; } } @@ -350,10 +343,8 @@ QString ContainerProps::easySetupHeader(DockerContainer container) QString ContainerProps::easySetupDescription(DockerContainer container) { switch (container) { - case DockerContainer::WireGuard: return tr("I just want to increase the level of my privacy."); - case DockerContainer::Awg: return tr("I want to bypass censorship. This option recommended in most cases."); - // case DockerContainer::Cloak: - // return tr("Most VPN protocols are blocked. Recommended if other options are not working."); + case DockerContainer::Awg: return tr("AmneziaWG protocol will be installed. " + "It provides high connection speed and ensures stable operation even in the most challenging network conditions."); default: return ""; } } @@ -361,9 +352,7 @@ QString ContainerProps::easySetupDescription(DockerContainer container) int ContainerProps::easySetupOrder(DockerContainer container) { switch (container) { - case DockerContainer::WireGuard: return 3; - case DockerContainer::Awg: return 2; - // case DockerContainer::Cloak: return 1; + case DockerContainer::Awg: return 1; default: return 0; } } @@ -382,9 +371,24 @@ bool ContainerProps::isShareable(DockerContainer container) QJsonObject ContainerProps::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig) { QString protocolConfigString = containerConfig.value(ProtocolProps::protoToString(protocol)) - .toObject() - .value(config_key::last_config) - .toString(); + .toObject() + .value(config_key::last_config) + .toString(); return QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); } + +int ContainerProps::installPageOrder(DockerContainer container) +{ + switch (container) { + case DockerContainer::OpenVpn: return 4; + case DockerContainer::Cloak: return 5; + case DockerContainer::ShadowSocks: return 6; + case DockerContainer::WireGuard: return 2; + case DockerContainer::Awg: return 1; + case DockerContainer::Xray: return 3; + case DockerContainer::Ipsec: return 7; + case DockerContainer::SSXray: return 8; + default: return 0; + } +} diff --git a/client/containers/containers_defs.h b/client/containers/containers_defs.h index a63e217b..0d7f9aa1 100644 --- a/client/containers/containers_defs.h +++ b/client/containers/containers_defs.h @@ -72,6 +72,8 @@ namespace amnezia static bool isShareable(amnezia::DockerContainer container); static QJsonObject getProtocolConfigFromContainer(const amnezia::Proto protocol, const QJsonObject &containerConfig); + + static int installPageOrder(amnezia::DockerContainer container); }; static void declareQmlContainerEnum() diff --git a/client/core/api/apiDefs.h b/client/core/api/apiDefs.h new file mode 100644 index 00000000..41dd80ba --- /dev/null +++ b/client/core/api/apiDefs.h @@ -0,0 +1,51 @@ +#ifndef APIDEFS_H +#define APIDEFS_H + +#include + +namespace apiDefs +{ + enum ConfigType { + AmneziaFreeV2 = 0, + AmneziaFreeV3, + AmneziaPremiumV1, + AmneziaPremiumV2, + SelfHosted + }; + + enum ConfigSource { + Telegram = 1, + AmneziaGateway + }; + + namespace key + { + constexpr QLatin1String configVersion("config_version"); + + constexpr QLatin1String apiConfig("api_config"); + constexpr QLatin1String stackType("stack_type"); + constexpr QLatin1String serviceType("service_type"); + + constexpr QLatin1String vpnKey("vpn_key"); + + constexpr QLatin1String installationUuid("installation_uuid"); + constexpr QLatin1String workerLastUpdated("worker_last_updated"); + constexpr QLatin1String lastDownloaded("last_downloaded"); + constexpr QLatin1String sourceType("source_type"); + + constexpr QLatin1String serverCountryCode("server_country_code"); + constexpr QLatin1String serverCountryName("server_country_name"); + + constexpr QLatin1String osVersion("os_version"); + + constexpr QLatin1String availableCountries("available_countries"); + constexpr QLatin1String activeDeviceCount("active_device_count"); + constexpr QLatin1String maxDeviceCount("max_device_count"); + constexpr QLatin1String subscriptionEndDate("subscription_end_date"); + constexpr QLatin1String issuedConfigs("issued_configs"); + } + + const int requestTimeoutMsecs = 12 * 1000; // 12 secs +} + +#endif // APIDEFS_H diff --git a/client/core/api/apiUtils.cpp b/client/core/api/apiUtils.cpp new file mode 100644 index 00000000..9f518b52 --- /dev/null +++ b/client/core/api/apiUtils.cpp @@ -0,0 +1,87 @@ +#include "apiUtils.h" + +#include +#include + +bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate) +{ + QDateTime now = QDateTime::currentDateTime(); + QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs); + return endDate < now; +} + +bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject) +{ + auto configVersion = serverConfigObject.value(apiDefs::key::configVersion).toInt(); + switch (configVersion) { + case apiDefs::ConfigSource::Telegram: return true; + case apiDefs::ConfigSource::AmneziaGateway: return true; + default: return false; + } +} + +apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObject) +{ + auto configVersion = serverConfigObject.value(apiDefs::key::configVersion).toInt(); + switch (configVersion) { + case apiDefs::ConfigSource::Telegram: { + }; + case apiDefs::ConfigSource::AmneziaGateway: { + constexpr QLatin1String stackPremium("prem"); + constexpr QLatin1String stackFree("free"); + + constexpr QLatin1String servicePremium("amnezia-premium"); + constexpr QLatin1String serviceFree("amnezia-free"); + + auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); + auto stackType = apiConfigObject.value(apiDefs::key::stackType).toString(); + auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString(); + + if (serviceType == servicePremium || stackType == stackPremium) { + return apiDefs::ConfigType::AmneziaPremiumV2; + } else if (serviceType == serviceFree || stackType == stackFree) { + return apiDefs::ConfigType::AmneziaFreeV3; + } + } + default: { + return apiDefs::ConfigType::SelfHosted; + } + }; +} + +apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigObject) +{ + return static_cast(serverConfigObject.value(apiDefs::key::configVersion).toInt()); +} + +amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply) +{ + const int httpStatusCodeConflict = 409; + const int httpStatusCodeNotFound = 404; + + if (!sslErrors.empty()) { + qDebug().noquote() << sslErrors; + return amnezia::ErrorCode::ApiConfigSslError; + } else if (reply->error() == QNetworkReply::NoError) { + return amnezia::ErrorCode::NoError; + } else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + return amnezia::ErrorCode::ApiConfigTimeoutError; + } else { + QString err = reply->errorString(); + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << QString::fromUtf8(reply->readAll()); + qDebug() << reply->error(); + qDebug() << err; + qDebug() << httpStatusCode; + if (httpStatusCode == httpStatusCodeConflict) { + return amnezia::ErrorCode::ApiConfigLimitError; + } else if (httpStatusCode == httpStatusCodeNotFound) { + return amnezia::ErrorCode::ApiNotFoundError; + } + return amnezia::ErrorCode::ApiConfigDownloadError; + } + + qDebug() << "something went wrong"; + return amnezia::ErrorCode::InternalError; +} diff --git a/client/core/api/apiUtils.h b/client/core/api/apiUtils.h new file mode 100644 index 00000000..82ac315b --- /dev/null +++ b/client/core/api/apiUtils.h @@ -0,0 +1,22 @@ +#ifndef APIUTILS_H +#define APIUTILS_H + +#include +#include + +#include "apiDefs.h" +#include "core/defs.h" + +namespace apiUtils +{ + bool isServerFromApi(const QJsonObject &serverConfigObject); + + bool isSubscriptionExpired(const QString &subscriptionEndDate); + + apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); + apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); + + amnezia::ErrorCode checkNetworkReplyErrors(const QList &sslErrors, QNetworkReply *reply); +} + +#endif // APIUTILS_H diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp deleted file mode 100644 index 35b459be..00000000 --- a/client/core/controllers/apiController.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "apiController.h" - -#include -#include -#include -#include - -#include "amnezia_application.h" -#include "configurators/wireguard_configurator.h" -#include "version.h" - -namespace -{ - namespace configKey - { - constexpr char cloak[] = "cloak"; - constexpr char awg[] = "awg"; - - constexpr char apiEdnpoint[] = "api_endpoint"; - constexpr char accessToken[] = "api_key"; - constexpr char certificate[] = "certificate"; - constexpr char publicKey[] = "public_key"; - constexpr char protocol[] = "protocol"; - - constexpr char uuid[] = "installation_uuid"; - constexpr char osVersion[] = "os_version"; - constexpr char appVersion[] = "app_version"; - } -} - -ApiController::ApiController(QObject *parent) : QObject(parent) -{ -} - -void ApiController::processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config) -{ - if (protocol == configKey::cloak) { - config.replace("", "\n"); - config.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); - return; - } else if (protocol == configKey::awg) { - config.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); - auto serverConfig = QJsonDocument::fromJson(config.toUtf8()).object(); - auto containers = serverConfig.value(config_key::containers).toArray(); - if (containers.isEmpty()) { - return; - } - auto container = containers.at(0).toObject(); - QString containerName = ContainerProps::containerTypeToString(DockerContainer::Awg); - auto containerConfig = container.value(containerName).toObject(); - auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object(); - containerConfig[config_key::junkPacketCount] = protocolConfig.value(config_key::junkPacketCount); - containerConfig[config_key::junkPacketMinSize] = protocolConfig.value(config_key::junkPacketMinSize); - containerConfig[config_key::junkPacketMaxSize] = protocolConfig.value(config_key::junkPacketMaxSize); - containerConfig[config_key::initPacketJunkSize] = protocolConfig.value(config_key::initPacketJunkSize); - containerConfig[config_key::responsePacketJunkSize] = protocolConfig.value(config_key::responsePacketJunkSize); - containerConfig[config_key::initPacketMagicHeader] = protocolConfig.value(config_key::initPacketMagicHeader); - containerConfig[config_key::responsePacketMagicHeader] = protocolConfig.value(config_key::responsePacketMagicHeader); - containerConfig[config_key::underloadPacketMagicHeader] = protocolConfig.value(config_key::underloadPacketMagicHeader); - containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); - container[containerName] = containerConfig; - containers.replace(0, container); - serverConfig[config_key::containers] = containers; - config = QString(QJsonDocument(serverConfig).toJson()); - } - return; -} - -ApiController::ApiPayloadData ApiController::generateApiPayloadData(const QString &protocol) -{ - ApiController::ApiPayloadData apiPayload; - if (protocol == configKey::cloak) { - apiPayload.certRequest = OpenVpnConfigurator::createCertRequest(); - } else if (protocol == configKey::awg) { - auto connData = WireguardConfigurator::genClientKeys(); - apiPayload.wireGuardClientPubKey = connData.clientPubKey; - apiPayload.wireGuardClientPrivKey = connData.clientPrivKey; - } - return apiPayload; -} - -QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData) -{ - QJsonObject obj; - if (protocol == configKey::cloak) { - obj[configKey::certificate] = apiPayloadData.certRequest.request; - } else if (protocol == configKey::awg) { - obj[configKey::publicKey] = apiPayloadData.wireGuardClientPubKey; - } - - obj[configKey::osVersion] = QSysInfo::productType(); - obj[configKey::appVersion] = QString(APP_VERSION); - - return obj; -} - -void ApiController::updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig) -{ -#ifdef Q_OS_IOS - IosController::Instance()->requestInetAccess(); - QThread::msleep(10); -#endif - - auto containerConfig = serverConfig.value(config_key::containers).toArray(); - - if (serverConfig.value(config_key::configVersion).toInt()) { - QNetworkRequest request; - request.setTransferTimeout(7000); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); - QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); - request.setUrl(endpoint); - - QString protocol = serverConfig.value(configKey::protocol).toString(); - - ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); - - QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); - apiPayload[configKey::uuid] = installationUuid; - - QByteArray requestBody = QJsonDocument(apiPayload).toJson(); - - QNetworkReply *reply = amnApp->manager()->post(request, requestBody); // ?? - - QObject::connect(reply, &QNetworkReply::finished, [this, reply, protocol, apiPayloadData, serverIndex, serverConfig]() mutable { - if (reply->error() == QNetworkReply::NoError) { - QString contents = QString::fromUtf8(reply->readAll()); - QString data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString(); - - data.replace("vpn://", ""); - QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - - if (ba.isEmpty()) { - emit errorOccurred(ErrorCode::ApiConfigEmptyError); - return; - } - - QByteArray ba_uncompressed = qUncompress(ba); - if (!ba_uncompressed.isEmpty()) { - ba = ba_uncompressed; - } - - QString configStr = ba; - processApiConfig(protocol, apiPayloadData, configStr); - - QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); - serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1); - serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2); - serverConfig[config_key::containers] = apiConfig.value(config_key::containers); - serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName); - - auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString(); - serverConfig[config_key::defaultContainer] = defaultContainer; - - emit configUpdated(true, serverConfig, serverIndex); - } else { - if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError - || reply->error() == QNetworkReply::NetworkError::TimeoutError) { - emit errorOccurred(ErrorCode::ApiConfigTimeoutError); - } else { - QString err = reply->errorString(); - qDebug() << QString::fromUtf8(reply->readAll()); - qDebug() << reply->error(); - qDebug() << err; - qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - emit errorOccurred(ErrorCode::ApiConfigDownloadError); - } - } - - reply->deleteLater(); - }); - - QObject::connect(reply, &QNetworkReply::errorOccurred, - [this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; }); - connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList &errors) { - qDebug().noquote() << errors; - emit errorOccurred(ErrorCode::ApiConfigSslError); - }); - } -} diff --git a/client/core/controllers/apiController.h b/client/core/controllers/apiController.h deleted file mode 100644 index cc5d9f31..00000000 --- a/client/core/controllers/apiController.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef APICONTROLLER_H -#define APICONTROLLER_H - -#include - -#include "configurators/openvpn_configurator.h" - -#ifdef Q_OS_IOS - #include "platforms/ios/ios_controller.h" -#endif - -class ApiController : public QObject -{ - Q_OBJECT - -public: - explicit ApiController(QObject *parent = nullptr); - -public slots: - void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig); - -signals: - void errorOccurred(ErrorCode errorCode); - void configUpdated(const bool updateConfig, const QJsonObject &config, const int serverIndex); - -private: - struct ApiPayloadData - { - OpenVpnConfigurator::ConnectionData certRequest; - - QString wireGuardClientPrivKey; - QString wireGuardClientPubKey; - }; - - ApiPayloadData generateApiPayloadData(const QString &protocol); - QJsonObject fillApiPayload(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData); - void processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config); -}; - -#endif // APICONTROLLER_H diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp new file mode 100644 index 00000000..82232c99 --- /dev/null +++ b/client/core/controllers/coreController.cpp @@ -0,0 +1,345 @@ +#include "coreController.h" + +#include + +#if defined(Q_OS_ANDROID) + #include "core/installedAppsImageProvider.h" + #include "platforms/android/android_controller.h" +#endif + +#if defined(Q_OS_IOS) + #include "platforms/ios/ios_controller.h" + #include +#endif + +CoreController::CoreController(const QSharedPointer &vpnConnection, const std::shared_ptr &settings, + QQmlApplicationEngine *engine, QObject *parent) + : QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine) +{ + initModels(); + initControllers(); + initSignalHandlers(); + + initAndroidController(); + initAppleController(); + + initNotificationHandler(); + + auto locale = m_settings->getAppLanguage(); + m_translator.reset(new QTranslator()); + updateTranslator(locale); +} + +void CoreController::initModels() +{ + m_containersModel.reset(new ContainersModel(this)); + m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); + + m_defaultServerContainersModel.reset(new ContainersModel(this)); + m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get()); + + m_serversModel.reset(new ServersModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); + + m_languageModel.reset(new LanguageModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); + + m_sitesModel.reset(new SitesModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("SitesModel", m_sitesModel.get()); + + m_appSplitTunnelingModel.reset(new AppSplitTunnelingModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("AppSplitTunnelingModel", m_appSplitTunnelingModel.get()); + + m_protocolsModel.reset(new ProtocolsModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ProtocolsModel", m_protocolsModel.get()); + + m_openVpnConfigModel.reset(new OpenVpnConfigModel(this)); + m_engine->rootContext()->setContextProperty("OpenVpnConfigModel", m_openVpnConfigModel.get()); + + m_shadowSocksConfigModel.reset(new ShadowSocksConfigModel(this)); + m_engine->rootContext()->setContextProperty("ShadowSocksConfigModel", m_shadowSocksConfigModel.get()); + + m_cloakConfigModel.reset(new CloakConfigModel(this)); + m_engine->rootContext()->setContextProperty("CloakConfigModel", m_cloakConfigModel.get()); + + m_wireGuardConfigModel.reset(new WireGuardConfigModel(this)); + m_engine->rootContext()->setContextProperty("WireGuardConfigModel", m_wireGuardConfigModel.get()); + + m_awgConfigModel.reset(new AwgConfigModel(this)); + m_engine->rootContext()->setContextProperty("AwgConfigModel", m_awgConfigModel.get()); + + m_xrayConfigModel.reset(new XrayConfigModel(this)); + m_engine->rootContext()->setContextProperty("XrayConfigModel", m_xrayConfigModel.get()); + +#ifdef Q_OS_WINDOWS + m_ikev2ConfigModel.reset(new Ikev2ConfigModel(this)); + m_engine->rootContext()->setContextProperty("Ikev2ConfigModel", m_ikev2ConfigModel.get()); +#endif + + m_sftpConfigModel.reset(new SftpConfigModel(this)); + m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); + + m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this)); + m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get()); + + m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); + + m_apiServicesModel.reset(new ApiServicesModel(this)); + m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get()); + + m_apiCountryModel.reset(new ApiCountryModel(this)); + m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get()); + + m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this)); + m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get()); + + m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this)); + m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get()); +} + +void CoreController::initControllers() +{ + m_connectionController.reset( + new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings)); + m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); + + m_pageController.reset(new PageController(m_serversModel, m_settings)); + m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); + + m_focusController.reset(new FocusController(m_engine, this)); + m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get()); + + m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, m_settings)); + m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); + + connect(m_installController.get(), &InstallController::currentContainerUpdated, m_connectionController.get(), + &ConnectionController::onCurrentContainerUpdated); // TODO remove this + + m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); + m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); + + m_exportController.reset(new ExportController(m_serversModel, m_containersModel, m_clientManagementModel, m_settings)); + m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); + + m_settingsController.reset( + new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); + m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); + + m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); + m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get()); + + m_appSplitTunnelingController.reset(new AppSplitTunnelingController(m_settings, m_appSplitTunnelingModel)); + m_engine->rootContext()->setContextProperty("AppSplitTunnelingController", m_appSplitTunnelingController.get()); + + m_systemController.reset(new SystemController(m_settings)); + m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); + + m_apiSettingsController.reset( + new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings)); + m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); + + m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); + m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); +} + +void CoreController::initAndroidController() +{ +#ifdef Q_OS_ANDROID + if (!AndroidController::initLogging()) { + qFatal("Android logging initialization failed"); + } + AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); + connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); + + AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled()); + connect(m_settings.get(), &Settings::screenshotsEnabledChanged, AndroidController::instance(), &AndroidController::setScreenshotsEnabled); + + connect(m_settings.get(), &Settings::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer); + + connect(m_settings.get(), &Settings::settingsCleared, []() { AndroidController::instance()->resetLastServer(-1); }); + + connect(AndroidController::instance(), &AndroidController::initConnectionState, this, [this](Vpn::ConnectionState state) { + m_connectionController->onConnectionStateChanged(state); + if (m_vpnConnection) + m_vpnConnection->restoreConnection(); + }); + if (!AndroidController::instance()->initialize()) { + qFatal("Android controller initialization failed"); + } + + connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); + m_importController->extractConfigFromData(data); + data.clear(); + emit m_pageController->goToPageViewConfig(); + }); + + m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider); +#endif +} + +void CoreController::initAppleController() +{ +#ifdef Q_OS_IOS + IosController::Instance()->initialize(); + connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { + emit m_pageController->goToPageHome(); + m_importController->extractConfigFromData(data); + emit m_pageController->goToPageViewConfig(); + }); + + connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) { + emit m_pageController->goToPageHome(); + m_pageController->goToPageSettingsBackup(); + emit m_settingsController->importBackupFromOutside(filePath); + }); + + QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); }); + + connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); }); +#endif +} + +void CoreController::initSignalHandlers() +{ + initErrorMessagesHandler(); + + initApiCountryModelUpdateHandler(); + initContainerModelUpdateHandler(); + initAdminConfigRevokedHandler(); + initPassphraseRequestHandler(); + initTranslationsUpdatedHandler(); + initAutoConnectHandler(); + initAmneziaDnsToggledHandler(); + initPrepareConfigHandler(); +} + +void CoreController::initNotificationHandler() +{ +#ifndef Q_OS_ANDROID + m_notificationHandler.reset(NotificationHandler::create(nullptr)); + + connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(), + &NotificationHandler::setConnectionState); + + connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), &PageController::raiseMainWindow); + connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(), + static_cast(&ConnectionController::openConnection)); + connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(), + &ConnectionController::closeConnection); + connect(this, &CoreController::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated); +#endif +} + +void CoreController::updateTranslator(const QLocale &locale) +{ + if (!m_translator->isEmpty()) { + QCoreApplication::removeTranslator(m_translator.get()); + } + + QString strFileName = QString(":/translations/amneziavpn") + QLatin1String("_") + locale.name() + ".qm"; + if (m_translator->load(strFileName)) { + if (QCoreApplication::installTranslator(m_translator.get())) { + m_settings->setAppLanguage(locale); + } + } else { + m_settings->setAppLanguage(QLocale::English); + } + + m_engine->retranslate(); + + emit translationsUpdated(); +} + +void CoreController::initErrorMessagesHandler() +{ + connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](ErrorCode errorCode) { + emit m_pageController->showErrorMessage(errorCode); + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + }); + + connect(m_apiConfigsController.get(), &ApiConfigsController::errorOccurred, m_pageController.get(), + qOverload(&PageController::showErrorMessage)); +} + +void CoreController::setQmlRoot() +{ + m_systemController->setQmlRoot(m_engine->rootObjects().value(0)); +} + +void CoreController::initApiCountryModelUpdateHandler() +{ + // TODO + connect(m_serversModel.get(), &ServersModel::updateApiCountryModel, this, [this]() { + m_apiCountryModel->updateModel(m_serversModel->getProcessedServerData("apiAvailableCountries").toJsonArray(), + m_serversModel->getProcessedServerData("apiServerCountryCode").toString()); + }); + connect(m_serversModel.get(), &ServersModel::updateApiServicesModel, this, + [this]() { m_apiServicesModel->updateModel(m_serversModel->getProcessedServerData("apiConfig").toJsonObject()); }); +} + +void CoreController::initContainerModelUpdateHandler() +{ + connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); + connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), + &ContainersModel::updateModel); + m_serversModel->resetModel(); +} + +void CoreController::initAdminConfigRevokedHandler() +{ + connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(), + &ServersModel::clearCachedProfile); +} + +void CoreController::initPassphraseRequestHandler() +{ + connect(m_installController.get(), &InstallController::passphraseRequestStarted, m_pageController.get(), + &PageController::showPassphraseRequestDrawer); + connect(m_pageController.get(), &PageController::passphraseRequestDrawerClosed, m_installController.get(), + &InstallController::setEncryptedPassphrase); +} + +void CoreController::initTranslationsUpdatedHandler() +{ + connect(m_languageModel.get(), &LanguageModel::updateTranslations, this, &CoreController::updateTranslator); + connect(this, &CoreController::translationsUpdated, m_languageModel.get(), &LanguageModel::translationsUpdated); + connect(this, &CoreController::translationsUpdated, m_connectionController.get(), &ConnectionController::onTranslationsUpdated); +} + +void CoreController::initAutoConnectHandler() +{ + if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) { + QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); }); + } +} + +void CoreController::initAmneziaDnsToggledHandler() +{ + connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled, m_serversModel.get(), &ServersModel::toggleAmneziaDns); +} + +void CoreController::initPrepareConfigHandler() +{ + connect(m_connectionController.get(), &ConnectionController::prepareConfig, this, [this]() { + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing); + + if (!m_apiConfigsController->isConfigValid()) { + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + return; + } + + if (!m_installController->isConfigValid()) { + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + return; + } + + m_connectionController->openConnection(); + }); +} + +QSharedPointer CoreController::pageController() const +{ + return m_pageController; +} diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h new file mode 100644 index 00000000..700504af --- /dev/null +++ b/client/core/controllers/coreController.h @@ -0,0 +1,136 @@ +#ifndef CORECONTROLLER_H +#define CORECONTROLLER_H + +#include +#include +#include + +#include "ui/controllers/api/apiConfigsController.h" +#include "ui/controllers/api/apiSettingsController.h" +#include "ui/controllers/appSplitTunnelingController.h" +#include "ui/controllers/connectionController.h" +#include "ui/controllers/exportController.h" +#include "ui/controllers/focusController.h" +#include "ui/controllers/importController.h" +#include "ui/controllers/installController.h" +#include "ui/controllers/pageController.h" +#include "ui/controllers/settingsController.h" +#include "ui/controllers/sitesController.h" +#include "ui/controllers/systemController.h" + +#include "ui/models/containers_model.h" +#include "ui/models/languageModel.h" +#include "ui/models/protocols/cloakConfigModel.h" +#ifdef Q_OS_WINDOWS + #include "ui/models/protocols/ikev2ConfigModel.h" +#endif +#include "ui/models/api/apiAccountInfoModel.h" +#include "ui/models/api/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" +#include "ui/models/protocols/openvpnConfigModel.h" +#include "ui/models/protocols/shadowsocksConfigModel.h" +#include "ui/models/protocols/wireguardConfigModel.h" +#include "ui/models/protocols/xrayConfigModel.h" +#include "ui/models/protocols_model.h" +#include "ui/models/servers_model.h" +#include "ui/models/services/sftpConfigModel.h" +#include "ui/models/services/socks5ProxyConfigModel.h" +#include "ui/models/sites_model.h" + +#ifndef Q_OS_ANDROID + #include "ui/notificationhandler.h" +#endif + +class CoreController : public QObject +{ + Q_OBJECT + +public: + explicit CoreController(const QSharedPointer &vpnConnection, const std::shared_ptr &settings, + QQmlApplicationEngine *engine, QObject *parent = nullptr); + + QSharedPointer pageController() const; + void setQmlRoot(); + +signals: + void translationsUpdated(); + +private: + void initModels(); + void initControllers(); + void initAndroidController(); + void initAppleController(); + void initSignalHandlers(); + + void initNotificationHandler(); + + void updateTranslator(const QLocale &locale); + + void initErrorMessagesHandler(); + + void initApiCountryModelUpdateHandler(); + void initContainerModelUpdateHandler(); + void initAdminConfigRevokedHandler(); + void initPassphraseRequestHandler(); + void initTranslationsUpdatedHandler(); + void initAutoConnectHandler(); + void initAmneziaDnsToggledHandler(); + void initPrepareConfigHandler(); + + QQmlApplicationEngine *m_engine {}; // TODO use parent child system here? + std::shared_ptr m_settings; + QSharedPointer m_vpnConnection; + QSharedPointer m_translator; + +#ifndef Q_OS_ANDROID + QScopedPointer m_notificationHandler; +#endif + + QMetaObject::Connection m_reloadConfigErrorOccurredConnection; + + QScopedPointer m_connectionController; + QScopedPointer m_focusController; + QSharedPointer m_pageController; // TODO + QScopedPointer m_installController; + QScopedPointer m_importController; + QScopedPointer m_exportController; + QScopedPointer m_settingsController; + QScopedPointer m_sitesController; + QScopedPointer m_systemController; + QScopedPointer m_appSplitTunnelingController; + + QScopedPointer m_apiSettingsController; + QScopedPointer m_apiConfigsController; + + QSharedPointer m_containersModel; + QSharedPointer m_defaultServerContainersModel; + QSharedPointer m_serversModel; + QSharedPointer m_languageModel; + QSharedPointer m_protocolsModel; + QSharedPointer m_sitesModel; + QSharedPointer m_appSplitTunnelingModel; + QSharedPointer m_clientManagementModel; + + QSharedPointer m_apiServicesModel; + QSharedPointer m_apiCountryModel; + QSharedPointer m_apiAccountInfoModel; + QSharedPointer m_apiDevicesModel; + + QScopedPointer m_openVpnConfigModel; + QScopedPointer m_shadowSocksConfigModel; + QScopedPointer m_cloakConfigModel; + QScopedPointer m_xrayConfigModel; + QScopedPointer m_wireGuardConfigModel; + QScopedPointer m_awgConfigModel; +#ifdef Q_OS_WINDOWS + QScopedPointer m_ikev2ConfigModel; +#endif + QScopedPointer m_sftpConfigModel; + QScopedPointer m_socks5ConfigModel; +}; + +#endif // CORECONTROLLER_H diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp new file mode 100644 index 00000000..be42ad4d --- /dev/null +++ b/client/core/controllers/gatewayController.cpp @@ -0,0 +1,316 @@ +#include "gatewayController.h" + +#include +#include + +#include +#include +#include +#include + +#include "QBlockCipher.h" +#include "QRsa.h" + +#include "amnezia_application.h" +#include "core/api/apiUtils.h" +#include "utilities.h" + +namespace +{ + namespace configKey + { + constexpr char aesKey[] = "aes_key"; + constexpr char aesIv[] = "aes_iv"; + constexpr char aesSalt[] = "aes_salt"; + + constexpr char apiPayload[] = "api_payload"; + constexpr char keyPayload[] = "key_payload"; + } + + 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) + : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment), m_requestTimeoutMsecs(requestTimeoutMsecs) +{ +} + +ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody) +{ +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + request.setUrl(QString(endpoint).arg(m_gatewayEndpoint)); + + QNetworkReply *reply; + reply = amnApp->networkManager()->get(request); + + QEventLoop wait; + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + + QList sslErrors; + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + responseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) { + auto requestFunction = [&request, &responseBody](const QString &url) { + request.setUrl(url); + return amnApp->networkManager()->get(request); + }; + + auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply, + const QList &nestedSslErrors) { + responseBody = nestedReply->readAll(); + if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) { + sslErrors = nestedSslErrors; + reply = nestedReply; + return true; + } + return false; + }; + + bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); + } + + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); + reply->deleteLater(); + + return errorCode; +} + +ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) +{ +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + request.setUrl(endpoint.arg(m_gatewayEndpoint)); + + QSimpleCrypto::QBlockCipher blockCipher; + QByteArray key = blockCipher.generatePrivateSalt(32); + QByteArray iv = blockCipher.generatePrivateSalt(32); + QByteArray salt = blockCipher.generatePrivateSalt(8); + + QJsonObject keyPayload; + keyPayload[configKey::aesKey] = QString(key.toBase64()); + keyPayload[configKey::aesIv] = QString(iv.toBase64()); + keyPayload[configKey::aesSalt] = QString(salt.toBase64()); + + QByteArray encryptedKeyPayload; + QByteArray encryptedApiPayload; + try { + QSimpleCrypto::QRsa rsa; + + EVP_PKEY *publicKey = nullptr; + try { + QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + QSimpleCrypto::QRsa rsa; + publicKey = rsa.getPublicKeyFromByteArray(rsaKey); + } catch (...) { + Utils::logException(); + qCritical() << "error loading public key from environment variables"; + return ErrorCode::ApiMissingAgwPublicKey; + } + + encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); + EVP_PKEY_free(publicKey); + + encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); + } catch (...) { // todo change error handling in QSimpleCrypto? + Utils::logException(); + qCritical() << "error when encrypting the request body"; + return ErrorCode::ApiConfigDecryptionError; + } + + QJsonObject requestBody; + requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); + requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); + + QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); + + QEventLoop wait; + connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + + QList sslErrors; + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + QByteArray encryptedResponseBody = reply->readAll(); + + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { + auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { + request.setUrl(url); + return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); + }; + + auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, + this](QNetworkReply *nestedReply, const QList &nestedSslErrors) { + encryptedResponseBody = nestedReply->readAll(); + reply = nestedReply; + if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { + sslErrors = nestedSslErrors; + return false; + } + return true; + }; + + bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); + } + + auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); + reply->deleteLater(); + if (errorCode) { + return errorCode; + } + + try { + responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); + return ErrorCode::NoError; + } catch (...) { // todo change error handling in QSimpleCrypto? + Utils::logException(); + qCritical() << "error when decrypting the request body"; + return ErrorCode::ApiConfigDecryptionError; + } +} + +QStringList GatewayController::getProxyUrls() +{ + QNetworkRequest request; + request.setTransferTimeout(m_requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QEventLoop wait; + QList sslErrors; + QNetworkReply *reply; + + QStringList proxyStorageUrls; + if (m_isDevEnvironment) { + proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", "); + } else { + proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", "); + } + + QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; + + for (const auto &proxyStorageUrl : proxyStorageUrls) { + request.setUrl(proxyStorageUrl); + reply = amnApp->networkManager()->get(request); + + connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + if (reply->error() == QNetworkReply::NetworkError::NoError) { + auto encryptedResponseBody = reply->readAll(); + reply->deleteLater(); + + EVP_PKEY *privateKey = nullptr; + QByteArray responseBody; + try { + if (!m_isDevEnvironment) { + QCryptographicHash hash(QCryptographicHash::Sha512); + hash.addData(key); + QByteArray hashResult = hash.result().toHex(); + + QByteArray key = QByteArray::fromHex(hashResult.left(64)); + QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); + + QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); + + QSimpleCrypto::QBlockCipher blockCipher; + responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); + } else { + responseBody = encryptedResponseBody; + } + } catch (...) { + Utils::logException(); + qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; + continue; + } + + auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); + + QStringList endpoints; + for (const auto &endpoint : endpointsArray) { + endpoints.push_back(endpoint.toString()); + } + return endpoints; + } else { + reply->deleteLater(); + } + } + 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"; + return true; + } else if (responseBody.contains("html")) { + qDebug() << "The response contains an html tag"; + return true; + } else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) { + if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) + || responseBody.contains(errorResponsePattern3)) { + return false; + } else { + return true; + } + } else if (reply->error() != QNetworkReply::NetworkError::NoError) { + return true; + } else if (checkEncryption) { + try { + QSimpleCrypto::QBlockCipher blockCipher; + static_cast(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt)); + } catch (...) { + qDebug() << "Failed to decrypt the data"; + return true; + } + } + return false; +} + +void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply, + std::function requestFunction, + std::function &sslErrors)> replyProcessingFunction) +{ + QStringList proxyUrls = getProxyUrls(); + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); + + QEventLoop wait; + QList sslErrors; + QByteArray responseBody; + + for (const QString &proxyUrl : proxyUrls) { + qDebug() << "Go to the next endpoint"; + reply->deleteLater(); // delete the previous reply + reply = requestFunction(endpoint.arg(proxyUrl)); + + QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + if (replyProcessingFunction(reply, sslErrors)) { + break; + } + } +} diff --git a/client/core/controllers/gatewayController.h b/client/core/controllers/gatewayController.h new file mode 100644 index 00000000..45d989f0 --- /dev/null +++ b/client/core/controllers/gatewayController.h @@ -0,0 +1,35 @@ +#ifndef GATEWAYCONTROLLER_H +#define GATEWAYCONTROLLER_H + +#include +#include + +#include "core/defs.h" + +#ifdef Q_OS_IOS + #include "platforms/ios/ios_controller.h" +#endif + +class GatewayController : public QObject +{ + Q_OBJECT + +public: + explicit GatewayController(const QString &gatewayEndpoint, bool isDevEnvironment, int requestTimeoutMsecs, QObject *parent = nullptr); + + amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody); + amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); + +private: + QStringList getProxyUrls(); + bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", + const QByteArray &iv = "", const QByteArray &salt = ""); + void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function requestFunction, + std::function &sslErrors)> replyProcessingFunction); + + int m_requestTimeoutMsecs; + QString m_gatewayEndpoint; + bool m_isDevEnvironment = false; +}; + +#endif // GATEWAYCONTROLLER_H diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index a2a6b61b..5ce62863 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -83,7 +83,6 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr } qDebug().noquote() << lineToExec; - Logger::appendSshLog("Run command:" + lineToExec); error = m_sshClient.executeCommand(lineToExec, cbReadStdOut, cbReadStdErr); if (error != ErrorCode::NoError) { @@ -100,13 +99,13 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti const std::function &cbReadStdErr) { QString fileName = "/opt/amnezia/" + Utils::getRandomString(16) + ".sh"; - Logger::appendSshLog("Run container script for " + ContainerProps::containerToString(container) + ":\n" + script); ErrorCode e = uploadTextFileToContainer(container, credentials, script, fileName); if (e) return e; - QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash")); + QString runner = + QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash")); e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName); @@ -347,7 +346,9 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c } if (container == DockerContainer::Awg) { - if ((oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort) + if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) + != newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)) + || (oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort) != newProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort)) || (oldProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount) != newProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount)) @@ -371,8 +372,10 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c } if (container == DockerContainer::WireGuard) { - if (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort) - != newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort)) + if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) + != newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)) + || (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort) + != newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort))) return true; } @@ -426,7 +429,7 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden if (errorCode) return errorCode; - errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),dockerFilePath); + errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(), dockerFilePath); if (errorCode) return errorCode; @@ -437,9 +440,10 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden return ErrorCode::NoError; }; - errorCode = runScript(credentials, - replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)), - cbReadStdOut); + errorCode = + runScript(credentials, + replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)), + cbReadStdOut); if (errorCode) return errorCode; @@ -569,6 +573,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential // Xray vars vars.append({ { "$XRAY_SITE_NAME", xrayConfig.value(config_key::site).toString(protocols::xray::defaultSite) } }); + vars.append({ { "$XRAY_SERVER_PORT", xrayConfig.value(config_key::port).toString(protocols::xray::defaultPort) } }); // Wireguard vars vars.append({ { "$WIREGUARD_SUBNET_IP", @@ -606,6 +611,8 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential vars.append({ { "$SFTP_PASSWORD", sftpConfig.value(config_key::password).toString() } }); // Amnezia wireguard vars + vars.append({ { "$AWG_SUBNET_IP", + amneziaWireguarConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) } }); vars.append({ { "$AWG_SERVER_PORT", amneziaWireguarConfig.value(config_key::port).toString(protocols::awg::defaultPort) } }); vars.append({ { "$JUNK_PACKET_COUNT", amneziaWireguarConfig.value(config_key::junkPacketCount).toString() } }); @@ -620,13 +627,15 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential // Socks5 proxy vars vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } }); - auto username = socks5ProxyConfig.value(config_key:: userName).toString(); + auto username = socks5ProxyConfig.value(config_key::userName).toString(); auto password = socks5ProxyConfig.value(config_key::password).toString(); QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : ""; - vars.append({ { "$SOCKS5_USER", socks5user } }); - vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } }); + vars.append({ { "$SOCKS5_USER", socks5user } }); + vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } }); - QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); + QString serverIp = (container != DockerContainer::Awg && container != DockerContainer::WireGuard && container != DockerContainer::Xray) + ? NetworkUtilities::getIPAddress(credentials.hostName) + : credentials.hostName; if (!serverIp.isEmpty()) { vars.append({ { "$SERVER_IP_ADDRESS", serverIp } }); } else { @@ -700,7 +709,7 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential QString transportProto = containerConfig.value(config_key::transport_proto).toString(defaultTransportProto); // TODO reimplement with netstat - QString script = QString("which lsof &>/dev/null || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port); + QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port); for (auto &port : fixedPorts) { script = script.append("|:%1").arg(port); } @@ -712,7 +721,8 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential udpProtoScript.append("' | grep -i udp"); tcpProtoScript.append(" | grep LISTEN"); - ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); + ErrorCode errorCode = + runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); if (errorCode != ErrorCode::NoError) { return errorCode; } diff --git a/client/core/controllers/vpnConfigurationController.cpp b/client/core/controllers/vpnConfigurationController.cpp index 818cf57e..61287972 100644 --- a/client/core/controllers/vpnConfigurationController.cpp +++ b/client/core/controllers/vpnConfigurationController.cpp @@ -77,8 +77,7 @@ ErrorCode VpnConfigurationsController::createProtocolConfigString(const bool isA } QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair &dns, const QJsonObject &serverConfig, - const QJsonObject &containerConfig, const DockerContainer container, - ErrorCode &errorCode) + const QJsonObject &containerConfig, const DockerContainer container) { QJsonObject vpnConfiguration {}; @@ -100,7 +99,14 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPairprocessConfigWithLocalSettings(dns, isApiConfig, protocolConfigString); QJsonObject vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); + if (container == DockerContainer::Awg || container == DockerContainer::WireGuard) { + // add mtu for old configs + if (vpnConfigData[config_key::mtu].toString().isEmpty()) { + vpnConfigData[config_key::mtu] = + container == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; + } + } + vpnConfiguration.insert(ProtocolProps::key_proto_config_data(proto), vpnConfigData); } diff --git a/client/core/controllers/vpnConfigurationController.h b/client/core/controllers/vpnConfigurationController.h index 1b10d448..6d0d43b0 100644 --- a/client/core/controllers/vpnConfigurationController.h +++ b/client/core/controllers/vpnConfigurationController.h @@ -12,7 +12,8 @@ class VpnConfigurationsController : public QObject { Q_OBJECT public: - explicit VpnConfigurationsController(const std::shared_ptr &settings, QSharedPointer serverController, QObject *parent = nullptr); + explicit VpnConfigurationsController(const std::shared_ptr &settings, QSharedPointer serverController, + QObject *parent = nullptr); public slots: ErrorCode createProtocolConfigForContainer(const ServerCredentials &credentials, const DockerContainer container, @@ -21,7 +22,7 @@ public slots: const DockerContainer container, const QJsonObject &containerConfig, const Proto protocol, QString &protocolConfigString); QJsonObject createVpnConfiguration(const QPair &dns, const QJsonObject &serverConfig, - const QJsonObject &containerConfig, const DockerContainer container, ErrorCode &errorCode); + const QJsonObject &containerConfig, const DockerContainer container); static void updateContainerConfigAfterInstallation(const DockerContainer container, QJsonObject &containerConfig, const QString &stdOut); signals: diff --git a/client/core/defs.h b/client/core/defs.h index a441ee1c..6c85c65d 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -6,9 +6,6 @@ namespace amnezia { - - constexpr const qint16 qrMagicCode = 1984; - struct ServerCredentials { QString hostName; @@ -47,6 +44,7 @@ namespace amnezia InternalError = 101, NotImplementedError = 102, AmneziaServiceNotRunning = 103, + NotSupportedOnThisPlatform = 104, // Server errors ServerCheckFailed = 200, @@ -96,6 +94,8 @@ namespace amnezia // import and install errors ImportInvalidConfigError = 900, + ImportOpenConfigError = 901, + NoInstalledContainersError = 902, // Android errors AndroidError = 1000, @@ -106,6 +106,11 @@ namespace amnezia ApiConfigEmptyError = 1102, ApiConfigTimeoutError = 1103, ApiConfigSslError = 1104, + ApiMissingAgwPublicKey = 1105, + ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, + ApiConfigLimitError = 1108, + ApiNotFoundError = 1109, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 645ec6c5..2b9182cf 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -12,6 +12,7 @@ QString errorString(ErrorCode code) { case(ErrorCode::UnknownError): errorMessage = QObject::tr("Unknown error"); break; case(ErrorCode::NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break; case(ErrorCode::AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break; + case(ErrorCode::NotSupportedOnThisPlatform): errorMessage = QObject::tr("The selected protocol is not supported on the current platform"); break; // Server errors case(ErrorCode::ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break; @@ -50,6 +51,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break; case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break; + case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break; + case(ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break; // Android errors case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break; @@ -60,7 +63,12 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break; case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break; case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; - + case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; + case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; + case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + case (ErrorCode::ApiConfigLimitError): errorMessage = QObject::tr("The limit of allowed configurations per subscription has been exceeded"); break; + case (ErrorCode::ApiNotFoundError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/core/ipcclient.cpp b/client/core/ipcclient.cpp index 3e364452..69edcd15 100644 --- a/client/core/ipcclient.cpp +++ b/client/core/ipcclient.cpp @@ -5,12 +5,12 @@ IpcClient *IpcClient::m_instance = nullptr; IpcClient::IpcClient(QObject *parent) : QObject(parent) { - } IpcClient::~IpcClient() { - if (m_localSocket) m_localSocket->close(); + if (m_localSocket) + m_localSocket->close(); } bool IpcClient::isSocketConnected() const @@ -25,10 +25,18 @@ IpcClient *IpcClient::Instance() QSharedPointer IpcClient::Interface() { - if (!Instance()) return nullptr; + if (!Instance()) + return nullptr; return Instance()->m_ipcClient; } +QSharedPointer IpcClient::InterfaceTun2Socks() +{ + if (!Instance()) + return nullptr; + return Instance()->m_Tun2SocksClient; +} + bool IpcClient::init(IpcClient *instance) { m_instance = instance; @@ -36,36 +44,54 @@ bool IpcClient::init(IpcClient *instance) Instance()->m_localSocket = new QLocalSocket(Instance()); connect(Instance()->m_localSocket.data(), &QLocalSocket::connected, &Instance()->m_ClientNode, []() { Instance()->m_ClientNode.addClientSideConnection(Instance()->m_localSocket.data()); + auto cliNode = Instance()->m_ClientNode.acquire(); + cliNode->waitForSource(5000); + Instance()->m_ipcClient.reset(cliNode); + + if (!Instance()->m_ipcClient) { + qWarning() << "IpcClient is not ready!"; + } - Instance()->m_ipcClient.reset(Instance()->m_ClientNode.acquire()); Instance()->m_ipcClient->waitForSource(1000); if (!Instance()->m_ipcClient->isReplicaValid()) { qWarning() << "IpcClient replica is not connected!"; } + auto t2sNode = Instance()->m_ClientNode.acquire(); + t2sNode->waitForSource(5000); + Instance()->m_Tun2SocksClient.reset(t2sNode); + + if (!Instance()->m_Tun2SocksClient) { + qWarning() << "IpcClient::m_Tun2SocksClient is not ready!"; + } + + Instance()->m_Tun2SocksClient->waitForSource(1000); + + if (!Instance()->m_Tun2SocksClient->isReplicaValid()) { + qWarning() << "IpcClient::m_Tun2SocksClient replica is not connected!"; + } }); - connect(Instance()->m_localSocket, &QLocalSocket::disconnected, [instance](){ - instance->m_isSocketConnected = false; - }); + connect(Instance()->m_localSocket, &QLocalSocket::disconnected, + [instance]() { instance->m_isSocketConnected = false; }); Instance()->m_localSocket->connectToServer(amnezia::getIpcServiceUrl()); - Instance()->m_localSocket->waitForConnected(); if (!Instance()->m_ipcClient) { qDebug() << "IpcClient::init failed"; return false; } + qDebug() << "IpcClient::init succeed"; - return Instance()->m_ipcClient->isReplicaValid(); + return (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid()); } QSharedPointer IpcClient::CreatePrivilegedProcess() { - if (! Instance()->m_ipcClient || ! Instance()->m_ipcClient->isReplicaValid()) { + if (!Instance()->m_ipcClient || !Instance()->m_ipcClient->isReplicaValid()) { qWarning() << "IpcClient::createPrivilegedProcess : IpcClient IpcClient replica is not valid"; return nullptr; } @@ -88,18 +114,15 @@ QSharedPointer IpcClient::CreatePrivilegedProcess() pd->ipcProcess.reset(priv); if (!pd->ipcProcess) { qWarning() << "Acquire PrivilegedProcess failed"; - } - else { + } else { pd->ipcProcess->waitForSource(1000); if (!pd->ipcProcess->isReplicaValid()) { qWarning() << "PrivilegedProcess replica is not connected!"; } - QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(), [pd](){ - pd->replicaNode->deleteLater(); - }); + QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(), + [pd]() { pd->replicaNode->deleteLater(); }); } - }); pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid)); pd->localSocket->waitForConnected(); @@ -107,5 +130,3 @@ QSharedPointer IpcClient::CreatePrivilegedProcess() auto processReplica = QSharedPointer(pd->ipcProcess); return processReplica; } - - diff --git a/client/core/ipcclient.h b/client/core/ipcclient.h index ab5d750a..ad2e6b6e 100644 --- a/client/core/ipcclient.h +++ b/client/core/ipcclient.h @@ -6,6 +6,7 @@ #include "ipc.h" #include "rep_ipc_interface_replica.h" +#include "rep_ipc_process_tun2socks_replica.h" #include "privileged_process.h" @@ -18,6 +19,7 @@ public: static IpcClient *Instance(); static bool init(IpcClient *instance); static QSharedPointer Interface(); + static QSharedPointer InterfaceTun2Socks(); static QSharedPointer CreatePrivilegedProcess(); bool isSocketConnected() const; @@ -28,8 +30,11 @@ private: ~IpcClient() override; QRemoteObjectNode m_ClientNode; + QRemoteObjectNode m_Tun2SocksNode; QSharedPointer m_ipcClient; QPointer m_localSocket; + QPointer m_tun2socksSocket; + QSharedPointer m_Tun2SocksClient; struct ProcessDescriptor { ProcessDescriptor () { diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index 7ffd4c41..a5825f0d 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -109,7 +109,10 @@ QStringList NetworkUtilities::summarizeRoutes(const QStringList &ips, const QStr QString NetworkUtilities::getIPAddress(const QString &host) { - if (ipAddressRegExp().match(host).hasMatch()) { + QHostAddress address(host); + if (QAbstractSocket::IPv4Protocol == address.protocol()) { + return host; + } else if (QAbstractSocket::IPv6Protocol == address.protocol()) { return host; } diff --git a/client/core/networkUtilities.h b/client/core/networkUtilities.h index 3057b852..3b64b547 100644 --- a/client/core/networkUtilities.h +++ b/client/core/networkUtilities.h @@ -5,6 +5,7 @@ #include #include #include +#include class NetworkUtilities : public QObject @@ -30,7 +31,6 @@ public: static QString ipAddressFromIpWithSubnet(const QString ip); static QStringList summarizeRoutes(const QStringList &ips, const QString cidr); - }; #endif // NETWORKUTILITIES_H diff --git a/client/core/qrCodeUtils.cpp b/client/core/qrCodeUtils.cpp new file mode 100644 index 00000000..a18af172 --- /dev/null +++ b/client/core/qrCodeUtils.cpp @@ -0,0 +1,35 @@ +#include "qrCodeUtils.h" + +#include +#include + +QList qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data) +{ + double k = 850; + + quint8 chunksCount = std::ceil(data.size() / k); + QList chunks; + for (int i = 0; i < data.size(); i = i + k) { + QByteArray chunk; + QDataStream s(&chunk, QIODevice::WriteOnly); + s << qrCodeUtils::qrMagicCode << chunksCount << (quint8)std::round(i / k) << data.mid(i, k); + + QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(ba, qrcodegen::QrCode::Ecc::LOW); + QString svg = QString::fromStdString(toSvgString(qr, 1)); + chunks.append(svgToBase64(svg)); + } + + return chunks; +} + +QString qrCodeUtils::svgToBase64(const QString &image) +{ + return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); +} + +qrcodegen::QrCode qrCodeUtils::generateQrCode(const QByteArray &data) +{ + return qrcodegen::QrCode::encodeText(data, qrcodegen::QrCode::Ecc::LOW); +} diff --git a/client/core/qrCodeUtils.h b/client/core/qrCodeUtils.h new file mode 100644 index 00000000..cda0723b --- /dev/null +++ b/client/core/qrCodeUtils.h @@ -0,0 +1,17 @@ +#ifndef QRCODEUTILS_H +#define QRCODEUTILS_H + +#include + +#include "qrcodegen.hpp" + +namespace qrCodeUtils +{ + constexpr const qint16 qrMagicCode = 1984; + + QList generateQrCodeImageSeries(const QByteArray &data); + qrcodegen::QrCode generateQrCode(const QByteArray &data); + QString svgToBase64(const QString &image); +}; + +#endif // QRCODEUTILS_H diff --git a/client/core/serialization/vmess_new.cpp b/client/core/serialization/vmess_new.cpp index 6f3ec3e1..68d32203 100644 --- a/client/core/serialization/vmess_new.cpp +++ b/client/core/serialization/vmess_new.cpp @@ -104,7 +104,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes server.users.first().security = "auto"; } - const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { + const auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { if (query.hasQueryItem(key)) return query.queryItemValue(key, QUrl::FullyDecoded); else diff --git a/client/daemon/daemon.cpp b/client/daemon/daemon.cpp index 3e237e9c..081a7a90 100644 --- a/client/daemon/daemon.cpp +++ b/client/daemon/daemon.cpp @@ -78,7 +78,7 @@ bool Daemon::activate(const InterfaceConfig& config) { return false; } - if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { + if (!dnsutils()->restoreResolvers()) { return false; } @@ -114,12 +114,23 @@ bool Daemon::activate(const InterfaceConfig& config) { // Bring up the wireguard interface if not already done. if (!wgutils()->interfaceExists()) { + // Create the interface. if (!wgutils()->addInterface(config)) { logger.error() << "Interface creation failed."; return false; } } + // Bring the interface up. + if (supportIPUtils()) { + if (!iputils()->addInterfaceIPs(config)) { + return false; + } + if (!iputils()->setMTUAndUp(config)) { + return false; + } + } + // Configure routing for excluded addresses. for (const QString& i : config.m_excludedAddresses) { addExclusionRoute(IPAddress(i)); @@ -135,15 +146,6 @@ bool Daemon::activate(const InterfaceConfig& config) { return false; } - if (supportIPUtils()) { - if (!iputils()->addInterfaceIPs(config)) { - return false; - } - if (!iputils()->setMTUAndUp(config)) { - return false; - } - } - // set routing for (const IPAddress& ip : config.m_allowedIPAddressRanges) { if (!wgutils()->updateRoutePrefix(ip)) { @@ -165,10 +167,6 @@ bool Daemon::activate(const InterfaceConfig& config) { } bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) { - if (!supportDnsUtils()) { - return true; - } - if ((config.m_hopType == InterfaceConfig::MultiHopExit) || (config.m_hopType == InterfaceConfig::SingleHop)) { QList resolvers; @@ -423,13 +421,8 @@ bool Daemon::deactivate(bool emitSignals) { } // Cleanup DNS - if (supportDnsUtils() && !dnsutils()->restoreResolvers()) { - return false; - } - - if (!wgutils()->interfaceExists()) { - logger.warning() << "Wireguard interface does not exist."; - return false; + if (!dnsutils()->restoreResolvers()) { + logger.warning() << "Failed to restore DNS resolvers."; } // Cleanup peers and routing @@ -449,13 +442,9 @@ bool Daemon::deactivate(bool emitSignals) { } m_excludedAddrSet.clear(); - // Delete the interface - if (!wgutils()->deleteInterface()) { - return false; - } - m_connections.clear(); - return true; + // Delete the interface + return wgutils()->deleteInterface(); } QString Daemon::logs() { diff --git a/client/daemon/daemon.h b/client/daemon/daemon.h index d3d8c34d..757c9ff0 100644 --- a/client/daemon/daemon.h +++ b/client/daemon/daemon.h @@ -8,6 +8,8 @@ #include #include +#include "daemon/daemonerrors.h" +#include "daemonerrors.h" #include "dnsutils.h" #include "interfaceconfig.h" #include "iputils.h" @@ -51,7 +53,7 @@ class Daemon : public QObject { */ void activationFailure(); void disconnected(); - void backendFailure(); + void backendFailure(DaemonError reason = DaemonError::ERROR_FATAL); private: bool maybeUpdateResolvers(const InterfaceConfig& config); @@ -69,7 +71,6 @@ class Daemon : public QObject { virtual WireguardUtils* wgutils() const = 0; virtual bool supportIPUtils() const { return false; } virtual IPUtils* iputils() { return nullptr; } - virtual bool supportDnsUtils() const { return false; } virtual DnsUtils* dnsutils() { return nullptr; } static bool parseStringList(const QJsonObject& obj, const QString& name, diff --git a/client/daemon/daemonerrors.h b/client/daemon/daemonerrors.h new file mode 100644 index 00000000..3d271244 --- /dev/null +++ b/client/daemon/daemonerrors.h @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#pragma once + +#include + +enum class DaemonError : uint8_t { + ERROR_NONE = 0u, + ERROR_FATAL = 1u, + ERROR_SPLIT_TUNNEL_INIT_FAILURE = 2u, + ERROR_SPLIT_TUNNEL_START_FAILURE = 3u, + ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE = 4u, + + DAEMON_ERROR_MAX = 5u, +}; diff --git a/client/daemon/daemonlocalserverconnection.cpp b/client/daemon/daemonlocalserverconnection.cpp index 1a49b7e5..bc57c71f 100644 --- a/client/daemon/daemonlocalserverconnection.cpp +++ b/client/daemon/daemonlocalserverconnection.cpp @@ -92,6 +92,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { logger.debug() << "Command received:" << type; + // It is expected that sometimes the client will request backend logs + // before the first authentication. In these cases we just return empty + // logs. + if (type == "logs") { + QJsonObject obj; + obj.insert("type", "logs"); + obj.insert("logs", ""); + write(obj); + return; + } + if (type == "activate") { InterfaceConfig config; if (!Daemon::parseConfig(obj, config)) { @@ -115,8 +126,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { if (type == "status") { QJsonObject obj = Daemon::instance()->getStatus(); obj.insert("type", "status"); - m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - m_socket->write("\n"); + write(obj); return; } @@ -124,8 +134,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { QJsonObject obj; obj.insert("type", "logs"); obj.insert("logs", Daemon::instance()->logs().replace("\n", "|")); - m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - m_socket->write("\n"); + write(obj); return; } @@ -150,9 +159,10 @@ void DaemonLocalServerConnection::disconnected() { write(obj); } -void DaemonLocalServerConnection::backendFailure() { +void DaemonLocalServerConnection::backendFailure(DaemonError err) { QJsonObject obj; obj.insert("type", "backendFailure"); + obj.insert("errorCode", static_cast(err)); write(obj); } diff --git a/client/daemon/daemonlocalserverconnection.h b/client/daemon/daemonlocalserverconnection.h index ec32df75..34170cb3 100644 --- a/client/daemon/daemonlocalserverconnection.h +++ b/client/daemon/daemonlocalserverconnection.h @@ -7,6 +7,8 @@ #include +#include "daemonerrors.h" + class QLocalSocket; class DaemonLocalServerConnection final : public QObject { @@ -23,7 +25,7 @@ class DaemonLocalServerConnection final : public QObject { void connected(const QString& pubkey); void disconnected(); - void backendFailure(); + void backendFailure(DaemonError err); void write(const QJsonObject& obj); diff --git a/client/daemon/wireguardutils.h b/client/daemon/wireguardutils.h index cdee40ef..b600a923 100644 --- a/client/daemon/wireguardutils.h +++ b/client/daemon/wireguardutils.h @@ -45,9 +45,11 @@ class WireguardUtils : public QObject { virtual bool updateRoutePrefix(const IPAddress& prefix) = 0; virtual bool deleteRoutePrefix(const IPAddress& prefix) = 0; - + virtual bool addExclusionRoute(const IPAddress& prefix) = 0; virtual bool deleteExclusionRoute(const IPAddress& prefix) = 0; + + virtual bool excludeLocalNetworks(const QList& addresses) = 0; }; #endif // WIREGUARDUTILS_H diff --git a/client/fonts/Lato-Black.ttf b/client/fonts/Lato-Black.ttf deleted file mode 100644 index a87109f5..00000000 Binary files a/client/fonts/Lato-Black.ttf and /dev/null differ diff --git a/client/fonts/Lato-BlackItalic.ttf b/client/fonts/Lato-BlackItalic.ttf deleted file mode 100644 index 505e23fa..00000000 Binary files a/client/fonts/Lato-BlackItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Bold.ttf b/client/fonts/Lato-Bold.ttf deleted file mode 100644 index 59c48434..00000000 Binary files a/client/fonts/Lato-Bold.ttf and /dev/null differ diff --git a/client/fonts/Lato-BoldItalic.ttf b/client/fonts/Lato-BoldItalic.ttf deleted file mode 100644 index 3371a4bf..00000000 Binary files a/client/fonts/Lato-BoldItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Hairline.ttf b/client/fonts/Lato-Hairline.ttf deleted file mode 100644 index bc06473a..00000000 Binary files a/client/fonts/Lato-Hairline.ttf and /dev/null differ diff --git a/client/fonts/Lato-HairlineItalic.ttf b/client/fonts/Lato-HairlineItalic.ttf deleted file mode 100644 index d680800a..00000000 Binary files a/client/fonts/Lato-HairlineItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Heavy.ttf b/client/fonts/Lato-Heavy.ttf deleted file mode 100644 index 4a56a97c..00000000 Binary files a/client/fonts/Lato-Heavy.ttf and /dev/null differ diff --git a/client/fonts/Lato-HeavyItalic.ttf b/client/fonts/Lato-HeavyItalic.ttf deleted file mode 100644 index b25f78d1..00000000 Binary files a/client/fonts/Lato-HeavyItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Italic.ttf b/client/fonts/Lato-Italic.ttf deleted file mode 100644 index 9babe6a8..00000000 Binary files a/client/fonts/Lato-Italic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Light.ttf b/client/fonts/Lato-Light.ttf deleted file mode 100644 index ccba01a9..00000000 Binary files a/client/fonts/Lato-Light.ttf and /dev/null differ diff --git a/client/fonts/Lato-LightItalic.ttf b/client/fonts/Lato-LightItalic.ttf deleted file mode 100644 index fd58065e..00000000 Binary files a/client/fonts/Lato-LightItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Medium.ttf b/client/fonts/Lato-Medium.ttf deleted file mode 100644 index a208b991..00000000 Binary files a/client/fonts/Lato-Medium.ttf and /dev/null differ diff --git a/client/fonts/Lato-MediumItalic.ttf b/client/fonts/Lato-MediumItalic.ttf deleted file mode 100644 index ac0ee4c9..00000000 Binary files a/client/fonts/Lato-MediumItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Regular.ttf b/client/fonts/Lato-Regular.ttf deleted file mode 100644 index f01f5589..00000000 Binary files a/client/fonts/Lato-Regular.ttf and /dev/null differ diff --git a/client/fonts/Lato-Semibold.ttf b/client/fonts/Lato-Semibold.ttf deleted file mode 100644 index 0b95c643..00000000 Binary files a/client/fonts/Lato-Semibold.ttf and /dev/null differ diff --git a/client/fonts/Lato-SemiboldItalic.ttf b/client/fonts/Lato-SemiboldItalic.ttf deleted file mode 100644 index ed70b675..00000000 Binary files a/client/fonts/Lato-SemiboldItalic.ttf and /dev/null differ diff --git a/client/fonts/Lato-Thin.ttf b/client/fonts/Lato-Thin.ttf deleted file mode 100644 index ca2cc0de..00000000 Binary files a/client/fonts/Lato-Thin.ttf and /dev/null differ diff --git a/client/fonts/Lato-ThinItalic.ttf b/client/fonts/Lato-ThinItalic.ttf deleted file mode 100644 index 1e319da5..00000000 Binary files a/client/fonts/Lato-ThinItalic.ttf and /dev/null differ diff --git a/client/images/amneziaBigLogo.svg b/client/images/amneziaBigLogo.svg deleted file mode 100644 index c50c7743..00000000 --- a/client/images/amneziaBigLogo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/animation.gif b/client/images/animation.gif deleted file mode 100644 index 6f7f38e8..00000000 Binary files a/client/images/animation.gif and /dev/null differ diff --git a/client/images/arrow_left.png b/client/images/arrow_left.png deleted file mode 100644 index 3a4d149d..00000000 Binary files a/client/images/arrow_left.png and /dev/null differ diff --git a/client/images/background_connected.png b/client/images/background_connected.png deleted file mode 100644 index 62480d5f..00000000 Binary files a/client/images/background_connected.png and /dev/null differ diff --git a/client/images/background_connected@2x.png b/client/images/background_connected@2x.png deleted file mode 100644 index 4f76b956..00000000 Binary files a/client/images/background_connected@2x.png and /dev/null differ diff --git a/client/images/check.png b/client/images/check.png deleted file mode 100644 index 43039eb1..00000000 Binary files a/client/images/check.png and /dev/null differ diff --git a/client/images/close.png b/client/images/close.png deleted file mode 100644 index 072232c1..00000000 Binary files a/client/images/close.png and /dev/null differ diff --git a/client/images/connected.png b/client/images/connected.png deleted file mode 100644 index b3c907c8..00000000 Binary files a/client/images/connected.png and /dev/null differ diff --git a/client/images/connectionOff.svg b/client/images/connectionOff.svg deleted file mode 100644 index 27905ff9..00000000 --- a/client/images/connectionOff.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/client/images/connectionOn.svg b/client/images/connectionOn.svg deleted file mode 100644 index ef317622..00000000 --- a/client/images/connectionOn.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/client/images/connectionProgress.svg b/client/images/connectionProgress.svg deleted file mode 100644 index 8c4024c9..00000000 --- a/client/images/connectionProgress.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/images/controls/archive-restore.svg b/client/images/controls/archive-restore.svg new file mode 100644 index 00000000..d3ad8c9e --- /dev/null +++ b/client/images/controls/archive-restore.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/images/controls/bug.svg b/client/images/controls/bug.svg new file mode 100644 index 00000000..80fd2277 --- /dev/null +++ b/client/images/controls/bug.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/client/images/controls/external-link.svg b/client/images/controls/external-link.svg new file mode 100644 index 00000000..6a51c902 --- /dev/null +++ b/client/images/controls/external-link.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/folder-search-2.svg b/client/images/controls/folder-search-2.svg new file mode 100644 index 00000000..f77ce57d --- /dev/null +++ b/client/images/controls/folder-search-2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/gauge.svg b/client/images/controls/gauge.svg new file mode 100644 index 00000000..4b9c1444 --- /dev/null +++ b/client/images/controls/gauge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/controls/help-circle.svg b/client/images/controls/help-circle.svg new file mode 100644 index 00000000..7bcd4450 --- /dev/null +++ b/client/images/controls/help-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/history.svg b/client/images/controls/history.svg new file mode 100644 index 00000000..73beb8b3 --- /dev/null +++ b/client/images/controls/history.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/info.svg b/client/images/controls/info.svg new file mode 100644 index 00000000..43a40245 --- /dev/null +++ b/client/images/controls/info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/map-pin.svg b/client/images/controls/map-pin.svg new file mode 100644 index 00000000..64b75b48 --- /dev/null +++ b/client/images/controls/map-pin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/controls/monitor.svg b/client/images/controls/monitor.svg new file mode 100644 index 00000000..1cdf57c2 --- /dev/null +++ b/client/images/controls/monitor.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/refresh-cw.svg b/client/images/controls/refresh-cw.svg new file mode 100644 index 00000000..9572e3e2 --- /dev/null +++ b/client/images/controls/refresh-cw.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/images/controls/scan-line.svg b/client/images/controls/scan-line.svg new file mode 100644 index 00000000..be4acc2a --- /dev/null +++ b/client/images/controls/scan-line.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/images/controls/tag.svg b/client/images/controls/tag.svg new file mode 100644 index 00000000..88a9db8d --- /dev/null +++ b/client/images/controls/tag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/delete.png b/client/images/delete.png deleted file mode 100644 index 59687bd2..00000000 Binary files a/client/images/delete.png and /dev/null differ diff --git a/client/images/disconnected.png b/client/images/disconnected.png deleted file mode 100644 index 199f71dc..00000000 Binary files a/client/images/disconnected.png and /dev/null differ diff --git a/client/images/download.png b/client/images/download.png deleted file mode 100644 index 0e949133..00000000 Binary files a/client/images/download.png and /dev/null differ diff --git a/client/images/favorites_disabled.png b/client/images/favorites_disabled.png deleted file mode 100644 index 12a821ac..00000000 Binary files a/client/images/favorites_disabled.png and /dev/null differ diff --git a/client/images/favorites_enabled.png b/client/images/favorites_enabled.png deleted file mode 100644 index 61e28f42..00000000 Binary files a/client/images/favorites_enabled.png and /dev/null differ diff --git a/client/images/favorites_hover.png b/client/images/favorites_hover.png deleted file mode 100644 index 71e7a1b2..00000000 Binary files a/client/images/favorites_hover.png and /dev/null differ diff --git a/client/images/flagKit/AD.svg b/client/images/flagKit/AD.svg new file mode 100644 index 00000000..4855f9fb --- /dev/null +++ b/client/images/flagKit/AD.svg @@ -0,0 +1,35 @@ + + + + AD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AE.svg b/client/images/flagKit/AE.svg new file mode 100644 index 00000000..3095fe31 --- /dev/null +++ b/client/images/flagKit/AE.svg @@ -0,0 +1,33 @@ + + + + AE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AF.svg b/client/images/flagKit/AF.svg new file mode 100644 index 00000000..75216b74 --- /dev/null +++ b/client/images/flagKit/AF.svg @@ -0,0 +1,34 @@ + + + + AF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AG.svg b/client/images/flagKit/AG.svg new file mode 100644 index 00000000..ac56b808 --- /dev/null +++ b/client/images/flagKit/AG.svg @@ -0,0 +1,44 @@ + + + + AG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AI.svg b/client/images/flagKit/AI.svg new file mode 100644 index 00000000..7f53e464 --- /dev/null +++ b/client/images/flagKit/AI.svg @@ -0,0 +1,50 @@ + + + + AI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AL.svg b/client/images/flagKit/AL.svg new file mode 100644 index 00000000..43ff1a3b --- /dev/null +++ b/client/images/flagKit/AL.svg @@ -0,0 +1,27 @@ + + + + AL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AM.svg b/client/images/flagKit/AM.svg new file mode 100644 index 00000000..5224d30f --- /dev/null +++ b/client/images/flagKit/AM.svg @@ -0,0 +1,32 @@ + + + + AM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AO.svg b/client/images/flagKit/AO.svg new file mode 100644 index 00000000..86044f3b --- /dev/null +++ b/client/images/flagKit/AO.svg @@ -0,0 +1,37 @@ + + + + AO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AR.svg b/client/images/flagKit/AR.svg new file mode 100644 index 00000000..4dbc96f1 --- /dev/null +++ b/client/images/flagKit/AR.svg @@ -0,0 +1,26 @@ + + + + AR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AS.svg b/client/images/flagKit/AS.svg new file mode 100644 index 00000000..afb37540 --- /dev/null +++ b/client/images/flagKit/AS.svg @@ -0,0 +1,36 @@ + + + + AS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AT.svg b/client/images/flagKit/AT.svg new file mode 100644 index 00000000..627245e3 --- /dev/null +++ b/client/images/flagKit/AT.svg @@ -0,0 +1,24 @@ + + + + AT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AU.svg b/client/images/flagKit/AU.svg new file mode 100644 index 00000000..aad6b1e6 --- /dev/null +++ b/client/images/flagKit/AU.svg @@ -0,0 +1,36 @@ + + + + AU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AW.svg b/client/images/flagKit/AW.svg new file mode 100644 index 00000000..892d8aa0 --- /dev/null +++ b/client/images/flagKit/AW.svg @@ -0,0 +1,30 @@ + + + + AW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AX.svg b/client/images/flagKit/AX.svg new file mode 100644 index 00000000..577cd268 --- /dev/null +++ b/client/images/flagKit/AX.svg @@ -0,0 +1,32 @@ + + + + AX + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/AZ.svg b/client/images/flagKit/AZ.svg new file mode 100644 index 00000000..3f082f33 --- /dev/null +++ b/client/images/flagKit/AZ.svg @@ -0,0 +1,33 @@ + + + + AZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BA.svg b/client/images/flagKit/BA.svg new file mode 100644 index 00000000..a16324e1 --- /dev/null +++ b/client/images/flagKit/BA.svg @@ -0,0 +1,32 @@ + + + + BA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BB.svg b/client/images/flagKit/BB.svg new file mode 100644 index 00000000..5c89e132 --- /dev/null +++ b/client/images/flagKit/BB.svg @@ -0,0 +1,38 @@ + + + + BB + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BD.svg b/client/images/flagKit/BD.svg new file mode 100644 index 00000000..e1a3cd31 --- /dev/null +++ b/client/images/flagKit/BD.svg @@ -0,0 +1,27 @@ + + + + BD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BE.svg b/client/images/flagKit/BE.svg new file mode 100644 index 00000000..ac00173d --- /dev/null +++ b/client/images/flagKit/BE.svg @@ -0,0 +1,32 @@ + + + + BE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BF.svg b/client/images/flagKit/BF.svg new file mode 100644 index 00000000..5b4286bb --- /dev/null +++ b/client/images/flagKit/BF.svg @@ -0,0 +1,28 @@ + + + + BF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BG.svg b/client/images/flagKit/BG.svg new file mode 100644 index 00000000..e8256f47 --- /dev/null +++ b/client/images/flagKit/BG.svg @@ -0,0 +1,28 @@ + + + + BG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BH.svg b/client/images/flagKit/BH.svg new file mode 100644 index 00000000..e1c11093 --- /dev/null +++ b/client/images/flagKit/BH.svg @@ -0,0 +1,23 @@ + + + + BH + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BI.svg b/client/images/flagKit/BI.svg new file mode 100644 index 00000000..2f208253 --- /dev/null +++ b/client/images/flagKit/BI.svg @@ -0,0 +1,36 @@ + + + + BI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BJ.svg b/client/images/flagKit/BJ.svg new file mode 100644 index 00000000..b21c46e0 --- /dev/null +++ b/client/images/flagKit/BJ.svg @@ -0,0 +1,32 @@ + + + + BJ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BL.svg b/client/images/flagKit/BL.svg new file mode 100644 index 00000000..b99bc2c7 --- /dev/null +++ b/client/images/flagKit/BL.svg @@ -0,0 +1,42 @@ + + + + BL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BM.svg b/client/images/flagKit/BM.svg new file mode 100644 index 00000000..798dd8b9 --- /dev/null +++ b/client/images/flagKit/BM.svg @@ -0,0 +1,49 @@ + + + + BM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BN.svg b/client/images/flagKit/BN.svg new file mode 100644 index 00000000..1fe9afc4 --- /dev/null +++ b/client/images/flagKit/BN.svg @@ -0,0 +1,28 @@ + + + + BN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BO.svg b/client/images/flagKit/BO.svg new file mode 100644 index 00000000..7ee247bd --- /dev/null +++ b/client/images/flagKit/BO.svg @@ -0,0 +1,32 @@ + + + + BO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BR.svg b/client/images/flagKit/BR.svg new file mode 100644 index 00000000..17edb103 --- /dev/null +++ b/client/images/flagKit/BR.svg @@ -0,0 +1,35 @@ + + + + BR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BS.svg b/client/images/flagKit/BS.svg new file mode 100644 index 00000000..767423af --- /dev/null +++ b/client/images/flagKit/BS.svg @@ -0,0 +1,33 @@ + + + + BS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BT.svg b/client/images/flagKit/BT.svg new file mode 100644 index 00000000..d2f749bd --- /dev/null +++ b/client/images/flagKit/BT.svg @@ -0,0 +1,27 @@ + + + + BT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BV.svg b/client/images/flagKit/BV.svg new file mode 100644 index 00000000..00a47ee5 --- /dev/null +++ b/client/images/flagKit/BV.svg @@ -0,0 +1,28 @@ + + + + BV + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BW.svg b/client/images/flagKit/BW.svg new file mode 100644 index 00000000..ccac652b --- /dev/null +++ b/client/images/flagKit/BW.svg @@ -0,0 +1,29 @@ + + + + BW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BY.svg b/client/images/flagKit/BY.svg new file mode 100644 index 00000000..d584988d --- /dev/null +++ b/client/images/flagKit/BY.svg @@ -0,0 +1,30 @@ + + + + BY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/BZ.svg b/client/images/flagKit/BZ.svg new file mode 100644 index 00000000..8758df23 --- /dev/null +++ b/client/images/flagKit/BZ.svg @@ -0,0 +1,30 @@ + + + + BZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CA.svg b/client/images/flagKit/CA.svg new file mode 100644 index 00000000..786b609b --- /dev/null +++ b/client/images/flagKit/CA.svg @@ -0,0 +1,25 @@ + + + + CA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CC.svg b/client/images/flagKit/CC.svg new file mode 100644 index 00000000..b96f3016 --- /dev/null +++ b/client/images/flagKit/CC.svg @@ -0,0 +1,33 @@ + + + + CC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CD.svg b/client/images/flagKit/CD.svg new file mode 100644 index 00000000..0d351c30 --- /dev/null +++ b/client/images/flagKit/CD.svg @@ -0,0 +1,31 @@ + + + + CD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CF.svg b/client/images/flagKit/CF.svg new file mode 100644 index 00000000..68566a2e --- /dev/null +++ b/client/images/flagKit/CF.svg @@ -0,0 +1,43 @@ + + + + CF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CG.svg b/client/images/flagKit/CG.svg new file mode 100644 index 00000000..bc4eb95b --- /dev/null +++ b/client/images/flagKit/CG.svg @@ -0,0 +1,34 @@ + + + + CG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CH.svg b/client/images/flagKit/CH.svg new file mode 100644 index 00000000..772f4fa3 --- /dev/null +++ b/client/images/flagKit/CH.svg @@ -0,0 +1,23 @@ + + + + CH + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CI.svg b/client/images/flagKit/CI.svg new file mode 100644 index 00000000..096d98ab --- /dev/null +++ b/client/images/flagKit/CI.svg @@ -0,0 +1,28 @@ + + + + CI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CK.svg b/client/images/flagKit/CK.svg new file mode 100644 index 00000000..c1ea3734 --- /dev/null +++ b/client/images/flagKit/CK.svg @@ -0,0 +1,31 @@ + + + + CK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CL.svg b/client/images/flagKit/CL.svg new file mode 100644 index 00000000..d456d951 --- /dev/null +++ b/client/images/flagKit/CL.svg @@ -0,0 +1,29 @@ + + + + CL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CM.svg b/client/images/flagKit/CM.svg new file mode 100644 index 00000000..482f4a97 --- /dev/null +++ b/client/images/flagKit/CM.svg @@ -0,0 +1,38 @@ + + + + CM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CN.svg b/client/images/flagKit/CN.svg new file mode 100644 index 00000000..883ba157 --- /dev/null +++ b/client/images/flagKit/CN.svg @@ -0,0 +1,32 @@ + + + + CN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CO.svg b/client/images/flagKit/CO.svg new file mode 100644 index 00000000..be492e3d --- /dev/null +++ b/client/images/flagKit/CO.svg @@ -0,0 +1,32 @@ + + + + CO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CR.svg b/client/images/flagKit/CR.svg new file mode 100644 index 00000000..271204eb --- /dev/null +++ b/client/images/flagKit/CR.svg @@ -0,0 +1,29 @@ + + + + CR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CU.svg b/client/images/flagKit/CU.svg new file mode 100644 index 00000000..23750cd9 --- /dev/null +++ b/client/images/flagKit/CU.svg @@ -0,0 +1,32 @@ + + + + CU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CV.svg b/client/images/flagKit/CV.svg new file mode 100644 index 00000000..4b6152fb --- /dev/null +++ b/client/images/flagKit/CV.svg @@ -0,0 +1,30 @@ + + + + CV + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CW.svg b/client/images/flagKit/CW.svg new file mode 100644 index 00000000..14acd27f --- /dev/null +++ b/client/images/flagKit/CW.svg @@ -0,0 +1,29 @@ + + + + CW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CX.svg b/client/images/flagKit/CX.svg new file mode 100644 index 00000000..b3fe73d9 --- /dev/null +++ b/client/images/flagKit/CX.svg @@ -0,0 +1,38 @@ + + + + CX + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CY.svg b/client/images/flagKit/CY.svg new file mode 100644 index 00000000..b7860aa9 --- /dev/null +++ b/client/images/flagKit/CY.svg @@ -0,0 +1,24 @@ + + + + CY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/CZ.svg b/client/images/flagKit/CZ.svg new file mode 100644 index 00000000..d56c61b8 --- /dev/null +++ b/client/images/flagKit/CZ.svg @@ -0,0 +1,28 @@ + + + + CZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/DE.svg b/client/images/flagKit/DE.svg new file mode 100644 index 00000000..4ff1ebd5 --- /dev/null +++ b/client/images/flagKit/DE.svg @@ -0,0 +1,32 @@ + + + + DE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/DJ.svg b/client/images/flagKit/DJ.svg new file mode 100644 index 00000000..c0a019f9 --- /dev/null +++ b/client/images/flagKit/DJ.svg @@ -0,0 +1,33 @@ + + + + DJ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/DK.svg b/client/images/flagKit/DK.svg new file mode 100644 index 00000000..27900e15 --- /dev/null +++ b/client/images/flagKit/DK.svg @@ -0,0 +1,23 @@ + + + + DK + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/DM.svg b/client/images/flagKit/DM.svg new file mode 100644 index 00000000..d5c401eb --- /dev/null +++ b/client/images/flagKit/DM.svg @@ -0,0 +1,41 @@ + + + + DM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/DO.svg b/client/images/flagKit/DO.svg new file mode 100644 index 00000000..9188e0be --- /dev/null +++ b/client/images/flagKit/DO.svg @@ -0,0 +1,33 @@ + + + + DO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/DZ.svg b/client/images/flagKit/DZ.svg new file mode 100644 index 00000000..0920d712 --- /dev/null +++ b/client/images/flagKit/DZ.svg @@ -0,0 +1,29 @@ + + + + DZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/EC.svg b/client/images/flagKit/EC.svg new file mode 100644 index 00000000..0fbd3ea6 --- /dev/null +++ b/client/images/flagKit/EC.svg @@ -0,0 +1,39 @@ + + + + EC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/EE.svg b/client/images/flagKit/EE.svg new file mode 100644 index 00000000..63605223 --- /dev/null +++ b/client/images/flagKit/EE.svg @@ -0,0 +1,28 @@ + + + + EE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/EG.svg b/client/images/flagKit/EG.svg new file mode 100644 index 00000000..32d4447e --- /dev/null +++ b/client/images/flagKit/EG.svg @@ -0,0 +1,30 @@ + + + + EG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ER.svg b/client/images/flagKit/ER.svg new file mode 100644 index 00000000..bb70368b --- /dev/null +++ b/client/images/flagKit/ER.svg @@ -0,0 +1,40 @@ + + + + ER + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ES.svg b/client/images/flagKit/ES.svg new file mode 100644 index 00000000..883554f8 --- /dev/null +++ b/client/images/flagKit/ES.svg @@ -0,0 +1,34 @@ + + + + ES + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ET.svg b/client/images/flagKit/ET.svg new file mode 100644 index 00000000..c4387b9f --- /dev/null +++ b/client/images/flagKit/ET.svg @@ -0,0 +1,42 @@ + + + + ET + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/EU.svg b/client/images/flagKit/EU.svg new file mode 100644 index 00000000..db74ffaf --- /dev/null +++ b/client/images/flagKit/EU.svg @@ -0,0 +1,27 @@ + + + + EU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/FI.svg b/client/images/flagKit/FI.svg new file mode 100644 index 00000000..9d243ed5 --- /dev/null +++ b/client/images/flagKit/FI.svg @@ -0,0 +1,22 @@ + + + + FI + Created with sketchtool. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/FJ.svg b/client/images/flagKit/FJ.svg new file mode 100644 index 00000000..e3ebc9bb --- /dev/null +++ b/client/images/flagKit/FJ.svg @@ -0,0 +1,51 @@ + + + + FJ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/FK.svg b/client/images/flagKit/FK.svg new file mode 100644 index 00000000..01b0f2a8 --- /dev/null +++ b/client/images/flagKit/FK.svg @@ -0,0 +1,58 @@ + + + + FK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/FM.svg b/client/images/flagKit/FM.svg new file mode 100644 index 00000000..befd157c --- /dev/null +++ b/client/images/flagKit/FM.svg @@ -0,0 +1,23 @@ + + + + FM + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/FO.svg b/client/images/flagKit/FO.svg new file mode 100644 index 00000000..77618c05 --- /dev/null +++ b/client/images/flagKit/FO.svg @@ -0,0 +1,27 @@ + + + + FO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/FR.svg b/client/images/flagKit/FR.svg new file mode 100644 index 00000000..940de616 --- /dev/null +++ b/client/images/flagKit/FR.svg @@ -0,0 +1,28 @@ + + + + FR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GA.svg b/client/images/flagKit/GA.svg new file mode 100644 index 00000000..45c68087 --- /dev/null +++ b/client/images/flagKit/GA.svg @@ -0,0 +1,32 @@ + + + + GA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GB.svg b/client/images/flagKit/GB.svg new file mode 100644 index 00000000..679d27c7 --- /dev/null +++ b/client/images/flagKit/GB.svg @@ -0,0 +1,32 @@ + + + + GB + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GD.svg b/client/images/flagKit/GD.svg new file mode 100644 index 00000000..210dc3fd --- /dev/null +++ b/client/images/flagKit/GD.svg @@ -0,0 +1,49 @@ + + + + GD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GE.svg b/client/images/flagKit/GE.svg new file mode 100644 index 00000000..818f3f5b --- /dev/null +++ b/client/images/flagKit/GE.svg @@ -0,0 +1,26 @@ + + + + GE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GF.svg b/client/images/flagKit/GF.svg new file mode 100644 index 00000000..bae1448d --- /dev/null +++ b/client/images/flagKit/GF.svg @@ -0,0 +1,32 @@ + + + + GF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GG.svg b/client/images/flagKit/GG.svg new file mode 100644 index 00000000..fa428535 --- /dev/null +++ b/client/images/flagKit/GG.svg @@ -0,0 +1,27 @@ + + + + GG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GH.svg b/client/images/flagKit/GH.svg new file mode 100644 index 00000000..528473ff --- /dev/null +++ b/client/images/flagKit/GH.svg @@ -0,0 +1,37 @@ + + + + GH + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GI.svg b/client/images/flagKit/GI.svg new file mode 100644 index 00000000..ecd8530a --- /dev/null +++ b/client/images/flagKit/GI.svg @@ -0,0 +1,38 @@ + + + + GI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GL.svg b/client/images/flagKit/GL.svg new file mode 100644 index 00000000..33b22333 --- /dev/null +++ b/client/images/flagKit/GL.svg @@ -0,0 +1,33 @@ + + + + GL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GM.svg b/client/images/flagKit/GM.svg new file mode 100644 index 00000000..b6330f52 --- /dev/null +++ b/client/images/flagKit/GM.svg @@ -0,0 +1,33 @@ + + + + GM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GN.svg b/client/images/flagKit/GN.svg new file mode 100644 index 00000000..2d20595e --- /dev/null +++ b/client/images/flagKit/GN.svg @@ -0,0 +1,32 @@ + + + + GN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GP.svg b/client/images/flagKit/GP.svg new file mode 100644 index 00000000..3dbdcc13 --- /dev/null +++ b/client/images/flagKit/GP.svg @@ -0,0 +1,40 @@ + + + + GP + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GQ.svg b/client/images/flagKit/GQ.svg new file mode 100644 index 00000000..e2d5c67d --- /dev/null +++ b/client/images/flagKit/GQ.svg @@ -0,0 +1,34 @@ + + + + GQ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GR.svg b/client/images/flagKit/GR.svg new file mode 100644 index 00000000..a9b12c00 --- /dev/null +++ b/client/images/flagKit/GR.svg @@ -0,0 +1,22 @@ + + + + GR + Created with sketchtool. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GS.svg b/client/images/flagKit/GS.svg new file mode 100644 index 00000000..03984521 --- /dev/null +++ b/client/images/flagKit/GS.svg @@ -0,0 +1,112 @@ + + + + GS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GT.svg b/client/images/flagKit/GT.svg new file mode 100644 index 00000000..be45ee89 --- /dev/null +++ b/client/images/flagKit/GT.svg @@ -0,0 +1,26 @@ + + + + GT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GU.svg b/client/images/flagKit/GU.svg new file mode 100644 index 00000000..6233a0bb --- /dev/null +++ b/client/images/flagKit/GU.svg @@ -0,0 +1,65 @@ + + + + GU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GW.svg b/client/images/flagKit/GW.svg new file mode 100644 index 00000000..b09530d4 --- /dev/null +++ b/client/images/flagKit/GW.svg @@ -0,0 +1,37 @@ + + + + GW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/GY.svg b/client/images/flagKit/GY.svg new file mode 100644 index 00000000..e5937c24 --- /dev/null +++ b/client/images/flagKit/GY.svg @@ -0,0 +1,42 @@ + + + + GY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/HK.svg b/client/images/flagKit/HK.svg new file mode 100644 index 00000000..f99b8882 --- /dev/null +++ b/client/images/flagKit/HK.svg @@ -0,0 +1,23 @@ + + + + HK + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/HM.svg b/client/images/flagKit/HM.svg new file mode 100644 index 00000000..8ef4f346 --- /dev/null +++ b/client/images/flagKit/HM.svg @@ -0,0 +1,36 @@ + + + + HM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/HN.svg b/client/images/flagKit/HN.svg new file mode 100644 index 00000000..50a48cd9 --- /dev/null +++ b/client/images/flagKit/HN.svg @@ -0,0 +1,33 @@ + + + + HN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/HR.svg b/client/images/flagKit/HR.svg new file mode 100644 index 00000000..a6cf5daa --- /dev/null +++ b/client/images/flagKit/HR.svg @@ -0,0 +1,35 @@ + + + + HR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/HT.svg b/client/images/flagKit/HT.svg new file mode 100644 index 00000000..0cd82be1 --- /dev/null +++ b/client/images/flagKit/HT.svg @@ -0,0 +1,46 @@ + + + + HT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/HU.svg b/client/images/flagKit/HU.svg new file mode 100644 index 00000000..795319ea --- /dev/null +++ b/client/images/flagKit/HU.svg @@ -0,0 +1,28 @@ + + + + HU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ID.svg b/client/images/flagKit/ID.svg new file mode 100644 index 00000000..8101da05 --- /dev/null +++ b/client/images/flagKit/ID.svg @@ -0,0 +1,23 @@ + + + + ID + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IE.svg b/client/images/flagKit/IE.svg new file mode 100644 index 00000000..60d9af87 --- /dev/null +++ b/client/images/flagKit/IE.svg @@ -0,0 +1,28 @@ + + + + IE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IL.svg b/client/images/flagKit/IL.svg new file mode 100644 index 00000000..7646f91e --- /dev/null +++ b/client/images/flagKit/IL.svg @@ -0,0 +1,26 @@ + + + + IL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IM.svg b/client/images/flagKit/IM.svg new file mode 100644 index 00000000..ecc7c12e --- /dev/null +++ b/client/images/flagKit/IM.svg @@ -0,0 +1,30 @@ + + + + IM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IN.svg b/client/images/flagKit/IN.svg new file mode 100644 index 00000000..3726ceb7 --- /dev/null +++ b/client/images/flagKit/IN.svg @@ -0,0 +1,31 @@ + + + + IN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IO.svg b/client/images/flagKit/IO.svg new file mode 100644 index 00000000..4d8b5229 --- /dev/null +++ b/client/images/flagKit/IO.svg @@ -0,0 +1,33 @@ + + + + IO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IQ.svg b/client/images/flagKit/IQ.svg new file mode 100644 index 00000000..16c4cf18 --- /dev/null +++ b/client/images/flagKit/IQ.svg @@ -0,0 +1,33 @@ + + + + IQ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IR.svg b/client/images/flagKit/IR.svg new file mode 100644 index 00000000..af325017 --- /dev/null +++ b/client/images/flagKit/IR.svg @@ -0,0 +1,31 @@ + + + + IR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IS.svg b/client/images/flagKit/IS.svg new file mode 100644 index 00000000..385a2bf9 --- /dev/null +++ b/client/images/flagKit/IS.svg @@ -0,0 +1,28 @@ + + + + IS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/IT.svg b/client/images/flagKit/IT.svg new file mode 100644 index 00000000..9e76f24c --- /dev/null +++ b/client/images/flagKit/IT.svg @@ -0,0 +1,28 @@ + + + + IT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/JE.svg b/client/images/flagKit/JE.svg new file mode 100644 index 00000000..6663c504 --- /dev/null +++ b/client/images/flagKit/JE.svg @@ -0,0 +1,32 @@ + + + + JE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/JM.svg b/client/images/flagKit/JM.svg new file mode 100644 index 00000000..54779e77 --- /dev/null +++ b/client/images/flagKit/JM.svg @@ -0,0 +1,33 @@ + + + + JM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/JO.svg b/client/images/flagKit/JO.svg new file mode 100644 index 00000000..b0788e76 --- /dev/null +++ b/client/images/flagKit/JO.svg @@ -0,0 +1,34 @@ + + + + JO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/JP.svg b/client/images/flagKit/JP.svg new file mode 100644 index 00000000..0a655c04 --- /dev/null +++ b/client/images/flagKit/JP.svg @@ -0,0 +1,22 @@ + + + + JP + Created with sketchtool. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KE.svg b/client/images/flagKit/KE.svg new file mode 100644 index 00000000..6c6a6cf6 --- /dev/null +++ b/client/images/flagKit/KE.svg @@ -0,0 +1,43 @@ + + + + KE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KG.svg b/client/images/flagKit/KG.svg new file mode 100644 index 00000000..12e6a244 --- /dev/null +++ b/client/images/flagKit/KG.svg @@ -0,0 +1,28 @@ + + + + KG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KH.svg b/client/images/flagKit/KH.svg new file mode 100644 index 00000000..9ea454bb --- /dev/null +++ b/client/images/flagKit/KH.svg @@ -0,0 +1,29 @@ + + + + KH + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KI.svg b/client/images/flagKit/KI.svg new file mode 100644 index 00000000..e00e2352 --- /dev/null +++ b/client/images/flagKit/KI.svg @@ -0,0 +1,35 @@ + + + + KI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KM.svg b/client/images/flagKit/KM.svg new file mode 100644 index 00000000..2da152d5 --- /dev/null +++ b/client/images/flagKit/KM.svg @@ -0,0 +1,39 @@ + + + + KM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KN.svg b/client/images/flagKit/KN.svg new file mode 100644 index 00000000..e65b7b61 --- /dev/null +++ b/client/images/flagKit/KN.svg @@ -0,0 +1,39 @@ + + + + KN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KP.svg b/client/images/flagKit/KP.svg new file mode 100644 index 00000000..649feb27 --- /dev/null +++ b/client/images/flagKit/KP.svg @@ -0,0 +1,30 @@ + + + + KP + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KR.svg b/client/images/flagKit/KR.svg new file mode 100644 index 00000000..078665a5 --- /dev/null +++ b/client/images/flagKit/KR.svg @@ -0,0 +1,38 @@ + + + + KR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KW.svg b/client/images/flagKit/KW.svg new file mode 100644 index 00000000..a73b0113 --- /dev/null +++ b/client/images/flagKit/KW.svg @@ -0,0 +1,33 @@ + + + + KW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KY.svg b/client/images/flagKit/KY.svg new file mode 100644 index 00000000..2240dbc6 --- /dev/null +++ b/client/images/flagKit/KY.svg @@ -0,0 +1,44 @@ + + + + KY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/KZ.svg b/client/images/flagKit/KZ.svg new file mode 100644 index 00000000..6076ac54 --- /dev/null +++ b/client/images/flagKit/KZ.svg @@ -0,0 +1,29 @@ + + + + KZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LA.svg b/client/images/flagKit/LA.svg new file mode 100644 index 00000000..5b740da7 --- /dev/null +++ b/client/images/flagKit/LA.svg @@ -0,0 +1,29 @@ + + + + LA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LB.svg b/client/images/flagKit/LB.svg new file mode 100644 index 00000000..401a235d --- /dev/null +++ b/client/images/flagKit/LB.svg @@ -0,0 +1,29 @@ + + + + LB + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LC.svg b/client/images/flagKit/LC.svg new file mode 100644 index 00000000..8d809d3e --- /dev/null +++ b/client/images/flagKit/LC.svg @@ -0,0 +1,33 @@ + + + + LC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LI.svg b/client/images/flagKit/LI.svg new file mode 100644 index 00000000..1160975a --- /dev/null +++ b/client/images/flagKit/LI.svg @@ -0,0 +1,27 @@ + + + + LI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LICENSE b/client/images/flagKit/LICENSE new file mode 100644 index 00000000..59f47bf1 --- /dev/null +++ b/client/images/flagKit/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Bowtie AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/client/images/flagKit/LK.svg b/client/images/flagKit/LK.svg new file mode 100644 index 00000000..55386d5c --- /dev/null +++ b/client/images/flagKit/LK.svg @@ -0,0 +1,43 @@ + + + + LK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LR.svg b/client/images/flagKit/LR.svg new file mode 100644 index 00000000..3d6cef1e --- /dev/null +++ b/client/images/flagKit/LR.svg @@ -0,0 +1,36 @@ + + + + LR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LS.svg b/client/images/flagKit/LS.svg new file mode 100644 index 00000000..3ec5277d --- /dev/null +++ b/client/images/flagKit/LS.svg @@ -0,0 +1,34 @@ + + + + LS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LT.svg b/client/images/flagKit/LT.svg new file mode 100644 index 00000000..8e592267 --- /dev/null +++ b/client/images/flagKit/LT.svg @@ -0,0 +1,32 @@ + + + + LT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LU.svg b/client/images/flagKit/LU.svg new file mode 100644 index 00000000..860e730b --- /dev/null +++ b/client/images/flagKit/LU.svg @@ -0,0 +1,28 @@ + + + + LU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LV.svg b/client/images/flagKit/LV.svg new file mode 100644 index 00000000..5d0255e6 --- /dev/null +++ b/client/images/flagKit/LV.svg @@ -0,0 +1,24 @@ + + + + LV + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/LY.svg b/client/images/flagKit/LY.svg new file mode 100644 index 00000000..4b9f2a0e --- /dev/null +++ b/client/images/flagKit/LY.svg @@ -0,0 +1,33 @@ + + + + LY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MA.svg b/client/images/flagKit/MA.svg new file mode 100644 index 00000000..cb22ba95 --- /dev/null +++ b/client/images/flagKit/MA.svg @@ -0,0 +1,23 @@ + + + + MA + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MC.svg b/client/images/flagKit/MC.svg new file mode 100644 index 00000000..207590a7 --- /dev/null +++ b/client/images/flagKit/MC.svg @@ -0,0 +1,23 @@ + + + + MC + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MD.svg b/client/images/flagKit/MD.svg new file mode 100644 index 00000000..301e93ee --- /dev/null +++ b/client/images/flagKit/MD.svg @@ -0,0 +1,42 @@ + + + + MD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ME.svg b/client/images/flagKit/ME.svg new file mode 100644 index 00000000..9b0838e9 --- /dev/null +++ b/client/images/flagKit/ME.svg @@ -0,0 +1,29 @@ + + + + ME + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MF.svg b/client/images/flagKit/MF.svg new file mode 100644 index 00000000..c45b62a0 --- /dev/null +++ b/client/images/flagKit/MF.svg @@ -0,0 +1,28 @@ + + + + MF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MG.svg b/client/images/flagKit/MG.svg new file mode 100644 index 00000000..c173fdd5 --- /dev/null +++ b/client/images/flagKit/MG.svg @@ -0,0 +1,28 @@ + + + + MG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MH.svg b/client/images/flagKit/MH.svg new file mode 100644 index 00000000..e6b66091 --- /dev/null +++ b/client/images/flagKit/MH.svg @@ -0,0 +1,29 @@ + + + + MH + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MK.svg b/client/images/flagKit/MK.svg new file mode 100644 index 00000000..35b92297 --- /dev/null +++ b/client/images/flagKit/MK.svg @@ -0,0 +1,29 @@ + + + + MK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ML.svg b/client/images/flagKit/ML.svg new file mode 100644 index 00000000..babc6e59 --- /dev/null +++ b/client/images/flagKit/ML.svg @@ -0,0 +1,32 @@ + + + + ML + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MM.svg b/client/images/flagKit/MM.svg new file mode 100644 index 00000000..eb3c18a3 --- /dev/null +++ b/client/images/flagKit/MM.svg @@ -0,0 +1,33 @@ + + + + MM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MN.svg b/client/images/flagKit/MN.svg new file mode 100644 index 00000000..8af15a51 --- /dev/null +++ b/client/images/flagKit/MN.svg @@ -0,0 +1,33 @@ + + + + MN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MO.svg b/client/images/flagKit/MO.svg new file mode 100644 index 00000000..be4bc875 --- /dev/null +++ b/client/images/flagKit/MO.svg @@ -0,0 +1,26 @@ + + + + MO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MP.svg b/client/images/flagKit/MP.svg new file mode 100644 index 00000000..33151489 --- /dev/null +++ b/client/images/flagKit/MP.svg @@ -0,0 +1,29 @@ + + + + MP + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MQ.svg b/client/images/flagKit/MQ.svg new file mode 100644 index 00000000..adc82074 --- /dev/null +++ b/client/images/flagKit/MQ.svg @@ -0,0 +1,27 @@ + + + + MQ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MR.svg b/client/images/flagKit/MR.svg new file mode 100644 index 00000000..da5adee6 --- /dev/null +++ b/client/images/flagKit/MR.svg @@ -0,0 +1,27 @@ + + + + MR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MS.svg b/client/images/flagKit/MS.svg new file mode 100644 index 00000000..184c9178 --- /dev/null +++ b/client/images/flagKit/MS.svg @@ -0,0 +1,47 @@ + + + + MS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MT.svg b/client/images/flagKit/MT.svg new file mode 100644 index 00000000..5ce0b3fe --- /dev/null +++ b/client/images/flagKit/MT.svg @@ -0,0 +1,29 @@ + + + + MT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MU.svg b/client/images/flagKit/MU.svg new file mode 100644 index 00000000..f2c6f3f8 --- /dev/null +++ b/client/images/flagKit/MU.svg @@ -0,0 +1,37 @@ + + + + MU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MV.svg b/client/images/flagKit/MV.svg new file mode 100644 index 00000000..f10e07d5 --- /dev/null +++ b/client/images/flagKit/MV.svg @@ -0,0 +1,28 @@ + + + + MV + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MW.svg b/client/images/flagKit/MW.svg new file mode 100644 index 00000000..5b0cc5c6 --- /dev/null +++ b/client/images/flagKit/MW.svg @@ -0,0 +1,33 @@ + + + + MW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MX.svg b/client/images/flagKit/MX.svg new file mode 100644 index 00000000..7ed245bc --- /dev/null +++ b/client/images/flagKit/MX.svg @@ -0,0 +1,30 @@ + + + + MX + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MY.svg b/client/images/flagKit/MY.svg new file mode 100644 index 00000000..e7ff885f --- /dev/null +++ b/client/images/flagKit/MY.svg @@ -0,0 +1,32 @@ + + + + MY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/MZ.svg b/client/images/flagKit/MZ.svg new file mode 100644 index 00000000..7f553b00 --- /dev/null +++ b/client/images/flagKit/MZ.svg @@ -0,0 +1,43 @@ + + + + MZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NA.svg b/client/images/flagKit/NA.svg new file mode 100644 index 00000000..cb0ba69f --- /dev/null +++ b/client/images/flagKit/NA.svg @@ -0,0 +1,75 @@ + + + + NA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NC.svg b/client/images/flagKit/NC.svg new file mode 100644 index 00000000..bae580e8 --- /dev/null +++ b/client/images/flagKit/NC.svg @@ -0,0 +1,42 @@ + + + + NC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NE.svg b/client/images/flagKit/NE.svg new file mode 100644 index 00000000..12bcf8a0 --- /dev/null +++ b/client/images/flagKit/NE.svg @@ -0,0 +1,33 @@ + + + + NE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NF.svg b/client/images/flagKit/NF.svg new file mode 100644 index 00000000..b707e52d --- /dev/null +++ b/client/images/flagKit/NF.svg @@ -0,0 +1,29 @@ + + + + NF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NG.svg b/client/images/flagKit/NG.svg new file mode 100644 index 00000000..4063ff84 --- /dev/null +++ b/client/images/flagKit/NG.svg @@ -0,0 +1,24 @@ + + + + NG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NI.svg b/client/images/flagKit/NI.svg new file mode 100644 index 00000000..7adb4ba4 --- /dev/null +++ b/client/images/flagKit/NI.svg @@ -0,0 +1,26 @@ + + + + NI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NL.svg b/client/images/flagKit/NL.svg new file mode 100644 index 00000000..c62f42ad --- /dev/null +++ b/client/images/flagKit/NL.svg @@ -0,0 +1,28 @@ + + + + NL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NO.svg b/client/images/flagKit/NO.svg new file mode 100644 index 00000000..cdc23f49 --- /dev/null +++ b/client/images/flagKit/NO.svg @@ -0,0 +1,28 @@ + + + + NO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NP.svg b/client/images/flagKit/NP.svg new file mode 100644 index 00000000..c879fa80 --- /dev/null +++ b/client/images/flagKit/NP.svg @@ -0,0 +1,35 @@ + + + + NP + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NR.svg b/client/images/flagKit/NR.svg new file mode 100644 index 00000000..1a6c3a21 --- /dev/null +++ b/client/images/flagKit/NR.svg @@ -0,0 +1,28 @@ + + + + NR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NU.svg b/client/images/flagKit/NU.svg new file mode 100644 index 00000000..3d9bc80c --- /dev/null +++ b/client/images/flagKit/NU.svg @@ -0,0 +1,41 @@ + + + + NU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/NZ.svg b/client/images/flagKit/NZ.svg new file mode 100644 index 00000000..c1f624df --- /dev/null +++ b/client/images/flagKit/NZ.svg @@ -0,0 +1,34 @@ + + + + NZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/OM.svg b/client/images/flagKit/OM.svg new file mode 100644 index 00000000..cb08ac82 --- /dev/null +++ b/client/images/flagKit/OM.svg @@ -0,0 +1,29 @@ + + + + OM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PA.svg b/client/images/flagKit/PA.svg new file mode 100644 index 00000000..d8516682 --- /dev/null +++ b/client/images/flagKit/PA.svg @@ -0,0 +1,30 @@ + + + + PA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PE.svg b/client/images/flagKit/PE.svg new file mode 100644 index 00000000..98a26cf2 --- /dev/null +++ b/client/images/flagKit/PE.svg @@ -0,0 +1,24 @@ + + + + PE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PF.svg b/client/images/flagKit/PF.svg new file mode 100644 index 00000000..b29385f4 --- /dev/null +++ b/client/images/flagKit/PF.svg @@ -0,0 +1,52 @@ + + + + PF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PG.svg b/client/images/flagKit/PG.svg new file mode 100644 index 00000000..0630fab6 --- /dev/null +++ b/client/images/flagKit/PG.svg @@ -0,0 +1,36 @@ + + + + PG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PH.svg b/client/images/flagKit/PH.svg new file mode 100644 index 00000000..4c1087b8 --- /dev/null +++ b/client/images/flagKit/PH.svg @@ -0,0 +1,33 @@ + + + + PH + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PK.svg b/client/images/flagKit/PK.svg new file mode 100644 index 00000000..7ecb09cf --- /dev/null +++ b/client/images/flagKit/PK.svg @@ -0,0 +1,32 @@ + + + + PK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PL.svg b/client/images/flagKit/PL.svg new file mode 100644 index 00000000..fadbd2d6 --- /dev/null +++ b/client/images/flagKit/PL.svg @@ -0,0 +1,23 @@ + + + + PL + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PM.svg b/client/images/flagKit/PM.svg new file mode 100644 index 00000000..1f39fd0a --- /dev/null +++ b/client/images/flagKit/PM.svg @@ -0,0 +1,66 @@ + + + + PM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PN.svg b/client/images/flagKit/PN.svg new file mode 100644 index 00000000..f2b2cc4f --- /dev/null +++ b/client/images/flagKit/PN.svg @@ -0,0 +1,51 @@ + + + + PN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PR.svg b/client/images/flagKit/PR.svg new file mode 100644 index 00000000..7d120445 --- /dev/null +++ b/client/images/flagKit/PR.svg @@ -0,0 +1,30 @@ + + + + PR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PS.svg b/client/images/flagKit/PS.svg new file mode 100644 index 00000000..e68583ba --- /dev/null +++ b/client/images/flagKit/PS.svg @@ -0,0 +1,33 @@ + + + + PS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PT.svg b/client/images/flagKit/PT.svg new file mode 100644 index 00000000..49b59be2 --- /dev/null +++ b/client/images/flagKit/PT.svg @@ -0,0 +1,38 @@ + + + + PT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PW.svg b/client/images/flagKit/PW.svg new file mode 100644 index 00000000..4ab7f166 --- /dev/null +++ b/client/images/flagKit/PW.svg @@ -0,0 +1,27 @@ + + + + PW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/PY.svg b/client/images/flagKit/PY.svg new file mode 100644 index 00000000..2ae00546 --- /dev/null +++ b/client/images/flagKit/PY.svg @@ -0,0 +1,30 @@ + + + + PY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/QA.svg b/client/images/flagKit/QA.svg new file mode 100644 index 00000000..985171d1 --- /dev/null +++ b/client/images/flagKit/QA.svg @@ -0,0 +1,23 @@ + + + + QA + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/RE.svg b/client/images/flagKit/RE.svg new file mode 100644 index 00000000..7e130938 --- /dev/null +++ b/client/images/flagKit/RE.svg @@ -0,0 +1,28 @@ + + + + RE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/RO.svg b/client/images/flagKit/RO.svg new file mode 100644 index 00000000..dd82b266 --- /dev/null +++ b/client/images/flagKit/RO.svg @@ -0,0 +1,32 @@ + + + + RO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/RS.svg b/client/images/flagKit/RS.svg new file mode 100644 index 00000000..892dd5e4 --- /dev/null +++ b/client/images/flagKit/RS.svg @@ -0,0 +1,39 @@ + + + + RS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/RU.svg b/client/images/flagKit/RU.svg new file mode 100644 index 00000000..a9ba65b5 --- /dev/null +++ b/client/images/flagKit/RU.svg @@ -0,0 +1,28 @@ + + + + RU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/RW.svg b/client/images/flagKit/RW.svg new file mode 100644 index 00000000..43b26156 --- /dev/null +++ b/client/images/flagKit/RW.svg @@ -0,0 +1,37 @@ + + + + RW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SA.svg b/client/images/flagKit/SA.svg new file mode 100644 index 00000000..735b986f --- /dev/null +++ b/client/images/flagKit/SA.svg @@ -0,0 +1,26 @@ + + + + SA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SB.svg b/client/images/flagKit/SB.svg new file mode 100644 index 00000000..768c45c0 --- /dev/null +++ b/client/images/flagKit/SB.svg @@ -0,0 +1,39 @@ + + + + SB + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SC.svg b/client/images/flagKit/SC.svg new file mode 100644 index 00000000..62b380b8 --- /dev/null +++ b/client/images/flagKit/SC.svg @@ -0,0 +1,43 @@ + + + + SC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SD.svg b/client/images/flagKit/SD.svg new file mode 100644 index 00000000..c68d6b1b --- /dev/null +++ b/client/images/flagKit/SD.svg @@ -0,0 +1,33 @@ + + + + SD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SE.svg b/client/images/flagKit/SE.svg new file mode 100644 index 00000000..bb4f4e11 --- /dev/null +++ b/client/images/flagKit/SE.svg @@ -0,0 +1,27 @@ + + + + SE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SG.svg b/client/images/flagKit/SG.svg new file mode 100644 index 00000000..27011483 --- /dev/null +++ b/client/images/flagKit/SG.svg @@ -0,0 +1,24 @@ + + + + SG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SH.svg b/client/images/flagKit/SH.svg new file mode 100644 index 00000000..e0dde764 --- /dev/null +++ b/client/images/flagKit/SH.svg @@ -0,0 +1,53 @@ + + + + SH + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SI.svg b/client/images/flagKit/SI.svg new file mode 100644 index 00000000..497f8705 --- /dev/null +++ b/client/images/flagKit/SI.svg @@ -0,0 +1,28 @@ + + + + SI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SJ.svg b/client/images/flagKit/SJ.svg new file mode 100644 index 00000000..bef7e505 --- /dev/null +++ b/client/images/flagKit/SJ.svg @@ -0,0 +1,28 @@ + + + + SJ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SK.svg b/client/images/flagKit/SK.svg new file mode 100644 index 00000000..2b8ba801 --- /dev/null +++ b/client/images/flagKit/SK.svg @@ -0,0 +1,46 @@ + + + + SK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SL.svg b/client/images/flagKit/SL.svg new file mode 100644 index 00000000..817419ef --- /dev/null +++ b/client/images/flagKit/SL.svg @@ -0,0 +1,28 @@ + + + + SL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SM.svg b/client/images/flagKit/SM.svg new file mode 100644 index 00000000..abf62171 --- /dev/null +++ b/client/images/flagKit/SM.svg @@ -0,0 +1,25 @@ + + + + SM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SN.svg b/client/images/flagKit/SN.svg new file mode 100644 index 00000000..09484160 --- /dev/null +++ b/client/images/flagKit/SN.svg @@ -0,0 +1,33 @@ + + + + SN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SO.svg b/client/images/flagKit/SO.svg new file mode 100644 index 00000000..6372e377 --- /dev/null +++ b/client/images/flagKit/SO.svg @@ -0,0 +1,23 @@ + + + + SO + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SR.svg b/client/images/flagKit/SR.svg new file mode 100644 index 00000000..97963b0c --- /dev/null +++ b/client/images/flagKit/SR.svg @@ -0,0 +1,34 @@ + + + + SR + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SS.svg b/client/images/flagKit/SS.svg new file mode 100644 index 00000000..e8d68dd0 --- /dev/null +++ b/client/images/flagKit/SS.svg @@ -0,0 +1,44 @@ + + + + SS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ST.svg b/client/images/flagKit/ST.svg new file mode 100644 index 00000000..4b355d71 --- /dev/null +++ b/client/images/flagKit/ST.svg @@ -0,0 +1,39 @@ + + + + ST + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SV.svg b/client/images/flagKit/SV.svg new file mode 100644 index 00000000..9bfdd5ce --- /dev/null +++ b/client/images/flagKit/SV.svg @@ -0,0 +1,30 @@ + + + + SV + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SX.svg b/client/images/flagKit/SX.svg new file mode 100644 index 00000000..ccefe037 --- /dev/null +++ b/client/images/flagKit/SX.svg @@ -0,0 +1,45 @@ + + + + SX + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SY.svg b/client/images/flagKit/SY.svg new file mode 100644 index 00000000..040530b6 --- /dev/null +++ b/client/images/flagKit/SY.svg @@ -0,0 +1,34 @@ + + + + SY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/SZ.svg b/client/images/flagKit/SZ.svg new file mode 100644 index 00000000..fc4120de --- /dev/null +++ b/client/images/flagKit/SZ.svg @@ -0,0 +1,47 @@ + + + + SZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TC.svg b/client/images/flagKit/TC.svg new file mode 100644 index 00000000..c3ea149a --- /dev/null +++ b/client/images/flagKit/TC.svg @@ -0,0 +1,40 @@ + + + + TC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TD.svg b/client/images/flagKit/TD.svg new file mode 100644 index 00000000..74756faf --- /dev/null +++ b/client/images/flagKit/TD.svg @@ -0,0 +1,32 @@ + + + + TD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TF.svg b/client/images/flagKit/TF.svg new file mode 100644 index 00000000..d1ea6918 --- /dev/null +++ b/client/images/flagKit/TF.svg @@ -0,0 +1,35 @@ + + + + TF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TG.svg b/client/images/flagKit/TG.svg new file mode 100644 index 00000000..e9f6360f --- /dev/null +++ b/client/images/flagKit/TG.svg @@ -0,0 +1,33 @@ + + + + TG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TH.svg b/client/images/flagKit/TH.svg new file mode 100644 index 00000000..1bf403a2 --- /dev/null +++ b/client/images/flagKit/TH.svg @@ -0,0 +1,29 @@ + + + + TH + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TJ.svg b/client/images/flagKit/TJ.svg new file mode 100644 index 00000000..77d6728b --- /dev/null +++ b/client/images/flagKit/TJ.svg @@ -0,0 +1,29 @@ + + + + TJ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TK.svg b/client/images/flagKit/TK.svg new file mode 100644 index 00000000..3cde9608 --- /dev/null +++ b/client/images/flagKit/TK.svg @@ -0,0 +1,31 @@ + + + + TK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TL.svg b/client/images/flagKit/TL.svg new file mode 100644 index 00000000..41b89521 --- /dev/null +++ b/client/images/flagKit/TL.svg @@ -0,0 +1,33 @@ + + + + TL + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TM.svg b/client/images/flagKit/TM.svg new file mode 100644 index 00000000..dac62a13 --- /dev/null +++ b/client/images/flagKit/TM.svg @@ -0,0 +1,74 @@ + + + + TM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TN.svg b/client/images/flagKit/TN.svg new file mode 100644 index 00000000..3ff74a9e --- /dev/null +++ b/client/images/flagKit/TN.svg @@ -0,0 +1,23 @@ + + + + TN + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TO.svg b/client/images/flagKit/TO.svg new file mode 100644 index 00000000..e0e42ee2 --- /dev/null +++ b/client/images/flagKit/TO.svg @@ -0,0 +1,28 @@ + + + + TO + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TR.svg b/client/images/flagKit/TR.svg new file mode 100644 index 00000000..e5c0924d --- /dev/null +++ b/client/images/flagKit/TR.svg @@ -0,0 +1,23 @@ + + + + TR + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TT.svg b/client/images/flagKit/TT.svg new file mode 100644 index 00000000..69bdb9a9 --- /dev/null +++ b/client/images/flagKit/TT.svg @@ -0,0 +1,28 @@ + + + + TT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TV.svg b/client/images/flagKit/TV.svg new file mode 100644 index 00000000..839c97f1 --- /dev/null +++ b/client/images/flagKit/TV.svg @@ -0,0 +1,36 @@ + + + + TV + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TW.svg b/client/images/flagKit/TW.svg new file mode 100644 index 00000000..488d1120 --- /dev/null +++ b/client/images/flagKit/TW.svg @@ -0,0 +1,28 @@ + + + + TW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/TZ.svg b/client/images/flagKit/TZ.svg new file mode 100644 index 00000000..d652e211 --- /dev/null +++ b/client/images/flagKit/TZ.svg @@ -0,0 +1,37 @@ + + + + TZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/UA.svg b/client/images/flagKit/UA.svg new file mode 100644 index 00000000..8dac8366 --- /dev/null +++ b/client/images/flagKit/UA.svg @@ -0,0 +1,27 @@ + + + + UA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/UG.svg b/client/images/flagKit/UG.svg new file mode 100644 index 00000000..7fabd77c --- /dev/null +++ b/client/images/flagKit/UG.svg @@ -0,0 +1,37 @@ + + + + UG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/UM.svg b/client/images/flagKit/UM.svg new file mode 100644 index 00000000..1a8fc6a7 --- /dev/null +++ b/client/images/flagKit/UM.svg @@ -0,0 +1,28 @@ + + + + UM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/US.svg b/client/images/flagKit/US.svg new file mode 100644 index 00000000..846ec9d2 --- /dev/null +++ b/client/images/flagKit/US.svg @@ -0,0 +1,28 @@ + + + + US + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/UY.svg b/client/images/flagKit/UY.svg new file mode 100644 index 00000000..81c28154 --- /dev/null +++ b/client/images/flagKit/UY.svg @@ -0,0 +1,29 @@ + + + + UY + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/UZ.svg b/client/images/flagKit/UZ.svg new file mode 100644 index 00000000..f6cf2140 --- /dev/null +++ b/client/images/flagKit/UZ.svg @@ -0,0 +1,29 @@ + + + + UZ + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VA.svg b/client/images/flagKit/VA.svg new file mode 100644 index 00000000..14c78aaa --- /dev/null +++ b/client/images/flagKit/VA.svg @@ -0,0 +1,39 @@ + + + + VA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VC.svg b/client/images/flagKit/VC.svg new file mode 100644 index 00000000..22cc1d53 --- /dev/null +++ b/client/images/flagKit/VC.svg @@ -0,0 +1,37 @@ + + + + VC + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VE.svg b/client/images/flagKit/VE.svg new file mode 100644 index 00000000..1a14634f --- /dev/null +++ b/client/images/flagKit/VE.svg @@ -0,0 +1,33 @@ + + + + VE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VG.svg b/client/images/flagKit/VG.svg new file mode 100644 index 00000000..c3c31ed1 --- /dev/null +++ b/client/images/flagKit/VG.svg @@ -0,0 +1,42 @@ + + + + VG + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VI.svg b/client/images/flagKit/VI.svg new file mode 100644 index 00000000..071cf62c --- /dev/null +++ b/client/images/flagKit/VI.svg @@ -0,0 +1,49 @@ + + + + VI + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VN.svg b/client/images/flagKit/VN.svg new file mode 100644 index 00000000..2bb79564 --- /dev/null +++ b/client/images/flagKit/VN.svg @@ -0,0 +1,27 @@ + + + + VN + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/VU.svg b/client/images/flagKit/VU.svg new file mode 100644 index 00000000..26e02981 --- /dev/null +++ b/client/images/flagKit/VU.svg @@ -0,0 +1,38 @@ + + + + VU + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/WF.svg b/client/images/flagKit/WF.svg new file mode 100644 index 00000000..26a5e414 --- /dev/null +++ b/client/images/flagKit/WF.svg @@ -0,0 +1,28 @@ + + + + WF + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/WS.svg b/client/images/flagKit/WS.svg new file mode 100644 index 00000000..756c78f5 --- /dev/null +++ b/client/images/flagKit/WS.svg @@ -0,0 +1,28 @@ + + + + WS + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/XK.svg b/client/images/flagKit/XK.svg new file mode 100644 index 00000000..a9c245fd --- /dev/null +++ b/client/images/flagKit/XK.svg @@ -0,0 +1,28 @@ + + + + XK + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/YE.svg b/client/images/flagKit/YE.svg new file mode 100644 index 00000000..535406f9 --- /dev/null +++ b/client/images/flagKit/YE.svg @@ -0,0 +1,28 @@ + + + + YE + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/YT.svg b/client/images/flagKit/YT.svg new file mode 100644 index 00000000..be67985d --- /dev/null +++ b/client/images/flagKit/YT.svg @@ -0,0 +1,77 @@ + + + + YT + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ZA.svg b/client/images/flagKit/ZA.svg new file mode 100644 index 00000000..f3ad3726 --- /dev/null +++ b/client/images/flagKit/ZA.svg @@ -0,0 +1,44 @@ + + + + ZA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ZM.svg b/client/images/flagKit/ZM.svg new file mode 100644 index 00000000..3e6f42a8 --- /dev/null +++ b/client/images/flagKit/ZM.svg @@ -0,0 +1,42 @@ + + + + ZM + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/flagKit/ZW.svg b/client/images/flagKit/ZW.svg new file mode 100644 index 00000000..dfaf1f3f --- /dev/null +++ b/client/images/flagKit/ZW.svg @@ -0,0 +1,43 @@ + + + + ZW + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/folder.png b/client/images/folder.png deleted file mode 100644 index 07407596..00000000 Binary files a/client/images/folder.png and /dev/null differ diff --git a/client/images/icon_src.png b/client/images/icon_src.png deleted file mode 100644 index 1201a89a..00000000 Binary files a/client/images/icon_src.png and /dev/null differ diff --git a/client/images/icon_src.svg b/client/images/icon_src.svg deleted file mode 100644 index b27d1360..00000000 --- a/client/images/icon_src.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - diff --git a/client/images/settings.png b/client/images/settings.png deleted file mode 100644 index a64c6116..00000000 Binary files a/client/images/settings.png and /dev/null differ diff --git a/client/images/settings_grey.png b/client/images/settings_grey.png deleted file mode 100644 index 60127b5c..00000000 Binary files a/client/images/settings_grey.png and /dev/null differ diff --git a/client/images/share.png b/client/images/share.png deleted file mode 100644 index e1451e06..00000000 Binary files a/client/images/share.png and /dev/null differ diff --git a/client/images/svg/close_black_24dp.svg b/client/images/svg/close_black_24dp.svg deleted file mode 100644 index 5f1267d7..00000000 --- a/client/images/svg/close_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/control_point_black_24dp.svg b/client/images/svg/control_point_black_24dp.svg deleted file mode 100644 index 75b25e67..00000000 --- a/client/images/svg/control_point_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/delete_black_24dp.svg b/client/images/svg/delete_black_24dp.svg deleted file mode 100644 index 69a68354..00000000 --- a/client/images/svg/delete_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/density_small_black_24dp.svg b/client/images/svg/density_small_black_24dp.svg deleted file mode 100644 index f79483de..00000000 --- a/client/images/svg/density_small_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/done_black_24dp.svg b/client/images/svg/done_black_24dp.svg deleted file mode 100644 index b7e19d35..00000000 --- a/client/images/svg/done_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/format_list_bulleted_black_24dp.svg b/client/images/svg/format_list_bulleted_black_24dp.svg deleted file mode 100644 index 21821a14..00000000 --- a/client/images/svg/format_list_bulleted_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/gpp_good_black_24dp.svg b/client/images/svg/gpp_good_black_24dp.svg deleted file mode 100644 index 45d4a819..00000000 --- a/client/images/svg/gpp_good_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/gpp_maybe_black_24dp.svg b/client/images/svg/gpp_maybe_black_24dp.svg deleted file mode 100644 index dceeac79..00000000 --- a/client/images/svg/gpp_maybe_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/logout_black_24dp.svg b/client/images/svg/logout_black_24dp.svg deleted file mode 100644 index 1b785f84..00000000 --- a/client/images/svg/logout_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/miscellaneous_services_black_24dp.svg b/client/images/svg/miscellaneous_services_black_24dp.svg deleted file mode 100644 index 425990e5..00000000 --- a/client/images/svg/miscellaneous_services_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/refresh_black_24dp.svg b/client/images/svg/refresh_black_24dp.svg deleted file mode 100644 index f31411f5..00000000 --- a/client/images/svg/refresh_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/settings_black_24dp.svg b/client/images/svg/settings_black_24dp.svg deleted file mode 100644 index 4165162b..00000000 --- a/client/images/svg/settings_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/settings_suggest_black_24dp.svg b/client/images/svg/settings_suggest_black_24dp.svg deleted file mode 100644 index 80053d0e..00000000 --- a/client/images/svg/settings_suggest_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/share_black_24dp.svg b/client/images/svg/share_black_24dp.svg deleted file mode 100644 index 4c5fa323..00000000 --- a/client/images/svg/share_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/svg/vpn_key_black_24dp.svg b/client/images/svg/vpn_key_black_24dp.svg deleted file mode 100644 index 2c18df46..00000000 --- a/client/images/svg/vpn_key_black_24dp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/images/uncheck.png b/client/images/uncheck.png deleted file mode 100644 index b6723a44..00000000 Binary files a/client/images/uncheck.png and /dev/null differ diff --git a/client/images/upload.png b/client/images/upload.png deleted file mode 100644 index 185e7a7b..00000000 Binary files a/client/images/upload.png and /dev/null differ diff --git a/client/ios/networkextension/CMakeLists.txt b/client/ios/networkextension/CMakeLists.txt index 80f3f1f1..dde03b3b 100644 --- a/client/ios/networkextension/CMakeLists.txt +++ b/client/ios/networkextension/CMakeLists.txt @@ -27,12 +27,7 @@ set_target_properties(networkextension PROPERTIES XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks" - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution" - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development" - - XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual - XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "match AppStore org.amnezia.AmneziaVPN.network-extension" - XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "match Development org.amnezia.AmneziaVPN.network-extension" + XCODE_ATTRIBUTE_CODE_SIGN_STYLE Automatic ) set_target_properties(networkextension PROPERTIES @@ -50,10 +45,12 @@ set_target_properties("networkextension" PROPERTIES find_library(FW_ASSETS_LIBRARY AssetsLibrary) find_library(FW_MOBILE_CORE MobileCoreServices) find_library(FW_UI_KIT UIKit) +find_library(FW_LIBRESOLV libresolv.9.tbd) target_link_libraries(networkextension PRIVATE ${FW_ASSETS_LIBRARY}) target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE}) target_link_libraries(networkextension PRIVATE ${FW_UI_KIT}) +target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV}) target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\") target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1) @@ -80,13 +77,16 @@ target_sources(networkextension PRIVATE ${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift + ${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift ${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift ${CLIENT_ROOT_DIR}/platforms/ios/Log.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift + ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift ${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift + ${CLIENT_ROOT_DIR}/platforms/ios/XrayConfig.swift ${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm ) @@ -114,3 +114,5 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR}) target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/ios/arm64/libwg-go.a) + +target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework) diff --git a/client/ios/scripts/openvpn.sh b/client/ios/scripts/openvpn.sh deleted file mode 100755 index 544b8078..00000000 --- a/client/ios/scripts/openvpn.sh +++ /dev/null @@ -1,19 +0,0 @@ -XCODEBUILD="/usr/bin/xcodebuild" -WORKINGDIR=`pwd` -PATCH="/usr/bin/patch" - - cat $WORKINGDIR/3rd/OpenVPNAdapter/Configuration/Project.xcconfig > $WORKINGDIR/3rd/OpenVPNAdapter/Configuration/amnezia.xcconfig - cat << EOF >> $WORKINGDIR/3rd/OpenVPNAdapter/Configuration/amnezia.xcconfig - PROJECT_TEMP_DIR = $WORKINGDIR/3rd/OpenVPNAdapter/build/OpenVPNAdapter.build - CONFIGURATION_BUILD_DIR = $WORKINGDIR/3rd/OpenVPNAdapter/build/Release-iphoneos - BUILT_PRODUCTS_DIR = $WORKINGDIR/3rd/OpenVPNAdapter/build/Release-iphoneos -EOF - - - cd 3rd/OpenVPNAdapter - if $XCODEBUILD -scheme OpenVPNAdapter -configuration Release -xcconfig Configuration/amnezia.xcconfig -sdk iphoneos -destination 'generic/platform=iOS' -project OpenVPNAdapter.xcodeproj ; then - echo "OpenVPNAdapter built successfully" - else - echo "OpenVPNAdapter build failed" - fi - cd ../../ diff --git a/client/logger.cpp b/client/logger.cpp deleted file mode 100644 index a2c5607b..00000000 --- a/client/logger.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include "logger.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "version.h" -#include "utilities.h" - -#ifdef AMNEZIA_DESKTOP -#include -#endif - -#ifdef Q_OS_IOS - #include -#endif - -QFile Logger::m_file; -QTextStream Logger::m_textStream; -QString Logger::m_logFileName = QString("%1.log").arg(APPLICATION_NAME); - -void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) -{ - if (msg.simplified().isEmpty()) { - return; - } - - // Skip annoying messages from Qt - if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") || msg.startsWith("stale focus object")) { - return; - } - - Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; - Logger::appendAllLog(qFormatLogMessage(type, context, msg)); - - std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; -} - -Logger &Logger::Instance() -{ - static Logger s; - return s; -} - -void Logger::appendSshLog(const QString &log) -{ - QString dt = QDateTime::currentDateTime().toString(); - Instance().m_sshLog.append(dt + ": " + log + "\n"); - emit Instance().sshLogChanged(Instance().sshLog()); -} - -void Logger::appendAllLog(const QString &log) -{ - Instance().m_allLog.append(log + "\n"); - emit Instance().allLogChanged(Instance().allLog()); -} - -bool Logger::init() -{ - qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); - - QString path = userLogsDir(); - QDir appDir(path); - if (!appDir.mkpath(path)) { - return false; - } - - m_file.setFileName(appDir.filePath(m_logFileName)); - if (!m_file.open(QIODevice::Append)) { - qWarning() << "Cannot open log file:" << m_logFileName; - return false; - } - m_file.setTextModeEnabled(true); - m_textStream.setDevice(&m_file); - -#if !defined(QT_DEBUG) || defined(Q_OS_IOS) - qInstallMessageHandler(debugMessageHandler); -#endif - - return true; -} - -void Logger::deInit() -{ - qInstallMessageHandler(nullptr); - qSetMessagePattern("%{message}"); - m_textStream.setDevice(nullptr); - m_file.close(); -} - -QString Logger::userLogsDir() -{ - return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/log"; -} - -QString Logger::userLogsFilePath() -{ - return userLogsDir() + QDir::separator() + m_logFileName; -} - -QString Logger::getLogFile() -{ - m_file.flush(); - QFile file(userLogsFilePath()); - - file.open(QIODevice::ReadOnly); - QString qtLog = file.readAll(); - -#ifdef Q_OS_IOS - return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); -#else - return qtLog; -#endif - -} - -bool Logger::openLogsFolder() -{ - QString path = userLogsDir(); -#ifdef Q_OS_WIN - path = "file:///" + path; -#endif - if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path))) { - qWarning() << "Can't open url:" << path; - return false; - } - return true; -} - -bool Logger::openServiceLogsFolder() -{ - QString path = Utils::systemLogPath(); - path = "file:///" + path; - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); - return true; -} - -QString Logger::appLogFileNamePath() -{ - return m_file.fileName(); -} - -void Logger::clearLogs() -{ - bool isLogActive = m_file.isOpen(); - m_file.close(); - - QFile file(userLogsFilePath()); - - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.resize(0); - file.close(); - -#ifdef Q_OS_IOS - AmneziaVPN::swiftDeleteLog(); -#endif - - if (isLogActive) { - init(); - } -} - -void Logger::clearServiceLogs() -{ -#ifdef AMNEZIA_DESKTOP - IpcClient *m_IpcClient = new IpcClient; - - if (!m_IpcClient->isSocketConnected()) { - if (!IpcClient::init(m_IpcClient)) { - qWarning() << "Error occurred when init IPC client"; - return; - } - } - - if (m_IpcClient->Interface()) { - m_IpcClient->Interface()->setLogsEnabled(false); - m_IpcClient->Interface()->cleanUp(); - } - else { - qWarning() << "Error occurred cleaning up service logs"; - } -#endif -} - -void Logger::cleanUp() -{ - clearLogs(); - QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - dir.removeRecursively(); - - clearServiceLogs(); -} - -Logger::Log::Log(Logger* logger, LogLevel logLevel) - : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) {} - -Logger::Log::~Log() { - qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); - delete m_data; -} - -Logger::Log Logger::error() { return Log(this, LogLevel::Error); } -Logger::Log Logger::warning() { return Log(this, LogLevel::Warning); } -Logger::Log Logger::info() { return Log(this, LogLevel::Info); } -Logger::Log Logger::debug() { return Log(this, LogLevel::Debug); } -QString Logger::sensitive(const QString& input) { -#ifdef Q_DEBUG - return input; -#else - Q_UNUSED(input); - return QString(8, 'X'); -#endif -} - - -#define CREATE_LOG_OP_REF(x) \ -Logger::Log& Logger::Log::operator<<(x t) { \ - m_data->m_ts << t << ' '; \ - return *this; \ -} - -CREATE_LOG_OP_REF(uint64_t); -CREATE_LOG_OP_REF(const char*); -CREATE_LOG_OP_REF(const QString&); -CREATE_LOG_OP_REF(const QByteArray&); -CREATE_LOG_OP_REF(const void*); - -#undef CREATE_LOG_OP_REF - -Logger::Log& Logger::Log::operator<<(const QStringList& t) { - m_data->m_ts << '[' << t.join(",") << ']' << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(const QJsonObject& t) { - m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { - m_data->m_ts << t; - return *this; -} - -void Logger::Log::addMetaEnum(quint64 value, const QMetaObject* meta, - const char* name) { - QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); - - QString out; - QTextStream ts(&out); - - if (const char* scope = me.scope()) { - ts << scope << "::"; - } - - const char* key = me.valueToKey(static_cast(value)); - const bool scoped = me.isScoped(); - if (scoped || !key) { - ts << me.enumName() << (!key ? "(" : "::"); - } - - if (key) { - ts << key; - } else { - ts << value << ")"; - } - - m_data->m_ts << out; -} diff --git a/client/logger.h b/client/logger.h deleted file mode 100644 index f8bfc225..00000000 --- a/client/logger.h +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include -#include - -#include "ui/property_helper.h" - -#include "mozilla/shared/loglevel.h" - -class Logger : public QObject -{ - Q_OBJECT - AUTO_PROPERTY(QString, sshLog) - AUTO_PROPERTY(QString, allLog) - -public: - static Logger& Instance(); - - static void appendSshLog(const QString &log); - static void appendAllLog(const QString &log); - - - static bool init(); - static void deInit(); - static bool openLogsFolder(); - static bool openServiceLogsFolder(); - static QString appLogFileNamePath(); - static void clearLogs(); - static void clearServiceLogs(); - static void cleanUp(); - - static QString userLogsFilePath(); - static QString getLogFile(); - - // compat with Mozilla logger - Logger(const QString &className) { m_className = className; } - const QString& className() const { return m_className; } - - class Log { - public: - Log(Logger* logger, LogLevel level); - ~Log(); - - Log& operator<<(uint64_t t); - Log& operator<<(const char* t); - Log& operator<<(const QString& t); - Log& operator<<(const QStringList& t); - Log& operator<<(const QByteArray& t); - Log& operator<<(const QJsonObject& t); - Log& operator<<(QTextStreamFunction t); - Log& operator<<(const void* t); - - // Q_ENUM - template - typename std::enable_if::Value, Log&>::type - operator<<(T t) { - const QMetaObject* meta = qt_getEnumMetaObject(t); - const char* name = qt_getEnumName(t); - addMetaEnum(typename QFlags::Int(t), meta, name); - return *this; - } - - private: - void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name); - - Logger* m_logger; - LogLevel m_logLevel; - - struct Data { - Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {} - - QString m_buffer; - QTextStream m_ts; - }; - - Data* m_data; - }; - - Log error(); - Log warning(); - Log info(); - Log debug(); - QString sensitive(const QString& input); - -private: - Logger() {} - Logger(Logger const &) = delete; - Logger& operator= (Logger const&) = delete; - - static QString userLogsDir(); - - static QFile m_file; - static QTextStream m_textStream; - static QString m_logFileName; - - friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - // compat with Mozilla logger - QString m_className; -}; - -#endif // LOGGER_H diff --git a/client/main.cpp b/client/main.cpp index 3a719096..aca9e62b 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -15,13 +15,24 @@ #include "platforms/ios/QtAppDelegate-C-Interface.h" #endif +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +bool isAnotherInstanceRunning() +{ + QLocalSocket socket; + socket.connectToServer("AmneziaVPNInstance"); + if (socket.waitForConnected(500)) { + qWarning() << "AmneziaVPN is already running"; + return true; + } + return false; +} +#endif + int main(int argc, char *argv[]) { Migrations migrationsManager; migrationsManager.doMigrations(); - QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); - #ifdef Q_OS_WIN AllowSetForegroundWindow(ASFW_ANY); #endif @@ -32,16 +43,14 @@ int main(int argc, char *argv[]) qputenv("ANDROID_OPENSSL_SUFFIX", "_3"); #endif -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) AmneziaApplication app(argc, argv); -#else - AmneziaApplication app(argc, argv, true, - SingleApplication::Mode::User | SingleApplication::Mode::SecondaryNotification); - if (!app.isPrimary()) { +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (isAnotherInstanceRunning()) { QTimer::singleShot(1000, &app, [&]() { app.quit(); }); return app.exec(); } + app.startLocalServer(); #endif // Allow to raise app window if secondary instance launched diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 0502facc..1081bcae 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -1,9 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "protocols/protocols_defs.h" #include "localsocketcontroller.h" +#include + #include #include #include @@ -17,6 +18,9 @@ #include "leakdetector.h" #include "logger.h" #include "models/server.h" +#include "daemon/daemonerrors.h" + +#include "protocols/protocols_defs.h" // How many times do we try to reconnect. constexpr int MAX_CONNECTION_RETRY = 10; @@ -34,8 +38,8 @@ LocalSocketController::LocalSocketController() { m_socket = new QLocalSocket(this); connect(m_socket, &QLocalSocket::connected, this, &LocalSocketController::daemonConnected); - connect(m_socket, &QLocalSocket::disconnected, this, - &LocalSocketController::disconnected); + connect(m_socket, &QLocalSocket::disconnected, this, + [&] { errorOccurred(QLocalSocket::PeerClosedError); }); connect(m_socket, &QLocalSocket::errorOccurred, this, &LocalSocketController::errorOccurred); connect(m_socket, &QLocalSocket::readyRead, this, @@ -149,7 +153,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { QJsonArray jsAllowedIPAddesses; QJsonArray plainAllowedIP = wgConfig.value(amnezia::config_key::allowed_ips).toArray(); - QJsonArray defaultAllowedIP = QJsonArray::fromStringList(QString("0.0.0.0/0, ::/0").split(",")); + QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" }; if (plainAllowedIP != defaultAllowedIP && !plainAllowedIP.isEmpty()) { // Use AllowedIP list from WG config because of higher priority @@ -451,8 +455,39 @@ void LocalSocketController::parseCommand(const QByteArray& command) { } if (type == "backendFailure") { - qCritical() << "backendFailure"; - return; + if (!obj.contains("errorCode")) { + // report a generic error if we dont know what it is. + logger.error() << "generic backend failure error"; + // REPORTERROR(ErrorHandler::ControllerError, "controller"); + return; + } + auto errorCode = static_cast(obj["errorCode"].toInt()); + if (errorCode >= (uint8_t)DaemonError::DAEMON_ERROR_MAX) { + // Also report a generic error if the code is invalid. + logger.error() << "invalid backend failure error code"; + // REPORTERROR(ErrorHandler::ControllerError, "controller"); + return; + } + switch (static_cast(errorCode)) { + case DaemonError::ERROR_NONE: + [[fallthrough]]; + case DaemonError::ERROR_FATAL: + logger.error() << "generic backend failure error (fatal or error none)"; + // REPORTERROR(ErrorHandler::ControllerError, "controller"); + break; + case DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE: + [[fallthrough]]; + case DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE: + [[fallthrough]]; + case DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE: + logger.error() << "split tunnel backend failure error"; + //REPORTERROR(ErrorHandler::SplitTunnelError, "controller"); + break; + case DaemonError::DAEMON_ERROR_MAX: + // We should not get here. + Q_ASSERT(false); + break; + } } if (type == "logs") { diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index be404a15..d9195f87 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -98,6 +98,7 @@ bool AndroidController::initialize() {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)}, + {"onAuthResult", "(Z)V", reinterpret_cast(onAuthResult)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)} }; @@ -162,9 +163,7 @@ QString AndroidController::openFile(const QString &filter) QString fileName; connect(this, &AndroidController::fileOpened, this, [&fileName, &wait](const QString &uri) { - qDebug() << "Android event: file opened; uri:" << uri; - fileName = QQmlFile::urlToLocalFileOrQrc(uri); - qDebug() << "Android opened filename:" << fileName; + fileName = uri; wait.quit(); }, static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); @@ -174,11 +173,35 @@ QString AndroidController::openFile(const QString &filter) return fileName; } +int AndroidController::getFd(const QString &fileName) +{ + return callActivityMethod("getFd", "(Ljava/lang/String;)I", + QJniObject::fromString(fileName).object()); +} + +void AndroidController::closeFd() +{ + callActivityMethod("closeFd", "()V"); +} + +QString AndroidController::getFileName(const QString &uri) +{ + auto fileName = callActivityMethod("getFileName", "(Ljava/lang/String;)Ljava/lang/String;", + QJniObject::fromString(uri).object()); + QJniEnvironment env; + return AndroidUtils::convertJString(env.jniEnv(), fileName.object()); +} + bool AndroidController::isCameraPresent() { return callActivityMethod("isCameraPresent", "()Z"); } +bool AndroidController::isOnTv() +{ + return callActivityMethod("isOnTv", "()Z"); +} + void AndroidController::startQrReaderActivity() { callActivityMethod("startQrCodeReader", "()V"); @@ -205,6 +228,11 @@ void AndroidController::setScreenshotsEnabled(bool enabled) callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled); } +void AndroidController::setNavigationBarColor(unsigned int color) +{ + callActivityMethod("setNavigationBarColor", "(I)V", color); +} + void AndroidController::minimizeApp() { callActivityMethod("minimizeApp", "()V"); @@ -260,6 +288,27 @@ void AndroidController::requestNotificationPermission() callActivityMethod("requestNotificationPermission", "()V"); } +bool AndroidController::requestAuthentication() +{ + QEventLoop wait; + bool result; + connect(this, &AndroidController::authenticationResult, this, + [&result, &wait](const bool &authResult){ + qDebug() << "Android authentication result:" << authResult; + result = authResult; + wait.quit(); + }, + static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); + callActivityMethod("requestAuthentication", "()V"); + wait.exec(); + return result; +} + +void AndroidController::sendTouch(float x, float y) +{ + callActivityMethod("sendTouch", "(FF)V", x, y); +} + // Moving log processing to the Android side jclass AndroidController::log; jmethodID AndroidController::logDebug; @@ -457,6 +506,14 @@ void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data)); } +// static +void AndroidController::onAuthResult(JNIEnv *env, jobject thiz, jboolean result) +{ + Q_UNUSED(thiz); + + emit AndroidController::instance()->authenticationResult(result); +} + // static bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data) { diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index d015dbe3..5707771e 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -34,17 +34,24 @@ public: void resetLastServer(int serverIndex); void saveFile(const QString &fileName, const QString &data); QString openFile(const QString &filter); + int getFd(const QString &fileName); + void closeFd(); + QString getFileName(const QString &uri); bool isCameraPresent(); + bool isOnTv(); void startQrReaderActivity(); void setSaveLogs(bool enabled); void exportLogsFile(const QString &fileName); void clearLogs(); void setScreenshotsEnabled(bool enabled); + void setNavigationBarColor(unsigned int color); void minimizeApp(); QJsonArray getAppList(); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); bool isNotificationPermissionGranted(); void requestNotificationPermission(); + bool requestAuthentication(); + void sendTouch(float x, float y); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); @@ -62,6 +69,7 @@ signals: void configImported(QString config); void importConfigFromOutside(QString config); void initConnectionState(Vpn::ConnectionState state); + void authenticationResult(bool result); private: bool isWaitingStatus = true; @@ -88,6 +96,7 @@ private: static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); + static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); template diff --git a/client/platforms/android/authResultReceiver.cpp b/client/platforms/android/authResultReceiver.cpp deleted file mode 100644 index 21e838a2..00000000 --- a/client/platforms/android/authResultReceiver.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "authResultReceiver.h" - -AuthResultReceiver::AuthResultReceiver(QSharedPointer ¬ifier) : m_notifier(notifier) -{ -} - -void AuthResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) -{ - qDebug() << "receiverRequestCode" << receiverRequestCode << "resultCode" << resultCode; - - if (resultCode == -1) { // ResultOK - emit m_notifier->authSuccessful(); - } else { - emit m_notifier->authFailed(); - } -} diff --git a/client/platforms/android/authResultReceiver.h b/client/platforms/android/authResultReceiver.h deleted file mode 100644 index 9a88dcf5..00000000 --- a/client/platforms/android/authResultReceiver.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef AUTHRESULTRECEIVER_H -#define AUTHRESULTRECEIVER_H - -#include - -#include - -class AuthResultNotifier : public QObject -{ - Q_OBJECT - -public: - AuthResultNotifier(QObject *parent = nullptr) : QObject(parent) {}; - -signals: - void authFailed(); - void authSuccessful(); -}; - -/* Auth result handler for Android */ -class AuthResultReceiver final : public QAndroidActivityResultReceiver -{ -public: - AuthResultReceiver(QSharedPointer ¬ifier); - - void handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) override; - -private: - QSharedPointer m_notifier; -}; - -#endif // AUTHRESULTRECEIVER_H diff --git a/client/platforms/ios/HevSocksTunnel.swift b/client/platforms/ios/HevSocksTunnel.swift new file mode 100644 index 00000000..87d995e8 --- /dev/null +++ b/client/platforms/ios/HevSocksTunnel.swift @@ -0,0 +1,74 @@ +import HevSocks5Tunnel +import NetworkExtension + +public enum Socks5Tunnel { + + private static var tunnelFileDescriptor: Int32? { + var ctlInfo = ctl_info() + withUnsafeMutablePointer(to: &ctlInfo.ctl_name) { + $0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) { + _ = strcpy($0, "com.apple.net.utun_control") + } + } + for fd: Int32 in 0...1024 { + var addr = sockaddr_ctl() + var ret: Int32 = -1 + var len = socklen_t(MemoryLayout.size(ofValue: addr)) + withUnsafeMutablePointer(to: &addr) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + ret = getpeername(fd, $0, &len) + } + } + if ret != 0 || addr.sc_family != AF_SYSTEM { + continue + } + if ctlInfo.ctl_id == 0 { + ret = ioctl(fd, CTLIOCGINFO, &ctlInfo) + if ret != 0 { + continue + } + } + if addr.sc_id == ctlInfo.ctl_id { + return fd + } + } + return nil + } + + private static var interfaceName: String? { + guard let tunnelFileDescriptor = self.tunnelFileDescriptor else { + return nil + } + var buffer = [UInt8](repeating: 0, count: Int(IFNAMSIZ)) + return buffer.withUnsafeMutableBufferPointer { mutableBufferPointer in + guard let baseAddress = mutableBufferPointer.baseAddress else { + return nil + } + var ifnameSize = socklen_t(IFNAMSIZ) + let result = getsockopt( + tunnelFileDescriptor, + 2 /* SYSPROTO_CONTROL */, + 2 /* UTUN_OPT_IFNAME */, + baseAddress, + &ifnameSize + ) + if result == 0 { + return String(cString: baseAddress) + } else { + return nil + } + } + } + + @discardableResult + public static func run(withConfig filePath: String) -> Int32 { + guard let fileDescriptor = self.tunnelFileDescriptor else { + fatalError("Get tunnel file descriptor failed.") + } + return hev_socks5_tunnel_main(filePath.cString(using: .utf8), fileDescriptor) + } + + public static func quit() { + hev_socks5_tunnel_quit() + } +} diff --git a/client/platforms/ios/NELogController.swift b/client/platforms/ios/NELogController.swift index e1d71e60..257dc087 100644 --- a/client/platforms/ios/NELogController.swift +++ b/client/platforms/ios/NELogController.swift @@ -13,6 +13,10 @@ public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { neLog(type, title: "OVPN: \(title)", message: message) } +public func xrayLog(_ type: OSLogType, title: String = "", message: String) { + neLog(type, title: "XRAY: \(title)", message: message) +} + public func neLog(_ type: OSLogType, title: String = "", message: String) { Log.log(type, title: "NE: \(title)", message: message) } diff --git a/client/platforms/ios/PacketTunnelProvider+Xray.swift b/client/platforms/ios/PacketTunnelProvider+Xray.swift new file mode 100644 index 00000000..648b3613 --- /dev/null +++ b/client/platforms/ios/PacketTunnelProvider+Xray.swift @@ -0,0 +1,187 @@ +import Foundation +import NetworkExtension +import WireGuardKitGo + +enum XrayErrors: Error { + case noXrayConfig + case xrayConfigIsWrong + case cantSaveXrayConfig + case cantParseListenAndPort + case cantSaveHevSocksConfig +} + +extension Constants { + static let cachesDirectory: URL = { + if let cachesDirectoryURL = FileManager.default.urls(for: .cachesDirectory, + in: .userDomainMask).first { + return cachesDirectoryURL + } else { + fatalError("Unable to retrieve caches directory.") + } + }() +} + +extension PacketTunnelProvider { + func startXray(completionHandler: @escaping (Error?) -> Void) { + + // Xray configuration + guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfiguration = protocolConfiguration.providerConfiguration, + let configData = providerConfiguration[Constants.xrayConfigKey] as? Data else { + xrayLog(.error, message: "Can't get xray configuration") + completionHandler(XrayErrors.noXrayConfig) + return + } + + // Tunnel settings + let ipv6Enabled = false + let hideVPNIcon = false + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "254.1.1.1") + settings.mtu = 9000 + + settings.ipv4Settings = { + let settings = NEIPv4Settings(addresses: ["198.18.0.1"], subnetMasks: ["255.255.0.0"]) + settings.includedRoutes = [NEIPv4Route.default()] + return settings + }() + + settings.ipv6Settings = { + guard ipv6Enabled else { + return nil + } + let settings = NEIPv6Settings(addresses: ["fd6e:a81b:704f:1211::1"], networkPrefixLengths: [64]) + settings.includedRoutes = [NEIPv6Route.default()] + if hideVPNIcon { + settings.excludedRoutes = [NEIPv6Route(destinationAddress: "::", networkPrefixLength: 128)] + } + return settings + }() + + do { + let xrayConfig = try JSONDecoder().decode(XrayConfig.self, + from: configData) + + var dnsArray = [String]() + if let dns1 = xrayConfig.dns1 { + dnsArray.append(dns1) + } + if let dns2 = xrayConfig.dns2 { + dnsArray.append(dns2) + } + + settings.dnsSettings = !dnsArray.isEmpty + ? NEDNSSettings(servers: dnsArray) + : NEDNSSettings(servers: ["1.1.1.1"]) + + let xrayConfigData = xrayConfig.config.data(using: .utf8) + + guard let xrayConfigData else { + xrayLog(.error, message: "Can't encode config to data") + completionHandler(XrayErrors.xrayConfigIsWrong) + return + } + + let jsonDict = try JSONSerialization.jsonObject(with: xrayConfigData, + options: []) as? [String: Any] + + guard var jsonDict else { + xrayLog(.error, message: "Can't parse address and port for hevSocks") + completionHandler(XrayErrors.cantParseListenAndPort) + return + } + + let port = 10808 + let address = "::1" + + if var inboundsArray = jsonDict["inbounds"] as? [[String: Any]], !inboundsArray.isEmpty { + inboundsArray[0]["port"] = port + inboundsArray[0]["listen"] = address + jsonDict["inbounds"] = inboundsArray + } + + let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: []) + + setTunnelNetworkSettings(settings) { [weak self] error in + if let error { + completionHandler(error) + return + } + + // Launch xray + self?.setupAndStartXray(configData: updatedData) { xrayError in + if let xrayError { + completionHandler(xrayError) + return + } + + // Launch hevSocks + self?.setupAndRunTun2socks(configData: updatedData, + address: address, + port: port, + completionHandler: completionHandler) + } + } + } catch { + completionHandler(error) + return + } + } + + func stopXray(completionHandler: () -> Void) { + Socks5Tunnel.quit() + LibXrayStopXray() + completionHandler() + } + + private func setupAndStartXray(configData: Data, + completionHandler: @escaping (Error?) -> Void) { + let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path + guard FileManager.default.createFile(atPath: path, contents: configData) else { + xrayLog(.error, message: "Can't save xray configuration") + completionHandler(XrayErrors.cantSaveXrayConfig) + return + } + + LibXrayRunXray(nil, + path, + Int64.max) + + completionHandler(nil) + xrayLog(.info, message: "Xray started") + } + + private func setupAndRunTun2socks(configData: Data, + address: String, + port: Int, + completionHandler: @escaping (Error?) -> Void) { + let config = """ + tunnel: + mtu: 9000 + socks5: + port: \(port) + address: \(address) + udp: 'udp' + misc: + task-stack-size: 20480 + connect-timeout: 5000 + read-write-timeout: 60000 + log-file: stderr + log-level: error + limit-nofile: 65535 + """ + + let configurationFilePath = Constants.cachesDirectory.appendingPathComponent("config.yml", isDirectory: false).path + guard FileManager.default.createFile(atPath: configurationFilePath, contents: config.data(using: .utf8)!) else { + xrayLog(.info, message: "Cant save hevSocks configuration") + completionHandler(XrayErrors.cantSaveHevSocksConfig) + return + } + + DispatchQueue.global().async { + xrayLog(.info, message: "Hev socks started") + completionHandler(nil) + Socks5Tunnel.run(withConfig: configurationFilePath) + } + } +} diff --git a/client/platforms/ios/PacketTunnelProvider.swift b/client/platforms/ios/PacketTunnelProvider.swift index f78db345..9a5a5846 100644 --- a/client/platforms/ios/PacketTunnelProvider.swift +++ b/client/platforms/ios/PacketTunnelProvider.swift @@ -5,7 +5,8 @@ import Darwin import OpenVPNAdapter enum TunnelProtoType: String { - case wireguard, openvpn + case wireguard, openvpn, xray + } struct Constants { @@ -13,6 +14,7 @@ struct Constants { static let processQueueName = "org.amnezia.process-packets" static let kActivationAttemptId = "activationAttemptId" static let ovpnConfigKey = "ovpn" + static let xrayConfigKey = "xray" static let wireGuardConfigKey = "wireguard" static let loggerTag = "NET" @@ -91,6 +93,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { protoType = .openvpn } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { protoType = .wireguard + } else if (providerConfiguration?[Constants.xrayConfigKey] as? Data) != nil { + protoType = .xray } } @@ -107,6 +111,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider { completionHandler: completionHandler) case .openvpn: startOpenVPN(completionHandler: completionHandler) + case .xray: + startXray(completionHandler: completionHandler) + } } @@ -124,6 +131,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { case .openvpn: stopOpenVPN(with: reason, completionHandler: completionHandler) + case .xray: + stopXray(completionHandler: completionHandler) } } @@ -138,6 +147,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { handleWireguardStatusMessage(messageData, completionHandler: completionHandler) case .openvpn: handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler) + case .xray: + break; } } diff --git a/client/platforms/ios/ScreenProtection.swift b/client/platforms/ios/ScreenProtection.swift index 1355dc13..200cf0cb 100644 --- a/client/platforms/ios/ScreenProtection.swift +++ b/client/platforms/ios/ScreenProtection.swift @@ -14,10 +14,15 @@ extension UIApplication { var keyWindows: [UIWindow] { connectedScenes .compactMap { + guard let windowScene = $0 as? UIWindowScene else { return nil } if #available(iOS 15.0, *) { - ($0 as? UIWindowScene)?.keyWindow + guard let keywindow = windowScene.keyWindow else { + windowScene.windows.first?.makeKey() + return windowScene.windows.first + } + return keywindow } else { - ($0 as? UIWindowScene)?.windows.first { $0.isKeyWindow } + return windowScene.windows.first { $0.isKeyWindow } } } } diff --git a/client/platforms/ios/WGConfig.swift b/client/platforms/ios/WGConfig.swift index 3dbdf7d0..e3b67efe 100644 --- a/client/platforms/ios/WGConfig.swift +++ b/client/platforms/ios/WGConfig.swift @@ -13,7 +13,7 @@ struct WGConfig: Decodable { let clientIP: String let clientPrivateKey: String let serverPublicKey: String - let presharedKey: String + let presharedKey: String? var allowedIPs: [String] var persistentKeepAlive: String let splitTunnelType: Int @@ -65,7 +65,7 @@ struct WGConfig: Decodable { \(settings) [Peer] PublicKey = \(serverPublicKey) - PresharedKey = \(presharedKey) + \(presharedKey == nil ? "" : "PresharedKey = \(presharedKey!)") AllowedIPs = \(allowedIPs.joined(separator: ", ")) Endpoint = \(hostName):\(port) PersistentKeepalive = \(persistentKeepAlive) diff --git a/client/platforms/ios/XrayConfig.swift b/client/platforms/ios/XrayConfig.swift new file mode 100644 index 00000000..9c47a2a1 --- /dev/null +++ b/client/platforms/ios/XrayConfig.swift @@ -0,0 +1,7 @@ +import Foundation + +struct XrayConfig: Decodable { + let dns1: String? + let dns2: String? + let config: String +} diff --git a/client/platforms/ios/ios_controller.h b/client/platforms/ios/ios_controller.h index a36bbef5..85580769 100644 --- a/client/platforms/ios/ios_controller.h +++ b/client/platforms/ios/ios_controller.h @@ -72,9 +72,12 @@ private: bool setupCloak(); bool setupWireGuard(); bool setupAwg(); + bool setupXray(); + bool setupSSXray(); bool startOpenVPN(const QString &config); bool startWireGuard(const QString &jsonConfig); + bool startXray(const QString &jsonConfig); void startTunnel(); diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 44924452..85fb50b7 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -216,6 +216,12 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur if (proto == amnezia::Proto::Awg) { return setupAwg(); } + if (proto == amnezia::Proto::Xray) { + return setupXray(); + } + if (proto == amnezia::Proto::SSXray) { + return setupSSXray(); + } return false; } @@ -345,8 +351,6 @@ void IosController::vpnStatusDidChange(void *pNotification) } } } - } else { - qDebug() << "Disconnect error is absent"; } }]; } else { @@ -495,12 +499,62 @@ bool IosController::setupWireGuard() wgConfig.insert(config_key::persistent_keep_alive, "25"); } + if (config.contains(config_key::isObfuscationEnabled) && config.value(config_key::isObfuscationEnabled).toBool()) { + wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]); + wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]); + wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]); + wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]); + + wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]); + wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]); + + wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]); + wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]); + wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]); + } + QJsonDocument wgConfigDoc(wgConfig); QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact)); return startWireGuard(wgConfigDocStr); } +bool IosController::setupXray() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Xray)].toObject(); + QJsonDocument xrayConfigDoc(config); + + QString xrayConfigStr(xrayConfigDoc.toJson(QJsonDocument::Compact)); + + QJsonObject finalConfig; + finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString()); + finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString()); + finalConfig.insert(config_key::config, xrayConfigStr); + + QJsonDocument finalConfigDoc(finalConfig); + QString finalConfigStr(finalConfigDoc.toJson(QJsonDocument::Compact)); + + return startXray(finalConfigStr); +} + +bool IosController::setupSSXray() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::SSXray)].toObject(); + QJsonDocument ssXrayConfigDoc(config); + + QString ssXrayConfigStr(ssXrayConfigDoc.toJson(QJsonDocument::Compact)); + + QJsonObject finalConfig; + finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]); + finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]); + finalConfig.insert(config_key::config, ssXrayConfigStr); + + QJsonDocument finalConfigDoc(finalConfig); + QString finalConfigStr(finalConfigDoc.toJson(QJsonDocument::Compact)); + + return startXray(finalConfigStr); +} + bool IosController::setupAwg() { QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject(); @@ -590,6 +644,20 @@ bool IosController::startWireGuard(const QString &config) startTunnel(); } +bool IosController::startXray(const QString &config) +{ + qDebug() << "IosController::startXray"; + + NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; + tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; + tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]}; + tunnelProtocol.serverAddress = m_serverAddress; + + m_currentTunnel.protocolConfiguration = tunnelProtocol; + + startTunnel(); +} + void IosController::startTunnel() { NSString *protocolName = @"Unknown"; @@ -779,7 +847,7 @@ QString IosController::openFile() { void IosController::requestInetAccess() { NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"]; - if (url) { + if (!url) { qDebug() << "IosController::requestInetAccess URL error"; return; } @@ -791,7 +859,6 @@ void IosController::requestInetAccess() { } else { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length); - qDebug() << "IosController::requestInetAccess server response:" << httpResponse.statusCode << "\n\n" < WireguardUtilsLinux::getPeerStatus() { return peerList; } - -void WireguardUtilsLinux::applyFirewallRules(FirewallParams& params) -{ - // double-check + ensure our firewall is installed and enabled - if (!LinuxFirewall::isInstalled()) LinuxFirewall::install(); - - // Note: rule precedence is handled inside IpTablesFirewall - LinuxFirewall::ensureRootAnchorPriority(); - - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), params.blockAll); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), params.allowNets); - LinuxFirewall::updateAllowNets(params.allowAddrs); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), params.blockNets); - LinuxFirewall::updateBlockNets(params.blockAddrs); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true); - LinuxFirewall::updateDNSServers(params.dnsServers); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); -} - bool WireguardUtilsLinux::updateRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; @@ -377,6 +352,26 @@ bool WireguardUtilsLinux::deleteExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->deleteExclusionRoute(prefix); } +bool WireguardUtilsLinux::excludeLocalNetworks(const QList& routes) { + if (!m_rtmonitor) { + return false; + } + + // Explicitly discard LAN traffic that makes its way into the tunnel. This + // doesn't really exclude the LAN traffic, we just don't take any action to + // overrule the routes of other interfaces. + bool result = true; + for (const auto& prefix : routes) { + logger.error() << "Attempting to exclude:" << prefix.toString(); + if (!m_rtmonitor->insertRoute(prefix)) { + result = false; + } + } + + // TODO: A kill switch would be nice though :) + return result; +} + QString WireguardUtilsLinux::uapiCommand(const QString& command) { QLocalSocket socket; QTimer uapiTimeout; @@ -450,3 +445,27 @@ QString WireguardUtilsLinux::waitForTunnelName(const QString& filename) { return QString(); } + +void WireguardUtilsLinux::applyFirewallRules(FirewallParams& params) +{ + // double-check + ensure our firewall is installed and enabled + if (!LinuxFirewall::isInstalled()) LinuxFirewall::install(); + + // Note: rule precedence is handled inside IpTablesFirewall + LinuxFirewall::ensureRootAnchorPriority(); + + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), params.blockAll); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), params.allowNets); + LinuxFirewall::updateAllowNets(params.allowAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), params.blockNets); + LinuxFirewall::updateBlockNets(params.blockAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true); + LinuxFirewall::updateDNSServers(params.dnsServers); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); +} diff --git a/client/platforms/linux/daemon/wireguardutilslinux.h b/client/platforms/linux/daemon/wireguardutilslinux.h index 9746ea4b..c324111d 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.h +++ b/client/platforms/linux/daemon/wireguardutilslinux.h @@ -37,6 +37,9 @@ public: bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + + bool excludeLocalNetworks(const QList& lanAddressRanges) override; + void applyFirewallRules(FirewallParams& params); signals: void backendFailure(); diff --git a/client/platforms/macos/daemon/macosdaemon.h b/client/platforms/macos/daemon/macosdaemon.h index a48c326c..4181648e 100644 --- a/client/platforms/macos/daemon/macosdaemon.h +++ b/client/platforms/macos/daemon/macosdaemon.h @@ -21,7 +21,6 @@ class MacOSDaemon final : public Daemon { protected: WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } DnsUtils* dnsutils() override { return m_dnsutils; } bool supportIPUtils() const override { return true; } IPUtils* iputils() override { return m_iputils; } diff --git a/client/platforms/macos/daemon/macosroutemonitor.cpp b/client/platforms/macos/daemon/macosroutemonitor.cpp index 395f008a..bd991c01 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.cpp +++ b/client/platforms/macos/daemon/macosroutemonitor.cpp @@ -358,8 +358,8 @@ void MacosRouteMonitor::rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, } bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix, - unsigned int ifindex, - const void* gateway) { + unsigned int ifindex, const void* gateway, + int flags) { constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) + sizeof(struct sockaddr_in6) * 2 + sizeof(struct sockaddr_storage); @@ -370,7 +370,7 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix, rtm->rtm_version = RTM_VERSION; rtm->rtm_type = action; rtm->rtm_index = ifindex; - rtm->rtm_flags = RTF_STATIC | RTF_UP; + rtm->rtm_flags = flags | RTF_STATIC | RTF_UP; rtm->rtm_addrs = 0; rtm->rtm_pid = 0; rtm->rtm_seq = m_rtseq++; @@ -490,7 +490,7 @@ bool MacosRouteMonitor::rtmFetchRoutes(int family) { return false; } -bool MacosRouteMonitor::insertRoute(const IPAddress& prefix) { +bool MacosRouteMonitor::insertRoute(const IPAddress& prefix, int flags) { struct sockaddr_dl datalink; memset(&datalink, 0, sizeof(datalink)); datalink.sdl_family = AF_LINK; @@ -502,11 +502,11 @@ bool MacosRouteMonitor::insertRoute(const IPAddress& prefix) { datalink.sdl_slen = 0; memcpy(&datalink.sdl_data, qPrintable(m_ifname), datalink.sdl_nlen); - return rtmSendRoute(RTM_ADD, prefix, m_ifindex, &datalink); + return rtmSendRoute(RTM_ADD, prefix, m_ifindex, &datalink, flags); } -bool MacosRouteMonitor::deleteRoute(const IPAddress& prefix) { - return rtmSendRoute(RTM_DELETE, prefix, m_ifindex, nullptr); +bool MacosRouteMonitor::deleteRoute(const IPAddress& prefix, int flags) { + return rtmSendRoute(RTM_DELETE, prefix, m_ifindex, nullptr, flags); } bool MacosRouteMonitor::addExclusionRoute(const IPAddress& prefix) { diff --git a/client/platforms/macos/daemon/macosroutemonitor.h b/client/platforms/macos/daemon/macosroutemonitor.h index b2483d76..78396690 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.h +++ b/client/platforms/macos/daemon/macosroutemonitor.h @@ -24,8 +24,8 @@ class MacosRouteMonitor final : public QObject { MacosRouteMonitor(const QString& ifname, QObject* parent = nullptr); ~MacosRouteMonitor(); - bool insertRoute(const IPAddress& prefix); - bool deleteRoute(const IPAddress& prefix); + bool insertRoute(const IPAddress& prefix, int flags = 0); + bool deleteRoute(const IPAddress& prefix, int flags = 0); int interfaceFlags() { return m_ifflags; } bool addExclusionRoute(const IPAddress& prefix); @@ -37,7 +37,7 @@ class MacosRouteMonitor final : public QObject { void handleRtmUpdate(const struct rt_msghdr* msg, const QByteArray& payload); void handleIfaceInfo(const struct if_msghdr* msg, const QByteArray& payload); bool rtmSendRoute(int action, const IPAddress& prefix, unsigned int ifindex, - const void* gateway); + const void* gateway, int flags = 0); bool rtmFetchRoutes(int family); static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr, const void* sa); diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.cpp b/client/platforms/macos/daemon/wireguardutilsmacos.cpp index 8affd724..eae22837 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.cpp +++ b/client/platforms/macos/daemon/wireguardutilsmacos.cpp @@ -5,6 +5,7 @@ #include "wireguardutilsmacos.h" #include +#include #include #include @@ -130,14 +131,14 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) { } int err = uapiErrno(uapiCommand(message)); - if (err != 0) { logger.error() << "Interface configuration failed:" << strerror(err); } else { if (config.m_killSwitchEnabled) { FirewallParams params { }; params.dnsServers.append(config.m_dnsServer); - if (config.m_allowedIPAddressRanges.at(0).toString() == "0.0.0.0/0"){ + + if (config.m_allowedIPAddressRanges.contains(IPAddress("0.0.0.0/0"))) { params.blockAll = true; if (config.m_excludedAddresses.size()) { params.allowNets = true; @@ -210,7 +211,6 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { logger.warning() << "Failed to create peer with no endpoints"; return false; } - out << config.m_serverPort << "\n"; out << "replace_allowed_ips=true\n"; @@ -322,10 +322,10 @@ bool WireguardUtilsMacos::deleteRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; } - if (prefix.prefixLength() > 0) { - return m_rtmonitor->insertRoute(prefix); - } + if (prefix.prefixLength() > 0) { + return m_rtmonitor->deleteRoute(prefix); + } // Ensure that we do not replace the default route. if (prefix.type() == QAbstractSocket::IPv4Protocol) { return m_rtmonitor->deleteRoute(IPAddress("0.0.0.0/1")) && @@ -345,31 +345,6 @@ bool WireguardUtilsMacos::addExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->addExclusionRoute(prefix); } -void WireguardUtilsMacos::applyFirewallRules(FirewallParams& params) -{ - // double-check + ensure our firewall is installed and enabled. This is necessary as - // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) - if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); - - MacOSFirewall::ensureRootAnchorPriority(); - MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), params.blockAll); - MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), params.allowNets); - MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), params.allowNets, - QStringLiteral("allownets"), params.allowAddrs); - - MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), params.blockNets); - MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), params.blockNets, - QStringLiteral("blocknets"), params.blockAddrs); - - MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true); - MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), params.dnsServers); -} - bool WireguardUtilsMacos::deleteExclusionRoute(const IPAddress& prefix) { if (!m_rtmonitor) { return false; @@ -377,6 +352,26 @@ bool WireguardUtilsMacos::deleteExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->deleteExclusionRoute(prefix); } +bool WireguardUtilsMacos::excludeLocalNetworks(const QList& routes) { + if (!m_rtmonitor) { + return false; + } + + // Explicitly discard LAN traffic that makes its way into the tunnel. This + // doesn't really exclude the LAN traffic, we just don't take any action to + // overrule the routes of other interfaces. + bool result = true; + for (const auto& prefix : routes) { + logger.error() << "Attempting to exclude:" << prefix.toString(); + if (!m_rtmonitor->insertRoute(prefix, RTF_IFSCOPE | RTF_REJECT)) { + result = false; + } + } + + // TODO: A kill switch would be nice though :) + return result; +} + QString WireguardUtilsMacos::uapiCommand(const QString& command) { QLocalSocket socket; QTimer uapiTimeout; @@ -453,3 +448,28 @@ QString WireguardUtilsMacos::waitForTunnelName(const QString& filename) { return QString(); } + +void WireguardUtilsMacos::applyFirewallRules(FirewallParams& params) +{ + // double-check + ensure our firewall is installed and enabled. This is necessary as + // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) + if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + + MacOSFirewall::ensureRootAnchorPriority(); + MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), params.blockAll); + MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), params.allowNets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), params.allowNets, + QStringLiteral("allownets"), params.allowAddrs); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), params.blockNets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), params.blockNets, + QStringLiteral("blocknets"), params.blockAddrs); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true); + MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), params.dnsServers); +} diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.h b/client/platforms/macos/daemon/wireguardutilsmacos.h index 243f4b64..3f0d3391 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.h +++ b/client/platforms/macos/daemon/wireguardutilsmacos.h @@ -35,6 +35,9 @@ class WireguardUtilsMacos final : public WireguardUtils { bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + + bool excludeLocalNetworks(const QList& lanAddressRanges) override; + void applyFirewallRules(FirewallParams& params); signals: diff --git a/client/platforms/windows/daemon/windowsdaemon.cpp b/client/platforms/windows/daemon/windowsdaemon.cpp index 00435f0b..620e0a1c 100644 --- a/client/platforms/windows/daemon/windowsdaemon.cpp +++ b/client/platforms/windows/daemon/windowsdaemon.cpp @@ -5,6 +5,7 @@ #include "windowsdaemon.h" #include +#include #include #include @@ -15,28 +16,34 @@ #include #include +#include "daemon/daemonerrors.h" #include "dnsutilswindows.h" #include "leakdetector.h" #include "logger.h" -#include "core/networkUtilities.h" +#include "platforms/windows/daemon/windowsfirewall.h" +#include "platforms/windows/daemon/windowssplittunnel.h" #include "platforms/windows/windowscommons.h" -#include "platforms/windows/windowsservicemanager.h" #include "windowsfirewall.h" +#include "core/networkUtilities.h" + namespace { Logger logger("WindowsDaemon"); } -WindowsDaemon::WindowsDaemon() : Daemon(nullptr), m_splitTunnelManager(this) { +WindowsDaemon::WindowsDaemon() : Daemon(nullptr) { MZ_COUNT_CTOR(WindowsDaemon); + m_firewallManager = WindowsFirewall::create(this); + Q_ASSERT(m_firewallManager != nullptr); - m_wgutils = new WireguardUtilsWindows(this); + m_wgutils = WireguardUtilsWindows::create(m_firewallManager, this); m_dnsutils = new DnsUtilsWindows(this); + m_splitTunnelManager = WindowsSplitTunnel::create(m_firewallManager); - connect(m_wgutils, &WireguardUtilsWindows::backendFailure, this, + connect(m_wgutils.get(), &WireguardUtilsWindows::backendFailure, this, &WindowsDaemon::monitorBackendFailure); connect(this, &WindowsDaemon::activationFailure, - []() { WindowsFirewall::instance()->disableKillSwitch(); }); + [this]() { m_firewallManager->disableKillSwitch(); }); } WindowsDaemon::~WindowsDaemon() { @@ -57,28 +64,42 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) { if (config.m_vpnDisabledApps.length() > 0) { - m_splitTunnelManager.start(m_inetAdapterIndex, vpnAdapterIndex); - m_splitTunnelManager.setRules(config.m_vpnDisabledApps); + m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex); + m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps); } else { - m_splitTunnelManager.stop(); + m_splitTunnelManager->stop(); } } bool WindowsDaemon::run(Op op, const InterfaceConfig& config) { - if (op == Down) { - m_splitTunnelManager.stop(); + if (!m_splitTunnelManager) { + if (config.m_vpnDisabledApps.length() > 0) { + // The Client has sent us a list of disabled apps, but we failed + // to init the the split tunnel driver. + // So let the client know this was not possible + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE); + } return true; } - if (op == Up) { - logger.debug() << "Tunnel UP, Starting SplitTunneling"; - if (!WindowsSplitTunnel::isInstalled()) { - logger.warning() << "Split Tunnel Driver not Installed yet, fixing this."; - WindowsSplitTunnel::installDriver(); - } + if (op == Down) { + m_splitTunnelManager->stop(); + return true; } - - activateSplitTunnel(config); + if (config.m_vpnDisabledApps.length() > 0) { + if (!m_splitTunnelManager->start(m_inetAdapterIndex)) { + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE); + }; + if (!m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps)) { + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE); + }; + // Now the driver should be running (State == 4) + if (!m_splitTunnelManager->isRunning()) { + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE); + } + return true; + } + m_splitTunnelManager->stop(); return true; } diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h index 9d051bae..b17dc811 100644 --- a/client/platforms/windows/daemon/windowsdaemon.h +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -5,8 +5,11 @@ #ifndef WINDOWSDAEMON_H #define WINDOWSDAEMON_H +#include + #include "daemon/daemon.h" #include "dnsutilswindows.h" +#include "windowsfirewall.h" #include "windowssplittunnel.h" #include "windowstunnelservice.h" #include "wireguardutilswindows.h" @@ -25,8 +28,7 @@ class WindowsDaemon final : public Daemon { protected: bool run(Op op, const InterfaceConfig& config) override; - WireguardUtils* wgutils() const override { return m_wgutils; } - bool supportDnsUtils() const override { return true; } + WireguardUtils* wgutils() const override { return m_wgutils.get(); } DnsUtils* dnsutils() override { return m_dnsutils; } private: @@ -40,9 +42,10 @@ class WindowsDaemon final : public Daemon { int m_inetAdapterIndex = -1; - WireguardUtilsWindows* m_wgutils = nullptr; + std::unique_ptr m_wgutils; DnsUtilsWindows* m_dnsutils = nullptr; - WindowsSplitTunnel m_splitTunnelManager; + std::unique_ptr m_splitTunnelManager; + QPointer m_firewallManager; }; #endif // WINDOWSDAEMON_H diff --git a/client/platforms/windows/daemon/windowsfirewall.cpp b/client/platforms/windows/daemon/windowsfirewall.cpp index 3d45f228..03525387 100644 --- a/client/platforms/windows/daemon/windowsfirewall.cpp +++ b/client/platforms/windows/daemon/windowsfirewall.cpp @@ -9,11 +9,12 @@ #include #include #include -//#include -#include - +#include +#include #include #include +#include +#include "winsock.h" #include #include @@ -27,7 +28,6 @@ #include "leakdetector.h" #include "logger.h" #include "platforms/windows/windowsutils.h" -#include "winsock.h" #define IPV6_ADDRESS_SIZE 16 @@ -49,18 +49,13 @@ constexpr uint8_t HIGH_WEIGHT = 13; constexpr uint8_t MAX_WEIGHT = 15; } // namespace -WindowsFirewall* WindowsFirewall::instance() { - if (s_instance == nullptr) { - s_instance = new WindowsFirewall(qApp); +WindowsFirewall* WindowsFirewall::create(QObject* parent) { + if (s_instance != nullptr) { + // Only one instance of the firewall is allowed +// Q_ASSERT(false); + return s_instance; } - return s_instance; -} - -WindowsFirewall::WindowsFirewall(QObject* parent) : QObject(parent) { - MZ_COUNT_CTOR(WindowsFirewall); - Q_ASSERT(s_instance == nullptr); - - HANDLE engineHandle = NULL; + HANDLE engineHandle = nullptr; DWORD result = ERROR_SUCCESS; // Use dynamic sessions for efficiency and safety: // -> Filtering policy objects are deleted even when the application crashes/ @@ -71,15 +66,24 @@ WindowsFirewall::WindowsFirewall(QObject* parent) : QObject(parent) { logger.debug() << "Opening the filter engine."; - result = - FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &engineHandle); + result = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, &session, + &engineHandle); if (result != ERROR_SUCCESS) { WindowsUtils::windowsLog("FwpmEngineOpen0 failed"); - return; + return nullptr; } logger.debug() << "Filter engine opened successfully."; - m_sessionHandle = engineHandle; + if (!initSublayer()) { + return nullptr; + } + s_instance = new WindowsFirewall(engineHandle, parent); + return s_instance; +} + +WindowsFirewall::WindowsFirewall(HANDLE session, QObject* parent) + : QObject(parent), m_sessionHandle(session) { + MZ_COUNT_CTOR(WindowsFirewall); } WindowsFirewall::~WindowsFirewall() { @@ -89,15 +93,8 @@ WindowsFirewall::~WindowsFirewall() { } } -bool WindowsFirewall::init() { - if (m_init) { - logger.warning() << "Alread initialised FW_WFP layer"; - return true; - } - if (m_sessionHandle == INVALID_HANDLE_VALUE) { - logger.error() << "Cant Init Sublayer with invalid wfp handle"; - return false; - } +// static +bool WindowsFirewall::initSublayer() { // If we were not able to aquire a handle, this will fail anyway. // We need to open up another handle because of wfp rules: // If a wfp resource was created with SESSION_DYNAMIC, @@ -157,11 +154,10 @@ bool WindowsFirewall::init() { return false; } logger.debug() << "Initialised Sublayer"; - m_init = true; return true; } -bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { +bool WindowsFirewall::enableInterface(int vpnAdapterIndex) { // Checks if the FW_Rule was enabled succesfully, // disables the whole killswitch and returns false if not. #define FW_OK(rule) \ @@ -184,7 +180,7 @@ bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { } \ } - logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex; + logger.info() << "Enabling firewall Using Adapter:" << vpnAdapterIndex; FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT, "Allow usage of VPN Adapter")); FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic")); @@ -200,6 +196,36 @@ bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { #undef FW_OK } +// Allow unprotected traffic sent to the following local address ranges. +bool WindowsFirewall::enableLanBypass(const QList& ranges) { + // Start the firewall transaction + auto result = FwpmTransactionBegin(m_sessionHandle, NULL); + if (result != ERROR_SUCCESS) { + disableKillSwitch(); + return false; + } + auto cleanup = qScopeGuard([&] { + FwpmTransactionAbort0(m_sessionHandle); + disableKillSwitch(); + }); + + // Blocking unprotected traffic + for (const IPAddress& prefix : ranges) { + if (!allowTrafficTo(prefix, LOW_WEIGHT + 1, "Allow LAN bypass traffic")) { + return false; + } + } + + result = FwpmTransactionCommit0(m_sessionHandle); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionCommit0 failed with error:" << result; + return false; + } + + cleanup.dismiss(); + return true; +} + bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) { // Start the firewall transaction auto result = FwpmTransactionBegin(m_sessionHandle, NULL); @@ -238,10 +264,10 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) { if (!config.m_excludedAddresses.empty()) { for (const QString& i : config.m_excludedAddresses) { - logger.debug() << "range: " << i; + logger.debug() << "excludedAddresses range: " << i; - if (!allowTrafficToRange(i, HIGH_WEIGHT, - "Allow Ecxlude route", config.m_serverPublicKey)) { + if (!allowTrafficTo(i, HIGH_WEIGHT, + "Allow Ecxlude route", config.m_serverPublicKey)) { return false; } } @@ -421,9 +447,59 @@ bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight, return true; } +bool WindowsFirewall::allowTrafficTo(const IPAddress& addr, int weight, + const QString& title, + const QString& peer) { + GUID layerKeyOut; + GUID layerKeyIn; + if (addr.type() == QAbstractSocket::IPv4Protocol) { + layerKeyOut = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + layerKeyIn = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + } else { + layerKeyOut = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + layerKeyIn = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + } + + // Match the IP address range. + FWPM_FILTER_CONDITION0 cond[1] = {}; + FWP_RANGE0 ipRange; + QByteArray lowIpV6Buffer; + QByteArray highIpV6Buffer; + + importAddress(addr.address(), ipRange.valueLow, &lowIpV6Buffer); + importAddress(addr.broadcastAddress(), ipRange.valueHigh, &highIpV6Buffer); + + cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; + cond[0].matchType = FWP_MATCH_RANGE; + cond[0].conditionValue.type = FWP_RANGE_TYPE; + cond[0].conditionValue.rangeValue = &ipRange; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.numFilterConditions = 1; + filter.filterCondition = cond; + + // Send the filters down to the firewall. + QString description = "Permit traffic %1 " + addr.toString(); + filter.layerKey = layerKeyOut; + if (!enableFilter(&filter, title, description.arg("to"), peer)) { + return false; + } + filter.layerKey = layerKeyIn; + if (!enableFilter(&filter, title, description.arg("from"), peer)) { + return false; + } + return true; +} + bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port, - int weight, const QString& title, - const QString& peer) { + int weight, const QString& title, + const QString& peer) { bool isIPv4 = targetIP.protocol() == QAbstractSocket::IPv4Protocol; GUID layerOut = isIPv4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6; @@ -484,57 +560,6 @@ bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port, return true; } -bool WindowsFirewall::allowTrafficToRange(const IPAddress& addr, uint8_t weight, - const QString& title, - const QString& peer) { - QString description("Allow traffic %1 %2 "); - - auto lower = addr.address(); - auto upper = addr.broadcastAddress(); - - const bool isV4 = addr.type() == QAbstractSocket::IPv4Protocol; - const GUID layerKeyOut = - isV4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6; - const GUID layerKeyIn = isV4 ? FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 - : FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; - - // Assemble the Filter base - FWPM_FILTER0 filter; - memset(&filter, 0, sizeof(filter)); - filter.action.type = FWP_ACTION_PERMIT; - filter.weight.type = FWP_UINT8; - filter.weight.uint8 = weight; - filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; - - FWPM_FILTER_CONDITION0 cond[1] = {0}; - FWP_RANGE0 ipRange; - QByteArray lowIpV6Buffer; - QByteArray highIpV6Buffer; - - importAddress(lower, ipRange.valueLow, &lowIpV6Buffer); - importAddress(upper, ipRange.valueHigh, &highIpV6Buffer); - - cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; - cond[0].matchType = FWP_MATCH_RANGE; - cond[0].conditionValue.type = FWP_RANGE_TYPE; - cond[0].conditionValue.rangeValue = &ipRange; - - filter.numFilterConditions = 1; - filter.filterCondition = cond; - - filter.layerKey = layerKeyOut; - if (!enableFilter(&filter, title, description.arg("to").arg(addr.toString()), - peer)) { - return false; - } - filter.layerKey = layerKeyIn; - if (!enableFilter(&filter, title, - description.arg("from").arg(addr.toString()), peer)) { - return false; - } - return true; -} - bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) { // Allow outbound DHCPv4 { @@ -734,7 +759,7 @@ bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight, filter.weight.uint8 = weight; filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; - FWPM_FILTER_CONDITION0 cond[1] = {0}; + FWPM_FILTER_CONDITION0 cond[1] = {}; FWP_RANGE0 ipRange; QByteArray lowIpV6Buffer; QByteArray highIpV6Buffer; diff --git a/client/platforms/windows/daemon/windowsfirewall.h b/client/platforms/windows/daemon/windowsfirewall.h index e0d5ebe8..55ee9417 100644 --- a/client/platforms/windows/daemon/windowsfirewall.h +++ b/client/platforms/windows/daemon/windowsfirewall.h @@ -26,18 +26,27 @@ struct FWP_CONDITION_VALUE0_; class WindowsFirewall final : public QObject { public: - ~WindowsFirewall(); + /** + * @brief Opens the Windows Filtering Platform, initializes the session, + * sublayer. Returns a WindowsFirewall object if successful, otherwise + * nullptr. If there is already a WindowsFirewall object, it will be returned. + * + * @param parent - parent QObject + * @return WindowsFirewall* - nullptr if failed to open the Windows Filtering + * Platform. + */ + static WindowsFirewall* create(QObject* parent); + ~WindowsFirewall() override; - static WindowsFirewall* instance(); - bool init(); - - bool enableKillSwitch(int vpnAdapterIndex); + bool enableInterface(int vpnAdapterIndex); + bool enableLanBypass(const QList& ranges); bool enablePeerTraffic(const InterfaceConfig& config); bool disablePeerTraffic(const QString& pubkey); bool disableKillSwitch(); private: - WindowsFirewall(QObject* parent); + static bool initSublayer(); + WindowsFirewall(HANDLE session, QObject* parent); HANDLE m_sessionHandle; bool m_init = false; QList m_activeRules; @@ -50,11 +59,10 @@ class WindowsFirewall final : public QObject { bool blockTrafficTo(const IPAddress& addr, uint8_t weight, const QString& title, const QString& peer = QString()); bool blockTrafficOnPort(uint port, uint8_t weight, const QString& title); + bool allowTrafficTo(const IPAddress& addr, int weight, const QString& title, + const QString& peer = QString()); bool allowTrafficTo(const QHostAddress& targetIP, uint port, int weight, const QString& title, const QString& peer = QString()); - bool allowTrafficToRange(const IPAddress& addr, uint8_t weight, - const QString& title, - const QString& peer); bool allowTrafficOfAdapter(int networkAdapter, uint8_t weight, const QString& title); bool allowDHCPTraffic(uint8_t weight, const QString& title); diff --git a/client/platforms/windows/daemon/windowsroutemonitor.cpp b/client/platforms/windows/daemon/windowsroutemonitor.cpp index 69967526..fb0fbf7e 100644 --- a/client/platforms/windows/daemon/windowsroutemonitor.cpp +++ b/client/platforms/windows/daemon/windowsroutemonitor.cpp @@ -13,6 +13,12 @@ namespace { Logger logger("WindowsRouteMonitor"); }; // namespace +// Attempt to mark routing entries that we create with a relatively +// high metric. This ensures that we can skip over routes of our own +// creation when processing route changes, and ensures that we give +// way to other routing entries. +constexpr const ULONG EXCLUSION_ROUTE_METRIC = 0x5e72; + // Called by the kernel on route changes - perform some basic filtering and // invoke the routeChanged slot to do the real work. static void routeChangeCallback(PVOID context, PMIB_IPFORWARD_ROW2 row, @@ -20,22 +26,17 @@ static void routeChangeCallback(PVOID context, PMIB_IPFORWARD_ROW2 row, WindowsRouteMonitor* monitor = (WindowsRouteMonitor*)context; Q_UNUSED(type); - // Ignore host route changes, and unsupported protocols. - if (row->DestinationPrefix.Prefix.si_family == AF_INET6) { - if (row->DestinationPrefix.PrefixLength >= 128) { - return; - } - } else if (row->DestinationPrefix.Prefix.si_family == AF_INET) { - if (row->DestinationPrefix.PrefixLength >= 32) { - return; - } - } else { + // Ignore route changes that we created. + if ((row->Protocol == MIB_IPPROTO_NETMGMT) && + (row->Metric == EXCLUSION_ROUTE_METRIC)) { + return; + } + if (monitor->getLuid() == row->InterfaceLuid.Value) { return; } - if (monitor->getLuid() != row->InterfaceLuid.Value) { - QMetaObject::invokeMethod(monitor, "routeChanged", Qt::QueuedConnection); - } + // Invoke the route changed signal to do the real work in Qt. + QMetaObject::invokeMethod(monitor, "routeChanged", Qt::QueuedConnection); } // Perform prefix matching comparison on IP addresses in host order. @@ -57,7 +58,8 @@ static int prefixcmp(const void* a, const void* b, size_t bits) { return 0; } -WindowsRouteMonitor::WindowsRouteMonitor(QObject* parent) : QObject(parent) { +WindowsRouteMonitor::WindowsRouteMonitor(quint64 luid, QObject* parent) + : QObject(parent), m_luid(luid) { MZ_COUNT_CTOR(WindowsRouteMonitor); logger.debug() << "WindowsRouteMonitor created."; @@ -67,11 +69,13 @@ WindowsRouteMonitor::WindowsRouteMonitor(QObject* parent) : QObject(parent) { WindowsRouteMonitor::~WindowsRouteMonitor() { MZ_COUNT_DTOR(WindowsRouteMonitor); CancelMibChangeNotify2(m_routeHandle); - flushExclusionRoutes(); + + flushRouteTable(m_exclusionRoutes); + flushRouteTable(m_clonedRoutes); logger.debug() << "WindowsRouteMonitor destroyed."; } -void WindowsRouteMonitor::updateValidInterfaces(int family) { +void WindowsRouteMonitor::updateInterfaceMetrics(int family) { PMIB_IPINTERFACE_TABLE table; DWORD result = GetIpInterfaceTable(family, &table); if (result != NO_ERROR) { @@ -82,10 +86,10 @@ void WindowsRouteMonitor::updateValidInterfaces(int family) { // Flush the list of interfaces that are valid for routing. if ((family == AF_INET) || (family == AF_UNSPEC)) { - m_validInterfacesIpv4.clear(); + m_interfaceMetricsIpv4.clear(); } if ((family == AF_INET6) || (family == AF_UNSPEC)) { - m_validInterfacesIpv6.clear(); + m_interfaceMetricsIpv6.clear(); } // Rebuild the list of interfaces that are valid for routing. @@ -101,12 +105,12 @@ void WindowsRouteMonitor::updateValidInterfaces(int family) { if (row->Family == AF_INET) { logger.debug() << "Interface" << row->InterfaceIndex << "is valid for IPv4 routing"; - m_validInterfacesIpv4.append(row->InterfaceLuid.Value); + m_interfaceMetricsIpv4[row->InterfaceLuid.Value] = row->Metric; } if (row->Family == AF_INET6) { logger.debug() << "Interface" << row->InterfaceIndex << "is valid for IPv6 routing"; - m_validInterfacesIpv6.append(row->InterfaceLuid.Value); + m_interfaceMetricsIpv6[row->InterfaceLuid.Value] = row->Metric; } } } @@ -126,72 +130,72 @@ void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, if (row->InterfaceLuid.Value == m_luid) { continue; } - // Ignore host routes, and shorter potential matches. - if (row->DestinationPrefix.PrefixLength >= - data->DestinationPrefix.PrefixLength) { + if (row->DestinationPrefix.PrefixLength < bestMatch) { continue; } - if (row->DestinationPrefix.PrefixLength < bestMatch) { + // Ignore routes of our own creation. + if ((row->Protocol == data->Protocol) && (row->Metric == data->Metric)) { continue; } // Check if the routing table entry matches the destination. + if (!routeContainsDest(&row->DestinationPrefix, &data->DestinationPrefix)) { + continue; + } + + // Compute the combined interface and routing metric. + ULONG routeMetric = row->Metric; if (data->DestinationPrefix.Prefix.si_family == AF_INET6) { - if (row->DestinationPrefix.Prefix.Ipv6.sin6_family != AF_INET6) { - continue; - } - if (!m_validInterfacesIpv6.contains(row->InterfaceLuid.Value)) { - continue; - } - if (prefixcmp(&data->DestinationPrefix.Prefix.Ipv6.sin6_addr, - &row->DestinationPrefix.Prefix.Ipv6.sin6_addr, - row->DestinationPrefix.PrefixLength) != 0) { + if (!m_interfaceMetricsIpv6.contains(row->InterfaceLuid.Value)) { continue; } + routeMetric += m_interfaceMetricsIpv6[row->InterfaceLuid.Value]; } else if (data->DestinationPrefix.Prefix.si_family == AF_INET) { - if (row->DestinationPrefix.Prefix.Ipv4.sin_family != AF_INET) { - continue; - } - if (!m_validInterfacesIpv4.contains(row->InterfaceLuid.Value)) { - continue; - } - if (prefixcmp(&data->DestinationPrefix.Prefix.Ipv4.sin_addr, - &row->DestinationPrefix.Prefix.Ipv4.sin_addr, - row->DestinationPrefix.PrefixLength) != 0) { + if (!m_interfaceMetricsIpv4.contains(row->InterfaceLuid.Value)) { continue; } + routeMetric += m_interfaceMetricsIpv4[row->InterfaceLuid.Value]; } else { // Unsupported destination address family. continue; } + if (routeMetric < row->Metric) { + routeMetric = ULONG_MAX; + } // Prefer routes with lower metric if we find multiple matches // with the same prefix length. if ((row->DestinationPrefix.PrefixLength == bestMatch) && - (row->Metric >= bestMetric)) { + (routeMetric >= bestMetric)) { continue; } // If we got here, then this is the longest prefix match so far. memcpy(&nexthop, &row->NextHop, sizeof(SOCKADDR_INET)); - bestLuid = row->InterfaceLuid.Value; bestMatch = row->DestinationPrefix.PrefixLength; - bestMetric = row->Metric; + bestMetric = routeMetric; + if (bestMatch == data->DestinationPrefix.PrefixLength) { + bestLuid = 0; // Don't write to the table if we find an exact match. + } else { + bestLuid = row->InterfaceLuid.Value; + } } // If neither the interface nor next-hop have changed, then do nothing. - if ((data->InterfaceLuid.Value) == bestLuid && + if (data->InterfaceLuid.Value == bestLuid && memcmp(&nexthop, &data->NextHop, sizeof(SOCKADDR_INET)) == 0) { return; } - // Update the routing table entry. + // Delete the previous routing table entry, if any. if (data->InterfaceLuid.Value != 0) { DWORD result = DeleteIpForwardEntry2(data); if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { logger.error() << "Failed to delete route:" << result; } } + + // Update the routing table entry. data->InterfaceLuid.Value = bestLuid; memcpy(&data->NextHop, &nexthop, sizeof(SOCKADDR_INET)); if (data->InterfaceLuid.Value != 0) { @@ -202,10 +206,178 @@ void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, } } +// static +bool WindowsRouteMonitor::routeContainsDest(const IP_ADDRESS_PREFIX* route, + const IP_ADDRESS_PREFIX* dest) { + if (route->Prefix.si_family != dest->Prefix.si_family) { + return false; + } + if (route->PrefixLength > dest->PrefixLength) { + return false; + } + if (route->Prefix.si_family == AF_INET) { + return prefixcmp(&route->Prefix.Ipv4.sin_addr, &dest->Prefix.Ipv4.sin_addr, + route->PrefixLength) == 0; + } else if (route->Prefix.si_family == AF_INET6) { + return prefixcmp(&route->Prefix.Ipv6.sin6_addr, + &dest->Prefix.Ipv6.sin6_addr, route->PrefixLength) == 0; + } else { + return false; + } +} + +// static +QHostAddress WindowsRouteMonitor::prefixToAddress( + const IP_ADDRESS_PREFIX* dest) { + if (dest->Prefix.si_family == AF_INET6) { + return QHostAddress(dest->Prefix.Ipv6.sin6_addr.s6_addr); + } else if (dest->Prefix.si_family == AF_INET) { + quint32 addr = htonl(dest->Prefix.Ipv4.sin_addr.s_addr); + return QHostAddress(addr); + } else { + return QHostAddress(); + } +} + +bool WindowsRouteMonitor::isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const { + auto i = m_exclusionRoutes.constBegin(); + while (i != m_exclusionRoutes.constEnd()) { + const MIB_IPFORWARD_ROW2* row = i.value(); + if (routeContainsDest(&row->DestinationPrefix, dest)) { + return true; + } + i++; + } + return false; +} + +void WindowsRouteMonitor::updateCapturedRoutes(int family) { + if (!m_defaultRouteCapture) { + return; + } + + PMIB_IPFORWARD_TABLE2 table; + DWORD error = GetIpForwardTable2(family, &table); + if (error != NO_ERROR) { + updateCapturedRoutes(family, table); + FreeMibTable(table); + } +} + +void WindowsRouteMonitor::updateCapturedRoutes(int family, void* ptable) { + PMIB_IPFORWARD_TABLE2 table = reinterpret_cast(ptable); + if (!m_defaultRouteCapture) { + return; + } + + for (ULONG i = 0; i < table->NumEntries; i++) { + MIB_IPFORWARD_ROW2* row = &table->Table[i]; + // Ignore routes into the VPN interface. + if (row->InterfaceLuid.Value == m_luid) { + continue; + } + // Ignore the default route + if (row->DestinationPrefix.PrefixLength == 0) { + continue; + } + // Ignore routes of our own creation. + if ((row->Protocol == MIB_IPPROTO_NETMGMT) && + (row->Metric == EXCLUSION_ROUTE_METRIC)) { + continue; + } + // Ignore routes which should be excluded. + if (isRouteExcluded(&row->DestinationPrefix)) { + continue; + } + QHostAddress destination = prefixToAddress(&row->DestinationPrefix); + if (destination.isLoopback() || destination.isBroadcast() || + destination.isLinkLocal() || destination.isMulticast()) { + continue; + } + + // If we get here, this route should be cloned. + IPAddress prefix(destination, row->DestinationPrefix.PrefixLength); + MIB_IPFORWARD_ROW2* data = m_clonedRoutes.value(prefix, nullptr); + if (data != nullptr) { + // Count the number of matching entries in the main table. + data->Age++; + continue; + } + logger.debug() << "Capturing route to" + << logger.sensitive(prefix.toString()); + + // Clone the route and direct it into the VPN tunnel. + data = new MIB_IPFORWARD_ROW2; + InitializeIpForwardEntry(data); + data->InterfaceLuid.Value = m_luid; + data->DestinationPrefix = row->DestinationPrefix; + data->NextHop.si_family = data->DestinationPrefix.Prefix.si_family; + + // Set the rest of the flags for a static route. + data->ValidLifetime = 0xffffffff; + data->PreferredLifetime = 0xffffffff; + data->Metric = 0; + data->Protocol = MIB_IPPROTO_NETMGMT; + data->Loopback = false; + data->AutoconfigureAddress = false; + data->Publish = false; + data->Immortal = false; + data->Age = 0; + + // Route this traffic into the VPN tunnel. + DWORD result = CreateIpForwardEntry2(data); + if (result != NO_ERROR) { + logger.error() << "Failed to update route:" << result; + delete data; + } else { + m_clonedRoutes.insert(prefix, data); + data->Age++; + } + } + + // Finally scan for any routes which were removed from the table. We do this + // by reusing the age field to count the number of matching entries in the + // main table. + auto i = m_clonedRoutes.begin(); + while (i != m_clonedRoutes.end()) { + MIB_IPFORWARD_ROW2* data = i.value(); + if (data->Age > 0) { + // Entry is in use, don't delete it. + data->Age = 0; + i++; + continue; + } + if ((family != AF_UNSPEC) && + (data->DestinationPrefix.Prefix.si_family != family)) { + // We are not processing updates to this address family. + i++; + continue; + } + + logger.debug() << "Removing route capture for" + << logger.sensitive(i.key().toString()); + + // Otherwise, this route is no longer in use. + DWORD result = DeleteIpForwardEntry2(data); + if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { + logger.error() << "Failed to delete route:" << result; + } + delete data; + i = m_clonedRoutes.erase(i); + } +} + bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { logger.debug() << "Adding exclusion route for" << logger.sensitive(prefix.toString()); + // Silently ignore non-routeable addresses. + QHostAddress addr = prefix.address(); + if (addr.isLoopback() || addr.isBroadcast() || addr.isLinkLocal() || + addr.isMulticast()) { + return true; + } + if (m_exclusionRoutes.contains(prefix)) { logger.warning() << "Exclusion route already exists"; return false; @@ -232,7 +404,7 @@ bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { // Set the rest of the flags for a static route. data->ValidLifetime = 0xffffffff; data->PreferredLifetime = 0xffffffff; - data->Metric = 0; + data->Metric = EXCLUSION_ROUTE_METRIC; data->Protocol = MIB_IPPROTO_NETMGMT; data->Loopback = false; data->AutoconfigureAddress = false; @@ -254,7 +426,8 @@ bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { delete data; return false; } - updateValidInterfaces(family); + updateInterfaceMetrics(family); + updateCapturedRoutes(family, table); updateExclusionRoute(data, table); FreeMibTable(table); @@ -266,26 +439,28 @@ bool WindowsRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { logger.debug() << "Deleting exclusion route for" << logger.sensitive(prefix.address().toString()); - for (;;) { - MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix); - if (data == nullptr) { - break; - } - - DWORD result = DeleteIpForwardEntry2(data); - if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { - logger.error() << "Failed to delete route to" - << logger.sensitive(prefix.toString()) - << "result:" << result; - } - delete data; + MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix); + if (data == nullptr) { + return true; } + DWORD result = DeleteIpForwardEntry2(data); + if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { + logger.error() << "Failed to delete route to" + << logger.sensitive(prefix.toString()) + << "result:" << result; + } + + // Captured routes might have changed. + updateCapturedRoutes(data->DestinationPrefix.Prefix.si_family); + + delete data; return true; } -void WindowsRouteMonitor::flushExclusionRoutes() { - for (auto i = m_exclusionRoutes.begin(); i != m_exclusionRoutes.end(); i++) { +void WindowsRouteMonitor::flushRouteTable( + QHash& table) { + for (auto i = table.begin(); i != table.end(); i++) { MIB_IPFORWARD_ROW2* data = i.value(); DWORD result = DeleteIpForwardEntry2(data); if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { @@ -295,7 +470,17 @@ void WindowsRouteMonitor::flushExclusionRoutes() { } delete data; } - m_exclusionRoutes.clear(); + table.clear(); +} + +void WindowsRouteMonitor::setDetaultRouteCapture(bool enable) { + m_defaultRouteCapture = enable; + + // Flush any captured routes when disabling the feature. + if (!m_defaultRouteCapture) { + flushRouteTable(m_clonedRoutes); + return; + } } void WindowsRouteMonitor::routeChanged() { @@ -308,7 +493,8 @@ void WindowsRouteMonitor::routeChanged() { return; } - updateValidInterfaces(AF_UNSPEC); + updateInterfaceMetrics(AF_UNSPEC); + updateCapturedRoutes(AF_UNSPEC, table); for (MIB_IPFORWARD_ROW2* data : m_exclusionRoutes) { updateExclusionRoute(data, table); } diff --git a/client/platforms/windows/daemon/windowsroutemonitor.h b/client/platforms/windows/daemon/windowsroutemonitor.h index 0ae9a8a2..fa04f646 100644 --- a/client/platforms/windows/daemon/windowsroutemonitor.h +++ b/client/platforms/windows/daemon/windowsroutemonitor.h @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include "ipaddress.h" @@ -19,28 +21,41 @@ class WindowsRouteMonitor final : public QObject { Q_OBJECT public: - WindowsRouteMonitor(QObject* parent); + WindowsRouteMonitor(quint64 luid, QObject* parent); ~WindowsRouteMonitor(); + void setDetaultRouteCapture(bool enable); + bool addExclusionRoute(const IPAddress& prefix); bool deleteExclusionRoute(const IPAddress& prefix); - void flushExclusionRoutes(); + void flushExclusionRoutes() { return flushRouteTable(m_exclusionRoutes); }; - void setLuid(quint64 luid) { m_luid = luid; } - quint64 getLuid() { return m_luid; } + quint64 getLuid() const { return m_luid; } public slots: void routeChanged(); private: + bool isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const; + static bool routeContainsDest(const IP_ADDRESS_PREFIX* route, + const IP_ADDRESS_PREFIX* dest); + static QHostAddress prefixToAddress(const IP_ADDRESS_PREFIX* dest); + + void flushRouteTable(QHash& table); void updateExclusionRoute(MIB_IPFORWARD_ROW2* data, void* table); - void updateValidInterfaces(int family); + void updateInterfaceMetrics(int family); + void updateCapturedRoutes(int family); + void updateCapturedRoutes(int family, void* table); QHash m_exclusionRoutes; - QList m_validInterfacesIpv4; - QList m_validInterfacesIpv6; + QMap m_interfaceMetricsIpv4; + QMap m_interfaceMetricsIpv6; - quint64 m_luid = 0; + // Default route cloning + bool m_defaultRouteCapture = false; + QHash m_clonedRoutes; + + const quint64 m_luid = 0; HANDLE m_routeHandle = INVALID_HANDLE_VALUE; }; diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp index 39941933..63de153d 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.cpp +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -4,9 +4,15 @@ #include "windowssplittunnel.h" +#include + +#include + #include "../windowscommons.h" #include "../windowsservicemanager.h" #include "logger.h" +#include "platforms/windows/daemon/windowsfirewall.h" +#include "platforms/windows/daemon/windowssplittunnel.h" #include "platforms/windows/windowsutils.h" #include "windowsfirewall.h" @@ -18,34 +24,252 @@ #include #include #include -#include + +#pragma region + +// Driver Configuration structures +using CONFIGURATION_ENTRY = struct { + // Offset into buffer region that follows all entries. + // The image name uses the device path. + SIZE_T ImageNameOffset; + // Length of the String + USHORT ImageNameLength; +}; + +using CONFIGURATION_HEADER = struct { + // Number of entries immediately following the header. + SIZE_T NumEntries; + + // Total byte length: header + entries + string buffer. + SIZE_T TotalLength; +}; + +// Used to Configure Which IP is network/vpn +using IP_ADDRESSES_CONFIG = struct { + IN_ADDR TunnelIpv4; + IN_ADDR InternetIpv4; + + IN6_ADDR TunnelIpv6; + IN6_ADDR InternetIpv6; +}; + +// Used to Define Which Processes are alive on activation +using PROCESS_DISCOVERY_HEADER = struct { + SIZE_T NumEntries; + SIZE_T TotalLength; +}; + +using PROCESS_DISCOVERY_ENTRY = struct { + HANDLE ProcessId; + HANDLE ParentProcessId; + + SIZE_T ImageNameOffset; + USHORT ImageNameLength; +}; + +using ProcessInfo = struct { + DWORD ProcessId; + DWORD ParentProcessId; + FILETIME CreationTime; + std::wstring DevicePath; +}; + +#ifndef CTL_CODE + +# define FILE_ANY_ACCESS 0x0000 + +# define METHOD_BUFFERED 0 +# define METHOD_IN_DIRECT 1 +# define METHOD_NEITHER 3 + +# define CTL_CODE(DeviceType, Function, Method, Access) \ + (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)) +#endif + +// Known ControlCodes +#define IOCTL_INITIALIZE CTL_CODE(0x8000, 1, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_DEQUEUE_EVENT \ + CTL_CODE(0x8000, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_REGISTER_PROCESSES \ + CTL_CODE(0x8000, 3, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_REGISTER_IP_ADDRESSES \ + CTL_CODE(0x8000, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GET_IP_ADDRESSES \ + CTL_CODE(0x8000, 5, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SET_CONFIGURATION \ + CTL_CODE(0x8000, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GET_CONFIGURATION \ + CTL_CODE(0x8000, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_CLEAR_CONFIGURATION \ + CTL_CODE(0x8000, 8, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_GET_STATE CTL_CODE(0x8000, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_QUERY_PROCESS \ + CTL_CODE(0x8000, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_ST_RESET CTL_CODE(0x8000, 11, METHOD_NEITHER, FILE_ANY_ACCESS) + +constexpr static const auto DRIVER_SYMLINK = L"\\\\.\\MULLVADSPLITTUNNEL"; +constexpr static const auto DRIVER_FILENAME = "mullvad-split-tunnel.sys"; +constexpr static const auto DRIVER_SERVICE_NAME = L"AmneziaVPNSplitTunnel"; +constexpr static const auto MV_SERVICE_NAME = L"MullvadVPN"; + +#pragma endregion namespace { Logger logger("WindowsSplitTunnel"); + +ProcessInfo getProcessInfo(HANDLE process, const PROCESSENTRY32W& processMeta) { + ProcessInfo pi; + pi.ParentProcessId = processMeta.th32ParentProcessID; + pi.ProcessId = processMeta.th32ProcessID; + pi.CreationTime = {0, 0}; + pi.DevicePath = L""; + + FILETIME creationTime, null_time; + auto ok = GetProcessTimes(process, &creationTime, &null_time, &null_time, + &null_time); + if (ok) { + pi.CreationTime = creationTime; + } + wchar_t imagepath[MAX_PATH + 1]; + if (K32GetProcessImageFileNameW( + process, imagepath, sizeof(imagepath) / sizeof(*imagepath)) != 0) { + pi.DevicePath = imagepath; + } + return pi; } -WindowsSplitTunnel::WindowsSplitTunnel(QObject* parent) : QObject(parent) { +} // namespace + +std::unique_ptr WindowsSplitTunnel::create( + WindowsFirewall* fw) { + if (fw == nullptr) { + // Pre-Condition: + // Make sure the Windows Firewall has created the sublayer + // otherwise the driver will fail to initialize + logger.error() << "Failed to did not pass a WindowsFirewall obj" + << "The Driver cannot work with the sublayer not created"; + return nullptr; + } + // 00: Check if we conflict with mullvad, if so. if (detectConflict()) { logger.error() << "Conflict detected, abort Split-Tunnel init."; - uninstallDriver(); - return; + return nullptr; } - - m_tries = 0; - + // 01: Check if the driver is installed, if not do so. if (!isInstalled()) { logger.debug() << "Driver is not Installed, doing so"; auto handle = installDriver(); if (handle == INVALID_HANDLE_VALUE) { WindowsUtils::windowsLog("Failed to install Driver"); - return; + return nullptr; } logger.debug() << "Driver installed"; CloseServiceHandle(handle); } else { - logger.debug() << "Driver is installed"; + logger.debug() << "Driver was installed"; } - initDriver(); + // 02: Now check if the service is running + auto driver_manager = + WindowsServiceManager::open(QString::fromWCharArray(DRIVER_SERVICE_NAME)); + if (Q_UNLIKELY(driver_manager == nullptr)) { + // Let's be fair if we end up here, + // after checking it exists and installing it, + // this is super unlikeley + Q_ASSERT(false); + logger.error() + << "WindowsServiceManager was unable fo find Split Tunnel service?"; + return nullptr; + } + if (!driver_manager->isRunning()) { + logger.debug() << "Driver is not running, starting it"; + // Start the service + if (!driver_manager->startService()) { + logger.error() << "Failed to start Split Tunnel Service"; + return nullptr; + }; + } + // 03: Open the Driver Symlink + auto driverFile = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + ; + if (driverFile == INVALID_HANDLE_VALUE) { + WindowsUtils::windowsLog("Failed to open Driver: "); + // Only once, if the opening did not work. Try to reboot it. # + logger.info() + << "Failed to open driver, attempting only once to reboot driver"; + if (!driver_manager->stopService()) { + logger.error() << "Unable stop driver"; + return nullptr; + }; + logger.info() << "Stopped driver, starting it again."; + if (!driver_manager->startService()) { + logger.error() << "Unable start driver"; + return nullptr; + }; + logger.info() << "Opening again."; + driverFile = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + if (driverFile == INVALID_HANDLE_VALUE) { + logger.error() << "Opening Failed again, sorry!"; + return nullptr; + } + } + if (!initDriver(driverFile)) { + logger.error() << "Failed to init driver"; + return nullptr; + } + // We're ready to talk to the driver, it's alive and setup. + return std::make_unique(driverFile); +} + +bool WindowsSplitTunnel::initDriver(HANDLE driverIO) { + // We need to now check the state and init it, if required + auto state = getState(driverIO); + if (state == STATE_UNKNOWN) { + logger.debug() << "Cannot check if driver is initialized"; + return false; + } + if (state >= STATE_INITIALIZED) { + logger.debug() << "Driver already initialized: " << state; + // Reset Driver as it has wfp handles probably >:( + resetDriver(driverIO); + + auto newState = getState(driverIO); + logger.debug() << "New state after reset:" << newState; + if (newState >= STATE_INITIALIZED) { + logger.debug() << "Reset unsuccesfull"; + return false; + } + } + + DWORD bytesReturned; + auto ok = DeviceIoControl(driverIO, IOCTL_INITIALIZE, nullptr, 0, nullptr, 0, + &bytesReturned, nullptr); + if (!ok) { + auto err = GetLastError(); + logger.error() << "Driver init failed err -" << err; + logger.error() << "State:" << getState(driverIO); + + return false; + } + logger.debug() << "Driver initialized" << getState(driverIO); + return true; +} + +WindowsSplitTunnel::WindowsSplitTunnel(HANDLE driverIO) : m_driver(driverIO) { + logger.debug() << "Connected to the Driver"; + + Q_ASSERT(getState() == STATE_INITIALIZED); } WindowsSplitTunnel::~WindowsSplitTunnel() { @@ -53,73 +277,12 @@ WindowsSplitTunnel::~WindowsSplitTunnel() { uninstallDriver(); } -void WindowsSplitTunnel::initDriver() { - if (detectConflict()) { - logger.error() << "Conflict detected, abort Split-Tunnel init."; - return; - } - logger.debug() << "Try to open Split Tunnel Driver"; - // Open the Driver Symlink - m_driver = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, - nullptr, OPEN_EXISTING, 0, nullptr); - ; - if (m_driver == INVALID_HANDLE_VALUE && m_tries < 500) { - WindowsUtils::windowsLog("Failed to open Driver: "); - m_tries++; - Sleep(100); - // If the handle is not present, try again after the serivce has started; - auto driver_manager = WindowsServiceManager(DRIVER_SERVICE_NAME); - QObject::connect(&driver_manager, &WindowsServiceManager::serviceStarted, - this, &WindowsSplitTunnel::initDriver); - driver_manager.startService(); - return; - } - - logger.debug() << "Connected to the Driver"; - // Reset Driver as it has wfp handles probably >:( - - if (!WindowsFirewall::instance()->init()) { - logger.error() << "Init WFP-Sublayer failed, driver won't be functional"; - return; - } - - // We need to now check the state and init it, if required - - auto state = getState(); - if (state == STATE_UNKNOWN) { - logger.debug() << "Cannot check if driver is initialized"; - } - if (state >= STATE_INITIALIZED) { - logger.debug() << "Driver already initialized: " << state; - reset(); - - auto newState = getState(); - logger.debug() << "New state after reset:" << newState; - if (newState >= STATE_INITIALIZED) { - logger.debug() << "Reset unsuccesfull"; - return; - } - } - - DWORD bytesReturned; - auto ok = DeviceIoControl(m_driver, IOCTL_INITIALIZE, nullptr, 0, nullptr, 0, - &bytesReturned, nullptr); - if (!ok) { - auto err = GetLastError(); - logger.error() << "Driver init failed err -" << err; - logger.error() << "State:" << getState(); - - return; - } - logger.debug() << "Driver initialized" << getState(); -} - -void WindowsSplitTunnel::setRules(const QStringList& appPaths) { +bool WindowsSplitTunnel::excludeApps(const QStringList& appPaths) { auto state = getState(); if (state != STATE_READY && state != STATE_RUNNING) { logger.warning() << "Driver is not in the right State to set Rules" << state; - return; + return false; } logger.debug() << "Pushing new Ruleset for Split-Tunnel " << state; @@ -133,12 +296,13 @@ void WindowsSplitTunnel::setRules(const QStringList& appPaths) { auto err = GetLastError(); WindowsUtils::windowsLog("Set Config Failed:"); logger.error() << "Failed to set Config err code " << err; - return; + return false; } - logger.debug() << "New Configuration applied: " << getState(); + logger.debug() << "New Configuration applied: " << stateString(); + return true; } -void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { +bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { // To Start we need to send 2 things: // Network info (what is vpn what is network) logger.debug() << "Starting SplitTunnel"; @@ -151,7 +315,7 @@ void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { 0, &bytesReturned, nullptr); if (!ok) { logger.error() << "Driver init failed"; - return; + return false; } } @@ -164,16 +328,16 @@ void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { nullptr); if (!ok) { logger.error() << "Failed to set Process Config"; - return; + return false; } - logger.debug() << "Set Process Config ok || new State:" << getState(); + logger.debug() << "Set Process Config ok || new State:" << stateString(); } if (getState() == STATE_INITIALIZED) { logger.warning() << "Driver is still not ready after process list send"; - return; + return false; } - logger.debug() << "Driver is ready || new State:" << getState(); + logger.debug() << "Driver is ready || new State:" << stateString(); auto config = generateIPConfiguration(inetAdapterIndex, vpnAdapterIndex); auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_IP_ADDRESSES, &config[0], @@ -181,9 +345,10 @@ void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { nullptr); if (!ok) { logger.error() << "Failed to set Network Config"; - return; + return false; } - logger.debug() << "New Network Config Applied || new State:" << getState(); + logger.debug() << "New Network Config Applied || new State:" << stateString(); + return true; } void WindowsSplitTunnel::stop() { @@ -197,25 +362,27 @@ void WindowsSplitTunnel::stop() { logger.debug() << "Stopping Split tunnel successfull"; } -void WindowsSplitTunnel::reset() { +bool WindowsSplitTunnel::resetDriver(HANDLE driverIO) { DWORD bytesReturned; - auto ok = DeviceIoControl(m_driver, IOCTL_ST_RESET, nullptr, 0, nullptr, 0, + auto ok = DeviceIoControl(driverIO, IOCTL_ST_RESET, nullptr, 0, nullptr, 0, &bytesReturned, nullptr); if (!ok) { logger.error() << "Reset Split tunnel not successfull"; - return; + return false; } logger.debug() << "Reset Split tunnel successfull"; + return true; } -DRIVER_STATE WindowsSplitTunnel::getState() { - if (m_driver == INVALID_HANDLE_VALUE) { +// static +WindowsSplitTunnel::DRIVER_STATE WindowsSplitTunnel::getState(HANDLE driverIO) { + if (driverIO == INVALID_HANDLE_VALUE) { logger.debug() << "Can't query State from non Opened Driver"; return STATE_UNKNOWN; } DWORD bytesReturned; SIZE_T outBuffer; - bool ok = DeviceIoControl(m_driver, IOCTL_GET_STATE, nullptr, 0, &outBuffer, + bool ok = DeviceIoControl(driverIO, IOCTL_GET_STATE, nullptr, 0, &outBuffer, sizeof(outBuffer), &bytesReturned, nullptr); if (!ok) { WindowsUtils::windowsLog("getState response failure"); @@ -225,7 +392,10 @@ DRIVER_STATE WindowsSplitTunnel::getState() { WindowsUtils::windowsLog("getState response is empty"); return STATE_UNKNOWN; } - return static_cast(outBuffer); + return static_cast(outBuffer); +} +WindowsSplitTunnel::DRIVER_STATE WindowsSplitTunnel::getState() { + return getState(m_driver); } std::vector WindowsSplitTunnel::generateAppConfiguration( @@ -273,58 +443,59 @@ std::vector WindowsSplitTunnel::generateAppConfiguration( return outBuffer; } -std::vector WindowsSplitTunnel::generateIPConfiguration( +std::vector WindowsSplitTunnel::generateIPConfiguration( int inetAdapterIndex, int vpnAdapterIndex) { - std::vector out(sizeof(IP_ADDRESSES_CONFIG)); + std::vector out(sizeof(IP_ADDRESSES_CONFIG)); auto config = reinterpret_cast(&out[0]); auto ifaces = QNetworkInterface::allInterfaces(); - if (vpnAdapterIndex == 0) { + if (vpnAdapterIndex == 0) { vpnAdapterIndex = WindowsCommons::VPNAdapterIndex(); } - // Always the VPN - getAddress(vpnAdapterIndex, &config->TunnelIpv4, - &config->TunnelIpv6); - // 2nd best route - getAddress(inetAdapterIndex, &config->InternetIpv4, &config->InternetIpv6); + if (!getAddress(vpnAdapterIndex, &config->TunnelIpv4, + &config->TunnelIpv6)) { + return {}; + } + // 2nd best route is usually the internet adapter + if (!getAddress(inetAdapterIndex, &config->InternetIpv4, + &config->InternetIpv6)) { + return {}; + }; return out; } -void WindowsSplitTunnel::getAddress(int adapterIndex, IN_ADDR* out_ipv4, +bool WindowsSplitTunnel::getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6) { QNetworkInterface target = QNetworkInterface::interfaceFromIndex(adapterIndex); logger.debug() << "Getting adapter info for:" << target.humanReadableName(); - // take the first v4/v6 Adress and convert to in_addr - for (auto address : target.addressEntries()) { - if (address.ip().protocol() == QAbstractSocket::IPv4Protocol) { - auto adrr = address.ip().toString(); - std::wstring wstr = adrr.toStdWString(); - logger.debug() << "IpV4" << logger.sensitive(adrr); - PCWSTR w_str_ip = wstr.c_str(); - auto ok = InetPtonW(AF_INET, w_str_ip, out_ipv4); - if (ok != 1) { - logger.debug() << "Ipv4 Conversation error" << WSAGetLastError(); + auto get = [&target](QAbstractSocket::NetworkLayerProtocol protocol) { + for (auto address : target.addressEntries()) { + if (address.ip().protocol() != protocol) { + continue; } - break; + return address.ip().toString().toStdWString(); } + return std::wstring{}; + }; + auto ipv4 = get(QAbstractSocket::IPv4Protocol); + auto ipv6 = get(QAbstractSocket::IPv6Protocol); + + if (InetPtonW(AF_INET, ipv4.c_str(), out_ipv4) != 1) { + logger.debug() << "Ipv4 Conversation error" << WSAGetLastError(); + return false; } - for (auto address : target.addressEntries()) { - if (address.ip().protocol() == QAbstractSocket::IPv6Protocol) { - auto adrr = address.ip().toString(); - std::wstring wstr = adrr.toStdWString(); - logger.debug() << "IpV6" << logger.sensitive(adrr); - PCWSTR w_str_ip = wstr.c_str(); - auto ok = InetPtonW(AF_INET6, w_str_ip, out_ipv6); - if (ok != 1) { - logger.error() << "Ipv6 Conversation error" << WSAGetLastError(); - } - break; - } + if (ipv6.empty()) { + std::memset(out_ipv6, 0x00, sizeof(IN6_ADDR)); + return true; } + if (InetPtonW(AF_INET6, ipv6.c_str(), out_ipv6) != 1) { + logger.debug() << "Ipv6 Conversation error" << WSAGetLastError(); + } + return true; } std::vector WindowsSplitTunnel::generateProcessBlob() { @@ -411,33 +582,6 @@ std::vector WindowsSplitTunnel::generateProcessBlob() { return out; } -void WindowsSplitTunnel::close() { - CloseHandle(m_driver); - m_driver = INVALID_HANDLE_VALUE; -} - -ProcessInfo WindowsSplitTunnel::getProcessInfo( - HANDLE process, const PROCESSENTRY32W& processMeta) { - ProcessInfo pi; - pi.ParentProcessId = processMeta.th32ParentProcessID; - pi.ProcessId = processMeta.th32ProcessID; - pi.CreationTime = {0, 0}; - pi.DevicePath = L""; - - FILETIME creationTime, null_time; - auto ok = GetProcessTimes(process, &creationTime, &null_time, &null_time, - &null_time); - if (ok) { - pi.CreationTime = creationTime; - } - wchar_t imagepath[MAX_PATH + 1]; - if (K32GetProcessImageFileNameW( - process, imagepath, sizeof(imagepath) / sizeof(*imagepath)) != 0) { - pi.DevicePath = imagepath; - } - return pi; -} - // static SC_HANDLE WindowsSplitTunnel::installDriver() { LPCWSTR displayName = L"Amnezia Split Tunnel Service"; @@ -448,15 +592,15 @@ SC_HANDLE WindowsSplitTunnel::installDriver() { return (SC_HANDLE)INVALID_HANDLE_VALUE; } auto path = driver.absolutePath() + "/" + DRIVER_FILENAME; - LPCWSTR binPath = (const wchar_t*)path.utf16(); + auto binPath = (const wchar_t*)path.utf16(); auto scm_rights = SC_MANAGER_ALL_ACCESS; - auto serviceManager = OpenSCManager(NULL, // local computer - NULL, // servicesActive database + auto serviceManager = OpenSCManager(nullptr, // local computer + nullptr, // servicesActive database scm_rights); - auto service = CreateService(serviceManager, DRIVER_SERVICE_NAME, displayName, - SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, - SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, - binPath, nullptr, 0, nullptr, nullptr, nullptr); + auto service = CreateService( + serviceManager, DRIVER_SERVICE_NAME, displayName, SERVICE_ALL_ACCESS, + SERVICE_KERNEL_DRIVER, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, binPath, + nullptr, nullptr, nullptr, nullptr, nullptr); CloseServiceHandle(serviceManager); return service; } @@ -502,7 +646,7 @@ QString WindowsSplitTunnel::convertPath(const QString& path) { // device should contain : for e.g C: return ""; } - QByteArray buffer(2048, 0xFF); + QByteArray buffer(2048, 0xFFu); auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter), (wchar_t*)buffer.data(), buffer.size() / 2); @@ -554,3 +698,25 @@ bool WindowsSplitTunnel::detectConflict() { CloseServiceHandle(servicehandle); return err == ERROR_SERVICE_DOES_NOT_EXIST; } + +bool WindowsSplitTunnel::isRunning() { return getState() == STATE_RUNNING; } +QString WindowsSplitTunnel::stateString() { + switch (getState()) { + case STATE_UNKNOWN: + return "STATE_UNKNOWN"; + case STATE_NONE: + return "STATE_NONE"; + case STATE_STARTED: + return "STATE_STARTED"; + case STATE_INITIALIZED: + return "STATE_INITIALIZED"; + case STATE_READY: + return "STATE_READY"; + case STATE_RUNNING: + return "STATE_RUNNING"; + case STATE_ZOMBIE: + return "STATE_ZOMBIE"; + break; + } + return {}; +} diff --git a/client/platforms/windows/daemon/windowssplittunnel.h b/client/platforms/windows/daemon/windowssplittunnel.h index 466036d6..85c827f6 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.h +++ b/client/platforms/windows/daemon/windowssplittunnel.h @@ -8,6 +8,7 @@ #include #include #include +#include // Note: the ws2tcpip.h import must come before the others. // clang-format off @@ -18,160 +19,78 @@ #include #include -// States for GetState -enum DRIVER_STATE { - STATE_UNKNOWN = -1, - STATE_NONE = 0, - STATE_STARTED = 1, - STATE_INITIALIZED = 2, - STATE_READY = 3, - STATE_RUNNING = 4, - STATE_ZOMBIE = 5, -}; +class WindowsFirewall; -#ifndef CTL_CODE - -# define FILE_ANY_ACCESS 0x0000 - -# define METHOD_BUFFERED 0 -# define METHOD_IN_DIRECT 1 -# define METHOD_NEITHER 3 - -# define CTL_CODE(DeviceType, Function, Method, Access) \ - (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)) -#endif - -// Known ControlCodes -#define IOCTL_INITIALIZE CTL_CODE(0x8000, 1, METHOD_NEITHER, FILE_ANY_ACCESS) - -#define IOCTL_DEQUEUE_EVENT \ - CTL_CODE(0x8000, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_REGISTER_PROCESSES \ - CTL_CODE(0x8000, 3, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_REGISTER_IP_ADDRESSES \ - CTL_CODE(0x8000, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_GET_IP_ADDRESSES \ - CTL_CODE(0x8000, 5, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_SET_CONFIGURATION \ - CTL_CODE(0x8000, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_GET_CONFIGURATION \ - CTL_CODE(0x8000, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_CLEAR_CONFIGURATION \ - CTL_CODE(0x8000, 8, METHOD_NEITHER, FILE_ANY_ACCESS) - -#define IOCTL_GET_STATE CTL_CODE(0x8000, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_QUERY_PROCESS \ - CTL_CODE(0x8000, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_ST_RESET CTL_CODE(0x8000, 11, METHOD_NEITHER, FILE_ANY_ACCESS) - -// Driver Configuration structures - -typedef struct { - // Offset into buffer region that follows all entries. - // The image name uses the device path. - SIZE_T ImageNameOffset; - // Length of the String - USHORT ImageNameLength; -} CONFIGURATION_ENTRY; - -typedef struct { - // Number of entries immediately following the header. - SIZE_T NumEntries; - - // Total byte length: header + entries + string buffer. - SIZE_T TotalLength; -} CONFIGURATION_HEADER; - -// Used to Configure Which IP is network/vpn -typedef struct { - IN_ADDR TunnelIpv4; - IN_ADDR InternetIpv4; - - IN6_ADDR TunnelIpv6; - IN6_ADDR InternetIpv6; -} IP_ADDRESSES_CONFIG; - -// Used to Define Which Processes are alive on activation -typedef struct { - SIZE_T NumEntries; - SIZE_T TotalLength; -} PROCESS_DISCOVERY_HEADER; - -typedef struct { - HANDLE ProcessId; - HANDLE ParentProcessId; - - SIZE_T ImageNameOffset; - USHORT ImageNameLength; -} PROCESS_DISCOVERY_ENTRY; - -typedef struct { - DWORD ProcessId; - DWORD ParentProcessId; - FILETIME CreationTime; - std::wstring DevicePath; -} ProcessInfo; - -class WindowsSplitTunnel final : public QObject { - Q_OBJECT - Q_DISABLE_COPY_MOVE(WindowsSplitTunnel) +class WindowsSplitTunnel final { public: - explicit WindowsSplitTunnel(QObject* parent); + /** + * @brief Installs and Initializes the Split Tunnel Driver. + * + * @param fw - + * @return std::unique_ptr - Is null on failure. + */ + static std::unique_ptr create(WindowsFirewall* fw); + + /** + * @brief Construct a new Windows Split Tunnel object + * + * @param driverIO - The Handle to the Driver's IO file, it assumes the driver + * is in STATE_INITIALIZED and the Firewall has been setup. + * Prefer using create() to get to this state. + */ + WindowsSplitTunnel(HANDLE driverIO); + /** + * @brief Destroy the Windows Split Tunnel object and uninstalls the Driver. + */ ~WindowsSplitTunnel(); // void excludeApps(const QStringList& paths); // Excludes an Application from the VPN - void setRules(const QStringList& appPaths); + bool excludeApps(const QStringList& appPaths); // Fetches and Pushed needed info to move to engaged mode - void start(int inetAdapterIndex, int vpnAdapterIndex = 0); + bool start(int inetAdapterIndex, int vpnAdapterIndex = 0); // Deletes Rules and puts the driver into passive mode void stop(); - // Resets the Whole Driver - void reset(); - // Just close connection, leave state as is - void close(); + // Returns true if the split-tunnel driver is now up and running. + bool isRunning(); + static bool detectConflict(); + + // States for GetState + enum DRIVER_STATE { + STATE_UNKNOWN = -1, + STATE_NONE = 0, + STATE_STARTED = 1, + STATE_INITIALIZED = 2, + STATE_READY = 3, + STATE_RUNNING = 4, + STATE_ZOMBIE = 5, + }; + + private: // Installes the Kernel Driver as Driver Service static SC_HANDLE installDriver(); static bool uninstallDriver(); static bool isInstalled(); - static bool detectConflict(); + static bool initDriver(HANDLE driverIO); + static DRIVER_STATE getState(HANDLE driverIO); + static bool resetDriver(HANDLE driverIO); - private slots: - void initDriver(); - - private: HANDLE m_driver = INVALID_HANDLE_VALUE; - constexpr static const auto DRIVER_SYMLINK = L"\\\\.\\MULLVADSPLITTUNNEL"; - constexpr static const auto DRIVER_FILENAME = "mullvad-split-tunnel.sys"; - constexpr static const auto DRIVER_SERVICE_NAME = L"AmneziaVPNSplitTunnel"; - constexpr static const auto MV_SERVICE_NAME = L"MullvadVPN"; DRIVER_STATE getState(); - - int m_tries; - // Initializes the WFP Sublayer - bool initSublayer(); + QString stateString(); // Generates a Configuration for Each APP std::vector generateAppConfiguration(const QStringList& appPaths); // Generates a Configuration which IP's are VPN and which network - std::vector generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0); + std::vector generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0); std::vector generateProcessBlob(); - void getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6); + [[nodiscard]] bool getAddress(int adapterIndex, IN_ADDR* out_ipv4, + IN6_ADDR* out_ipv6); // Collects info about an Opened Process - ProcessInfo getProcessInfo(HANDLE process, - const PROCESSENTRY32W& processMeta); // Converts a path to a Dos Path: // e.g C:/a.exe -> /harddisk0/a.exe diff --git a/client/platforms/windows/daemon/wireguardutilswindows.cpp b/client/platforms/windows/daemon/wireguardutilswindows.cpp index a68551d7..0823b9d7 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.cpp +++ b/client/platforms/windows/daemon/wireguardutilswindows.cpp @@ -24,8 +24,20 @@ namespace { Logger logger("WireguardUtilsWindows"); }; // namespace -WireguardUtilsWindows::WireguardUtilsWindows(QObject* parent) - : WireguardUtils(parent), m_tunnel(this), m_routeMonitor(this) { +std::unique_ptr WireguardUtilsWindows::create( + WindowsFirewall* fw, QObject* parent) { + if (!fw) { + logger.error() << "WireguardUtilsWindows::create: no wfp handle"; + return {}; + } + + // Can't use make_unique here as the Constructor is private :( + auto utils = new WireguardUtilsWindows(parent, fw); + return std::unique_ptr(utils); +} + +WireguardUtilsWindows::WireguardUtilsWindows(QObject* parent, WindowsFirewall* fw) + : WireguardUtils(parent), m_tunnel(this), m_firewall(fw) { MZ_COUNT_CTOR(WireguardUtilsWindows); logger.debug() << "WireguardUtilsWindows created."; @@ -114,13 +126,13 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) { return false; } m_luid = luid.Value; - m_routeMonitor.setLuid(luid.Value); + m_routeMonitor = new WindowsRouteMonitor(luid.Value, this); if (config.m_killSwitchEnabled) { // Enable the windows firewall NET_IFINDEX ifindex; ConvertInterfaceLuidToIndex(&luid, &ifindex); - WindowsFirewall::instance()->enableKillSwitch(ifindex); + m_firewall->enableInterface(ifindex); } logger.debug() << "Registration completed"; @@ -128,7 +140,11 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) { } bool WireguardUtilsWindows::deleteInterface() { - WindowsFirewall::instance()->disableKillSwitch(); + if (m_routeMonitor) { + m_routeMonitor->deleteLater(); + } + + m_firewall->disableKillSwitch(); m_tunnel.stop(); return true; } @@ -141,7 +157,7 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) { if (config.m_killSwitchEnabled) { // Enable the windows firewall for this peer. - WindowsFirewall::instance()->enablePeerTraffic(config); + m_firewall->enablePeerTraffic(config); } logger.debug() << "Configuring peer" << publicKey.toHex() << "via" << config.m_serverIpv4AddrIn; @@ -171,9 +187,9 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) { } // Exclude the server address, except for multihop exit servers. - if (config.m_hopType != InterfaceConfig::MultiHopExit) { - m_routeMonitor.addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); - m_routeMonitor.addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) { + m_routeMonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_routeMonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); } QString reply = m_tunnel.uapiCommand(message); @@ -186,13 +202,13 @@ bool WireguardUtilsWindows::deletePeer(const InterfaceConfig& config) { QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); // Clear exclustion routes for this peer. - if (config.m_hopType != InterfaceConfig::MultiHopExit) { - m_routeMonitor.deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); - m_routeMonitor.deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) { + m_routeMonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_routeMonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); } // Disable the windows firewall for this peer. - WindowsFirewall::instance()->disablePeerTraffic(config.m_serverPublicKey); + m_firewall->disablePeerTraffic(config.m_serverPublicKey); QString message; QTextStream out(&message); @@ -238,6 +254,13 @@ void WireguardUtilsWindows::buildMibForwardRow(const IPAddress& prefix, } bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { + if (m_routeMonitor && (prefix.prefixLength() == 0)) { + // If we are setting up a default route, instruct the route monitor to + // capture traffic to all non-excluded destinations + m_routeMonitor->setDetaultRouteCapture(true); + } + // Build the route + MIB_IPFORWARD_ROW2 entry; buildMibForwardRow(prefix, &entry); @@ -248,13 +271,19 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { } if (result != NO_ERROR) { logger.error() << "Failed to create route to" - << logger.sensitive(prefix.toString()) + << prefix.toString() << "result:" << result; } return result == NO_ERROR; } bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { + if (m_routeMonitor && (prefix.prefixLength() == 0)) { + // Deactivate the route capture feature. + m_routeMonitor->setDetaultRouteCapture(false); + } + // Build the route + MIB_IPFORWARD_ROW2 entry; buildMibForwardRow(prefix, &entry); @@ -265,16 +294,35 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { } if (result != NO_ERROR) { logger.error() << "Failed to delete route to" - << logger.sensitive(prefix.toString()) + << prefix.toString() << "result:" << result; } return result == NO_ERROR; } bool WireguardUtilsWindows::addExclusionRoute(const IPAddress& prefix) { - return m_routeMonitor.addExclusionRoute(prefix); + return m_routeMonitor->addExclusionRoute(prefix); } bool WireguardUtilsWindows::deleteExclusionRoute(const IPAddress& prefix) { - return m_routeMonitor.deleteExclusionRoute(prefix); + return m_routeMonitor->deleteExclusionRoute(prefix); +} + +bool WireguardUtilsWindows::excludeLocalNetworks( + const QList& addresses) { + // If the interface isn't up then something went horribly wrong. + Q_ASSERT(m_routeMonitor); + // For each destination - attempt to exclude it from the VPN tunnel. + bool result = true; + for (const IPAddress& prefix : addresses) { + if (!m_routeMonitor->addExclusionRoute(prefix)) { + result = false; + } + } + // Permit LAN traffic through the firewall. + if (!m_firewall->enableLanBypass(addresses)) { + result = false; + } + + return result; } diff --git a/client/platforms/windows/daemon/wireguardutilswindows.h b/client/platforms/windows/daemon/wireguardutilswindows.h index 4fd67fad..276966b4 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.h +++ b/client/platforms/windows/daemon/wireguardutilswindows.h @@ -9,16 +9,21 @@ #include #include +#include #include "daemon/wireguardutils.h" #include "windowsroutemonitor.h" #include "windowstunnelservice.h" +class WindowsFirewall; +class WindowsRouteMonitor; + class WireguardUtilsWindows final : public WireguardUtils { Q_OBJECT public: - WireguardUtilsWindows(QObject* parent); + static std::unique_ptr create(WindowsFirewall* fw, + QObject* parent); ~WireguardUtilsWindows(); bool interfaceExists() override { return m_tunnel.isRunning(); } @@ -39,15 +44,19 @@ class WireguardUtilsWindows final : public WireguardUtils { bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + bool WireguardUtilsWindows::excludeLocalNetworks(const QList& addresses) override; + signals: void backendFailure(); private: + WireguardUtilsWindows(QObject* parent, WindowsFirewall* fw); void buildMibForwardRow(const IPAddress& prefix, void* row); quint64 m_luid = 0; WindowsTunnelService m_tunnel; - WindowsRouteMonitor m_routeMonitor; + QPointer m_routeMonitor; + QPointer m_firewall; }; #endif // WIREGUARDUTILSWINDOWS_H diff --git a/client/platforms/windows/windowscommons.cpp b/client/platforms/windows/windowscommons.cpp index c0a14dda..4c0d8176 100644 --- a/client/platforms/windows/windowscommons.cpp +++ b/client/platforms/windows/windowscommons.cpp @@ -21,7 +21,7 @@ #include "platforms/windows/windowsutils.h" constexpr const char* VPN_NAME = "AmneziaVPN"; -constexpr const char* WIREGUARD_DIR = "WireGuard"; +constexpr const char* WIREGUARD_DIR = "AmneziaWG"; constexpr const char* DATA_DIR = "Data"; namespace { diff --git a/client/platforms/windows/windowsservicemanager.cpp b/client/platforms/windows/windowsservicemanager.cpp index 3a334224..d5a21170 100644 --- a/client/platforms/windows/windowsservicemanager.cpp +++ b/client/platforms/windows/windowsservicemanager.cpp @@ -4,6 +4,7 @@ #include "windowsservicemanager.h" +#include #include #include "Windows.h" @@ -16,35 +17,44 @@ namespace { Logger logger("WindowsServiceManager"); } -WindowsServiceManager::WindowsServiceManager(LPCWSTR serviceName) { +WindowsServiceManager::WindowsServiceManager(SC_HANDLE serviceManager, + SC_HANDLE service) + : QObject(qApp), m_serviceManager(serviceManager), m_service(service) { + m_timer.setSingleShot(false); +} + +std::unique_ptr WindowsServiceManager::open( + const QString serviceName) { + LPCWSTR service = (const wchar_t*)serviceName.utf16(); + DWORD err = NULL; auto scm_rights = SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_QUERY_LOCK_STATUS | STANDARD_RIGHTS_READ; - m_serviceManager = OpenSCManager(NULL, // local computer - NULL, // servicesActive database - scm_rights); + auto manager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); err = GetLastError(); if (err != NULL) { logger.error() << " OpenSCManager failed code: " << err; - return; + return {}; } logger.debug() << "OpenSCManager access given - " << err; - logger.debug() << "Opening Service - " - << QString::fromWCharArray(serviceName); + logger.debug() << "Opening Service - " << serviceName; // Try to get an elevated handle - m_service = OpenService(m_serviceManager, // SCM database - serviceName, // name of service - (GENERIC_READ | SERVICE_START | SERVICE_STOP)); + auto serviceHandle = + OpenService(manager, // SCM database + service, // name of service + (GENERIC_READ | SERVICE_START | SERVICE_STOP)); err = GetLastError(); if (err != NULL) { + CloseServiceHandle(manager); WindowsUtils::windowsLog("OpenService failed"); - return; + return {}; } - m_has_access = true; - m_timer.setSingleShot(false); logger.debug() << "Service manager execute access granted"; + return std::make_unique(manager, serviceHandle); } WindowsServiceManager::~WindowsServiceManager() { @@ -85,10 +95,6 @@ bool WindowsServiceManager::startPolling(DWORD goal_state, int max_wait_sec) { SERVICE_STATUS_PROCESS WindowsServiceManager::getStatus() { SERVICE_STATUS_PROCESS serviceStatus; - if (!m_has_access) { - logger.debug() << "Need read access to get service state"; - return serviceStatus; - } DWORD dwBytesNeeded; // Contains missing bytes if struct is too small? QueryServiceStatusEx(m_service, // handle to service SC_STATUS_PROCESS_INFO, // information level @@ -119,10 +125,6 @@ bool WindowsServiceManager::startService() { } bool WindowsServiceManager::stopService() { - if (!m_has_access) { - logger.error() << "Need execute access to stop services"; - return false; - } auto state = getStatus().dwCurrentState; if (state != SERVICE_RUNNING && state != SERVICE_START_PENDING) { logger.warning() << ("Service stop not possible, as its not running"); diff --git a/client/platforms/windows/windowsservicemanager.h b/client/platforms/windows/windowsservicemanager.h index e0709309..31521245 100644 --- a/client/platforms/windows/windowsservicemanager.h +++ b/client/platforms/windows/windowsservicemanager.h @@ -12,7 +12,7 @@ #include "Winsvc.h" /** - * @brief The WindowsServiceManager provides controll over the MozillaVPNBroker + * @brief The WindowsServiceManager provides control over the a * service via SCM */ class WindowsServiceManager : public QObject { @@ -20,7 +20,10 @@ class WindowsServiceManager : public QObject { Q_DISABLE_COPY_MOVE(WindowsServiceManager) public: - WindowsServiceManager(LPCWSTR serviceName); + // Creates a WindowsServiceManager for the Named service. + // returns nullptr if + static std::unique_ptr open(const QString serviceName); + WindowsServiceManager(SC_HANDLE serviceManager, SC_HANDLE service); ~WindowsServiceManager(); // true if the Service is running @@ -45,8 +48,6 @@ class WindowsServiceManager : public QObject { // See // SERVICE_STOPPED,SERVICE_STOP_PENDING,SERVICE_START_PENDING,SERVICE_RUNNING SERVICE_STATUS_PROCESS getStatus(); - bool m_has_access = false; - LPWSTR m_serviceName; SC_HANDLE m_serviceManager; SC_HANDLE m_service; // Service handle with r/w priv. DWORD m_state_target; diff --git a/client/protocols/ikev2_vpn_protocol_windows.cpp b/client/protocols/ikev2_vpn_protocol_windows.cpp index 5c471e22..b4110f03 100644 --- a/client/protocols/ikev2_vpn_protocol_windows.cpp +++ b/client/protocols/ikev2_vpn_protocol_windows.cpp @@ -10,6 +10,7 @@ #include "ikev2_vpn_protocol_windows.h" #include "utilities.h" + static Ikev2Protocol* self = nullptr; static std::mutex rasDialFuncMutex; @@ -80,10 +81,10 @@ void Ikev2Protocol::newConnectionStateEventReceived(UINT unMsg, tagRASCONNSTATE case RASCS_AuthNotify: //qDebug()<<__FUNCTION__ << __LINE__; if (dwError != 0) { - //qDebug() << "have error" << dwError; + qDebug() << "have error" << dwError; setConnectionState(Vpn::ConnectionState::Disconnected); } else { - //qDebug() << "RASCS_AuthNotify but no error" << dwError; + qDebug() << "RASCS_AuthNotify but no error" << dwError; } break; case RASCS_AuthRetry: @@ -179,11 +180,13 @@ ErrorCode Ikev2Protocol::start() QByteArray cert = QByteArray::fromBase64(m_config[config_key::cert].toString().toUtf8()); setConnectionState(Vpn::ConnectionState::Connecting); - QTemporaryFile certFile; - certFile.setAutoRemove(false); - certFile.open(); - certFile.write(cert); - certFile.close(); + QTemporaryFile * certFile = new QTemporaryFile; + certFile->setAutoRemove(false); + certFile->open(); + QString m_filename = certFile->fileName(); + certFile->write(cert); + certFile->close(); + delete certFile; { auto certInstallProcess = IpcClient::CreatePrivilegedProcess(); @@ -193,19 +196,19 @@ ErrorCode Ikev2Protocol::start() return ErrorCode::AmneziaServiceConnectionFailed; } - certInstallProcess->waitForSource(1000); + certInstallProcess->waitForSource(); if (!certInstallProcess->isInitialized()) { qWarning() << "IpcProcess replica is not connected!"; setLastError(ErrorCode::AmneziaServiceConnectionFailed); return ErrorCode::AmneziaServiceConnectionFailed; } certInstallProcess->setProgram(PermittedProcess::CertUtil); - QStringList arguments({"-f" , "-importpfx", - "-p", m_config[config_key::password].toString(), - certFile.fileName(), "NoExport" - }); - certInstallProcess->setArguments(arguments); + QStringList arguments({"-f", "-importpfx", "-p", m_config[config_key::password].toString(), + QDir::toNativeSeparators(m_filename), "NoExport" + }); + + certInstallProcess->setArguments(arguments); certInstallProcess->start(); } // /* @@ -219,40 +222,40 @@ ErrorCode Ikev2Protocol::start() } { - { - if ( !create_new_vpn(tunnelName(), m_config[config_key::hostName].toString())){ - qDebug() <<"Can't create the VPN connect"; - } - } - } + { + if ( !create_new_vpn(tunnelName(), m_config[config_key::hostName].toString())){ + qDebug() <<"Can't create the VPN connect"; +} +} +} - { - auto adapterConfigProcess = new QProcess; +{ + QProcess adapterConfigProcess; + adapterConfigProcess.setProgram("powershell"); + QString arguments = QString("-command \"Set-VpnConnectionIPsecConfiguration\" " + "-ConnectionName '%1' " + "-AuthenticationTransformConstants GCMAES128 " + "-CipherTransformConstants GCMAES128 " + "-EncryptionMethod AES256 " + "-IntegrityCheckMethod SHA256 " + "-PfsGroup PFS2048 " + "-DHGroup Group14 " + "-PassThru -Force\"") + .arg(tunnelName()); - adapterConfigProcess->setProgram("powershell"); - QString arguments = QString("-command \"Set-VpnConnectionIPsecConfiguration\" " - "-ConnectionName '%1' " - "-AuthenticationTransformConstants GCMAES128 " - "-CipherTransformConstants GCMAES128 " - "-EncryptionMethod AES256 " - "-IntegrityCheckMethod SHA256 " - "-PfsGroup None " - "-DHGroup Group14 " - "-PassThru -Force\"") - .arg(tunnelName()); - adapterConfigProcess->setNativeArguments(arguments); + adapterConfigProcess.setNativeArguments(arguments); - adapterConfigProcess->start(); - adapterConfigProcess->waitForFinished(5000); + adapterConfigProcess.start(); + adapterConfigProcess.waitForFinished(5000); +} +//*/ +{ + if (!connect_to_vpn(tunnelName())) { + qDebug()<<"We can't connect to VPN"; } - //*/ - { - if (!connect_to_vpn(tunnelName())) { - qDebug()<<"We can't connect to VPN"; - } - } - //setConnectionState(Connecting); - return ErrorCode::NoError; +} +//setConnectionState(Connecting); +return ErrorCode::NoError; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bool Ikev2Protocol::create_new_vpn(const QString & vpn_name, @@ -299,6 +302,7 @@ bool Ikev2Protocol::connect_to_vpn(const QString & vpn_name){ auto ret = RasDial(NULL, NULL, &RasDialParams, 0, &RasDialFuncCallback, &hRasConn); + if (ret == ERROR_SUCCESS){ return true; } diff --git a/client/protocols/openvpnprotocol.cpp b/client/protocols/openvpnprotocol.cpp index 04a18327..4c2feb52 100644 --- a/client/protocols/openvpnprotocol.cpp +++ b/client/protocols/openvpnprotocol.cpp @@ -6,6 +6,7 @@ #include #include +#include "core/networkUtilities.h" #include "logger.h" #include "openvpnprotocol.h" #include "utilities.h" @@ -127,7 +128,6 @@ void OpenVpnProtocol::sendManagementCommand(const QString &command) uint OpenVpnProtocol::selectMgmtPort() { - for (int i = 0; i < 100; ++i) { quint32 port = QRandomGenerator::global()->generate(); port = (double)(65000 - 15001) * port / UINT32_MAX + 15001; @@ -137,7 +137,6 @@ uint OpenVpnProtocol::selectMgmtPort() if (ok) return port; } - return m_managementPort; } @@ -343,7 +342,8 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) } m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index()); m_configData.insert("vpnGateway", m_vpnGateway); - m_configData.insert("vpnServer", m_configData.value(amnezia::config_key::hostName).toString()); + m_configData.insert("vpnServer", + NetworkUtilities::getIPAddress(m_configData.value(amnezia::config_key::hostName).toString())); IpcClient::Interface()->enablePeerTraffic(m_configData); } } @@ -352,6 +352,8 @@ void OpenVpnProtocol::updateVpnGateway(const QString &line) #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) // killSwitch toggle if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { + m_configData.insert("vpnServer", + NetworkUtilities::getIPAddress(m_configData.value(amnezia::config_key::hostName).toString())); IpcClient::Interface()->enableKillSwitch(m_configData, 0); } #endif diff --git a/client/protocols/protocols_defs.cpp b/client/protocols/protocols_defs.cpp index 9be5a75f..ac5bb1ad 100644 --- a/client/protocols/protocols_defs.cpp +++ b/client/protocols/protocols_defs.cpp @@ -65,14 +65,14 @@ QString ProtocolProps::transportProtoToString(TransportProto proto, Proto p) QMap ProtocolProps::protocolHumanNames() { return { { Proto::OpenVpn, "OpenVPN" }, - { Proto::ShadowSocks, "ShadowSocks" }, + { Proto::ShadowSocks, "Shadowsocks" }, { Proto::Cloak, "Cloak" }, { Proto::WireGuard, "WireGuard" }, { Proto::Awg, "AmneziaWG" }, { Proto::Ikev2, "IKEv2" }, { Proto::L2tp, "L2TP" }, { Proto::Xray, "XRay" }, - { Proto::SSXray, "ShadowSocks"}, + { Proto::SSXray, "Shadowsocks"}, { Proto::TorWebSite, "Website in Tor network" }, @@ -154,6 +154,7 @@ bool ProtocolProps::defaultPortChangeable(Proto p) case Proto::Awg: return true; case Proto::Ikev2: return false; case Proto::L2tp: return false; + case Proto::Xray: return true; case Proto::TorWebSite: return false; case Proto::Dns: return false; diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h index 56be0d7d..865edae4 100644 --- a/client/protocols/protocols_defs.h +++ b/client/protocols/protocols_defs.h @@ -65,6 +65,7 @@ namespace amnezia constexpr char last_config[] = "last_config"; constexpr char isThirdPartyConfig[] = "isThirdPartyConfig"; + constexpr char isObfuscationEnabled[] = "isObfuscationEnabled"; constexpr char junkPacketCount[] = "Jc"; constexpr char junkPacketMinSize[] = "Jmin"; diff --git a/client/protocols/wireguardprotocol.cpp b/client/protocols/wireguardprotocol.cpp index 61b2e261..80579f16 100644 --- a/client/protocols/wireguardprotocol.cpp +++ b/client/protocols/wireguardprotocol.cpp @@ -4,9 +4,8 @@ #include #include -#include "logger.h" -#include "utilities.h" #include "wireguardprotocol.h" +#include "core/networkUtilities.h" #include "mozilla/localsocketcontroller.h" @@ -37,6 +36,12 @@ void WireguardProtocol::stop() ErrorCode WireguardProtocol::startMzImpl() { + QString protocolName = m_rawConfig.value("protocol").toString(); + QJsonObject vpnConfigData = m_rawConfig.value(protocolName + "_config_data").toObject(); + vpnConfigData[config_key::hostName] = NetworkUtilities::getIPAddress(vpnConfigData.value(config_key::hostName).toString()); + m_rawConfig.insert(protocolName + "_config_data", vpnConfigData); + m_rawConfig[config_key::hostName] = NetworkUtilities::getIPAddress(m_rawConfig[config_key::hostName].toString()); + m_impl->activate(m_rawConfig); return ErrorCode::NoError; } diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp old mode 100644 new mode 100755 index 15106c51..faad8e94 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -1,29 +1,26 @@ #include "xrayprotocol.h" -#include "utilities.h" -#include "containers/containers_defs.h" -#include "core/networkUtilities.h" - #include #include #include #include +#include "core/networkUtilities.h" +#include "utilities.h" -XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent): - VpnProtocol(configuration, parent) +XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent) { readXrayConfiguration(configuration); m_routeGateway = NetworkUtilities::getGatewayAndIface(); m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr; m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr; + m_t2sProcess = IpcClient::InterfaceTun2Socks(); } XrayProtocol::~XrayProtocol() { + qDebug() << "XrayProtocol::~XrayProtocol()"; XrayProtocol::stop(); - QThread::msleep(200); - m_xrayProcess.close(); } ErrorCode XrayProtocol::start() @@ -35,25 +32,27 @@ ErrorCode XrayProtocol::start() return lastError(); } - if (Utils::processIsRunning(Utils::executable(xrayExecPath(), true))) { - Utils::killProcessByName(Utils::executable(xrayExecPath(), true)); - } - #ifdef QT_DEBUG m_xrayCfgFile.setAutoRemove(false); #endif m_xrayCfgFile.open(); - m_xrayCfgFile.write(QJsonDocument(m_xrayConfig).toJson()); + QString config = QJsonDocument(m_xrayConfig).toJson(); + config.replace(m_remoteHost, m_remoteAddress); + m_xrayCfgFile.write(config.toUtf8()); m_xrayCfgFile.close(); QStringList args = QStringList() << "-c" << m_xrayCfgFile.fileName() << "-format=json"; - qDebug().noquote() << "XrayProtocol::start()" - << xrayExecPath() << args.join(" "); + qDebug().noquote() << "XrayProtocol::start()" << xrayExecPath() << args.join(" "); m_xrayProcess.setProcessChannelMode(QProcess::MergedChannels); - m_xrayProcess.setProgram(xrayExecPath()); + + if (Utils::processIsRunning(Utils::executable("xray", false))) { + qDebug().noquote() << "kill previos xray"; + Utils::killProcessByName(Utils::executable("xray", false)); + } + m_xrayProcess.setArguments(args); connect(&m_xrayProcess, &QProcess::readyReadStandardOutput, this, [this]() { @@ -62,18 +61,15 @@ ErrorCode XrayProtocol::start() #endif }); - connect(&m_xrayProcess, QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { - qDebug().noquote() << "XrayProtocol finished, exitCode, exiStatus" << exitCode << exitStatus; - setConnectionState(Vpn::ConnectionState::Disconnected); - if (exitStatus != QProcess::NormalExit) { - emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); - stop(); - } - if (exitCode != 0) { - emit protocolError(amnezia::ErrorCode::InternalError); - stop(); - } - }); + connect(&m_xrayProcess, QOverload::of(&QProcess::finished), this, + [this](int exitCode, QProcess::ExitStatus exitStatus) { + qDebug().noquote() << "XrayProtocol finished, exitCode, exitStatus" << exitCode << exitStatus; + setConnectionState(Vpn::ConnectionState::Disconnected); + if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { + emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); + emit setConnectionState(Vpn::ConnectionState::Error); + } + }); m_xrayProcess.start(); m_xrayProcess.waitForStarted(); @@ -82,70 +78,36 @@ ErrorCode XrayProtocol::start() setConnectionState(Vpn::ConnectionState::Connecting); QThread::msleep(1000); return startTun2Sock(); - } - else return ErrorCode::XrayExecutableMissing; + } else + return ErrorCode::XrayExecutableMissing; } - ErrorCode XrayProtocol::startTun2Sock() { - if (!QFileInfo::exists(Utils::tun2socksPath())) { - setLastError(ErrorCode::Tun2SockExecutableMissing); - return lastError(); - } + m_t2sProcess->start(); - m_t2sProcess = IpcClient::CreatePrivilegedProcess(); - - if (!m_t2sProcess) { - setLastError(ErrorCode::AmneziaServiceConnectionFailed); - return ErrorCode::AmneziaServiceConnectionFailed; - } - - m_t2sProcess->waitForSource(1000); - if (!m_t2sProcess->isInitialized()) { - qWarning() << "IpcProcess replica is not connected!"; - setLastError(ErrorCode::AmneziaServiceConnectionFailed); - return ErrorCode::AmneziaServiceConnectionFailed; - } - - QString XrayConStr = "socks5://127.0.0.1:" + QString::number(m_localPort); - - m_t2sProcess->setProgram(PermittedProcess::Tun2Socks); #ifdef Q_OS_WIN m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress))); - QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up", - QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255").arg(amnezia::protocols::xray::defaultLocalAddr)}); #endif -#ifdef Q_OS_LINUX - QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr}); -#endif -#ifdef Q_OS_MAC - QStringList arguments({"-device", "utun22", "-proxy", XrayConStr}); -#endif - m_t2sProcess->setArguments(arguments); - qDebug() << arguments.join(" "); - connect(m_t2sProcess.data(), &PrivilegedProcess::errorOccurred, - [&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; }); + connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this, + [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); - connect(m_t2sProcess.data(), &PrivilegedProcess::stateChanged, - [&](QProcess::ProcessState newState) { - qDebug() << "PrivilegedProcess stateChanged" << newState; - if (newState == QProcess::Running) - { + connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) { + qDebug() << "PrivilegedProcess setConnectionState " << vpnState; + if (vpnState == Vpn::ConnectionState::Connected) { setConnectionState(Vpn::ConnectionState::Connecting); QList dnsAddr; dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns1).toString())); dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns2).toString())); - +#ifdef Q_OS_WIN + QThread::msleep(8000); +#endif #ifdef Q_OS_MACOS QThread::msleep(5000); IpcClient::Interface()->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr); IpcClient::Interface()->updateResolvers("utun22", dnsAddr); #endif -#ifdef Q_OS_WINDOWS - QThread::msleep(15000); -#endif #ifdef Q_OS_LINUX QThread::msleep(1000); IpcClient::Interface()->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); @@ -154,10 +116,11 @@ ErrorCode XrayProtocol::startTun2Sock() #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) // killSwitch toggle if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { + m_configData.insert("vpnServer", m_remoteAddress); IpcClient::Interface()->enableKillSwitch(m_configData, 0); } #endif - if (m_routeMode == 0) { + if (m_routeMode == Settings::RouteMode::VpnAllSites) { IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "0.0.0.0/1"); IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "128.0.0.0/1"); IpcClient::Interface()->routeAddList(m_routeGateway, QStringList() << m_remoteAddress); @@ -167,8 +130,7 @@ ErrorCode XrayProtocol::startTun2Sock() IpcClient::Interface()->updateResolvers("tun2", dnsAddr); QList netInterfaces = QNetworkInterface::allInterfaces(); for (int i = 0; i < netInterfaces.size(); i++) { - for (int j=0; j < netInterfaces.at(i).addressEntries().size(); j++) - { + for (int j = 0; j < netInterfaces.at(i).addressEntries().size(); j++) { // killSwitch toggle if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { @@ -184,21 +146,15 @@ ErrorCode XrayProtocol::startTun2Sock() #endif setConnectionState(Vpn::ConnectionState::Connected); } - }); - - #if !defined(Q_OS_MACOS) - connect(m_t2sProcess.data(), &PrivilegedProcess::finished, this, - [&]() { - setConnectionState(Vpn::ConnectionState::Disconnected); - IpcClient::Interface()->deleteTun("tun2"); - IpcClient::Interface()->StartRoutingIpv6(); - IpcClient::Interface()->clearSavedRoutes(); - }); + if (vpnState == Vpn::ConnectionState::Disconnected) { + setConnectionState(Vpn::ConnectionState::Disconnected); + IpcClient::Interface()->deleteTun("tun2"); + IpcClient::Interface()->StartRoutingIpv6(); + IpcClient::Interface()->clearSavedRoutes(); + } #endif - - m_t2sProcess->start(); - + }); return ErrorCode::NoError; } @@ -210,14 +166,14 @@ void XrayProtocol::stop() IpcClient::Interface()->StartRoutingIpv6(); #endif qDebug() << "XrayProtocol::stop()"; - m_xrayProcess.terminate(); + m_xrayProcess.disconnect(); + m_xrayProcess.kill(); + m_xrayProcess.waitForFinished(3000); if (m_t2sProcess) { - m_t2sProcess->close(); + m_t2sProcess->stop(); } -#ifdef Q_OS_WIN - Utils::signalCtrl(m_xrayProcess.processId(), CTRL_C_EVENT); -#endif + setConnectionState(Vpn::ConnectionState::Disconnected); } QString XrayProtocol::xrayExecPath() @@ -238,8 +194,9 @@ void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration) } m_xrayConfig = xrayConfiguration; m_localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort).toInt(); - m_remoteAddress = configuration.value(amnezia::config_key::hostName).toString(); - m_routeMode = configuration.value(amnezia::config_key::splitTunnelType).toInt(); + m_remoteHost = configuration.value(amnezia::config_key::hostName).toString(); + m_remoteAddress = NetworkUtilities::getIPAddress(m_remoteHost); + m_routeMode = static_cast(configuration.value(amnezia::config_key::splitTunnelType).toInt()); m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString(); m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString(); } diff --git a/client/protocols/xrayprotocol.h b/client/protocols/xrayprotocol.h index 8df2afb2..c79ef608 100644 --- a/client/protocols/xrayprotocol.h +++ b/client/protocols/xrayprotocol.h @@ -1,14 +1,16 @@ #ifndef XRAYPROTOCOL_H #define XRAYPROTOCOL_H -#include "openvpnprotocol.h" #include "QProcess" + #include "containers/containers_defs.h" +#include "openvpnprotocol.h" +#include "settings.h" class XrayProtocol : public VpnProtocol { public: - XrayProtocol(const QJsonObject& configuration, QObject* parent = nullptr); + XrayProtocol(const QJsonObject &configuration, QObject *parent = nullptr); virtual ~XrayProtocol() override; ErrorCode start() override; @@ -24,16 +26,18 @@ protected: private: static QString xrayExecPath(); static QString tun2SocksExecPath(); + private: int m_localPort; + QString m_remoteHost; QString m_remoteAddress; - int m_routeMode; + Settings::RouteMode m_routeMode; QJsonObject m_configData; QString m_primaryDNS; QString m_secondaryDNS; #ifndef Q_OS_IOS QProcess m_xrayProcess; - QSharedPointer m_t2sProcess; + QSharedPointer m_t2sProcess; #endif QTemporaryFile m_xrayCfgFile; }; diff --git a/client/resources.qrc b/client/resources.qrc index 84296462..16071da0 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -1,248 +1,485 @@ - images/close.png - images/settings.png - images/favorites_disabled.png - images/favorites_enabled.png - images/favorites_hover.png - images/download.png - images/upload.png + fonts/pt-root-ui_vf.ttf + images/amneziaBigLogo.png + images/AmneziaVPN.png + images/controls/alert-circle.svg + images/controls/amnezia.svg + images/controls/app.svg + images/controls/archive-restore.svg + images/controls/arrow-left.svg + images/controls/arrow-right.svg + images/controls/bug.svg + images/controls/check.svg + images/controls/chevron-down.svg + images/controls/chevron-right.svg + images/controls/chevron-up.svg + images/controls/close.svg + images/controls/copy.svg + images/controls/delete.svg + images/controls/download.svg + images/controls/edit-3.svg + images/controls/eye-off.svg + images/controls/eye.svg + images/controls/external-link.svg + images/controls/file-check-2.svg + images/controls/file-cog-2.svg + images/controls/folder-open.svg + images/controls/folder-search-2.svg + images/controls/gauge.svg + images/controls/github.svg + images/controls/help-circle.svg + images/controls/history.svg + images/controls/home.svg + images/controls/info.svg + images/controls/mail.svg + images/controls/map-pin.svg + images/controls/more-vertical.svg + images/controls/plus.svg + images/controls/qr-code.svg + images/controls/radio-button-inner-circle-pressed.png + images/controls/radio-button-inner-circle.png + images/controls/radio-button-pressed.svg + images/controls/radio-button.svg + images/controls/radio.svg + images/controls/refresh-cw.svg + images/controls/save.svg + images/controls/scan-line.svg + images/controls/search.svg + images/controls/server.svg + images/controls/settings-2.svg + images/controls/settings.svg + images/controls/share-2.svg + images/controls/split-tunneling.svg + images/controls/tag.svg + images/controls/telegram.svg + images/controls/text-cursor.svg + images/controls/trash.svg + images/controls/x-circle.svg images/tray/active.png images/tray/default.png images/tray/error.png - images/arrow_left.png - fonts/Lato-Black.ttf - fonts/Lato-BlackItalic.ttf - fonts/Lato-Bold.ttf - fonts/Lato-BoldItalic.ttf - fonts/Lato-Italic.ttf - fonts/Lato-Light.ttf - fonts/Lato-LightItalic.ttf - fonts/Lato-Regular.ttf - fonts/Lato-Thin.ttf - fonts/Lato-ThinItalic.ttf - images/AmneziaVPN.png - images/share.png - server_scripts/remove_container.sh - server_scripts/setup_host_firewall.sh - server_scripts/openvpn_cloak/Dockerfile + server_scripts/awg/configure_container.sh + server_scripts/awg/Dockerfile + server_scripts/awg/run_container.sh + server_scripts/awg/start.sh + server_scripts/awg/template.conf + server_scripts/build_container.sh + server_scripts/check_connection.sh + server_scripts/check_server_is_busy.sh + server_scripts/check_user_in_sudo.sh + server_scripts/dns/configure_container.sh + server_scripts/dns/Dockerfile + server_scripts/dns/run_container.sh + server_scripts/install_docker.sh + server_scripts/ipsec/configure_container.sh + server_scripts/ipsec/Dockerfile + server_scripts/ipsec/mobileconfig.plist + server_scripts/ipsec/run_container.sh + server_scripts/ipsec/start.sh + server_scripts/ipsec/strongswan.profile server_scripts/openvpn_cloak/configure_container.sh + server_scripts/openvpn_cloak/Dockerfile + server_scripts/openvpn_cloak/run_container.sh server_scripts/openvpn_cloak/start.sh server_scripts/openvpn_cloak/template.ovpn - server_scripts/install_docker.sh - server_scripts/build_container.sh - server_scripts/prepare_host.sh - images/check.png - images/uncheck.png - images/settings_grey.png - server_scripts/check_connection.sh - server_scripts/remove_all_containers.sh - server_scripts/openvpn_cloak/run_container.sh - server_scripts/openvpn/configure_container.sh - server_scripts/openvpn/run_container.sh - server_scripts/openvpn/template.ovpn - server_scripts/openvpn/Dockerfile - server_scripts/openvpn/start.sh server_scripts/openvpn_shadowsocks/configure_container.sh server_scripts/openvpn_shadowsocks/Dockerfile server_scripts/openvpn_shadowsocks/run_container.sh server_scripts/openvpn_shadowsocks/start.sh server_scripts/openvpn_shadowsocks/template.ovpn - images/folder.png + server_scripts/openvpn/configure_container.sh + server_scripts/openvpn/Dockerfile + server_scripts/openvpn/run_container.sh + server_scripts/openvpn/start.sh + server_scripts/openvpn/template.ovpn + server_scripts/prepare_host.sh + server_scripts/remove_all_containers.sh + server_scripts/remove_container.sh + server_scripts/setup_host_firewall.sh + server_scripts/sftp/configure_container.sh + server_scripts/sftp/Dockerfile + server_scripts/sftp/run_container.sh + server_scripts/socks5_proxy/configure_container.sh + server_scripts/socks5_proxy/Dockerfile + server_scripts/socks5_proxy/run_container.sh + server_scripts/socks5_proxy/start.sh + server_scripts/website_tor/configure_container.sh + server_scripts/website_tor/Dockerfile + server_scripts/website_tor/run_container.sh server_scripts/wireguard/configure_container.sh server_scripts/wireguard/Dockerfile server_scripts/wireguard/run_container.sh server_scripts/wireguard/start.sh server_scripts/wireguard/template.conf - server_scripts/website_tor/configure_container.sh - server_scripts/website_tor/run_container.sh - ui/qml/Config/GlobalConfig.qml - ui/qml/Config/qmldir - server_scripts/check_server_is_busy.sh - server_scripts/dns/configure_container.sh - server_scripts/dns/Dockerfile - server_scripts/dns/run_container.sh - server_scripts/sftp/configure_container.sh - server_scripts/sftp/Dockerfile - server_scripts/sftp/run_container.sh - server_scripts/ipsec/configure_container.sh - server_scripts/ipsec/Dockerfile - server_scripts/ipsec/run_container.sh - server_scripts/ipsec/start.sh - server_scripts/ipsec/mobileconfig.plist - server_scripts/ipsec/strongswan.profile - images/background_connected.png - images/background_connected@2x.png - images/delete.png - images/animation.gif - images/connected.png - images/disconnected.png - images/svg/gpp_good_black_24dp.svg - images/svg/gpp_maybe_black_24dp.svg - images/svg/close_black_24dp.svg - images/svg/delete_black_24dp.svg - images/svg/done_black_24dp.svg - images/svg/format_list_bulleted_black_24dp.svg - images/svg/logout_black_24dp.svg - images/svg/miscellaneous_services_black_24dp.svg - images/svg/refresh_black_24dp.svg - images/svg/settings_black_24dp.svg - images/svg/share_black_24dp.svg - images/svg/vpn_key_black_24dp.svg - images/svg/control_point_black_24dp.svg - images/svg/settings_suggest_black_24dp.svg - server_scripts/website_tor/Dockerfile - server_scripts/check_user_in_sudo.sh - ui/qml/Controls2/BasicButtonType.qml - ui/qml/Controls2/TextFieldWithHeaderType.qml - fonts/pt-root-ui_vf.ttf - ui/qml/Controls2/LabelWithButtonType.qml - images/controls/arrow-right.svg - images/controls/chevron-right.svg - ui/qml/Controls2/ImageButtonType.qml - ui/qml/Controls2/CardType.qml - ui/qml/Controls2/CheckBoxType.qml - images/controls/check.svg - ui/qml/Controls2/DropDownType.qml - ui/qml/Pages2/PageSetupWizardStart.qml - ui/qml/main2.qml - images/amneziaBigLogo.png - images/amneziaBigLogo.svg - ui/qml/Controls2/FlickableType.qml - ui/qml/Pages2/PageSetupWizardCredentials.qml - ui/qml/Controls2/HeaderType.qml - images/controls/arrow-left.svg - ui/qml/Pages2/PageSetupWizardProtocols.qml - ui/qml/Pages2/PageSetupWizardEasy.qml - images/controls/chevron-down.svg - images/controls/chevron-up.svg - ui/qml/Controls2/TextTypes/ParagraphTextType.qml - ui/qml/Controls2/TextTypes/Header2TextType.qml - ui/qml/Controls2/HorizontalRadioButton.qml - ui/qml/Controls2/VerticalRadioButton.qml - ui/qml/Controls2/SwitcherType.qml - ui/qml/Controls2/TabButtonType.qml - ui/qml/Pages2/PageSetupWizardProtocolSettings.qml - ui/qml/Pages2/PageSetupWizardInstalling.qml - ui/qml/Pages2/PageSetupWizardConfigSource.qml - images/controls/folder-open.svg - images/controls/qr-code.svg - images/controls/text-cursor.svg - ui/qml/Pages2/PageSetupWizardTextKey.qml - ui/qml/Pages2/PageStart.qml - ui/qml/Controls2/TabImageButtonType.qml - images/controls/home.svg - images/controls/settings-2.svg - images/controls/share-2.svg - ui/qml/Pages2/PageHome.qml - ui/qml/Pages2/PageSettingsServersList.qml - ui/qml/Pages2/PageShare.qml - ui/qml/Controls2/TextTypes/Header1TextType.qml - ui/qml/Controls2/TextTypes/LabelTextType.qml - ui/qml/Controls2/TextTypes/ButtonTextType.qml - ui/qml/Controls2/Header2Type.qml - images/controls/plus.svg - ui/qml/Components/ConnectButton.qml - images/connectionProgress.svg - images/connectionOff.svg - images/connectionOn.svg - images/controls/download.svg - ui/qml/Controls2/ProgressBarType.qml - ui/qml/Components/ConnectionTypeSelectionDrawer.qml - ui/qml/Components/HomeContainersListView.qml - ui/qml/Controls2/TextTypes/CaptionTextType.qml - images/controls/settings.svg - ui/qml/Pages2/PageSettingsServerInfo.qml - ui/qml/Controls2/PageType.qml - ui/qml/Controls2/PopupType.qml - images/controls/edit-3.svg - ui/qml/Pages2/PageSettingsServerData.qml - ui/qml/Components/SettingsContainersListView.qml - ui/qml/Controls2/TextTypes/ListItemTitleType.qml - ui/qml/Controls2/DividerType.qml - ui/qml/Controls2/StackViewType.qml - ui/qml/Pages2/PageSettings.qml - images/controls/amnezia.svg - images/controls/app.svg - images/controls/radio.svg - images/controls/save.svg - images/controls/server.svg - ui/qml/Pages2/PageSettingsServerProtocols.qml - ui/qml/Pages2/PageSettingsServerServices.qml - ui/qml/Pages2/PageSetupWizardViewConfig.qml - images/controls/file-cog-2.svg - ui/qml/Components/QuestionDrawer.qml - ui/qml/Pages2/PageDeinstalling.qml - ui/qml/Controls2/BackButtonType.qml - ui/qml/Pages2/PageSettingsServerProtocol.qml - ui/qml/Components/TransportProtoSelector.qml - ui/qml/Controls2/ListViewWithRadioButtonType.qml - images/controls/radio-button.svg - images/controls/radio-button-inner-circle.png - images/controls/radio-button-pressed.svg - images/controls/radio-button-inner-circle-pressed.png - ui/qml/Components/ShareConnectionDrawer.qml - ui/qml/Pages2/PageSettingsConnection.qml - ui/qml/Pages2/PageSettingsDns.qml - ui/qml/Pages2/PageSettingsApplication.qml - ui/qml/Pages2/PageSettingsBackup.qml - images/controls/delete.svg - ui/qml/Pages2/PageSettingsAbout.qml - images/controls/github.svg - images/controls/mail.svg - images/controls/telegram.svg - ui/qml/Controls2/TextTypes/SmallTextType.qml - ui/qml/Filters/ContainersModelFilters.qml - ui/qml/Components/SelectLanguageDrawer.qml - ui/qml/Controls2/BusyIndicatorType.qml - ui/qml/Pages2/PageProtocolOpenVpnSettings.qml - ui/qml/Pages2/PageProtocolShadowSocksSettings.qml - ui/qml/Pages2/PageProtocolCloakSettings.qml - ui/qml/Pages2/PageProtocolXraySettings.qml - ui/qml/Pages2/PageProtocolRaw.qml - ui/qml/Pages2/PageSettingsLogging.qml - ui/qml/Pages2/PageServiceSftpSettings.qml - images/controls/copy.svg - ui/qml/Pages2/PageServiceTorWebsiteSettings.qml - ui/qml/Pages2/PageSetupWizardQrReader.qml - images/controls/eye.svg - images/controls/eye-off.svg - ui/qml/Pages2/PageSettingsSplitTunneling.qml - ui/qml/Controls2/ContextMenuType.qml - ui/qml/Controls2/TextAreaType.qml - images/controls/trash.svg - images/controls/more-vertical.svg - ui/qml/Controls2/ListViewWithLabelsType.qml - ui/qml/Pages2/PageServiceDnsSettings.qml - ui/qml/Controls2/TopCloseButtonType.qml - images/controls/x-circle.svg - ui/qml/Pages2/PageProtocolAwgSettings.qml - server_scripts/awg/template.conf - server_scripts/awg/start.sh - server_scripts/awg/configure_container.sh - server_scripts/awg/run_container.sh - server_scripts/awg/Dockerfile - ui/qml/Pages2/PageShareFullAccess.qml - images/controls/close.svg - images/controls/search.svg server_scripts/xray/configure_container.sh server_scripts/xray/Dockerfile server_scripts/xray/run_container.sh server_scripts/xray/start.sh server_scripts/xray/template.json - ui/qml/Pages2/PageProtocolWireGuardSettings.qml + ui/qml/Components/AdLabel.qml + ui/qml/Components/ConnectButton.qml + ui/qml/Components/ConnectionTypeSelectionDrawer.qml + ui/qml/Components/HomeContainersListView.qml ui/qml/Components/HomeSplitTunnelingDrawer.qml - images/controls/split-tunneling.svg - ui/qml/Controls2/DrawerType2.qml - ui/qml/Pages2/PageSettingsAppSplitTunneling.qml ui/qml/Components/InstalledAppsDrawer.qml - images/controls/alert-circle.svg - images/controls/file-check-2.svg + ui/qml/Components/QuestionDrawer.qml + ui/qml/Components/SelectLanguageDrawer.qml + ui/qml/Components/ServersListView.qml + ui/qml/Components/SettingsContainersListView.qml + ui/qml/Components/ShareConnectionDrawer.qml + ui/qml/Components/TransportProtoSelector.qml + ui/qml/Config/GlobalConfig.qml + ui/qml/Config/qmldir + ui/qml/Controls2/BackButtonType.qml + ui/qml/Controls2/BasicButtonType.qml + ui/qml/Controls2/BusyIndicatorType.qml + ui/qml/Controls2/CardType.qml + ui/qml/Controls2/CardWithIconsType.qml + ui/qml/Controls2/CheckBoxType.qml + ui/qml/Controls2/ContextMenuType.qml + ui/qml/Controls2/DividerType.qml + ui/qml/Controls2/DrawerType2.qml + ui/qml/Controls2/DropDownType.qml + ui/qml/Controls2/FlickableType.qml + ui/qml/Controls2/Header2Type.qml + ui/qml/Controls2/HeaderType.qml + ui/qml/Controls2/HorizontalRadioButton.qml + ui/qml/Controls2/ImageButtonType.qml + ui/qml/Controls2/LabelWithButtonType.qml + ui/qml/Controls2/LabelWithImageType.qml + ui/qml/Controls2/ListViewWithLabelsType.qml + ui/qml/Controls2/ListViewWithRadioButtonType.qml + ui/qml/Controls2/PageType.qml + ui/qml/Controls2/PopupType.qml + ui/qml/Controls2/ProgressBarType.qml + ui/qml/Controls2/ScrollBarType.qml + ui/qml/Controls2/StackViewType.qml + ui/qml/Controls2/SwitcherType.qml + ui/qml/Controls2/TabButtonType.qml + ui/qml/Controls2/TabImageButtonType.qml + ui/qml/Controls2/TextAreaType.qml + ui/qml/Controls2/TextAreaWithFooterType.qml + ui/qml/Controls2/TextFieldWithHeaderType.qml + ui/qml/Controls2/TextTypes/ButtonTextType.qml + ui/qml/Controls2/TextTypes/CaptionTextType.qml + ui/qml/Controls2/TextTypes/Header1TextType.qml + ui/qml/Controls2/TextTypes/Header2TextType.qml + ui/qml/Controls2/TextTypes/LabelTextType.qml + ui/qml/Controls2/TextTypes/ListItemTitleType.qml + ui/qml/Controls2/TextTypes/ParagraphTextType.qml + ui/qml/Controls2/TextTypes/SmallTextType.qml + ui/qml/Controls2/TopCloseButtonType.qml + ui/qml/Controls2/VerticalRadioButton.qml ui/qml/Controls2/WarningType.qml + ui/qml/Filters/ContainersModelFilters.qml + ui/qml/main2.qml + ui/qml/Modules/Style/AmneziaStyle.qml + ui/qml/Modules/Style/qmldir + ui/qml/Pages2/PageDeinstalling.qml + ui/qml/Pages2/PageDevMenu.qml + ui/qml/Pages2/PageHome.qml + ui/qml/Pages2/PageProtocolAwgSettings.qml + ui/qml/Pages2/PageProtocolCloakSettings.qml + ui/qml/Pages2/PageProtocolOpenVpnSettings.qml + ui/qml/Pages2/PageProtocolRaw.qml + ui/qml/Pages2/PageProtocolShadowSocksSettings.qml + ui/qml/Pages2/PageProtocolWireGuardSettings.qml + ui/qml/Pages2/PageProtocolXraySettings.qml + ui/qml/Pages2/PageServiceDnsSettings.qml + ui/qml/Pages2/PageServiceSftpSettings.qml ui/qml/Pages2/PageServiceSocksProxySettings.qml - server_scripts/socks5_proxy/run_container.sh - server_scripts/socks5_proxy/Dockerfile - server_scripts/socks5_proxy/configure_container.sh - server_scripts/socks5_proxy/start.sh + ui/qml/Pages2/PageServiceTorWebsiteSettings.qml + ui/qml/Pages2/PageSettings.qml + ui/qml/Pages2/PageSettingsAbout.qml + ui/qml/Pages2/PageSettingsApiAvailableCountries.qml + ui/qml/Pages2/PageSettingsApiServerInfo.qml + ui/qml/Pages2/PageSettingsApplication.qml + ui/qml/Pages2/PageSettingsAppSplitTunneling.qml + ui/qml/Pages2/PageSettingsBackup.qml + ui/qml/Pages2/PageSettingsConnection.qml + ui/qml/Pages2/PageSettingsDns.qml + ui/qml/Pages2/PageSettingsLogging.qml + ui/qml/Pages2/PageSettingsServerData.qml + ui/qml/Pages2/PageSettingsServerInfo.qml + ui/qml/Pages2/PageSettingsServerProtocol.qml + ui/qml/Pages2/PageSettingsServerProtocols.qml + ui/qml/Pages2/PageSettingsServerServices.qml + ui/qml/Pages2/PageSettingsServersList.qml + ui/qml/Pages2/PageSettingsSplitTunneling.qml + ui/qml/Pages2/PageProtocolAwgClientSettings.qml + ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml + ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml + ui/qml/Pages2/PageSetupWizardApiServicesList.qml + ui/qml/Pages2/PageSetupWizardConfigSource.qml + ui/qml/Pages2/PageSetupWizardCredentials.qml + ui/qml/Pages2/PageSetupWizardEasy.qml + ui/qml/Pages2/PageSetupWizardInstalling.qml + ui/qml/Pages2/PageSetupWizardProtocols.qml + ui/qml/Pages2/PageSetupWizardProtocolSettings.qml + ui/qml/Pages2/PageSetupWizardQrReader.qml + ui/qml/Pages2/PageSetupWizardStart.qml + ui/qml/Pages2/PageSetupWizardTextKey.qml + ui/qml/Pages2/PageSetupWizardViewConfig.qml + ui/qml/Pages2/PageShare.qml + ui/qml/Pages2/PageShareFullAccess.qml + ui/qml/Pages2/PageStart.qml + ui/qml/Components/RenameServerDrawer.qml + ui/qml/Controls2/ListViewType.qml + ui/qml/Pages2/PageSettingsApiSupport.qml + ui/qml/Pages2/PageSettingsApiInstructions.qml + ui/qml/Pages2/PageSettingsApiNativeConfigs.qml + ui/qml/Pages2/PageSettingsApiDevices.qml + images/controls/monitor.svg + + + images/flagKit/ZW.svg + images/flagKit/ZM.svg + images/flagKit/ZA.svg + images/flagKit/YT.svg + images/flagKit/YE.svg + images/flagKit/XK.svg + images/flagKit/WS.svg + images/flagKit/WF.svg + images/flagKit/VU.svg + images/flagKit/VN.svg + images/flagKit/VI.svg + images/flagKit/VG.svg + images/flagKit/VE.svg + images/flagKit/VC.svg + images/flagKit/VA.svg + images/flagKit/UZ.svg + images/flagKit/UY.svg + images/flagKit/US.svg + images/flagKit/UM.svg + images/flagKit/UG.svg + images/flagKit/UA.svg + images/flagKit/TZ.svg + images/flagKit/TW.svg + images/flagKit/TV.svg + images/flagKit/TT.svg + images/flagKit/TR.svg + images/flagKit/TO.svg + images/flagKit/TN.svg + images/flagKit/TM.svg + images/flagKit/TL.svg + images/flagKit/TK.svg + images/flagKit/TJ.svg + images/flagKit/TH.svg + images/flagKit/TG.svg + images/flagKit/TF.svg + images/flagKit/TD.svg + images/flagKit/TC.svg + images/flagKit/SZ.svg + images/flagKit/SY.svg + images/flagKit/SX.svg + images/flagKit/SV.svg + images/flagKit/ST.svg + images/flagKit/SS.svg + images/flagKit/SR.svg + images/flagKit/SO.svg + images/flagKit/SN.svg + images/flagKit/SM.svg + images/flagKit/SL.svg + images/flagKit/SK.svg + images/flagKit/SJ.svg + images/flagKit/SI.svg + images/flagKit/SH.svg + images/flagKit/SG.svg + images/flagKit/SE.svg + images/flagKit/SD.svg + images/flagKit/SC.svg + images/flagKit/SB.svg + images/flagKit/SA.svg + images/flagKit/RW.svg + images/flagKit/RU.svg + images/flagKit/RS.svg + images/flagKit/RO.svg + images/flagKit/RE.svg + images/flagKit/QA.svg + images/flagKit/PY.svg + images/flagKit/PW.svg + images/flagKit/PT.svg + images/flagKit/PS.svg + images/flagKit/PR.svg + images/flagKit/PN.svg + images/flagKit/PM.svg + images/flagKit/PL.svg + images/flagKit/PK.svg + images/flagKit/PH.svg + images/flagKit/PG.svg + images/flagKit/PF.svg + images/flagKit/PE.svg + images/flagKit/PA.svg + images/flagKit/OM.svg + images/flagKit/NZ.svg + images/flagKit/NU.svg + images/flagKit/NR.svg + images/flagKit/NP.svg + images/flagKit/NO.svg + images/flagKit/NL.svg + images/flagKit/NI.svg + images/flagKit/NG.svg + images/flagKit/NF.svg + images/flagKit/NE.svg + images/flagKit/NC.svg + images/flagKit/NA.svg + images/flagKit/MZ.svg + images/flagKit/MY.svg + images/flagKit/MX.svg + images/flagKit/MW.svg + images/flagKit/MV.svg + images/flagKit/MU.svg + images/flagKit/MT.svg + images/flagKit/MS.svg + images/flagKit/MR.svg + images/flagKit/MQ.svg + images/flagKit/MP.svg + images/flagKit/MO.svg + images/flagKit/MN.svg + images/flagKit/MM.svg + images/flagKit/ML.svg + images/flagKit/MK.svg + images/flagKit/MH.svg + images/flagKit/MG.svg + images/flagKit/MF.svg + images/flagKit/ME.svg + images/flagKit/MD.svg + images/flagKit/MC.svg + images/flagKit/MA.svg + images/flagKit/LY.svg + images/flagKit/LV.svg + images/flagKit/LU.svg + images/flagKit/LT.svg + images/flagKit/LS.svg + images/flagKit/LR.svg + images/flagKit/LK.svg + images/flagKit/LI.svg + images/flagKit/LC.svg + images/flagKit/LB.svg + images/flagKit/LA.svg + images/flagKit/KZ.svg + images/flagKit/KY.svg + images/flagKit/KW.svg + images/flagKit/KR.svg + images/flagKit/KP.svg + images/flagKit/KN.svg + images/flagKit/KM.svg + images/flagKit/KI.svg + images/flagKit/KH.svg + images/flagKit/KG.svg + images/flagKit/KE.svg + images/flagKit/JP.svg + images/flagKit/JO.svg + images/flagKit/JM.svg + images/flagKit/JE.svg + images/flagKit/IT.svg + images/flagKit/IS.svg + images/flagKit/IR.svg + images/flagKit/IQ.svg + images/flagKit/IO.svg + images/flagKit/IN.svg + images/flagKit/IM.svg + images/flagKit/IL.svg + images/flagKit/IE.svg + images/flagKit/ID.svg + images/flagKit/HU.svg + images/flagKit/HT.svg + images/flagKit/HR.svg + images/flagKit/HN.svg + images/flagKit/HM.svg + images/flagKit/HK.svg + images/flagKit/GY.svg + images/flagKit/GW.svg + images/flagKit/GU.svg + images/flagKit/GT.svg + images/flagKit/GS.svg + images/flagKit/GR.svg + images/flagKit/GQ.svg + images/flagKit/GP.svg + images/flagKit/GN.svg + images/flagKit/GM.svg + images/flagKit/GL.svg + images/flagKit/GI.svg + images/flagKit/GH.svg + images/flagKit/GG.svg + images/flagKit/GF.svg + images/flagKit/GE.svg + images/flagKit/GD.svg + images/flagKit/GB.svg + images/flagKit/GA.svg + images/flagKit/FR.svg + images/flagKit/FO.svg + images/flagKit/FM.svg + images/flagKit/FK.svg + images/flagKit/FJ.svg + images/flagKit/FI.svg + images/flagKit/EU.svg + images/flagKit/ET.svg + images/flagKit/ES.svg + images/flagKit/ER.svg + images/flagKit/EG.svg + images/flagKit/EE.svg + images/flagKit/EC.svg + images/flagKit/DZ.svg + images/flagKit/DO.svg + images/flagKit/DM.svg + images/flagKit/DK.svg + images/flagKit/DJ.svg + images/flagKit/DE.svg + images/flagKit/CZ.svg + images/flagKit/CY.svg + images/flagKit/CX.svg + images/flagKit/CW.svg + images/flagKit/CV.svg + images/flagKit/CU.svg + images/flagKit/CR.svg + images/flagKit/CO.svg + images/flagKit/CN.svg + images/flagKit/CM.svg + images/flagKit/CL.svg + images/flagKit/CK.svg + images/flagKit/CI.svg + images/flagKit/CH.svg + images/flagKit/CG.svg + images/flagKit/CF.svg + images/flagKit/CD.svg + images/flagKit/CC.svg + images/flagKit/CA.svg + images/flagKit/BZ.svg + images/flagKit/BY.svg + images/flagKit/BW.svg + images/flagKit/BV.svg + images/flagKit/BT.svg + images/flagKit/BS.svg + images/flagKit/BR.svg + images/flagKit/BO.svg + images/flagKit/BN.svg + images/flagKit/BM.svg + images/flagKit/BL.svg + images/flagKit/BJ.svg + images/flagKit/BI.svg + images/flagKit/BH.svg + images/flagKit/BG.svg + images/flagKit/BF.svg + images/flagKit/BE.svg + images/flagKit/BD.svg + images/flagKit/BB.svg + images/flagKit/BA.svg + images/flagKit/AZ.svg + images/flagKit/AX.svg + images/flagKit/AW.svg + images/flagKit/AU.svg + images/flagKit/AT.svg + images/flagKit/AS.svg + images/flagKit/AR.svg + images/flagKit/AO.svg + images/flagKit/AM.svg + images/flagKit/AL.svg + images/flagKit/AI.svg + images/flagKit/AG.svg + images/flagKit/AF.svg + images/flagKit/AE.svg + images/flagKit/AD.svg diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index 592f77d4..4fd199db 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -15,6 +15,12 @@ using namespace QKeychain; +namespace { + constexpr const char *settingsKeyTag = "settingsKeyTag"; + constexpr const char *settingsIvTag = "settingsIvTag"; + constexpr const char *keyChainName = "AmneziaVPN-Keychain"; +} + SecureQSettings::SecureQSettings(const QString &organization, const QString &application, QObject *parent) : QObject { parent }, m_settings(organization, application, parent), encryptedKeys({ "Servers/serversList" }) { @@ -49,7 +55,7 @@ QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue // check if value is not encrypted, v. < 2.0.x retVal = m_settings.value(key); if (retVal.isValid()) { - if (retVal.userType() == QVariant::ByteArray && retVal.toByteArray().mid(0, magicString.size()) == magicString) { + if (retVal.userType() == QMetaType::QByteArray && retVal.toByteArray().mid(0, magicString.size()) == magicString) { if (getEncKey().isEmpty() || getEncIv().isEmpty()) { qCritical() << "SecureQSettings::setValue Decryption requested, but key is empty"; @@ -174,18 +180,30 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json) QByteArray SecureQSettings::encryptText(const QByteArray &value) const { QSimpleCrypto::QBlockCipher cipher; - return cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); + QByteArray result; + try { + result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv()); + } catch (...) { // todo change error handling in QSimpleCrypto? + qCritical() << "error when encrypting the settings value"; + } + return result; } QByteArray SecureQSettings::decryptText(const QByteArray &ba) const { QSimpleCrypto::QBlockCipher cipher; - return cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); + QByteArray result; + try { + result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv()); + } catch (...) { // todo change error handling in QSimpleCrypto? + qCritical() << "error when decrypting the settings value"; + } + return result; } bool SecureQSettings::encryptionRequired() const { -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) // QtKeyChain failing on Linux return false; #endif @@ -200,7 +218,7 @@ QByteArray SecureQSettings::getEncKey() const if (m_key.isEmpty()) { // Create new key QSimpleCrypto::QBlockCipher cipher; - QByteArray key = cipher.generateSecureRandomBytes(32); + QByteArray key = cipher.generatePrivateSalt(32); if (key.isEmpty()) { qCritical() << "SecureQSettings::getEncKey Unable to generate new enc key"; } @@ -226,7 +244,7 @@ QByteArray SecureQSettings::getEncIv() const if (m_iv.isEmpty()) { // Create new IV QSimpleCrypto::QBlockCipher cipher; - QByteArray iv = cipher.generateSecureRandomBytes(32); + QByteArray iv = cipher.generatePrivateSalt(32); if (iv.isEmpty()) { qCritical() << "SecureQSettings::getEncIv Unable to generate new enc IV"; } diff --git a/client/secure_qsettings.h b/client/secure_qsettings.h index 43890578..3f04096e 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -8,10 +8,6 @@ #include "keychain.h" -constexpr const char *settingsKeyTag = "settingsKeyTag"; -constexpr const char *settingsIvTag = "settingsIvTag"; -constexpr const char *keyChainName = "AmneziaVPN-Keychain"; - class SecureQSettings : public QObject { Q_OBJECT @@ -44,7 +40,7 @@ public: private: QSettings m_settings; - mutable QMap m_cache; + mutable QHash m_cache; QStringList encryptedKeys; // encode only key listed here // only this fields need for backup diff --git a/client/server_scripts/awg/configure_container.sh b/client/server_scripts/awg/configure_container.sh index 322cc38f..2000c965 100644 --- a/client/server_scripts/awg/configure_container.sh +++ b/client/server_scripts/awg/configure_container.sh @@ -12,7 +12,7 @@ echo $WIREGUARD_PSK > /opt/amnezia/awg/wireguard_psk.key cat > /opt/amnezia/awg/wg0.conf <&1 | grep moby-engine)" ]; then echo "Docker elif [ -n "$(sudo docker --version 2>&1 | grep podman)" ]; then check_srv="podman.socket podman"; docker_pkg="podman-docker";\ if [ -n "$(sudo docker --version 2>&1 | grep /etc/containers/nodocker)" ]; then sudo touch /etc/containers/nodocker; fi;\ fi;\ +if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \ + if ! command -v apparmor_parser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst apparmor; fi;\ +fi;\ if [ "$(systemctl is-active $check_srv | head -n1)" != "active" ]; then \ sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\ sleep 5; sudo systemctl start $check_srv; sleep 5;\ if [ "$(systemctl is-active $check_srv | head -n1)" != "active" ]; then echo "Failed to status docker"; echo "command not found"; exit 1; fi;\ fi;\ -sudo docker --version +sudo docker --version \ No newline at end of file diff --git a/client/server_scripts/ipsec/configure_container.sh b/client/server_scripts/ipsec/configure_container.sh index 76c4dfaf..1f0a45cb 100644 --- a/client/server_scripts/ipsec/configure_container.sh +++ b/client/server_scripts/ipsec/configure_container.sh @@ -33,14 +33,14 @@ conn shared right=%any encapsulation=yes authby=secret - pfs=no + pfs=yes rekey=no keyingtries=5 dpddelay=30 dpdtimeout=120 dpdaction=clear ikev2=never - ike=aes256-sha2,aes128-sha2,aes256-sha1,aes128-sha1,aes256-sha2;modp1024,aes128-sha1;modp1024 + ike=aes256-sha2,aes128-sha2,aes256-sha1,aes128-sha1,aes256-sha2;modp2048,aes128-sha1;modp2048 phase2alg=aes_gcm-null,aes128-sha1,aes256-sha1,aes256-sha2_512,aes128-sha2,aes256-sha2 ikelifetime=24h salifetime=24h @@ -244,9 +244,9 @@ conn ikev2-cp auto=add ikev2=insist rekey=no - pfs=no - ike=aes256-sha2,aes128-sha2,aes256-sha1,aes128-sha1 - phase2alg=aes_gcm-null,aes128-sha1,aes256-sha1,aes128-sha2,aes256-sha2 + pfs=yes + ike=aes256-sha2,aes128-sha2,aes256-sha1,aes128-sha1,aes256-sha2;modp2048,aes128-sha1;modp2048 + phase2alg=aes_gcm-null,aes128-sha1,aes256-sha1,aes256-sha2_512,aes128-sha2,aes256-sha2 ikelifetime=24h salifetime=24h encapsulation=yes diff --git a/client/server_scripts/xray/configure_container.sh b/client/server_scripts/xray/configure_container.sh index 541e155b..a84751c7 100644 --- a/client/server_scripts/xray/configure_container.sh +++ b/client/server_scripts/xray/configure_container.sh @@ -29,7 +29,7 @@ cat > /opt/amnezia/xray/server.json < Settings::containers(int serverIndex) const QMap containersMap; for (const QJsonValue &val : containers) { - containersMap.insert(ContainerProps::containerFromString(val.toObject().value(config_key::container).toString()), - val.toObject()); + containersMap.insert(ContainerProps::containerFromString(val.toObject().value(config_key::container).toString()), val.toObject()); } return containersMap; @@ -221,11 +227,13 @@ void Settings::setSaveLogs(bool enabled) if (!isSaveLogs()) { Logger::deInit(); } else { - if (!Logger::init()) { + if (!Logger::init(false)) { qWarning() << "Initialization of debug subsystem failed"; } } #endif + Logger::setServiceLogsEnabled(enabled); + if (enabled) { setLogEnableDate(QDateTime::currentDateTime()); } @@ -440,6 +448,17 @@ QString Settings::getInstallationUuid(const bool needCreate) auto uuid = value("Conf/installationUuid", "").toString(); if (needCreate && uuid.isEmpty()) { uuid = QUuid::createUuid().toString(); + + //remove {} from uuid + uuid.remove(0, 1); + uuid.chop(1); + + setInstallationUuid(uuid); + } else if (uuid.contains("{") && uuid.contains("}")) { + //remove {} from old uuid + uuid.remove(0, 1); + uuid.chop(1); + setInstallationUuid(uuid); } return uuid; @@ -474,11 +493,8 @@ QVariant Settings::value(const QString &key, const QVariant &defaultValue) const if (QThread::currentThread() == QCoreApplication::instance()->thread()) { returnValue = m_settings.value(key, defaultValue); } else { - QMetaObject::invokeMethod(&m_settings, "value", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, returnValue), - Q_ARG(const QString&, key), - Q_ARG(const QVariant&, defaultValue)); + QMetaObject::invokeMethod(&m_settings, "value", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, returnValue), + Q_ARG(const QString &, key), Q_ARG(const QVariant &, defaultValue)); } return returnValue; } @@ -488,9 +504,47 @@ void Settings::setValue(const QString &key, const QVariant &value) if (QThread::currentThread() == QCoreApplication::instance()->thread()) { m_settings.setValue(key, value); } else { - QMetaObject::invokeMethod(&m_settings, "setValue", - Qt::BlockingQueuedConnection, - Q_ARG(const QString&, key), - Q_ARG(const QVariant&, value)); + QMetaObject::invokeMethod(&m_settings, "setValue", Qt::BlockingQueuedConnection, Q_ARG(const QString &, key), + Q_ARG(const QVariant &, value)); } } + +void Settings::resetGatewayEndpoint() +{ + m_gatewayEndpoint = gatewayEndpoint; +} + +void Settings::setGatewayEndpoint(const QString &endpoint) +{ + m_gatewayEndpoint = endpoint; +} + +void Settings::setDevGatewayEndpoint() +{ + m_gatewayEndpoint = DEV_AGW_ENDPOINT; +} + +QString Settings::getGatewayEndpoint() +{ + return m_gatewayEndpoint; +} + +bool Settings::isDevGatewayEnv() +{ + return m_isDevGatewayEnv; +} + +void Settings::toggleDevGatewayEnv(bool enabled) +{ + m_isDevGatewayEnv = enabled; +} + +bool Settings::isHomeAdLabelVisible() +{ + return value("Conf/homeAdLabelVisible", true).toBool(); +} + +void Settings::disableHomeAdLabel() +{ + setValue("Conf/homeAdLabelVisible", false); +} diff --git a/client/settings.h b/client/settings.h index 74d1b4b9..b383d3da 100644 --- a/client/settings.h +++ b/client/settings.h @@ -160,9 +160,6 @@ public: setValue("Conf/secondaryDns", secondaryDns); } - static const char cloudFlareNs1[]; - static const char cloudFlareNs2[]; - // static constexpr char openNicNs5[] = "94.103.153.176"; // static constexpr char openNicNs13[] = "144.76.103.143"; @@ -186,7 +183,7 @@ public: bool isScreenshotsEnabled() const { - return value("Conf/screenshotsEnabled", false).toBool(); + return value("Conf/screenshotsEnabled", true).toBool(); } void setScreenshotsEnabled(bool enabled) { @@ -218,6 +215,16 @@ public: void setKillSwitchEnabled(bool enabled); QString getInstallationUuid(const bool needCreate); + void resetGatewayEndpoint(); + void setGatewayEndpoint(const QString &endpoint); + void setDevGatewayEndpoint(); + QString getGatewayEndpoint(); + bool isDevGatewayEnv(); + void toggleDevGatewayEnv(bool enabled); + + bool isHomeAdLabelVisible(); + void disableHomeAdLabel(); + signals: void saveLogsChanged(bool enabled); void screenshotsEnabledChanged(bool enabled); @@ -231,6 +238,9 @@ private: void setInstallationUuid(const QString &uuid); mutable SecureQSettings m_settings; + + QString m_gatewayEndpoint; + bool m_isDevGatewayEnv = false; }; #endif // SETTINGS_H diff --git a/client/translations/amneziavpn_ar_EG.ts b/client/translations/amneziavpn_ar_EG.ts index 8e6f2764..1d88eea0 100644 --- a/client/translations/amneziavpn_ar_EG.ts +++ b/client/translations/amneziavpn_ar_EG.ts @@ -1,52 +1,159 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + شبكة VPN كلاسيكية للعمل المريح وتنزيل الملفات الكبيرة ومشاهدة مقاطع الفيديو. تعمل مع أي موقع. تصل السرعة إلى %1 ميجابت/ثانية + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + شبكة VPN للولوج للمواقع المحظورة في بلاد ذو مستوي عالي من الرقابة علي الانترنت. + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + Amenzia Premium - شبكة VPN للعمل المريح, تحميل ملفات كبيرة الحجم, ومشاهدة مقاطع الفيديو ب جودة عالية. تعمل لجميع المواقع, حتي في البلاد ذو مستوي عالي من الرقابة علي الانترنت + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + Amnezia Free هو VPN مجاني لتخطي الحظر في البلاد ذو مستوي عالي من الرقابة علي الانترنت + + + + %1 MBit/s + %1 ميجابت/ثانية + + + + %1 days + %1 ايام + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + سيقوم VPN فقط بفتح المواقع المشهورة المحظورة في بلدك, مثل Instagram, Facebook, Twitter و مواقع اخري. المواقع الاخري ستٌفتح من عنوان ال IP الحقيقي الخاص بك, <a href="%1/free" style="color: #FBB26A;">معلومات اخري علي الموقع.</a> + + + + Free + مجاني + + + + %1 $/month + %1 دولار/الشهر + + + + AppSplitTunnelingController + + + Application added: %1 + تمت إضافة التطبيق: %1 + + + + The application has already been added + التطبيق مٌضاف بالفعل + + + + The selected applications have been added + تمت إضافة التطبيقات المٌختارة + + + + Application removed: %1 + تم حذف التطبيق: %1 + + + + ConnectButton + + + Unable to disconnect during configuration preparation + غير قادر علي قطع الاتصال اثناء إعداد التكوين + + ConnectionController - - - - + + + + Connect اتصل - + VPN Protocols is not installed. Please install VPN container at first لم يتم تثبيت بروتوكولات VPN, من فضلك قم بتنزيل حاوية VPN اولاً - + Connecting... اتصال... - + Connected تم الاتصال - + Reconnecting... إعادة الاتصال... - + Disconnecting... إنهاء الاتصال... - + + Preparing... + جاري التحضير... + + + Settings updated successfully, reconnnection... تم تحديث الاعدادات بنجاح, جاري إعادة الاتصال... - + Settings updated successfully تم تحديث الاعدادات بنجاح + + + The selected protocol is not supported on the current platform + البروتوكول المحدد غير مدعوم علي المنصة الحالية + + + + unable to create configuration + غير قادر علي إنشاء تكوين + ConnectionTypeSelectionDrawer @@ -61,7 +168,7 @@ قم بتهيئة الخادم الخاص بك - + Open config file, key or QR code افتح ملف تعريف, مفتاح تعريف او رمز QR @@ -84,7 +191,7 @@ &لصق - + &SelectAll &تحديد الكل @@ -92,65 +199,61 @@ ExportController - Access error! - خطأ في الوصول! + خطأ في الوصول! HomeContainersListView - + Unable change protocol while there is an active connection - قم بتغيير البروتوكول عند تواجد اتصال - - - - The selected protocol is not supported on the current platform - البروتوكول المحدد غير مدعوم علي المنصة الحالية + غير قادر علي تغيير البروتوكول اثناء تواجد اتصال HomeSplitTunnelingDrawer - + Split tunneling تقسيم الانفاق - + Allows you to connect to some sites or applications through a VPN connection and bypass others يسمح لك بألاتصال ببعض المواقع او البرامج خلال اتصال VPN و تجاوز الاخرين - + Split tunneling on the server تقسيم الانفاق علي الخادم - + Enabled Can't be disabled for current server مٌفعل لا يمكن إقافة للخادم الحالي - + Site-based split tunneling انقسام الانفاق القائم علي الموقع - + + Enabled مٌفعل - + + Disabled مٌعطل - + App-based split tunneling انقسام الانفاق القائم علي التطبيق @@ -158,53 +261,45 @@ Can't be disabled for current server ImportController - Unable to open file - + غير قادر علي فتح الملف - - Invalid configuration file - + ملف تكوين غير صحيح - + Scanned %1 of %2. تم فحص%1 من %2. + + + In the imported configuration, potentially dangerous lines were found: + في التكوين المستورد، تم العثور على سطور يحتمل أن تكون خطرة: + InstallController - installed successfully. - تم التثبيت بنجاح - - - is already installed on the server. - بالفعل مٌثبت علي الخادم - - - - + %1 installed successfully. %1 تم التثبيت بنجاح. - - + %1 is already installed on the server. %1 بالفعل مٌثبت علي الخادم. - + Added containers that were already installed on the server تمت إضافة الحاويات التي كانت مٌثبتة بالفعل علي الخادم - + Already installed containers were found on the server. All installed containers have been added to the application @@ -212,56 +307,83 @@ Already installed containers were found on the server. All installed containers تمت إضافة جميع الحاويات المٌثبتة إلي التطبيق - + Settings updated successfully تم تحديث الاعدادات بنجاح - + Server '%1' was rebooted تمت إعادة تشغيل الخادم%1 - + Server '%1' was removed تمت إزالة الخادم '%1' - + All containers from server '%1' have been removed قد تم حذفها '%1' جميع الحاويات من الخادم - + %1 has been removed from the server '%2' %1 تم حدف '%2' اسم الخادم - 1% has been removed from the server '%2' - %1 من الخادم '%2' تم مسحة + + Api config removed + تم حذف تكوين Api - Server ' - خادم + + %1 cached profile cleared + تم مسح ملف تعريف %1 المخزن مؤقتًا - ' was removed - تم حذفة - - - has been removed from the server ' - قد تمت إزالتة من الخادم - - - + Please login as the user من فضلك قم بتسجيل الدخول كمستخدم - + Server added successfully تمت إضافة الخادم بنجاح + + + %1 installed successfully. + تم تحميل %1 بنجاح + + + + API config reloaded + تمت إعادة تحميل تكوين API + + + + Successfully changed the country of connection to %1 + تم تغيير بلد الاتصال بنجاح إلى %1 + + + + InstalledAppsDrawer + + + Choose application + اختر تطبيق + + + + application name + اسم التطبيق + + + + Add selected + اضف اختيارك + KeyChainClass @@ -284,28 +406,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN AmneziaVPN - + VPN Connected تم الاتصال - + VPN Disconnected تم إنهاء الاتصال - + AmneziaVPN notification إشعار من AmneziaVPN - + Unsecured network detected: تم العثور علي شبكة غير مؤمنة: @@ -313,114 +435,226 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 من %1 مسح الخدمة - + Usually it takes no more than 5 minutes في العادة تستغرق اقل من 5 دقائق - PageHome + PageDevMenu - - Logging enabled - + + Gateway endpoint + نقطة نهاية البوابة - + + Dev gateway environment + + + + + PageHome + + + Logging enabled + تم تمكين التسجيل + + + Split tunneling enabled تقسيم الانفاق مٌفعل - + Split tunneling disabled تقسيم الانفاق مٌعطل - + VPN protocol بروتوكول VPN - + Servers الخوادم - Unable change server while there is an active connection - لا يمكن تغير الخادم بينما هناك اتصال مفعل + لا يمكن تغير الخادم بينما هناك اتصال مفعل + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + اعدادات AmneziaWG + + + + MTU + + + + + Server settings + + + + + Port + منفذ + + + + Save + احفظ + + + + Save settings? + احفظ الإعدادات؟ + + + + Only the settings for this device will be changed + + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط PageProtocolAwgSettings - + AmneziaWG settings اعدادات AmneziaWG - + Port منفذ - - MTU - - - - - Remove AmneziaWG - قم بحذف AmneziaWG - - - - Remove AmneziaWG from server? - قم بحذف AmneziaWG من الخادم؟ - - - - + All users with whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - + Save احفظ - + + VPN address subnet + الشبكة الفرعية لعنوان VPN + + + + Jc - Junk packet count + Jc - عدد الحزم غير المرغوب فيها + + + + Jmin - Junk packet minimum size + Jmin - الحجم الادني للحزم الغير مرغوب فيها + + + + Jmax - Junk packet maximum size + Jmax - الحجم الاقصي للحزم الغير مرغوب فيها + + + + S1 - Init packet junk size + S1 - حجم حزمة البيانات العشوائية الأولية + + + + S2 - Response packet junk size + S2 - حجم حزمة الاستجابة غير المرغوب فيها + + + + H1 - Init packet magic header + H1 - حزمة رأس سحرية مبدئية + + + + H2 - Response packet magic header + H2 - رأس حزمة الاستجابة السحرية + + + + H4 - Transport packet magic header + H4 - رأس حزمة النقل السحرية + + + + H3 - Underload packet magic header + H3 - رأس حزمة السحر غير المحمل + + + + The values of the H1-H4 fields must be unique + يجب أن تكون قيم الحقول H1-H4 فريدة + + + + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + يجب ألا تساوي قيمة الحقل S1 + حجم بدء الرسالة (148) S2 + حجم استجابة الرسالة (92) + + + Save settings? احفظ الإعدادات؟ - - + Continue واصل - - + Cancel إلغاء + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + PageProtocolCloakSettings - + Cloak settings Cloak إعدادات - + Disguised as traffic from متنكراً في حركة مرور من @@ -430,16 +664,21 @@ Already installed containers were found on the server. All installed containers منفذ - - + + Cipher الشفرة - + Save احفظ + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + PageProtocolOpenVpnSettings @@ -459,7 +698,7 @@ Already installed containers were found on the server. All installed containers بروتوكول الشبكة - + Port منفذ @@ -581,117 +820,81 @@ Already installed containers were found on the server. All installed containers لا شئ - + TLS auth TLS مصادقة - + Block DNS requests outside of VPN احظر طلبات DNS خارج ال VPN - + Additional client configuration commands اوامر تكوين العميل الاضافية - - + + Commands: الاوامر: - + Additional server configuration commands اوامر تكوين الخادم الاضافية - - Remove OpenVPN - احذف OpenVPN + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط - - Remove OpenVPN from server? - احذف OpenVPN من الخادم? - - - - All users with whom you shared a connection with will no longer be able to connect to it. - جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - - - + Save احفظ - - All users with whom you shared a connection will no longer be able to connect to it - جميع المستخدمين الذين شاركت اتصال معهم لن يستطيعو الاتصال بعد الان - - - - Continue - واصل - - - - Cancel - إلغاء - PageProtocolRaw - + settings إعدادات - + Show connection options اظهر اختيارات الاتصال - Connection options - اختيارات الاتصال - - - + Connection options %1 %1 اختيارات الاتصال - + Remove احذف - + Remove %1 from server? احذف %1 من الخادم ? - + All users with whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - from server? - من الخادم - - - All users with whom you shared a connection will no longer be able to connect to it - جميع المستخدمين الذين شاركت اتصال معهم لن يستطيعو الاتصال بعد الان - - - + Continue واصل - + Cancel إلغاء @@ -699,110 +902,192 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Shadowsocks إعدادات - + Port منفذ - - + + Cipher تشفير - + Save احفظ + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + + + + PageProtocolWireGuardClientSettings + + + WG settings + إعدادات WG + + + + MTU + + + + + Server settings + + + + + Port + منفذ + + + + Save + احفظ + + + + Save settings? + احفظ الإعدادات؟ + + + + Only the settings for this device will be changed + + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + PageProtocolWireGuardSettings - + WG settings - + إعدادات WG - + + VPN address subnet + الشبكة الفرعية لعنوان VPN + + + Port - منفذ + منفذ - - MTU - + + Save settings? + احفظ الإعدادات؟ - - Remove WG - + + All users with whom you shared a connection with will no longer be able to connect to it. + جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - - Remove WG from server? - - - - - All users with whom you shared a connection will no longer be able to connect to it. - جميع المستخدمين الذين شاركت معاهم اتصال لن يستطيعو الاتصال بعد الان. - - - + Continue - واصل + واصل - + Cancel - إلغاء + إلغاء - + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + + + Save احفظ + + PageProtocolXraySettings + + + XRay settings + إعدادات XRay + + + + Disguised as traffic from + متنكراً في حركة مرور من + + + + Save + احفظ + + + + Unable change settings while there is an active connection + لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط + + PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. تم تثبيت خدمة DNS علي الخادم الخاص بك, و فقط متاح من خلال VPN. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. عنوان ال DNS متطابق لنفس عنوان الخادم بك, يمكنك تهيئة DNS في الاعدادات, تحت علامة تبويب الاتصال. - + Remove احذف - + Remove %1 from server? احذف %1 ? - from server? - من الخادم + + Cannot remove AmneziaDNS from running server + لا يمكن إزالة AmneziaDNS من الخادم قيد التشغيل - + Continue واصل - + Cancel إلغاء @@ -810,183 +1095,218 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully تم تحديث الإعدادات بنجاح - + SFTP settings SFTP إعدادات - + Host استضافة - - - - + + + + Copied تم الاستنساخ - + Port منفذ - + User name اسم المستخدم - + Password كلمة المرور - + Mount folder on device قم بتثبيت المجلد علي الجهاز - + In order to mount remote SFTP folder as local drive, perform following steps: <br> لتثبيت مجلد SFTP كمحرك اقراص محلي, اتبع هذه الخطوات : <br> - - + + <br>1. Install the latest version of <br>1. تحميل اخر اصدار من - - + + <br>2. Install the latest version of <br>2. تحمير اخر اصدار من - + Detailed instructions تعليمات مفصلة + + + PageServiceSocksProxySettings - - Remove SFTP and all data stored there - امسح SFTP وجميع البيانات المخزنة + + Settings updated successfully + تم تحديث الإعدادات بنجاح - - Remove SFTP and all data stored there? - امسح SFTP وجميع البيانات المخزنة؟ + + + SOCKS5 settings + إعدادات SOCKS5 - - Continue - واصل + + Host + استضافة - - Cancel - إلغاء + + + + + Copied + تم النسخ + + + + + Port + منفذ + + + + User name + اسم المستخدم + + + + + Password + كلمة المرور + + + + Username + اسم المستخدم + + + + + Change connection settings + تغيير إعدادات الاتصال + + + + The port must be in the range of 1 to 65535 + يجب أن يكون المنفذ في النطاق من 1 إلى 65535 + + + + Password cannot be empty + لا يمكن ان تكون كلمة المرور فارغة + + + + Username cannot be empty + اسم المستخدم لا يمكن ان يكون فارغ PageServiceTorWebsiteSettings - + Settings updated successfully تم تحديث الإعدادات بنجاح - + Tor website settings Tor إعدادات متصفح - + Website address عنوان المتصفح - + Copied تم الاستنساخ - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - + When configuring WordPress set the this onion address as domain. عند تكوين WordPress قم بتعيين عنوان ال onion هذا ك domain. - - - Remove website - احذف متصفح - - - - The site with all data will be removed from the tor network. - سيتم حذف الموقع وجميع البيانات من الشبكة. - - - - Continue - واصل - - - - Cancel - إلغاء - PageSettings - + Settings إعدادات - + Servers الخوادم - + Connection الاتصال - + Application تطبيق - + Backup نسخة احتياطية - + About AmneziaVPN عن AmneziaVPN - + + Dev console + وحدة تحكم التطوير + + + Close application إغلاق التطبيق @@ -994,384 +1314,545 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - This is a free and open source application. If you like it, support the developers with a donation. -And if you don't like the app, all the more support it - the donation will be used to improve the app. - هذا تطبيق مجاني و مفتوح المصدر. إذا عجبك التطبيق, ادعم المطورين ب تبرع. - وإذا لما يعجبك, فهذا سبب اكبر لدعمة - تستخدم التبرعات في تطوير التطبيق - - - + Support Amnezia دعم Amenzia - + Amnezia is a free and open-source application. You can support the developers if you like it. هو تطبيق مجاني ومفتوح المصدر يمكنك دعم مطورين Amnezia إذا اعجبك. - + Contacts التواصل - + Telegram group مجموعة ال Telegram - + To discuss features لمناقشة الميزات - + https://t.me/amnezia_vpn_en - Mail - البريد + البريد - + + support@amnezia.org + + + + For reviews and bug reports لل مراجعات والابلاغات عن المشاكل - + + Copied + + + + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client - + Website موقع - - https://amnezia.org - + + Visit official website + - + Software version: %1 %1 :إصدار البرنامج - + Check for updates تحقق من وجود تحديثات - + Privacy Policy سياسات الخصوصية - PageSettingsApplication + PageSettingsApiLanguageList - - Application - تطبيق + + Unable change server location while there is an active connection + + + + + PageSettingsApiServerInfo + + + For the region + للمنطقة - - Allow application screenshots - اسمح بلقطات شاشة التطبيق + + Price + السعر - - Auto start - تشغيل تلقائي + + Work period + مدة العمل - Launch the application every time - شغل البرنامج كل مرة + + Valid until + - starts - يبدأ + + Speed + السرعة - - Launch the application every time the device is starts - قم بتشغيل التطبيق فكل مرة يتم فيها تشغيل الجهاز + + Support tag + علامة الدعم - - Auto connect - اتصال تلقائي + + Copied + تم النسخ - - Connect to VPN on app start - اتصل ب ال VPN عند تشغيل التطبيق + + Reload API config + إعادة تحميل تكوين API - - Start minimized - ابدأ ب الحجم الادني + + Reload API config? + إعادة تحميل تكوين API - - Launch application minimized - تشغيل التطبيق في الحد الادني - - - - Language - اللغة - - - - Logging - تسجيل - - - - Enabled - مٌفعل - - - - Disabled - مٌعطل - - - - Reset settings and remove all data from the application - إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق - - - - Reset settings and remove all data from the application? - إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق؟ - - - - All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. - سيتم ضبط الاعدادات الافتراضية. جميع خدمات AmneziaVPN المٌثبتة ستبقي علي الخادم. - - - + + Continue واصل - + + Cancel إلغاء + + + Cannot reload API config during active connection + لا يمكن إعادة تحميل تكوين API اثناء تواجد اتصال نشط + + + + Remove from application + احذف من التطبيق + + + + Remove from application? + احذف من التطبيق؟ + + + + Cannot remove server during active connection + لا يمكن إزالة الخادم أثناء الاتصال النشط + + + + PageSettingsAppSplitTunneling + + + Cannot change split tunneling settings during active connection + لا يمكن تغير إعدادات تقسيم الانفاق بينما هناك اتصال مٌفعل + + + + Only the apps from the list should have access via VPN + يجب أن تتمتع التطبيقات الموجودة في القائمة فقط بإمكانية الوصول عبر VPN + + + + Apps from the list should not have access via VPN + لا يجب ان تتمتع التطبيقات في القائمة بولوج ل VPN + + + + App split tunneling + تقسيم نفق التطبيق + + + + Mode + وضع + + + + Remove + احذف + + + + Continue + واصل + + + + Cancel + إلغاء + + + + application name + اسم التطبيق + + + + Open executable file + افتح ملف قابل للتنفيذ + + + + Executable files (*.*) + ملفات قابلة للتنفيذ (*.*) + + + + PageSettingsApplication + + + Application + تطبيق + + + + Allow application screenshots + اسمح بلقطات شاشة التطبيق + + + + Enable notifications + تفعيل الإشعارات + + + + Enable notifications to show the VPN state in the status bar + تفعيل الإشعارات لإظهار حالة ال VPN في شريط الحالة + + + + Auto start + تشغيل تلقائي + + + + Launch the application every time the device is starts + قم بتشغيل التطبيق فكل مرة يتم فيها تشغيل الجهاز + + + + Auto connect + اتصال تلقائي + + + + Connect to VPN on app start + اتصل ب ال VPN عند تشغيل التطبيق + + + + Start minimized + ابدأ ب الحجم الادني + + + + Launch application minimized + تشغيل التطبيق في الحد الادني + + + + Language + اللغة + + + + Logging + تسجيل + + + + Enabled + مٌفعل + + + + Disabled + مٌعطل + + + + Reset settings and remove all data from the application + إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق + + + + Reset settings and remove all data from the application? + إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق؟ + + + + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. + سيتم ضبط الاعدادات الافتراضية. جميع خدمات AmneziaVPN المٌثبتة ستبقي علي الخادم. + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Cannot reset settings during active connection + لا يمكن إعادة ضبط الإعدادات اثناء تواجد اتصال فعال + PageSettingsBackup - + Settings restored from backup file تم إعادة الاعدادات من ملف نسخة احتياطية - - It will help you instantly restore connection settings at the next installation - سيساعدك علي إعادة إعدادات الاتصال بسرعة عند إعادة تثبيت التطبيق - Back up your configuration قم بعمل نسخة احتياطية - + You can save your settings to a backup file to restore them the next time you install the application. يمكنك حفظ الإعدادات في ملف نسخة احتياطية لأعادتهم في المرة القادمة التي تثبت فيها التطبيق. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. ستحتوي النسخة الاحتياطية علي كلمات مرورك و المفاتيح الخاصة للخوادم المٌضافة إلي AmneziaVPN. احفظ هذه المعلومات في مكان امن. - + Make a backup إضافة نسخة احتياطية - + Save backup file احفظ ملف النسخه الاحتياطيه - - + + Backup files (*.backup) ملفات نٌسخ احتياطية (*.backup) - + Backup file saved تم حفظ ملف النسخ الاحتياطي - + Restore from backup استرجاع من ملف يحتوي علي نسخة احتياطية - + Open backup file افتح ملف نسخ احتياطي - + Import settings from a backup file? استرد الإعدادات من ملف نسخ احتياطي؟ - + All current settings will be reset ستتم إعادة ضبط جميع الإعدادات الحالية - + Continue واصل - + Cancel إلغاء + + + Cannot restore backup settings during active connection + لا يمكن استعادة إعدادات النسخ الاحتياطي أثناء الاتصال النشط + PageSettingsConnection - + Connection الاتصال - + When AmneziaDNS is not used or installed عندما يكون AmneziaDNS غير مٌثبت او غير مستخدم - + Allows you to use the VPN only for certain Apps يسمح لك بأستخدام ال VPN علي تطبيقات معينة - Use AmneziaDNS if installed on the server - استخدم AmneziaDNS إذا كان مٌثبت علي الخادم - - - + Use AmneziaDNS استخدم AmneziaDNS - + If AmneziaDNS is installed on the server في حالة كان AmneziaDNS مٌثبت علي الخادم - + DNS servers خوادم DNS - + Site-based split tunneling انقسام الانفاق القائم علي الموقع - + Allows you to select which sites you want to access through the VPN يسمح لك بتحديد اي موقع تريد الوصول له عن طريق ال VPN - + App-based split tunneling انقسام الانفاق القائم علي التطبيق - Split site tunneling - قسم نفق الموقع + + KillSwitch + - Allows you to connect to some sites through a secure connection, and to others bypassing it - يسمحلك بألاتصال ببعض المواقع بسرية, وعلي الاخرين تجاوزه + + Disables your internet if your encrypted VPN connection drops out for any reason. + يعطل اتصال الإنترنت الخاص بك إذا انقطع اتصال VPN المشفر لأي سبب من الأسباب. - Separate application tunneling - فرق نفق التطبيق + + Cannot change killSwitch settings during active connection + لا يمكن تغيير إعدادات KillSwitch اثناء تواجد اتصال فعال PageSettingsDns - + Default server does not support custom DNS الخادم الافتراضي لا يدعم DNS مخصص - + DNS servers خوادم ال DNS - + If AmneziaDNS is not used or installed AmneziaVPN ليس مٌستخدم او مٌثبت - + Primary DNS الرئيسي DNS - + Secondary DNS الثانوي DNS - + Restore default استعادة الافتراضي - + Restore default DNS settings? قم بأعادة ضبط إعدادات ال DNS الافتراضية؟ - + Continue واصل - + Cancel إلغاء - + Settings have been reset لم يتم إعادة ضبط الإعدادات - + Save احفظ - + Settings saved تم حفظ الإعدادات @@ -1379,72 +1860,106 @@ And if you don't like the app, all the more support it - the donation will PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + تم تمكين التسجيل. لاحظ أنه سيتم تعطيل السجلات تلقائيًا بعد 14 يومًا، وسيتم حذف جميع ملفات السجل. - + Logging التسجيل - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. سيتم حفظ سجلات البرنامج بشكل تلقائي عند تفعيل هذه الميزة, بشكل افتراضي, هذه الميزة مٌعطلة. قم بتفعيل هذه الميزة في حالة هناك خلل في التطبيق. - Save logs - احفظ السجلات + احفظ السجلات - Open folder with logs - افتح مجلد يحتوي علي سجلات + افتح مجلد يحتوي علي سجلات - + + Save احفظ - + + Logs files (*.log) ملفات الولوج (*.log) - + + Logs file saved تم حفظ ملف السجل - Save logs to file - احفظ السجلات في ملف + احفظ السجلات في ملف - + + Enable logs + + + + Clear logs? مسح السجلات؟ - + Continue واصل - + Cancel إلغاء - + Logs have been cleaned up تم مسح السجلات - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs احذف السجلات @@ -1452,37 +1967,22 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerData - + All installed containers have been added to the application تمت إضافة جميع الحاويات المٌثبتة للتطبيق - + No new installed containers found - لم يتم العثور علي اي حاويات جديدة مٌثبتة + لم يتم العثور علي اي خدمات مٌثبتة سابقاً - - Clear Amnezia cache - حذف ذاكرة تخزين Amnezia المؤقتة - - - - May be needed when changing other settings - قد يكون ضروري عند تغير الإعدادات الاخري - - - - Clear cached profiles? - حذف الملفات الشخصية المخزنة مؤقتاً؟ - - - + Do you want to reboot the server? هل تريد إعادة تشغيل الخادم؟ - + Do you want to clear server from Amnezia software? هل تريد حذف الخادم من Amnezia? @@ -1492,75 +1992,93 @@ And if you don't like the app, all the more support it - the donation will - - - - - + + + + Continue واصل - - - - - + + + + Cancel إلغاء - + Check the server for previously installed Amnezia services افحص الخادم عن اي خدمات Amnezia مٌثبتة سابقاُ - + Add them to the application if they were not displayed اضفهم إلي التطبيق إذا لم يكونو ظاهرين - + Reboot server إعادة تشغيل الخادم - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? عملية إعادة التشغيل قد تستغرق 30 ثانية, هل تريد الاستكمال؟ - + + Cannot reboot server during active connection + لا يمكن إعادة تشغيل الخادم أثناء الاتصال النشط + + + Remove server from application احذف خادم من التطبيق - + Do you want to remove the server from application? هل تريد حذف الخادم من التطبيق؟ - + + Cannot remove server during active connection + لا يمكن إزالة الخادم أثناء الاتصال النشط + + + All users whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يستطيعو الاتصال مرة اخري. - + + Cannot clear server from Amnezia software during active connection + لا يمكن مسح الخادم من برنامج Amnezia أثناء الاتصال النشط + + + Reset API config إعادة تكوين API - + Do you want to reset API config? هل تريد إعادة تكوين API? - + + Cannot reset API config during active connection + لا يمكن إعادة تعيين تكوين API أثناء الاتصال النشط + + + All installed AmneziaVPN services will still remain on the server. جميع خدمات AmneziaVPN المٌثبتة ستظل علي الخادم. - + Clear server from Amnezia software احذف خادم من Amnezia @@ -1568,27 +2086,32 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerInfo - + + Subscription is valid until + + + + Server name اسم الخادم - + Save احفظ - + Protocols البروتوكولات - + Services الخدمات - + Management الإدارة @@ -1596,40 +2119,83 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerProtocol - + settings الإعدادات - + Clear %1 profile + مسح ملف تعريف %1 + + + + Clear %1 profile? + مسح ملف تعريف %1؟ + + + + + + + + + Unable to clear %1 profile while there is an active connection + غير قادر على مسح ملف تعريف %1 أثناء وجود اتصال نشط + + + Remove احذف - + + Remove %1 from server? + احذف %1 من الخادم؟ + + + All users with whom you shared a connection will no longer be able to connect to it. جميع المستخدمين الذين شاركت معاهم اتصال لن يستطيعو الاتصال بعد الان. - from server? - من الخادم؟ + + Cannot remove active container + لا يمكن إزالة الحاوية النشطة - - Remove %1 from server? - احذف %1 من الخادم ? - - - All users with whom you shared a connection will no longer be able to connect to it - جميع المستخدمين الذين شاركت اتصال معهم لن يستطيعو الاتصال بعد الان - - - + + Continue واصل - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Cancel إلغاء @@ -1637,7 +2203,7 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServersList - + Servers الخوادم @@ -1645,221 +2211,347 @@ And if you don't like the app, all the more support it - the donation will PageSettingsSplitTunneling - Only the addresses in the list must be opened via VPN - يجب فتح العنواين التي في القائمة عبر VPN - - - Addresses from the list should never be opened via VPN - لا يجب ابداً فتح العنواين التي في القائمة عن طريق VPN - - - Split site tunneling - قسم نفق الموقع - - - + Default server does not support split tunneling function السرفر الافتراضي لا يدعم ميزة تقسيم الانفاق - + Addresses from the list should not be accessed via VPN لا يجب الولوج للعنواين المذكورة هنا من خلال ال VPN - + Split tunneling تقسيم الانفاق - + Mode وضع - + Remove احذف - + Continue واصل - + Cancel إلغاء - + Only the sites listed here will be accessed through the VPN سيتم الولوج للمواقع المذكورة هنا فقط عن طريق ال VPN - + Cannot change split tunneling settings during active connection لا يمكن تغير إعدادات تقسيم الانفاق بينما هناك اتصال مٌفعل - + website or IP موقع او IP - + Import / Export Sites - + Import استرد - + Save site list احفظ قائمة المواقع - + Save sites احفظ المواقع - - - + + + Sites files (*.json) - + Import a list of sites استرد قائمة من المواقع - + Replace site list تبديل قائمة المواقع - - + + Open sites file افتح ملف المواقع - + Add imported sites to existing ones إضافة المواقع المستردة للمواقع الموجودة + + PageSetupWizardApiServiceInfo + + + For the region + للمنطقة + + + + Price + السعر + + + + Work period + مدة العمل + + + + Speed + السرعة + + + + Features + المميزات + + + + Connect + اتصل + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + VPN بواسطة Amnezia + + + + Choose a VPN service that suits your needs. + اختر خدمة VPN تلبي احتياجاتك + + PageSetupWizardConfigSource - - Server connection - اتصال الخادم + + Connection + الاتصال - Do not use connection code from public sources. It may have been created to intercept your data. - -It's okay as long as it's from someone you trust. - لا تستخدم رمز الاتصال من المصادر العامة. ربما تم إنشاؤه لاعتراض بياناتك - -لا بأس طالما انه من شخص تثق به. + + Settings + إعدادات - - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - لا تستخدم رموز اتصال من مصادر غير موثوقة, حيث قد يكون تم إنشاؤها لاعتراض بياناتك. + + Enable logs + - - What do you have? - ماذا لديك؟ + + Support tag + علامة الدعم - - File with connection settings or backup - ملف إعدادات اتصال او نسخ احتياطي + + Copied + - + + Insert the key, add a configuration file or scan the QR-code + أدخل المفتاح، أضف ملف تكوين أو امسح رمز الاستجابة السريعة + + + + Insert key + أدخل مفتاح + + + + Insert + أدخل + + + + Continue + واصل + + + + Other connection options + اختيارات اتصال اخري + + + + Site Amnezia + + + + + VPN by Amnezia + VPN بواسطة Amnezia + + + + Connect to classic paid and free VPN services from Amnezia + اتصل بخدمات VPN الكلاسيكية المدفوعة والمجانية من Amnezia + + + + Self-hosted VPN + VPN ذاتية الاستضافة + + + + Configure Amnezia VPN on your own server + قم بتكوين Amnezia VPN على الخادم الخاص بك + + + + Restore from backup + استرجاع من ملف يحتوي علي نسخة احتياطية + + + + + + + + + Open backup file + افتح ملف نسخ احتياطي + + + + Backup files (*.backup) + ملفات نٌسخ احتياطية (*.backup) + + + File with connection settings ملف إعدادات اتصال - + + + + + + Open config file افتح ملف تكوين - + QR code رمز QR - - Key as text - مفتاح كنص + + + + + + + I have nothing + ليس لدي اي شئ + + + + + PageSetupWizardCredentials - + Configure your server تكوين الخادم الخاص بك - + Server IP address [:port] عنوان خادم IP [:منفذ] - + Continue واصل - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties ستظل جميع البيانات التي تدخلها سرية للغاية ولن تتم مشاركتها أو الكشف عنها ل Amnezia أو أي طرف ثالث - + 255.255.255.255:22 - + SSH Username - + Password or SSH private key كلمة مرور او مفتاح SSH خاص - + + How to run your VPN server + كيف تقوم بتشغيل خادم ال VPN الخاص بك + + + + Where to get connection data, step-by-step instructions for buying a VPS + اين تحصل علي بيانات الاتصال, تعليمات خطوة ب خطوة لشراء VPS + + + Ip address cannot be empty لا يمكن لعنوان IP ان يكون فارغ - + Enter the address in the format 255.255.255.255:88 ادخل العنوان في شكل 255.255.255.255:88 - + Login cannot be empty تسجيل دخول لا يمكن ان يكون فارغ - + Password/private key cannot be empty كلمة مرور/مفتاح خاص لأ يمكن ان يكونو فارغين @@ -1867,22 +2559,22 @@ It's okay as long as it's from someone you trust. PageSetupWizardEasy - + What is the level of internet control in your region? ما هو مستوي التحكم في الانترنت في منطقتك؟ - + Choose a VPN protocol اختر بروتوكول VPN - + Skip setup تخطي الإعداد - + Continue واصل @@ -1890,38 +2582,38 @@ It's okay as long as it's from someone you trust. PageSetupWizardInstalling - - + + Usually it takes no more than 5 minutes عادة لا تستغرق اكثر من 5 دقائق - + The server has already been added to the application تمت إضافة الخادم بالفعل للتطبيق - + Amnezia has detected that your server is currently اكتشف Amnezia الخادم الخاص بك موجود حاليًا - + busy installing other software. Amnezia installation مشغول بتثبيت برامج اخري, تثبيت Amnezia - + Cancel installation إلغاء التثبيت - + will pause until the server finishes installing other software سيتوقف مؤقتًا حتى ينتهي الخادم من تثبيت البرامج الأخرى - + Installing جاري التثبيت @@ -1929,45 +2621,50 @@ It's okay as long as it's from someone you trust. PageSetupWizardProtocolSettings - + Installing %1 تثبيت %1 - + More detailed اكثر تفصيلاً - + Close اغلق - + Network protocol بروتوكول شبكة - + Port منفذ - + Install تثبيت + + + The port must be in the range of 1 to 65535 + يجب أن يكون المنفذ في النطاق من 1 إلى 65535 + PageSetupWizardProtocols - + VPN protocol VPN بروتوكول - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. اختر بالنسبة للأولوية القصوى بالنسبة لك. ويمكنك لاحقًا تثبيت بروتوكولات وخدمات إضافية أخرى، مثل وكيل DNS وSFTP. @@ -1975,7 +2672,7 @@ It's okay as long as it's from someone you trust. PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. قم بتوجيه الكاميرا نحو رمز QR و اثبت لبضع ثوان. @@ -1983,34 +2680,9 @@ It's okay as long as it's from someone you trust. PageSetupWizardStart - - Settings restored from backup file - تم استرداد الإعدادات من ملف نسخة احتياطية - - - - Free service for creating a personal VPN on your server. - خدمة مجانية لأنشاء VPN شخصي علي الخادم الشخصي. - - - - Helps you access blocked content without revealing your privacy, even to VPN providers. - يساعدك في الولوج للمحتوي المحظور بدون إظهار خصوصيات, حتي لمزود ال VPN. - - - - I have the data to connect - لدي البيانات المطلوبة للأتصال - - - - I have nothing - ليس لدي اي شئ - - - - https://amnezia.org/instructions/0_starter-guide - + + Let's get started + هيا نبدأ @@ -2037,7 +2709,7 @@ It's okay as long as it's from someone you trust. ادخل - + Continue واصل @@ -2045,27 +2717,32 @@ It's okay as long as it's from someone you trust. PageSetupWizardViewConfig - + New connection اتصال جديد - + Collapse content طي المحتوي - + Show content اظهر المحتوي - + + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. + تمكين تشويش WireGuard. قد يكون من المفيد إذا تم حظر WireGuard على مزود الخدمة الخاص بك. + + + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. استخدم رموز اتصال فقط من المصادر التي تثق بها, ربما تم إنشاء رموز من مصادر عامة لاعتراض بياناتك. - + Connect اتصل @@ -2073,194 +2750,212 @@ It's okay as long as it's from someone you trust. PageShare - + Save OpenVPN config احفظ تكوين OpenVPN - + Save WireGuard config احفظ تكوين WireGuard - + Save AmneziaWG config احفظ تكوين AmneziaWG - + Save Shadowsocks config احفظ تكوين Shadowsocks - + Save Cloak config احفظ تكوين Cloak - + + Save XRay config + حفظ تكوين XRay + + + For the AmneziaVPN app AmneziaVPN من اجل تطبيق - + OpenVPN native format تنسيق OpenVPN الاصلي - + WireGuard native format تنسيق WireGuard الاصلي - + AmneziaWG native format تنسيق AmneziaWG اصلي - + Shadowsocks native format تنسيق Shadowsocks الاصلي - + Cloak native format تنسيق Cloak الاصلي - + + XRay native format + الشكل الاصلي ل XRay + + + Share VPN Access شارك اتصال VPN - + Share full access to the server and VPN شارك ولوج كامل للخادم و ال VPN - + Use for your own devices, or share with those you trust to manage the server. استخدمه للأجهزة الخاصة بك، أو شاركه مع من تثق بهم لإدارة الخادم. - - + + Users المستخدمين - + Share VPN access without the ability to manage the server شارك اتصال VPN بدون القدرة علي إدارة الخادم - + Search ابحث - - Creation date: - تاريخ الإنشاء: + + Creation date: %1 + تاريخ الإنشاء: %1 - + + Latest handshake: %1 + اخر تصافح: %1 + + + + Data received: %1 + البيانات المستلمة: %1 + + + + Data sent: %1 + البيانات المٌرسلة: %1 + + + + Allowed IPs: %1 + + + + Rename إعادة التسمية - + Client name اسم العميل - + Save احفظ - + Revoke سحب وإبطال - + Revoke the config for a user - %1? سحب وإبطال للمستخدم - %1? - + The user will no longer be able to connect to your server. المستخدم لن يكون قادر علي الاتصال بعد الان. - + Continue واصل - + Cancel إلغاء - + Connection الاتصال - Full access to server - ولوج كامل للخادم - - - Servers - الخوادم - - - - + + Server خادم - + File with connection settings to ملف بإعدادات إلي - Protocols - البروتوكولات - - - - + + Protocol بروتوكول - + Connection to اتصال إلي - + Config revoked تم سحب وإبطال التكوين - + User name اسم المستخدم - - + + Connection format تنسيق الاتصال - - + + Share شارك @@ -2268,50 +2963,55 @@ It's okay as long as it's from someone you trust. PageShareFullAccess - + Full access to the server and VPN ولوج كامل للخادم و ال VPN - + We recommend that you use full access to the server only for your own additional devices. نحن ننصحك بأستخدام ولوج كامل للخادم فقط لأجهزتك الاضافية. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. إذا شاركت ولوج كامل مع الاشخاص, سيكونو قادرين علي حذف وإضافة بروتوكولات و خدمات إلي الخادم, والذي سيجعل VPN يعمل بشكل غير صحيح لجميع المستخدمين. - - + + Server خادم - + Accessing التواصل - + File with accessing settings to ملف مع إعدادات الوصول إلي - + Share مشاركة - + + Access error! + خطأ في الوصول! + + + Connection to اتصال إلي - + File with connection settings to معلف مع إعدادات الاتصال إلي @@ -2319,15 +3019,25 @@ It's okay as long as it's from someone you trust. PageStart - + Logging was disabled after 14 days, log files were deleted + تم تعطيل التسجيل بعد 14 يومًا، وتم حذف ملفات السجل + + + + Settings restored from backup file + تم تحميل الإعدادات من ملف نسخة احتياطية + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. PopupType - + Close اغلاق @@ -2361,12 +3071,12 @@ It's okay as long as it's from someone you trust. لم يتم العثور علي كلمة المرور - + Could not open keystore فشل فتح مخزن المفاتيح - + Could not remove private key from keystore فشل حذف المفتاح الخاص من مخزن المفاتيح @@ -2542,27 +3252,27 @@ It's okay as long as it's from someone you trust. فشل في فتح مخزن المفاتيح - + Could not create private key generator فشل ف إنشاء مولد المفاتيح الخاصة - + Could not generate new private key فشل في إنشاء مفتاح خاص جديد - + Could not retrieve private key from keystore فشل في استرداد مفتاح خاص من مخزن المفاتيح - + Could not create encryption cipher فشل في إنشاء شفرة التشفير - + Could not encrypt data فشل في تشفير الداتا @@ -2570,10 +3280,17 @@ It's okay as long as it's from someone you trust. QObject - + SFTP service خدمة SFTP + + + + + SOCKS5 proxy server + + No error @@ -2581,6 +3298,7 @@ It's okay as long as it's from someone you trust. + Unknown error خطأ غير معروف @@ -2590,269 +3308,280 @@ It's okay as long as it's from someone you trust. لم يتم تنفيذ الوظيفة - + Server check failed فشل في فحص الخادم - + Server port already used. Check for another software منفذ الخادم بالفعل مٌستخدم, تحقق من باقي التطبيقات - + Server error: Docker container missing خطأ من الخادم: حاوية Docker مفقودة - + Server error: Docker failed خطأ من الخادم: فشل Docker - + Installation canceled by user تم اغلاق التثبيت بواسطة المستخدم - + The user does not have permission to use sudo ليس لدي المستخدم الصلحيات لأستخدام sudo - + SSH request was denied طلب SSH محظو - + SSH request was interrupted إنقطع طلب SSH - + SSH internal error مشكلة داخلية SSH - + Invalid private key or invalid passphrase entered مفتا ح خاص غير صحيح او عبارة مرور غير صحيحة - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types التنسيق المٌحدد للمفتاح الخاص غير مدعوم, استخدم نوع مفتاح openssh ED25519 او نوع مفتاح PEM - + Timeout connecting to server انتهت مدة الاتصال بالخادم - Sftp error: File does not exist - خطأ Sftp: الملف غير موجود - - - Sftp error: Permission denied - خطأ Sftp: تم حظر الصلحيات - - - Sftp error: Generic failure - خطأ Sftp: فشل عام - - - Sftp error: Garbage received from server - خطأ Sftp: تم استلام نفايات من الخادم - - - Sftp error: No connection has been set up - خطأ Sftp: لم يتم إعداد اتصال - - - Sftp error: There was a connection, but we lost it - خطأ Sftp: كان هناك اتصال, ولكن خسرناه - - - Sftp error: Operation not supported by libssh yet - خطأ Sftp: العملية ليست مدعومة من libssh بعد - - - Sftp error: No such file or directory path exists - خطأ Sftp: لا يوجد مسار ملف او مجلد مثل هذا - - - Sftp error: An attempt to create an already existing file or directory has been made - خطأ Sftp: محاولة إنشاء ملف او مجلد موجود بالفعل - - - Sftp error: Write-protected filesystem - خطأ Sftp: نظام كتابة الملفات محمي - - - Sftp error: No media was in remote drive - خطأ Sftp: لا يوجد وسائط في القرص البعيد - - - + VPN connection error - + Error when retrieving configuration from API خطأ عند استرداد التكوين من API - + This config has already been added to the application هذا التكوين بالفعل تمت إضافتة للبرنامج - + ErrorCode: %1. - + OpenVPN config missing OpenVPN تكوين مفقود - - SCP error: Generic failure - + + Background service is not running + خدمة الخلفية ليست قيد التشغيل - + + Server error: Packet manager error + خطأ في الخادم: خطأ في مدير الحزم + + + + SCP error: Generic failure + خطأ SCP: فشل عام + + + OpenVPN management server error OpenVPN خطأ في إدارة الخادم - + OpenVPN executable missing OpenVPN executable مفقود - + Shadowsocks (ss-local) executable missing Shadowsocks (ss-local) executable مفقود - + Cloak (ck-client) executable missing Cloak (ck-client) executable مفقود - + Amnezia helper service error خطأ في خدمة مٌساعد Amnezia - + OpenSSL failed فشل OpenSSL - + Can't connect: another VPN connection is active لا يمكن الاتصال: هناك اتصال VPN اخر بالفعل يعمل - + Can't setup OpenVPN TAP network adapter لا يمك نتثبيت محول شبكة OpenVPN TAP - + VPN pool error: no available addresses VPN pool error: لا يوجد عنواين مٌتاحة - + The config does not contain any containers and credentials for connecting to the server التكوين لا يحتوي علي اي حاويات و اعتماد للأتصال بالخادم - - QFile error: The file could not be opened + + Unable to open config file - QFile error: An error occurred when reading from the file - + In the response from the server, an empty config was received + في الاستجابة من الخادم، تم تلقي تكوين فارغ - QFile error: The file could not be accessed - + SSL error occurred + حدث خطأ SSL - QFile error: An unspecified error occurred - + Server response timeout on api request + انتهت مهلة استجابة الخادم عند طلب واجهة برمجة التطبيقات - QFile error: A fatal error occurred - + Missing AGW public key + مفتاح AGW عام مفقود - QFile error: The operation was aborted + Failed to decrypt response payload + + + + + Missing list of available services + QFile error: The file could not be opened + خطأ QFile: لا يمكن فتح الملف + + + + QFile error: An error occurred when reading from the file + خطأ QFile: ظهر خطأ اثناء القراءه من الملف + + + + QFile error: The file could not be accessed + خطأ QFile: لا يمكن الوصول للملف + + + + QFile error: An unspecified error occurred + خطأ QFile: ظهر خطأ غير محدد + + + + QFile error: A fatal error occurred + خطأ QFile: حدث خطأ فادح + + + + QFile error: The operation was aborted + خطأ QFile: تم إحباط العملية + + + Internal error خطأ داخلي - + IPsec - - + + Website in Tor network موقع في شبكة Tor - + AmneziaDNS AmneziaDNS - + SFTP file sharing service ملف SFTP: خدمة المشاركة - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. بروتوكول OpenVPN احد اشهر بروتوكولات VPN, مع مرونة في إعدادات التكوين. يستخدم بروتوكول امان خاص به مع SSL/TLS لتغير المفاتيح. - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. بروتوكول Shadowsocks- يتنكر في حركة مرور VPN, يبدو ك حركة مرور الويب العادية ولكن قد يتم التعرف عليه من خلال أنظمة التحليل في بعض المناطق شديدة الرقابة. - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. بروتوكول OpenVPN over Cloak هو OpenVPN مع VPN يتنكر كحركة مرور على الويب ويوفر الحماية ضد عمليات الكشف النشط. مثالية لتجاوز الحجب في المناطق ذات أعلى مستويات الرقابة. - + + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. + XRay مع REALITY - مناسبة للبلدان التي لديها أعلى مستوى من الرقابة على الإنترنت. إخفاء حركة المرور كحركة مرور على الويب على مستوى TLS، والحماية من الكشف عن طريق طرق التحقيق النشطة. + + + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + IKEv2/IPsec - بروتوكول مستقر حديث، أسرع قليلاً من البروتوكولات الأخرى، يستعيد الاتصال بعد فقدان الإشارة. يتمتع بدعم أصلي على أحدث إصدارات Android وiOS. + + + Create a file vault on your server to securely store and transfer files. انشأ مخزن ملفات علي الخادم الخاص بك حتي تخزن الملفات و تنقلها بسرية. - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -2893,7 +3622,7 @@ Cloak يحمي OpenVPN من ان يٌكتشف والحجب - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -2914,7 +3643,18 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * يعمل عبر بروتوكول شبكة UDP. - + + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. + تم تصميم بروتوكول REALITY، وهو تطور رائد قام به مبدعو XRay، خصيصًا لمواجهة أعلى مستويات الرقابة على الإنترنت من خلال نهجه الجديد في التهرب. +فهو يحدد بشكل فريد الرقباء أثناء مرحلة مصافحة TLS، ويعمل بسلاسة كوكيل للعملاء الشرعيين بينما يحول الرقباء إلى مواقع الويب الأصلية مثل google.com، وبالتالي يقدم شهادة وبيانات TLS أصلية. +هذه الإمكانية المتقدمة تميز REALITY عن التقنيات المشابهة من خلال قدرتها على إخفاء حركة مرور الويب على أنها قادمة من مواقع عشوائية وشرعية دون الحاجة إلى تكوينات محددة. +على عكس البروتوكولات القديمة مثل VMess وVLESS ونقل XTLS-Vision، فإن التعرف المبتكر على "الصديق أو العدو" من REALITY عند مصافحة TLS يعزز الأمان ويتحايل على الكشف بواسطة أنظمة DPI المتطورة التي تستخدم تقنيات التحقيق النشطة. وهذا يجعل من REALITY حلاً قويًا للحفاظ على حرية الإنترنت في البيئات التي تخضع لرقابة صارمة. + + + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -2933,32 +3673,27 @@ For more detailed information, you can إيجادها في قسم الدعم تحت "انشاء مخزن ملفات SFTP." - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. بروتوكول WireGuard - بروتوكول شائع ب اداء عالي, سرعة عالية واستهلاك قليل للطاقة. ينصح للمناطق ذات مستوي منخفض من الرقابة. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. بروتوكول AmneziaWG - بروتوكول خاص من Amnezia, يعتمد علي WireGuard. سريع مثل WireGuard, لكن مقاوم جداً للحجب. ينصح للمناطق ذات مستوي عالي من الرقابة. - - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - بروتوكول IKEv2/IPsec - بروتوكول مستقر حديث, اسرع بقليل من الباقي, يسترجع الاتصال بعد خسارة الاشارة. - - - + Deploy a WordPress site on the Tor network in two clicks. انشر موقع WordPress علي شبكة Tor في ضغطتين. - + Replace the current DNS server with your own. This will increase your privacy level. استبدل خادم ال DNS الحالي مع الخادم الخاص بك, هذا سيزيد من خصوصيتك. - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -2977,7 +3712,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * يمكن ان يعمل علي بروتوكولات شبكة TCP و UDP. - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -2992,7 +3727,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * يعمل عبر بروتوكول شبكة TCP. - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -3013,7 +3748,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin * يعمل عبر بروتوكول شبكة UDP. - + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -3033,7 +3768,7 @@ While it offers a blend of security, stability, and speed, it's essential t * يعمل عبر بروتوكول شبكة UDP, منفذ 500 و منفذ 4500. - + DNS Service خدمة ال DNS @@ -3087,35 +3822,169 @@ While it offers a blend of security, stability, and speed, it's essential t No match لا تطباق - - - Unknown error - خطأ غير معروف - error 0x%1: %2 خطأ %1: %2 + + + vmess:// url is invalid + عنوان //:vmess غير صحيح + + + + Invalid streamSettings protocol: + بروتوكول streamSettings غير صحيح: + + + + Unknown transport method: + طريقة نقل غير معروفة: + + + + VMess string should start with 'vmess://' + نص VMess يجب ان يبدأ ب '//:vmess' + + + + VMess string should be a valid base64 string + نص VMess يجب ان يكون نص base64 صحيح + + + + JSON should not be empty + لا يجب ان يكون JSON فارغ + + + + VLESS link should start with vless:// + رابط VLESS يجب ان يبدأ ب //:vless + + + + link parse failed: %1 + فشل تحليل الرابط: %1 + + + + empty host + مضيف فارغ + + + + missing port + منفذ مفقود + + + + missing uuid + uuid مفقود + + + + Invalid ssd link: json: field %1 must exist + رابط ssd غير صالح: json: يجب أن يكون الحقل %1 موجودًا + + + + Invalid ssd link: json: field %1 must be valid port number + رابط ssd غير صالح: json: الحقل %1 يجب أن يكون رقم منفذ صالحًا + + + + Invalid ssd link: json: field %1 must be of type 'string' + رابط ssd غير صالح: json: يجب أن يكون الحقل %1 من النوع "string" + + + + Invalid ssd link: json: field %1 must be an array + رابط ssd غير صالح: json: الحقل %1 يجب أن يكون قائمة + + + + Skipping invalid ssd server: server must be an object + تخطي خادم ssd غير صالح: يجب أن يكون الخادم كائنًا + + + + Skipping invalid ssd server: missing required field %1 + تخطي خادم ssd غير صالح: الحقل المطلوب %1 مفقود + + + + Skipping invalid ssd server: field %1 should be of type 'string' + تخطي خادم ssd غير صالح: يجب أن يكون الحقل %1 من النوع "string" + + + + Invalid ssd link: should begin with ssd:// + رابط ssd غير صالح: يجب أن يبدأ بـ //:ssd + + + + Invalid ssd link: base64 parse failed + رابط SSD غير صالح: فشل تحليل Base64 + + + + Invalid ssd link: json parse failed + رابط ssd غير صالح: فشل تحليل json + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + رابط ssd غير صالح: تشفير rc4-md5 غير مدعوم بواسطة v2ray-core + + + + SS URI is too short + عنوان SS قصير جداً + + + + + Can't find the colon separator between method and password + لا يمكن العثور على فاصل النقطتين بين method وكلمة المرور + + + + Can't find the at separator between password and hostname + لا يمكن العثور على فاصل النقطتين بين كلمة المرور وكلمة واسم المستضيف + + + + Can't find the colon separator between hostname and port + لا يمكن العثور على فاصل النقطتين بين اسم المستضيف و المنفذ + SelectLanguageDrawer - + Choose language اختر لغة + + ServersListView + + + Unable change server while there is an active connection + لا يمكن تغير الخادم بينما هناك اتصال مفعل + + Settings - + Server #1 خادم #1 - - + + Server خادم @@ -3123,61 +3992,52 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted ملف النسخه الاحتياطيه تالف - + All settings have been reset to default values تم استرجاع جميع الإعدادات للإعدادات الافتراضية - - - Cached profiles cleared - تم حذف الملفات الشخصية المٌخزنة مؤقتاُ - ShareConnectionDrawer - - + + Save AmneziaVPN config احفظ تكوين AmneziaVPN - + Share شارك - + Copy انسخ - - + + Copied تم النسخ - + Copy config string انسخ نص التكوين - + Show connection settings اظهر إعدادات الاتصال - Show content - 展示内容 - - - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" حتي تقرأ رمز ال QR في تطبيق Amnezia, اختار "إضافة خادم" - "لدي بيانات الاتصال" - "رمز Qr, او مفتاح تعريف او ملف إعدادات" @@ -3200,27 +4060,27 @@ While it offers a blend of security, stability, and speed, it's essential t تم حذف الموقع: %1 - + Can't open file: %1 لا يمكن فتح ملف: %1 - + Failed to parse JSON data from file: %1 فشل قراءه بيانات JSON من الملف: %1 - + The JSON data is not an array in file: %1 بيانات ال JSON ليست مصفوفة في الملف: %1 - + Import completed اكتمل الاستيراد - + Export completed اكتمل التصدير @@ -3261,7 +4121,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty الحقل لا يمكن ان يكون فارغ @@ -3269,7 +4129,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps @@ -3277,42 +4137,42 @@ While it offers a blend of security, stability, and speed, it's essential t VpnProtocol - + Unknown غير معرف - + Disconnected انقطع الاتصال - + Preparing جاري التحضير - + Connecting... جاري الاتصال... - + Connected تم الاتصال - + Disconnecting... جاري قطع الاتصال... - + Reconnecting... جاري إعادة الاتصال... - + Error خطأ @@ -3320,45 +4180,35 @@ While it offers a blend of security, stability, and speed, it's essential t amnezia::ContainerProps - + Low منخفض - + High متوسط او عالي - - Extreme - شديد - - - + I just want to increase the level of my privacy. انا فقط اريد زيادة مستوي الخصوصية. - + I want to bypass censorship. This option recommended in most cases. أريد تجاوز الرقابة. يوصى بهذا الخيار في معظم الحالات. - - - Most VPN protocols are blocked. Recommended if other options are not working. - يتم حظر معظم بروتوكولات VPN. يوصى به إذا كانت الخيارات الأخرى لا تعمل. - main2 - + Private key passphrase عبارة المرور الخاصة بالمفتاح - + Save احفظ diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts index e91be74b..c48606be 100644 --- a/client/translations/amneziavpn_fa_IR.ts +++ b/client/translations/amneziavpn_fa_IR.ts @@ -1,51 +1,158 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیوها، از VPN کلاسیک استفاده کنید. این VPN برای هر سایتی کار می‌کند و سرعت آن تا %1 مگابیت بر ثانیه است. + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + وی پی ان برای دسترسی به سایت‌های مسدود شده در مناطق با سانسور شدید اینترنت. + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + امنزیا پریمیوم - یک وی پی ان کلاسیک برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیو با کیفیت بالا. قابل استفاده برای تمامی سایت‌ها، حتی در کشورهایی با بالاترین سطح سانسور اینترنت. + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + امنزیا رایگان یک وی پی ان رایگان برای دور زدن مسدودیت‌ها در کشورهایی با سطح بالای سانسور اینترنت است. + + + + %1 MBit/s + %1 MBit/s + + + + %1 days + %1 روز + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + وی پی ان فقط سایت‌های محبوبی را که در منطقه شما مسدود شده‌اند، مانند اینستاگرام، فیسبوک، توییتر و غیره باز می‌کند. سایر سایت‌ها با آدرس آی‌پی واقعی شما باز خواهند شد. <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + + + + Free + رایگان + + + + %1 $/month + %1 $/ماه + + + + AppSplitTunnelingController + + + Application added: %1 + برنامه اضافه شد: %1 + + + + The application has already been added + برنامه از قبل اضافه شده است + + + + The selected applications have been added + برنامه‌های انتخاب شده اضافه شدند + + + + Application removed: %1 + برنامه حذف شد: %1 + + + + ConnectButton + + + Unable to disconnect during configuration preparation + در هنگام آماده‌سازی پیکربندی، نمی‌توان از اتصال خارج شد. + + ConnectionController - + VPN Protocols is not installed. Please install VPN container at first پروتکل وی‎پی‎ان نصب نشده است لطفا کانتینر وی‎پی‎ان را نصب کنید - + Connecting... در حال ارتباط... - + Connected متصل - + + Preparing... + در حال آماده‌سازی... + + + Settings updated successfully, reconnnection... تنظیمات به روز رسانی شد در حال اتصال دوباره... - + Settings updated successfully تنظیمات با موفقیت به‎روز‎رسانی شدند - + + The selected protocol is not supported on the current platform + پروتکل انتخاب‌شده در پلتفرم فعلی پشتیبانی نمی‌شود. + + + + unable to create configuration + نمی‌توان پیکربندی را ایجاد کرد. + + + Reconnecting... اتصال دوباره... - - - - + + + + Connect اتصال - + Disconnecting... قطع ارتباط... @@ -63,7 +170,7 @@ تنظیم سرور - + Open config file, key or QR code بارگذاری فایل تنظیمات، کلید یا QR Code @@ -86,7 +193,7 @@ &پیوست - + &SelectAll &انتخاب همه @@ -94,64 +201,64 @@ ExportController - Access error! - خطای دسترسی! + خطای دسترسی! HomeContainersListView - + Unable change protocol while there is an active connection امکان تغییر پروتکل در هنگام متصل بودن وجود ندارد - The selected protocol is not supported on the current platform - پروتکل انتخاب شده بر روی این پلتفرم پشتیبانی نمی‎‎شود + پروتکل انتخاب شده بر روی این پلتفرم پشتیبانی نمی‎‎شود HomeSplitTunnelingDrawer - + Split tunneling جداسازی ترافیک - + Allows you to connect to some sites or applications through a VPN connection and bypass others اجازه می‌دهد به شما که از طریق اتصال VPN به برخی از وب‌سایت‌ها یا برنامه‌ها وصل شوید و از دیگران عبور کنید - + Split tunneling on the server تقسیم تونل‌ها در سرور - + Enabled Can't be disabled for current server فعال - + Site-based split tunneling جداسازی ترافیک بر اساس سایت - + + Enabled فعال - + + Disabled غیر فعال - + App-based split tunneling جداسازی ترافیک بر اساس نرم‎افزار @@ -159,85 +266,128 @@ Can't be disabled for current server ImportController - Unable to open file - + نمی‌توان فایل را باز کرد. - - Invalid configuration file - + فایل پیکربندی نامعتبر است. - + Scanned %1 of %2. ارزیابی %1 از %2. + + + In the imported configuration, potentially dangerous lines were found: + در پیکربندی وارد شده، خطوطی که ممکن است خطرناک باشند، یافت شدند: + InstallController - - + %1 installed successfully. %1 با موفقیت نصب شد. - - + %1 is already installed on the server. %1 در حال حاضر بر روی سرور نصب شده است. - + Added containers that were already installed on the server کانتینرهایی که بر روی سرور موجود بودند اضافه شدند - + Already installed containers were found on the server. All installed containers have been added to the application کانتینرهای نصب شده بر روی سرور شناسایی شدند. تمام کانتینترهای نصب شده به نرم افزار اضافه شدند - + Settings updated successfully تنظیمات با موفقیت به‎روز‎رسانی شدند - + Server '%1' was rebooted سرور %1 راه اندازی مجدد شد - + Server '%1' was removed سرور %1 حذف شد - + All containers from server '%1' have been removed تمام کانتینترها از سرور %1 حذف شدند - + %1 has been removed from the server '%2' %1 از سرور %2 حذف شد - + + Api config removed + پیکربندی API حذف شد. + + + + %1 cached profile cleared + %1 پروفایل ذخیره شده پاک شد. + + + Please login as the user لطفا به عنوان کاربر وارد شوید - + Server added successfully سرور با موفقیت اضافه شد + + + %1 installed successfully. + %1 با موفقیت نصب شد. + + + + API config reloaded + پیکربندی API دوباره بارگذاری شد. + + + + Successfully changed the country of connection to %1 + کشور اتصال با موفقیت به %1 تغییر یافت. + + + + InstalledAppsDrawer + + + Choose application + انتخاب برنامه + + + + application name + نام برنامه + + + + Add selected + اضافه کردن انتخاب شده + KeyChainClass @@ -260,28 +410,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN AmneziaVPN - + VPN Connected وی‎پی‎ان وصل شد - + VPN Disconnected وی‎پی‎ان قطع شد - + AmneziaVPN notification اخطار AmneziaVPN - + Unsecured network detected: شبکه ناامن شناسایی شد: @@ -289,114 +439,234 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 حذف سرویس‎ها از %1 - + Usually it takes no more than 5 minutes معمولا بیش از 5 دقیقه طول نمی‎کشد - PageHome + PageDevMenu - - Logging enabled + + Gateway endpoint - + + Dev gateway environment + + + + + PageHome + + + Logging enabled + لاگ‌برداری فعال شد + + + Split tunneling enabled فعال شدن تونل تقسیم‌شده - + Split tunneling disabled تونل تقسیم‌شده غیرفعال شده - + VPN protocol پروتکل وی‎پی‎ان - + Servers سرورها - Unable change server while there is an active connection - امکان تغییر سرور در هنگام متصل بودن وجود ندارد + امکان تغییر سرور در هنگام متصل بودن وجود ندارد + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + تنظیمات AmneziaWG + + + + MTU + + + + + Server settings + + + + + Port + پورت + + + + Save + ذخیره + + + + Save settings? + تنظیمات را ذخیره کن? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. PageProtocolAwgSettings - + AmneziaWG settings تنظیمات AmneziaWG - + Port پورت - - MTU - - - - Remove AmneziaWG - حذف AmneziaWG + حذف AmneziaWG - Remove AmneziaWG from server? - آیا میخواهید AmneziaWG از سرور حذف شود؟ + آیا میخواهید AmneziaWG از سرور حذف شود؟ - - + All users with whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - + Save ذخیره - + + VPN address subnet + زیرشبکه آدرس VPN + + + + Jc - Junk packet count + + + + + Jmin - Junk packet minimum size + + + + + Jmax - Junk packet maximum size + + + + + S1 - Init packet junk size + + + + + S2 - Response packet junk size + + + + + H1 - Init packet magic header + + + + + H2 - Response packet magic header + + + + + H4 - Transport packet magic header + + + + + H3 - Underload packet magic header + + + + + The values of the H1-H4 fields must be unique + + + + + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + + + + Save settings? تنظیمات را ذخیره کن? - - + Continue ادامه - - + Cancel کنسل + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + PageProtocolCloakSettings - + Cloak settings تنظیمات Cloak - + Disguised as traffic from پنهان کردن به عنوان ترافیک از @@ -406,16 +676,21 @@ Already installed containers were found on the server. All installed containers پورت - - + + Cipher رمزگذاری - + Save ذخیره + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + PageProtocolOpenVpnSettings @@ -435,7 +710,7 @@ Already installed containers were found on the server. All installed containers پروتکل شبکه - + Port پورت @@ -557,58 +832,58 @@ Already installed containers were found on the server. All installed containers none - + TLS auth اعتبار TLS - + Block DNS requests outside of VPN مسدود کردن درخواست‎های DNS خارج از وی‎پی‎ان - + Additional client configuration commands تنظیمات و دستورات اضافه برنامه متصل شونده - - + + Commands: دستورات: - + Additional server configuration commands تنظیمات و دستورات اضافه سرور - + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + Remove OpenVPN - حذف OpenVPN + حذف OpenVPN - Remove OpenVPN from server? - آیا میخواهید OpenVPN از سرور حذف شود؟ + آیا میخواهید OpenVPN از سرور حذف شود؟ - All users with whom you shared a connection with will no longer be able to connect to it. - همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. + همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - Continue - ادامه + ادامه - Cancel - کنسل + کنسل - + Save ذخیره @@ -616,32 +891,32 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings تنظیمات - + Show connection options نمایش تنظیمات اتصال - + Connection options %1 تنظیمات اتصال %1 - + Remove حذف - + Remove %1 from server? %1 از سرور حذف شود؟ - + All users with whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. @@ -650,12 +925,12 @@ Already installed containers were found on the server. All installed containers همه کاربرانی که با آن این پروتکل VPN را به اشتراک گذاشته‌اید دیگر نمی‌توانند به آن متصل شوند. - + Continue ادامه - + Cancel کنسل @@ -663,75 +938,160 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings تنظیمات Shadowsocks - + Port پورت - - + + Cipher رمزگذاری - + Save ذخیره + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + + + PageProtocolWireGuardClientSettings + + + WG settings + تنظیمات WG + + + + MTU + + + + + Server settings + + + + + Port + پورت + + + + Save + ذخیره + + + + Save settings? + تنظیمات را ذخیره کن? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + PageProtocolWireGuardSettings - + WG settings - + تنظیمات WG - + + VPN address subnet + زیرشبکه آدرس VPN + + + Port - پورت + پورت - - MTU - + + Save settings? + تنظیمات را ذخیره کن? - - Remove WG - + + All users with whom you shared a connection with will no longer be able to connect to it. + همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - - Remove WG from server? - - - - - All users with whom you shared a connection will no longer be able to connect to it. - تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. - - - + Continue - + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + + All users with whom you shared a connection will no longer be able to connect to it. + تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. + + + Cancel کنسل - + Save ذخیره + + PageProtocolXraySettings + + + XRay settings + تنظیمات XRay + + + + Disguised as traffic from + به‌عنوان ترافیک از طرف زیر نمایش داده می‌شود + + + + Save + ذخیره + + + + Unable change settings while there is an active connection + نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. + + PageServerContainers @@ -742,218 +1102,290 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. یک سرویس DSN بر روی سرور شما نصب شده و فقط از طریق وی‎پی‎ان قابل دسترسی می‎باشد. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. آدرس DSN همان آدرس سرور شماست. میتوانید از قسمت تنظیمات و تب اتصالات DSN خود را تنظیم کنید. - + Remove جذف - + Remove %1 from server? %1 از سرور حذف شود؟ - + Continue ادامه - + Cancel کنسل + + + Cannot remove AmneziaDNS from running server + نمی‌توان AmneziaDNS را از سرور در حال اجرا حذف کرد. + PageServiceSftpSettings - + Settings updated successfully تنظیمات با موفقیت به‎روز‎رسانی شد - + SFTP settings تنظیمات SFTP - + Host هاست - - - - + + + + Copied کپی شد - + Port پورت - + User name نام کاربری - + Password رمز عبور - + Mount folder on device بارگذاری پوشه بر روی دستگاه - + In order to mount remote SFTP folder as local drive, perform following steps: <br> برای بارگذاری پوشه SFTP بر روی درایو محلی قدم‎های زیر را انجام دهید: <br> - - + + <br>1. Install the latest version of <br> 1. آخرین نسخه را نصب کنید - - + + <br>2. Install the latest version of <br> 2. آخرین نسخه را نصب کنید - + Detailed instructions جزییات دستورالعمل‎ها - Remove SFTP and all data stored there - حذف SFTP و تمام داده‎های ذخیره شده در آن + حذف SFTP و تمام داده‎های ذخیره شده در آن - Remove SFTP and all data stored there? - پوشه SFTP و تمام داده‎های آن حذف شوند؟ + پوشه SFTP و تمام داده‎های آن حذف شوند؟ - Continue - ادامه + ادامه - Cancel - کنسل + کنسل + + + + PageServiceSocksProxySettings + + + Settings updated successfully + تنظیمات با موفقیت به‌روزرسانی شد. + + + + + SOCKS5 settings + تنظیمات SOCKS5 + + + + Host + هاستمیزبان + + + + + + + Copied + کپی شد + + + + + Port + پورت + + + + User name + نام کاربری + + + + + Password + رمز عبور + + + + Username + نام کاربری + + + + + Change connection settings + تغییر تنظیمات اتصال + + + + The port must be in the range of 1 to 65535 + پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد + + + + Password cannot be empty + رمز عبور نمی‌تواند خالی باشد + + + + Username cannot be empty + نام کاربری نمی‌تواند خالی باشد PageServiceTorWebsiteSettings - + Settings updated successfully تنظیمات با موفقیت به‎روز‎‌رسانی شد - + Tor website settings تنظیمات وب‎سایت Tor - + Website address آدرس وب‎سایت - + Copied کپی شد - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. پس از ایجاد سایت پیاز خود، چند دقیقه طول می‌کشد تا شبکه تور آن را برای استفاده فراهم کند. - + When configuring WordPress set the this onion address as domain. زمانی که سایت وردپرس را تنظیم میکنید این آدرس پیازی را به عنوان دامنه قرار دهید. - Remove website - حذف وب سایت + حذف وب سایت - The site with all data will be removed from the tor network. - سایت با تمام داده‎ها از شبکه Tor حذف خواهد شد. + سایت با تمام داده‎ها از شبکه Tor حذف خواهد شد. - Continue - ادامه + ادامه - Cancel - کنسل + کنسل PageSettings - + Settings تنظیمات - + Servers سرورها - + Connection ارتباط - + Application نرم‎افزار - + Backup بک‎آپ - + About AmneziaVPN درباره Amnezia - + + Dev console + + + + Close application بستن نرم‎افزار @@ -961,173 +1393,352 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia پشتیبانی از Amnezia - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia یک برنامه رایگان و متن باز است. اگر دوست دارید می توانید از توسعه دهندگان حمایت کنید. - + Contacts مخاطب - + Telegram group گروه تلگرام - + To discuss features برای گفتگو در مورد ویژگی‎ها - + https://t.me/amnezia_vpn_en - https://t.me/amnezia_vpn + https://t.me/amnezia_vpn_ir + + + + support@amnezia.org + - Mail - ایمیل + ایمیل - + For reviews and bug reports برای ارائه نظرات و گزارشات باگ - + + Copied + کپی شد + + + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website وب سایت - - https://amnezia.org - https://amnezia.org + + Visit official website + - + https://amnezia.org + https://amnezia.org + + + Software version: %1 %1 :نسخه نرم‎افزار - + Check for updates بررسی بروز‎رسانی - + Privacy Policy + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + + + PageSettingsApiServerInfo + + + For the region + برای منطقه + + + + Price + قیمت + + + + Work period + مدت زمان کار + + + + Valid until + + + + + Speed + سرعت + + + + Support tag + + + + + Copied + کپی شد + + + + Reload API config + بارگذاری مجدد پیکربندی API + + + + Reload API config? + آیا می‌خواهید پیکربندی API را دوباره بارگذاری کنید؟ + + + + + Continue + ادامه دهید + + + + + Cancel + لغو + + + + Cannot reload API config during active connection + نمی‌توان پیکربندی API را در حین اتصال فعال دوباره بارگذاری کرد. + + + + Remove from application + حذف از برنامه + + + + Remove from application? + آیا می‌خواهید از برنامه حذف کنید؟ + + + + Cannot remove server during active connection + نمی‌توان سرور را در حین اتصال فعال حذف کرد. + + + + PageSettingsAppSplitTunneling + + + Cannot change split tunneling settings during active connection + نمی توان تنظیمات تونل تقسیم را در طول اتصال فعال تغییر دادنمی‌توان تنظیمات تقسیم تونلینگ را در حین اتصال فعال تغییر داد. + + + + Only the apps from the list should have access via VPN + فقط برنامه‌های موجود در لیست باید از طریق VPN دسترسی داشته باشند. + + + + Apps from the list should not have access via VPN + برنامه‌های موجود در لیست نباید از طریق VPN دسترسی داشته باشند. + + + + App split tunneling + تقسیم تونلینگ برنامه‌ها + + + + Mode + حالت + + + + Remove + حذف + + + + Continue + ادامه دهید + + + + Cancel + کنسل + + + + application name + نام برنامه + + + + Open executable file + فایل اجرایی را باز کنید + + + + Executable files (*.*) + فایل‌های اجرایی (*.*) + + PageSettingsApplication - + Application نرم افزار - + Allow application screenshots مجوز اسکرین‎شات در برنامه - + + Enable notifications + فعال کردن اعلان‌ها + + + + Enable notifications to show the VPN state in the status bar + اعلان ها را فعال کنید تا وضعیت VPN را در نوار وضعیت ببینید + + + Auto start شروع خودکار - + Launch the application every time the device is starts راه‎اندازی نرم‎افزار با هر بار روشن شدن دستگاه - + Auto connect اتصال خودکار - + Connect to VPN on app start اتصال به وی‎‎پی‎ان با شروع نرم‎افزار - + Start minimized شروع به صورت کوچک - + Launch application minimized راه‎اندازی برنامه به صورت کوچک - + Language زبان - + Logging گزارشات - + Enabled فعال - + Disabled غیر فعال - + Reset settings and remove all data from the application ریست کردن تنظیمات و حذف تمام داده‎ها از نرم‎افزار - + Reset settings and remove all data from the application? ریست کردن تنظیمات و حذف تمام داده‎ها از نرم‎افزار؟ - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. تمام تنظیمات به حالت پیش‎فرض ریست می‎شوند. تمام سرویس‎های Amnezia بر روی سرور باقی می‎مانند. - + Continue ادامه - + Cancel کنسل + + + Cannot reset settings during active connection + نمی‌توان تنظیمات را در حین اتصال فعال بازنشانی کرد. + PageSettingsBackup - + Settings restored from backup file تنظیمات از فایل پشتیبان بازیابی شد @@ -1137,111 +1748,131 @@ Already installed containers were found on the server. All installed containers یک نسخه پشتیبان از تنظیمات خود تهیه - + You can save your settings to a backup file to restore them the next time you install the application. می‎توانید تنظیمات را در یک فایل پشتیبان ذخیره کرده و دفعه بعد که نرم‎افزار را نصب کردید آن‎ها را بازیابی کنید. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. پشتیبان حاوی رمزهای عبور و کلیدهای خصوصی شما برای تمام سرورهای اضافه شده به AmneziaVPN خواهد بود. این اطلاعات را در یک مکان امن نگه دارید - + Make a backup ایجاد یک پشتیبان - + Save backup file ذخیره فایل پشتیبان - - + + Backup files (*.backup) Backup files (*.backup) - + Backup file saved فایل پشتیبان ذخیره شد - + Restore from backup بازیابی از پشتیبان - + Open backup file باز کردن فایل پشتیبان - + Import settings from a backup file? ورود تنظیمات از فایل پشتیبان؟ - + All current settings will be reset تمام تنظیمات جاری ریست خواهد شد - + Continue ادامه - + Cancel کنسل + + + Cannot restore backup settings during active connection + نمی‌توان تنظیمات پشتیبان را در حین اتصال فعال بازیابی کرد. + PageSettingsConnection - + Connection ارتباط - + Use AmneziaDNS استفاده از AmneziaDNS - + If AmneziaDNS is installed on the server اگر AmneziaDNS بر روی سرور نصب شده باشد - + DNS servers سرورهای DNS - + When AmneziaDNS is not used or installed وقتی AmneziaDNS استفاده نشده یا نصب نشده است - + Allows you to use the VPN only for certain Apps به شما امکان می دهد از VPN فقط برای برخی برنامه ها استفاده کنید - + + KillSwitch + KillSwitch + + + + Disables your internet if your encrypted VPN connection drops out for any reason. + اگر به هر دلیلی اتصال VPN رمزگذاری شده شما قطع شود، اینترنت شما را غیرفعال می‌کند. + + + + Cannot change killSwitch settings during active connection + نمی‌توان تنظیمات Kill Switch را در حین اتصال فعال تغییر داد. + + + Site-based split tunneling جداسازی ترافیک بر اساس سایت - + Allows you to select which sites you want to access through the VPN میتوانید مشخص کنید که چه سایت‎هایی از وی‎پی‎ان استفاده کنند - + App-based split tunneling جداسازی ترافیک بر اساس نرم‎افزار @@ -1249,62 +1880,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS سرور پیش‌فرض از دی‌ان‌اس سفارشی پشتیبانی نمی‌کند - + DNS servers سرورهای DNS - + If AmneziaDNS is not used or installed اگر AmneziaDNS نصب نباشد یا استفاده نشود - + Primary DNS DNS اصلی - + Secondary DNS DNS ثانویه - + Restore default بازگشت به پیش‎فرض - + Restore default DNS settings? بازگشت به تنظیمات پیش‎فرض DNS؟ - + Continue ادامه - + Cancel کنسل - + Settings have been reset تنظیمات ریست شد - + Save ذخیره - + Settings saved ذخیره تنظیمات @@ -1312,72 +1943,106 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + ثبت وقایع فعال است. توجه داشته باشید که ثبت وقایع به‌طور خودکار پس از ۱۴ روز غیرفعال شده و تمام فایل‌های ثبت وقایع حذف خواهند شد. - + Logging گزارشات - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. فعال کردن این عملکرد باعث ذخیره خودکار لاگ‌های برنامه می‌شود. به طور پیش‌فرض، قابلیت ثبت لاگ غیرفعال است. در صورت بروز خطا در برنامه، ذخیره لاگ را فعال کنید. - Save logs - ذخیره گزارشات + ذخیره گزارشات - Open folder with logs - باز کردن پوشه گزارشات + باز کردن پوشه گزارشات - + + Save ذخیره - + + Logs files (*.log) Logs files (*.log) - + + Logs file saved فایل گزارشات ذخیره شد - Save logs to file - ذخیره گزارشات در فایل + ذخیره گزارشات در فایل - + + Enable logs + + + + Clear logs? پاک کردن گزارشات؟ - + Continue ادامه - + Cancel کنسل - + Logs have been cleaned up گزارشات پاک شدند - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs پاک کردن گزارشات @@ -1385,27 +2050,24 @@ Already installed containers were found on the server. All installed containers PageSettingsServerData - + All installed containers have been added to the application تمام کانتینرهای نصب شده به نرم‎افزار اضافه شدند - Clear Amnezia cache - پاک کردن حافظه داخلی Amnezia + پاک کردن حافظه داخلی Amnezia - May be needed when changing other settings - وقتی تنظیمات دیگر را تغییر دهید ممکن است نیاز باشد + وقتی تنظیمات دیگر را تغییر دهید ممکن است نیاز باشد - Clear cached profiles? - پاک کردن پروفایل ذخیره شده؟ + پاک کردن پروفایل ذخیره شده؟ - + No new installed containers found کانتینر نصب شده جدیدی پیدا نشد @@ -1415,85 +2077,103 @@ Already installed containers were found on the server. All installed containers - - - - - + + + + Continue ادامه - - - - - + + + + Cancel کنسل - + Check the server for previously installed Amnezia services چک کردن سرویس‎های نصب شده Amnezia بر روی سرور - + Add them to the application if they were not displayed اضافه کردن آنها به نرم‎افزار اگر نمایش داده نشده‎اند - + Reboot server سرور را دوباره راه‌اندازی کنید - + Do you want to reboot the server? آیا می‌خواهید سرور را دوباره راه‌اندازی کنید؟ - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? فرآیند راه‌اندازی ممکن است حدود ۳۰ ثانیه طول بکشد. آیا مطمئن هستید که می‌خواهید ادامه دهید؟ - + + Cannot reboot server during active connection + نمی‌توان سرور را در حین اتصال فعال راه‌اندازی مجدد کرد. + + + Do you want to remove the server from application? آیا می‌خواهید سرور را از برنامه حذف کنید؟ - + + Cannot remove server during active connection + نمی‌توان سرور را در حین اتصال فعال حذف کرد. + + + Do you want to clear server from Amnezia software? آیا می‌خواهید سرور را از نرم‌افزار Amnezia پاک کنید؟ - + All users whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - + + Cannot clear server from Amnezia software during active connection + نمی‌توان سرور را در حین اتصال فعال از نرم‌افزار Amnezia پاک کرد. + + + Reset API config تنظیمات API را بازنشانی کنید - + Do you want to reset API config? آیا می خواهید پیکربندی API را بازنشانی کنید؟ - + + Cannot reset API config during active connection + نمی‌توان پیکربندی API را در حین اتصال فعال بازنشانی کرد. + + + Remove server from application حذف کردن سرور از نرم‎افزار - + All installed AmneziaVPN services will still remain on the server. تمام سرویس‎های نصب‎شده Amnezia همچنان بر روی سرور باقی خواهند ماند. - + Clear server from Amnezia software پاک کردن سرور از نرم‎افزار Amnezia @@ -1501,27 +2181,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name نام سرور - + Save ذخیره - + Protocols پروتکل‎ها - + Services سرویس‎ها - + Management مدیریت @@ -1529,32 +2214,83 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings تنظیمات - + Clear %1 profile + پاک کردن پروفایل %1 + + + + Clear %1 profile? + آیا می‌خواهید پروفایل %1 را پاک کنید؟ + + + + + + + + + Unable to clear %1 profile while there is an active connection + نمی‌توان پروفایل %1 را در حین اتصال فعال پاک کرد. + + + Remove حذف - + Remove %1 from server? حذف %1 از سرور؟ - + All users with whom you shared a connection will no longer be able to connect to it. تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. - + + Cannot remove active container + نمی‌توان کانتینر فعال را حذف کرد. + + + + Continue ادامه - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Cancel کنسل @@ -1562,7 +2298,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers سرورها @@ -1570,110 +2306,155 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function سرور پیش‌فرض از عملکرد تونل‌سازی تقسیم شده پشتیبانی نمی‌کند - + Addresses from the list should not be accessed via VPN دسترسی به آدرس‎های لیست بدون وی‎پی‎ان - + Split tunneling جداسازی ترافیک - + Mode حالت - + Remove حذف - + Continue ادامه - + Cancel کنسل - + Cannot change split tunneling settings during active connection نمی توان تنظیمات تونل تقسیم را در طول اتصال فعال تغییر داد - + Only the sites listed here will be accessed through the VPN تنها سایت‌های موجود در اینجا از طریق VPN دسترسی داده خواهند شد - + website or IP وب‌سایت یا آدرس IP - + Import / Export Sites وارد کردن / صادر کردن وب‌سایت‌ها - + Import بارگذاری - + Save site list ذخیره لیست سایت‎ها - + Save sites ذخیره سایت‎ها - - - + + + Sites files (*.json) Sites files (*.json) - + Import a list of sites بارگذاری لیست سایت‎ها - + Replace site list جایگزین کردن لیست سایت - - + + Open sites file باز کردن فایل سایت‎ها - + Add imported sites to existing ones اضافه کردن سایت‎های بارگذاری شده به سایت‎های موجود + + PageSetupWizardApiServiceInfo + + + For the region + برای منطقه + + + + Price + قیمت + + + + Work period + مدت زمان کار + + + + Speed + سرعت + + + + Features + ویژگی‌ها + + + + Connect + اتصال + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + VPN توسط Amnezia + + + + Choose a VPN service that suits your needs. + یک سرویس VPN که مناسب نیازهای شما باشد را انتخاب کنید. + + PageSetupWizardConfigSource - Server connection - ارتباط سرور + ارتباط سرور Do not use connection code from public sources. It may have been created to intercept your data. @@ -1684,95 +2465,216 @@ It's okay as long as it's from someone you trust. ایرادی ندارد که از طرف کسی باشد که به او اعتماد دارید. - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - از کدهای اتصال از منابع نامعتبر استفاده نکنید، زیرا ممکن است برای رهگیری داده های شما ایجاد شده باشند. + از کدهای اتصال از منابع نامعتبر استفاده نکنید، زیرا ممکن است برای رهگیری داده های شما ایجاد شده باشند. - What do you have? - چی داری؟ + چی داری؟ - + File with connection settings فایل شامل تنظیمات اتصال - File with connection settings or backup - فایل شامل تنظیمات اتصال یا بک‎آپ + فایل شامل تنظیمات اتصال یا بک‎آپ - + + Connection + ارتباط + + + + Settings + تنظیمات + + + + Enable logs + + + + + Support tag + + + + + Copied + کپی شد + + + + Insert the key, add a configuration file or scan the QR-code + کلید را وارد کنید، فایل پیکربندی را اضافه کنید یا کد QR را اسکن کنید + + + + Insert key + کلید را وارد کنید + + + + Insert + وارد کردن + + + + Continue + ادامه دهید + + + + Other connection options + گزینه‌های اتصال دیگر + + + + Site Amnezia + + + + + VPN by Amnezia + VPN توسط Amnezia + + + + Connect to classic paid and free VPN services from Amnezia + اتصال به سرویس‌های VPN کلاسیک پولی و رایگان از Amnezia + + + + Self-hosted VPN + Self-hosted VPN + + + + Configure Amnezia VPN on your own server + پیکربندی VPN Amnezia بر روی سرور خودتان + + + + Restore from backup + بازیابی از پشتیبان + + + + + + + + + Open backup file + باز کردن فایل پشتیبان + + + + Backup files (*.backup) + Backup files (*.backup) + + + + + + + + Open config file باز کردن فایل تنظیمات - + QR code QR-Code - + + + + + + + I have nothing + من هیچی ندارم + + + + + + + Key as text - متن شامل کلید + متن شامل کلید PageSetupWizardCredentials - + Server IP address [:port] آدرس آی‎پی سرور (:پورت) - + Continue ادامه - + Enter the address in the format 255.255.255.255:88 آدرس را با فرمت 255.255.255.255:88 وارد کنید - + Configure your server سرور خود را پیکربندی کنید - + 255.255.255.255:22 - + SSH Username - + Password or SSH private key رمز عبور یا کلید خصوصی SSH - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties تمام داده‎هایی که شما وارد می‎کنید به شدت محرمانه‎ است و با Amnezia یا هر شخص ثالث دیگری به اشتراک گذاشته نمی‎شود - + + How to run your VPN server + چگونه سرور VPN خود را اجرا کنید + + + + Where to get connection data, step-by-step instructions for buying a VPS + داده‌های اتصال را از کجا دریافت کنید و دستورالعمل‌های مرحله به مرحله برای خرید یک VPS + + + Ip address cannot be empty آدرس آی‎پی نمی‎تواند خالی باشد - + Login cannot be empty نام‎کاربری نمی‎تواند خالی باشد - + Password/private key cannot be empty پسورد یا کلید خصوصی نمی‎تواند خالی باشد @@ -1780,22 +2682,22 @@ It's okay as long as it's from someone you trust. PageSetupWizardEasy - + What is the level of internet control in your region? سطح کنترل اینترنت در منطقه شما چگونه است؟ - + Choose a VPN protocol یک پروتکل VPN را انتخاب کنید - + Skip setup رد شدن از تنظیم - + Continue ادامه @@ -1803,38 +2705,38 @@ It's okay as long as it's from someone you trust. PageSetupWizardInstalling - + The server has already been added to the application سرور در حال حاضر به نرم‎افزار اضافه شده است - + Amnezia has detected that your server is currently Amnezia has detected that your server is currently - + busy installing other software. Amnezia installation مشغول نصب نرم افزارهای دیگر نصب Amnezia - + will pause until the server finishes installing other software متوقف شده تا زمانی که سرور نصب نرم‎افزار دیگر را تمام کند - + Installing در حال نصب - + Cancel installation لغو عملیات نصب - - + + Usually it takes no more than 5 minutes معمولا بیش از 5 دقیقه طول نمی‎کشد @@ -1842,45 +2744,50 @@ It's okay as long as it's from someone you trust. PageSetupWizardProtocolSettings - + Installing %1 در حال نصب %1 - + More detailed جزییات بیشتر - + Close بستن - + Network protocol پروتکل شبکه - + Port پورت - + Install نصب + + + The port must be in the range of 1 to 65535 + پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد + PageSetupWizardProtocols - + VPN protocol پروتکل وی‎پی‎ان - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. پروتکلی که بیشترین اولویت را برای شما دارد انتخاب کنید. بعدا، میتوانید پروتکل‎ها و سرویس‎های اضافه مانند پروکسی DNS و SFTP را هم نصب کنید. @@ -1888,7 +2795,7 @@ It's okay as long as it's from someone you trust. PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. دوربین را روی QR Code بگیرید و برای چند ثانیه آن را نگه دارید. @@ -1896,34 +2803,29 @@ It's okay as long as it's from someone you trust. PageSetupWizardStart - Settings restored from backup file - تنظیمات از فایل بک‎آپ بازیابی شدند + تنظیمات از فایل بک‎آپ بازیابی شدند - Free service for creating a personal VPN on your server. - سرویس رایگان برای ایجاد وی‎پی‎ان شخصی بر روی سرور خودتان. + سرویس رایگان برای ایجاد وی‎پی‎ان شخصی بر روی سرور خودتان. - Helps you access blocked content without revealing your privacy, even to VPN providers. - به شما کمک می‎کند که بدون فاش کردن حریم شخصی خودتان, حتی برای ارائه دهنده وی‎پی‎ان به محتوای مسدود شده دسترسی پیدا کنید. + به شما کمک می‎کند که بدون فاش کردن حریم شخصی خودتان, حتی برای ارائه دهنده وی‎پی‎ان به محتوای مسدود شده دسترسی پیدا کنید. - I have the data to connect - من داده برای اتصال دارم + من داده برای اتصال دارم - I have nothing - من هیچی ندارم + من هیچی ندارم - - https://amnezia.org/instructions/0_starter-guide - + + Let's get started + بیایید شروع کنیم @@ -1949,7 +2851,7 @@ It's okay as long as it's from someone you trust. وارد کردن - + Continue ادامه @@ -1957,27 +2859,32 @@ It's okay as long as it's from someone you trust. PageSetupWizardViewConfig - + New connection ارتباط جدید - + Collapse content جمع کردن محتوا - + Show content نمایش محتوا - + + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. + فعال‌سازی استتار WireGuard. این ممکن است مفید باشد اگر WireGuard توسط ارائه‌دهنده شما مسدود شده باشد. + + + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. از کدهای اتصال فقط از منابع مورد اعتماد خود استفاده کنید. ممکن است کدهایی از منابع عمومی برای رهگیری داده های شما ایجاد شده باشند - + Connect اتصال @@ -1985,182 +2892,216 @@ It's okay as long as it's from someone you trust. PageShare - + OpenVPN native format فرمت OpenVPN - + WireGuard native format فرمت WireGuard - + Connection ارتباط - - + + Server سرور - + Config revoked تنظیمات ابطال‎شد - + Connection to ارتباط با - + File with connection settings to فایل شامل تنظیمات ارتباط با - + Save OpenVPN config ذخیره تنظیمات OpenVPN - + Save WireGuard config ذخیره تنظیمات WireGuard - + Save AmneziaWG config تنظیمات AmneziaWG را ذخیره کنید - + Save Shadowsocks config ذخیره تنظیمات Shadowsocks - + Save Cloak config ذخیره تنظیمات Cloak - + + Save XRay config + ذخیره پیکربندی XRay + + + For the AmneziaVPN app برای نرم‎افزار AmneziaVPN - + AmneziaWG native format فرمت بومی AmneziaWG - + Shadowsocks native format فرمت Shadowsocks - + Cloak native format فرمت Cloak - + + XRay native format + فرمت بومی XRay + + + Share VPN Access اتصال vpn را به اشتراک بگذارید - + Share full access to the server and VPN به اشتراک گذاشتن دسترسی کامل به سرور و وی‎پی‎ان - + Use for your own devices, or share with those you trust to manage the server. برای دستگاه‎های خودتان استفاده کنید یا با آنهایی که برای مدیریت سرور به آن‎ها اعتماد دارید به اشتراک بگذارید. - - + + Users کاربران - + User name نام کاربری - + Search جستجو - - Creation date: - تاریخ ایجاد: + + Creation date: %1 + تاریخ ایجاد: %1 - + + Latest handshake: %1 + آخرین ارتباط: %1 + + + + Data received: %1 + داده‌های دریافت شده: %1 + + + + Data sent: %1 + داده‌های ارسال شده: %1 + + + + Allowed IPs: %1 + + + + Creation date: + تاریخ ایجاد: + + + Rename تغییر نام - + Client name نام کلاینت - + Save ذخیره - + Revoke ابطال - + Revoke the config for a user - %1? لغو پیکربندی برای یک کاربر - %1? - + The user will no longer be able to connect to your server. کاربر دیگر نمی‎تواند به سرور وصل شود. - + Continue ادامه - + Cancel کنسل - + Share VPN access without the ability to manage the server به اشتراک گذاشتن دسترسی وی‎پی‎ان بدون امکان مدیریت سرور - - + + Protocol پروتکل - - + + Connection format فرمت ارتباط - - + + Share اشتراک‎گذاری @@ -2168,50 +3109,55 @@ It's okay as long as it's from someone you trust. PageShareFullAccess - + Full access to the server and VPN دسترسی کامل به سرور و وی‎پی‎ان - + We recommend that you use full access to the server only for your own additional devices. ما پیشنهاد میکنیم که ازحالت دسترسی کامل به سرور فقط برای دستگاه‎های دیگر خودتان استفاده کنید. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. اگر دسترسی کامل را با دیگران به اشتراک بگذارید، آن‎ها می‎توانند پروتکل‎ها و سرویس‎ها را حذف یا اضافه کنند که باعث می‎شود که وی‎پی‎ان دیگر برای سایر کاربران کار نکند. - - + + Server سرور - + Accessing در حال دسترسی به - + File with accessing settings to فایل شامل تنظیمات دسترسی به - + Share اشتراک‎گذاری - + + Access error! + خطای دسترسی! + + + Connection to ارتباط با - + File with connection settings to فایل شامل تنظیمات ارتباط با @@ -2219,15 +3165,25 @@ It's okay as long as it's from someone you trust. PageStart - + Logging was disabled after 14 days, log files were deleted + ثبت وقایع پس از ۱۴ روز غیرفعال شد و فایل‌های ثبت وقایع حذف شدند + + + + Settings restored from backup file + تنظیمات از فایل پشتیبان بازیابی شد + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. PopupType - + Close بستن @@ -2261,12 +3217,12 @@ It's okay as long as it's from someone you trust. Password not found - + Could not open keystore Could not open keystore - + Could not remove private key from keystore Could not remove private key from keystore @@ -2442,27 +3398,27 @@ It's okay as long as it's from someone you trust. Could not open keystore - + Could not create private key generator Could not create private key generator - + Could not generate new private key Could not generate new private key - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create encryption cipher Could not create encryption cipher - + Could not encrypt data Could not encrypt data @@ -2476,6 +3432,7 @@ It's okay as long as it's from someone you trust. + Unknown error Unknown error @@ -2485,62 +3442,62 @@ It's okay as long as it's from someone you trust. Function not implemented - + Server check failed Server check failed - + Server port already used. Check for another software Server port already used. Check for another software - + Server error: Docker container missing Server error: Docker container missing - + Server error: Docker failed Server error: Docker failed - + Installation canceled by user Installation canceled by user - + The user does not have permission to use sudo The user does not have permission to use sudo - + SSH request was denied SSH request was denied - + SSH request was interrupted SSH request was interrupted - + SSH internal error SSH internal error - + Invalid private key or invalid passphrase entered Invalid private key or invalid passphrase entered - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types The selected private key format is not supported, use openssh ED25519 key types or PEM key types - + Timeout connecting to server Timeout connecting to server @@ -2597,142 +3554,197 @@ It's okay as long as it's from someone you trust. Sftp error: No media was in remote drive - + The config does not contain any containers and credentials for connecting to the server تنظیمات شامل هیچ کانتینر یا اعتبارنامه‎ای برای اتصال به سرور نیست - + VPN connection error خطای اتصال VPN - + Error when retrieving configuration from API خطا هنگام بازیابی پیکربندی از API - + This config has already been added to the application این پیکربندی قبلاً به برنامه اضافه شده است - + ErrorCode: %1. کد خطا: %1. - + OpenVPN config missing OpenVPN config missing - - SCP error: Generic failure - + + Background service is not running + Background service is not running - + + Server error: Packet manager error + Server error: Packet manager error + + + + SCP error: Generic failure + SCP error: Generic failure + + + OpenVPN management server error OpenVPN management server error - + OpenVPN executable missing OpenVPN executable missing - + Shadowsocks (ss-local) executable missing Shadowsocks (ss-local) executable missing - + Cloak (ck-client) executable missing Cloak (ck-client) executable missing - + Amnezia helper service error Amnezia helper service error - + OpenSSL failed OpenSSL failed - + Can't connect: another VPN connection is active Can't connect: another VPN connection is active - + Can't setup OpenVPN TAP network adapter Can't setup OpenVPN TAP network adapter - + VPN pool error: no available addresses VPN pool error: no available addresses - - QFile error: The file could not be opened + + Unable to open config file - QFile error: An error occurred when reading from the file - + In the response from the server, an empty config was received + در پاسخ از سرور، پیکربندی خالی دریافت شد - QFile error: The file could not be accessed - + SSL error occurred + SSL error occurred - QFile error: An unspecified error occurred - + Server response timeout on api request + Server response timeout on api request - QFile error: A fatal error occurred + Missing AGW public key - QFile error: The operation was aborted + Failed to decrypt response payload + + + + + Missing list of available services + QFile error: The file could not be opened + + + + + QFile error: An error occurred when reading from the file + + + + + QFile error: The file could not be accessed + + + + + QFile error: An unspecified error occurred + + + + + QFile error: A fatal error occurred + + + + + QFile error: The operation was aborted + + + + Internal error Internal error - + IPsec IPsec - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. شدوساکس - ترافیک VPN را پنهان می کند، به طوری که مشابه ترافیک وب عادی می شود، اما ممکن است توسط سیستم های تجزیه و تحلیل در برخی از مناطق با سانسور شدید شناسایی شود. - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. OpenVPN روی Cloak - OpenVPN با VPN که به عنوان ترافیک وب پنهان می‌شود و مقاومت در برابر تشخیص فعال از طریق پیشرفته. ایده‌آل برای دور زدن مسدود کردن در مناطق با بالاترین سطوح سانسور. - + + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. + XRay با REALITY - مناسب برای کشورهایی با بالاترین سطح سانسور اینترنت. استتار ترافیک به عنوان ترافیک وب در سطح TLS و حفاظت در برابر شناسایی با روش‌های پروب فعال. + + + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + IKEv2/IPsec - پروتکل مدرن و پایدار، کمی سریع‌تر از سایرین است و پس از قطع شدن سیگنال، اتصال را بازیابی می‌کند. از پشتیبانی بومی در آخرین نسخه‌های Android و iOS برخوردار است. + + + Create a file vault on your server to securely store and transfer files. ساختن یک گنجانده فایل بر روی سرور شما برای ذخیره و انتقال ایمن فایل‌ها. - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -2771,7 +3783,7 @@ Cloak می‌تواند اطلاعات فراداده بسته را تغییر - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -2792,7 +3804,19 @@ WireGuard به دلیل امضاهای بسته متمایز خود، بسیار * روی پروتکل شبکه UDP کار می کند. - + + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. + پروتکل REALITY، یک توسعه پیشگامانه توسط خالقان XRay، به‌طور خاص برای مقابله با بالاترین سطح سانسور اینترنتی طراحی شده است و از رویکرد نوآورانه‌ای برای دور زدن محدودیت‌ها استفاده می‌کند. + +REALITY به‌طور منحصربه‌فردی سانسورچیان را در مرحله دست‌دهی TLS شناسایی می‌کند و به‌صورت یکپارچه به‌عنوان پراکسی برای کاربران قانونی عمل می‌کند، در حالی که سانسورچیان را به سایت‌های معتبر مانند google.com هدایت می‌کند و در نتیجه یک گواهی TLS واقعی و داده‌های اصلی ارائه می‌دهد. + +این قابلیت پیشرفته، REALITY را از فناوری‌های مشابه متمایز می‌کند، زیرا می‌تواند ترافیک وب را بدون نیاز به پیکربندی‌های خاص، به‌عنوان ترافیک از سایت‌های تصادفی و معتبر جا بزند. برخلاف پروتکل‌های قدیمی‌تر مانند VMess، VLESS و انتقال XTLS-Vision، تشخیص نوآورانه "دوست یا دشمن" REALITY در مرحله دست‌دهی TLS امنیت را افزایش داده و از شناسایی توسط سیستم‌های پیشرفته DPI که از تکنیک‌های پروب فعال استفاده می‌کنند، جلوگیری می‌کند. این ویژگی REALITY را به یک راه‌حل قوی برای حفظ آزادی اینترنت در محیط‌هایی با سانسور شدید تبدیل می‌کند. + + + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -2813,58 +3837,57 @@ While it offers a blend of security, stability, and speed, it's essential t * روی پروتکل شبکه UDP، پورت‎های 500 و 4500 کار می‎کند. - + DNS Service سرویس DNS - + SFTP file sharing service سرویس فایل اشتراک SFTP - - + + Website in Tor network وب سایت در شبکه Tor - + AmneziaDNS AmneziaDNS - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. پروتکل OpenVPN یکی از پروتکل‎های وی‎پی‎ان محبوب می‎باشد با تنظیمات و پیکربندی‎های قابل تغییر. از پروتکل امنیتی داخلی خود با تبادل کلید SSL/TLS استفاده می‎کند. - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. پروتکل WireGuard یک پروتکل وی‎پی‎ان جدید با عملکرد بسیار خوب، سرعت بالا و مصرف انرژی پایین. برای مناطقی که سطح سانسور پایینی دارند پیشنهاد می‎شود. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. پروتکل AmneziaWG یک پروتکل اختصاصی Amnezia که بر اساس WireGaurd کار میکند. به اندازه WireGaurd پرسرعت است و در عین حال بسیار مقاوم به بلاک شدن توسط شبکه ست. مناسب برای مناطق با سطح سانسور بالاست. - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - پروتکل IKEv2/IPsec پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکل‎هاست. بعد از قطع سیگنال دوباره اتصال را بازیابی می‎کند. + پروتکل IKEv2/IPsec پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکل‎هاست. بعد از قطع سیگنال دوباره اتصال را بازیابی می‎کند. - + Deploy a WordPress site on the Tor network in two clicks. با دو کلیک یک سایت وردپرس در شبکه Tor راه‎اندازی کنید. - + Replace the current DNS server with your own. This will increase your privacy level. سرور DNS را با مال خودتان جایگزین کنید. این کار سطح حریم خصوصی شما را افزایش می‎دهد. - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -2887,7 +3910,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * امکان کار بر روی دو پروتکل TCP و UDP - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -2902,7 +3925,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * عملکرد بر روی پروتکل شبکه TCP - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -2923,7 +3946,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin * کار بر روی پروتکل شبکه UDP - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -2942,10 +3965,17 @@ For more detailed information, you can آن را در بخش پشتیبانی تحت "ایجاد ذخیره سازی فایل SFTP" پیدا کنید." - + SFTP service سرویس SFTP + + + + + SOCKS5 proxy server + سرور پروکسی SOCKS5 + Entry not found @@ -2996,35 +4026,169 @@ For more detailed information, you can No match No match - - - Unknown error - Unknown error - error 0x%1: %2 error 0x%1: %2 + + + vmess:// url is invalid + + + + + Invalid streamSettings protocol: + + + + + Unknown transport method: + + + + + VMess string should start with 'vmess://' + + + + + VMess string should be a valid base64 string + + + + + JSON should not be empty + + + + + VLESS link should start with vless:// + + + + + link parse failed: %1 + + + + + empty host + + + + + missing port + + + + + missing uuid + + + + + Invalid ssd link: json: field %1 must exist + + + + + Invalid ssd link: json: field %1 must be valid port number + + + + + Invalid ssd link: json: field %1 must be of type 'string' + + + + + Invalid ssd link: json: field %1 must be an array + + + + + Skipping invalid ssd server: server must be an object + + + + + Skipping invalid ssd server: missing required field %1 + + + + + Skipping invalid ssd server: field %1 should be of type 'string' + + + + + Invalid ssd link: should begin with ssd:// + + + + + Invalid ssd link: base64 parse failed + + + + + Invalid ssd link: json parse failed + + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + + + + + SS URI is too short + + + + + + Can't find the colon separator between method and password + + + + + Can't find the at separator between password and hostname + + + + + Can't find the colon separator between hostname and port + + SelectLanguageDrawer - + Choose language انتخاب زبان + + ServersListView + + + Unable change server while there is an active connection + امکان تغییر سرور در هنگام متصل بودن وجود ندارد + + Settings - + Server #1 Server #1 - - + + Server Server @@ -3032,17 +4196,16 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values تمام تنظیمات به مقادیر پیش فرض ریست شد - Cached profiles cleared - پروفایل ذخیره شده پاک شد + پروفایل ذخیره شده پاک شد - + Backup file is corrupted فایل بک‎آپ خراب شده است @@ -3050,39 +4213,39 @@ For more detailed information, you can ShareConnectionDrawer - - + + Save AmneziaVPN config ذخیره تنظیمات AmneziaVPN - + Share اشتراک‎گذاری - + Copy کپی - - + + Copied کپی شد - + Copy config string کپی‎کردن متن تنظیمات - + Show connection settings نمایش تنظیمات ارتباط - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" برای خواندن QR Code در نرم‎افزار AmneziaVPN "اضافه کردن سرور" -> "من داده برای اتصال دارم" -> "QR Code، کلید یا فایل تنظیمات" @@ -3105,27 +4268,27 @@ For more detailed information, you can سایت حذف شد: %1 - + Can't open file: %1 فایل باز نشد: %1 - + Failed to parse JSON data from file: %1 مشکل در تحلیل داده‎های JSON در فایل: %1 - + The JSON data is not an array in file: %1 داده‎های JSON در فایل به صورت آرایه نیستند: %1 - + Import completed بارگذاری کامل شد - + Export completed خروجی گرفتن کامل شد @@ -3166,7 +4329,7 @@ For more detailed information, you can TextFieldWithHeaderType - + The field can't be empty این فیلد نمی‌تواند خالی باشد. @@ -3174,7 +4337,7 @@ For more detailed information, you can VpnConnection - + Mbps Mbps @@ -3182,42 +4345,42 @@ For more detailed information, you can VpnProtocol - + Unknown ناشناخته - + Disconnected قطع شده - + Preparing درحال آماده‎سازی - + Connecting... برقراری ارتباط... - + Connected وصل شد - + Disconnecting... در حال قطع شدن... - + Reconnecting... برقراری ارتباط دوباره... - + Error خطا @@ -3225,45 +4388,43 @@ For more detailed information, you can amnezia::ContainerProps - + Low پایین - + High متوسط یا بالا - Extreme - شدید + شدید - + I just want to increase the level of my privacy. من فقط میخواهم سطح حریم شخصی خودم را بالا ببرم - + I want to bypass censorship. This option recommended in most cases. من میخواهم از سانسور عبور کنم. این گزینه در اکثر موارد توصیه می‎‌شود - Most VPN protocols are blocked. Recommended if other options are not working. - اکثر پروتکل‎های وی‎پی‎ان مسدود شده‎اند. در مواردی که بقیه گزینه‎ها کار نمی‎کنند توصی می‎شود. + اکثر پروتکل‎های وی‎پی‎ان مسدود شده‎اند. در مواردی که بقیه گزینه‎ها کار نمی‎کنند توصی می‎شود. main2 - + Private key passphrase عبارت کلید خصوصی - + Save ذخیره diff --git a/client/translations/amneziavpn_hi_IN.ts b/client/translations/amneziavpn_hi_IN.ts index 68f324c6..db095d5c 100644 --- a/client/translations/amneziavpn_hi_IN.ts +++ b/client/translations/amneziavpn_hi_IN.ts @@ -1,6 +1,67 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + + + + + %1 MBit/s + + + + + %1 days + + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + + + + + Free + + + + + %1 $/month + + + AppSplitTunnelingController @@ -27,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation कॉन्फ़िगरेशन तैयारी के दौरान डिस्कनेक्ट करने में असमर्थ @@ -35,62 +96,62 @@ ConnectionController - - - - + + + + Connect कनेक्ट - + VPN Protocols is not installed. Please install VPN container at first पीएन प्रोटोकॉल स्थापित नहीं है. कृपया पहले वीपीएन कंटेनर स्थापित करें - + Connected जुड़ा हुआ - + The selected protocol is not supported on the current platform चयनित प्रोटोकॉल वर्तमान प्लेटफ़ॉर्म पर समर्थित नहीं है - + unable to create configuration कॉन्फ़िगरेशन बनाने में असमर्थ - + Connecting... कनेक्ट... - + Reconnecting... पुनः कनेक्ट हो रहा है... - + Disconnecting... डिस्कनेक्ट हो रहा है... - + Preparing... तैयार कर रहे हैं... - + Settings updated successfully, reconnnection... सेटिंग्स सफलतापूर्वक अपडेट हो गईं... - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं @@ -98,17 +159,17 @@ ConnectionTypeSelectionDrawer - + Add new connection नया कनेक्शन जोड़ें - + Configure your server अपना सर्वर कॉन्फ़िगर करें - + Open config file, key or QR code कॉन्फ़िग फ़ाइल, कुंजी या QR कोड खोलें @@ -131,7 +192,7 @@ &चपकाएं - + &SelectAll &सबका चयन करें @@ -139,15 +200,14 @@ ExportController - Access error! - प्रवेश त्रुटि! + प्रवेश त्रुटि! HomeContainersListView - + Unable change protocol while there is an active connection सक्रिय कनेक्शन होने पर प्रोटोकॉल बदलने में असमर्थ @@ -155,46 +215,46 @@ HomeSplitTunnelingDrawer - + Split tunneling विभाजित सुरंग - + Allows you to connect to some sites or applications through a VPN connection and bypass others आपको वीपीएन कनेक्शन के माध्यम से कुछ साइटों या एप्लिकेशन से जुड़ने और अन्य को बायपास करने की अनुमति देता है - + Split tunneling on the server सर्वर पर स्प्लिट टनलिंग - + Enabled Can't be disabled for current server सक्रिय वर्तमान सर्वर के लिए अक्षम नहीं किया जा सकता - + Site-based split tunneling साइट-आधारित विभाजित टनलिंग - - + + Enabled सक्षम किया - - + + Disabled अक्षम - + App-based split tunneling ऐप-आधारित स्प्लिट टनलिंग @@ -202,93 +262,115 @@ Can't be disabled for current server ImportController - Unable to open file - फाइल खोलने में असमर्थ + फाइल खोलने में असमर्थ - - Invalid configuration file - अमान्य कॉन्फ़िगरेशन फ़ाइल + अमान्य कॉन्फ़िगरेशन फ़ाइल - + Scanned %1 of %2. %2 में से %1 स्कैन किया गया. + + + In the imported configuration, potentially dangerous lines were found: + + InstallController - + %1 installed successfully. %1 सफलतापूर्वक स्थापित हुआ. - + %1 is already installed on the server. %1 पहले से ही सर्वर पर स्थापित है. - + Added containers that were already installed on the server सर्वर पर पहले से स्थापित कंटेनर जोड़े गए - + Already installed containers were found on the server. All installed containers have been added to the application सर्वर पर पहले से स्थापित कंटेनर पाए गए। सभी स्थापित कंटेनरों को एप्लिकेशन में जोड़ दिया गया है - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - + Server '%1' was rebooted सर्वर '%1' रीबूट किया गया था - + Server '%1' was removed सर्वर '%1' रीबूट किया गया था - + All containers from server '%1' have been removed सर्वर '%1' से सभी कंटेनर हटा दिए गए हैं - + %1 has been removed from the server '%2' %1 को सर्वर '%2' से हटा दिया गया है - + + Api config removed + + + + %1 cached profile cleared %1 कैश्ड प्रोफ़ाइल साफ़ की गई - + Please login as the user कृपया उपयोगकर्ता के रूप में लॉगिन करें - + Server added successfully सर्वर सफलतापूर्वक जोड़ा गया + + + %1 installed successfully. + + + + + API config reloaded + + + + + Successfully changed the country of connection to %1 + + InstalledAppsDrawer - + Choose application एप्लिकेशन चुनें @@ -324,28 +406,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN AmneziaVPN - + VPN Connected कनेक्ट - + VPN Disconnected कनेक्ट - + AmneziaVPN notification AmneziaVPN अधिसूचना - + Unsecured network detected: असुरक्षित नेटवर्क का पता चला: @@ -353,103 +435,217 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 सर्वर से %1 हटाया गया - + Usually it takes no more than 5 minutes आमतौर पर इसमें 5 मिनट से अधिक समय नहीं लगता है + + PageDevMenu + + + Gateway endpoint + + + + + Dev gateway environment + + + PageHome - + Logging enabled लॉगिंग सक्षम - + Split tunneling enabled स्प्लिट टनलिंग सक्षम - + Split tunneling disabled स्प्लिट टनलिंग अक्षम - + VPN protocol VPN प्रोटोकॉल - + Servers सर्वर - Unable change server while there is an active connection - सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ + सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + Amneziaडब्ल्यूजी सेटिंग्स + + + + MTU + एमटीयू + + + + Server settings + + + + + Port + + + + + Save + सहेजें + + + + Save settings? + सेटिंग्स सेव करें? + + + + Only the settings for this device will be changed + + + + + Continue + जारी रखना + + + + Cancel + रद्द करना + + + + Unable change settings while there is an active connection + सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ PageProtocolAwgSettings - + AmneziaWG settings Amneziaडब्ल्यूजी सेटिंग्स - + + VPN address subnet + VPN एड्रेस सबनेट + + + Port पोर्ट - MTU - एमटीयू + एमटीयू - + + Jc - Junk packet count + + + + + Jmin - Junk packet minimum size + + + + + Jmax - Junk packet maximum size + + + + + S1 - Init packet junk size + + + + + S2 - Response packet junk size + + + + + H1 - Init packet magic header + + + + + H2 - Response packet magic header + + + + + H4 - Transport packet magic header + + + + + H3 - Underload packet magic header + + + + Save सहेजें - + The values of the H1-H4 fields must be unique H1-H4 फ़ील्ड का मान अद्वितीय होना चाहिए - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) फ़ील्ड S1 + संदेश आरंभ आकार (148) का मान S2 + संदेश प्रतिक्रिया आकार (92) के बराबर नहीं होना चाहिए - + Save settings? सेटिंग्स सेव करें? - + All users with whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ - + Continue जारी रखना - + Cancel रद्द करना @@ -457,33 +653,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings लबादा सेटिंग - + Disguised as traffic from से यातायात के रूप में प्रच्छन्न - + Port पोर्ट - - + + Cipher साइफर - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -491,175 +687,175 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings OpenVPN सेटिंग्स - + VPN address subnet VPN एड्रेस सबनेट - + Network protocol नेटवर्क प्रोटोकॉल - + Port पोर्ट - + Auto-negotiate encryption स्वतः-निगोशिएट एन्क्रिप्शन - - + + Hash - + SHA512 - + SHA384 - + SHA256 - + SHA3-512 - + SHA3-384 - + SHA3-256 - + whirlpool - + BLAKE2b512 - + BLAKE2s256 अक्षम - + SHA1 - - + + Cipher साइफर - + AES-256-GCM - + AES-192-GCM - + AES-128-GCM एईएस-128-जीसीएम - + AES-256-CBC - + AES-192-CBC - + AES-128-CBC एईएस-128-सीबीसी - + ChaCha20-Poly1305 चाचा20-पॉली1305 - + ARIA-256-CBC एआरआईए-256-सीबीसी - + CAMELLIA-256-CBC कैमेलिया-256-सीबीसी - + none कोई नहीं - + TLS auth टीएलएस प्राधिकरण - + Block DNS requests outside of VPN VPN के बाहर डीएनएस अनुरोधों को ब्लॉक करें - + Additional client configuration commands अतिरिक्त क्लाइंट कॉन्फ़िगरेशन आदेश - - + + Commands: आदेश: - + Additional server configuration commands अतिरिक्त सर्वर कॉन्फ़िगरेशन आदेश - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -667,42 +863,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings समायोजन - + Show connection options कनेक्शन विकल्प दिखाएँ - + Connection options %1 कनेक्शन विकल्प%1 - + Remove निकालना - + Remove %1 from server? सर्वर से %1 हटाएँ? - + All users with whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Continue जारी रखना - + Cancel रद्द करना @@ -710,56 +906,133 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings शैडोसॉक्स सेटिंग्स - + Port पोर्ट - - + + Cipher साइफर - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ + + PageProtocolWireGuardClientSettings + + + WG settings + डब्ल्यूजी सेटिंग्स + + + + MTU + एमटीयू + + + + Server settings + + + + + Port + + + + + Save + सहेजें + + + + Save settings? + सेटिंग्स सेव करें? + + + + Only the settings for this device will be changed + + + + + Continue + जारी रखना + + + + Cancel + रद्द करना + + + + Unable change settings while there is an active connection + सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ + + PageProtocolWireGuardSettings - + WG settings डब्ल्यूजी सेटिंग्स - + + VPN address subnet + VPN एड्रेस सबनेट + + + Port बंदरगाह - MTU - एमटीयू + एमटीयू - + Save सहेजें - + + Save settings? + सेटिंग्स सेव करें? + + + + All users with whom you shared a connection with will no longer be able to connect to it. + वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. + + + + Continue + जारी रखना + + + + Cancel + रद्द करना + + + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -767,22 +1040,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings एक्सरे सेटिंग्स - + Disguised as traffic from से यातायात के रूप में प्रच्छन्न - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -790,223 +1063,290 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. आपके सर्वर पर एक DNS सेवा स्थापित है, और यह केवल वीपीएन के माध्यम से पहुंच योग्य है. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. DNS पता आपके सर्वर के पते के समान है। आप कनेक्शन टैब के अंतर्गत सेटिंग्स में DNS को कॉन्फ़िगर कर सकते हैं. - + Remove निकालना - + Remove %1 from server? सर्वर से %1 हटाएँ? - + Continue जारी रखना - + Cancel रद्द करना - - Cannot remove Amnezia DNS from running server + + Cannot remove AmneziaDNS from running server चल रहे सर्वर से एम्नेज़िया डीएनएस को नहीं हटाया जा सकता PageServiceSftpSettings - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - + SFTP settings एसएफटीपी सेटिंग्स - + Host मेज़बान - - - - + + + + Copied कॉपी किया गया - + Port पोर्ट - + User name उपयोगकर्ता नाम - + Password पासवर्ड - + Mount folder on device डिवाइस पर फ़ोल्डर माउंट करें - + In order to mount remote SFTP folder as local drive, perform following steps: <br> दूरस्थ SFTP फ़ोल्डर को स्थानीय ड्राइव के रूप में माउंट करने के लिए, निम्नलिखित चरण निष्पादित करें: <br> - - + + <br>1. Install the latest version of <br>1. का नवीनतम संस्करण स्थापित करें - - + + <br>2. Install the latest version of <br>2. का नवीनतम संस्करण स्थापित करें - + Detailed instructions विस्तृत निर्देश - Remove SFTP and all data stored there - एसएफटीपी और वहां संग्रहीत सभी डेटा हटा दें + एसएफटीपी और वहां संग्रहीत सभी डेटा हटा दें - Remove SFTP and all data stored there? - एसएफटीपी और वहां संग्रहीत सभी डेटा हटाएं? + एसएफटीपी और वहां संग्रहीत सभी डेटा हटाएं? - Continue - जारी रखना + जारी रखना - Cancel - रद्द करना + रद्द करना + + + + PageServiceSocksProxySettings + + + Settings updated successfully + सेटिंग्स सफलतापूर्वक अपडेट हो गईं + + + + + SOCKS5 settings + + + + + Host + मेज़बान + + + + + + + Copied + कॉपी किया गया + + + + + Port + + + + + User name + उपयोगकर्ता नाम + + + + + Password + पासवर्ड + + + + Username + + + + + + Change connection settings + + + + + The port must be in the range of 1 to 65535 + + + + + Password cannot be empty + + + + + Username cannot be empty + PageServiceTorWebsiteSettings - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - + Tor website settings टोर वेबसाइट सेटिंग्स - + Website address वेबसाइट का पता - + Copied कॉपी किया गया - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. इस यूआरएल को खोलने के लिए <a href='https://www.torproject.org/download/' style='color: #FBB26A;'>Tor ब्राउज़र</a> का उपयोग करें। - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. आपकी onionसाइट बनाने के बाद, टोर नेटवर्क को इसे उपयोग के लिए उपलब्ध कराने में कुछ मिनट लगते हैं. - + When configuring WordPress set the this onion address as domain. वर्डप्रेस को कॉन्फ़िगर करते समय इस प्याज पते को डोमेन के रूप में सेट करें. - Remove website - वेबसाइट हटाएँ + वेबसाइट हटाएँ - The site with all data will be removed from the tor network. - सभी डेटा वाली साइट को टोर नेटवर्क से हटा दिया जाएगा. + सभी डेटा वाली साइट को टोर नेटवर्क से हटा दिया जाएगा. - Continue - जारी रखना + जारी रखना - Cancel - रद्द करना + रद्द करना PageSettings - + Settings समायोजन - + Servers सर्वर - + Connection कनेक्शन - + Application एप्लिकेशन - + Backup बैकअप - + About AmneziaVPN AmneziaVPN के बारे में - + + Dev console + + + + Close application एप्लिकेशन बंद करो @@ -1014,228 +1354,348 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Amnezia का समर्थन करें - + Amnezia is a free and open-source application. You can support the developers if you like it. एमनेज़िया एक निःशुल्क और ओपन-सोर्स एप्लिकेशन है। यदि आपको यह पसंद है तो आप डेवलपर्स का समर्थन कर सकते हैं।. - + Contacts संपर्क - + Telegram group टेलीग्राम समूह - + To discuss features सुविधाओं पर चर्चा करना - + https://t.me/amnezia_vpn_en - Mail - मेल + मेल - + + support@amnezia.org + + + + For reviews and bug reports समीक्षाओं और बग रिपोर्टों के लिए - + + Copied + कॉपी किया गया + + + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website वेबसाइट - - https://amnezia.org - + + Visit official website + - + Software version: %1 सॉफ़्टवेयर संस्करण: %1 - + Check for updates अद्यतन के लिए जाँच - + Privacy Policy गोपनीयता नीति + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + + + PageSettingsApiServerInfo + + + For the region + + + + + Price + + + + + Work period + + + + + Valid until + + + + + Speed + + + + + Support tag + + + + + Copied + कॉपी किया गया + + + + Reload API config + + + + + Reload API config? + + + + + + Continue + जारी रखना + + + + + Cancel + रद्द करना + + + + Cannot reload API config during active connection + + + + + Remove from application + + + + + Remove from application? + + + + + Cannot remove server during active connection + सक्रिय कनेक्शन के दौरान सर्वर को हटाया नहीं जा सकता + + PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection सक्रिय कनेक्शन के दौरान स्प्लिट टनलिंग सेटिंग्स को नहीं बदला जा सकता - Only the Apps listed here will be accessed through the VPN - केवल यहां सूचीबद्ध ऐप्स को ही वीपीएन के माध्यम से एक्सेस किया जाएगा + केवल यहां सूचीबद्ध ऐप्स को ही वीपीएन के माध्यम से एक्सेस किया जाएगा - Apps from the list should not be accessed via VPN - सूची के ऐप्स को वीपीएन के माध्यम से एक्सेस नहीं किया जाना चाहिए + सूची के ऐप्स को वीपीएन के माध्यम से एक्सेस नहीं किया जाना चाहिए - + + Only the apps from the list should have access via VPN + + + + + Apps from the list should not have access via VPN + + + + App split tunneling ऐप स्प्लिट टनलिंग - + Mode तरीका - + Remove निकालना - + Continue जारी रखना - + Cancel रद्द करना - + application name आवेदन का नाम - + Open executable file निष्पादन योग्य फ़ाइल खोलें - - Executable file (*.*) + + Executable files (*.*) निष्पादनीय फाइल (*.*) PageSettingsApplication - + Application एप्लिकेशन - + Allow application screenshots एप्लिकेशन स्क्रीनशॉट की अनुमति दें - + + Enable notifications + + + + + Enable notifications to show the VPN state in the status bar + + + + Auto start ऑटो स्टार्ट - + Launch the application every time the device is starts हर बार डिवाइस चालू होने पर एप्लिकेशन लॉन्च करें - + Auto connect ऑटो कनेक्ट - + Connect to VPN on app start ऐप शुरू होने पर वीपीएन से कनेक्ट करें - + Start minimized स्टार्ट को मिनिमाइज किया गया - + Launch application minimized लॉन्च एप्लिकेशन को न्यूनतम किया गया - + Language भाषा - + Logging लॉगिंग - + Enabled सक्रिय किया - + Disabled अक्षम - + Reset settings and remove all data from the application सेटिंग्स रीसेट करें और एप्लिकेशन से सभी डेटा हटा दें - + Reset settings and remove all data from the application? सेटिंग्स रीसेट करें और एप्लिकेशन से सभी डेटा हटा दें? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. सभी सेटिंग्स डिफ़ॉल्ट पर रीसेट हो जाएंगी. सभी स्थापित AmneziaVPN सेवाएँ अभी भी सर्वर पर रहेंगी।. - + Continue जारी रखना - + Cancel रद्द करना - + Cannot reset settings during active connection सक्रिय कनेक्शन के दौरान सेटिंग्स रीसेट नहीं की जा सकतीं @@ -1243,78 +1703,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं - + Back up your configuration अपने कॉन्फ़िगरेशन का बैकअप लें - + You can save your settings to a backup file to restore them the next time you install the application. अगली बार जब आप एप्लिकेशन इंस्टॉल करेंगे तो उन्हें पुनर्स्थापित करने के लिए आप अपनी सेटिंग्स को बैकअप फ़ाइल में सहेज सकते हैं. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. बैकअप में AmneziaVPN में जोड़े गए सभी सर्वरों के लिए आपके पासवर्ड और निजी कुंजी शामिल होंगी। इस जानकारी को सुरक्षित स्थान पर रखें. - + Make a backup बैकअप बनाएं - + Save backup file बैकअप फ़ाइल सहेजें - - + + Backup files (*.backup) बैकअप फ़ाइलें (*.backup) - + Backup file saved बैकअप फ़ाइल सहेजी गई - + Restore from backup बैकअप से बहाल करना - + Open backup file बैकअप फ़ाइल खोलें - + Import settings from a backup file? बैकअप फ़ाइल से सेटिंग्स आयात करें? - + All current settings will be reset सभी मौजूदा सेटिंग्स रीसेट कर दी जाएंगी - + Continue जारी रखना - + Cancel रद्द करना - + Cannot restore backup settings during active connection सक्रिय कनेक्शन के दौरान बैकअप सेटिंग्स को पुनर्स्थापित नहीं किया जा सकता @@ -1322,62 +1782,62 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection कनेक्शन - + When AmneziaDNS is not used or installed जब AmneziaDNS का उपयोग या स्थापित नहीं किया जाता है - + Allows you to use the VPN only for certain Apps आपको केवल कुछ ऐप्स के लिए वीपीएन का उपयोग करने की अनुमति देता है - + Use AmneziaDNS Amneziaडीएनएस का प्रयोग करें - + If AmneziaDNS is installed on the server यदि AmneziaDNS सर्वर पर स्थापित है - + DNS servers DNS सर्वर - + Site-based split tunneling साइट-आधारित विभाजित टनलिंग - + Allows you to select which sites you want to access through the VPN आपको यह चुनने की अनुमति देता है कि आप वीपीएन के माध्यम से किन साइटों तक पहुंचना चाहते हैं - + App-based split tunneling ऐप-आधारित स्प्लिट टनलिंग - + KillSwitch स्विच बन्द कर दो - + Disables your internet if your encrypted VPN connection drops out for any reason. यदि आपका एन्क्रिप्टेड वीपीएन कनेक्शन किसी भी कारण से बंद हो जाता है तो आपका इंटरनेट अक्षम कर देता है. - + Cannot change killSwitch settings during active connection सक्रिय कनेक्शन के दौरान किलस्विच सेटिंग्स को नहीं बदला जा सकता @@ -1385,62 +1845,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS डिफ़ॉल्ट सर्वर कस्टम डीएनएस का समर्थन नहीं करता है - + DNS servers DNS सर्वर - + If AmneziaDNS is not used or installed यदि AmneziaDNS का उपयोग या स्थापित नहीं किया गया है - + Primary DNS प्राथमिक डीएनएस - + Secondary DNS द्वितीयक डीएनएस - + Restore default डिफ़ॉल्ट बहाल - + Restore default DNS settings? डिफ़ॉल्ट DNS सेटिंग्स पुनर्स्थापित करें? - + Continue जारी रखना - + Cancel रद्द करना - + Settings have been reset सेटिंग्स रीसेट कर दी गई हैं - + Save सहेजें - + Settings saved सेटिंग्स को सहेजा गया @@ -1448,72 +1908,106 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - लॉगिंग सक्षम है. ध्यान दें कि 14 दिनों के बाद लॉग स्वचालित रूप से अक्षम हो जाएंगे, और सभी लॉग फ़ाइलें हटा दी जाएंगी. + लॉगिंग सक्षम है. ध्यान दें कि 14 दिनों के बाद लॉग स्वचालित रूप से अक्षम हो जाएंगे, और सभी लॉग फ़ाइलें हटा दी जाएंगी. - + Logging लॉगिंग - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. इस फ़ंक्शन को सक्षम करने से एप्लिकेशन के लॉग स्वचालित रूप से सहेजे जाएंगे, डिफ़ॉल्ट रूप से, लॉगिंग कार्यक्षमता अक्षम है। एप्लिकेशन की खराबी की स्थिति में लॉग सेविंग सक्षम करें. - Save logs - लॉग सहेजें + लॉग सहेजें - Open folder with logs - लॉग के साथ फ़ोल्डर खोलें + लॉग के साथ फ़ोल्डर खोलें - + + Save सहेजें - + + Logs files (*.log) लॉग फ़ाइलें (*.log) - + + Logs file saved लॉग फ़ाइल सहेजी गई - Save logs to file - फ़ाइल में लॉग सहेजें + फ़ाइल में लॉग सहेजें - + + Enable logs + + + + Clear logs? लॉग साफ़ करें? - + Continue जारी रखना - + Cancel रद्द करना - + Logs have been cleaned up लॉग साफ़ कर दिए गए हैं - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs लॉग साफ़ करें @@ -1521,22 +2015,22 @@ Already installed containers were found on the server. All installed containers PageSettingsServerData - + All installed containers have been added to the application सभी स्थापित कंटेनरों को एप्लिकेशन में जोड़ दिया गया है - + No new installed containers found कोई नया स्थापित कंटेनर नहीं मिला - + Do you want to reboot the server? क्या आप सर्वर को रीबूट करना चाहते हैं? - + Do you want to clear server from Amnezia software? क्या आप एमनेज़िया सॉफ़्टवेयर से सर्वर साफ़ करना चाहते हैं? @@ -1546,93 +2040,93 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue जारी रखना - - - - + + + + Cancel रद्द करना - + Check the server for previously installed Amnezia services पहले से स्थापित एमनेज़िया सेवाओं के लिए सर्वर की जाँच करें - + Add them to the application if they were not displayed यदि वे प्रदर्शित नहीं थे तो उन्हें एप्लिकेशन में जोड़ें - + Reboot server सर्वर रीबूट करें - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? रीबूट प्रक्रिया में लगभग 30 सेकंड लग सकते हैं। आप निश्चित है आप आगे बढ़ना चाहते है? - + Cannot reboot server during active connection सक्रिय कनेक्शन के दौरान सर्वर को रीबूट नहीं किया जा सकता - + Remove server from application एप्लिकेशन से सर्वर हटाएं - + Do you want to remove the server from application? क्या आप एप्लिकेशन से सर्वर हटाना चाहते हैं? - + Cannot remove server during active connection सक्रिय कनेक्शन के दौरान सर्वर को हटाया नहीं जा सकता - + All users whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Cannot clear server from Amnezia software during active connection सक्रिय कनेक्शन के दौरान एमनेज़िया सॉफ़्टवेयर से सर्वर साफ़ नहीं किया जा सकता - + Reset API config एपीआई कॉन्फिगरेशन रीसेट करें - + Do you want to reset API config? क्या आप एपीआई कॉन्फिगरेशन रीसेट करना चाहते हैं? - + Cannot reset API config during active connection सक्रिय कनेक्शन के दौरान एपीआई कॉन्फिगरेशन को रीसेट नहीं किया जा सकता - + All installed AmneziaVPN services will still remain on the server. सभी स्थापित AmneziaVPN सेवाएँ अभी भी सर्वर पर रहेंगी. - + Clear server from Amnezia software एमनेज़िया सॉफ़्टवेयर से सर्वर साफ़ करें @@ -1640,27 +2134,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name सर्वर का नाम - + Save सहेजें - + Protocols प्रोटोकॉल - + Services सेवाएं - + Management प्रबंध @@ -1668,17 +2167,16 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings समायोजन - Clear %1 profile - %1 प्रोफ़ाइल साफ़ करें + %1 प्रोफ़ाइल साफ़ करें - + Clear %1 profile? %1 प्रोफ़ाइल साफ़ करें? @@ -1688,39 +2186,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection सक्रिय कनेक्शन होने पर %1 प्रोफ़ाइल साफ़ करने में असमर्थ - + Remove निकालना - + All users with whom you shared a connection will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Cannot remove active container सक्रिय कंटेनर को हटाया नहीं जा सकता - + Remove %1 from server? सर्वर से %1 हटाएँ? - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Continue जारी रखना - - + + Cancel रद्द करना @@ -1728,7 +2251,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers सर्वर @@ -1736,201 +2259,367 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function डिफ़ॉल्ट सर्वर स्प्लिट टनलिंग फ़ंक्शन का समर्थन नहीं करता है - + Addresses from the list should not be accessed via VPN सूची के पतों को वीपीएन के माध्यम से एक्सेस नहीं किया जाना चाहिए - + Split tunneling विभाजित सुरंग - + Mode तरीका - + Remove निकालना - + Continue जारी रखना - + Cancel रद्द करना - + Only the sites listed here will be accessed through the VPN केवल यहां सूचीबद्ध साइटों को ही वीपीएन के माध्यम से एक्सेस किया जाएगा - + Cannot change split tunneling settings during active connection सक्रिय कनेक्शन के दौरान स्प्लिट टनलिंग सेटिंग्स को नहीं बदला जा सकता - + website or IP वेबसाइट या आईपी - + Import / Export Sites आयात/निर्यात साइटें - + Import आयात - + Save site list साइट सूची सहेजें - + Save sites साइटें सहेजें - - - + + + Sites files (*.json) - + Import a list of sites साइटों की सूची आयात करें - + Replace site list साइट सूची बदलें - - + + Open sites file साइट फ़ाइल खोलें - + Add imported sites to existing ones आयातित साइटों को मौजूदा साइटों में जोड़ें + + PageSetupWizardApiServiceInfo + + + For the region + + + + + Price + + + + + Work period + + + + + Speed + + + + + Features + + + + + Connect + कनेक्ट + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + + + + + Choose a VPN service that suits your needs. + + + PageSetupWizardConfigSource - Server connection - सर्वर कनेक्शन + सर्वर कनेक्शन - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - अविश्वसनीय स्रोतों से कनेक्शन कोड का उपयोग न करें, क्योंकि वे आपके डेटा को बाधित करने के लिए बनाए जा सकते हैं. + अविश्वसनीय स्रोतों से कनेक्शन कोड का उपयोग न करें, क्योंकि वे आपके डेटा को बाधित करने के लिए बनाए जा सकते हैं. - What do you have? - तुम्हारे पास क्या है? + तुम्हारे पास क्या है? - File with connection settings or backup - कनेक्शन सेटिंग्स वाली फ़ाइल + कनेक्शन सेटिंग्स वाली फ़ाइल - + + Connection + कनेक्शन + + + + Settings + समायोजन + + + + Enable logs + + + + + Support tag + + + + + Copied + कॉपी किया गया + + + + Insert the key, add a configuration file or scan the QR-code + + + + + Insert key + + + + + Insert + डालना + + + + Continue + जारी रखना + + + + Other connection options + + + + + Site Amnezia + + + + + VPN by Amnezia + + + + + Connect to classic paid and free VPN services from Amnezia + + + + + Self-hosted VPN + + + + + Configure Amnezia VPN on your own server + + + + + Restore from backup + बैकअप से बहाल करना + + + + + + + + + Open backup file + बैकअप फ़ाइल खोलें + + + + Backup files (*.backup) + बैकअप फ़ाइलें (*.backup) + + + File with connection settings कनेक्शन सेटिंग्स वाली फ़ाइल - + + + + + + Open config file कॉन्फ़िग फ़ाइल खोलें - + QR code क्यू आर संहिता - + + + + + + + I have nothing + मेरे पास कुछ नहीं है + + + + + + + Key as text - पाठ के रूप में कुंजी + पाठ के रूप में कुंजी PageSetupWizardCredentials - + Configure your server अपना सर्वर कॉन्फ़िगर करें - + Server IP address [:port] सर्वर आईपी पता [:पोर्ट] - + Continue जारी रखना - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties आपके द्वारा दर्ज किया गया सभी डेटा पूरी तरह से गोपनीय रहेगा और एमनेज़िया या किसी तीसरे पक्ष को साझा या प्रकट नहीं किया जाएगा - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username SSH उपयोगकर्ता नाम - + Password or SSH private key पासवर्ड या SSH निजी कुंजी - + + How to run your VPN server + + + + + Where to get connection data, step-by-step instructions for buying a VPS + + + + Ip address cannot be empty आईपी ​​पता खाली नहीं हो सकता - + Enter the address in the format 255.255.255.255:88 पता 255.255.255.255:88 प्रारूप में दर्ज करें - + Login cannot be empty लॉगिन खाली नहीं हो सकता - + Password/private key cannot be empty पासवर्ड/निजी कुंजी खाली नहीं हो सकती @@ -1938,22 +2627,22 @@ Already installed containers were found on the server. All installed containers PageSetupWizardEasy - + What is the level of internet control in your region? आपके क्षेत्र में इंटरनेट नियंत्रण का स्तर क्या है? - + Choose a VPN protocol एक वीपीएन प्रोटोकॉल चुनें - + Continue जारी रखना - + Skip setup सेटअप छोड़ें @@ -1961,38 +2650,38 @@ Already installed containers were found on the server. All installed containers PageSetupWizardInstalling - - + + Usually it takes no more than 5 minutes आमतौर पर इसमें 5 मिनट से अधिक समय नहीं लगता है - + The server has already been added to the application सर्वर को पहले ही एप्लिकेशन में जोड़ा जा चुका है - + Amnezia has detected that your server is currently Amnezia ने पता लगाया है कि आपका सर्वर वर्तमान में है - + busy installing other software. Amnezia installation अन्य सॉफ़्टवेयर स्थापित करने में व्यस्त। भूलने की बीमारी की स्थापना - + Cancel installation स्थापना रद्द करें - + will pause until the server finishes installing other software जब तक सर्वर अन्य सॉफ़्टवेयर इंस्टॉल करना समाप्त नहीं कर लेता तब तक रुकेगा - + Installing स्थापित कर रहा है @@ -2000,45 +2689,50 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocolSettings - + Installing %1 %1 स्थापित किया जा रहा है - + More detailed अधिक विवरण - + Close बंद करना - + Network protocol नेटवर्क प्रोटोकॉल - + Port منفذ - + Install स्थापित करना + + + The port must be in the range of 1 to 65535 + + PageSetupWizardProtocols - + VPN protocol VPN प्रोटोकॉल - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. वह चुनें जो आपके लिए सर्वोच्च प्राथमिकता हो। बाद में, आप अन्य प्रोटोकॉल और अतिरिक्त सेवाएँ, जैसे DNS प्रॉक्सी और SFTP स्थापित कर सकते हैं. @@ -2046,7 +2740,7 @@ Already installed containers were found on the server. All installed containers PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. कैमरे को QR कोड पर रखें और कुछ सेकंड के लिए रोककर रखें. @@ -2054,60 +2748,55 @@ Already installed containers were found on the server. All installed containers PageSetupWizardStart - Settings restored from backup file - बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं + बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं - Free service for creating a personal VPN on your server. - आपके सर्वर पर व्यक्तिगत वीपीएन बनाने के लिए निःशुल्क सेवा. + आपके सर्वर पर व्यक्तिगत वीपीएन बनाने के लिए निःशुल्क सेवा. - Helps you access blocked content without revealing your privacy, even to VPN providers. - आपकी गोपनीयता को उजागर किए बिना, यहां तक ​​कि वीपीएन प्रदाताओं को भी, अवरुद्ध सामग्री तक पहुंचने में आपकी सहायता करता है. + आपकी गोपनीयता को उजागर किए बिना, यहां तक ​​कि वीपीएन प्रदाताओं को भी, अवरुद्ध सामग्री तक पहुंचने में आपकी सहायता करता है. - I have the data to connect - मेरे पास कनेक्ट करने के लिए डेटा है + मेरे पास कनेक्ट करने के लिए डेटा है - I have nothing - मेरे पास कुछ नहीं है + मेरे पास कुछ नहीं है - - https://amnezia.org/instructions/0_starter-guide - + + Let's get started + PageSetupWizardTextKey - + Connection key कनेक्शन कुंजी - + A line that starts with vpn://... एक लाइन जो vpn://... से शुरू होती है... - + Key चाबी - + Insert डालना - + Continue जारी रखना @@ -2115,32 +2804,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardViewConfig - + New connection नया कनेक्शन - + Collapse content सामग्री संक्षिप्त करें - + Show content सामग्री दिखाओ - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. वायरगार्ड अस्पष्टीकरण सक्षम करें. यदि आपके प्रदाता पर वायरगार्ड अवरुद्ध है तो यह उपयोगी हो सकता है. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. केवल उन स्रोतों से कनेक्शन कोड का उपयोग करें जिन पर आपको भरोसा है। हो सकता है कि आपके डेटा को इंटरसेप्ट करने के लिए सार्वजनिक स्रोतों से कोड बनाए गए हों. - + Connect कनेक्ट @@ -2148,32 +2837,32 @@ Already installed containers were found on the server. All installed containers PageShare - + Save OpenVPN config OpenVPN कॉन्फ़िगरेशन सहेजें - + Save WireGuard config वायरगार्ड कॉन्फ़िगरेशन सहेजें - + Save AmneziaWG config AmneziaWG कॉन्फ़िगरेशन सहेजें - + Save Shadowsocks config शैडोसॉक्स कॉन्फ़िगरेशन सहेजें - + Save Cloak config क्लोक कॉन्फ़िगरेशन सहेजें - + Save XRay config एक्सरे कॉन्फिगरेशन सहेजें @@ -2183,9 +2872,8 @@ Already installed containers were found on the server. All installed containers AmneziaVPN ऐप के लिए - OpenVpn native format - OpenVpn मूल स्वरूप + OpenVpn मूल स्वरूप @@ -2213,146 +2901,156 @@ Already installed containers were found on the server. All installed containers एक्सरे देशी प्रारूप - + Share VPN Access VPN एक्सेस साझा करें - + Share full access to the server and VPN सर्वर और वीपीएन तक पूर्ण पहुंच साझा करें - + Use for your own devices, or share with those you trust to manage the server. अपने स्वयं के उपकरणों के लिए उपयोग करें, या सर्वर को प्रबंधित करने के लिए उन लोगों के साथ साझा करें जिन पर आप भरोसा करते हैं. - - + + Users उपयोगकर्ताओं - + Share VPN access without the ability to manage the server सर्वर को प्रबंधित करने की क्षमता के बिना वीपीएन एक्सेस साझा करें - + Search खोज - + Creation date: %1 निर्माण दिनांक: %1 - + Latest handshake: %1 नवीनतम हाथ मिलाना: %1 - + Data received: %1 प्राप्त डेटा: %1 - + Data sent: %1 डेटा भेजा गया: %1 + + + Allowed IPs: %1 + + Creation date: निर्माण तिथि: - + Rename नाम बदलें - + Client name ग्राहक नाम - + Save सहेजें - + Revoke निरस्त करें - + Revoke the config for a user - %1? किसी उपयोक्ता के लिए कॉन्फ़िगरेशन निरस्त करें - %1? - + The user will no longer be able to connect to your server. उपयोगकर्ता अब आपके सर्वर से कनेक्ट नहीं हो पाएगा. - + Continue जारी रखना - + Cancel रद्द करना - + Connection कनेक्शन - - + + Server सर्वर - + File with connection settings to कनेक्शन सेटिंग्स वाली फ़ाइल - - + + Protocol शिष्टाचार - + Connection to कनेक्शन के लिए - + Config revoked कॉन्फ़िगरेशन निरस्त कर दिया गया - + + OpenVPN native format + + + + User name उपयोगकर्ता नाम - - + + Connection format कनेक्शन प्रारूप - - + + Share शेयर करना @@ -2360,50 +3058,55 @@ Already installed containers were found on the server. All installed containers PageShareFullAccess - + Full access to the server and VPN सर्वर और वीपीएन तक पूर्ण पहुंच - + We recommend that you use full access to the server only for your own additional devices. हम अनुशंसा करते हैं कि आप सर्वर तक पूर्ण पहुंच का उपयोग केवल अपने अतिरिक्त उपकरणों के लिए करें. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. यदि आप अन्य लोगों के साथ पूर्ण पहुंच साझा करते हैं, तो वे सर्वर पर प्रोटोकॉल और सेवाओं को हटा और जोड़ सकते हैं, जिससे वीपीएन सभी उपयोगकर्ताओं के लिए गलत तरीके से काम करेगा. - - + + Server सर्वर - + Accessing एक्सेस करना - + File with accessing settings to एक्सेस सेटिंग्स वाली फ़ाइल - + Share शेयर करना - + + Access error! + प्रवेश त्रुटि! + + + Connection to के लिए कनेक्शन - + File with connection settings to कनेक्शन सेटिंग्स वाली फ़ाइल @@ -2411,15 +3114,25 @@ Already installed containers were found on the server. All installed containers PageStart - + Logging was disabled after 14 days, log files were deleted 14 दिनों के बाद लॉगिंग अक्षम कर दी गई, लॉग फ़ाइलें हटा दी गईं + + + Settings restored from backup file + बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType - + Close बंद करना @@ -2453,12 +3166,12 @@ Already installed containers were found on the server. All installed containers पासवर्ड नहीं मिला - + Could not open keystore कीस्टोर नहीं खुल सका - + Could not remove private key from keystore कीस्टोर से निजी कुंजी नहीं हटाई जा सकी @@ -2634,27 +3347,27 @@ Already installed containers were found on the server. All installed containers कीस्टोर नहीं खुल सका - + Could not create private key generator निजी कुंजी जेनरेटर नहीं बनाया जा सका - + Could not generate new private key नई निजी कुंजी उत्पन्न नहीं हो सकी - + Could not retrieve private key from keystore कीस्टोर से निजी कुंजी पुनर्प्राप्त नहीं की जा सकी - + Could not create encryption cipher एन्क्रिप्शन सिफर नहीं बनाया जा सका - + Could not encrypt data डेटा एन्क्रिप्ट नहीं किया जा सका @@ -2662,10 +3375,17 @@ Already installed containers were found on the server. All installed containers QObject - + SFTP service एसएफटीपी सेवा + + + + + SOCKS5 proxy server + + No error @@ -2673,6 +3393,7 @@ Already installed containers were found on the server. All installed containers + Unknown error अज्ञात त्रुटि @@ -2682,233 +3403,278 @@ Already installed containers were found on the server. All installed containers फ़ंक्शन क्रियान्वित नहीं किया गया - + + Background service is not running + + + + Server check failed सर्वर जाँच विफल रही - + Server port already used. Check for another software सर्वर पोर्ट पहले ही उपयोग किया जा चुका है. किसी अन्य सॉफ़्टवेयर की जाँच करें - + Server error: Docker container missing सर्वर त्रुटि: डॉकर कंटेनर गायब है - + Server error: Docker failed सर्वर त्रुटि: डॉकर विफल - + Installation canceled by user उपयोगकर्ता द्वारा इंस्टॉलेशन रद्द कर दिया गया - + The user does not have permission to use sudo उपयोगकर्ता के पास sudo का उपयोग करने की अनुमति नहीं है - + Server error: Packet manager error सर्वर त्रुटि: पैकेट प्रबंधक त्रुटि - + SSH request was denied SSH अनुरोध अस्वीकार कर दिया गया - + SSH request was interrupted SSH अनुरोध बाधित हो गया था - + SSH internal error SSH आंतरिक त्रुटि - + Invalid private key or invalid passphrase entered अमान्य निजी कुंजी या अमान्य पासफ़्रेज़ दर्ज किया गया - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types चयनित निजी कुंजी प्रारूप समर्थित नहीं है, ओपनश ED25519 कुंजी प्रकार या PEM कुंजी प्रकार का उपयोग करें - + Timeout connecting to server सर्वर से कनेक्ट होने का समय समाप्त - + + Unable to open config file + + + + VPN connection error VPN कनेक्शन त्रुटि - + Error when retrieving configuration from API एपीआई से कॉन्फ़िगरेशन पुनर्प्राप्त करते समय त्रुटि - + This config has already been added to the application यह कॉन्फ़िगरेशन पहले ही एप्लिकेशन में जोड़ा जा चुका है - + + In the response from the server, an empty config was received + + + + + SSL error occurred + + + + + Server response timeout on api request + + + + + Missing AGW public key + + + + + Failed to decrypt response payload + + + + + Missing list of available services + + + + ErrorCode: %1. ErrorCode: %1. - + OpenVPN config missing OpenVPN प्रबंधन सर्वर त्रुटि - + SCP error: Generic failure एससीपी त्रुटि: सामान्य विफलता - + OpenVPN management server error OpenVPN प्रबंधन सर्वर त्रुटि - + OpenVPN executable missing OpenVPN निष्पादन योग्य गायब है - + Shadowsocks (ss-local) executable missing शैडोसॉक्स (एसएस-स्थानीय) निष्पादन योग्य गायब है - + Cloak (ck-client) executable missing क्लोक (सीके-क्लाइंट) निष्पादन योग्य गायब है - + Amnezia helper service error Amnezia भूलने की बीमारी सहायक सेवा त्रुटि - + OpenSSL failed ओपनएसएसएल विफल रहा - + Can't connect: another VPN connection is active कनेक्ट नहीं हो सकता: कोई अन्य वीपीएन कनेक्शन सक्रिय है - + Can't setup OpenVPN TAP network adapter OpenVPN TAP नेटवर्क एडाप्टर सेटअप नहीं कर सकता - + VPN pool error: no available addresses VPN pool error: لا يوجد عنواين مٌتاحة - + The config does not contain any containers and credentials for connecting to the server कॉन्फ़िगरेशन में सर्वर से कनेक्ट करने के लिए कोई कंटेनर और क्रेडेंशियल नहीं है - + QFile error: The file could not be opened Qफ़ाइल त्रुटि: फ़ाइल खोली नहीं जा सकी - + QFile error: An error occurred when reading from the file Qफ़ाइल त्रुटि: फ़ाइल से पढ़ते समय एक त्रुटि उत्पन्न हुई - + QFile error: The file could not be accessed Qफ़ाइल त्रुटि: फ़ाइल तक नहीं पहुंचा जा सका - + QFile error: An unspecified error occurred Qफ़ाइल त्रुटि: एक अनिर्दिष्ट त्रुटि उत्पन्न हुई - + QFile error: A fatal error occurred Qफ़ाइल त्रुटि: एक घातक त्रुटि उत्पन्न हुई - + QFile error: The operation was aborted Qफ़ाइल त्रुटि: ऑपरेशन निरस्त कर दिया गया था - + Internal error आंतरिक त्रुटि - + IPsec - - + + Website in Tor network टोर नेटवर्क में वेबसाइट - + AmneziaDNS AmneziaDNS - + SFTP file sharing service SFTP फ़ाइल साझाकरण सेवा - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. लचीले कॉन्फ़िगरेशन विकल्पों के साथ ओपनवीपीएन सबसे लोकप्रिय वीपीएन प्रोटोकॉल है। यह कुंजी विनिमय के लिए एसएसएल/टीएलएस के साथ अपने स्वयं के सुरक्षा प्रोटोकॉल का उपयोग करता है. - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. शैडोसॉक्स - वीपीएन ट्रैफ़िक को मास्क करता है, जिससे यह सामान्य वेब ट्रैफ़िक के समान हो जाता है, लेकिन इसे कुछ अत्यधिक सेंसर किए गए क्षेत्रों में विश्लेषण प्रणालियों द्वारा पहचाना जा सकता है. - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. क्लोक पर ओपनवीपीएन - ओपनवीपीएन वीपीएन के साथ वेब ट्रैफिक और सक्रिय-जांच पहचान के खिलाफ सुरक्षा का मुखौटा लगाता है। उच्चतम स्तर की सेंसरशिप वाले क्षेत्रों में अवरोध को दूर करने के लिए आदर्श. - + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. वास्तविकता के साथ एक्सरे - उच्चतम स्तर की इंटरनेट सेंसरशिप वाले देशों के लिए उपयुक्त। टीएलएस स्तर पर ट्रैफ़िक को वेब ट्रैफ़िक के रूप में छिपाया जाता है, और सक्रिय जांच विधियों द्वारा पता लगाने से सुरक्षा प्रदान की जाती है. - + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + + + + Create a file vault on your server to securely store and transfer files. फ़ाइलों को सुरक्षित रूप से संग्रहीत और स्थानांतरित करने के लिए अपने सर्वर पर एक फ़ाइल वॉल्ट बनाएं. - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -2947,7 +3713,7 @@ OpenVPN क्लाइंट और सर्वर के बीच सभी - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -2968,7 +3734,7 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * यूडीपी नेटवर्क प्रोटोकॉल पर काम करता है।. - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -2987,32 +3753,31 @@ For more detailed information, you can इसे "एसएफटीपी फ़ाइल संग्रहण बनाएं" के अंतर्गत सहायता अनुभाग में ढूंढें - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. वायरगार्ड - उच्च प्रदर्शन, उच्च गति और कम बिजली की खपत के साथ नया लोकप्रिय वीपीएन प्रोटोकॉल। सेंसरशिप के निम्न स्तर वाले क्षेत्रों के लिए अनुशंसित. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. AmneziaWG - वायरगार्ड पर आधारित Amnezia का विशेष प्रोटोकॉल। यह वायरगार्ड की तरह तेज़ है, लेकिन रुकावटों के प्रति बहुत प्रतिरोधी है। उच्च स्तर की सेंसरशिप वाले क्षेत्रों के लिए अनुशंसित. - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec - आधुनिक स्थिर प्रोटोकॉल, दूसरों की तुलना में थोड़ा तेज़, सिग्नल हानि के बाद कनेक्शन पुनर्स्थापित करता है। + IKEv2/IPsec - आधुनिक स्थिर प्रोटोकॉल, दूसरों की तुलना में थोड़ा तेज़, सिग्नल हानि के बाद कनेक्शन पुनर्स्थापित करता है। - + Deploy a WordPress site on the Tor network in two clicks. दो क्लिक में टोर नेटवर्क पर एक वर्डप्रेस साइट तैनात करें।. - + Replace the current DNS server with your own. This will increase your privacy level. वर्तमान DNS सर्वर को अपने स्वयं के DNS सर्वर से बदलें। इससे आपकी गोपनीयता का स्तर बढ़ जाएगा. - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -3031,7 +3796,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * टीसीपी और यूडीपी दोनों नेटवर्क प्रोटोकॉल पर काम कर सकता है।. - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -3046,7 +3811,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * टीसीपी नेटवर्क प्रोटोकॉल पर काम करता है।. - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -3067,7 +3832,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin * यूडीपी नेटवर्क प्रोटोकॉल पर काम करता है।. - + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. @@ -3078,7 +3843,7 @@ Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REAL VMess, VLESS और XTLS-Vision ट्रांसपोर्ट जैसे पुराने प्रोटोकॉल के विपरीत, TLS हैंडशेक पर REALITY की अभिनव "दोस्त या दुश्मन" पहचान सुरक्षा को बढ़ाती है और सक्रिय जांच तकनीकों को नियोजित करने वाले परिष्कृत DPI सिस्टम द्वारा पहचान को रोकती है। यह REALITY को कठोर सेंसरशिप वाले वातावरण में इंटरनेट की स्वतंत्रता बनाए रखने के लिए एक मजबूत समाधान बनाता है. - + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -3099,7 +3864,7 @@ While it offers a blend of security, stability, and speed, it's essential t * यूडीपी नेटवर्क प्रोटोकॉल, पोर्ट 500 और 4500 पर काम करता है. - + DNS Service DNS सेवाएँ @@ -3153,35 +3918,169 @@ While it offers a blend of security, stability, and speed, it's essential t No match कोई मुकाबला नहीं - - - Unknown error - अज्ञात त्रुटि - error 0x%1: %2 त्रुटि 0x%1: %2 + + + vmess:// url is invalid + + + + + Invalid streamSettings protocol: + + + + + Unknown transport method: + + + + + VMess string should start with 'vmess://' + + + + + VMess string should be a valid base64 string + + + + + JSON should not be empty + + + + + VLESS link should start with vless:// + + + + + link parse failed: %1 + + + + + empty host + + + + + missing port + + + + + missing uuid + + + + + Invalid ssd link: json: field %1 must exist + + + + + Invalid ssd link: json: field %1 must be valid port number + + + + + Invalid ssd link: json: field %1 must be of type 'string' + + + + + Invalid ssd link: json: field %1 must be an array + + + + + Skipping invalid ssd server: server must be an object + + + + + Skipping invalid ssd server: missing required field %1 + + + + + Skipping invalid ssd server: field %1 should be of type 'string' + + + + + Invalid ssd link: should begin with ssd:// + + + + + Invalid ssd link: base64 parse failed + + + + + Invalid ssd link: json parse failed + + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + + + + + SS URI is too short + + + + + + Can't find the colon separator between method and password + + + + + Can't find the at separator between password and hostname + + + + + Can't find the colon separator between hostname and port + + SelectLanguageDrawer - + Choose language भाषा चुनें + + ServersListView + + + Unable change server while there is an active connection + सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ + + Settings - + Server #1 सर्वर #1 - - + + Server सर्वर @@ -3189,12 +4088,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted बैकअप फ़ाइल दूषित है - + All settings have been reset to default values सभी सेटिंग्स को डिफ़ॉल्ट मानों पर रीसेट कर दिया गया है @@ -3202,29 +4101,29 @@ While it offers a blend of security, stability, and speed, it's essential t ShareConnectionDrawer - - + + Save AmneziaVPN config AmneziaVPN कॉन्फ़िगरेशन सहेजें - + Share शेयर करना - + Copy कॉपी - - + + Copied कॉपी किया गया - + Copy config string कॉन्फिग स्ट्रिंग कॉपी करें @@ -3234,7 +4133,7 @@ While it offers a blend of security, stability, and speed, it's essential t कनेक्शन सेटिंग दिखाएं - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" एमनेज़िया ऐप में क्यूआर कोड पढ़ने के लिए, "सर्वर जोड़ें" → "मेरे पास कनेक्ट करने के लिए डेटा है" → "क्यूआर कोड, कुंजी या सेटिंग्स फ़ाइल" चुनें। @@ -3257,27 +4156,27 @@ While it offers a blend of security, stability, and speed, it's essential t साइट हटाई गई: %1 - + Can't open file: %1 फ़ाइल नहीं खुल सकती: %1 - + Failed to parse JSON data from file: %1 फ़ाइल से JSON डेटा पार्स करने में विफल:%1 - + The JSON data is not an array in file: %1 JSON डेटा फ़ाइल में कोई सरणी नहीं है: %1 - + Import completed आयात पूरा हुआ - + Export completed निर्यात पूरा हुआ @@ -3318,7 +4217,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty फ़ील्ड खाली नहीं हो सकती @@ -3326,7 +4225,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps @@ -3334,42 +4233,42 @@ While it offers a blend of security, stability, and speed, it's essential t VpnProtocol - + Unknown अज्ञात - + Disconnected اडिस्कनेक्ट किया गया - + Preparing तैयार कर रहे हैं - + Connecting... कनेक्ट... - + Connected जुड़ा हुआ - + Disconnecting... कनेक्ट... - + Reconnecting... कनेक्ट... - + Error गलती @@ -3377,45 +4276,47 @@ While it offers a blend of security, stability, and speed, it's essential t amnezia::ContainerProps - + Low कम - Medium or High - मध्यम या उच्च + मध्यम या उच्च - Extreme - चरम + चरम - + + High + + + + I just want to increase the level of my privacy. मैं बस अपनी गोपनीयता का स्तर बढ़ाना चाहता हूं. - + I want to bypass censorship. This option recommended in most cases. मैं सेंसरशिप को दरकिनार करना चाहता हूं। अधिकांश मामलों में इस विकल्प की अनुशंसा की जाती है. - Most VPN protocols are blocked. Recommended if other options are not working. - अधिकांश वीपीएन प्रोटोकॉल अवरुद्ध हैं। यदि अन्य विकल्प काम नहीं कर रहे हों तो अनुशंसित. + अधिकांश वीपीएन प्रोटोकॉल अवरुद्ध हैं। यदि अन्य विकल्प काम नहीं कर रहे हों तो अनुशंसित. main2 - + Private key passphrase निजी कुंजी पासफ़्रेज़ - + Save सहेजें diff --git a/client/translations/amneziavpn_my_MM.ts b/client/translations/amneziavpn_my_MM.ts index 25970722..55243d1b 100644 --- a/client/translations/amneziavpn_my_MM.ts +++ b/client/translations/amneziavpn_my_MM.ts @@ -1,50 +1,157 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ မည်သည့်ဆိုက်များအတွက်မဆိုအလုပ်လုပ်ပြီး လိုင်းအရှိန် %1 MBit/s အထိအသုံးပြုနိုင်ပါတယ်။ + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + အင်တာနက် ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသော ဒေသများရှိ ပိတ်ဆို့ထားသော ဆိုက်များကို ဝင်ရောက်ရန် VPN။. + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + Amnezia Premium - သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကိုကြည်လင်ပြတ်သားစွာကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ အင်တာနက်ဆင်ဆာဖြတ်မှု အဆင့်အမြင့်ဆုံးနိုင်ငံများတွင်ပင် မည်သည့်ဆိုက်များအတွက်မဆို အလုပ်လုပ်ပါသည်။. + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + Amnezia Free သည် အင်တာနက်ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသောနိုင်ငံများတွင် ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်ရန်အတွက် အခမဲ့ VPN တစ်ခုဖြစ်ပါသည်။ + + + + %1 MBit/s + %1 MBit/s + + + + %1 days + %1 ရက် + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + ဤ VPN သည် သင့်ဒေသရှိ Instagram၊ Facebook၊ Twitter နှင့် အခြားသော လူကြိုက်များသော ဆိုက်များကိုသာ ဖွင့်ပေးပါမည်။ အခြားဝဘ်ဆိုက်များကိုမူ သင်၏ IP လိပ်စာအစစ်အမှန်ဖြင့်သာ ဖွင့်ပေးပါမည်၊ <a href="%1/free" style="color: #FBB26A;">နောက်ထပ်အသေးစိတ်အချက်အလက်များကို ဝဘ်ဆိုဒ်ပေါ်တွင်ကြည့်ရန်</a> + + + + Free + အခမဲ့ + + + + %1 $/month + %1 $/တစ်လ + + + + AppSplitTunnelingController + + + Application added: %1 + အပလီကေးရှင်းထည့်ပြီးပါပြီ: %1 + + + + The application has already been added + အပလီကေးရှင်းကို ထည့်သွင်းသားဖြစ်သည် + + + + The selected applications have been added + ရွေးချယ်ထားသောအပလီကေးရှင်းများကို ထည့်သွင်းပြီးပါပြီ + + + + Application removed: %1 + အပလီကေးရှင်းကို ဖယ်ရှားလိုက်သည်: %1 + + + + ConnectButton + + + Unable to disconnect during configuration preparation + Configuration ပြင်ဆင်ခြင်းလုပ်ဆောင်နေချိန်အတွင်း ချိတ်ဆက်မှုဖြတ်တောက်၍မရပါ + + ConnectionController - + VPN Protocols is not installed. Please install VPN container at first VPN ပရိုတိုကောများကို မထည့်သွင်းရသေးပါ။ ကျေးဇူးပြု၍ VPN ကွန်တိန်နာကို အရင်ထည့်သွင်းပါ။ - + Connecting... ချိတ်ဆက်နေပါပြီ... - + Connected ချိတ်ဆက်ပြီးသွားပါပြီ - + + Preparing... + ပြင်ဆင်နေသည်... + + + Settings updated successfully, reconnnection... ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ၊ ပြန်လည်ချိတ်ဆက်နေပါသည်... - + Settings updated successfully - ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ + ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ - + + The selected protocol is not supported on the current platform + ရွေးချယ်ထားသော ပရိုတိုကောကို လက်ရှိပလက်ဖောင်းပေါ်တွင် အ‌ထောက်အပံ့မပေးထားပါ + + + + unable to create configuration + configuration ဖန်တီး၍မရပါ + + + Reconnecting... ပြန်လည်ချိတ်ဆက်နေပါသည်... - - - - + + + + Connect ချိတ်ဆက်မည် - + Disconnecting... အဆက်အသွယ်ဖြတ်နေပါသည်... @@ -54,17 +161,17 @@ Add new connection - ချိတ်ဆက်မှုအသစ်ထည့်သွင်းပါ။ + ချိတ်ဆက်မှုအသစ်ထည့်သွင်းမည် Configure your server - သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိပါ။ + သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိမည် - + Open config file, key or QR code - config ဖိုင်၊ key သို့မဟုတ် QR ကုဒ်ကို ဖွင့်ပါ။ + config ဖိုင်၊ key သို့မဟုတ် QR ကုဒ်ကို ဖွင့်မည် @@ -85,7 +192,7 @@ &Paste - + &SelectAll &SelectAll @@ -93,65 +200,61 @@ ExportController - Access error! - အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! + အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! HomeContainersListView - + Unable change protocol while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ပရိုတိုကောကို ပြောင်းလဲ၍မရပါ။ - - - The selected protocol is not supported on the current platform - ရွေးချယ်ထားသော ပရိုတိုကောကို လက်ရှိပလက်ဖောင်းပေါ်တွင် အ‌ထောက်အပံ့မပေးထားပါ။ - HomeSplitTunnelingDrawer - + Split tunneling Split tunneling - + Allows you to connect to some sites or applications through a VPN connection and bypass others VPN ချိတ်ဆက်မှုကြားခံ၍ အချို့သောဆိုက်များ သို့မဟုတ် အပလီကေးရှင်းများသို့ ချိတ်ဆက်ဖို့ရန်နှင့် အခြားအရာများကို ကျော်ဖြတ်ရန် လုပ်ဆောင်ပေးသည်။ - + Split tunneling on the server ဆာဗာပေါ်တွင် split tunneling အသုံးပြုထားပါသည်။ - + Enabled Can't be disabled for current server ဖွင့်ထားပါသည်။ လက်ရှိဆာဗာအတွက် ပိတ်၍မရပါ။ - + Site-based split tunneling ဝက်ဆိုဒ်အခြေပြု split tunneling - + + Enabled ဖွင့်ထားပါသည်။ - + + Disabled ပိတ်ထားပါသည်။ - + App-based split tunneling App အခြေပြု split tunneling @@ -159,84 +262,127 @@ Can't be disabled for current server ImportController - Unable to open file - + ဖိုင်ကိုဖွင့်၍မရပါ - - Invalid configuration file - + Configuration ဖိုင် မမှန်ကန်ပါ - + Scanned %1 of %2. %2 ၏ %1 ကို စကင်န်ဖတ်ထားသည်. + + + In the imported configuration, potentially dangerous lines were found: + တင်သွင်းသည့် configuration တွင်၊ အန္တရာယ်ရှိနိုင်သည့်စာလိုင်းများကို တွေ့ရှိခဲ့သည်: + InstallController - - + %1 installed successfully. %1 ကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ. - - + %1 is already installed on the server. %1 ကို ဆာဗာတွင် ထည့်သွင်းပြီးဖြစ်သည်. - + Added containers that were already installed on the server ဆာဗာတွင် ထည့်သွင်းပြီးသား ကွန်တိန်နာများကို ပေါင်းထည့်ပြီးပါပြီ။ - + Already installed containers were found on the server. All installed containers have been added to the application ထည့်သွင်းပြီးသား ကွန်တိန်နာများကို ဆာဗာပေါ်တွင် တွေ့ရှိခဲ့သည်။ ထည့်သွင်းထားသည့် ကွန်တိန်နာအားလုံးကို အပလီကေးရှင်းထဲသို့ ပေါင်းထည့်ပြီးပါပြီ။ - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ - + Server '%1' was rebooted ဆာဗာ '%1' ကို ပြန်လည်စတင်ခဲ့သည်။ - + Server '%1' was removed ဆာဗာ '%1' ကို ဖယ်ရှားခဲ့သည်။ - + All containers from server '%1' have been removed ဆာဗာ '%1' မှ ကွန်တိန်နာအားလုံးကို ဖယ်ရှားလိုက်ပါပြီ။ - + %1 has been removed from the server '%2' %1 ကို ဆာဗာ '%2' မှ ဖယ်ရှားလိုက်ပါပြီ - - Please login as the user - အသုံးပြုသူအဖြစ် log in ဝင်ရောက်ပါ။ + + Api config removed + Api config ကိုဖယ်ရှားလိုက်သည် - + + %1 cached profile cleared + ကက်ရှ်လုပ်ထားတဲ့ ပရိုဖိုင် %1 ခုကို ရှင်းပြီးပါပြီ + + + + Please login as the user + အသုံးပြုသူအဖြစ် log in ဝင်ရောက်ပါ + + + Server added successfully - ဆာဗာကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ။ + ဆာဗာကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ + + + + %1 installed successfully. + %1 ခုကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ. + + + + API config reloaded + API config ကို ပြန်လည်စတင်လိုက်ပါပြီ + + + + Successfully changed the country of connection to %1 + ချိတ်ဆက်မှုနိုင်ငံကို %1 သို့ အောင်မြင်စွာ ပြောင်းလဲလိုက်ပါပြီ + + + + InstalledAppsDrawer + + + Choose application + အပလီကေးရှင်းရွေးမည် + + + + application name + အပလီကေးရှင်းအမည် + + + + Add selected + ရွေးချယ်ထားသည်များကိုထည့်မည် @@ -249,7 +395,7 @@ Already installed containers were found on the server. All installed containers Write key failed: %1 - key ရေးမှု မအောင်မြင်ပါ: %1 + key ရေးသားမှု မအောင်မြင်ပါ: %1 @@ -260,28 +406,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN AmneziaVPN - + VPN Connected VPN ချိတ်ဆက်ထားပါပြီ - + VPN Disconnected VPN ဖြုတ်လိုက်ပါပြီ - + AmneziaVPN notification AmneziaVPN နိုတီ - + Unsecured network detected: လုံခြုံမှုမရှိသောကွန်ရက်မှန်း ထောက်လှန်းမိသည်: @@ -289,114 +435,230 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 ဝန်ဆောင်မှုများကို %1 မှ ဖယ်ရှားနေပါသည်။ - + Usually it takes no more than 5 minutes - များသောအားဖြင့် 5 မိနစ်ထက်မပိုပါ။ + များသောအားဖြင့် 5 မိနစ်ထက်ပိုမကြာပါ + + + + PageDevMenu + + + Gateway endpoint + Gateway အဆုံးမှတ် + + + + Dev gateway environment + PageHome - + Logging enabled - + Logging ဖွင့်ထားပါသည် - + Split tunneling enabled - split tunnelling ဖွင့်ထားပါသည်။ + split tunnelling ဖွင့်ထားပါသည် - + Split tunneling disabled - split tunnelling ပိတ်ထားပါသည်။ + split tunnelling ပိတ်ထားပါသည် - + VPN protocol VPN ပရိုတိုကော - + Servers ဆာဗာများ - Unable change server while there is an active connection - လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ။ + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + AmneziaWG ဆက်တင်များ + + + + MTU + MTU + + + + Server settings + + + + + Port + Port + + + + Save + သိမ်းဆည်းမည် + + + + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? + + + + Only the settings for this device will be changed + + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ PageProtocolAwgSettings - + AmneziaWG settings AmneziaWG ဆက်တင်များ - + Port Port - MTU - + MTU - - Remove AmneziaWG - AmneziaWG ကို ဖယ်ရှားမည်။ - - - - Remove AmneziaWG from server? - AmneziaWG ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - - - - + All users with whom you shared a connection with will no longer be able to connect to it. - သင့်တွင် သင့်ကိုမည်သည့် ချိတ်ဆက်ထားသော အသုံးပြုသူများသည် အကြောင်းအရာသို့ ဆက်သွယ်ရန် မရနိုင်ပါ။ + သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Save သိမ်းဆည်းမည် - - Save settings? - သိမ်းဆည်းမည်လား။ + + VPN address subnet + VPN လိပ်စာ ကွန်ရက်ခွဲ + + + + Jc - Junk packet count + Jc - Junk packet အရေအတွက် + + + + Jmin - Junk packet minimum size + Jmin - Junk packet အသေးငယ်ဆုံးလက်ခံနိုင်မှုအရွယ်အစား + + + + Jmax - Junk packet maximum size + Jmax - Junk packet အကြီးဆုံးလက်ခံနိုင်မှုအရွယ်အစား + + + + S1 - Init packet junk size + S1 - Init packet junk အရွယ်အစား + + + + S2 - Response packet junk size + S2 - Response packet junk အရွယ်အစား + + + + H1 - Init packet magic header + H1 - Init packet magic header + + + + H2 - Response packet magic header + H2 - Response packet magic header + + + + H4 - Transport packet magic header + H4 - Transport packet magic header + + + + H3 - Underload packet magic header + H3 - Underload packet magic header + + + + The values of the H1-H4 fields must be unique + H1-H4 အကွက်များ၏ တန်ဖိုးများသည် အခြားတန်ဖိုးများနှင့်မတူ တမူထူးခြားနေရပါမည် + + + + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + အကွက် S1 + မက်ဆေ့ချ် စတင်ခြင်း အရွယ်အစား (148) ၏ တန်ဖိုးသည် S2 + မက်ဆေ့ချ် တုံ့ပြန်မှု အရွယ်အစား (92) နှင့် မညီမျှရပါ - + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? + + + Continue ဆက်လက်လုပ်ဆောင်မည် - - + Cancel ပယ်ဖျက်မည် + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + PageProtocolCloakSettings - + Cloak settings ဖုံးကွယ်အသွင်ယူမှု ဆက်တင်များ - + Disguised as traffic from traffic အဖြစ် အသွင်ယူထားသည် @@ -406,16 +668,21 @@ Already installed containers were found on the server. All installed containers Port - - + + Cipher စာဝှက် - + Save သိမ်းဆည်းမည် + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + PageProtocolOpenVpnSettings @@ -435,7 +702,7 @@ Already installed containers were found on the server. All installed containers ကွန်ယက် ပရိုတိုကော - + Port Port @@ -557,58 +824,38 @@ Already installed containers were found on the server. All installed containers none - + TLS auth TLS auth - + Block DNS requests outside of VPN VPN ပြင်ပရှိ DNS တောင်းဆိုမှုများကို ပိတ်ပင်မည်။ - + Additional client configuration commands ထပ်တိုး client ဖွဲ့စည်းမှုဆိုင်ရာ ညွှန်ကြားချက်များ - - + + Commands: အမိန့်ပေးခိုင်းစေချက်များ: - + Additional server configuration commands ထပ်တိုး ဆာဗာ ဖွဲ့စည်းမှုဆိုင်ရာ ညွှန်ကြားချက်များ - - Remove OpenVPN - AmneziaWG ကို ဖယ်ရှားမည်။ + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - - Remove OpenVPN from server? - AmneziaWG ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - - - - All users with whom you shared a connection with will no longer be able to connect to it. - သင့်တွင် သင့်ကိုမည်သည့် ချိတ်ဆက်ထားသော အသုံးပြုသူများသည် အကြောင်းအရာသို့ ဆက်သွယ်ရန် မရနိုင်ပါ။ - - - - Continue - ဆက်လက်လုပ်ဆောင်မည် - - - - Cancel - ပယ်ဖျက်မည် - - - + Save သိမ်းဆည်းမည် @@ -616,46 +863,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings ဆက်တင်များ - + Show connection options ချိတ်ဆက်မှုရွေးချယ်စရာများကို ပြပါ။ - + Connection options %1 ချိတ်ဆက်မှုရွေးချယ်စရာများ %1 - + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + All users with whom you shared a connection with will no longer be able to connect to it. - သင့်တွင် သင့်ကိုမည်သည့် ချိတ်ဆက်ထားသော အသုံးပြုသူများသည် အကြောင်းအရာသို့ ဆက်သွယ်ရန် မရနိုင်ပါ။ + သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - All users who you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. - - - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် @@ -663,297 +906,415 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Shadowsocks ဆက်တင်များ - + Port Port - - + + Cipher စာဝှက် - + Save သိမ်းဆည်းမည် + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + + + PageProtocolWireGuardClientSettings + + + WG settings + WG ဆက်တင်များ + + + + MTU + MTU + + + + Server settings + + + + + Port + Port + + + + Save + သိမ်းဆည်းမည် + + + + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? + + + + Only the settings for this device will be changed + + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + PageProtocolWireGuardSettings - + WG settings - + WG ဆက်တင်များ - + + VPN address subnet + VPN လိပ်စာ ကွန်ရက်ခွဲ + + + Port - Port + Port - - MTU - + + Save settings? + ဆက်တင်များကို သိမ်းဆည်းမည်လား? - - Remove WG - + + All users with whom you shared a connection with will no longer be able to connect to it. + သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - - Remove WG from server? - - - - - All users with whom you shared a connection will no longer be able to connect to it. - သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ဤချိတ်ဆက်မှုကိုချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - - - + Continue - + ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + MTU + MTU + + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + + Save သိမ်းဆည်းမည် - PageServerContainers + PageProtocolXraySettings - Continue - Продолжить + + XRay settings + XRay ဆက်တင်များ + + + + Disguised as traffic from + traffic အဖြစ် အသွင်ယူထားသည် + + + + Save + သိမ်းဆည်းမည် + + + + Unable change settings while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. DNS ဝန်ဆောင်မှုကို သင့်ဆာဗာတွင် ထည့်သွင်းထားပြီးဖြစ်ပြီး ၎င်းကို VPN မှတစ်ဆင့်သာ အသုံးပြုနိုင်သည်. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. DNS လိပ်စာသည် သင့်ဆာဗာလိပ်စာနှင့် အတူတူပင်ဖြစ်ပါသည်။ ချိတ်ဆက်မှုတက်ဘ်အောက်ရှိ ဆက်တင်များတွင် DNS ကို ပြင်ဆင်ချိန်ညှိနိုင်ပါသည်. - + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် + + + Cannot remove AmneziaDNS from running server + AmneziaDNS ကို လည်ပတ်နေသည့်ဆာဗာမှ ဖယ်ရှား၍မရပါ + PageServiceSftpSettings - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ - + SFTP settings SFTP ဆက်တင်များ - + Host Host - - - - + + + + Copied ကူးယူပြီးပါပြီ - + Port Port - + User name အသုံးပြုသူနာမည် - + Password စကားဝှက် - + Mount folder on device ဖိုင်တွဲကို စက်တွင် တပ်ဆင်မည်။ - + In order to mount remote SFTP folder as local drive, perform following steps: <br> အဝေးမှ SFTP ဖိုင်တွဲကို စက်တွင်း drive အဖြစ် တပ်ဆင်ရန်အတွက် အောက်ပါအဆင့်များကို လုပ်ဆောင်ပါ: <br> - - + + <br>1. Install the latest version of <br>၁။ နောက်ဆုံးထွက်ဗားရှင်းကို ထည့်သွင်းမည် - - + + <br>2. Install the latest version of <br>၂။ နောက်ဆုံးထွက်ဗားရှင်းကို ထည့်သွင်းမည် - + Detailed instructions အသေးစိတ်ညွှန်ကြားချက်များ + + + PageServiceSocksProxySettings - - Remove SFTP and all data stored there - SFTP ဖယ်ရှားပါ + + Settings updated successfully + ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ - - Remove SFTP and all data stored there? - SFTP နှင့် ထိုနေရာတွင် သိမ်းဆည်းထားသည့် ဒေတာအားလုံးကို ဖယ်ရှားမည်လား? + + + SOCKS5 settings + SOCKS5 ဆက်တင်များ - - Continue - ဆက်လက်လုပ်ဆောင်မည် + + Host + Host - - Cancel - ပယ်ဖျက်မည် + + + + + Copied + ကူးယူပြီးပါပြီ + + + + + Port + Port + + + + User name + အသုံးပြုသူနာမည် + + + + + Password + စကားဝှက် + + + + Username + အသုံးပြုသူနာမည် + + + + + Change connection settings + ချက်ဆက်မှုဆက်တင်များကို ပြောင်းလဲမည် + + + + The port must be in the range of 1 to 65535 + Port သည် 1 မှ 65535 အတွင်း ဖြစ်ရမည် + + + + Password cannot be empty + စကားဝှက် သည် ဗလာမဖြစ်ရပါ + + + + Username cannot be empty + အသုံးပြုသူနာမည် သည် ဗလာမဖြစ်ရပါ PageServiceTorWebsiteSettings - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ - + Tor website settings Tor ဝဘ်ဆိုက်ဆက်တင်များ - + Website address ဝဘ်ဆိုဒ်လိပ်စာ - + Copied ကူးယူပြီးပါပြီ - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. ဤ URL ကိုဖွင့်ရန် <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> ကို အသုံးပြုပါ. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. သင်၏ onion ဆိုက်ကို ဖန်တီးပြီးနောက်၊ Tor ကွန်ရက်က ၄င်းကိုအသုံးပြုနိုင်အောင်ပြုလုပ်‌ပေးရန် မိနစ်အနည်းငယ်အချိန်ယူသည်. - + When configuring WordPress set the this onion address as domain. WordPress ကို ချိန်ညှိသည့်အခါ ဤ onion လိပ်စာကို domain အဖြစ် သတ်မှတ်ပါ. - - - Remove website - ဝဘ်ဆိုက်ကိုဖယ်ရှားမည် - - - - The site with all data will be removed from the tor network. - ဒေတာအားလုံးပါသည့် ဆိုက်ကို tor ကွန်ရက်မှ ဖယ်ရှားပါမည်. - - - - Continue - ဆက်လက်လုပ်ဆောင်မည် - - - - Cancel - ပယ်ဖျက်မည် - PageSettings - + Settings ဆက်တင်များ - + Servers ဆာဗာများ - + Connection ချိတ်ဆက်မှု - + Application အပလီကေးရှင်း - + Backup backup ယူမည် - + About AmneziaVPN AmneziaVPN အကြောင်း - + + Dev console + ဒက်ဗယ်လော်ပါ console + + + Close application အပလီကေးရှင်းကို ပိတ်မည် @@ -961,173 +1322,348 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Amnezia ကိုကူညီပံ့ပိုးမည် - + Amnezia is a free and open-source application. You can support the developers if you like it. - Amnezia သည် အခမဲ့ဖြစ်ပြီး open-source application တစ်ခုဖြစ်သည်။ သင်နှစ်သက်ပါက developer များကို ပံ့ပိုးနိုင်ပါသည်။ + Amnezia သည် အခမဲ့ open-source application တစ်ခုဖြစ်သည်။ သင်နှစ်သက်ပါက developer များကို ပံ့ပိုးနိုင်ပါသည်. - + Contacts ဆက်သွယ်ရန်လိပ်စာများ - + Telegram group Telegram ဂရု - + To discuss features feature များကိုဆွေးနွေးရန် - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn - Mail - မေးလ် + မေးလ် - + + support@amnezia.org + + + + For reviews and bug reports သုံးသပ်ချက်များနှင့် ချွတ်ယွင်းချက်အစီရင်ခံစာများအတွက် - + + Copied + ကူးယူပြီးပါပြီ + + + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website ဝဘ်ဆိုက် - - https://amnezia.org - https://amnezia.org + + Visit official website + - + Software version: %1 ဆော့ဖ်ဝဲဗားရှင်း: %1 - + Check for updates အပ်ဒိတ်များရှိမရှိ စစ်ဆေးမည် - + Privacy Policy ကိုယ်ရေးအချက်အလက်မူဝါဒ - PageSettingsApplication + PageSettingsApiLanguageList - - Application - အပလီကေးရှင်း + + Unable change server location while there is an active connection + + + + + PageSettingsApiServerInfo + + + For the region + ဒေသအတွက် - - Allow application screenshots - အပလီကေးရှင်းကို screenshot ရိုက်ရန်ခွင့်ပြုမည် + + Price + စျေးနှုန်း - - Auto start - အလိုအ‌လျှောက်စတင်မည် + + Work period + အလုပ်လုပ်မည့်ကာလ - - Launch the application every time the device is starts - စက်စတင်ချိန်တိုင်း အပလီကေးရှင်းကို စတင်မည် + + Valid until + - - Auto connect - အလိုအ‌လျှောက်ချိတ်ဆက်မည် + + Speed + မြန်နှုန်း - - Connect to VPN on app start - အက်ပ်စတင်ချိန်တွင် VPN သို့ ချိတ်ဆက်မည် + + Support tag + ကူညီပံ့ပိုးမှု tag - - Start minimized - အက်ပ်စတင်သည့်အခါ minimized ထားပြီးစတင်မည် + + Copied + ကူးယူပြီးပါပြီ - - Launch application minimized - အက်ပ်စတင်သည့်အခါ minimized ထားပြီးစတင်မည် + + Reload API config + API config ကို ပြန်လည်စတင်မည် - - Language - ဘာသာစကား + + Reload API config? + API config ကို ပြန်လည်စတင်မည်လား? - - Logging - လော့ဂ်အင် - - - - Enabled - ဖွင့်ထားပါသည် - - - - Disabled - ပိတ်ထားပါသည် - - - - Reset settings and remove all data from the application - ဆက်တင်များနဂိုတိုင်းထားပြီး အပလီကေးရှင်းမှဒေတာအားလုံးဖယ်ရှားမည် - - - - Reset settings and remove all data from the application? - ဆက်တင်များကို ပြန်လည်သတ်မှတ်ပြီး အပလီကေးရှင်းမှ ဒေတာအားလုံးကို ဖယ်ရှားမည်လား? - - - - All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. - ဆက်တင်အားလုံးကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ပါမည်. ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်. - - - + + Continue ဆက်လက်လုပ်ဆောင်မည် - + + Cancel ပယ်ဖျက်မည် + + + Cannot reload API config during active connection + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း API config ကို ပြန်လည်စတင်၍မရပါ + + + + Remove from application + အပလီကေးရှင်းမှဖယ်ရှားမည် + + + + Remove from application? + အပလီကေးရှင်းမှဖယ်ရှားမည်လား? + + + + Cannot remove server during active connection + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ဖယ်ရှား၍မရပါ + + + + PageSettingsAppSplitTunneling + + + Cannot change split tunneling settings during active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် split tunneling ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + + + Only the apps from the list should have access via VPN + စာရင်းတွင်းပါဝင်သောအက်ပ်များသာလျှင် VPN မှတစ်ဆင့် ဝင်ရောက်ခွင့်ရှိလိမ့်မည်ဖြစ်သည် + + + + Apps from the list should not have access via VPN + စာရင်းတွင်းပါဝင်သောအက်ပ်များကို VPN မှတစ်ဆင့် ဝင်ရောက်ခွင့်ရရှိလိမ့်မည်မဟုတ်ပေ + + + + App split tunneling + App split tunneling + + + + Mode + Mode + + + + Remove + ဖယ်ရှားမည် + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + + application name + အပလီကေးရှင်းအမည် + + + + Open executable file + စီမံလုပ်ဆောင်နိုင်မှုဖိုင်ကိုဖွင့်မည် + + + + Executable files (*.*) + စီမံလုပ်ဆောင်နိုင်မှုဖိုင်များ (*.*) + + + + PageSettingsApplication + + + Application + အပလီကေးရှင်း + + + + Allow application screenshots + အပလီကေးရှင်းကို screenshot ရိုက်ရန်ခွင့်ပြုမည် + + + + Enable notifications + နိုတီများဖွင့်မည် + + + + Enable notifications to show the VPN state in the status bar + စတေးတပ်ဘားတွင် VPN အခြေအနေကိုပြသရန် နိုတီများကို ဖွင့်မည် + + + + Auto start + အလိုအ‌လျှောက်စတင်မည် + + + + Launch the application every time the device is starts + စက်စတင်ချိန်တိုင်း အပလီကေးရှင်းကို စတင်မည် + + + + Auto connect + အလိုအ‌လျှောက်ချိတ်ဆက်မည် + + + + Connect to VPN on app start + အက်ပ်စတင်ချိန်တွင် VPN သို့ ချိတ်ဆက်မည် + + + + Start minimized + အက်ပ်စတင်သည့်အခါ minimized ထားပြီးစတင်မည် + + + + Launch application minimized + အက်ပ်ဖွင့်သည့်အခါ minimized ထားပြီးဖွင့်မည် + + + + Language + ဘာသာစကား + + + + Logging + Logging + + + + Enabled + ဖွင့်ထားပါသည် + + + + Disabled + ပိတ်ထားပါသည် + + + + Reset settings and remove all data from the application + ဆက်တင်များနဂိုတိုင်းထားပြီး အပလီကေးရှင်းမှဒေတာအားလုံးဖယ်ရှားမည် + + + + Reset settings and remove all data from the application? + ဆက်တင်များကို ပြန်လည်သတ်မှတ်ပြီး အပလီကေးရှင်းမှ ဒေတာအားလုံးကို ဖယ်ရှားမည်လား? + + + + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. + ဆက်တင်အားလုံးကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ပါမည်။ ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်။. + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Cancel + ပယ်ဖျက်မည် + + + + Cannot reset settings during active connection + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်၍မရပါ + PageSettingsBackup - + Settings restored from backup file ဆက်တင်များကို အရန်ဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ @@ -1137,111 +1673,131 @@ Already installed containers were found on the server. All installed containers သင်၏ဖွဲ့စည်းပုံကို အရန်သိမ်းပါ။ - + You can save your settings to a backup file to restore them the next time you install the application. သင်၏ဆက်တင်များကို အရန်ဖိုင်တွင် သိမ်းဆည်းထားခြင်းဖြင့် အပလီကေးရှင်းကို နောက်တစ်ကြိမ်ထည့်သွင်းသည့်အခါ ၎င်းဆက်တင်များကို ပြန်လည်ရယူနိုင်သည်. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. - မိတ္တူတွင် AmneziaVPN သို့ ထည့်ထားသော ဆာဗာအားလုံးအတွက် သင့်စကားဝှက်များနှင့် လျှို့ဝှက်သော့များ ပါဝင်ပါမည်။ ဤအချက်အလက်ကို လုံခြုံသောနေရာတွင် ထားပါ။ + အရံဖိုင်တွင် AmneziaVPN သို့ ထည့်ထားသော ဆာဗာအားလုံးအတွက် သင့်စကားဝှက်များနှင့် လျှို့ဝှက်သော့များ ပါဝင်ပါမည်။ ဤအချက်အလက်ကို လုံခြုံသောနေရာတွင် ထားပါ။. - + Make a backup - အရန်ဖိုင်တစ်ခု ပြုလုပ်မည် + အရံဖိုင်တစ်ခု ပြုလုပ်မည် - + Save backup file - အရန်ဖိုင်ကို သိမ်းဆည်းမည် + အရံဖိုင်ကို သိမ်းဆည်းမည် - - + + Backup files (*.backup) - ဖိုင်များကိုအရန်သိမ်းဆည်းမည် (*.backup) + အရံဖိုင်များ (*.backup) - + Backup file saved - ဖိုင်များကိုအရန်သိမ်းဆည်းပြီးပါပြီ + အရံဖိုင်ကိုသိမ်းဆည်းပြီးပါပြီ - + Restore from backup - အရန်သိမ်းထားသည့်ဖိုင်မှ ပြန်လည်ရယူမည် + အရံဖိုင်မှ ပြန်လည်ရယူမည် - + Open backup file - အရန်သိမ်းထားသည့်ဖိုင်ကို ဖွင့်မည် + အရံဖိုင်ကို ဖွင့်မည် - + Import settings from a backup file? - ဆက်တင်များကို အရန်ဖိုင်တစ်ခုမှ ပြန်လည်တင်သွင်းမည်လား? + ဆက်တင်များကို အရံဖိုင်တစ်ခုမှ ပြန်လည်တင်သွင်းမည်လား? - + All current settings will be reset လက်ရှိဆက်တင်များအားလုံးကို ပြန်လည်သတ်မှတ်ပါမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် + + + Cannot restore backup settings during active connection + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း အရံဆက်တင်များကို ပြန်လည်ရယူ၍မရပါ + PageSettingsConnection - + Connection ချိတ်ဆက်မှု - + Use AmneziaDNS AmneziaDNS ကို အသုံးပြုမည် - + If AmneziaDNS is installed on the server အကယ်၍ AmneziaDNS ကို ဆာဗာတွင် ထည့်သွင်းထားလျှင် - + DNS servers DNS ဆာဗာများ - + When AmneziaDNS is not used or installed AmneziaDNS ကို အသုံးမပြု သို့မဟုတ် ထည့်သွင်းခြင်းမပြုသည့်အခါ - + Allows you to use the VPN only for certain Apps အချို့သောအက်ပ်များအတွက်သာ VPN ကို အသုံးပြုခွင့်ပေးသည် - + + KillSwitch + KillSwitch + + + + Disables your internet if your encrypted VPN connection drops out for any reason. + အကြောင်းတစ်ခုခုကြောင့် VPN ချိတ်ဆက်မှု ပျက်သွားပါက သင့်အင်တာနက်ကို ချက်ချင်းရပ်ဆိုင်းပေးသည်. + + + + Cannot change killSwitch settings during active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် killSwitch ဆက်တင်များကို ပြောင်းလဲ၍မရပါ + + + Site-based split tunneling ဝက်ဆိုဒ်အခြေပြု split tunneling - + Allows you to select which sites you want to access through the VPN VPN မှတဆင့် သင်ဝင်ရောက်လိုသည့်ဆိုဒ်များကို ရွေးချယ်စေနိုင်သည် - + App-based split tunneling App အခြေပြု split tunneling @@ -1249,62 +1805,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS - မူရင်းဆာဗာသည် စိတ်ကြိုက် DNS ကို အထောက်အပံ့မပေးပါ + မူရင်းဆာဗာသည် စိတ်ကြိုက်ထားနိုင်သည့် DNS ကို အထောက်အပံ့မပေးပါ - + DNS servers DNS ဆာဗာများ - + If AmneziaDNS is not used or installed AmneziaDNS ကို အသုံးမပြု သို့မဟုတ် ထည့်သွင်းခြင်းမပြုသည့်အခါ - + Primary DNS Primary DNS - + Secondary DNS Secondary DNS - + Restore default မူရင်းအတိုင်းပြန်လည်ထားရှိမည် - + Restore default DNS settings? မူရင်း DNS ဆက်တင်များကို ပြန်လည်ရယူလိုပါသလား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Settings have been reset ဆက်တင်များကို ပြန်လည်သတ်မှတ်ပြီးပါပြီ - + Save သိမ်းဆည်းမည် - + Settings saved ဆက်တင်များကို သိမ်းဆည်းပြီးပြီ @@ -1312,73 +1868,107 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + Logging ကို ဖွင့်ထားသည်။ မှတ်တမ်းများကို ၁၄ ရက်အကြာတွင် အလိုအလျောက်ပိတ်ထားမည်ဖြစ်ပြီး မှတ်တမ်းဖိုင်များအားလုံး ပျက်သွားမည်ဖြစ်ကြောင်း သတိပြုပါ။. - + Logging - လော့ဂ်အင် + Logging - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. - ဤလုပ်ဆောင်ချက်ကို ဖွင့်ခြင်းဖြင့် အပလီကေးရှင်း၏ မှတ်တမ်းများကို အလိုအလျောက် သိမ်းဆည်းပေးမည် ဖြစ်ပြီး မူရင်းအတိုင်း၊ မှတ်တမ်းလုပ်ဆောင်ချက်ကို ပိတ်ထားသည်။ အပလီကေးရှင်းချို့ယွင်းချက်ရှိသောအခါ မှတ်တမ်းသိမ်းဆည်းခြင်းကို ဖွင့်ပါ။ + ဤလုပ်ဆောင်ချက်ကို ဖွင့်ခြင်းဖြင့် အပလီကေးရှင်း၏ မှတ်တမ်းများကို အလိုအလျောက် သိမ်းဆည်းပေးမည် ဖြစ်သည်။ ပုံမှန်အတိုင်းဆိုလျှင် Logging လုပ်ဆောင်ချက်ကို ပိတ်ထားမည်ဖြစ်သည်။ အပလီကေးရှင်းချို့ယွင်းချက်ရှိခဲ့ပါသော် မှတ်တမ်းကိုပြန်လည်ကြည့်ရှုနိုင်ရန် မှတ်တမ်းသိမ်းဆည်းမှုကို ဖွင့်ထားလိုက်ပါ။. - Save logs - မှတ်တမ်းများကိုသိမ်းဆည်းမည် + မှတ်တမ်းများကိုသိမ်းဆည်းမည် - Open folder with logs - မှတ်တမ်းများဖြင့် ဖိုင်တွဲကိုဖွင့်မည် + မှတ်တမ်းများရှိသောဖိုင်တွဲကိုဖွင့်မည် - + + Save သိမ်းဆည်းမည် - + + Logs files (*.log) မှတ်တမ်းဖိုင်များ (*.log) မှတ်တမ်းဖိုင်များ (*.log) - + + Logs file saved မှတ်တမ်းဖိုင်များသိမ်းဆည်းပြီးပါပြီ - Save logs to file - မှတ်တမ်းများကို ဖိုင်တွင်သိမ်းဆည်းမည် + မှတ်တမ်းများကို ဖိုင်တွင်သိမ်းဆည်းမည် - + + Enable logs + + + + Clear logs? မှတ်တမ်းများရှင်းလင်းမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Logs have been cleaned up မှတ်တမ်းများကို ရှင်းလင်းပြီးပါပြီ - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs မှတ်တမ်းများရှင်းလင်းမည် @@ -1386,27 +1976,12 @@ Already installed containers were found on the server. All installed containers PageSettingsServerData - + All installed containers have been added to the application ထည့်သွင်းထားသည့် ကွန်တိန်နာအားလုံးကို အပလီကေးရှင်းသို့ ပေါင်းထည့်လိုက်ပြီ - - Clear Amnezia cache - Amnezia ကက်ရှ်ဖိုင်များကို ရှင်းလင်းမည် - - - - May be needed when changing other settings - အခြားဆက်တင်များကို ပြောင်းလဲသည့်အခါ လိုအပ်နိုင်သည် - - - - Clear cached profiles? - ကက်ရှ်ပရိုဖိုင်များကို ရှင်းမည်လား? - - - + No new installed containers found အသစ်ထည့်သွင်းထားသော ကွန်တိန်နာများ မတွေ့ရှိပါ @@ -1416,85 +1991,103 @@ Already installed containers were found on the server. All installed containers - - - - - + + + + Continue ဆက်လက်လုပ်ဆောင်မည် - - - - - + + + + Cancel ပယ်ဖျက်မည် - + Check the server for previously installed Amnezia services ယခင်က ထည့်သွင်းထားသော Amnezia ဝန်ဆောင်မှုများရှိမရှိ ဆာဗာကို စစ်ဆေးမည် - + Add them to the application if they were not displayed ဖော်ဆောင်ပြသခြင်းမရှိပါက ၎င်းတို့ကို အပလီကေးရှင်းထဲသို့ ထည့်မည် - + Reboot server ဆာဗာကို ပြန်လည်စတင်မည် - + Do you want to reboot the server? ဆာဗာကို ပြန်လည်စတင်ချင်ပါသလား? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? ပြန်လည်စတင်သည့် လုပ်ငန်းစဉ်သည် စက္ကန့် 30 ခန့် ကြာနိုင်သည်. ဆက်လက်လုပ်ဆောင်လိုပါသလား? - + + Cannot reboot server during active connection + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ပြန်လည်စတင်၍မရပါ + + + Do you want to remove the server from application? ဆာဗာကို အပလီကေးရှင်းမှဖယ်ရှားချင်ပါသလား? - + + Cannot remove server during active connection + ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ဖယ်ရှား၍မရပါ + + + Do you want to clear server from Amnezia software? ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်းလိုပါသလား? - + All users whom you shared a connection with will no longer be able to connect to it. - သင်ချိတ်ဆက်မှုတစ်ခုနှင့် မျှဝေထားသည့် အသုံးပြုသူအားလုံး ၎င်းကို ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ။ + သင်၏ချိတ်ဆက်မှကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + + Cannot clear server from Amnezia software during active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်း၍မရပါ + + + Reset API config API config ကို ပြန်လည်သတ်မှတ်မည် - + Do you want to reset API config? API config ကို ပြန်လည်သတ်မှတ်ချင်ပါသလား? - + + Cannot reset API config during active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် API config ကို ပြန်လည်သတ်မှတ်၍မရပါ + + + Remove server from application ဆာဗာကို အပလီကေးရှင်းမှဖယ်ရှားမည် - + All installed AmneziaVPN services will still remain on the server. ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်. - + Clear server from Amnezia software ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်းမည် @@ -1502,27 +2095,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name ဆာဗာအမည် - + Save သိမ်းဆည်းမည် - + Protocols ပရိုတိုကောများ - + Services ဝန်ဆောင်မှုများ - + Management စီမံခန့်ခွဲမှု @@ -1530,32 +2128,83 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings ဆက်တင်များ - + Clear %1 profile + %1 ပရိုဖိုင်ကို ရှင်းလင်းမည် + + + + Clear %1 profile? + %1 ပရိုဖိုင်ကို ရှင်းလင်းမည်လား? + + + + + + + + + Unable to clear %1 profile while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် %1 ပရိုဖိုင်ကို ရှင်းလင်း၍မရပါ + + + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + All users with whom you shared a connection will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ဤချိတ်ဆက်မှုကိုချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + + Cannot remove active container + Active container ကိုဖယ်ရှား၍မရပါ + + + + Continue ဆက်လက်လုပ်ဆောင်မည် - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Cancel ပယ်ဖျက်မည် @@ -1563,7 +2212,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers ဆာဗာများ @@ -1571,209 +2220,347 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function မူရင်းဆာဗာသည် split tunneling လုပ်ဆောင်ချက်ကို အထောက်အပံ့မပေးပါ - + Addresses from the list should not be accessed via VPN စာရင်းတွင်ဖော်ပြထားသောလိပ်စာများကို VPN ဖြင့် ဝင်ရောက်ခြင်းပြုနိုင်လိမ့်မည် မဟုတ်ပေ - + Split tunneling Split tunneling - + Mode Mode - + Remove ဖယ်ရှားမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Import / Export Sites ဆိုက်များ သွင်း/ထုတ်မည် - + Only the sites listed here will be accessed through the VPN ဤနေရာတွင်ဖော်ပြထားသောဆိုက်များကိုသာ VPN မှတဆင့်ဝင်ရောက်ပါမည် - + Cannot change split tunneling settings during active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် split tunneling ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + website or IP ဝဘ်ဆိုက် သို့မဟုတ် IP - + Import တင်သွင်းမည် - + Save site list ဆိုက်စာရင်းကို သိမ်းဆည်းမည် - + Save sites ဆိုက်များသိမ်းဆည်းမည် - - - + + + Sites files (*.json) ဆိုက်ဖိုင်များ (*.json) - + Import a list of sites ဆိုက်စာရင်းတစ်ခု တင်သွင်းမည် - + Replace site list ဆိုက်စာရင်းကို အစားထိုးမည် - - + + Open sites file ဆိုက်ဖိုင်များ ဖွင့်မည် - + Add imported sites to existing ones တင်သွင်းထားသော ဆိုက်များကို ရှိပြီးသားဆိုက်များထဲသို့ ထည့်မည် + + PageSetupWizardApiServiceInfo + + + For the region + ဒေသအတွက် + + + + Price + စျေးနှုန်း + + + + Work period + အလုပ်လုပ်မည့်ကာလ + + + + Speed + မြန်နှုန်း + + + + Features + Feature များ + + + + Connect + ချိတ်ဆက်မည် + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + Amnezia မှ VPN + + + + Choose a VPN service that suits your needs. + သင့်လိုအပ်ချက်များနှင့် ကိုက်ညီသော VPN ဝန်ဆောင်မှုကို ရွေးချယ်ပါ. + + PageSetupWizardConfigSource - - Server connection - ဆာဗာချိတ်ဆက်မှု - - - Do not use connection code from public sources. It may have been created to intercept your data. - -It's okay as long as it's from someone you trust. - အများသူငှာအသုံးပြုသည့် ရင်းမြစ်များမှ ချိတ်ဆက်ကုဒ်ကို မသုံးပါနှင့်.အဆိုပါကုဒ်များသည် သင့်ဒေတာကို ကြားဖြတ်ရယူရန် ဖန်တီးထားခြင်းဖြစ်နိုင်သည်. - -သင်ယုံကြည်ရတဲ့သူတစ်ယောက်ဆီမှ ရရှိတဲ့ကုဒ်ဖြစ်နေသရွေ့တော့ အဆင်ပြေပါသည်. - - - - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - သင့်ဒေတာကို ကြားဖြတ်ရန် ဖန်တီးထားနိုင်သောကြောင့် မယုံကြည်ရသော ရင်းမြစ်များမှ ချိတ်ဆက်ကုဒ်များကို မသုံးပါနှင့်။ - - - - What do you have? - သင့်တွင်ဘာရှိပါသလဲ? - - - + File with connection settings ချိတ်ဆက်မှုဆက်တင်များပါဝင်သောဖိုင် - - File with connection settings or backup - ချိတ်ဆက်မှုဆက်တင်များ သို့မဟုတ် အရန်သိမ်းဆည်းထားမှုပါဝင်သောဖိုင် + + Connection + ချိတ်ဆက်မှု - + + Settings + ဆက်တင်များ + + + + Enable logs + + + + + Support tag + ကူညီပံ့ပိုးမှု tag + + + + Copied + ကူးယူပြီးပါပြီ + + + + Insert the key, add a configuration file or scan the QR-code + Key ကိုထည့်မည်၊ ဖွဲ့စည်းမှုဖိုင်တစ်ခုကိုထည့်မည် သို့မဟုတ် QR-ကုဒ်ကို စကင်န်ဖတ်မည် + + + + Insert key + Key ကိုထည့်သွင်းမည် + + + + Insert + ထည့်သွင်းမည် + + + + Continue + ဆက်လက်လုပ်ဆောင်မည် + + + + Other connection options + အခြားချိတ်ဆက်မှုရွေးချယ်စရာများ + + + + Site Amnezia + + + + + VPN by Amnezia + Amnezia မှ VPN + + + + Connect to classic paid and free VPN services from Amnezia + Amnezia မှ အခပေးနှင့် အခမဲ့ မူလ VPN ဝန်ဆောင်မှုများသို့ ချိတ်ဆက်မည် + + + + Self-hosted VPN + ကိုယ်တိုင် host လုပ်ထားသော VPN + + + + Configure Amnezia VPN on your own server + Amnezia VPN ကို သင်၏ကိုယ်ပိုင်ဆာဗာပေါ်တွင် စီစဥ်ချိန်ညှိမည် + + + + Restore from backup + အရံဖိုင်မှ ပြန်လည်ရယူမည် + + + + + + + + + Open backup file + အရံဖိုင်ကို ဖွင့်မည် + + + + Backup files (*.backup) + အရံဖိုင်များ (*.backup) + + + + + + + + Open config file config ဖိုင်ကိုဖွင့်မည် - + QR code QR-ကုဒ် - - Key as text - Key ကိုစာသားအဖြစ် + + + + + + + I have nothing + ကျွန်ုပ်တွင်ဘာမှမရှိပါ + + + + + PageSetupWizardCredentials - + Server IP address [:port] ဆာဗာ IP လိပ်စာ [:port] - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Enter the address in the format 255.255.255.255:88 လိပ်စာကို 255.255.255.255:88 ဖော်မတ်ဖြင့် ထည့်ပါ - + Configure your server - သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိပါ။ + သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိမည် - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username SSH အသုံးပြုသူအမည် - + Password or SSH private key စကားဝှက် သိုမဟုတ် SSH private key - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties သင်ထည့်သွင်းသည့်ဒေတာအားလုံးကို တင်းကြပ်လုံခြုံစွာလျှို့ဝှက်ထားမည်ဖြစ်ပြီး Amnezia သို့မဟုတ် မည်သည့်ပြင်ပအဖွဲ့အစည်းကိုမျှ မျှဝေမည် သို့မဟုတ် ထုတ်ဖော်မည်မဟုတ်ပါ - + + How to run your VPN server + သင်၏ဆာဗာကို လည်ပတ်ပုံလည်ပတ်နည်း + + + + Where to get connection data, step-by-step instructions for buying a VPS + ချိတ်ဆက်မှုဒေတာကို ဘယ်မှာရနိုင်မလဲ၊ VPS ဝယ်ယူပုံဝယ်ယူနည်းအတွက် အဆင့်ဆင့် ညွှန်ကြားချက်များ + + + Ip address cannot be empty IP လိပ်စာသည် ဗလာမဖြစ်ရပါ - + Login cannot be empty လော့ဂ်အင်အချက်အလက်သည် ဗလာမဖြစ်ရပါ - + Password/private key cannot be empty စကားဝှက်/private key သည် ဗလာမဖြစ်ရပါ @@ -1781,22 +2568,22 @@ It's okay as long as it's from someone you trust. PageSetupWizardEasy - + What is the level of internet control in your region? သင့်ဒေသရှိ အင်တာနက်ထိန်းချုပ်မှုအဆင့်က ဘယ်လောက်ရှိပါသလဲ? - + Choose a VPN protocol - VPN ပရိုတိုကောကို ရွေးပါ။ + VPN ပရိုတိုကောကို ရွေးပါ - + Skip setup - စနစ်ထည့်သွင်းမှုကို ကျော်သွားပါ။ + စနစ်ထည့်သွင်းမှုကို ကျော်မည် - + Continue ဆက်လက်လုပ်ဆောင်မည် @@ -1804,92 +2591,97 @@ It's okay as long as it's from someone you trust. PageSetupWizardInstalling - + The server has already been added to the application ဆာဗာကို အပလီကေးရှင်းတွင် ထည့်သွင်းပြီးပါပြီ - + Amnezia has detected that your server is currently Amnezia သည် သင့်ဆာဗာက - + busy installing other software. Amnezia installation အခြားဆော့ဖ်ဝဲကို ထည့်သွင်းနေသောကြောင့် အလုပ်ရှုပ်နေကြောင်းထောက်လှန်းမိပါသည်. Amnezia ထည့်သွင်းခြင်းလုပ်ငန်းစဥ် - + will pause until the server finishes installing other software ဆာဗာကို အခြားဆော့ဖ်ဝဲကို ထည့်သွင်းခြင်း မပြီးမချင်း ခေတ္တရပ်ထားပါမည် - + Installing ထည့်သွင်းနေသည် - + Cancel installation ထည့်သွင်းမှုကို ပယ်ဖျက်မည် - - + + Usually it takes no more than 5 minutes - များသောအားဖြင့် 5 မိနစ်ထက်မပိုပါ + များသောအားဖြင့် 5 မိနစ်ထက်ပိုမကြာပါ PageSetupWizardProtocolSettings - + Installing %1 - ထည့်သွင်းနေသည် %1 + %1 ကိုထည့်သွင်းနေသည် - + More detailed ပိုမိုအသေးစိတ် - + Close ပိတ်မည် - + Network protocol ကွန်ရက်ပရိုတိုကော - + Port Port - + Install ထည်သွင်းမည် + + + The port must be in the range of 1 to 65535 + Port သည် 1 မှ 65535 အတွင်း ဖြစ်ရမည် + PageSetupWizardProtocols - + VPN protocol VPN ပရိုတိုကော - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. - သင့်အတွက် ဦးစားပေးအဖြစ်ဆုံးကို ရွေးချယ်ပါ. နောက်ပိုင်းတွင်၊ သင်သည် DNS proxy နှင့် SFTP ကဲ့သို့သော အခြားပရိုတိုကောများနှင့် ထပ်ဆောင်းဝန်ဆောင်မှုများကို ထည့်သွင်းနိုင်သည်. + သင့်အတွက် ဦးစားပေးအဖြစ်ဆုံးကို ရွေးချယ်ပါ။ နောက်ပိုင်းတွင် DNS proxy နှင့် SFTP ကဲ့သို့သော အခြားပရိုတိုကောများနှင့် ထပ်ဆောင်းဝန်ဆောင်မှုများကို ထည့်သွင်းနိုင်သည်။. PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. ကင်မရာနှင့် QR ကုဒ်ကို ချိန်ပြီး စက္ကန့်အနည်းငယ်လောက် ငြိမ်ထားပေးပါ. @@ -1897,34 +2689,9 @@ It's okay as long as it's from someone you trust. PageSetupWizardStart - - Settings restored from backup file - ဆက်တင်များကို အရန်သိမ်းဆည်းထားသောဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ - - - - Free service for creating a personal VPN on your server. - သင့်ဆာဗာပေါ်တွင် ကိုယ်ပိုင် VPN ဖန်တီးရန်အတွက် အခမဲ့ဝန်ဆောင်မှု. - - - - Helps you access blocked content without revealing your privacy, even to VPN providers. - အခြား VPN ဝန်ဆောင်မှုများကိုပင် သင်၏ privacy ကိုမဖော်ပြဘဲ ပိတ်ဆို့ထားသော အကြောင်းအရာများကို သင်ဝင်ရောက်ကြည့်ရှုနိုင်ရန် အကူအညီပေးပါသည်. - - - - I have the data to connect - ကျွန်ုပ်တွင်ချိတ်ဆက်ဖို့အတွက်ဒေတာရှိသည် - - - - I have nothing - ကျွန်ုပ်တွင်ဘာမှမရှိပါ - - - - https://amnezia.org/instructions/0_starter-guide - https://amnezia.org/instructions/0_starter-guide + + Let's get started + စတင်လိုက်ကြရအောင် @@ -1950,7 +2717,7 @@ It's okay as long as it's from someone you trust. ထည်သွင်းမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် @@ -1958,27 +2725,32 @@ It's okay as long as it's from someone you trust. PageSetupWizardViewConfig - + New connection ချိတ်ဆက်မှုအသစ် - + Collapse content အကြောင်းအရာများကိုဖြန့်ချမည် - + Show content အကြောင်းအရာများကိုပြမည် - - Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. - သင်ယုံကြည်ရသော ရင်းမြစ်များမှသာ ချိတ်ဆက်ကုဒ်များကို အသုံးပြုပါ။ သင့်ဒေတာကို ကြားဖြတ်ရန် အများသူငှာ ရင်းမြစ်များမှ ကုဒ်များကို ဖန်တီးထားသည်။ + + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. + WireGuard obfuscation ကိုဖွင့်ထားပါ။ အကယ်၍ သင်၏အင်တာနက်ဝန်ဆောင်မှုပေးသောကုမ္ပဏီက WireGuard ပိတ်ဆို့ထားသော် ၎င်းကိုဖွင့်ထားခြင်းအားဖြင့်အသုံးဝင်နိုင်သည်။. - + + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. + သင်ယုံကြည်ရသော ရင်းမြစ်များမှရရှိသော ချိတ်ဆက်ကုဒ်များကိုသာ အသုံးပြုပါ။ လူတိုင်းဝင်ရောက်ရယူနေနိုင်သော ရင်းမြစ်များမှကုဒ်များသည် သင့်ဒေတာကို ကြားဖြတ်ရယူရန် ဖန်တီးထားသောကုဒ်များဖြစ်နေနိုင်သည်။. + + + Connect ချိတ်ဆက်မည် @@ -1986,182 +2758,212 @@ It's okay as long as it's from someone you trust. PageShare - + OpenVPN native format OpenVPN မူရင်းဖောမတ် - + WireGuard native format WireGuard မူရင်းဖော်မတ် - + Connection ချိတ်ဆက်မှု - - + + Server ဆာဗာ - + Config revoked Config ကိုပြန်ရုပ်သိမ်းလိုက်ပါပြီ - + Connection to ဤဆာဗာသို့ချိတ်ဆက်မှု - + File with connection settings to ဤဆာဗာနှင့်ချိတ်ဆက်မှု ဆက်တင်များပါရှိသော ဖိုင် - + Save OpenVPN config OpenVPN config ကိုသိမ်းဆည်းမည် - + Save WireGuard config WireGuard config ကိုသိမ်းဆည်းမည် - + Save AmneziaWG config AmneziaWG config ကိုသိမ်းဆည်းမည် - + Save Shadowsocks config Shadowsocks config ကိုသိမ်းဆည်းမည် - + Save Cloak config Cloak config ကိုသိမ်းဆည်းမည် - + + Save XRay config + XRay config ကိုသိမ်းဆည်းမည် + + + For the AmneziaVPN app AmneziaVPN အက်ပ်အတွက် - + AmneziaWG native format AmneziaWG မူရင်းဖော်မတ် - + Shadowsocks native format Shadowsocks မူရင်းဖောမတ် - + Cloak native format Cloak မူရင်းဖော်မတ် - + + XRay native format + XRay မူရင်းဖော်မတ် + + + Share VPN Access VPN အသုံးပြုခွင့်ကိုမျှဝေမည် - + Share full access to the server and VPN ဆာဗာနှင့် VPN သို့ အပြည့်အဝဝင်ရောက်ခွင့်ကို မျှဝေမည် - + Use for your own devices, or share with those you trust to manage the server. သင့်ကိုယ်ပိုင်စက်ပစ္စည်းများအတွက် အသုံးပြုရန် သို့မဟုတ် ဆာဗာကို စီမံခန့်ခွဲရန် သင်ယုံကြည်ရသူများနှင့် မျှဝေရန်. - - + + Users အသုံးပြုသူများ - + User name အသုံးပြုသူနာမည် - + Search ရှာဖွေမည် - - Creation date: - ဖန်တီးပြုလုပ်သည့်ရက်စွဲ: + + Creation date: %1 + ဖန်တီးပြုလုပ်သည့်ရက်စွဲ: %1 - + + Latest handshake: %1 + နောက်ဆုံး handshake လုပ်ခြင်း: %1 + + + + Data received: %1 + လက်ခံရရှိသည့်ဒေတာ: %1 + + + + Data sent: %1 + ပေးပို့လိုက်သည့်ဒေတာ: %1 + + + + Allowed IPs: %1 + + + + Rename အမည်ပြောင်းမည် - + Client name ကလိုင်းရင့်အမည် - + Save သိမ်းဆည်းမည် - + Revoke ပြန်ရုပ်သိမ်းမည် - + Revoke the config for a user - %1? အသုံးပြုသူ %1 အတွက် config ကို ပြန်လည်ရုပ်သိမ်းမည်လား? - + The user will no longer be able to connect to your server. ဤအသုံးပြုသူသည် သင့်ဆာဗာသို့ ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Share VPN access without the ability to manage the server ဆာဗာကို စီမံခန့်ခွဲနိုင်စွမ်းမပါရှိဘဲ VPN အသုံးပြုခွင့်ကို မျှဝေမည် - - + + Protocol ပရိုတိုကော - - + + Connection format ချိတ်ဆက်မှုဖောမတ် - - + + Share မျှဝေမည် @@ -2169,50 +2971,55 @@ It's okay as long as it's from someone you trust. PageShareFullAccess - + Full access to the server and VPN ဆာဗာနှင့် VPN ကို အပြည့်အဝဝင်ရောက်ခွင့် - + We recommend that you use full access to the server only for your own additional devices. သင့်ကိုယ်ပိုင်အပိုပစ္စည်းများအတွက်သာ ဆာဗာသို့ အပြည့်အဝဝင်ရောက်ခွင့်ကို အသုံးပြုရန် ကျွန်ုပ်တို့ အကြံပြုပါသည်. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. သင်သည် အခြားသူများနှင့် အပြည့်အဝဝင်ရောက်ခွင့်ကို မျှဝေပါက၊ ၎င်းတို့သည် ပရိုတိုကောများနှင့် ဝန်ဆောင်မှုများကို ဆာဗာသို့ထည့်သွင်းခြင်း ဆာဗာမှဖယ်ရှားခြင်းများ ပြုလုပ်နိုင်သောကြောင့် အသုံးပြုသူများအားလုံးအတွက် VPN မှားယွင်းစွာ လုပ်ဆောင်ခြင်းများဖြစ်စေနိုင်ပါသည်. - - + + Server ဆာဗာ - + Accessing ဝင်ရောက်နေသည် - + File with accessing settings to ဤဆာဗာနှင့်ဝင်ရောက်နိုင်မှု ဆက်တင်များပါရှိသော ဖိုင် - + Share မျှဝေမည် - + + Access error! + အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! + + + Connection to ဤဆာဗာသို့ချိတ်ဆက်မှု - + File with connection settings to ဤဆာဗာနှင့်ချိတ်ဆက်မှု ဆက်တင်များပါရှိသော ဖိုင် @@ -2220,15 +3027,25 @@ It's okay as long as it's from someone you trust. PageStart - + Logging was disabled after 14 days, log files were deleted + ၁၄ ရက်အကြာတွင် Logging ကို ပိတ်ခဲ့သည်၊ မှတ်တမ်းဖိုင်များကို ဖျက်ပစ်လိုက်ပြီဖြစ်သည် + + + + Settings restored from backup file + ဆက်တင်များကို အရံဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. PopupType - + Close ပိတ်မည် @@ -2262,12 +3079,12 @@ It's okay as long as it's from someone you trust. စကားဝှက်ကို ရှာမတွေ့ပါ - + Could not open keystore keystore ကို ဖွင့်၍မရပါ - + Could not remove private key from keystore Key store မှ ကိုယ်ပိုင် key ကို ဖယ်ရှား၍မရပါ @@ -2443,27 +3260,27 @@ It's okay as long as it's from someone you trust. keystore ကို ဖွင့်၍မရပါ - + Could not create private key generator ကိုယ်ပိုင် key ဖန်တီးမှုစက်ကိုမဖန်တီးနိုင်ပါ - + Could not generate new private key ကိုယ်ပိုင် key အသစ် မထုတ်ပေးနိုင်ပါ - + Could not retrieve private key from keystore Key store မှ ကိုယ်ပိုင် key ကို ထုတ်ယူ၍မရပါ - + Could not create encryption cipher ကုတ်ဝှက်ဖြည်ခြင်းဖန်တီး၍မရပါ - + Could not encrypt data ဒေတာကို ကုတ်ဝှက်၍မရပါ @@ -2477,6 +3294,7 @@ It's okay as long as it's from someone you trust. + Unknown error အမည်မသိ မှားယွင်းမှု @@ -2486,254 +3304,257 @@ It's okay as long as it's from someone you trust. လုပ်ဆောင်ချက်ကို မတတ်ဆင်ရသေးပါ - + Server check failed ဆာဗာစစ်ဆေးမှု မအောင်မြင်ပါ - + Server port already used. Check for another software ဆာဗာ Port ကို အသုံးပြုပြီးဖြစ်သည်. အခြားဆော့ဖ်ဝဲရှိမရှိ စစ်ဆေးပါ - + Server error: Docker container missing ဆာဗာ မှားယွင်းမှု: Docker ကွန်တိန်နာ ပျောက်နေသည် - + Server error: Docker failed ဆာဗာ မှားယွင်းမှု: Docker မအောင်မြင်ပါ - + Installation canceled by user ထည့်သွင်းမှုကို အသုံးပြုသူမှ ပယ်ဖျက်လိုက်သည် - + The user does not have permission to use sudo ဤအသုံးပြုသူသည် sudo ကိုအသုံးပြုရန်ခွင့်ပြုချက်မရှိပါ - + SSH request was denied SSH တောင်းဆိုမှု ငြင်းဆိုခံလိုက်ရပါသည် - + SSH request was interrupted SSH တောင်းဆိုမှု အနှောက်အယက်ခံလိုက်ရပါသည် - + SSH internal error စက်တွင်းဖြစ်သော SSH မှားယွင်းမှု - + Invalid private key or invalid passphrase entered မမှန်ကန်သော ကိုယ်ပိုင် key သို့မဟုတ် မမှန်ကန်သော စကားဝှက်ကို ထည့်သွင်းထားသည် - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types ရွေးချယ်ထားသော ကိုယ်ပိုင် key ဖော်မတ်ကို ထောက်ပံ့မှုမပေးပါ၊ openssh ED25519 key အမျိုးအစားများ သို့မဟုတ် PEM သော့အမျိုးအစားများကို အသုံးပြုပါ - + Timeout connecting to server ဆာဗာသို့ ချိတ်ဆက်ခြင်း အချိန်ကုန်သွားသည် - Sftp error: End-of-file encountered - Sftp မှားယွင်းမှု: ဖိုင်အဆုံးသတ်ကို ကြုံတွေ့ခဲ့ရသည် - - - Sftp error: File does not exist - Sftp မှားယွင်းမှု: ဖိုင်မရှိပါ - - - Sftp error: Permission denied - Sftp မှားယွင်းမှု: ခွင့်ပြုချက် ငြင်းဆိုခံလိုက်ရပါသည် - - - Sftp error: Generic failure - Sftp မှားယွင်းမှု: ယေဘုယ မအောင်မြင်ခြင်း - - - Sftp error: Garbage received from server - မှားယွင်းမှု: ဆာဗာမှ အမှိုက်များကို လက်ခံရရှိခဲ့သည် - - - Sftp error: No connection has been set up - Sftp မှားယွင်းမှု: ချိတ်ဆက်မှု မသတ်မှတ်ရသေးပါ - - - Sftp error: There was a connection, but we lost it - Sftp မှားယွင်းမှု: ချိတ်ဆက်မှုတစ်ခုရှိခဲ့သော်လည်း ဆုံးရှုံးသွားခဲ့ပါသည် - - - Sftp error: Operation not supported by libssh yet - Sftp အမှား: လုပ်ဆောင်ချက်ကို libssh မှ မထောက်ပံ့သေးပါ - - - Sftp error: Invalid file handle - Sftp မှားယွင်းမှု: ဖိုင်ကိုင်တွယ်မှု မမှန်ကန်ပါ - - - Sftp error: No such file or directory path exists - Sftp မှားယွင်းမှု: ဤဖိုင်အမျိုးအစား သို့မဟုတ် လမ်းညွှန်လမ်းကြောင်းမျိုး မရှိပါ - - - Sftp error: An attempt to create an already existing file or directory has been made - Sftp မှားယွင်းမှု: ရှိပြီးသား ဖိုင် သို့မဟုတ် လမ်းညွှန်ကို ဖန်တီးရန် ကြိုးပမ်းမှုတစ်ခု ပြုလုပ်ပြီးဖြစ်သည် - - - Sftp error: Write-protected filesystem - Sftp မှားယွင်းမှု: ရေးသားခြင်းမှကာကွယ်ထားသော ဖိုင်စနစ် - - - Sftp error: No media was in remote drive - Sftp မှားယွင်းမှု: မီဒီယာသည် အဝေးမှ drive ထဲတွင် မရှိခဲ့ပါ - - - + The config does not contain any containers and credentials for connecting to the server Config တွင် ဆာဗာသို့ချိတ်ဆက်ရန်အတွက် ကွန်တိန်နာများနှင့် အထောက်အထားများ မပါဝင်ပါ - + Error when retrieving configuration from API API မှ စီစဉ်သတ်မှတ်မှုကို ရယူသည့်အခါ အမှားအယွင်းဖြစ်ပေါ်နေသည် - + This config has already been added to the application ဤ config ကို အပလီကေးရှင်းထဲသို့ ထည့်သွင်းပြီးဖြစ်သည် - + ErrorCode: %1. မှားယွင်းမှုကုတ်: %1. - + OpenVPN config missing OpenVPN config ပျောက်ဆုံးနေပါသည် - - SCP error: Generic failure - + + Background service is not running + နောက်ခံဝန်ဆောင်မှု လည်ပတ်နေခြင်းမရှိပါ - + + Server error: Packet manager error + ဆာဗာ မှားယွင်းမှု: Packet Manager မှားယွင်းမှု + + + + SCP error: Generic failure + SCP မှားယွင်းမှု: ယေဘုယ မအောင်မြင်ခြင်း + + + OpenVPN management server error OpenVPN စီမံခန့်ခွဲမှုဆာဗာ အမှားအယွင်း - + OpenVPN executable missing OpenVPN စီမံလုပ်ဆောင်နိုင်မှု ပျောက်ဆုံးနေပါသည် - + Shadowsocks (ss-local) executable missing Shadowsocks (ss-local) executable ပျောက်နေပါသည် - + Cloak (ck-client) executable missing Cloak (ck-client) စီမံလုပ်ဆောင်နိုင်မှု ပျောက်ဆုံးနေပါသည် - + Amnezia helper service error Amnezia helper ဝန်ဆောင်မှု မှားယွင်းမှု - + OpenSSL failed OpenSSL မအောင်မြင်ပါ - + Can't connect: another VPN connection is active ချိတ်ဆက်၍မရပါ: အခြား VPN ချိတ်ဆက်မှုတစ်ခုရှိနေပါသည် - + Can't setup OpenVPN TAP network adapter OpenVPN TAP ကွန်ရက် adapter ကို စနစ်တည်ဆောက်၍မရပါ - + VPN pool error: no available addresses VPN pool မှားယွင်းမှု: ရရှိနိုင်သောလိပ်စာများမရှိပါ + Unable to open config file + + + + VPN connection error VPN ချိတ်ဆက်မှုမှားယွင်းနေပါသည် - - - QFile error: The file could not be opened - - - QFile error: An error occurred when reading from the file - + In the response from the server, an empty config was received + ဆာဗာမှ တုံ့ပြန်မှုတွင်၊ config အလွတ်တစ်ခုကို လက်ခံရရှိခဲ့သည် - QFile error: The file could not be accessed - + SSL error occurred + SSL မှားယွင်းမှုဖြစ်သွားသည် - QFile error: An unspecified error occurred - + Server response timeout on api request + Api တောင်းဆိုမှုတွင် ဆာဗာတုံ့ပြန်မှု အချိန်ကုန်သွားသည် - QFile error: A fatal error occurred - + Missing AGW public key + AGW public key ပျောက်ဆုံးနေသည် - QFile error: The operation was aborted + Failed to decrypt response payload + + + + + Missing list of available services + QFile error: The file could not be opened + QFile မှားယွင်းမှု: ဖိုင်ကို ဖွင့်၍မရပါ + + + + QFile error: An error occurred when reading from the file + QFile မှားယွင်းမှု: ဖိုင်ကိုဖတ်နေစဥ်အတွင်း မှားယွင်းမှုဖြစ်သွားသည် + + + + QFile error: The file could not be accessed + QFile မှားယွင်းမှု: ဖိုင်ကို ဝင်၍မရပါ + + + + QFile error: An unspecified error occurred + QFile မှားယွင်းမှု: သတ်မှတ်မထားသော မှားယွင်းမှုတစ်ခု ဖြစ်ပွားခဲ့သည် + + + + QFile error: A fatal error occurred + QFile မှားယွင်းမှု: ကြီးမားသော မှားယွင်းမှုတစ်ခု ဖြစ်ပွားခဲ့သည် + + + + QFile error: The operation was aborted + QFile မှားယွင်းမှု: လုပ်ငန်းစဥ်ကို ဖျက်သိမ်းလိုက်ရသည် + + + Internal error စက်တွင်းဖြစ်သော မှားယွင်းမှု - + IPsec IPsec - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. Shadowsocks - ၎င်းသည် ပုံမှန်ဝဘ်လမ်းကြောင်းနှင့် ဆင်တူစေရန် VPN အသွားအလာကို ဖုံးကွယ်ထားသော်လည်း ၎င်းကို အချို့သော ဆင်ဆာဖြတ်ထားသော ဒေသများရှိ ခွဲခြမ်းစိတ်ဖြာမှုစနစ်များက ထောက်လှန်းသိရှိနိုင်ပါသည်. - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. OpenVPN over Cloak - ဝဘ်အသွားအလာအဖြစ် ဟန်ဆောင်ထားသည့် VPN ပါသော OpenVPN နှင့် active-probing ထောက်လှမ်းခြင်းမှ ကာကွယ်ပေးခြင်း. ဆင်ဆာဖြတ်တောက်မှု အမြင့်ဆုံးအဆင့်ရှိသော ဒေသများတွင် ပိတ်ဆို့ခြင်းများကို ကျော်ဖြတ်ရန်အတွက် အကောင်းဆုံးဖြစ်သည်. - + + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. + REALITY ပါဝင်သော XRay - အင်တာနက်ဆင်ဆာဖြတ်တောက်မှုအပြင်းထန်ဆုံးနိုင်ငံများအတွက် သင့်လျော်သည်။ Web traffic အဖြစ် အသွားအလာကို TLS အဆင့်ဖြင့် ဖုံးကွယ်ပေးထားခြင်း၊ Active probing နည်းလမ်းများဖြင့် ထောက်လှမ်းခံရခြင်းမှ ကာကွယ်ပေးခြင်းများ။. + + + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + IKEv2/IPsec - ခေတ်မီပြီးတည်ငြိမ်သော ပရိုတိုကော၊ အခြားပရိုတိုကောများထက် အနည်းငယ်ပိုမြန်သည်၊ Signal ဆုံးရှုံးပြီးနောက် ချိတ်ဆက်မှုကို ပြန်လည်ရယူနိုင်သည်။ Android နှင့် iOS ၏ နောက်ဆုံးဗားရှင်းများတွင် native ပံ့ပိုးမှုရရှိသည်။. + + + Create a file vault on your server to securely store and transfer files. ဖိုင်များကို လုံခြုံစွာသိမ်းဆည်းရန်နှင့် လွှဲပြောင်းရန်အတွက် သင့်ဆာဗာပေါ်တွင် fire vault တစ်ခု ဖန်တီးပါ. - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -2772,7 +3593,7 @@ Cloak သည် ပက်ကတ်မက်တာဒေတာကို မွမ - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -2793,7 +3614,18 @@ WireGuard သည် ၎င်း၏ ကွဲပြားသော packet လက * UDP ကွန်ရက်ပရိုတိုကောပေါ်တွင် အလုပ်လုပ်သည်။. - + + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. + + + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -2814,58 +3646,53 @@ IKEv2 သည် လုံခြုံရေး၊ တည်ငြိမ်မှ * UDP ကွန်ရက်ပရိုတိုကော၊ port 500 နှင့် 4500 ကျော်တွင် အလုပ်လုပ်သည်။. - + DNS Service DNS ဝန်ဆောင်မှု - + SFTP file sharing service SFTP ဖိုင်မျှဝေခြင်းဆားဗစ် - - + + Website in Tor network Tor ကွန်ရက်ထဲရှိ ဝဘ်ဆိုဒ် - + AmneziaDNS AmneziaDNS - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. OpenVPN သည် ပြောင်းလွယ်ပြင်လွယ် ဖွဲ့စည်းမှုရွေးချယ်စရာများပါရှိသော လူကြိုက်အများဆုံး VPN ပရိုတိုကောဖြစ်သည်. ၎င်းသည် key လဲလှယ်မှုအတွက် SSL/TLS ဖြင့် ၎င်း၏ကိုယ်ပိုင်လုံခြုံရေးပရိုတိုကောကို အသုံးပြုသည်. - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. WireGuard - မြင့်မားသောစွမ်းဆောင်ရည်၊ မြန်နှုန်းမြင့်နှင့် ပါဝါသုံးစွဲမှုနည်းသော လူကြိုက်များသော VPN ပရိုတိုကောအသစ်. ဆင်ဆာဖြတ်မှုအဆင့်နိမ့်သော ဒေသများတွင်အသုံးပြုရန်အကြံပြုထားသည်. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. AmneziaWG - WireGuard ကိုအခြေခံထားသော Amnezia မှ အထူးပရိုတိုကော. ၎င်းသည် WireGuard ကဲ့သို့မြန်ဆန်သော်ပြီး ပိတ်ဆို့ခြင်းများကိုလည်း ခံနိုင်ရည်ရှိပါသည်. ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသော ဒေသများတွင်အသုံးပြုရန် အကြံပြုပါသည်. - - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec - ခေတ်မီတည်ငြိမ်သောပရိုတိုကော၊ အခြားအရာများထက်အနည်းငယ်ပိုမြန်သည်၊ signal ပျောက်ဆုံးပြီးနောက် ချိတ်ဆက်မှုကို ပြန်လည်ရယူပေးသည်. - - - + Deploy a WordPress site on the Tor network in two clicks. ကလစ်နှစ်ချက်နှိပ်ရုံဖြင့် Tor ကွန်ရက်ပေါ်တွင် WordPress ဆိုက်တစ်ခုကို ဖြန့်ကျက်လိုက်ပါ. - + Replace the current DNS server with your own. This will increase your privacy level. လက်ရှိ DNS ဆာဗာကို သင့်ကိုယ်ပိုင် DNS ဆာဗာဖြင့် အစားထိုးပါ. ဤသို့ပြုလုပ်ခြင်းသည် သင်၏ကိုယ်ရေးကိုယ်တာလုံခြုံမှုအဆင့်ကို တိုးမြှင့်ပေးလိမ့်မည်. - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -2884,7 +3711,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * TCP နှင့် UDP ကွန်ရက် ပရိုတိုကော နှစ်ခုလုံးတွင် လည်ပတ်နိုင်သည်။. - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -2899,7 +3726,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * TCP ကွန်ရက် ပရိုတိုကောပေါ်တွင် အလုပ်လုပ်သည်။. - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -2920,7 +3747,7 @@ WireGuard သည် ၎င်း၏ စွမ်းဆောင်ရည်အ * UDP ကွန်ရက်ပရိုတိုကောပေါ်တွင် အလုပ်လုပ်သည်။. - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -2939,10 +3766,17 @@ For more detailed information, you can "create SFTP file storage." အောက်ရှိ ပံ့ပိုးမှုကဏ္ဍတွင် ၎င်းကိုရှာပါ။ - + SFTP service SFTP ဝန်ဆောင်မှု + + + + + SOCKS5 proxy server + SOCKS5 proxy ဆာဗာ + Entry not found @@ -2993,35 +3827,169 @@ For more detailed information, you can No match ကိုက်ညီမှုမရှိပါ - - - Unknown error - အမည်မသိ မှားယွင်းမှု - error 0x%1: %2 မှားယွင်းမှု 0x%1: %2 + + + vmess:// url is invalid + vmess:// url သည် မမှန်ကန်ပါ + + + + Invalid streamSettings protocol: + မမှန်ကန်သော streamSettings ပရိုတိုကော: + + + + Unknown transport method: + အမည်မသိ သယ်ယူပို့ဆောင်ရေးနည်းလမ်း: + + + + VMess string should start with 'vmess://' + VMess စာကြောင်းသည် 'vmess://' ဖြင့် စတင်သည် + + + + VMess string should be a valid base64 string + VMess စာကြောင်း သည် မှန်ကန်သော base64 စာကြောင်း ဖြစ်ရမည် + + + + JSON should not be empty + JSON သည် ဗလာမဖြစ်ရပါ + + + + VLESS link should start with vless:// + VLESS စာကြောင်းသည် 'vless://' ဖြင့် စတင်သည် + + + + link parse failed: %1 + လင့်ခ်ခွဲခြမ်းစိတ်ဖြာမှု မအောင်မြင်ပါ: %1 + + + + empty host + Host ဗလာဖြစ်နေသည် + + + + missing port + Port ပျောက်ဆုံးနေသည် + + + + missing uuid + uuid ပျောက်ဆုံးနေသည် + + + + Invalid ssd link: json: field %1 must exist + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 ရှိရပါမည် + + + + Invalid ssd link: json: field %1 must be valid port number + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 သည် မှန်ကန်သော port နံပါတ် ဖြစ်ရပါမည် + + + + Invalid ssd link: json: field %1 must be of type 'string' + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 သည် 'စာကြောင်း' အမျိုးအစား ဖြစ်ရမည် + + + + Invalid ssd link: json: field %1 must be an array + မမှန်ကန်သော ssd လင့်ခ်: json: အကွက် %1 သည် array တစ်ခု ဖြစ်ရမည် + + + + Skipping invalid ssd server: server must be an object + မမှန်ကန်သော ssd ဆာဗာကို ကျော်သွားသည်: ဆာဗာသည် object တစ်ခု ဖြစ်ရပါမည် + + + + Skipping invalid ssd server: missing required field %1 + မမှန်ကန်သော ssd ဆာဗာကို ကျော်သွားသည်: လိုအပ်သောအကွက် %1 ပျောက်ဆုံးနေပါသည် + + + + Skipping invalid ssd server: field %1 should be of type 'string' + မမှန်ကန်သော ssd ဆာဗာကို ကျော်သွားသည်: အကွက် %1 သည် 'စာကြောင်း' အမျိုးအစား ဖြစ်ရမည် + + + + Invalid ssd link: should begin with ssd:// + မမှန်ကန်သော ssd လင့်ခ်: ssd:// ဖြင့် စတင်ရမည် + + + + Invalid ssd link: base64 parse failed + မမှန်ကန်သော ssd လင့်ခ်: base64 ခွဲခြမ်းစိတ်ဖြာမှု မအောင်မြင်ပါ + + + + Invalid ssd link: json parse failed + မမှန်ကန်သော ssd လင့်ခ်: json ခွဲခြမ်းစိတ်ဖြာမှု မအောင်မြင်ပါ + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + မမှန်ကန်သော ssd လင့်ခ်: rc4-md5 ကုဒ်ဝှက်ခြင်းကို v2ray-core က မပံ့ပိုးပေးပါ + + + + SS URI is too short + SS URI တိုလွန်းသည် + + + + + Can't find the colon separator between method and password + Method နှင့် စကားဝှက်ကြားရှိ colon seperator ကို ရှာမတွေ့ပါ + + + + Can't find the at separator between password and hostname + စကားဝှက်နှင့် hostname ကြား at seperator ကို ရှာမတွေ့ပါ + + + + Can't find the colon separator between hostname and port + Hostname နှင့် port ကြားရှိ colon separator ကို ရှာမတွေ့ပါ + SelectLanguageDrawer - + Choose language ဘာသာစကားကို ရွေးချယ်ပါ + + ServersListView + + + Unable change server while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ + + Settings - + Server #1 ဆာဗာ #1 - - + + Server ဆာဗာ @@ -3029,57 +3997,52 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values ဆက်တင်အားလုံးကို မူရင်းတန်ဖိုးများအဖြစ် ပြန်လည်သတ်မှတ်ထားသည် - - Cached profiles cleared - ကက်ရှ်ပရိုဖိုင်များကို ရှင်းလင်းပြီးပါပြီ - - - + Backup file is corrupted - အရန်သိမ်းထားသည့်ဖိုင်ပျက်ဆီးနေသည် + အရံဖိုင်ပျက်ဆီးနေသည် ShareConnectionDrawer - - + + Save AmneziaVPN config AmneziaWG config ကိုသိမ်းဆည်းမည် - + Share မျှဝေမည် - + Copy ကူးယူမည် - - + + Copied ကူးယူပြီးပါပြီ - + Copy config string config string ကိုကူးယူမည် - + Show connection settings ချိတ်ဆက်မှုဆက်တင်များကို ပြပါ - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Amnezia အက်ပ်ရှိ QR ကုဒ်ကိုဖတ်ရန်အတွက်အောက်ပါအတိုင်း ရွေးချယ်ပါ "ဆာဗာထည့်ရန်" → "ချိတ်ဆက်ရန် ဒေတာရှိသည်" → "QR ကုဒ်၊ key သို့မဟုတ် ဆက်တင်ဖိုင်" @@ -3102,27 +4065,27 @@ For more detailed information, you can ဆိုက်ကို ဖယ်ရှားလိုက်သည်: %1 - + Can't open file: %1 ဖိုင်ကိုဖွင့်၍မရပါ: %1 - + Failed to parse JSON data from file: %1 JSON ဒေတာကို ဖိုင်မှ ခွဲခြမ်းထုပ်ယူမှု မအောင်မြင်ပါ: %1 - + The JSON data is not an array in file: %1 JSON ဒေတာသည် ဖိုင်ထဲရှိ array တစ်ခုမဟုတ်ပါ: %1 - + Import completed တင်သွင်းခြင်းပြီးဆုံးသွားပါပြီ - + Export completed ထုတ်ယူခြင်းပြီးဆုံးသွားပါပြီ @@ -3163,7 +4126,7 @@ For more detailed information, you can TextFieldWithHeaderType - + The field can't be empty ဖြည့်သွင်းရမည့်နေရာသည် အလွတ်မဖြစ်ရပါ @@ -3171,7 +4134,7 @@ For more detailed information, you can VpnConnection - + Mbps Mbps @@ -3179,42 +4142,42 @@ For more detailed information, you can VpnProtocol - + Unknown အမည်မသိ - + Disconnected ဖြုတ်လိုက်ပါပြီ - + Preparing ပြင်ဆင်နေပါသည် - + Connecting... ချိတ်ဆက်နေပါပြီ... - + Connected ချိတ်ဆက်ပြီးသွားပါပြီ - + Disconnecting... အဆက်အသွယ်ဖြတ်နေပါသည်... - + Reconnecting... ပြန်လည်ချိတ်ဆက်နေပါသည်... - + Error မှားယွင်းမှု @@ -3222,45 +4185,35 @@ For more detailed information, you can amnezia::ContainerProps - + Low Low - + High - Medium သို့မဟုတ် High + High - - Extreme - Extreme - - - + I just want to increase the level of my privacy. ကျွန်ုပ်၏ကိုယ်ရေးကိုယ်တာလုံခြုံမှုအဆင့်ကို မြှင့်တင်လိုပါသည်. - + I want to bypass censorship. This option recommended in most cases. ဆင်ဆာဖြတ်တောက်ခြင်းကို ကျော်ဖြတ်ချင်ပါသည်. ဤရွေးချယ်မှုကို ကိစ္စအများစုအတွက် အကြံပြုထားသည်. - - - Most VPN protocols are blocked. Recommended if other options are not working. - VPN ပရိုတိုကောအများစုကို ပိတ်ဆို့ထားသည်. အခြားရွေးချယ်စရာများ အလုပ်မလုပ်ပါက အသုံးပြုရန်အကြံပြုထားသည်. - main2 - + Private key passphrase ကိုယ်ပိုင် key စကားဝှက် - + Save သိမ်းဆည်းမည် diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 905ed3e0..ddf6a212 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1,50 +1,223 @@ + + AdLabel + + + Amnezia Premium - for access to any website + Amnezia Premium - для доступа к любым сайтам + + + + ApiAccountInfoModel + + + + Active + Активна + + + + Inactive + Не активна + + + + %1 out of %2 + %1 из %2 + + + + Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to 200 Mbps + Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с + + + + 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. + Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включен в бесплатный тариф. + + + + amnezia_free_support_bot + + + + + amnezia_premium_support_bot + + + + + ApiConfigsController + + + %1 installed successfully. + %1 успешно установлен. + + + + API config reloaded + Конфигурация API перезагружена + + + + Successfully changed the country of connection to %1 + Страна подключения изменена на %1 + + + + ApiServicesModel + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с + + + VPN to access blocked sites in regions with high levels of Internet censorship. + VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + <p><a style="color: #EB5757;">Недоступно в вашем регионе. Если у вас включен VPN, отключите его, вернитесь на предыдущий экран и попробуйте снова.</a> + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + Amnezia Premium — классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры. + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры + + + + Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. Works for any sites with no restrictions. Speed up to %1 MBit/s. Unlimited traffic. + Amnezia Premium — VPN для комфортной работы, скачивания больших файлов и просмотра видео в высоком разрешении. Скорость до %1 Мбит/с. Безлимитный трафик. + + + + + 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. + AmneziaFree предоставляет бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включен в бесплатный тариф. + + + + Amnezia Premium is VPN for comfortable work, downloading large files and watching videos in 8K resolution. Works for any sites with no restrictions. + Amnezia Premium — VPN для комфортной работы, скачивания больших файлов и просмотра видео в высоком разрешении. Работает для любых сайтов без ограничений. + + + + %1 MBit/s + + + + + %1 days + %1 дней + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + Через VPN будут открываться только популярные сайты, заблокированные в вашем регионе, такие как Instagram, Facebook, Twitter и другие. Остальные сайты будут открываться с вашего реального IP-адреса, <a href="%1/free" style="color: #FBB26A;">подробности на сайте.</a> + + + + Free + Бесплатно + + + + %1 $/month + %1 $/месяц + + + + AppSplitTunnelingController + + + Application added: %1 + Приложение добавлено: %1 + + + + The application has already been added + Приложение уже было добавлено + + + + The selected applications have been added + Выбранные приложения добавлены + + + + Application removed: %1 + Приложение удалено: %1 + + + + ConnectButton + + + Unable to disconnect during configuration preparation + Невозможно отключиться во время подготовки конфигурации + + ConnectionController - VPN Protocols is not installed. Please install VPN container at first - VPN-протоколы не установлены. + VPN-протоколы не установлены. Пожалуйста, установите протокол - + Connecting... Подключение... - + Connected Подключено - + + Preparing... + Подготовка... + + + Settings updated successfully, reconnnection... Настройки успешно обновлены, переподключение... - + Settings updated successfully Настройки успешно обновлены - + The selected protocol is not supported on the current platform + Выбранный протокол не поддерживается на данном устройстве + + + unable to create configuration + не удалось создать конфигурацию + + + Reconnecting... Переподключение... - - + - + + Connect Подключиться - + Disconnecting... Отключение... @@ -62,7 +235,7 @@ Настроить свой сервер - + Open config file, key or QR code Открыть файл конфигурации, ключ или QR-код @@ -72,86 +245,86 @@ C&ut - &Вырезать + Вырезать &Copy - &Копировать + Копировать &Paste - &Вставить + Вставить - + &SelectAll - &Выбрать всё + Выбрать всё ExportController - Access error! - Ошибка доступа! + Ошибка доступа! HomeContainersListView - + Unable change protocol while there is an active connection - Невозможно изменить протокол при активном соединении + Невозможно изменить протокол во время активного соединения - The selected protocol is not supported on the current platform - Выбранный протокол не поддерживается на данном устройстве + Выбранный протокол не поддерживается на данном устройстве HomeSplitTunnelingDrawer - + Split tunneling Раздельное VPN-туннелирование - + Allows you to connect to some sites or applications through a VPN connection and bypass others Позволяет подключаться к одним сайтам или приложениям через VPN-соединение, а к другим — в обход него - + Split tunneling on the server Раздельное туннелирование на сервере - + Enabled Can't be disabled for current server Включено Невозможно отключить для текущего сервера - + Site-based split tunneling Раздельное туннелирование сайтов - + + Enabled Включено - + + Disabled Отключено - + App-based split tunneling Раздельное туннелирование приложений @@ -159,85 +332,125 @@ Can't be disabled for current server ImportController - Unable to open file - Невозможно открыть файл + Невозможно открыть файл - - Invalid configuration file - Неверный файл конфигурации + Неверный файл конфигурации - + Scanned %1 of %2. Отсканировано %1 из %2. + + + In the imported configuration, potentially dangerous lines were found: + В импортированной конфигурации были обнаружены потенциально опасные строки: + InstallController - - + %1 installed successfully. %1 успешно установлен. - - + %1 is already installed on the server. %1 уже установлен на сервер. - + Added containers that were already installed on the server Добавлены сервисы и протоколы, которые были ранее установлены на сервер - + Already installed containers were found on the server. All installed containers have been added to the application На сервере обнаружены установленные протоколы и сервисы. Все они были добавлены в приложение - + Settings updated successfully Настройки успешно обновлены - + Server '%1' was rebooted Сервер '%1' был перезагружен - + Server '%1' was removed Сервер '%1' был удален - + All containers from server '%1' have been removed Все протоколы и сервисы были удалены с сервера '%1' - + %1 has been removed from the server '%2' %1 был удален с сервера '%2' - + + Api config removed + Конфигурация API удалена + + + + %1 cached profile cleared + %1 закэшированный профиль очищен + + + Please login as the user Пожалуйста, войдите в систему от имени пользователя - + Server added successfully Сервер успешно добавлен + + %1 installed successfully. + %1 успешно установлен. + + + API config reloaded + Конфигурация API перезагружена + + + Successfully changed the country of connection to %1 + Изменение страны подключения на %1 + + + + InstalledAppsDrawer + + + Choose application + Выберите приложение + + + + application name + название приложения + + + + Add selected + Добавить выбранные + KeyChainClass @@ -260,28 +473,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN AmneziaVPN - + VPN Connected VPN подключен - + VPN Disconnected VPN выключен - + AmneziaVPN notification Уведомление AmneziaVPN - + Unsecured network detected: Обнаружена незащищенная сеть: @@ -289,114 +502,238 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 Удаление сервисов c %1 - + Usually it takes no more than 5 minutes Обычно это занимает не более 5 минут + + PageDevMenu + + + Gateway endpoint + + + + + Dev gateway environment + + + PageHome - + Logging enabled Логирование включено - + Split tunneling enabled Раздельное туннелирование включено - + Split tunneling disabled Раздельное туннелирование выключено - + VPN protocol - VPN протокол + VPN-протокол - + Servers Серверы - Unable change server while there is an active connection - Невозможно изменить сервер при активном соединении + Невозможно изменить сервер во время активного соединения + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + Настройки AmneziaWG + + + + MTU + MTU + + + + Server settings + Настройки сервера + + + + Port + Порт + + + + Save + Сохранить + + + + Save settings? + Сохранить настройки? + + + + Only the settings for this device will be changed + Будут изменены настройки только для этого устройства + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения PageProtocolAwgSettings - + AmneziaWG settings Настройки AmneziaWG - + Port Порт - MTU - MTU + MTU - Remove AmneziaWG - Удалить AmneziaWG + Удалить AmneziaWG - Remove AmneziaWG from server? - Удалить AmneziaWG с сервера? + Удалить AmneziaWG с сервера? - - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Save Сохранить - + + VPN address subnet + Подсеть VPN-адресов + + + + Jc - Junk packet count + + + + + Jmin - Junk packet minimum size + + + + + Jmax - Junk packet maximum size + + + + + S1 - Init packet junk size + + + + + S2 - Response packet junk size + + + + + H1 - Init packet magic header + + + + + H2 - Response packet magic header + + + + + H4 - Transport packet magic header + + + + + H3 - Underload packet magic header + + + + + The values of the H1-H4 fields must be unique + Значения в полях H1-H4 должны быть уникальными + + + + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + Значение в поле S1 + размер инициации сообщения (148) не должно равняться значению в поле S2 + размер ответа на сообщение (92) + + + Save settings? Сохранить настройки? - - + Continue Продолжить - - + Cancel Отменить + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + PageProtocolCloakSettings - + Cloak settings Настройки Cloak - + Disguised as traffic from Замаскировать трафик под @@ -406,16 +743,21 @@ Already installed containers were found on the server. All installed containers Порт - - + + Cipher Шифрование - + Save Сохранить + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + PageProtocolOpenVpnSettings @@ -435,7 +777,7 @@ Already installed containers were found on the server. All installed containers Сетевой протокол - + Port Порт @@ -557,58 +899,58 @@ Already installed containers were found on the server. All installed containers none - + TLS auth TLS авторизация - + Block DNS requests outside of VPN Блокировать DNS-запросы за пределами VPN - + Additional client configuration commands Дополнительные команды конфигурации клиента - - + + Commands: Команды: - + Additional server configuration commands Дополнительные команды конфигурации сервера - + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + + Remove OpenVPN - Удалить OpenVPN + Удалить OpenVPN - Remove OpenVPN from server? - Удалить OpenVPN с сервера? + Удалить OpenVPN с сервера? - All users with whom you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. + Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - Continue - Продолжить + Продолжить - Cancel - Отменить + Отменить - + Save Сохранить @@ -616,32 +958,32 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings настройки - + Show connection options Показать параметры подключения - + Connection options %1 Параметры подключения %1 - + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. @@ -650,12 +992,12 @@ Already installed containers were found on the server. All installed containers Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. - + Continue Продолжить - + Cancel Отменить @@ -663,75 +1005,172 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Настройки Shadowsocks - + Port Порт - - + + Cipher Шифрование - + Save Сохранить + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + + + + PageProtocolWireGuardClientSettings + + + WG settings + Настройки WG + + + + MTU + MTU + + + + Server settings + Настройки сервера + + + + Port + Порт + + + + Save + Сохранить + + + + Save settings? + Сохранить настройки? + + + + Only the settings for this device will be changed + Будут изменены настройки только для этого устройства + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + PageProtocolWireGuardSettings - + WG settings Настройки WG - + + VPN address subnet + Подсеть VPN-адресов + + + Port Порт - - MTU - MTU + + Save settings? + Сохранить настройки? - - Remove WG - Удалить WG - - - - Remove WG from server? - Удалить WG с сервера? - - - - All users with whom you shared a connection will no longer be able to connect to it. + + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + MTU + MTU + + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + + + Remove WG + Удалить WG + + + Remove WG from server? + Удалить WG с сервера? + + + All users with whom you shared a connection will no longer be able to connect to it. + Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. + + + Continue Продолжить - + Cancel Отменить - + Save Сохранить + + PageProtocolXraySettings + + + XRay settings + Настройки XRay + + + + Disguised as traffic from + Замаскировать трафик под + + + + Save + Сохранить + + + + Unable change settings while there is an active connection + Невозможно изменить настройки во время активного соединения + + PageServerContainers @@ -742,218 +1181,290 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. На вашем сервере установлен DNS-сервис, доступ к нему возможен только через VPN. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. Адрес DNS совпадает с адресом вашего сервера. Настроить DNS можно во вкладке "Соединение" настроек приложения. - + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + Continue Продолжить - + Cancel Отменить + + + Cannot remove AmneziaDNS from running server + Невозможно удалить AmneziaDNS с работающего сервера + PageServiceSftpSettings - + Settings updated successfully Настройки успешно обновлены - + SFTP settings Настройки SFTP - + Host Хост - - - - + + + + Copied Скопировано - + Port Порт - + User name Имя пользователя - + Password Пароль - + Mount folder on device Смонтировать папку на устройстве - + In order to mount remote SFTP folder as local drive, perform following steps: <br> Чтобы смонтировать SFTP-папку как локальный диск, выполните следующие действия: <br> - - + + <br>1. Install the latest version of <br>1. Установите последнюю версию - - + + <br>2. Install the latest version of <br>2. Установите последнюю версию - + Detailed instructions Подробные инструкции - Remove SFTP and all data stored there - Удалить SFTP-хранилище со всеми данными + Удалить SFTP-хранилище со всеми данными - Remove SFTP and all data stored there? - Удалить SFTP-хранилище и все хранящиеся там данные? + Удалить SFTP-хранилище и все хранящиеся там данные? - Continue - Продолжить + Продолжить - Cancel - Отменить + Отменить + + + + PageServiceSocksProxySettings + + + Settings updated successfully + Настройки успешно обновлены + + + + + SOCKS5 settings + Настройки SOCKS5 + + + + Host + Хост + + + + + + + Copied + Скопировано + + + + + Port + Порт + + + + User name + Имя пользователя + + + + + Password + Пароль + + + + Username + Имя пользователя + + + + + Change connection settings + Изменить настройки соединения + + + + The port must be in the range of 1 to 65535 + Порт должен быть в диапазоне от 1 до 65535 + + + + Password cannot be empty + Пароль не может быть пустым + + + + Username cannot be empty + Имя пользователя не может быть пустым PageServiceTorWebsiteSettings - + Settings updated successfully Настройки успешно обновлены - + Tor website settings Настройки сайта в сети Тоr - + Website address Адрес сайта - + Copied Скопировано - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. Используйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для открытия этой ссылки. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. Через несколько минут после установки ваш onion-сайт станет доступен в сети Tor. - + When configuring WordPress set the this onion address as domain. При настройке WordPress укажите этот onion-адрес в качестве домена. - Remove website - Удалить сайт + Удалить сайт - The site with all data will be removed from the tor network. - Сайт со всеми данными будет удален из сети Tor. + Сайт со всеми данными будет удален из сети Tor. - Continue - Продолжить + Продолжить - Cancel - Отменить + Отменить PageSettings - + Settings Настройки - + Servers Серверы - + Connection Соединение - + Application Приложение - + Backup Резервное копирование - + About AmneziaVPN Об AmneziaVPN - + + Dev console + + + + Close application Закрыть приложение @@ -961,7 +1472,7 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Поддержите Amnezia @@ -970,174 +1481,718 @@ Already installed containers were found on the server. All installed containers Показать другие способы на GitHub - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia — это бесплатное приложение с открытым исходным кодом. Поддержите разработчиков, если оно вам нравится. - + Contacts Контакты - + Telegram group Группа в Telegram - + To discuss features Для обсуждения возможностей - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn - - Mail - Почта + + support@amnezia.org + support@amnezia.org - + Mail + Почта + + + For reviews and bug reports Для отзывов и сообщений об ошибках - + Copied + Скопировано + + + + mailto:support@amnezia.org + + + + GitHub GitHub - + + Discover the source code + Посмотреть исходный код + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт - - https://amnezia.org - https://amnezia.org + + Visit official website + Посетить официальный сайт - + https://amnezia.org + https://amnezia.org + + + Software version: %1 Версия ПО: %1 - + Check for updates Проверить обновления - + Privacy Policy Политика конфиденциальности - PageSettingsApplication + PageSettingsApiAvailableCountries - - Application - Приложение + + Location for connection + Страны для подключения - - Allow application screenshots - Разрешить скриншоты приложения + + Unable change server location while there is an active connection + Невозможно изменить локацию во время активного соединения + + + + PageSettingsApiDevices + + + Active devices + Активные устройства - - Auto start - Автозапуск + + Manage currently connected devices + Управление подключенными устройствами - - Launch the application every time the device is starts - Запускать приложение при загрузке устройства + + 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. + Вы можете найти support tag во вкладке «Поддержка» или, в более ранних версиях приложения, нажав «+» на нижней панели, а затем три точки вверху страницы. - - Auto connect - Автоподключение + + (current device) + (текущее устройство) - - Connect to VPN on app start - Подключаться к VPN при запуске приложения + + Support tag: + Support tag: - - Start minimized - Запускать в свернутом виде + + Last updated: + Последнее обновление: - - Launch application minimized - Запускать приложение в свернутом виде + + Cannot unlink device during active connection + Невозможно отвязать устройство во время активного соединения - - Language - Язык + + Are you sure you want to unlink this device? + Вы уверены, что хотите отвязать это устройство? - - Enable notifications - Включить уведомления + + This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect. + Это устройство будет отвязано от вашей подписки. Вы можете подключить его снова в любой момент, нажав кнопку "Подключиться". - - Enable notifications to show the VPN state in the status bar - Включить уведомления для отображения статуса VPN в строке состояния - - - - Logging - Логирование - - - - Enabled - Включено - - - - Disabled - Отключено - - - - Reset settings and remove all data from the application - Сбросить настройки и удалить все данные из приложения - - - - Reset settings and remove all data from the application? - Сбросить настройки и удалить все данные из приложения? - - - - All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. - Все настройки будут сброшены до значений по умолчанию. Все установленные сервисы AmneziaVPN останутся на сервере. - - - + Continue Продолжить - + Cancel Отменить + + PageSettingsApiInstructions + + + Windows + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#windows + + + + + macOS + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#macos + + + + + Android + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#android + + + + + AndroidTV + + + + + https://docs.amnezia.org/ru/documentation/instructions/android_tv_connect/ + + + + + iOS + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#ios + + + + + Linux + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#linux + + + + + Routers + + + + + https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#routers + + + + + How to connect on another device + Как подключить другие устройства + + + + Setup guides on the Amnezia website + Инструкции по настройке + + + + PageSettingsApiLanguageList + + Unable change server location while there is an active connection + Невозможно изменить локацию во время активного соединения + + + + PageSettingsApiNativeConfigs + + + Save AmneziaVPN config + Сохранить конфигурацию AmneziaVPN + + + + Configuration files + Файл конфигурации + + + + For router setup or the AmneziaWG app + Для настройки роутера или приложения AmneziaWG + + + + The configuration needs to be reissued + Необходимо заново скачать конфигурацию и добавить ее в приложение + + + + configuration file + файл конфигурации + + + + Generate a new configuration file + Создать новый файл конфигурации + + + + The previously created one will stop working + Ранее созданный файл перестанет работать + + + + Revoke the current configuration file + Отозвать текущий файл конфигурации + + + + Config file saved + Файл конфигурации сохранен + + + + The config has been revoked + Конфигурация была отозвана + + + + Generate a new %1 configuration file? + Создать новый %1 файл конфигурации? + + + + Revoke the current %1 configuration file? + Отозвать текущий %1 файл конфигурации? + + + + Your previous configuration file will no longer work, and it will not be possible to connect using it + Ваш предыдущий файл конфигурации не будет работать, и вы больше не сможете использовать его для подключения + + + + Download + Скачать + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + PageSettingsApiServerInfo + + For the region + Для региона + + + Price + Цена + + + Work period + Период работы + + + + Valid until + Действует до + + + Speed + Скорость + + + Copied + Скопировано + + + + Subscription status + Статус подписки + + + + Active connections + Активные соединения + + + + Configurations have been updated for some countries. Download and install the updated configuration files + Сетевые адреса одного или нескольких серверов были обновлены. Пожалуйста, удалите старые конфигурацию и загрузите новые файлы + + + + Subscription key + Ключ для подключения + + + + Amnezia Premium subscription key + Ключ подписки Amnezia Premium + + + + Save VPN key to file + Сохранить VPN-ключ в файле + + + + Copy VPN key + Скопировать VPN ключ + + + + Configuration files + Файл конфигурации + + + + Manage configuration files + Управление файлами конфигурации + + + + Active devices + Активные устройства + + + + Manage currently connected devices + Управление подключенными устройствами + + + + Support + Поддержка + + + + How to connect on another device + Как подключить другие устройства + + + + Reload API config + Перезагрузить конфигурацию API + + + + Reload API config? + Перезагрузить конфигурацию API? + + + + + + Continue + Продолжить + + + + + + Cancel + Отменить + + + + Cannot reload API config during active connection + Невозможно перзагрузить API конфигурацию при активном соединении + + + + Unlink this device + Отвязать это устройство + + + + Are you sure you want to unlink this device? + Вы уверены, что хотите отвязать это устройство? + + + + This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect. + Это устройство будет отвязано от вашей подписки. Вы можете подключить его снова в любой момент, нажав кнопку Подключиться. + + + + Cannot unlink device during active connection + Невозможно отвязать устройство во время активного соединения + + + + Remove from application + Удалить из приложения + + + + Remove from application? + Удалить из приложения? + + + + Cannot remove server during active connection + Невозможно удалить сервер во время активного соединения + + + + PageSettingsApiSupport + + + Telegram + + + + + Email Support + Email + + + + support@amnezia.org + + + + + Email Billing & Orders + По вопросам оплаты + + + + help@vpnpay.io + + + + + Website + Сайт + + + + amnezia.org + amnezia.org + + + + Support + Поддержка + + + + Our technical support specialists are available to assist you at any time + Наши специалисты технической поддержки всегда готовы помочь вам. + + + + Support tag + + + + + Copied + Скопировано + + + + PageSettingsAppSplitTunneling + + + Cannot change split tunneling settings during active connection + Невозможно изменить настройки раздельного туннелирования во время активного соединения + + + + Only the apps from the list should have access via VPN + Только приложения из списка должны работать через VPN + + + + Apps from the list should not have access via VPN + Приложения из списка не должны работать через VPN + + + + App split tunneling + Раздельное туннелирование приложений + + + + Mode + Режим + + + + Remove + Удалить + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + application name + название приложения + + + + Open executable file + Открыть исполняемый файл + + + + Executable files (*.*) + Исполняемые файлы (*.*) + + + + PageSettingsApplication + + + Application + Приложение + + + + Allow application screenshots + Разрешить скриншоты приложения + + + + Auto start + Автозапуск + + + + Launch the application every time the device is starts + Запускать приложение при загрузке устройства + + + + Auto connect + Автоподключение + + + + Connect to VPN on app start + Подключаться к VPN при запуске приложения + + + + Start minimized + Запускать в свернутом виде + + + + Launch application minimized + Запускать приложение в свернутом виде + + + + Language + Язык + + + + Enable notifications + Включить уведомления + + + + Enable notifications to show the VPN state in the status bar + Включить уведомления для отображения статуса VPN в строке состояния + + + + Logging + Логирование + + + + Enabled + Включено + + + + Disabled + Отключено + + + + Reset settings and remove all data from the application + Сбросить настройки и удалить все данные из приложения + + + + Reset settings and remove all data from the application? + Сбросить настройки и удалить все данные из приложения? + + + + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. + Все настройки будут сброшены до значений по умолчанию. Все установленные сервисы AmneziaVPN останутся на сервере. + + + + Continue + Продолжить + + + + Cancel + Отменить + + + + Cannot reset settings during active connection + Невозможно сбросить настройки во время активного соединения + + PageSettingsBackup @@ -1145,7 +2200,7 @@ Already installed containers were found on the server. All installed containers Резервное копирование - + Settings restored from backup file Настройки восстановлены из файла резервной копии @@ -1159,71 +2214,76 @@ Already installed containers were found on the server. All installed containers Резервная копия конфигурации - + You can save your settings to a backup file to restore them the next time you install the application. Вы можете сохранить настройки в файл резервной копии, чтобы восстановить их при следующей установке приложения. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. Резервная копия будет содержать ваши пароли и закрытые ключи для всех серверов, добавленных в AmneziaVPN. Храните эту информацию в надежном месте. - + Make a backup Создать резервную копию - + Save backup file Сохранить резервную копию - - + + Backup files (*.backup) Файлы резервных копий (*.backup) - + Backup file saved Резервная копия сохранена - + Restore from backup Восстановить из резервной копии - + Open backup file Открыть резервную копию - + Import settings from a backup file? Импортировать настройки из резервной копии? - + All current settings will be reset Все текущие настройки будут сброшены - + Continue Продолжить - + Cancel Отменить + + + Cannot restore backup settings during active connection + Невозможно восстановить настройки из резервной копии во время активного соединения + PageSettingsConnection - + Connection Соединение @@ -1236,42 +2296,57 @@ Already installed containers were found on the server. All installed containers Подключение к VPN при запуске приложения - + Use AmneziaDNS Использовать AmneziaDNS - + If AmneziaDNS is installed on the server Если AmneziaDNS установлен на сервере - + DNS servers DNS-серверы - + When AmneziaDNS is not used or installed Когда AmneziaDNS не используется или не установлен - + Allows you to use the VPN only for certain Apps Позволяет использовать VPN только для определенных приложений - + + KillSwitch + KillSwitch + + + + Disables your internet if your encrypted VPN connection drops out for any reason. + Отключает ваше интернет-соединение, если ваше зашифрованное VPN-соединение по какой-либо причине прерывается. + + + + Cannot change killSwitch settings during active connection + Невозможно изменить настройки аварийного выключателя во время активного соединения + + + Site-based split tunneling Раздельное туннелирование сайтов - + Allows you to select which sites you want to access through the VPN Позволяет выбирать, к каким сайтам подключаться через VPN - + App-based split tunneling Раздельное туннелирование приложений @@ -1283,12 +2358,12 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS Сервер по умолчанию не поддерживает пользовательские DNS - + DNS servers DNS-серверы @@ -1297,52 +2372,52 @@ Already installed containers were found on the server. All installed containers Когда AmneziaDNS не используется или не установлен - + If AmneziaDNS is not used or installed Если AmneziaDNS не используется или не установлен - + Primary DNS Первичный DNS - + Secondary DNS Вторичный DNS - + Restore default Восстановить по умолчанию - + Restore default DNS settings? Восстановить настройки DNS по умолчанию? - + Continue Продолжить - + Cancel Отменить - + Settings have been reset Настройки сброшены - + Save Сохранить - + Settings saved Настройки сохранены @@ -1350,72 +2425,106 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены. + Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены. - + Logging Логирование - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения. - Save logs - Сохранять логи + Сохранять логи - Open folder with logs - Открыть папку с логами + Открыть папку с логами - + + Save Сохранить - + + Logs files (*.log) Файлы логов (*.log) - + + Logs file saved Файл с логами сохранен - Save logs to file - Сохранить логи в файл + Сохранить логи в файл - + + Enable logs + Включить запись логов + + + Clear logs? Очистить логи? - + Continue Продолжить - + Cancel Отменить - + Logs have been cleaned up Логи очищены - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + Открыть папку с логами + + + + Export logs + Сохранить логи + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs Очистить логи @@ -1423,27 +2532,24 @@ Already installed containers were found on the server. All installed containers PageSettingsServerData - + All installed containers have been added to the application Все установленные протоколы и сервисы были добавлены в приложение - Clear Amnezia cache - Очистить кэш Amnezia + Очистить кэш Amnezia - May be needed when changing other settings - Может понадобиться при изменении других настроек + Может понадобиться при изменении других настроек - Clear cached profiles? - Удалить кэш Amnezia? + Удалить кэш Amnezia? - + No new installed containers found Новые установленные протоколы и сервисы не обнаружены @@ -1453,75 +2559,93 @@ Already installed containers were found on the server. All installed containers - - - - - + + + + Continue Продолжить - - - - - + + + + Cancel Отменить - + Check the server for previously installed Amnezia services Проверить сервер на наличие ранее установленных сервисов Amnezia - + Add them to the application if they were not displayed Добавить их в приложение, если они не отображаются - + Reboot server Перезагрузить сервер - + Do you want to reboot the server? Вы уверены, что хотите перезагрузить сервер? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? Процесс перезагрузки может занять около 30 секунд. Вы уверены, что хотите продолжить? - + + Cannot reboot server during active connection + Невозможно перезагрузить сервер во время активного соединения + + + Do you want to remove the server from application? Вы уверены, что хотите удалить сервер из приложения? - + + Cannot remove server during active connection + Невозможно удалить сервер во время активного соединения + + + Do you want to clear server from Amnezia software? Вы хотите очистить сервер от всех сервисов Amnezia? - + All users whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + + Cannot clear server from Amnezia software during active connection + Невозможно очистить сервер от сервисов Amnezia во время активного соединения + + + Reset API config Сбросить конфигурацию API - + Do you want to reset API config? Вы хотите сбросить конфигурацию API? - + + Cannot reset API config during active connection + Невозможно сбросить конфигурацию API во время активного соединения + + + Remove server from application Удалить сервер из приложения @@ -1530,12 +2654,12 @@ Already installed containers were found on the server. All installed containers Удалить сервер? - + All installed AmneziaVPN services will still remain on the server. Все установленные сервисы и протоколы Amnezia останутся на сервере. - + Clear server from Amnezia software Очистить сервер от протоколов и сервисов Amnezia @@ -1551,27 +2675,29 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + Subscription is valid until + Подписка заканчивается через + + Server name - Имя сервера + Имя сервера - Save - Сохранить + Сохранить - + Protocols Протоколы - + Services Сервисы - + Management Управление @@ -1583,36 +2709,87 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings настройки - + Clear %1 profile + Очистить профиль %1 + + + + Clear %1 profile? + Очистить профиль %1? + + + + + + + + + connection settings + настройки подключения + + + + Click the "connect" button to create a connection configuration + Нажмите кнопку «Подключиться», чтобы создать конфигурацию + + + + server settings + настройки сервера + + + + Clear profile + Очистить профиль + + + + The connection configuration will be deleted for this device only + Конфигурация подключения будет удалена только на этом устройстве + + + + Unable to clear %1 profile while there is an active connection + Невозможно очистить профиль %1 во время активного соединения + + + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + All users with whom you shared a connection will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. + + + Cannot remove active container + Невозможно удалить активный контейнер + All users who you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились VPN, больше не смогут к нему подключаться. - + + Continue Продолжить - + + Cancel Отменить @@ -1620,7 +2797,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers Серверы @@ -1628,7 +2805,7 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function Сервер по умолчанию не поддерживает раздельное туннелирование @@ -1637,32 +2814,32 @@ Already installed containers were found on the server. All installed containers Только адреса из списка должны открываться через VPN - + Addresses from the list should not be accessed via VPN Адреса из списка не должны открываться через VPN - + Split tunneling - Раздельное туннелирование + Раздельное туннелирование сайтов - + Mode Режим - + Remove Удалить - + Continue Продолжить - + Cancel Отменить @@ -1671,83 +2848,120 @@ Already installed containers were found on the server. All installed containers Сайт или IP - + Import / Export Sites Импорт/экспорт сайтов - + Only the sites listed here will be accessed through the VPN Только адреса из списка должны открываться через VPN - + Cannot change split tunneling settings during active connection - Невозможно изменить настройки раздельного туннелирования при активном соединении + Невозможно изменить настройки раздельного туннелирования во время активного соединения - + website or IP веб-сайт или IP - + Import Импорт - + Save site list Сохранить список сайтов - + Save sites - Сохранить + Сохранить сайты - - - + + + Sites files (*.json) Файлы сайтов (*.json) - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим - PageSettingsAppSplitTunneling + PageSetupWizardApiServiceInfo - - Cannot change split tunneling settings during active connection - Невозможно изменить настройки раздельного туннелирования при активном соединении + + For the region + Для региона + + + + Price + Цена + + + + Work period + Период работы + + + + Speed + Скорость + + + + Features + Особенности + + + + Connect + Подключиться + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + VPN от Amnezia + + + + Choose a VPN service that suits your needs. + Выберите VPN-сервис, который подходит именно вам. PageSetupWizardConfigSource - Server connection - Подключение к серверу + Подключение к серверу Do not use connection code from public sources. It may have been created to intercept your data. @@ -1758,39 +2972,135 @@ It's okay as long as it's from someone you trust. Всё в порядке, если кодом поделился пользователь, которому вы доверяете. - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - Не используйте коды подключения из ненадежных источников, так как они могут быть созданы для перехвата ваших данных. + Не используйте коды подключения из ненадежных источников, так как они могут быть созданы для перехвата ваших данных. - What do you have? - Что у вас есть? + Что у вас есть? - + File with connection settings Файл с настройками подключения - File with connection settings or backup - Файл с настройками подключения или резервной копией + Файл с настройками подключения или резервной копией - + + Connection + Соединение + + + + Settings + Настройки + + + + Enable logs + Включить запись логов + + + + Support tag + + + + + Copied + Скопировано + + + + Insert the key, add a configuration file or scan the QR-code + Вставьте ключ, добавьте файл конфигурации или отсканируйте QR-код + + + + Insert key + Вставьте ключ + + + + Insert + Вставить + + + + Continue + Продолжить + + + + Other connection options + Другие варианты подключения + + + + Site Amnezia + Сайт Amnezia + + + + VPN by Amnezia + VPN от Amnezia + + + + Connect to classic paid and free VPN services from Amnezia + Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia + + + + Self-hosted VPN + Self-hosted VPN + + + + Configure Amnezia VPN on your own server + Настроить VPN на собственном сервере + + + + Restore from backup + Восстановить из резервной копии + + + + + + + + + Open backup file + Открыть резервную копию + + + + Backup files (*.backup) + Файлы резервных копий (*.backup) + + + Open config file Открыть файл с конфигурацией - + QR code QR-код - + + I have nothing + У меня ничего нет + + Key as text - Ключ в виде текста + Ключ в виде текста @@ -1800,7 +3110,7 @@ It's okay as long as it's from someone you trust. Подключение к серверу - + Server IP address [:port] IP-адрес[:порт] сервера @@ -1813,7 +3123,7 @@ It's okay as long as it's from someone you trust. Password / SSH private key - + Continue Продолжить @@ -1823,7 +3133,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам - + Enter the address in the format 255.255.255.255:88 Введите адрес в формате 255.255.255.255:88 @@ -1832,42 +3142,52 @@ and will not be shared or disclosed to the Amnezia or any third parties Login to connect via SSH - + Configure your server Настроить ваш сервер - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username Имя пользователя SSH - + Password or SSH private key Пароль или закрытый ключ SSH - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам - + + How to run your VPN server + Как создать VPN на собственном сервере + + + + Where to get connection data, step-by-step instructions for buying a VPS + Где взять данные для подключения, пошаговые инструкции по покупке VPS + + + Ip address cannot be empty Поле с IP-адресом не может быть пустым - + Login cannot be empty Поле с логином не может быть пустым - + Password/private key cannot be empty Поле с паролем/закрытым ключом не может быть пустым @@ -1875,17 +3195,26 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - What is the level of internet control in your region? - Какой уровень контроля над интернетом в вашем регионе? + Какой уровень контроля над интернетом в вашем регионе? - + + Choose Installation Type + Выберите тип установки + + + + Manual + Ручная + + + Choose a VPN protocol - Выберите VPN-протокол + Выбрать VPN-протокол - + Skip setup Пропустить настройку @@ -1898,7 +3227,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Выбрать VPN-протокол - + Continue Продолжить @@ -1910,46 +3239,38 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardInstalling - + The server has already been added to the application Сервер уже был добавлен в приложение - Amnezia has detected that your server is currently - Amnezia обнаружила, что ваш сервер в настоящее время - - - busy installing other software. Amnezia installation - занят установкой других протоколов или сервисов. Установка Amnezia - - - + Amnezia has detected that your server is currently Amnezia обнаружила, что ваш сервер в настоящее время - + busy installing other software. Amnezia installation - занят установкой другого ПО. Установка Amnezia + занят установкой других протоколов или сервисов. Установка Amnezia - + will pause until the server finishes installing other software будет приостановлена до тех пор, пока сервер не завершит установку другого ПО - + Installing Установка - + Cancel installation Отменить установку - - + + Usually it takes no more than 5 minutes Обычно это занимает не более 5 минут @@ -1957,45 +3278,50 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocolSettings - + Installing %1 Устанавливается %1 - + More detailed Подробнее - + Close Закрыть - + Network protocol Сетевой протокол - + Port Порт - + Install Установить + + + The port must be in the range of 1 to 65535 + Порт должен быть в диапазоне от 1 до 65535 + PageSetupWizardProtocols - + VPN protocol VPN-протокол - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. Выберите протокол, который вам больше подходит. В дальнейшем можно установить другие протоколы и дополнительные сервисы, такие как DNS-прокси и SFTP. @@ -2003,7 +3329,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. Наведите камеру на QR-код и удерживайте ее в течение нескольких секунд. @@ -2011,34 +3337,33 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardStart - Settings restored from backup file - Настройки восстановлены из резервной копии + Настройки восстановлены из резервной копии - Free service for creating a personal VPN on your server. - Простое и бесплатное приложение для запуска собственного VPN на своем сервере. + Простое и бесплатное приложение для запуска собственного VPN на своем сервере. - Helps you access blocked content without revealing your privacy, even to VPN providers. - Помогает получить доступ к заблокированному контенту, не раскрывая вашу конфиденциальность даже провайдерам VPN. + Помогает получить доступ к заблокированному контенту, не раскрывая вашу конфиденциальность даже провайдерам VPN. - I have the data to connect - У меня есть данные для подключения + У меня есть данные для подключения - I have nothing - У меня ничего нет + У меня ничего нет - https://amnezia.org/instructions/0_starter-guide - https://amnezia.org/ru/starter-guide + https://amnezia.org/ru/starter-guide + + + + Let's get started + Приступим @@ -2064,7 +3389,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Вставить - + Continue Продолжить @@ -2072,7 +3397,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardViewConfig - + New connection Новое соединение @@ -2081,22 +3406,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Не используйте код подключения из публичных источников. Его могли создать, чтобы перехватить ваши данные. - + Collapse content Свернуть - + Show content Показать - + + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. + Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером. + + + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных. - + Connect Подключиться @@ -2104,12 +3434,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + OpenVPN native format Оригинальный формат OpenVPN - + WireGuard native format Оригинальный формат WireGuard @@ -2118,7 +3448,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN-Доступ - + Connection Соединение @@ -2131,8 +3461,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. - - + + Server Сервер @@ -2141,19 +3471,19 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ - + Config revoked Конфигурация отозвана - + Connection to Подключение к - + File with connection settings to - Файл с настройками доступа к + Файл с настройками подключения к @@ -2181,98 +3511,132 @@ and will not be shared or disclosed to the Amnezia or any third parties Сохранить конфигурацию Cloak - + + Save XRay config + Сохранить конфигурацию XRay + + + For the AmneziaVPN app Для приложения AmneziaVPN - + AmneziaWG native format Оригинальный формат AmneziaWG - + Shadowsocks native format Оригинальный формат Shadowsocks - + Cloak native format Оригинальный формат Cloak - + + XRay native format + Оригинальный формат XRay + + + Share VPN Access Поделиться VPN - + Share full access to the server and VPN Поделиться полным доступом к серверу и VPN - + Use for your own devices, or share with those you trust to manage the server. Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете. - - + + Users Пользователи - + User name Имя пользователя - + Search Поиск - - Creation date: - Дата создания: + + Creation date: %1 + Дата создания: %1 - + + Latest handshake: %1 + Последнее рукопожатие: %1 + + + + Data received: %1 + Получено данных: %1 + + + + Data sent: %1 + Отправлено данных: %1 + + + + Allowed IPs: %1 + + + + Creation date: + Дата создания: + + + Rename Переименовать - + Client name Имя клиента - + Save Сохранить - + Revoke Отозвать - + Revoke the config for a user - %1? Отозвать конфигурацию для пользователя - %1? - + The user will no longer be able to connect to your server. Пользователь больше не сможет подключаться к вашему серверу. - + Continue Продолжить - + Cancel Отменить @@ -2281,25 +3645,25 @@ and will not be shared or disclosed to the Amnezia or any third parties Полный доступ - + Share VPN access without the ability to manage the server Поделиться доступом к VPN без возможности управления сервером - - + + Protocol Протокол - - + + Connection format Формат подключения - - + + Share Поделиться @@ -2307,66 +3671,81 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShareFullAccess - + Full access to the server and VPN Полный доступ к серверу и VPN - + We recommend that you use full access to the server only for your own additional devices. Мы рекомендуем использовать полный доступ к серверу только для собственных устройств. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. Если вы поделитесь полным доступом с другими людьми, то они смогут удалять и добавлять протоколы и сервисы на сервер, что приведет к некорректной работе VPN для всех пользователей. - - + + Server Сервер - + Accessing Доступ - + File with accessing settings to Файл с настройками доступа к - + Share Поделиться - + + Access error! + Ошибка доступа! + + + Connection to Подключение к - + File with connection settings to - Файл с настройками доступа к + Файл с настройками подключения к PageStart - + Logging was disabled after 14 days, log files were deleted Логирование было отключено по прошествии 14 дней, файлы логов были удалены. + + + Settings restored from backup file + Настройки восстановлены из бэкап файла + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + Логирование включено. Обратите внимание, что через 14 дней оно будет автоматически отключено, а все файлы логов будут удалены. + PopupType - + Close Закрыть @@ -2400,12 +3779,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Пароль не найден - + Could not open keystore Не удалось открыть хранилище ключей - + Could not remove private key from keystore Не удалось удалить закрытый ключ из хранилища ключей @@ -2581,27 +3960,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Не удалось открыть хранилище ключей - + Could not create private key generator Не удалось создать генератор закрытых ключей - + Could not generate new private key Не удалось сгенерировать новый закрытый ключ - + Could not retrieve private key from keystore Не удалось получить закрытый ключ из хранилища ключей - + Could not create encryption cipher Не удалось создать шифр шифрования - + Could not encrypt data Не удалось зашифровать данные @@ -2614,9 +3993,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Нет ошибки - Unknown Error - Неизвестная ошибка + Неизвестная ошибка @@ -2624,67 +4002,82 @@ and will not be shared or disclosed to the Amnezia or any third parties Функция не реализована - + + Background service is not running + Фоновая служба не запущена + + + + The selected protocol is not supported on the current platform + Выбранный протокол не поддерживается на данном устройстве + + + Server check failed Проверка сервера завершилась неудачей - + Server port already used. Check for another software Порт сервера уже используется. Проверьте наличие другого ПО - + Server error: Docker container missing Ошибка сервера: отсутствует Docker-контейнер - + Server error: Docker failed Ошибка сервера: сбой в работе Docker - + Installation canceled by user Установка отменена пользователем - + The user does not have permission to use sudo У пользователя нет прав на использование sudo + Server error: Packet manager error + Ошибка сервера: ошибка менеджера пакетов + + + SSH request was denied SSH-запрос был отклонён - + SSH request was interrupted SSH-запрос был прерван - + SSH internal error Внутренняя ошибка SSH - + Invalid private key or invalid passphrase entered Введен неверный закрытый ключ или неверная парольная фраза - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types Выбранный формат закрытого ключа не поддерживается, используйте типы ключей openssh ED25519 или PEM - + Timeout connecting to server Тайм-аут подключения к серверу - + SCP error: Generic failure Ошибка SCP: общий сбой @@ -2741,22 +4134,23 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp error: No media was in remote drive - + The config does not contain any containers and credentials for connecting to the server Конфигурация не содержит каких-либо контейнеров и учетных данных для подключения к серверу - + + Error when retrieving configuration from API Ошибка при получении конфигурации из API - + This config has already been added to the application Данная конфигурация уже была добавлена в приложение - + ErrorCode: %1. Код ошибки: %1. @@ -2765,117 +4159,166 @@ and will not be shared or disclosed to the Amnezia or any third parties Failed to save config to disk - + OpenVPN config missing Отсутствует конфигурация OpenVPN - + OpenVPN management server error Серверная ошибка управлением OpenVPN - + OpenVPN executable missing Отсутствует исполняемый файл OpenVPN - + Shadowsocks (ss-local) executable missing Отсутствует исполняемый файл Shadowsocks (ss-local) - + Cloak (ck-client) executable missing Отсутствует исполняемый файл Cloak (ck-client) - + Amnezia helper service error Ошибка вспомогательной службы Amnezia - + OpenSSL failed Ошибка OpenSSL - + Can't connect: another VPN connection is active Невозможно подключиться: активно другое VPN-соединение - + Can't setup OpenVPN TAP network adapter Невозможно настроить сетевой адаптер OpenVPN TAP - + VPN pool error: no available addresses Ошибка пула VPN: нет доступных адресов - + + Unable to open config file + Не удалось открыть файл конфигурации + + + + VPN Protocols is not installed. + Please install VPN container at first + VPN-протоколы не установлены. + Пожалуйста, установите протокол + + + VPN connection error Ошибка VPN-соединения - + + In the response from the server, an empty config was received + В ответе от сервера была получена пустая конфигурация + + + + SSL error occurred + Произошла ошибка SSL + + + + Server response timeout on api request + Тайм-аут ответа сервера на запрос API + + + + Missing AGW public key + + + + + Failed to decrypt response payload + + + + + Missing list of available services + + + + + The limit of allowed configurations per subscription has been exceeded + Превышен лимит разрешенных конфигураций для одной подписки + + + QFile error: The file could not be opened Ошибка QFile: не удалось открыть файл - + QFile error: An error occurred when reading from the file Ошибка QFile: произошла ошибка при чтении из файла - + QFile error: The file could not be accessed Ошибка QFile: не удалось получить доступ к файлу - + QFile error: An unspecified error occurred Ошибка QFile: произошла неизвестная ошибка - + QFile error: A fatal error occurred Ошибка QFile: произошла фатальная ошибка - + QFile error: The operation was aborted Ошибка QFile: операция была прервана - + Internal error Внутренняя ошибка - + IPsec IPsec - Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - Shadowsocks маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. + Shadowsocks маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. - OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak — это OpenVPN с маскировкой VPN-трафика под обычный веб-трафик и защитой от обнаружения активным зондированием. Подходит для регионов с самым высоким уровнем цензуры. + OpenVPN over Cloak — это OpenVPN с маскировкой VPN-трафика под обычный веб-трафик и защитой от обнаружения активным зондированием. Подходит для регионов с самым высоким уровнем цензуры. - + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + IKEv2/IPsec — современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. Он имеет встроенную поддержку в последних версиях Android и iOS. + + + Create a file vault on your server to securely store and transfer files. Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. - This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -2894,7 +4337,7 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - Это связка протокола OpenVPN и плагина Cloak, разработанная специально для защиты от блокировки. + Это связка протокола OpenVPN и плагина Cloak, разработанная специально для защиты от блокировки. OpenVPN обеспечивает безопасное VPN-соединение, шифруя весь интернет-трафик между клиентом и сервером. @@ -2913,7 +4356,6 @@ Cloak изменяет метаданные пакетов таким образ * Работает по сетевому протоколу TCP, использует порт 443 - A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -2923,7 +4365,7 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - Относительно новый и популярный VPN-протокол с простой архитектурой. + Относительно новый и популярный VPN-протокол с простой архитектурой. WireGuard обеспечивает стабильное VPN-соединение и высокую производительность на всех устройствах. Он использует строго заданные настройки шифрования. WireGuard по сравнению с OpenVPN имеет меньшую задержку и лучшую пропускную способность при передаче данных. WireGuard очень уязвим для блокировки из-за характерных сигнатур пакетов. В отличие от некоторых других VPN-протоколов, использующих методы обфускации, последовательные сигнатуры пакетов WireGuard легче идентифицируются и, следовательно, могут блокироваться современными Deep Packet Inspection (DPI) системами и другими инструментами для сетевого мониторинга. @@ -2934,7 +4376,17 @@ WireGuard очень уязвим для блокировки из-за хара * Работает по сетевому протоколу UDP - + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. + Протокол REALITY, новаторская разработка создателей XRay, специально спроектирован для противодействия самой строгой цензуре с помощью нового способа обхода блокировок. +Он уникальным образом идентифицирует цензоров на этапе TLS-рукопожатия, беспрепятственно работая в качестве прокси для реальных клиентов и перенаправляя цензоров на реальные сайты, такие как google.com, тем самым предъявляя подлинный TLS-сертификат и данные. +REALITY отличается от аналогичных технологий благодаря способности без специальной настройки маскировать веб-трафик так, как будто он поступает со случайных легитимных сайтов. +В отличие от более старых протоколов, таких как VMess, VLESS и транспорт XTLS-Vision, технология распознавания "друг или враг" на этапе TLS-рукопожатия повышает безопасность и обходит обнаружение сложными системами DPI-анализа, которые используют методы активного зондирования. Это делает REALITY эффективным решением для поддержания свободы интернета в регионах с жесткой цензурой. + + + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -2955,63 +4407,188 @@ While it offers a blend of security, stability, and speed, it's essential t * Работает по сетевому протоколу UDP, использует порты 500 и 4500 - + DNS Service Сервис DNS - + SFTP file sharing service SFTP-сервис для обмена файлами - - + + Website in Tor network Веб-сайт в сети Tor - + AmneziaDNS AmneziaDNS - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. - OpenVPN — популярный VPN-протокол с гибкой настройкой. Имеет собственный протокол безопасности с SSL/TLS для обмена ключами. + OpenVPN — самый популярный VPN-протокол с гибкой настройкой. Имеет собственный протокол безопасности с SSL/TLS для обмена ключами. - - WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. - WireGuard — популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. Рекомендуется для регионов с низким уровнем цензуры. - - - - AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. - AmneziaWG — специальный протокол от Amnezia, основанный на протоколе WireGuard. Он такой же быстрый, как WireGuard, но очень устойчив к блокировкам. Рекомендуется для регионов с высоким уровнем цензуры. + + Shadowsocks masks VPN traffic, making it resemble normal web traffic, but it may still be detected by certain analysis systems. + Shadowsocks маскирует VPN-трафик, делая его похожим на обычный веб-трафик, но он все равно может быть обнаружен некоторыми системами анализа. - XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - XRay with REALITY подойдет для стран с самым высоким уровнем цензуры. Маскировка трафика под веб-трафик на уровне TLS и защита от обнаружения методами активного зондирования. + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. It is very resistant to detection, but offers low speed. + OpenVPN over Cloak — OpenVPN с маскировкой под веб-трафик , а также с защитой от обнаружения и систем анализа трафика. Он очень устойчив к обнаружению, но имеет низкую скорость работы в сравнении с другими похожими протоколами. + + + + WireGuard - popular VPN protocol with high performance, high speed and low power consumption. + WireGuard — популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec — современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. + AmneziaWG is a special protocol from Amnezia based on WireGuard. It provides high connection speed and ensures stable operation even in the most challenging network conditions. + AmneziaWG — специальный протокол от Amnezia, основанный на WireGuard. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях. + XRay with REALITY masks VPN traffic as web traffic and protects against active probing. It is highly resistant to detection and offers high speed. + XRay с REALITY маскирует VPN-трафик под веб-трафик. Обладает высокой устойчивостью к обнаружению и обеспечивает высокую скорость соединения. + + + + OpenVPN stands as one of the most popular and time-tested VPN protocols available. +It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. + +* Available in the AmneziaVPN across all platforms +* Normal power consumption on mobile devices +* Flexible customisation to suit user needs to work with different operating systems and devices +* Recognised by DPI systems and therefore susceptible to blocking +* Can operate over both TCP and UDP network protocols. + OpenVPN является одним из самых популярных и проверенных временем VPN-протоколов. Он использует собственный протокол безопасности, и криптографические протоколы SSL/TLS для шифрования и обмена ключами. Более того, поддержка множества методов аутентификации делает OpenVPN универсальным, адаптируемым и подходящим для широкого спектра устройств и операционных систем. Благодаря своему открытому коду, OpenVPN подвергается тщательной проверке со стороны мирового сообщества, что постоянно укрепляет его безопасность. Имея отличный баланс между производительностью, безопасностью и совместимостью OpenVPN остается лучшим выбором для людей и компаний, заботящихся о конфиденциальности, однако OpenVPN легко распознается современными системами анализа трафика. +Доступен в AmneziaVPN на всех платформах +Нормальное энергопотребление на мобильных устройствах +Гибкая настройка полезная при работе с различными операционными системами и устройствами +Распознается системами DPI и, следовательно, уязвим к блокировкам +Может работать как по TCP, так и по UDP протоколу. + + + + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against detection. + +OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. + +Cloak protects OpenVPN from detection. + +Cloak can modify packet metadata so that it completely masks VPN traffic as normal web traffic, and also protects the VPN from detection by Active Probing. This makes it very resistant to being detected + +Immediately after receiving the first data packet, Cloak authenticates the incoming connection. If authentication fails, the plugin masks the server as a fake website and your VPN becomes invisible to analysis systems. + +* Available in the AmneziaVPN across all platforms +* High power consumption on mobile devices +* Flexible settings +* Not recognised by detection systems +* Works over TCP network protocol, 443 port. + + Это связка протокола OpenVPN и плагина Cloak, созданная специально для защиты от обнаружения. + +OpenVPN обеспечивает безопасное VPN-соединение, шифруя весь интернет-трафик между клиентом и сервером. + +Плагин Cloak защищает OpenVPN от обнаружения. + +Cloak может изменять метаданные пакета, чтобы полностью замаскировать VPN-трафик под обычный веб-трафик, а также защищает VPN от обнаружения с помощью метода Active Probing. Это делает его очень устойчивым к обнаружению. + +Сразу после получения первого пакета данных Cloak аутентифицирует входящее соединение, если аутентификация не удалась, плагин маскирует сервер под настоящий веб-сайт, и ваш VPN становится невидимым для систем анализа. Имеет низкую скорость работы в сравнении с другими похожими протоколами. + +* Доступно в AmneziaVPN на всех платформах. +* Высокое энергопотребление на мобильных устройствах +* Гибкие настройки +* Не распознается системами обнаружения. +* Работает по сетевому протоколу TCP, порт 443. + + + + + A relatively new popular VPN protocol with a simplified architecture. +WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. +WireGuard is very susceptible to detection and blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Easily recognised by DPI analysis systems, susceptible to blocking +* Works over UDP network protocol. + Популярный VPN-протокол с упрощенной архитектурой. +WireGuard обеспечивает стабильное VPN-соединение и высокую производительность на всех устройствах. Он использует закодированные настройки шифрования. WireGuard по сравнению с OpenVPN имеет меньшую задержку и лучшую пропускную способность передачи данных. +WireGuard очень чувствителен к обнаружению и блокировке из-за различных сигнатур пакетов. В отличие от некоторых других VPN протоколов, использующих методы запутывания, последовательные шаблоны сигнатур пакетов WireGuard легко идентифицируются системами анализа трафика. + +* Доступно в AmneziaVPN на всех платформах. +* Низкое энергопотребление +* Минимальное количество настроек +* Легко распознается системами анализа DPI, подвержен блокировке. +* Работает по сетевому протоколу UDP. + + + + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. +While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. +This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Not recognised by traffic analysis systems +* Works over UDP network protocol. + AmneziaWG — это современная версия популярного VPN протокола, основанная на базе WireGuard, сохранившая упрощенную архитектуру и высокопроизводительные возможности на всех устройствах. +Хотя WireGuard известен своей эффективностью, обнаружить его довольно легко из-за различных сигнатур пакетов. AmneziaWG решает эту проблему, используя более совершенные методы работы, смешивая свой трафик с обычным интернет-трафиком. +Это означает, что AmneziaWG сохраняет высокую производительность оригинала, добавляя при этом дополнительный уровень скрытности, что делает его отличным выбором для тех, кому нужно быстрое и незаметное VPN-соединение. + +* Доступно в AmneziaVPN на всех платформах. +* Низкое энергопотребление +* Минимальное количество настроек +* Не распознается системами анализа трафика. +* Работает по сетевому протоколу UDP. + + + + The REALITY protocol, a pioneering development by the creators of XRay, is designed to provide the highest level of protection against detection through its innovative approach to security and privacy. +It uniquely identifies attackers during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting attackers to genuine websites, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security. This makes REALITY a robust solution for maintaining internet freedom. + Протокол REALITY, современная разработка от создателей XRay. Призван обеспечить высочайший уровень защиты от обнаружения благодаря инновационному подходу к безопасности и конфиденциальности. +Он безошибочно идентифицирует злоумышленников на этапе установления связи TLS, беспрепятственно работая в качестве прокси-сервера для оригинального клиента и перенаправляя злоумышленников на подлинные веб-сайты, предоставляя тем самым подлинный сертификат TLS и данные. +Эта расширенная возможность отличает REALITY от аналогичных технологий тем, что способна маскироваться под случайный веб-трафик без использования специальных настроек. +В отличие от старых протоколов, таких как VMess, VLESS и транспорт XTLS-Vision, REALITY имеет инновационную технологию распознавания «свой-чужой».Это делает REALITY надежным решением для обеспечения доступа к свободному интернету. + + + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. + WireGuard — новый популярный VPN-протокол с высокой производительностью, высокой скоростью и низким энергопотреблением. Рекомендуется для регионов с низким уровнем цензуры. + + + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. + AmneziaWG — специальный протокол от Amnezia, основанный на протоколе WireGuard. Он такой же быстрый, как WireGuard, но очень устойчив к блокировкам. Рекомендуется для регионов с высоким уровнем цензуры. + + + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. + XRay with REALITY подойдет для стран с самым высоким уровнем цензуры. Маскировка трафика под веб-трафик на уровне TLS и защита от обнаружения методами активного зондирования. + + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. + IKEv2/IPsec — современный стабильный протокол, немного быстрее других, восстанавливает соединение после потери сигнала. + + + Deploy a WordPress site on the Tor network in two clicks. Разверните сайт на WordPress в сети Tor в два клика. - + Replace the current DNS server with your own. This will increase your privacy level. Замените текущий DNS-сервер на свой собственный. Это повысит уровень вашей конфиденциальности. - OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -3020,7 +4597,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Flexible customisation to suit user needs to work with different operating systems and devices * Recognised by DPI analysis systems and therefore susceptible to blocking * Can operate over both TCP and UDP network protocols. - OpenVPN — один из самых популярных и проверенных временем VPN-протоколов. + OpenVPN — один из самых популярных и проверенных временем VPN-протоколов. В нем используется уникальный протокол безопасности, опирающийся на SSL/TLS для шифрования и обмена ключами. Кроме того, OpenVPN поддерживает множество методов аутентификации, что делает его универсальным и адаптируемым к широкому спектру устройств и операционных систем. Благодаря открытому исходному коду OpenVPN подвергается тщательному анализу со стороны мирового сообщества, что постоянно повышает его безопасность. Оптимальное соотношение производительности, безопасности и совместимости делает OpenVPN лучшим выбором как для частных лиц, так и для компаний, заботящихся о конфиденциальности. * Доступен в AmneziaVPN на всех платформах @@ -3030,33 +4607,32 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Может работать по сетевым протоколам TCP и UDP - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms * Configurable encryption protocol * Detectable by some DPI systems * Works over TCP network protocol. - Shadowsocks создан на основе протокола SOCKS5, защищает соединение с помощью шифра AEAD. Несмотря на то, что протокол Shadowsocks разработан таким образом, чтобы быть незаметным и сложным для идентификации, он не идентичен стандартному HTTPS-соединению. Поэтому некоторые системы анализа трафика всё же могут обнаружить соединение Shadowsocks. В связи с ограниченной поддержкой в Amnezia рекомендуется использовать протокол AmneziaWG. + Shadowsocks создан на основе протокола SOCKS5, защищает соединение с помощью шифра AEAD. Несмотря на то, что протокол Shadowsocks разработан таким образом, чтобы быть незаметным и сложным для идентификации, он не идентичен стандартному HTTPS-соединению, поэтому некоторые системы анализа трафика всё же могут обнаружить соединение Shadowsocks. В связи с ограниченной поддержкой в Amnezia рекомендуется использовать протокол AmneziaWG. * Доступен в AmneziaVPN только для ПК и ноутбуков * Настраиваемый протокол шифрования * Распознается некоторыми системами DPI-анализа -* Работает по сетевому протоколу TCP +* Работает по сетевому протоколу TCP. - The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - Протокол REALITY, новаторская разработка создателей XRay, специально спроектирован для противодействия самой строгой цензуре с помощью нового способа обхода блокировок. + Протокол REALITY, новаторская разработка создателей XRay, специально спроектирован для противодействия самой строгой цензуре с помощью нового способа обхода блокировок. Он уникальным образом идентифицирует цензоров на этапе TLS-рукопожатия, беспрепятственно работая в качестве прокси для реальных клиентов и перенаправляя цензоров на реальные сайты, такие как google.com, тем самым предъявляя подлинный TLS-сертификат и данные. REALITY отличается от аналогичных технологий благодаря способности без специальной настройки маскировать веб-трафик так, как будто он поступает со случайных легитимных сайтов. В отличие от более старых протоколов, таких как VMess, VLESS и XTLS-Vision, технология распознавания "друг или враг" на этапе TLS-рукопожатия повышает безопасность и обходит обнаружение сложными системами DPI-анализа, которые используют методы активного зондирования. Это делает REALITY эффективным решением для поддержания свободы интернета в регионах с жесткой цензурой. - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -3133,7 +4709,6 @@ WireGuard очень восприимчив к блокированию из-з * Работает по сетевому протоколу UDP. - A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -3143,7 +4718,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin * Minimum number of settings * Not recognised by DPI analysis systems, resistant to blocking * Works over UDP network protocol. - AmneziaWG — усовершенствованная версия популярного VPN-протокола WireGuard. AmneziaWG опирается на фундамент, заложенный WireGuard, сохраняя упрощенную архитектуру и высокую производительность на различных устройствах. + AmneziaWG — усовершенствованная версия популярного VPN-протокола WireGuard. AmneziaWG опирается на фундамент, заложенный WireGuard, сохраняя упрощенную архитектуру и высокую производительность на различных устройствах. Хотя WireGuard известен своей эффективностью, у него были проблемы с обнаружением из-за характерных сигнатур пакетов. AmneziaWG решает эту проблему за счет использования более совершенных методов обфускации, благодаря чему его трафик сливается с обычным интернет-трафиком. Таким образом, AmneziaWG сохраняет высокую производительность оригинального протокола, добавляя при этом дополнительный уровень скрытности, что делает его отличным выбором для тех, кому нужно быстрое и незаметное VPN-соединение. @@ -3162,9 +4737,8 @@ This means that AmneziaWG keeps the fast performance of the original while addin Файловое хранилище для безопасного хранения данных - Sftp service - SFTP-сервис + SFTP-сервис @@ -3217,6 +4791,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Нет совпадений + Unknown error Неизвестная ошибка @@ -3226,25 +4801,189 @@ This means that AmneziaWG keeps the fast performance of the original while addin error 0x%1: %2 Ошибка 0x%1: %2 + + + SFTP service + SFTP-сервис + + + + + + SOCKS5 proxy server + Прокси-сервер SOCKS5 + + + + vmess:// url is invalid + vmess:// URL-адрес недействителен + + + + Invalid streamSettings protocol: + Неверный протокол streamSettings: + + + + Unknown transport method: + Неизвестный метод транспорта: + + + + VMess string should start with 'vmess://' + Строка VMess должна начинаться с 'vmess://' + + + + VMess string should be a valid base64 string + Строка VMess должна быть действительной base64-строкой + + + + JSON should not be empty + JSON не должен быть пустым + + + + VLESS link should start with vless:// + Ссылка VLESS должна начинаться с vless:// + + + + link parse failed: %1 + не удалось выполнить разбор ссылки: %1 + + + + empty host + пустой хост + + + + missing port + отсутствует порт + + + + missing uuid + отсутствует UUID + + + + Invalid ssd link: json: field %1 must exist + Неверная SSD-ссылка: JSON: поле %1 должно существовать + + + + Invalid ssd link: json: field %1 must be valid port number + Неверная SSD-ссылка: JSON: поле %1 должно быть действительным номером порта + + + + Invalid ssd link: json: field %1 must be of type 'string' + Неверная SSD-ссылка: JSON: поле %1 должно иметь тип 'string' + + + + Invalid ssd link: json: field %1 must be an array + Неверная SSD-ссылка: JSON: поле %1 должно быть массивом + + + + Skipping invalid ssd server: server must be an object + Пропуск недействительного SSD-сервера: сервер должен быть объектом + + + + Skipping invalid ssd server: missing required field %1 + Пропуск недействительного SSD-сервера: отсутствует обязательное поле %1 + + + + Skipping invalid ssd server: field %1 should be of type 'string' + Пропуск недействительного SSD-сервера: поле %1 должно иметь тип 'string' + + + + Invalid ssd link: should begin with ssd:// + Неверная SSD-ссылка: должна начинаться с ssd:// + + + + Invalid ssd link: base64 parse failed + Неверная SSD-ссылка: не удалось выполнить разбор base64 + + + + Invalid ssd link: json parse failed + Неверная SSD-ссылка: не удалось выполнить разбор JSON + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + Неверная SSD-ссылка: шифрование rc4-md5 не поддерживается v2ray-core + + + + SS URI is too short + SS URI слишком короткий + + + + + Can't find the colon separator between method and password + Невозможно найти разделитель-двоеточие между методом и паролем + + + + Can't find the at separator between password and hostname + Невозможно найти разделитель-собаку между паролем и именем хоста + + + + Can't find the colon separator between hostname and port + Невозможно найти разделитель-двоеточие между именем хоста и портом + + + + RenameServerDrawer + + + Server name + Имя сервера + + + + Save + Сохранить + SelectLanguageDrawer - + Choose language Выберите язык + + ServersListView + + + Unable change server while there is an active connection + Невозможно изменить сервер во время активного соединения + + Settings - + Server #1 Сервер #1 - - + + Server Сервер @@ -3252,17 +4991,16 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + All settings have been reset to default values Все настройки сброшены до значений по умолчанию - Cached profiles cleared - Закэшированные профили очищены + Закэшированные профили очищены - + Backup file is corrupted Файл резервной копии поврежден @@ -3270,39 +5008,39 @@ This means that AmneziaWG keeps the fast performance of the original while addin ShareConnectionDrawer - - + + Save AmneziaVPN config Сохранить конфигурацию AmneziaVPN - + Share Поделиться - + Copy Скопировать - - + + Copied Скопировано - + Copy config string Скопировать строку конфигурации - + Show connection settings Показать настройки подключения - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код" @@ -3325,27 +5063,27 @@ This means that AmneziaWG keeps the fast performance of the original while addin Сайт удален: %1 - + Can't open file: %1 Невозможно открыть файл: %1 - + Failed to parse JSON data from file: %1 Не удалось разобрать JSON-данные из файла: %1 - + The JSON data is not an array in file: %1 JSON-данные не являются массивом в файле: %1 - + Import completed Импорт завершен - + Export completed Экспорт завершен @@ -3386,7 +5124,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin TextFieldWithHeaderType - + The field can't be empty Поле не может быть пустым @@ -3394,7 +5132,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Мбит/с @@ -3402,42 +5140,42 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnProtocol - + Unknown Неизвестный - + Disconnected Отключено - + Preparing Подготовка - + Connecting... Подключение... - + Connected Подключено - + Disconnecting... Отключение... - + Reconnecting... Переподключение... - + Error Ошибка @@ -3445,39 +5183,29 @@ This means that AmneziaWG keeps the fast performance of the original while addin amnezia::ContainerProps - Low - Низкий - - - - High - Высокий - - - - Extreme - Экстремальный - - - - I just want to increase the level of my privacy. - Я просто хочу повысить уровень своей приватности. - - - - I want to bypass censorship. This option recommended in most cases. - Я хочу обойти блокировки. Этот вариант рекомендуется в большинстве случаев. - - - - Most VPN protocols are blocked. Recommended if other options are not working. - Большинство VPN-протоколов заблокированы. Рекомендуется, если другие варианты не работают. + Низкий High Высокий + + Extreme + Экстремальный + + + I just want to increase the level of my privacy. + Я просто хочу повысить уровень своей приватности. + + + I want to bypass censorship. This option recommended in most cases. + Я хочу обойти блокировки. Этот вариант рекомендуется в большинстве случаев. + + + Most VPN protocols are blocked. Recommended if other options are not working. + Большинство VPN-протоколов заблокированы. Рекомендуется, если другие варианты не работают. + Medium Средний @@ -3494,16 +5222,26 @@ This means that AmneziaWG keeps the fast performance of the original while addin I just want to increase the level of privacy Хочу просто повысить уровень приватности + + + Automatic + Автоматическая + + + + AmneziaWG protocol will be installed. It provides high connection speed and ensures stable operation even in the most challenging network conditions. + Будет установлен протокол AmneziaWG. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях. + main2 - + Private key passphrase Парольная фраза для закрытого ключа - + Save Сохранить diff --git a/client/translations/amneziavpn_uk_UA.ts b/client/translations/amneziavpn_uk_UA.ts index f968793a..3709e30a 100644 --- a/client/translations/amneziavpn_uk_UA.ts +++ b/client/translations/amneziavpn_uk_UA.ts @@ -1,6 +1,14 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + AmneziaApplication @@ -24,91 +32,149 @@ VPN Підключено + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + Звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео. Працює для будь-яких сайтів. Швидкість до %1 MBit/s + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + VPN для доступу до заблокованих сайтів у регіонах з високим рівнем інтернет-цензури. + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + Amnezia Premium - звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео у високій роздільній здатності. Працює для всіх вебсайтів, навіть у країнах з найвищим рівнем інтернет-цензури. + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + Amnezia Free — це безкоштовний VPN для обходу блокувань у країнах з високим рівнем інтернет-цензури + + + + %1 MBit/s + %1 MBit/s + + + + %1 days + %1 днів + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + Лише популярні сайти, які заблоковані у вашому регіоні, будуть відкриватись за допомогою VPN підключення (Instagram, Facebook, Twitter та ін.). Звичайні сайти будуть відкриватися без використання VPN, <a href="%1/free" style="color: #FBB26A;">більш детально на нашому сайті.</a> + + + + Free + Безкоштовно + + + + %1 $/month + %1 $/місяць + + AppSplitTunnelingController Application added: %1 - + Застосунок додано: %1 The application has already been added - + Застосунок вже додано - + The selected applications have been added - + Вибрані застосунки додані - + Application removed: %1 - + Застосунок видалено: %1 ConnectButton - + Unable to disconnect during configuration preparation - + Неможливо відключитися під час підготовки конфігурації ConnectionController - + VPN Protocols is not installed. Please install VPN container at first VPN протоколи не встановлено. Будь-ласка, встановіть VPN контейнер - + unable to create configuration - + Неможливо створити конфігурацію - + Connecting... Підключення... - + Connected Підключено - + Preparing... - + Підготовка... - + Settings updated successfully, reconnnection... Налаштування оновлено, підключення... - + Settings updated successfully - Налаштування оновлено. + Налаштування оновлено - + + The selected protocol is not supported on the current platform + Вибраний протокол не підтримується на цьому пристрої + + + Reconnecting... Перепідключення... - - - - + + + + Connect Підключитись - + Disconnecting... Відключаємось... @@ -126,7 +192,7 @@ Налаштувати свій сервер - + Open config file, key or QR code Відкрити файл конфігурації, ключ або QR код @@ -149,7 +215,7 @@ &Вставити - + &SelectAll &Вибрати все @@ -157,22 +223,20 @@ ExportController - Access error! - Помилка доступу! + Помилка доступу! HomeContainersListView - + Unable change protocol while there is an active connection Неможливо змінити протокол при активному підключенні - The selected protocol is not supported on the current platform - Вибраний протокол не підтримується на цьому пристрої + Вибраний протокол не підтримується на цьому пристрої Reconnect via VPN Procotol: @@ -192,16 +256,16 @@ Дозволяє підключатись до одних сайтів та застосунків через захищене з'єднання, а іншим в обхід нього - + Split tunneling on the server Роздільне тунелювання на сервері - + Enabled Can't be disabled for current server Увімкнено. -Не може бути вимкнено для даного сервера. +Не може бути вимкнено для даного сервера @@ -210,18 +274,18 @@ Can't be disabled for current server - + Enabled Увімкнено - + Disabled Вимкнено - + App-based split tunneling Роздільне тунелювання застосунків @@ -229,99 +293,126 @@ Can't be disabled for current server ImportController - Unable to open file - + Неможливо відкрити файл - - Invalid configuration file - + Недійсний файл конфігурації - + Scanned %1 of %2. Відскановано %1 з %2. + + + In the imported configuration, potentially dangerous lines were found: + У імпортованій конфігурації знайдено потенційно небезпечні рядки: + InstallController - + %1 installed successfully. %1 встановлено. - + %1 is already installed on the server. %1 вже встановлено на сервері. - + Added containers that were already installed on the server Додані сервіси і протоколи, які були раніше встановлені на сервері - + Already installed containers were found on the server. All installed containers have been added to the application На сервері знайдені сервіси та протоколи, всі вони додані в застосунок - + Settings updated successfully Налаштування оновлено - + Server '%1' was rebooted Сервер '%1' перезавантажено - + Server '%1' was removed Сервер '%1' був видалений - + All containers from server '%1' have been removed Всі сервіси та протоколи були видалені з сервера '%1' - + %1 has been removed from the server '%2' %1 був видалений з сервера '%2' - - %1 cached profile cleared - + + Api config removed + Конфігурацію API видалено - + + %1 cached profile cleared + Кешований профіль %1 очищено + + + Please login as the user Буль-ласка, увійдіть в систему від імені користувача - + Server added successfully Сервер додано + + + %1 installed successfully. + %1 встановлено успішно. + + + + API config reloaded + Конфігурацію API перезавантажено + + + + Successfully changed the country of connection to %1 + Успішно змінено країну підключення на %1 + InstalledAppsDrawer - + Choose application - + Виберіть застосунок - + + application name + назва застосунку + + + Add selected - + Додати вибране @@ -345,28 +436,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN AmneziaVPN - + VPN Connected VPN Підключено - + VPN Disconnected VPN Вимкнено - + AmneziaVPN notification Сповіщення AmneziaVPN - + Unsecured network detected: Знайдена не захищена мережа: @@ -374,80 +465,205 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 Видалення сервісів з %1 - + Usually it takes no more than 5 minutes Зазвичай, це займає не більше 5 хвилин - PageHome + PageDevMenu - - Logging enabled + + Gateway endpoint - + + Dev gateway environment + + + + + PageHome + + + Logging enabled + Логування увімкнено + + + Split tunneling enabled - Роздільне тунелювання увімкнено + Роздільне тунелювання увімкнено - + Split tunneling disabled - Роздільне тунелювання вимкнено + Роздільне тунелювання вимкнено - + VPN protocol VPN протокол - + Servers Сервери - Unable change server while there is an active connection - Не можна змінити сервер при активному підключенні + Не можна змінити сервер при активному підключенні + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + налаштування AmneziaWG + + + + MTU + MTU + + + + Server settings + + + + + Port + Порт + + + + Save + Зберегти + + + + Save settings? + Зберегти налаштування? + + + + Only the settings for this device will be changed + + + + + Continue + Продовжити + + + + Cancel + Відмінити + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення PageProtocolAwgSettings - + AmneziaWG settings налаштування AmneziaWG - + + VPN address subnet + VPN address subnet + + + Port Порт - - MTU + + Jc - Junk packet count - + + Jmin - Junk packet minimum size + + + + + Jmax - Junk packet maximum size + + + + + S1 - Init packet junk size + + + + + S2 - Response packet junk size + + + + + H1 - Init packet magic header + + + + + H2 - Response packet magic header + + + + + H4 - Transport packet magic header + + + + + H3 - Underload packet magic header + + + + Save - Зберегти + Зберегти - + + The values of the H1-H4 fields must be unique + Значення полів H1-H4 мають бути унікальними + + + + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + Значення поля S1 + розмір повідомлення ініціалізації (148) не має бути рівним значенню S2 + розмір повідомлення відповіді (92) + + + Save settings? - + Зберегти налаштування? - + All users with whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення Remove AmneziaWG @@ -466,12 +682,12 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - + Continue Продовжити - + Cancel Відмінити @@ -483,12 +699,12 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Налаштування Cloak - + Disguised as traffic from Замаскувати трафік під @@ -498,15 +714,20 @@ Already installed containers were found on the server. All installed containers Порт - - + + Cipher Шифрування - + Save - Зберегти + Зберегти + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення Save and Restart Amnezia @@ -527,7 +748,7 @@ Already installed containers were found on the server. All installed containers VPN address subnet - + VPN address subnet @@ -535,157 +756,162 @@ Already installed containers were found on the server. All installed containers Мережевий притокол - + Port Порт - + Auto-negotiate encryption Автоматично отримувати шифрування - + Hash Хеш - + SHA512 SHA512 - + SHA384 SHA384 - + SHA256 SHA256 - + SHA3-512 SHA3-512 - + SHA3-384 SHA3-384 - + SHA3-256 SHA3-256 - + whirlpool whirlpool - + BLAKE2b512 BLAKE2b512 - + BLAKE2s256 BLAKE2s256 - + SHA1 SHA1 - + Cipher Шифрування - + AES-256-GCM AES-256-GCM - + AES-192-GCM AES-192-GCM - + AES-128-GCM AES-128-GCM - + AES-256-CBC AES-256-CBC - + AES-192-CBC AES-192-CBC - + AES-128-CBC AES-128-CBC - + ChaCha20-Poly1305 ChaCha20-Poly1305 - + ARIA-256-CBC ARIA-256-CBC - + CAMELLIA-256-CBC CAMELLIA-256-CBC - + none none - + TLS auth TLS авторизація - + Block DNS requests outside of VPN Блокувати DNS запити за межами VPN тунеля - + Additional client configuration commands Додаткові команди конфігурації клієнта - - + + Commands: Команди: - + Additional server configuration commands Додаткові команти конфігурації сервера - + Save - Зберегти + Зберегти + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення Remove OpenVPN @@ -719,34 +945,34 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings налаштування - + Show connection options Показати параметри підключення - + Connection options %1 Параметри підключення %1 - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + All users with whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. All users with whom you shared a connection will no longer be able to connect to it. @@ -757,12 +983,12 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - + Continue Продовжити - + Cancel Відмінити @@ -770,25 +996,30 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Налаштування Shadowsocks - + Port Порт - - + + Cipher Шифрування - + Save - Зберегти + Зберегти + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення Save and Restart Amnezia @@ -796,44 +1027,131 @@ Already installed containers were found on the server. All installed containers - PageProtocolWireGuardSettings + PageProtocolWireGuardClientSettings - + WG settings - + + MTU + MTU + + + + Server settings + + + + Port Порт - - MTU + + Save + Зберегти + + + + Save settings? + Зберегти налаштування? + + + + Only the settings for this device will be changed - + + Continue + Продовжити + + + + Cancel + Відмінити + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення + + + + PageProtocolWireGuardSettings + + + WG settings + + + + + VPN address subnet + VPN address subnet + + + + Port + Порт + + + MTU + MTU + + + Save - Зберегти + Зберегти + + + + Save settings? + Зберегти налаштування? + + + + All users with whom you shared a connection with will no longer be able to connect to it. + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. + + + + Continue + Продовжити + + + + Cancel + Відмінити + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення PageProtocolXraySettings - + XRay settings - + Налаштування XRay - + Disguised as traffic from - Замаскувати трафік під + Замаскувати трафік під - + Save - Зберегти + Зберегти + + + + Unable change settings while there is an active connection + Неможливо змінити налаштування, поки є активне підключення @@ -846,65 +1164,70 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. На вашому сервері встановлено DNS-сервіс, доступ до нього можливо тільки через VPN. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. - Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку + Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку. - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + Continue Продовжити - + Cancel Відмінити + + + Cannot remove AmneziaDNS from running server + Не вдається видалити AmneziaDNS з працюючого сервера + PageServiceSftpSettings - + Settings updated successfully Налаштування оновлено - + SFTP settings Налаштування SFTP - + Host Хост - - - - + + + + Copied Скопійовано - + Port Порт @@ -913,97 +1236,163 @@ Already installed containers were found on the server. All installed containers Логін - + User name - Імя користувача + Імя користувача - + Password Пароль - + Mount folder on device Змонтувати папку з вашого пристрою - + In order to mount remote SFTP folder as local drive, perform following steps: <br> - Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> + Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> - - + + <br>1. Install the latest version of <br>1. Встановіть останню версію - - + + <br>2. Install the latest version of <br>2. Встановіть останню версію - + Detailed instructions Детальні інструкції - Remove SFTP and all data stored there - Видалити SFTP-сховище з усіма даними + Видалити SFTP-сховище з усіма даними - Remove SFTP and all data stored there? - Видалити SFTP-сховище з усіма даними які там зберігаються? + Видалити SFTP-сховище з усіма даними які там зберігаються? - Continue - Продовжити + Продовжити - Cancel - Відмінити + Відмінити + + + + PageServiceSocksProxySettings + + + Settings updated successfully + Налаштування успішно оновлено + + + + + SOCKS5 settings + Налаштування SOCKS5 + + + + Host + Хост + + + + + + + Copied + Скопійовано + + + + + Port + Порт + + + + User name + User name + + + + + Password + Пароль + + + + Username + Username + + + + + Change connection settings + Змінити налаштування підключення + + + + The port must be in the range of 1 to 65535 + Порт повинен бути в межах від 1 до 65535 + + + + Password cannot be empty + Пароль не може бути порожнім + + + + Username cannot be empty + Ім'я користувача не може бути порожнім PageServiceTorWebsiteSettings - + Settings updated successfully Налаштування оновлено - + Tor website settings Налаштування сайту в мережі Тоr - + Website address Адреса сайту - + Copied Скопійовано - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. + Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. + Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. - + When configuring WordPress set the this onion address as domain. При налаштуванні WordPress, вкажіть цей Onion в якості домена. @@ -1012,60 +1401,61 @@ Already installed containers were found on the server. All installed containers При налаштуванні WordPress, вкажіть цей Onion в якості домена. - Remove website - Видалити сайт + Видалити сайт - The site with all data will be removed from the tor network. - Сайт з усіма даними буде видалено з мережі Tor. + Сайт з усіма даними буде видалено з мережі Tor. - Continue - Продовжити + Продовжити - Cancel - Відмінити + Відмінити PageSettings - + Settings Налаштування - + Servers Сервери - + Connection Підключення - + Application Застосунок - + Backup Резервне копіювання - + About AmneziaVPN Про AmneziaVPN - + + Dev console + + + + Close application Закрити застосунок @@ -1077,7 +1467,7 @@ Already installed containers were found on the server. All installed containers Підтримайте проект донатом - + Support Amnezia Підтримайте Amnezia @@ -1102,216 +1492,342 @@ Already installed containers were found on the server. All installed containers Показати інші способи на Github - + Amnezia is a free and open-source application. You can support the developers if you like it. - + Amnezia — це безкоштовний додаток з відкритим кодом. Якщо вам подобається цей додаток, ви можете підтримати розробників. - + Contacts Контакти - + Telegram group Група в Telegram - + To discuss features Для дискусій - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn - - Mail - Пошта + + support@amnezia.org + - + Mail + Пошта + + + For reviews and bug reports Для відгуків і повідомлень про помилки - + + Copied + Скопійовано + + + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт - - https://amnezia.org - https://amnezia.org + + Visit official website + - + https://amnezia.org + https://amnezia.org + + + Software version: %1 Версія ПЗ: %1 - + Check for updates Перевірити оновлення - + Privacy Policy + Політика конфіденційності + + + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + PageSettingsApiServerInfo + + + For the region + Для регіону + + + + Price + Ціна + + + + Work period + Період роботи + + + + Valid until + + + + + Speed + Швидкість + + + + Support tag + + + + + Copied + Скопійовано + + + + Reload API config + Перезавантажити конфігурацію API + + + + Reload API config? + Перезавантажити конфігурацію API? + + + + + Continue + Продовжити + + + + + Cancel + Відмінити + + + + Cannot reload API config during active connection + Неможливо перезавантажити конфігурацію API під час активного підключення + + + + Remove from application + Видалити з додатку + + + + Remove from application? + Видалити з додатку? + + + + Cannot remove server during active connection + Неможливо видалити сервер під час активного підключення + + PageSettingsAppSplitTunneling - + + Cannot change split tunneling settings during active connection + Не можна змінити налаштування роздільного тунелювання при підключеному VPN + + + Only the apps from the list should have access via VPN - + Доступ через VPN мають лише програми зі списку - + Apps from the list should not have access via VPN - + Програми зі списку не мають доступ через VPN - + App split tunneling - + Split tunneling для додатка - + Mode - Режим + Режим - + Remove - Видалити + Видалити - + Continue - Продовжити + Продовжити - + Cancel - Відмінити + Відмінити - + application name - + назва додатка - + Open executable file - + Відкрити виконуваний файл - - Executable file (*.*) - + + Executable files (*.*) + Виконувані файли (*.*) PageSettingsApplication - + Application Застосунок - + Allow application screenshots - Дозволити скріншоти в застосунку + Дозволити скріншоти у застосунку - + + Enable notifications + Увімкнути сповіщення + + + + Enable notifications to show the VPN state in the status bar + Увімкнути сповіщення (показує стан VPN у статус барі) + + + Auto start - Автозапуск + Автозапуск - + Launch the application every time the device is starts - Запускати застосунок при старті + Запускати застосунок при старті - + Auto connect - Автопідключення + Автопідключення - + Connect to VPN on app start - Підключення до VPN при старті застосунку + Підключення до VPN при старті застосунку - + Start minimized Запускати в згорнутому вигляді - + Launch application minimized Запускати застосунок в згорнутому вигляді - + Language Мова - + Logging Логування - + Enabled Увімкнено - + Disabled Вимкнено - + Reset settings and remove all data from the application Скинути налаштування і видалити всі дані із застосунку - + Reset settings and remove all data from the application? Скинути налаштування і видалити всі дані із застосунку? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. Всі дані із застосунку будуть видалені, всі встановлені сервіси AmneziaVPN залишаться на сервері. - + Continue Продовжити - + Cancel Відмінити + + + Cannot reset settings during active connection + Неможливо скинути налаштування під час активного підключення + PageSettingsBackup @@ -1320,7 +1836,7 @@ Already installed containers were found on the server. All installed containers Резервне копіювання - + Settings restored from backup file Відновлення налаштувань із бекап файлу @@ -1331,17 +1847,17 @@ Already installed containers were found on the server. All installed containers Back up your configuration - + Резервне копіювання вашої конфігурації You can save your settings to a backup file to restore them the next time you install the application. - Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми + Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми. The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. - + Резервна копія міститиме ваші паролі та приватні ключі для всіх серверів, доданих до AmneziaVPN. Зберігайте цю інформацію у безпечному місці. @@ -1349,56 +1865,61 @@ Already installed containers were found on the server. All installed containers Зробити бекап (резервну копію) - + Save backup file Зберегти бекап файл - - + + Backup files (*.backup) Файли резервної копії (*.backup) - + Backup file saved Бекап файл збережено - + Restore from backup Відновити із бекапа - + Open backup file Відкрити бекап файл - + Import settings from a backup file? Імпортувати налаштування із бекап файлу? - + All current settings will be reset Всі поточні налаштування будуть скинуті - + Continue Продовжити - + Cancel Відмінити + + + Cannot restore backup settings during active connection + Неможливо відновити резервну копію налаштувань під час активного підключення + PageSettingsConnection - + Connection З'єднання @@ -1411,42 +1932,57 @@ Already installed containers were found on the server. All installed containers Підключення до VPN при старті застосунку - + Use AmneziaDNS Використовувати AmneziaDNS - + If AmneziaDNS is installed on the server Якщо він встановлений на сервері - + DNS servers DNS сервер - + When AmneziaDNS is not used or installed - Ці адреси будуть використовуватись коли вимкнений AmneziaDNS + Ці адреси будуть використовуватись коли вимкнений AmneziaDNS - + Allows you to use the VPN only for certain Apps - Дозволяє використовувати VPN тільки для вибраних застосунків + Дозволяє використовувати VPN тільки для вибраних застосунків - + + KillSwitch + KillSwitch + + + + Disables your internet if your encrypted VPN connection drops out for any reason. + Вимикає ваш інтернет, якщо ваше захищене VPN-підключення зникає з будь-якої причини. + + + + Cannot change killSwitch settings during active connection + Неможливо змінити налаштування killSwitch під час активного підключення + + + Site-based split tunneling Роздільне тунелювання сайтів - + Allows you to select which sites you want to access through the VPN Дозволяє доступ до одних сайтів через VPN, а для інших в обхід VPN - + App-based split tunneling Роздільне VPN-тунелювання застосунків @@ -1458,12 +1994,12 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS - Сервер за замовчуванням не підтримує користувацький DNS + Сервер за замовчуванням не підтримує користувацький DNS - + DNS servers DNS сервер @@ -1472,52 +2008,52 @@ Already installed containers were found on the server. All installed containers Ці адреси будуть використовуватись, коли вимкнено або не встановлено AmneziaDNS - + If AmneziaDNS is not used or installed - Якщо AmneziaDNS вимкнено або не встановлено + Якщо AmneziaDNS вимкнено або не встановлено - + Primary DNS Основний DNS - + Secondary DNS Допоміжний DNS - + Restore default Відновити за замовчуванням - + Restore default DNS settings? Відновити налаштування DNS за замовчуванням? - + Continue Продовжити - + Cancel Відмінити - + Settings have been reset Налаштування скинуті - + Save Зберегти - + Settings saved Зберегти налаштування @@ -1525,72 +2061,106 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - + Логування увімкнене. Зверніть увагу, що логування буде автоматично вимкнене через 14 днів, а всі файли журналів будуть видалені. - + Logging Логування - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. - + Увімкнення цієї функції автоматично зберігатиме журнали додатка. За замовчуванням функція логування вимкнена. Увімкніть збереження журналів у випадку збою додатка. - Save logs - Зберегти логи + Зберегти логи - Open folder with logs - Відкрити папку з логами + Відкрити папку з логами - + + Save Зберегти - + + Logs files (*.log) Logs files (*.log) - + + Logs file saved Файл з логами збережено - Save logs to file - Зберегти логи в файл + Зберегти логи в файл - + + Enable logs + + + + Clear logs? Очистити логи? - + Continue Продовжити - + Cancel Відмінити - + Logs have been cleaned up Логи видалено - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs Видалити логи @@ -1598,7 +2168,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerData - + All installed containers have been added to the application Всі встановлені протоколи та сервіси були додані в застосунок @@ -1615,7 +2185,7 @@ Already installed containers were found on the server. All installed containers Видалити кеш Amnezia? - + No new installed containers found Нові встановлені протоколи і сервіси не виявлені @@ -1625,84 +2195,104 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue Продовжити - - - - + + + + Cancel Відмінити - + Check the server for previously installed Amnezia services Проверить сервер на наличие ранее установленных сервисов Amnezia - + Add them to the application if they were not displayed Додати їх в застосунок, якщо вони не були відображені - + Reboot server - Перезавантажити сервер + Перезавантажити сервер - + Do you want to reboot the server? - Ви впевнені, що хочете перезавантажити сервер? + Ви впевнені, що хочете перезавантажити сервер? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? - Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? + Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? - + + Cannot reboot server during active connection + Неможливо перезавантажити сервер під час активного підключення + + + Remove server from application - + Видалити сервер з додатка - + Do you want to remove the server from application? - Ви впевнені, що хочете видалити сервер із застосунку? + Ви впевнені, що хочете видалити сервер із застосунку? - + + Cannot remove server during active connection + Неможливо видалити сервер під час активного підключення + + + Clear server from Amnezia software - + Очистити сервер від програмного забезпечення Amnezia - + Do you want to clear server from Amnezia software? - + Ви дійсно хочете очистити сервер від програмного забезпечення Amnezia? - + All users whom you shared a connection with will no longer be able to connect to it. - + Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. + + + + Cannot clear server from Amnezia software during active connection + Неможливо очистити сервер від програмного забезпечення Amnezia під час активного підключення + + + + Cannot reset API config during active connection + Неможливо скинути конфігурацію API під час активного підключення Do you want to clear server Amnezia-installed services? Ви хочете очистити сервер від сервісів Amnezia? - + Reset API config Скинути API конфігурацію - + Do you want to reset API config? - Ви хочете скинути API конфігурацію + Ви хочете скинути API конфігурацію? Remove this server from the app @@ -1713,7 +2303,7 @@ Already installed containers were found on the server. All installed containers Видалити сервер із застосунку? - + All installed AmneziaVPN services will still remain on the server. Всі встановлені сервіси та протоколи Amnezia все ще залишаться на сервері. @@ -1733,29 +2323,34 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name Імя сервера - + Save Зберегти - + Protocols Протоколи - + Services Сервіси - + Management - + Управління Data @@ -1765,37 +2360,71 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings Налаштування - Clear %1 profile + Очистити профіль %1 + + + + connection settings - - Clear %1 profile? + + Click the "connect" button to create a connection configuration + + + server settings + + + + + Clear profile + + + + + Clear %1 profile? + Очистити профіль %1? + + + + The connection configuration will be deleted for this device only + + + + + Unable to clear %1 profile while there is an active connection + Неможливо очистити профіль %1 під час активного підключення + + + + Cannot remove active container + Неможливо видалити активний контейнер + - + - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + All users with whom you shared a connection will no longer be able to connect to it. Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. @@ -1804,14 +2433,14 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - - + + Continue Продовжити - - + + Cancel Відмінити @@ -1819,7 +2448,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers Сервери @@ -1831,32 +2460,32 @@ Already installed containers were found on the server. All installed containers Тільки адреси із списку мають відкриватись через VPN - + Addresses from the list should not be accessed via VPN Адреси із списку не повинні відкриватись через VPN - + Split tunneling Роздільне VPN-тунелювання - + Mode Режим - + Remove Видалити - + Continue Продовжити - + Cancel Відмінити @@ -1865,80 +2494,125 @@ Already installed containers were found on the server. All installed containers Сайт чи IP - + Import / Export Sites Імпорт / Експорт Сайтів - + Only the sites listed here will be accessed through the VPN Тільки адреси зі списку повинні відкриватись через VPN - + Cannot change split tunneling settings during active connection - Не можна змінити налаштування роздільного тунелювання при підключеному VPN + Не можна змінити налаштування роздільного тунелювання при підключеному VPN - + Default server does not support split tunneling function - + - + website or IP - вебсайт або IP + вебсайт або IP - + Import Імпорт - + Save site list Зберегти список сайтів - + Save sites Зберегти - - - + + + Sites files (*.json) Sites files (*.json) - + Import a list of sites Імпортувати список із сайтами - + Replace site list Замінити список із сайтами - - + + Open sites file Відкрити список із сайтами - + Add imported sites to existing ones Додати імпортовані сайти до існуючих + + PageSetupWizardApiServiceInfo + + + For the region + Для регіону + + + + Price + Ціна + + + + Work period + Період роботи + + + + Speed + Швидкість + + + + Features + Особливості + + + + Connect + Підключитись + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + VPN від Amnezia + + + + Choose a VPN service that suits your needs. + Виберіть VPN-сервіс, який відповідає вашим потребам. + + PageSetupWizardConfigSource - Server connection - Підключення до сервера + Підключення до сервера Do not use connection code from public sources. It may have been created to intercept your data. @@ -1949,39 +2623,150 @@ It's okay as long as it's from someone you trust. Все в порядку, якщо ви використовуєте код, яким поділився користувач, якому ви довіряєте. - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - Не використовуйте код підключення з загальнодоступних джерел. Можливо, його було створено для перехоплення ваших даних. + Не використовуйте код підключення з загальнодоступних джерел. Можливо, його було створено для перехоплення ваших даних. - What do you have? - Виберіть що у вас є + Виберіть що у вас є - + File with connection settings Файл з налаштуваннями підключення - File with connection settings or backup - Файл з налаштуваннями підключення або бекап + Файл з налаштуваннями підключення або бекап - + + Connection + Підключення + + + + Settings + Налаштування + + + + Enable logs + + + + + Support tag + + + + + Copied + Скопійовано + + + + Insert the key, add a configuration file or scan the QR-code + Вставте ключ, додайте файл конфігурації або відскануйте QR-код + + + + Insert key + Вставити ключ + + + + Insert + Вставити + + + + Continue + Продовжити + + + + Other connection options + Інші параметри підключення + + + + Site Amnezia + + + + + VPN by Amnezia + VPN від Amnezia + + + + Connect to classic paid and free VPN services from Amnezia + Підключайтеся до звичайних платних та безкоштовних VPN-сервісів від Amnezia + + + + Self-hosted VPN + Self-hosted VPN + + + + Configure Amnezia VPN on your own server + Налаштуйте Amnezia VPN на власному сервері + + + + Restore from backup + Відновити із бекапа + + + + + + + + + Open backup file + Відкрити бекап файл + + + + Backup files (*.backup) + Файли резервної копії (*.backup) + + + + + + + + Open config file Відкрити файл з конфігурацією - + QR code QR-код - + + + + + + + I have nothing + У мене нічого нема + + + + + + + Key as text - Ключ у вигляді тексту + Ключ у вигляді тексту @@ -1991,7 +2776,7 @@ It's okay as long as it's from someone you trust. Підключення до сервера - + Server IP address [:port] Server IP address [:port] @@ -2004,7 +2789,7 @@ It's okay as long as it's from someone you trust. Password / SSH private key - + Continue Продовжити @@ -2015,7 +2800,7 @@ and will not be shared or disclosed to the Amnezia or any third parties і не будуть передані чи розголошені Amnezia або будь-яким третім особам - + Enter the address in the format 255.255.255.255:88 Введіть адресу в форматі 255.255.255.255:88 @@ -2024,42 +2809,52 @@ and will not be shared or disclosed to the Amnezia or any third parties Login to connect via SSH - + Configure your server Налаштувати свій сервер - + 255.255.255.255:22 - + 255.255.255.255:22 - + SSH Username - + SSH Username - + Password or SSH private key - + Пароль або SSH ключ - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties - Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам + Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам - + + How to run your VPN server + Як запустити ваш VPN-сервер + + + + Where to get connection data, step-by-step instructions for buying a VPS + Де отримати дані для підключення: покрокові інструкції з придбання VPS + + + Ip address cannot be empty Поле IP address не може бути пустим - + Login cannot be empty Поле Login не може бути пустим - + Password/private key cannot be empty Поле Password/Private key не може бути пустим @@ -2067,19 +2862,19 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - + What is the level of internet control in your region? Який рівень контроля над інтернетом у вашому регіоні? - + Choose a VPN protocol - + Виберіть протокол VPN - + Skip setup - + Пропустити налаштування Set up a VPN yourself @@ -2090,7 +2885,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Вибрати VPN-протокол - + Continue Продовжити @@ -2102,7 +2897,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardInstalling - + The server has already been added to the application Сервер уже додано в застосунок @@ -2115,33 +2910,33 @@ and will not be shared or disclosed to the Amnezia or any third parties зайнятий встановленням інших протоколів та сервісів. Встановлення Amnezia - + Amnezia has detected that your server is currently Amnezia виявила, що сервер - + busy installing other software. Amnezia installation зайнятий встановленням інших протоколів та сервісів. Встановлення Amnezia - + will pause until the server finishes installing other software буде призупинено, поки сервер не завершить встановлення - + Installing Встановлення - + Cancel installation - Відмінити встановлення + Відмінити встановлення - - + + Usually it takes no more than 5 minutes Зазвичай, займає не більше 5 хвилин @@ -2149,45 +2944,50 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocolSettings - + Installing %1 Встановити %1 - + More detailed Детальніше - + Close Закрити - + Network protocol Мережевий протокол - + Port Порт - + Install Встановити + + + The port must be in the range of 1 to 65535 + Порт повинен бути в межах від 1 до 65535 + PageSetupWizardProtocols - + VPN protocol VPN протокол - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. Виберіть протокол, який вам більше підходить. Пізніше можна встановити інші протоколи і додаткові сервіси, такі як DNS-проксі, TOR-сайт и SFTP. @@ -2195,42 +2995,37 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. - Наведіть камеру на QR-код і утримуйте її протягом декількох секунд. + Наведіть камеру на QR-код і утримуйте її протягом декількох секунд. PageSetupWizardStart - Settings restored from backup file - Відновлення налаштувань із бекап файлу + Відновлення налаштувань із бекап файлу - Free service for creating a personal VPN on your server. - Простий і безкоштовний застосунок для запуска self-hosted VPN з високими вимогами до приватності. + Простий і безкоштовний застосунок для запуска self-hosted VPN з високими вимогами до приватності. - Helps you access blocked content without revealing your privacy, even to VPN providers. - Допомагає отримати доступ до заблокованого вмісту, не повідомляючи про вашу конфіденційність, навіть постачальникам VPN. + Допомагає отримати доступ до заблокованого вмісту, не повідомляючи про вашу конфіденційність, навіть постачальникам VPN. - I have the data to connect - У мене є дані для підключення + У мене є дані для підключення - I have nothing - У мене нічого нема + У мене нічого нема - - https://amnezia.org/instructions/0_starter-guide - + + Let's get started + Почнемо @@ -2256,7 +3051,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Вставити - + Continue Продовжити @@ -2264,7 +3059,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardViewConfig - + New connection Нове підключення @@ -2277,22 +3072,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Не використовуйте код підключення з загальнодоступних джерел. Він може бути створений для перехоплення ваших даних. - + Collapse content Згорнути - + Show content Показати вміст ключа - - Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. - + + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. + Увімкніть обфускацію WireGuard. Це може бути корисним, якщо WireGuard заблокований у вашого провайдера. - + + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. + Використовуйте коди підключення тільки з джерел, яким ви довіряєте. Коди з публічних джерел можуть бути створені для перехоплення ваших даних. + + + Connect Підключитись @@ -2314,7 +3114,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN-Доступ - + Connection З'єднання @@ -2327,8 +3127,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ до керування сервером. Користувач, з яким ви ділитесь повним доступом до підключення, зможе додавати та видаляти протоколи і служби на сервері, а також змінювати налаштування. - - + + Server Сервер @@ -2337,49 +3137,49 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ - + Config revoked - Кофігурацію відкликано + Кофігурацію відкликано - + Connection to Підключення до - + File with connection settings to Файл з налаштуванням доступу до - + Save OpenVPN config Зберегти OpenVPN конфігурацію - + Save WireGuard config Збергти WireGuard конфігурацію - + Save AmneziaWG config - Зберегти AmneziaWG конфігурацію + Зберегти AmneziaWG конфігурацію - + Save Shadowsocks config - Зберегти конфігурацію Shadowsocks + Зберегти конфігурацію Shadowsocks - + Save Cloak config - Зберегти конфігурацію Cloak + Зберегти конфігурацію Cloak - + Save XRay config - + Зберегти конфігурацію XRay @@ -2389,123 +3189,147 @@ and will not be shared or disclosed to the Amnezia or any third parties AmneziaWG native format - нативний формат AmneziaWG + нативний формат AmneziaWG Shadowsocks native format - Shadowsocks нативний формат + Shadowsocks нативний формат Cloak native format - Cloak нативний формат + Cloak нативний формат XRay native format - + XRay нативний формат - + Share VPN Access Поділитись VPN з'єднанням - + Share full access to the server and VPN - Поділитись повним доступом до серверу + Поділитись повним доступом до серверу - + Use for your own devices, or share with those you trust to manage the server. - Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. + Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. - - + + Users - Користувачі + Користувачі - + User name - Ім'я користувача + Ім'я користувача - + Search - Пошук + Пошук - - Creation date: - Дата створення: + + Creation date: %1 + Дата створення: %1 - - Rename - Перейменувати + + Latest handshake: %1 + Останнє з'єднання: %1 - - Client name - + + Data received: %1 + Отримано даних: %1 - - Save - Зберегти - - - - Revoke - Відкликати - - - - Revoke the config for a user - %1? - Відкликати доступ для користувача - %1? + + Data sent: %1 + Відправлено даних: %1 + Allowed IPs: %1 + + + + Creation date: + Дата створення: + + + + Rename + Перейменувати + + + + Client name + Назва клієнта + + + + Save + Зберегти + + + + Revoke + Відкликати + + + + Revoke the config for a user - %1? + Відкликати доступ для користувача - %1? + + + The user will no longer be able to connect to your server. - Користувач більше не зможе підключатись до вашого сервера + Користувач більше не зможе підключатись до вашого сервера - + Continue - Продовжити + Продовжити - + Cancel - Відмінити + Відмінити Full access Повний доступ - + Share VPN access without the ability to manage the server Поділитись доступом до VPN, без можливості керування сервером - - + + Protocol Протокол - - + + Connection format Формат підключення - - + + Share Поділитись @@ -2513,65 +3337,80 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShareFullAccess - + Full access to the server and VPN - Повний доступ до серверу та VPN + Повний доступ до серверу та VPN - + We recommend that you use full access to the server only for your own additional devices. - Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. + Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. - Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. + Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. - - + + Server - Сервер + Сервер - + Accessing - Доступ + Доступ - + File with accessing settings to - Файл з налаштуваннями доступу до + Файл з налаштуваннями доступу до - + Share - Поділитись + Поділитись - + + Access error! + Помилка доступу! + + + Connection to - Підключення до + Підключення до - + File with connection settings to - Файл з налаштуванням доступу до + Файл з налаштуванням доступу до PageStart - + Logging was disabled after 14 days, log files were deleted + Логування було вимкнене через 14 днів, файли журналів були видалені + + + + Settings restored from backup file + Відновлення налаштувань із бекап файлу + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. PopupType - + Close Закрити @@ -2605,12 +3444,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Пароль не знайдено - + Could not open keystore Could not open keystore - + Could not remove private key from keystore Could not remove private key from keystore @@ -2786,27 +3625,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Could not open keystore - + Could not create private key generator Could not create private key generator - + Could not generate new private key Could not generate new private key - + Could not retrieve private key from keystore Could not retrieve private key from keystore - + Could not create encryption cipher Could not create encryption cipher - + Could not encrypt data Could not encrypt data @@ -2820,6 +3659,7 @@ and will not be shared or disclosed to the Amnezia or any third parties + Unknown error Unknown error @@ -2829,72 +3669,77 @@ and will not be shared or disclosed to the Amnezia or any third parties Function not implemented - + + Background service is not running + + + + Server check failed Server check failed - + Server port already used. Check for another software Server port already used. Check for another software - + Server error: Docker container missing Server error: Docker container missing - + Server error: Docker failed Server error: Docker failed - + Installation canceled by user Installation canceled by user - + The user does not have permission to use sudo The user does not have permission to use sudo - + Server error: Packet manager error - + SSH request was denied SSH request was denied - + SSH request was interrupted SSH request was interrupted - + SSH internal error SSH internal error - + Invalid private key or invalid passphrase entered Invalid private key or invalid passphrase entered - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types The selected private key format is not supported, use openssh ED25519 key types or PEM key types - + Timeout connecting to server Timeout connecting to server - + SCP error: Generic failure @@ -2951,22 +3796,22 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp error: No media was in remote drive - + The config does not contain any containers and credentials for connecting to the server Конфігурація не містить контейнерів і облікових даних для підключення до серверу - + Error when retrieving configuration from API - + This config has already been added to the application Ця конфігурація вже була додана в застосунок - + ErrorCode: %1. @@ -2975,117 +3820,157 @@ and will not be shared or disclosed to the Amnezia or any third parties Failed to save config to disk - + OpenVPN config missing OpenVPN config missing - + OpenVPN management server error OpenVPN management server error - + OpenVPN executable missing OpenVPN executable missing - + Shadowsocks (ss-local) executable missing Shadowsocks (ss-local) executable missing - + Cloak (ck-client) executable missing Cloak (ck-client) executable missing - + Amnezia helper service error Amnezia helper service error - + OpenSSL failed OpenSSL failed - + Can't connect: another VPN connection is active Can't connect: another VPN connection is active - + Can't setup OpenVPN TAP network adapter Can't setup OpenVPN TAP network adapter - + VPN pool error: no available addresses VPN pool error: no available addresses - + + Unable to open config file + + + + VPN connection error - QFile error: The file could not be opened + In the response from the server, an empty config was received - QFile error: An error occurred when reading from the file + SSL error occurred - QFile error: The file could not be accessed + Server response timeout on api request - QFile error: An unspecified error occurred + Missing AGW public key - QFile error: A fatal error occurred + Failed to decrypt response payload - QFile error: The operation was aborted + Missing list of available services + + + + + QFile error: The file could not be opened + QFile error: An error occurred when reading from the file + + + + + QFile error: The file could not be accessed + + + + + QFile error: An unspecified error occurred + + + + + QFile error: A fatal error occurred + + + + + QFile error: The operation was aborted + + + + Internal error Internal error - + IPsec IPsec - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - Shadowsocks - маскує VPN-трафік під звичайний веб-трафік, але розпізнається системами аналізу трафіка в деяких регіонах з високим рівнем цензури. + Shadowsocks - маскує VPN-трафік під звичайний веб-трафік, але розпізнається системами аналізу трафіка в деяких регіонах з високим рівнем цензури. - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak - OpenVPN з маскуванням VPN під HTTPS трафік і захистом від active-probing. Підходить для регіонів з самим високим рівнем цензури. + OpenVPN over Cloak - OpenVPN з маскуванням VPN під HTTPS трафік і захистом від active-probing. Підходить для регіонів з самим високим рівнем цензури. - + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + IKEv2/IPsec — сучасний стабільний протокол, який дещо швидший за інші та відновлює підключення після втрати сигналу. Має нативну підтримку на останніх версіях Android та iOS. + + + Create a file vault on your server to securely store and transfer files. - Створіть на сервері файлове сховище для безпечного зберігання та передачі файлів. + Створіть на сервері файлове сховище для безпечного зберігання та передачі файлів. - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -3104,10 +3989,27 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - + Це комбінація протоколу OpenVPN та плагіна Cloak, розроблена спеціально для захисту від блокувань. + +OpenVPN забезпечує безпечне VPN-підключення шляхом шифрування всього інтернет-трафіку між клієнтом і сервером. + +Cloak захищає OpenVPN від виявлення та блокування. + +Cloak може змінювати метадані пакетів так, що повністю маскує VPN-трафік як звичайний веб-трафік і також захищає VPN від виявлення за допомогою активного сканування. Це робить його дуже стійким до виявлення. + +Одразу після отримання першого пакета даних Cloak аутентифікує вхідне підключення. Якщо аутентифікація не вдається, плагін маскує сервер як фальшивий вебсайт, і ваш VPN стає невидимим для систем аналізу. + +Якщо у вашому регіоні екстремальний рівень інтернет-цензури, ми радимо використовувати тільки OpenVPN через Cloak з самого початку роботи з додатком. + + Доступний в AmneziaVPN на всіх платформах + Високе споживання енергії на мобільних пристроях + Гнучкі налаштування + Не розпізнається системами аналізу DPI + Працює через TCP мережевий протокол, порт 443. + - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -3117,18 +4019,30 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - + Відносно новий популярний VPN-протокол з спрощеною архітектурою. +WireGuard забезпечує стабільне VPN-підключення та високу продуктивність на всіх пристроях. Він використовує жорстко закодовані налаштування шифрування. Порівняно з OpenVPN, WireGuard має нижчу затримку та кращу пропускну здатність передачі даних. + +WireGuard дуже чутливий до блокувань через свої чіткі підписи пакетів. На відміну від деяких інших VPN-протоколів, які використовують техніки обфускації, постійні шаблони підписів пакетів WireGuard легше ідентифікуються та можуть бути заблоковані просунутими системами глибокого аналізу пакетів (DPI) та іншими інструментами моніторингу мережі. + +* Доступний в AmneziaVPN на всіх платформах +* Низьке споживання енергії +* Мінімальна кількість налаштувань +* Легко розпізнається системами аналізу DPI, схильний до блокування +* Працює через UDP мережевий протокол. - + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. - + Протокол **REALITY**, сучасна розробка XRay. Спеціально розроблений для протидії найвищим рівням інтернет-цензури завдяки новому підходу до маскування. +REALITY унікально ідентифікує цензорів під час фази TLS-handshake, працюючи як проксі для VPN клієнтів, при цьому перенаправляючи цензорів на справжні вебсайти, такі як google.com, надаючи справжній TLS-сертифікат та інші дані. +Цей функціонал, відрізняє REALITY від подібних технологій, своєю здатністю маскувати веб-трафік у такий такий, що походить із випадкових справжніх сайтів без необхідності спеціальних налаштувань. +На відміну від старіших протоколів, таких як VMess, VLESS та XTLS-Vision transport, продвиуте розпізнавання "Свій — Чужий" REALITY під час TLS-handshake підвищує безпеку та протидіє виявленню складними системами DPI, що використовують активні техніки аналізу. Це робить REALITY надійним рішенням для підтримання інтернет-свободи в середовищах з жорсткою цензурою. - + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -3149,63 +4063,62 @@ While it offers a blend of security, stability, and speed, it's essential t * Працює по мережевому протоколу UDP, порти 500 і 4500. - + DNS Service DNS Сервіс - + SFTP file sharing service Сервіс обміну файлами SFTP - - + + Website in Tor network Веб-сайт в мережі Tor - + AmneziaDNS AmneziaDNS - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. OpenVPN - популярний VPN-протокол, гнучний в налаштуваннях. Має власний протокол оснований на обміні ключами SSL/TLS. - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. WireGuard - новий популярний VPN-протокол, з високою швидістю та низьким енергоспоживанням. Для регіонів з низьким рівнем цензури. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. AmneziaWG - фірмовий протокол Amnezia, оснований на протоколі WireGuard. Такий же швидкий, як і WireGuard, але стійкий до блокувань. Рекомендується для регіонів з високим рівнем цензури. - - - XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. - - - - - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec сучасний стабільний протокол, трішки швидше за інших відновлює підключення. - + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. + XRay with REALITY — підходить для країн з найвищим рівнем інтернет-цензури. Маскування трафіку під веб-трафік на рівні TLS. Захист від виявлення активними методами сканування (active-probing). + + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. + IKEv2/IPsec сучасний стабільний протокол, трішки швидше за інших відновлює підключення. + + + Deploy a WordPress site on the Tor network in two clicks. Розгорніть сайт WordPress в мережі Tor в два кліка. - + Replace the current DNS server with your own. This will increase your privacy level. Замініть DNS-сервер на AmneziaDNS. Це підвищить вашу рівень захищеності в інтернеті. - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -3224,7 +4137,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Може працювати за протоколом TCP і UDP. - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -3239,7 +4152,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * Працює по мережевому протоколу TCP. - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -3248,7 +4161,14 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for For more detailed information, you can find it in the support section under "Create SFTP file storage." - + Після встановлення Amnezia створить + + файл-сховище на вашому сервері. Ви зможете отримати + доступ до нього за допомогою FileZilla та інших SFTP-клієнтів, +а також змонтувати диск на вашому пристрої для безпосереднього доступу до нього. + +Для більш детальної інформації зверніться + до розділу підтримки під заголовком «Створення SFTP файл-сховища». This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for blocking protection. @@ -3308,7 +4228,7 @@ WireGuard дуже вразливий до блокування. На відмі * Працює по протоколу UDP. - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -3337,10 +4257,17 @@ This means that AmneziaWG keeps the fast performance of the original while addin Файлове сховище для безпечного зберігання даних - + SFTP service Сервіс SFTP + + + + + SOCKS5 proxy server + SOCKS5 proxy server + Entry not found @@ -3391,35 +4318,169 @@ This means that AmneziaWG keeps the fast performance of the original while addin No match No match - - - Unknown error - Unknown error - error 0x%1: %2 error 0x%1: %2 + + + vmess:// url is invalid + + + + + Invalid streamSettings protocol: + + + + + Unknown transport method: + + + + + VMess string should start with 'vmess://' + + + + + VMess string should be a valid base64 string + + + + + JSON should not be empty + + + + + VLESS link should start with vless:// + + + + + link parse failed: %1 + + + + + empty host + + + + + missing port + + + + + missing uuid + + + + + Invalid ssd link: json: field %1 must exist + + + + + Invalid ssd link: json: field %1 must be valid port number + + + + + Invalid ssd link: json: field %1 must be of type 'string' + + + + + Invalid ssd link: json: field %1 must be an array + + + + + Skipping invalid ssd server: server must be an object + + + + + Skipping invalid ssd server: missing required field %1 + + + + + Skipping invalid ssd server: field %1 should be of type 'string' + + + + + Invalid ssd link: should begin with ssd:// + + + + + Invalid ssd link: base64 parse failed + + + + + Invalid ssd link: json parse failed + + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + + + + + SS URI is too short + + + + + + Can't find the colon separator between method and password + + + + + Can't find the at separator between password and hostname + + + + + Can't find the colon separator between hostname and port + + SelectLanguageDrawer - + Choose language Выберите язык + + ServersListView + + + Unable change server while there is an active connection + Не можна змінити сервер при активному підключенні + + Settings - + Server #1 Server #1 - - + + Server Server @@ -3427,7 +4488,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + All settings have been reset to default values Всі налаштування були скинуті до значення "По замовчуванню" @@ -3436,7 +4497,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Кеш профілю очищено - + Backup file is corrupted Backup файл пошкодженно @@ -3444,39 +4505,39 @@ This means that AmneziaWG keeps the fast performance of the original while addin ShareConnectionDrawer - - + + Save AmneziaVPN config Зберегти config AmneziaVPN - + Share Поділитись - + Copy Скопіювати - - + + Copied Скопійовано - + Copy config string - Скопіювати стрічку конфігурації + Скопіювати стрічку конфігурації - + Show connection settings - Показати налаштування підключення + Показати налаштування підключення - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для зчитування QR-коду в застосунку Amnezia виберіть "Додати сервер" → "У мене є дані підключенн" → "QR-код, ключ чи файл налаштувань" @@ -3499,27 +4560,27 @@ This means that AmneziaWG keeps the fast performance of the original while addin Сайт видалено %1 - + Can't open file: %1 Неможливо відкрити файл: %1 - + Failed to parse JSON data from file: %1 Не вдалося розібрати JSON-данні із файлу: %1 - + The JSON data is not an array in file: %1 Данні JSON не являються масивом в файлі: %1 - + Import completed Імпорт завершено - + Export completed Експорт завершено @@ -3560,7 +4621,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin TextFieldWithHeaderType - + The field can't be empty Поле не може бути пустим @@ -3568,7 +4629,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Mbps @@ -3576,42 +4637,42 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnProtocol - + Unknown Невідомий - + Disconnected Відключено - + Preparing Підготовка - + Connecting... Підключення... - + Connected Підключено - + Disconnecting... Відключення... - + Reconnecting... Перепідключення... - + Error Помилка @@ -3619,38 +4680,32 @@ This means that AmneziaWG keeps the fast performance of the original while addin amnezia::ContainerProps - + Low Низький - + High Високий - Extreme - Екстремальний + Екстремальний - + I just want to increase the level of my privacy. Я просто хочу підвищити свій рівень безпеки в інтернеті. - + I want to bypass censorship. This option recommended in most cases. Я хочу обійти блокування. Цей варіант рекомендується в більшості випадків. - Most VPN protocols are blocked. Recommended if other options are not working. - Більшість протоколів VPN заблоковано. Рекомендовано, якщо інші варіанти не підходять. - - - High - Високий + Більшість протоколів VPN заблоковано. Рекомендовано, якщо інші варіанти не підходять. Medium @@ -3672,12 +4727,12 @@ This means that AmneziaWG keeps the fast performance of the original while addin main2 - + Private key passphrase Пароль для особистого ключа - + Save Зберегти diff --git a/client/translations/amneziavpn_ur_PK.ts b/client/translations/amneziavpn_ur_PK.ts index 4f3d74b5..95419cba 100644 --- a/client/translations/amneziavpn_ur_PK.ts +++ b/client/translations/amneziavpn_ur_PK.ts @@ -1,6 +1,67 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + + + + + %1 MBit/s + + + + + %1 days + + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + + + + + Free + + + + + %1 $/month + + + AppSplitTunnelingController @@ -14,12 +75,12 @@ یہ ایپلیکیشن پہلے سے شامل کیا گیا ہے - + The selected applications have been added منتخب شدہ ایپلیکیشنز شامل کردی گئی ہیں - + Application removed: %1 ایپلیکیشن ہٹا دی گئی: %1 @@ -27,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation تشکیل کی تیاری کے دوران منقطع ہونا ممکن نہیں ہے @@ -35,61 +96,61 @@ ConnectionController - - - - + + + + Connect جوڑنا - + The selected protocol is not supported on the current platform منتخب کردہ پروٹوکول موجودہ پلیٹ فارم پر تعاون یافتہ نہیں ہے - + VPN Protocols is not installed. Please install VPN container at first وی پی این پروٹوکول انسٹال نہیں ہے,براہ کرم پہلےوی پی این کنٹینر انسٹال کریں - + unable to create configuration تشکیل تیار کرنے میں ناکام - + Connecting... جوڑاجارھاھے.... - + Connected جوڑاجارھاھے - + Reconnecting... دوبارہ جوڑنےکی کوشش... - + Disconnecting... منقطع کرنا... - + Preparing... تیاری کیا جا رہا ہے... - + Settings updated successfully, reconnnection... ترتیب ک ھوگی،دوبارہ جوڑنےکی کوشش... - + Settings updated successfully دوبارہ ترتیب تاذہ کامیاب @@ -97,17 +158,17 @@ ConnectionTypeSelectionDrawer - + Add new connection نیا کنکشن کا اندراج کریں - + Configure your server اپنے سرور کو ترتیب دیں - + Open config file, key or QR code کھولو کنفیگ فاءیل،کی یا کور کوڈ @@ -130,7 +191,7 @@ چ&سپاںکریں - + &SelectAll &تمام منتخب کریں @@ -138,15 +199,14 @@ ExportController - Access error! - رساءی ناممکن! + رساءی ناممکن! HomeContainersListView - + Unable change protocol while there is an active connection موجودہ کنکشن ہونے کے دوران پروٹوکول کو تبدیل کرنے سے قاصر ہے @@ -154,45 +214,45 @@ HomeSplitTunnelingDrawer - + Split tunneling سپلٹ ٹنلنگ - + Allows you to connect to some sites or applications through a VPN connection and bypass others آپ کو VPN کنکشن کے ذریعے کچھ سائٹس یا ایپلیکیشنز سے جڑنے اور دوسروں کو نظرانداز کرنے کی اجازت دیتا ہے - + Split tunneling on the server سرور پر سپلٹ ٹنلنگ - + Enabled Can't be disabled for current server فعال کو موجودہ سرور کے لیے غیر فعال نہیں کیا جا سکتا - + Site-based split tunneling سائٹ پر مبنی ٹونلنگ - - + + Enabled فعال - - + + Disabled فعال نہیں - + App-based split tunneling ایپ پر مبنی سپلٹ ٹونلنگ @@ -200,93 +260,115 @@ Can't be disabled for current server ImportController - Unable to open file - فائل کو کھولنے سے قاصر ہے + فائل کو کھولنے سے قاصر ہے - - Invalid configuration file - غلط کنفیگریشن فائل + غلط کنفیگریشن فائل - + Scanned %1 of %2. سکین%1 کی%2. + + + In the imported configuration, potentially dangerous lines were found: + + InstallController - + %1 installed successfully. %1 کامیابی سےنصب. - + %1 is already installed on the server. %1 پہلے ہی سرور پر انسٹال ہے. - + Added containers that were already installed on the server وہ کنٹینرز شامل کیے گئے جو پہلے سے سرور پر نصب تھے - + Already installed containers were found on the server. All installed containers have been added to the application سرور پر پہلے سے نصب کنٹینرز پائے گئے۔ تمام نصب کنٹینرز کو ایپلی کیشن میں شامل کر دیا گیا ہے - + Settings updated successfully ترتیب کامیابی کے ساتھ اپ ڈیٹ ہو گئی - + Server '%1' was rebooted سرور %1 دوبارہ چالو کیا گیا تھا - + Server '%1' was removed سرور %1 ہٹا دیا گیا تھا - + All containers from server '%1' have been removed سرور '%1' سے تمام کنٹینرز ہٹا دیے گئے ہیں - + %1 has been removed from the server '%2' سرور '%2' سے %1 ہٹا دیا گیا ہے - + + Api config removed + + + + %1 cached profile cleared %1 کیش کردہ پروفائل ختم کر دی گئی - + Please login as the user براہ کرم صارف کے طور پر لاگ ان کریں - + Server added successfully سرور کامیابی سے شامل کیا گیا + + + %1 installed successfully. + + + + + API config reloaded + + + + + Successfully changed the country of connection to %1 + + InstalledAppsDrawer - + Choose application ایپلیکیشن کو منتخب کریں @@ -322,30 +404,30 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN The translation of "AmneziaVPN" in Urdu would be: امنیزیا وی پی ای - + VPN Connected وی پی این متصل ہوگیا - + VPN Disconnected وی پی این منقطع ہوگیا - + AmneziaVPN notification امنیزیا وی پی این کی اطلاعات - + Unsecured network detected: غیر محفوظ نیٹ ورک کا پتہ لگایا گیا ہے: @@ -353,103 +435,217 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 سروسز کو %1 سے ہٹایا جا رہا ہے - + Usually it takes no more than 5 minutes عام طور پر اس میں 5 منٹ سے زیادہ نہیں لگتا ہے + + PageDevMenu + + + Gateway endpoint + + + + + Dev gateway environment + + + PageHome - + Logging enabled لاگنگ فعال ہے - + Split tunneling enabled سپلٹ ٹنلنگ فعال ہے - + Split tunneling disabled سپلٹ ٹنلنگ غیر فعال ہے - + VPN protocol وی پی این پروٹوکول - + Servers سرور - Unable change server while there is an active connection - فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں + فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + امنیزیا وی جی کی ترتیبات + + + + MTU + ام ٹی یو + + + + Server settings + + + + + Port + پورٹ + + + + Save + + + + + Save settings? + ترتیبات محفوظ کریں? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا PageProtocolAwgSettings - + AmneziaWG settings امنیزیا وی جی کی ترتیبات - + Port پورٹ - MTU - ام ٹی یو + ام ٹی یو - + All users with whom you shared a connection with will no longer be able to connect to it. آپ جن لوگوں کے ساتھ آپ نے اس کنکشن کا اشتراک کیا تھا، وہ اس سے مزید جڑ نہیں سکیں گے۔ - + Save محفوظ کریں - + + VPN address subnet + وی پی این ایڈریس سب نیٹ + + + + Jc - Junk packet count + + + + + Jmin - Junk packet minimum size + + + + + Jmax - Junk packet maximum size + + + + + S1 - Init packet junk size + + + + + S2 - Response packet junk size + + + + + H1 - Init packet magic header + + + + + H2 - Response packet magic header + + + + + H4 - Transport packet magic header + + + + + H3 - Underload packet magic header + + + + The values of the H1-H4 fields must be unique H1 تا H4 فیلڈز کی قیمتیں مخصوص ہونی چاہیے - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) S1 + پیغام شروع کار (148) کے فیلڈ کی قیمت S2 + پیغام جواب (92) کے سائز کے برابر نہیں ہونی چاہئے - + Save settings? ترتیبات محفوظ کریں? - + Continue جاری رکھیں - + Cancel منسوخ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -457,33 +653,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings پوشیدہ ترتیب - + Disguised as traffic from کے طور پر ٹریفک کی طرح - + Port پورٹ - - + + Cipher رمز - + Save محفوظ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -491,175 +687,175 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings اوپن وی پی این ترتیبات - + VPN address subnet وی پی این ایڈریس سب نیٹ - + Network protocol نیٹ ورک پروٹوکول - + Port پورٹ - + Auto-negotiate encryption خود کار مذاکرتی رمزنگاری - - + + Hash ہیش - + SHA512 - + SHA384 - + SHA256 - + SHA3-512 - + SHA3-384 - + SHA3-256 - + whirlpool بھنور - + BLAKE2b512 - + BLAKE2s256 - + SHA1 - - + + Cipher رمز - + AES-256-GCM - + AES-192-GCM - + AES-128-GCM - + AES-256-CBC - + AES-192-CBC - + AES-128-CBC - + ChaCha20-Poly1305 - + ARIA-256-CBC - + CAMELLIA-256-CBC - + none کوئی نہیں - + TLS auth TLS توثیق - + Block DNS requests outside of VPN وی پی این کے باہر DNS درخواستوں کو بلاک کریں - + Additional client configuration commands مزید کلائنٹ ترتیباتی احکامات - - + + Commands: احکامات: - + Additional server configuration commands اضافی سرور کنفیگریشن احکامات - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا - + Save احفظ @@ -667,42 +863,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings ترتیبات - + Show connection options کنکشن کے اختیارات دکھائیں - + Connection options %1 کنکشن کے اختیارات %1 - + Remove ہٹائیں - + Remove %1 from server? سرور سے %1 کو ہٹائیں؟ - + All users with whom you shared a connection with will no longer be able to connect to it. آپ جن لوگوں کے ساتھ آپ نے ایک کنکشن شیئر کیا تھا، وہ اب اس سے مزید جڑ نہیں سکیں گے. - + Continue جاری رکھیں - + Cancel منسوخ کریں @@ -710,56 +906,133 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings شیڈو ساکس ترتیبات - + Port پورٹ - - + + Cipher رمز - + Save محفوظ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا + + PageProtocolWireGuardClientSettings + + + WG settings + وائر گارڈ ترتیبات + + + + MTU + ام ٹی یو + + + + Server settings + + + + + Port + پورٹ + + + + Save + + + + + Save settings? + ترتیبات محفوظ کریں? + + + + Only the settings for this device will be changed + + + + + Continue + + + + + Cancel + + + + + Unable change settings while there is an active connection + جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا + + PageProtocolWireGuardSettings - + WG settings وائر گارڈ ترتیبات - + + VPN address subnet + وی پی این ایڈریس سب نیٹ + + + Port پورٹ - - MTU - ام ٹی یو + + Save settings? + ترتیبات محفوظ کریں? - + + All users with whom you shared a connection with will no longer be able to connect to it. + + + + + Continue + + + + + Cancel + + + + MTU + ام ٹی یو + + + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا - + Save محفوظ کریں @@ -767,22 +1040,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings XRay کی ترتیبات - + Disguised as traffic from کے طور پر ٹریفک کی طرح - + Save محفوظ - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -790,39 +1063,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. ایک DNS سروس آپ کے سرور پر انسٹال کی گئی ہے، اور صرف VPN کے ذریعے یہ قابل رسائی ہے. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. ایڈریس وہی ہے جو آپ کے سرور کا پتہ ہے۔ آپ کنکشنز ٹیب کے نیچے سیٹنگز میں DNS کنفیگر کر سکتے ہیں. - + Remove ہٹائیں - + Remove %1 from server? سرور سے %1 کو ہٹا دیں? - - Cannot remove Amnezia DNS from running server + + Cannot remove AmneziaDNS from running server آمنیزیا ڈی این ایس کو چل رہے سرور سے ہٹا نہیں سکتے - + Continue جاری رہے - + Cancel منسوخ کریں @@ -830,185 +1103,254 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully ترتیبات کامیابی سے اپ ڈیٹ ہوگئیں - + SFTP settings ایس ایف ٹی پی ترتیبات - + Host The term "Host" in the context of SFTP (Secure File Transfer Protocol) can be translated into Urdu as: میزبان - - - - + + + + Copied نقل کر دیا گیا - + Port پورٹ - + User name صارف کا نام - + Password پاس ورڈ - + Mount folder on device آلے پر فولڈر ماؤنٹ کریں - + In order to mount remote SFTP folder as local drive, perform following steps: <br> ریموٹ SFTP فولڈر کو لوکل ڈرائیو کے طور پر ماؤنٹ کرنے کے لیے، درج ذیل اقدامات کریں - - + + <br>1. Install the latest version of <br>1کا تازہ ترین ورژن انسٹال کریں - - + + <br>2. Install the latest version of .<br>2کا تازہ ترین ورژن انسٹال کریں ۔ - + Detailed instructions تفصیلی ہدایات - Remove SFTP and all data stored there - کو ہٹا دیں اور وہاں ذخیرہ شدہ تمام ڈیٹا کو ختم کر دیں + کو ہٹا دیں اور وہاں ذخیرہ شدہ تمام ڈیٹا کو ختم کر دیں - Remove SFTP and all data stored there? - SFTP اور وہاں ذخیرہ کردہ تمام ڈیٹا کو ہٹائیں؟ + SFTP اور وہاں ذخیرہ کردہ تمام ڈیٹا کو ہٹائیں؟ - Continue - جاری رہے + جاری رہے - Cancel - منسوخ کریں + منسوخ کریں + + + + PageServiceSocksProxySettings + + + Settings updated successfully + + + + + + SOCKS5 settings + + + + + Host + The term "Host" in the context of SFTP (Secure File Transfer Protocol) can be translated into Urdu as: + +میزبان + + + + + + + Copied + + + + + + Port + پورٹ + + + + User name + صارف کا نام + + + + + Password + پاس ورڈ + + + + Username + + + + + + Change connection settings + + + + + The port must be in the range of 1 to 65535 + + + + + Password cannot be empty + + + + + Username cannot be empty + PageServiceTorWebsiteSettings - + Settings updated successfully ترتیبات کامیابی سے اپ ڈیٹ ہوگئی ہیں - + Tor website settings ویب سائٹ کی ترتیبات - + Website address ویب سائٹ کا پتہ - + Copied نقل کر لیا گیا - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. اپنی اونین ویب سائٹ بنانے کے بعد، ٹور نیٹ ورک کو اسے دستیاب کرنے میں کچھ منٹ لگ سکتے ہیں۔ - + When configuring WordPress set the this onion address as domain. وورڈپریس کو ترتیب دیتے وقت، اس انیون ایڈریس کو ڈومین کے طور پر مقرر کریں. - Remove website - ویب سائٹ کو ہٹا دیں + ویب سائٹ کو ہٹا دیں - The site with all data will be removed from the tor network. - تمام ڈیٹا والی سائٹ کو ٹور نیٹ ورک سے ہٹا دیا جائے گا. + تمام ڈیٹا والی سائٹ کو ٹور نیٹ ورک سے ہٹا دیا جائے گا. - Continue - جاری رکھیں + جاری رکھیں - Cancel - منسوخ کریں + منسوخ کریں PageSettings - + Settings ترتیبات - + Servers سرور - + Connection کنکشن - + Application ایپلیکیشن - + Backup بیک اپ - + About AmneziaVPN AmneziaVPN کے بارے میں - + + Dev console + + + + Close application براہ کرم ایپلیکیشن بند کریں @@ -1016,228 +1358,348 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Amnezia کی حمایت کریں - + Amnezia is a free and open-source application. You can support the developers if you like it. ایمنیزیا ایک مفت اور آزاد سورس ایپلیکیشن ہے۔ آپ اگر اسے پسند کریں تو ڈویلپرز کی حمایت کرسکتے ہیں. - + Contacts رابطے - + Telegram group ٹیلیگرام گروپ - + To discuss features "فیچرز" پر گفتگو کرنے کے لئے - + https://t.me/amnezia_vpn_en - Mail - میل + میل - + + support@amnezia.org + + + + For reviews and bug reports جائزہ اور بگ رپورٹس کے لئے - + + Copied + + + + GitHub گِٹ ہَب - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website ویب سائٹ - - https://amnezia.org - + + Visit official website + - + Software version: %1 سافٹ ویئر ورژن: %1 - + Check for updates اپ ڈیٹس چیک کریں - + Privacy Policy رازداری کی پالیسی + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + + + PageSettingsApiServerInfo + + + For the region + + + + + Price + + + + + Work period + + + + + Valid until + + + + + Speed + + + + + Support tag + + + + + Copied + + + + + Reload API config + + + + + Reload API config? + + + + + + Continue + + + + + + Cancel + + + + + Cannot reload API config during active connection + + + + + Remove from application + + + + + Remove from application? + + + + + Cannot remove server during active connection + چالو کنکشن کے دوران سرور کو ہٹایا نہیں جا سکتا + + PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection فعال کنکشن کے دوران سپلٹ ٹنلنگ کی ترتیبات تبدیل نہیں کی جا سکتیں - Only the Apps listed here will be accessed through the VPN - صرف اس ایپ کو وی پی این کے ذریعے دسترس حاصل کی جائے گی جو یہاں فہرست میں شامل ہے + صرف اس ایپ کو وی پی این کے ذریعے دسترس حاصل کی جائے گی جو یہاں فہرست میں شامل ہے - Apps from the list should not be accessed via VPN - فہرست میں شامل ایپ کو وی پی این کے ذریعے دسترس نہیں کیا جائے گا + فہرست میں شامل ایپ کو وی پی این کے ذریعے دسترس نہیں کیا جائے گا - + + Only the apps from the list should have access via VPN + + + + + Apps from the list should not have access via VPN + + + + App split tunneling ایپ اسپلٹ ٹنلنگ - + Mode موڈ - + Remove ہٹائیں - + Continue جاری رکھیں - + Cancel منسوخ - + application name ایپ کا نام - + Open executable file قابل اجراء فائل کو کھولیں - - Executable file (*.*) + + Executable files (*.*) قابل اجراء فائل (*.*) PageSettingsApplication - + Application ایپلیکیشن - + Allow application screenshots ایپلیکیشن میں تصویریں لینے کی اجازت دیں - + + Enable notifications + + + + + Enable notifications to show the VPN state in the status bar + + + + Auto start خود کار شروع - + Launch the application every time the device is starts جب بھی آلہ چلائے، ایپلیکیشن کو لانچ کریں - + Auto connect خودکار منسلک - + Connect to VPN on app start ایپ شروع ہونے پر VPN سے جڑیں - + Start minimized کمینائز شروع کریں - + Launch application minimized ایپلیکیشن کو کمینائز کر کے لانچ کریں - + Language زبان - + Logging لاگنگ - + Enabled فعال - + Disabled غیر فعال - + Reset settings and remove all data from the application ترتیبات کو دوبارہ ترتیب کریں اور ایپلیکیشن سے تمام ڈیٹا کو ختم کریں - + Reset settings and remove all data from the application? ترتیبات کو دوبارہ ترتیب دیں اور ایپلیکیشن سے تمام ڈیٹا کو ہٹا دیں؟ - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. تمام ترتیبات کو معمولی حالت پر لوٹایا جائے گا۔ سب انسٹال کیے گئے امنیزیا وی پی این سروسزسرورپرموجودرہیںگی. - + Continue جاری رکھیں - + Cancel منسوخ - + Cannot reset settings during active connection چالو کنکشن کے دوران ترتیبات کو دوبارہ ترتیب نہیں دی جا سکتی @@ -1245,78 +1707,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں - + Back up your configuration اپنی ترتیبات کا بیک اپ بنائیں - + You can save your settings to a backup file to restore them the next time you install the application. آپ اپنی ترتیبات کو بیک اپ فائل میں محفوظ کرکے اگلی دفعہ جب آپ ایپلیکیشن کو انسٹال کریں تو انہیں بحال کرنے کے لئے استعمال کرسکتے ہیں۔ - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. بیک اپ میں آپ کے تمام سرورز جنہیں AmneziaVPN میں شامل کیا گیا ہے، ان کے پاسورڈ اور نجی کلید شامل ہوں گے۔ اس معلومات کو ایک محفوظ جگہ میں محفوظ رکھیں. - + Make a backup ایک بیک اپ بنائیں - + Save backup file بیک اپ فائل کو محفوظ کریں - - + + Backup files (*.backup) بیک اپ فائلیں (*.backup) - + Backup file saved بیک اپ فائل محفوظ ہو گئی - + Restore from backup بیک اپ سے بحال کریں - + Open backup file بیک اپ فائل کو کھولیں - + Import settings from a backup file? بیک اپ فائل سے ترتیبات کو درآمد کریں؟ - + All current settings will be reset تمام موجودہ ترتیبات کو دوبارہ ترتیب دیں - + Continue جاری رکھیں - + Cancel منسوخ - + Cannot restore backup settings during active connection چالو کنکشن کے دوران بیک اپ ترتیبات کو دوبارہ قائم نہیں کیا جا سکتا @@ -1324,110 +1786,125 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection کنکشن - + When AmneziaDNS is not used or installed ایمنیزیا ڈی این ایس کو استعمال نہیں کیا گیا ہو یا اسے انسٹال نہیں کیا گیاہے - + Allows you to use the VPN only for certain Apps آپ کو صرف مخصوص ایپلیکیشنز کے لئے وی پی این استعمال کرنے کی اجازت دیتا ہے - + Use AmneziaDNS AmneziaDNS استعمال کریں - + If AmneziaDNS is installed on the server اگر سرور پر AmneziaDNS انسٹال کیا گیا ہو تو - + DNS servers ڈی این ایس سرور - + Site-based split tunneling سائٹ کے بنیادی سپلٹ ٹنلنگ - + Allows you to select which sites you want to access through the VPN آپ کو یہ امکان فراہم کرتا ہے کہ آپ وی پی این کے ذریعہ کس سائٹ کو دسترس دینا چاہتے ہیں وہ منتخب کریں - + App-based split tunneling ایپ کے بنیاد پر سپلٹ ٹنلنگ + + + KillSwitch + + + + + Disables your internet if your encrypted VPN connection drops out for any reason. + + + + + Cannot change killSwitch settings during active connection + + PageSettingsDns - + Default server does not support custom DNS افتراضی سرور کا مخصوص DNS کو سپورٹ نہیں کرتا ہے - + DNS servers ڈی این ایس سرور - + If AmneziaDNS is not used or installed اگر AmneziaDNS استعمال نہیں کیا گیا ہو یا انسٹال نہیں کیا گیا ہو تو - + Primary DNS اولین DNS - + Secondary DNS ثانوی DNS - + Restore default اصل حالت کو بحال کریں - + Restore default DNS settings? اصل DNS ترتیبات کو بحال کریں؟ - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Settings have been reset ترتیبات کو دوبارہ ترتیب دیا گیا ہے - + Save محفوظ - + Settings saved ترتیبات محفوظ ہوگئیں @@ -1435,72 +1912,106 @@ Already installed containers were found on the server. All installed containers PageSettingsLogging - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - لاگنگ فعال ہے۔ یاد رہے کہ لاگوں کو 14 دنوں کے بعد خود بخود غیر فعال کر دیا جائے گا، اور تمام لاگ فائلیں حذف کردی جائیں گی. + لاگنگ فعال ہے۔ یاد رہے کہ لاگوں کو 14 دنوں کے بعد خود بخود غیر فعال کر دیا جائے گا، اور تمام لاگ فائلیں حذف کردی جائیں گی. - + Logging لاگنگ - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. اس فعل کو فعال کرنے سے، ایپلیکیشن کے لاگ خود بخود محفوظ ہوجائیں گے۔ پہلے سے، لاگنگ کی فعالیت غیر فعال ہوتی ہے۔ اگر ایپلیکیشن میں کوئی خرابی ہو، تو لاگ کو بچانا فعال کریں. - Save logs - لاگوں کو محفوظ کریں + لاگوں کو محفوظ کریں - Open folder with logs - فائلوں کے فولڈر کو کھولیں + فائلوں کے فولڈر کو کھولیں - + + Save محفوظ - + + Logs files (*.log) لاگ فائلیں (*.log) - + + Logs file saved لاگ فائل محفوظ ہوگئی - Save logs to file - لاگوں کو فائل میں محفوظ کریں + لاگوں کو فائل میں محفوظ کریں - + + Enable logs + + + + Clear logs? کیا آپ لاگوں کو صاف کرنا چاہتے ہیں؟ - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Logs have been cleaned up تم مسح السجلاتلاگوں کو صاف کر دیا گیا ہے - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs لاگوں کو صاف کریں @@ -1508,22 +2019,22 @@ Already installed containers were found on the server. All installed containers PageSettingsServerData - + All installed containers have been added to the application تمام انسٹال شدہ کنٹینرز کو ایپلیکیشن میں شامل کر دیا گیا ہے - + No new installed containers found کوئی نئے انسٹال شدہ کنٹینرز نہیں ملے - + Do you want to reboot the server? کیا آپ سرور کو دوبارہ چالو کرنا چاہتے ہیں؟ - + Do you want to clear server from Amnezia software? هل تريد حذف الخادم من Amnezia?کیا آپ سرور کو Amnezia سافٹ ویئر سے صاف کرنا چاہتے ہیں؟ @@ -1533,93 +2044,93 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue براہ کرم جاری رکھیں - - - - + + + + Cancel منسوخ - + Check the server for previously installed Amnezia services سرور پر پہلے سے انسٹال کی گئی Amnezia سروسز کو چیک کریں - + Add them to the application if they were not displayed اگر وہ دکھایا نہیں گیا تو انہیں ایپلیکیشن میں شامل کریں - + Reboot server سرور کو دوبارہ چالو کریں - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? ریبوٹ کا عمل تقریباً 30 سیکنڈ لے سکتا ہے۔ کیا آپ براہ کرم جاری رکھنا چاہتے ہیں؟ - + Cannot reboot server during active connection چالو کنکشن کے دوران سرور کو دوبارہ چالو نہیں کیا جا سکتا - + Remove server from application ایپلیکیشن سے سرور کو ہٹا دیں - + Do you want to remove the server from application? کیا آپ ایپلیکیشن سے سرور کو ہٹانا چاہتے ہیں؟ - + Cannot remove server during active connection چالو کنکشن کے دوران سرور کو ہٹایا نہیں جا سکتا - + All users whom you shared a connection with will no longer be able to connect to it. آپ نے اپنی کنکشن کا اشتراک دینے والے تمام صارفین کو اس سے جڑنے کی اجازت نہیں ہوگی. - + Cannot clear server from Amnezia software during active connection چالو کنکشن کے دوران سرور کو ایمنیزیا سافٹ ویئر سے صاف کرنا ممکن نہیں - + Reset API config API کونفیگ کو دوبارہ ترتیب دیں - + Do you want to reset API config? کیا آپ API کونفیگ کو دوبارہ ترتیب دینا چاہتے ہیں؟ - + Cannot reset API config during active connection چالو کنکشن کے دوران API ترتیبات کو دوبارہ ترتیب نہیں دی جا سکتی - + All installed AmneziaVPN services will still remain on the server. سرور پر تمام انسٹال شدہ AmneziaVPN سروسز محفوظ رہیں گے. - + Clear server from Amnezia software Amnezia سافٹ ویئر کو سرور سے صاف کریں @@ -1627,27 +2138,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name سرور کا نام - + Save محفوظ - + Protocols پروٹوکولات - + Services خدمات - + Management مینجمنٹ @@ -1655,27 +2171,26 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings ترتیبات - Clear %1 profile - %1 پروفائل کو صاف کریں + %1 پروفائل کو صاف کریں - + Clear %1 profile? کیا آپ واقعی %1 پروفائل کو صاف کرنا چاہتے ہیں؟ - + Unable to clear %1 profile while there is an active connection فعال کنکشن کے دوران %1 پروفائل کو صاف نہیں کیا جا سکتا - + Cannot remove active container فعال کنٹینر کو ہٹانا ممکن نہیں @@ -1685,29 +2200,54 @@ Already installed containers were found on the server. All installed containers - + Remove ہٹائیں - + All users with whom you shared a connection will no longer be able to connect to it. آپ نے جن کے ساتھ کنکشن شئیر کیا تھا، ان تمام صارفین کو اس سے جڑنے کی اجازت نہیں ہوگی. - + Remove %1 from server? کیا آپ سرور سے %1 کو ہٹانا چاہتے ہیں؟ - - + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Continue براہ کرم جاری رکھیں - - + + Cancel منسوخ @@ -1715,7 +2255,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers سرور @@ -1723,201 +2263,367 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function افتراضی سرور سپلٹ ٹنلنگ فعال نہیں کرتا - + Addresses from the list should not be accessed via VPN اس فہرست سے پتوں کا وی پی این کے ذریعے دسترس حاصل نہیں کیا جانا چاہئے - + Split tunneling اسپلٹ ٹنلنگ - + Mode موڈ - + Remove ہٹائیں - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Only the sites listed here will be accessed through the VPN صرف یہاں درج کردہ سائٹس وی پی این کے ذریعے دسترس حاصل کریں گی - + Cannot change split tunneling settings during active connection فعال کنکشن کے دوران سپلٹ ٹنلنگ کی ترتیبات تبدیل نہیں کی جا سکتیں - + website or IP ویب سائٹ یا آئی پی - + Import / Export Sites سائٹس درآمد / برآمد - + Import درآمد - + Save site list سائٹ کی فہرست کو محفوظ کریں - + Save sites سائٹس کو محفوظ کریں - - - + + + Sites files (*.json) سائٹس فائلیں (*.json) - + Import a list of sites ایک فہرست کو درآمد کریں - + Replace site list سائٹ کی فہرست کو بدلیں - - + + Open sites file سائٹس فائل کو کھولیں - + Add imported sites to existing ones آمدہ سائٹس کو موجودہ میں شامل کریں + + PageSetupWizardApiServiceInfo + + + For the region + + + + + Price + + + + + Work period + + + + + Speed + + + + + Features + + + + + Connect + + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + + + + + Choose a VPN service that suits your needs. + + + PageSetupWizardConfigSource - Server connection - سرور کنکشن + سرور کنکشن - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - غیر معتبر ماخذ سے کنکشن کوڈ استعمال نہ کریں، کیونکہ یہ آپ کے ڈیٹا کو منسلک کرنے کے لئے تخلیق کیا گیا ہوسکتا ہے. + غیر معتبر ماخذ سے کنکشن کوڈ استعمال نہ کریں، کیونکہ یہ آپ کے ڈیٹا کو منسلک کرنے کے لئے تخلیق کیا گیا ہوسکتا ہے. - What do you have? - آپ کو کس میں مدد چاہیے؟ + آپ کو کس میں مدد چاہیے؟ - File with connection settings or backup - کنکشن کی ترتیبات یا بیک اپ والی فائل + کنکشن کی ترتیبات یا بیک اپ والی فائل - + + Connection + کنکشن + + + + Settings + ترتیبات + + + + Enable logs + + + + + Support tag + + + + + Copied + + + + + Insert the key, add a configuration file or scan the QR-code + + + + + Insert key + + + + + Insert + داخل کریں + + + + Continue + + + + + Other connection options + + + + + Site Amnezia + + + + + VPN by Amnezia + + + + + Connect to classic paid and free VPN services from Amnezia + + + + + Self-hosted VPN + + + + + Configure Amnezia VPN on your own server + + + + + Restore from backup + بیک اپ سے بحال کریں + + + + + + + + + Open backup file + بیک اپ فائل کو کھولیں + + + + Backup files (*.backup) + بیک اپ فائلیں (*.backup) + + + File with connection settings کنکشن کی ترتیبات والی فائل - + + + + + + Open config file کنفیگ فائل کو کھولیں - + QR code QR کوڈ - + + + + + + + I have nothing + میرے پاس کچھ نہیں ہے + + + + + + + Key as text - متن کے طور پر کلید + متن کے طور پر کلید PageSetupWizardCredentials - + Configure your server اپنے سرور کو ترتیب دیں - + Server IP address [:port] سرور آئی پی پتہ [:پورٹ] - + Continue براہ کرم جاری رکھیں - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties آپ جو ڈیٹا داخل کریں گے وہ بالکل خفیہ رہے گا اور نہ تو امنیزیا یا کسی تیسری شخصیت کے ساتھ اشتراک کیا جائے گا - + 255.255.255.255:22 - + SSH Username ایس ایس ایچ صارف نام - + Password or SSH private key پاس ورڈ یا SSH نجی کلید - + + How to run your VPN server + + + + + Where to get connection data, step-by-step instructions for buying a VPS + + + + Ip address cannot be empty آئی پی پتہ خالی نہیں ہو سکتا - + Enter the address in the format 255.255.255.255:88 ایڈریس درج کریں فارمیٹ 255.255.255.255:88 - + Login cannot be empty لاگ ان نام خالی نہیں ہو سکتا - + Password/private key cannot be empty پاس ورڈ یا نجی کلید خالی نہیں ہو سکتی @@ -1925,22 +2631,22 @@ Already installed containers were found on the server. All installed containers PageSetupWizardEasy - + What is the level of internet control in your region? آپ کے علاقے میں انٹرنیٹ کنٹرول کا سطح کیا ہے؟ - + Choose a VPN protocol ایک VPN پروٹوکول کا انتخاب کریں - + Skip setup ترتیب چھوڑیں - + Continue جاری رکھیں @@ -1948,38 +2654,38 @@ Already installed containers were found on the server. All installed containers PageSetupWizardInstalling - - + + Usually it takes no more than 5 minutes عموماً یہ 5 منٹ سے زیادہ نہیں لیتا - + The server has already been added to the application سرور پہلے ہی ایپلیکیشن میں شامل کر دیا گیا ہے - + Amnezia has detected that your server is currently ایمنیزیا نے دریافت کیا ہے کہ آپ کا سرور موجودہ - + busy installing other software. Amnezia installation مصروف ہے اور دوسرے سافٹ ویئر کی انسٹالیشن کر رہا ہے - + Cancel installation انسٹالیشن منسوخ - + will pause until the server finishes installing other software دوسرے سافٹ ویئر کی انسٹالیشن ختم ہونے تک انتظار کرے گا - + Installing انسٹال ہو رہی ہے @@ -1987,45 +2693,50 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocolSettings - + Installing %1 %1 کی انسٹالیشن - + More detailed مزید تفصیلات والا - + Close بند - + Network protocol نیٹ ورک پروٹوکول - + Port پورٹ - + Install انسٹال + + + The port must be in the range of 1 to 65535 + + PageSetupWizardProtocols - + VPN protocol وی پی این پروٹوکول - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. آپ کے لئے سب سے زیادہ اہم پروٹوکول کا انتخاب کریں۔ بعد میں، آپ دوسرے پروٹوکول اور اضافی خدمات، جیسے DNS پراکسی اور SFTP، انسٹال کر سکتے ہیں۔ @@ -2033,7 +2744,7 @@ Already installed containers were found on the server. All installed containers PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. کیمرے کو QR کوڈ پر موجود کریں اور کچھ سیکنڈ کے لئے رکھیں. @@ -2041,60 +2752,55 @@ Already installed containers were found on the server. All installed containers PageSetupWizardStart - Settings restored from backup file - ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں + ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں - Free service for creating a personal VPN on your server. - آپ کے سرور پر ایک ذاتی وی پی این بنانے کے لئے مفت خدمات. + آپ کے سرور پر ایک ذاتی وی پی این بنانے کے لئے مفت خدمات. - Helps you access blocked content without revealing your privacy, even to VPN providers. - آپ کو ریاست کردہ مواد تک رسائی فراہم کرتا ہے بغیر آپ کے خصوصیت کو وی پی این فراہم کرنے والوں تک بھی ظاہر نہیں کرتا. + آپ کو ریاست کردہ مواد تک رسائی فراہم کرتا ہے بغیر آپ کے خصوصیت کو وی پی این فراہم کرنے والوں تک بھی ظاہر نہیں کرتا. - I have the data to connect - میرے پاس اس کنکشن کے لئے ڈیٹا موجود ہے + میرے پاس اس کنکشن کے لئے ڈیٹا موجود ہے - I have nothing - میرے پاس کچھ نہیں ہے + میرے پاس کچھ نہیں ہے - - https://amnezia.org/instructions/0_starter-guide - + + Let's get started + PageSetupWizardTextKey - + Connection key کنکشن کی کلید - + A line that starts with vpn://... ایک لائن جو vpn:// سے شروع ہوتی ہے... - + Key کلید - + Insert داخل کریں - + Continue جاری رہنے دیں @@ -2102,32 +2808,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardViewConfig - + New connection نیا کنکشن - + Collapse content مواد کو غیر فعال کریں - + Show content مواد دکھائیں - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. وائر گارڈ کی غلط شناخت کو بروئے کار لانے کے لئے وائر گارڈ غلط شناخت کو فعال کریں۔ آپ کے پرووائیڈر پر وائر گارڈ بند ہونے کی صورت میں یہ کار آمد ہو سکتی ہے۔ - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. صرف ان ماخذ سے کنکشن کوڈ استعمال کریں جن پر آپ کو اعتماد ہو۔ عوامی ماخذوں سے کوڈز آپ کے ڈیٹا کو منسلک کرنے کے لیے بنائے گئے ہو سکتے ہیں. - + Connect کنکٹ @@ -2135,32 +2841,32 @@ Already installed containers were found on the server. All installed containers PageShare - + Save OpenVPN config اوپن وی پی این کی ترتیبات کو محفوظ کریں - + Save WireGuard config وائر گارڈ کی ترتیبات کو محفوظ کریں - + Save AmneziaWG config ایمنیزیا ڈبلیو جی کی ترتیبات کو محفوظ کریں - + Save Shadowsocks config شیڈو ساکس کی ترتیبات کو محفوظ کریں - + Save Cloak config چادر کی ترتیبات کو محفوظ کریں - + Save XRay config "XRay کنفیگ کو محفوظ کریں @@ -2170,9 +2876,8 @@ Already installed containers were found on the server. All installed containers AmneziaVPN ایپ کے لئے - OpenVpn native format - OpenVPN کا اصل فارمیٹ + OpenVPN کا اصل فارمیٹ @@ -2200,127 +2905,156 @@ Already installed containers were found on the server. All installed containers ایکس رے کا نیٹویٹ فارمیٹ - + Share VPN Access VPN دسترسی شیئر - + Share full access to the server and VPN سرور اور وی پی این کے لئے مکمل دسترسی کو شیئر کریں - + Use for your own devices, or share with those you trust to manage the server. اپنی خود کی ڈیوائسز کے لئے استعمال کریں، یا ان لوگوں کے ساتھ شیئر کریں جن پر آپ کا بھروسہ ہو کہ وہ سرور کو منظم کر سکیں. - - + + Users صارفین - + Share VPN access without the ability to manage the server سرور کو منظم کرنے کی صلاحیت کے بغیر وی پی این کی دسترسی شیئر - + Search تلاش - - Creation date: - تخلیق کی تاریخ: + + Creation date: %1 + - + + Latest handshake: %1 + + + + + Data received: %1 + + + + + Data sent: %1 + + + + + Allowed IPs: %1 + + + + Creation date: + تخلیق کی تاریخ: + + + Rename نام تبدیل - + Client name کلائنٹ کا نام - + Save محفوظ - + Revoke واپس لین - + Revoke the config for a user - %1? کیا آپ مستعمل کے لئے کنفیگ کو واپس لینا چاہتے ہیں - %1؟ - + The user will no longer be able to connect to your server. صارف آپ کے سرور سے متصل ہونے کا اختیار نہیں رہے گا. - + Continue جاری رکھیں - + Cancel منسوخ - + Connection کنکشن - - + + Server سرور - + File with connection settings to کنکشن کی ترتیبات کی فائل - - + + Protocol پروٹوکول - + Connection to کنکشن کو - + Config revoked کنفیگ منسوخ - + + OpenVPN native format + + + + User name صارف کا نام - - + + Connection format کنکشن فارمیٹ - - + + Share شیئر @@ -2328,50 +3062,55 @@ Already installed containers were found on the server. All installed containers PageShareFullAccess - + Full access to the server and VPN سرور اور وی پی این کی مکمل رسائی - + We recommend that you use full access to the server only for your own additional devices. ہم آپ کو سرور کا مکمل رسائی صرف اپنی اضافی ڈیوائسز کے لئے استعمال کرنے کی تجویز دیتے ہیں. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. "اگر آپ دوسروں کے ساتھ مکمل رسائی شیئر کریں تو وہ سرور پروٹوکول اور خدمات کو ہٹا سکتے ہیں اور شامل کر سکتے ہیں، جس سے وی پی این تمام صارفین کے لئے غلط کام کرے گا. - - + + Server سرور - + Accessing رسائی - + File with accessing settings to ترتیبات کے ساتھ دسترسی کی فائل - + Share شیئر - + + Access error! + رساءی ناممکن! + + + Connection to کنکشن کو - + File with connection settings to کنکشن کی ترتیبات کی فائل @@ -2379,15 +3118,25 @@ Already installed containers were found on the server. All installed containers PageStart - + Logging was disabled after 14 days, log files were deleted لاگنگ کو 14 دنوں کے بعد غیر فعال کر دیا گیا، لاگ فائلوں کو حذف کر دیا گیا + + + Settings restored from backup file + ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType - + Close بند @@ -2421,12 +3170,12 @@ Already installed containers were found on the server. All installed containers پاس ورڈ نہیں ملا - + Could not open keystore کی اسٹور کھولا نہیں جا سکا - + Could not remove private key from keystore خصوصی کلید کو کی اسٹور سے ہٹانا نہیں ہو سکا @@ -2602,27 +3351,27 @@ Already installed containers were found on the server. All installed containers کی اسٹور کھولنے میں ناکام - + Could not create private key generator پرائیویٹ کلید جنریٹر تخلیق نہیں کیا - + Could not generate new private key نیا نجی کلید تخلیق نہیں کیا جا سکا - + Could not retrieve private key from keystore کی اسٹور سے نجی کلید حاصل نہیں کیا - + Could not create encryption cipher تشکیل تشکیل نہیں کر سکا - + Could not encrypt data ڈیٹا کو محفوظ کرنے میں ناکام @@ -2630,10 +3379,17 @@ Already installed containers were found on the server. All installed containers QObject - + SFTP service ایس ایف ٹی پی سروس + + + + + SOCKS5 proxy server + + No error @@ -2641,6 +3397,7 @@ Already installed containers were found on the server. All installed containers + Unknown error نامعلوم خامی @@ -2650,233 +3407,278 @@ Already installed containers were found on the server. All installed containers فنکشن نافذ نہیں ہوا - + Server check failed سرور کی جانچ ناکام ہوگئی - + Server port already used. Check for another software سرور پورٹ پہلے ہی استعمال ہو چکا ہے۔ دوسرا سافٹ ویئر چیک کریں - + Server error: Docker container missing سرور کی خرابی: ڈوکر کنٹینر غائب ہے - + Server error: Docker failed سرور کی خرابی: ڈوکر ناکام ہو گیا - + Installation canceled by user صارف کے ذریعے انسٹالیشن منسوخ کر دی گئی - + The user does not have permission to use sudo صارف کو sudo استعمال کرنے کی اجازت نہیں ہے - + SSH request was denied SSH درخواست مسترد کر دی گئی - + SSH request was interrupted SSH درخواست میں خلل پڑ - + SSH internal error SSH اندرونی خرابی - + Invalid private key or invalid passphrase entered غلط نجی کلید یا غلط پاسفریز درج کیا گیا - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types منتخب کردہ پرائیویٹ کلیدی فارمیٹ تعاون یافتہ نہیں ہے، openssh ED25519 کلیدی اقسام یا PEM کلیدی اقسام استعمال کریں - + Timeout connecting to server سرور سے منسلک ہونے کا ٹائم آؤٹ - + VPN connection error VPN کنکشن کی خرابی - + Error when retrieving configuration from API آپی سے کنفیگریشن بازیافت کرتے وقت خرابی - + This config has already been added to the application یہ تشکیل پہلے ہی ایپلی کیشن میں شامل کی جا چکی ہے - + ErrorCode: %1. ایرر کوڈ: %1. - + OpenVPN config missing OpenVPN تشکیل غائب ہے - + + Background service is not running + + + + Server error: Packet manager error سرور خطا: پیکیج منیجر خطا - + SCP error: Generic failure ایس سی پی کی خرابی: عام ناکامی - + OpenVPN management server error OpenVPN مینجمنٹ سرور کی خرابی - + OpenVPN executable missing OpenVPN قابل عمل غائب ہے - + Shadowsocks (ss-local) executable missing شیڈو ساکس (ss-local) قابل عمل غائب - + Cloak (ck-client) executable missing Cloak (ck-client) قابل عمل غائب - + Amnezia helper service error ایمنیزیا مددگار سروس کی خرابی - + OpenSSL failed OpenSSL ناکام ہوگیا - + Can't connect: another VPN connection is active منسلک نہیں ہو سکتا: دوسرا VPN کنکشن فعال ہے - + Can't setup OpenVPN TAP network adapter OpenVPN TAP نیٹ ورک اڈاپٹر سیٹ اپ نہیں کر سکتے - + VPN pool error: no available addresses VPN پول کی خرابی: کوئی پتہ دستیاب نہیں ہے - + The config does not contain any containers and credentials for connecting to the server ترتیب میں سرور سے منسلک ہونے کے لیے کوئی کنٹینرز اور اسناد نہیں ہیں + + + Unable to open config file + + + In the response from the server, an empty config was received + + + + + SSL error occurred + + + + + Server response timeout on api request + + + + + Missing AGW public key + + + + + Failed to decrypt response payload + + + + + Missing list of available services + + + + QFile error: The file could not be opened QFile کی خرابی: فائل کو نہیں کھولا جا سکا - + QFile error: An error occurred when reading from the file کیو فائل کی خرابی: فائل سے پڑھتے وقت ایک خرابی پیش آگئی - + QFile error: The file could not be accessed QFile کی خرابی: فائل تک رسائی نہیں ہو سکی - + QFile error: An unspecified error occurred کیو فائل میں خرابی: ایک غیر متعینہ خرابی پیش آگئی - + QFile error: A fatal error occurred کیو فائل میں خرابی: ایک مہلک خرابی پیش آگئی - + QFile error: The operation was aborted کیو فائل کی خرابی: آپریشن روک دیا گیا تھا - + Internal error داخلی خامی - + IPsec آئی پی ایس سی - - + + Website in Tor network ٹور نیٹ ورک میں ویب سائٹ - + AmneziaDNS ایمنیزیا ڈی این ایس - + SFTP file sharing service SFTP فائل شیئرنگ سروس - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. اوپن وی پی این لچکدار ترتیب کے اختیارات کے ساتھ سب سے زیادہ مقبول VPN پروٹوکول ہے۔ یہ کلیدی تبادلے کے لیے SSL/TLS کے ساتھ اپنا سیکیورٹی پروٹوکول استعمال کرتا ہے۔ - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. شیڈو ساکس - VPN ٹریفک کو ماسک کرتا ہے، جو اسے عام ویب ٹریفک جیسا بناتا ہے، لیکن اسے کچھ انتہائی سنسر والے علاقوں میں تجزیہ کے نظام کے ذریعے پہچانا جا سکتا ہے. - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. اوپن وی پی این اوور کلوک - اوپن وی پی این کے ساتھ وی پی این کو ویب ٹریفک کے طور پر چھپانا اور ایکٹیو پروبنگ ڈٹیکشن کے خلاف تحفظ۔ سنسرشپ کی اعلی ترین سطح والے خطوں میں بلاکنگ کو نظرانداز کرنے کے لیے مثالی. - + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. حقیقت کے ساتھ ایکسرےواقعیت کے ساتھ ایکس رے - سب سے زیادہ انٹرنیٹ سینسرشپ والے ممالک کے لئے مناسب ہے۔ ٹریفک ویب ٹریفک کی سطح TLS پر ماسکنگ اور فعال پرابنگ کے طریقوں سے شناخت سے بچائے جانے کی حفاظت۔ - + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + + + + Create a file vault on your server to securely store and transfer files. فائلوں کو محفوظ طریقے سے اسٹور اور ٹرانسفر کرنے کے لیے اپنے سرور پر ایک فائل والٹ بنائیں. - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -2899,7 +3701,7 @@ If there is a extreme level of Internet censorship in your region, we advise you - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -2912,7 +3714,7 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. ایک نسبتاً نیا مقبول وی پی این پروٹوکول جس میں سادہ معماری ہے۔ وائر گارڈ تمام آلات پر مضبوط وی پی این کنکشن اور اعلی کارکردگی فراہم کرتا ہے۔ اس میں ہارڈ کوڈ کردہ انکرپشن سیٹنگز استعمال کی جاتی ہیں۔ وائر گارڈ کو اوپن وی پی این سے موازنہ کرنے پر لیٹنسی میں کمی اور بہتر ڈیٹا ٹرانسفر تھروپٹ حاصل ہوتی ہے۔ وائر گارڈ کا مخصوص پیکٹ سائنیچرز کی وجہ سے بلاک کرنا زیادہ آسان ہوتا ہے۔ کچھ دوسرے وی پی این پروٹوکول کے مخالف، جو اوبفسکیشن ٹیکنیکس کا استعمال کرتے ہیں، وائر گارڈ کے پیکٹس کے مسلسل سائنیچر پیٹرنز کو زیادہ آسانی سے پہچانا جا سکتا ہے اور اس طرح معقد ڈیپ پیکٹ انسپیکشن (DPI) سسٹمز اور دیگر نیٹ ورک مانیٹرنگ ٹولز کے ذریعے بلاک کیا جا سکتا ہے۔ * تمام پلیٹ فارمز پر دستیاب ہے * کم بجلی کی استعمال * کم سیٹنگز کی تعداد * ڈی پی آئی تجزیہ سسٹمز کے ذریعے آسانی سے پہچانا جاتا ہے، بلاک کرنے کے لئے زیادہ متاثر ہے * یو ڈی پی نیٹ ورک پروٹوکول پر کام کرتا ہے. - + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. @@ -2924,7 +3726,7 @@ Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REAL یہ REALITY کو سخت سینسرشپ والے ماحولوں میں انٹرنیٹ کی آزادی کو برقرار رکھنے کے لئے ایک مضبوط حل بناتا ہے۔ - + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -2936,32 +3738,31 @@ For more detailed information, you can انسٹالیشن کے بعد، ایمنیزیا آپ کے سرور پر ایک فائل اسٹوریج بنائے گا۔ آپ اس تک رسائی حاصل کر سکیں گے فائل زلا یا دیگر ایس ایف ٹی پی کلائنٹس کے ذریعے، اور اسکے علاوہ آپ اس ڈسک کو اپنے آلہ پر ماؤنٹ کر کے اس تک سیدھے راستے سے رسائی حاصل کر سکیں گے۔ مزید تفصیلات کے لئے، آپ سپورٹ سیکشن میں "ایس ایف ٹی پی فائل اسٹوریج بنانا" میں جا کر مزید معلومات حاصل کر سکتے ہیں." - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. وائر گارڈ - اعلی کارکردگی، تیز رفتار اور کم بجلی کی کھپت کے ساتھ نیا مقبول VPN پروٹوکول۔ سنسرشپ کی کم سطح والے علاقوں کے لیے تجویز کردہ. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. AmneziaWG - Amnezia سے خصوصی پروٹوکول، WireGuard پر مبنی۔ یہ وائر گارڈ کی طرح تیز ہے، لیکن رکاوٹوں کے خلاف بہت مزاحم ہے۔ اعلی درجے کی سنسر شپ والے خطوں کے لیے تجویز کردہ۔ - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec - جدید مستحکم پروٹوکول، دوسروں کے مقابلے میں تھوڑا تیز، سگنل ضائع ہونے کے بعد کنکشن بحال کرتا ہے۔ + IKEv2/IPsec - جدید مستحکم پروٹوکول، دوسروں کے مقابلے میں تھوڑا تیز، سگنل ضائع ہونے کے بعد کنکشن بحال کرتا ہے۔ - + Deploy a WordPress site on the Tor network in two clicks. ٹور نیٹ ورک پر ایک ورڈپریس سائٹ کو دو کلکس میں تعینات کریں. - + Replace the current DNS server with your own. This will increase your privacy level. موجودہ DNS سرور کو اپنے سے تبدیل کریں۔ اس سے آپ کی رازداری کی سطح میں اضافہ ہوگا. - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -2973,7 +3774,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for OpenVPN دستیاب سب سے زیادہ مقبول اور وقتی آزمائشی VPN پروٹوکولز میں سے ایک ہے۔ یہ انکرپشن اور کلیدی تبادلے کے لیے SSL/TLS کی طاقت کا فائدہ اٹھاتے ہوئے اپنا منفرد سیکیورٹی پروٹوکول استعمال کرتا ہے۔ مزید برآں، توثیق کے بہت سے طریقوں کے لیے OpenVPN کی حمایت اسے ورسٹائل اور قابل موافق بناتی ہے، جو آلات اور آپریٹنگ سسٹم کی ایک وسیع رینج کو پورا کرتی ہے۔ اوپن سورس کی نوعیت کی وجہ سے، اوپن وی پی این کو عالمی برادری کی طرف سے وسیع جانچ سے فائدہ ہوتا ہے، جو اس کی سلامتی کو مسلسل تقویت دیتا ہے۔ کارکردگی، سیکورٹی اور مطابقت کے مضبوط توازن کے ساتھ، OpenVPN رازداری کے بارے میں شعور رکھنے والے افراد اور کاروباروں کے لیے یکساں انتخاب ہے۔ * تمام پلیٹ فارمز پر AmneziaVPN میں دستیاب ہے * موبائل آلات پر بجلی کی عام کھپت * صارف کو مختلف آپریٹنگ سسٹمز اور ڈیوائسز کے ساتھ کام کرنے کی ضرورت کے مطابق لچکدار تخصیص * DPI تجزیہ سسٹمز کے ذریعہ پہچانا جاتا ہے اور اس وجہ سے بلاک کرنے کا خطرہ ہوتا ہے * TCP اور UDP دونوں نیٹ ورک پر کام کر سکتا ہے۔ پروٹوکول - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -2983,7 +3784,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for شیڈو ساکس، SOCKS5 پروٹوکول سے متاثر، AEAD سائفر کا استعمال کرتے ہوئے کنکشن کی حفاظت کرتا ہے۔ اگرچہ شیڈو ساکس کو سمجھدار اور شناخت کرنے کے لیے چیلنج کرنے کے لیے ڈیزائن کیا گیا ہے، لیکن یہ معیاری HTTPS کنکشن سے مماثل نہیں ہے۔ تاہم، کچھ ٹریفک تجزیہ نظام اب بھی شیڈو ساکس کنکشن کا پتہ لگا سکتے ہیں۔ Amnezia میں محدود تعاون کی وجہ سے، AmneziaWG پروٹوکول استعمال کرنے کی سفارش کی جاتی ہے۔ * صرف ڈیسک ٹاپ پلیٹ فارمز پر AmneziaVPN میں دستیاب ہے * قابل ترتیب انکرپشن پروٹوکول * کچھ DPI سسٹمز کے ذریعے قابل شناخت * TCP نیٹ ورک پروٹوکول پر کام کرتا ہے. - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -2996,7 +3797,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin ایک معاصر اشارہ جاتا ہے مقبول وی پی این پروٹوکول کا امنیزیہ ڈبلیو جی۔ امنیزیہ ڈبلیو جی وائر گارڈ کے بنیادی ڈھانچے پر مبنی ہے، جس نے اس کی آسانی سے معماری اور ایکسیلنٹ کارکردگی کی خصوصیات کو برقرار رکھا۔ جبکہ وائر گارڈ کو اس کی کارآمدی کے لئے جانا جاتا ہے، اس میں اپنے ممتاز پیکٹ سائنیچرز کی وجہ سے آسانی سے پہچان میں مسائل پیش آتے تھے۔ امنیزیہ ڈبلیو جی اس مسئلے کا حل پیش کرتا ہے بہتر اوبفسکیشن میتھڈس کے ذریعے، جس سے اس کی ٹریفک عام انٹرنیٹ ٹریفک کے ساتھ مل جل کر رہتی ہے۔ اس سے مطلب یہ ہے کہ امنیزیہ ڈبلیو جی نے اصل وائر گارڈ کی تیزی کارکردگی کو برقرار رکھا جبکہ اس میں ایک اضافی پردہ شامل کیا، جو اسے ایک تیز اور پرانے طریقہ سے وی پی این کنکشن کی درخواست کرنے والوں کے لئے ایک عمدہ چوئس بناتا ہے۔ * تمام پلیٹ فارمز پر دستیاب ہے * کم بجلی کی استعمال * کم سیٹنگز کی تعداد * ڈی پی آئی تجزیہ سسٹمز سے پہچانا نہیں جاتا، بند کرنے کے لئے مزید مضبوط ہے * یو ڈی پی نیٹ ورک پروٹوکول پر کام کرتا ہے۔ - + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -3009,7 +3810,7 @@ While it offers a blend of security, stability, and speed, it's essential t IKEv2، IPSec انکرپشن پرت کے ساتھ جوڑا، ایک جدید اور مستحکم VPN پروٹوکول کے طور پر کھڑا ہے۔ اس کی امتیازی خصوصیات میں سے ایک نیٹ ورکس اور ڈیوائسز کے درمیان تیزی سے سوئچ کرنے کی صلاحیت ہے، جو اسے متحرک نیٹ ورک کے ماحول میں خاص طور پر موافق بناتی ہے۔ اگرچہ یہ سیکیورٹی، استحکام اور رفتار کا امتزاج پیش کرتا ہے، لیکن یہ نوٹ کرنا ضروری ہے کہ IKEv2 کا آسانی سے پتہ لگایا جا سکتا ہے اور یہ بلاک کرنے کے لیے حساس ہے۔ * صرف ونڈوز پر AmneziaVPN میں دستیاب ہے * کم بجلی کی کھپت، موبائل ڈیوائسز پر * کم سے کم کنفیگریشن * DPI تجزیہ سسٹمز کے ذریعے پہچانا جاتا ہے * UDP نیٹ ورک پروٹوکول، پورٹ 500 اور 4500 پر کام .کرتا ہے - + DNS Service DNS سروس @@ -3063,35 +3864,169 @@ While it offers a blend of security, stability, and speed, it's essential t No match کوئی میچ نہیں - - - Unknown error - نامعلوم خامی - error 0x%1: %2 غلطی 0x%1: %2 + + + vmess:// url is invalid + + + + + Invalid streamSettings protocol: + + + + + Unknown transport method: + + + + + VMess string should start with 'vmess://' + + + + + VMess string should be a valid base64 string + + + + + JSON should not be empty + + + + + VLESS link should start with vless:// + + + + + link parse failed: %1 + + + + + empty host + + + + + missing port + + + + + missing uuid + + + + + Invalid ssd link: json: field %1 must exist + + + + + Invalid ssd link: json: field %1 must be valid port number + + + + + Invalid ssd link: json: field %1 must be of type 'string' + + + + + Invalid ssd link: json: field %1 must be an array + + + + + Skipping invalid ssd server: server must be an object + + + + + Skipping invalid ssd server: missing required field %1 + + + + + Skipping invalid ssd server: field %1 should be of type 'string' + + + + + Invalid ssd link: should begin with ssd:// + + + + + Invalid ssd link: base64 parse failed + + + + + Invalid ssd link: json parse failed + + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + + + + + SS URI is too short + + + + + + Can't find the colon separator between method and password + + + + + Can't find the at separator between password and hostname + + + + + Can't find the colon separator between hostname and port + + SelectLanguageDrawer - + Choose language زبان کا انتخاب کریں + + ServersListView + + + Unable change server while there is an active connection + فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں + + Settings - + Server #1 سرور نمبر 1 - - + + Server سرور @@ -3099,12 +4034,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted بیک اپ فائل خراب ہو گئی ہے - + All settings have been reset to default values تمام ترتیبات کو ڈیفالٹ اقدار پر دوبارہ ترتیب دیا گیا ہے @@ -3112,29 +4047,29 @@ While it offers a blend of security, stability, and speed, it's essential t ShareConnectionDrawer - - + + Save AmneziaVPN config AmneziaVPN ترتیب کو محفوظ کریں - + Share بانٹیں - + Copy کاپی - - + + Copied کاپی - + Copy config string تشکیل سٹرنگ کو کاپی کریں @@ -3144,7 +4079,7 @@ While it offers a blend of security, stability, and speed, it's essential t کنکشن کی ترتیبات دکھائیں - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" ایمنیزیا ایپ میں QR کوڈ پڑھنے کے لیے، "سرور شامل کریں" → "میرے پاس جوڑنے کے لیے ڈیٹا ہے" → "QR کوڈ، کلید یا سیٹنگ فائل" کو منتخب کریں @@ -3167,27 +4102,27 @@ While it offers a blend of security, stability, and speed, it's essential t سائٹ ہٹا دی گئی ہے: %1 - + Can't open file: %1 فائل نہیں کھول سکتا: %1 - + Failed to parse JSON data from file: %1 فائل سے JSON ڈیٹا پارس کرنے میں ناکامی: %1 - + The JSON data is not an array in file: %1 فائل میں JSON ڈیٹا ایک ایرے نہیں ہے: %1 - + Import completed واردات مکمل ہوگئی ہے - + Export completed ایکسپورٹ مکمل ہوگیا @@ -3228,7 +4163,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty یہ فیلڈ خالی نہیں ہو سکتا @@ -3236,7 +4171,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps ایم بی پی ایس @@ -3244,42 +4179,42 @@ While it offers a blend of security, stability, and speed, it's essential t VpnProtocol - + Unknown نامعلوم - + Disconnected منقطع - + Preparing تیاری مکمل - + Connecting... منسلک ہو رہا ہے... - + Connected منسلک - + Disconnecting... منقطع ہو رہا ہے... - + Reconnecting... دوبارہ منسلک ہو رہا ہے... - + Error خرابی @@ -3287,45 +4222,47 @@ While it offers a blend of security, stability, and speed, it's essential t amnezia::ContainerProps - + Low کم - Medium or High - متوسط یا زیادہ + متوسط یا زیادہ - Extreme - انتہائی + انتہائی - + + High + + + + I just want to increase the level of my privacy. میں صرف اپنی خصوصیت کا سطح بڑھانا چاہتا ہوں. - + I want to bypass censorship. This option recommended in most cases. میں سانسر شدگی سے چھٹکارا حاصل کرنا چاہتا ہوں۔ یہ اختیار بیشتر صورتوں میں تجویز کیا جاتا ہے. - Most VPN protocols are blocked. Recommended if other options are not working. - زیادہ تر وی پی این پروٹوکولز بلاک ہوتے ہیں۔ اگر دوسرے اختیارات کام نہیں کر رہے ہیں تو یہ تجویز کی جاتی ہے. + زیادہ تر وی پی این پروٹوکولز بلاک ہوتے ہیں۔ اگر دوسرے اختیارات کام نہیں کر رہے ہیں تو یہ تجویز کی جاتی ہے. main2 - + Private key passphrase نجی کلید پاس فریز - + Save محفوظ کریں diff --git a/client/translations/amneziavpn_zh_CN.ts b/client/translations/amneziavpn_zh_CN.ts index 6f357da6..cd39c2a6 100644 --- a/client/translations/amneziavpn_zh_CN.ts +++ b/client/translations/amneziavpn_zh_CN.ts @@ -1,52 +1,159 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + + + ApiServicesModel + + + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s + + + + + VPN to access blocked sites in regions with high levels of Internet censorship. + + + + + <p><a style="color: #EB5757;">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a> + + + + + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. + + + + + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship + + + + + %1 MBit/s + + + + + %1 days + + + + + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> + + + + + Free + + + + + %1 $/month + + + + + AppSplitTunnelingController + + + Application added: %1 + + + + + The application has already been added + + + + + The selected applications have been added + + + + + Application removed: %1 + + + + + ConnectButton + + + Unable to disconnect during configuration preparation + + + ConnectionController - - - - + + + + Connect 连接 - + VPN Protocols is not installed. Please install VPN container at first 请先安装VPN协议 - + Connecting... 连接中 - + Connected 已连接 - + Reconnecting... 重连中 - + Disconnecting... 断开中 - + + Preparing... + + + + Settings updated successfully, reconnnection... 配置已更新, 重连中... - + Settings updated successfully 配置更新成功 + + + The selected protocol is not supported on the current platform + 当前平台不支持所选协议 + + + + unable to create configuration + + ConnectionTypeSelectionDrawer @@ -61,7 +168,7 @@ 配置您的服务器 - + Open config file, key or QR code 配置文件,授权码或二维码 @@ -84,7 +191,7 @@ 粘贴 - + &SelectAll 全选 @@ -92,65 +199,65 @@ ExportController - Access error! - 访问错误 + 访问错误 HomeContainersListView - + Unable change protocol while there is an active connection 已建立连接时无法更改服务器配置 - The selected protocol is not supported on the current platform - 当前平台不支持所选协议 + 当前平台不支持所选协议 HomeSplitTunnelingDrawer - + Split tunneling 隧道分离 - + Allows you to connect to some sites or applications through a VPN connection and bypass others 允许您通过 VPN 连接连接到某些站点或应用程序,并绕过其他站点或应用程序 - + Split tunneling on the server 服务器上的分割隧道 - + Enabled Can't be disabled for current server 已启用 无法禁用当前服务器 - + Site-based split tunneling 基于网站的隧道分离 - + + Enabled 开启 - + + Disabled 禁用 - + App-based split tunneling 基于应用的隧道分离 @@ -158,21 +265,15 @@ Can't be disabled for current server ImportController - - Unable to open file - - - - - - Invalid configuration file - - - - + Scanned %1 of %2. 扫描 %1 of %2. + + + In the imported configuration, potentially dangerous lines were found: + + InstallController @@ -185,55 +286,78 @@ Can't be disabled for current server 已安装在服务器上 - - + %1 installed successfully. %1 安装成功。 - - + %1 is already installed on the server. 服务器上已经安装 %1。 - + Added containers that were already installed on the server 添加已安装在服务器上的容器 - + Already installed containers were found on the server. All installed containers have been added to the application 在服务上发现已经安装协议并添加至应用 - + Settings updated successfully 配置更新成功 - + Server '%1' was rebooted 服务器 '%1' 已重新启动 - + Server '%1' was removed 已移除服务器 '%1' - + All containers from server '%1' have been removed 服务器 '%1' 的所有容器已移除 - + %1 has been removed from the server '%2' %1 已从服务器 '%2' 上移除 + + + Api config removed + + + + + %1 cached profile cleared + + + + + %1 installed successfully. + + + + + API config reloaded + + + + + Successfully changed the country of connection to %1 + + 1% has been removed from the server '%2' %1 已从服务器 '%2' 上移除 @@ -251,16 +375,34 @@ Already installed containers were found on the server. All installed containers 协议已从 - + Please login as the user 请以用户身份登录 - + Server added successfully 增加服务器成功 + + InstalledAppsDrawer + + + Choose application + + + + + application name + + + + + Add selected + + + KeyChainClass @@ -282,28 +424,28 @@ Already installed containers were found on the server. All installed containers NotificationHandler - - + + AmneziaVPN - + VPN Connected 已连接到VPN - + VPN Disconnected 已从VPN断开 - + AmneziaVPN notification AmneziaVPN 提示 - + Unsecured network detected: 发现不安全网络 @@ -311,114 +453,234 @@ Already installed containers were found on the server. All installed containers PageDeinstalling - + Removing services from %1 正从 %1 移除服务 - + Usually it takes no more than 5 minutes 大约5分钟之内完成 + + PageDevMenu + + + Gateway endpoint + + + + + Dev gateway environment + + + PageHome - + Logging enabled - + Split tunneling enabled 用户分隔隧道已启用 - + Split tunneling disabled 分隔隧道已禁用 - + VPN protocol VPN协议 - + Servers 服务器 - Unable change server while there is an active connection - 已建立连接时无法更改服务器配置 + 已建立连接时无法更改服务器配置 + + + + PageProtocolAwgClientSettings + + + AmneziaWG settings + AmneziaWG 配置 + + + + MTU + + + + + Server settings + + + + + Port + 端口 + + + + Save + 保存 + + + + Save settings? + 保存设置? + + + + Only the settings for this device will be changed + + + + + Continue + 继续 + + + + Cancel + 取消 + + + + Unable change settings while there is an active connection + PageProtocolAwgSettings - + AmneziaWG settings AmneziaWG 配置 - + Port 端口 - - MTU - - - - Remove AmneziaWG - 移除AmneziaWG + 移除AmneziaWG - Remove AmneziaWG from server? - 从服务上移除AmneziaWG? + 从服务上移除AmneziaWG? - - + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Save 保存 - + + VPN address subnet + VPN 地址子网 + + + + Jc - Junk packet count + + + + + Jmin - Junk packet minimum size + + + + + Jmax - Junk packet maximum size + + + + + S1 - Init packet junk size + + + + + S2 - Response packet junk size + + + + + H1 - Init packet magic header + + + + + H2 - Response packet magic header + + + + + H4 - Transport packet magic header + + + + + H3 - Underload packet magic header + + + + + The values of the H1-H4 fields must be unique + + + + + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) + + + + Save settings? 保存设置? - - + Continue 继续 - - + Cancel 取消 + + + Unable change settings while there is an active connection + + PageProtocolCloakSettings - + Cloak settings Cloak 配置 - + Disguised as traffic from 伪装流量为 @@ -428,16 +690,21 @@ Already installed containers were found on the server. All installed containers 端口 - - + + Cipher 加密算法 - + Save 保存 + + + Unable change settings while there is an active connection + + PageProtocolOpenVpnSettings @@ -457,7 +724,7 @@ Already installed containers were found on the server. All installed containers 网络协议 - + Port 端口 @@ -579,48 +846,50 @@ Already installed containers were found on the server. All installed containers - + TLS auth TLS认证 - + Block DNS requests outside of VPN 阻止VPN外的DNS请求 - + Additional client configuration commands 附加客户端配置命令 - - + + Commands: 命令: - + Additional server configuration commands 附加服务器端配置命令 - + + Unable change settings while there is an active connection + + + Remove OpenVPN - 移除OpenVPN + 移除OpenVPN - Remove OpenVPN from server? - 从服务器移除OpenVPN吗? + 从服务器移除OpenVPN吗? - All users with whom you shared a connection with will no longer be able to connect to it. - 与您共享连接的所有用户将无法再连接到该连接。 + 与您共享连接的所有用户将无法再连接到该连接。 - + Save 保存 @@ -629,25 +898,23 @@ Already installed containers were found on the server. All installed containers 与您共享连接的所有用户将无法再连接到此链接 - Continue - 继续 + 继续 - Cancel - 取消 + 取消 PageProtocolRaw - + settings 配置 - + Show connection options 显示连接选项 @@ -656,22 +923,22 @@ Already installed containers were found on the server. All installed containers 连接选项 - + Connection options %1 %1 连接选项 - + Remove 移除 - + Remove %1 from server? 从服务器移除 %1 ? - + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 @@ -684,12 +951,12 @@ Already installed containers were found on the server. All installed containers 与您共享连接的所有用户将无法再连接到此链接 - + Continue 继续 - + Cancel 取消 @@ -697,110 +964,200 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Shadowsocks 配置 - + Port 端口 - - + + Cipher 加密算法 - + Save 保存 + + + Unable change settings while there is an active connection + + + + + PageProtocolWireGuardClientSettings + + + WG settings + + + + + MTU + + + + + Server settings + + + + + Port + 端口 + + + + Save + 保存 + + + + Save settings? + 保存设置? + + + + Only the settings for this device will be changed + + + + + Continue + 继续 + + + + Cancel + 取消 + + + + Unable change settings while there is an active connection + + PageProtocolWireGuardSettings - + WG settings - + + VPN address subnet + VPN 地址子网 + + + Port 端口 - - MTU - + + Save settings? + 保存设置? - - Remove WG - - - - - Remove WG from server? - - - - - All users with whom you shared a connection will no longer be able to connect to it. + + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + + Unable change settings while there is an active connection + + + + All users with whom you shared a connection will no longer be able to connect to it. + 与您共享连接的所有用户将无法再连接到该连接。 + + + Continue 继续 - + Cancel 取消 - + Save 保存 + + PageProtocolXraySettings + + + XRay settings + + + + + Disguised as traffic from + 伪装流量为 + + + + Save + 保存 + + + + Unable change settings while there is an active connection + + + PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. 您的服务器已安装DNS服务,仅能通过VPN访问。 - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. 其地址与您的服务器地址相同。您可以在 设置 连接 中进行配置。 - + Remove 移除 - + Remove %1 from server? 从服务器移除 %1 ? + + + Cannot remove AmneziaDNS from running server + + from server? 从服务器 - + Continue 继续 - + Cancel 取消 @@ -808,183 +1165,250 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully 配置更新成功 - + SFTP settings SFTP 配置 - + Host 主机 - - - - + + + + Copied 拷贝 - + Port 端口 - + User name 用户名 - + Password 密码 - + Mount folder on device 挂载文件夹 - + In order to mount remote SFTP folder as local drive, perform following steps: <br> 为将远程 SFTP 文件夹挂载到本地,请执行以下步骤: <br> - - + + <br>1. Install the latest version of <br>1. 安装最新版的 - - + + <br>2. Install the latest version of <br>2. 安装最新版的 - + Detailed instructions 详细说明 - Remove SFTP and all data stored there - 移除SFTP和其本地所有数据 + 移除SFTP和其本地所有数据 - Remove SFTP and all data stored there? - 移除SFTP和其本地所有数据? + 移除SFTP和其本地所有数据? - Continue - 继续 + 继续 - Cancel - 取消 + 取消 + + + + PageServiceSocksProxySettings + + + Settings updated successfully + 配置更新成功 + + + + + SOCKS5 settings + + + + + Host + 主机 + + + + + + + Copied + + + + + + Port + 端口 + + + + User name + 用户名 + + + + + Password + 密码 + + + + Username + + + + + + Change connection settings + + + + + The port must be in the range of 1 to 65535 + + + + + Password cannot be empty + + + + + Username cannot be empty + PageServiceTorWebsiteSettings - + Settings updated successfully 配置更新成功 - + Tor website settings Tor网站配置 - + Website address 网址 - + Copied 已拷贝 - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. 用 <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor 浏览器</a> 打开上面网址. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. 创建您的洋葱网站后,需要几分钟时间,才能使其在Tor网络上可用 - + When configuring WordPress set the this onion address as domain. 配置 WordPress 时,将此洋葱地址设置为域。 - Remove website - 移除网站 + 移除网站 - The site with all data will be removed from the tor network. - 网站及其所有数据将从 Tor 网络中删除 + 网站及其所有数据将从 Tor 网络中删除 - Continue - 继续 + 继续 - Cancel - 取消 + 取消 PageSettings - + Settings 设置 - + Servers 服务器 - + Connection 连接 - + Application 应用 - + Backup 备份 - + About AmneziaVPN 关于 - + + Dev console + + + + Close application 关闭应用 @@ -998,95 +1422,265 @@ And if you don't like the app, all the more support it - the donation will 如果您不喜欢,请捐助支持我们改进它。 - + Support Amnezia 支持Amnezia - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia 是一款免费的开源应用程序。 如果您喜欢的话可以支持开发者。 - + Contacts 联系方式 - + Telegram group 电报群 - + To discuss features 用于功能讨论 - + https://t.me/amnezia_vpn_en - Mail - 邮件 + 邮件 - + + support@amnezia.org + + + + For reviews and bug reports 用于评论和提交软件的缺陷 - + + Copied + + + + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website 官网 - - https://amnezia.org - + + Visit official website + - + Software version: %1 软件版本: %1 - + Check for updates 检查更新 - + Privacy Policy 隐私政策 + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + + + PageSettingsApiServerInfo + + + For the region + + + + + Price + + + + + Work period + + + + + Valid until + + + + + Speed + + + + + Support tag + + + + + Copied + + + + + Reload API config + + + + + Reload API config? + + + + + + Continue + 继续 + + + + + Cancel + 取消 + + + + Cannot reload API config during active connection + + + + + Remove from application + + + + + Remove from application? + + + + + Cannot remove server during active connection + + + + + PageSettingsAppSplitTunneling + + + Cannot change split tunneling settings during active connection + 无法在活动连接期间更改分割隧道设置 + + + + Only the apps from the list should have access via VPN + + + + + Apps from the list should not have access via VPN + + + + + App split tunneling + + + + + Mode + 规则 + + + + Remove + + + + + Continue + 继续 + + + + Cancel + 取消 + + + + application name + + + + + Open executable file + + + + + Executable files (*.*) + + + PageSettingsApplication - + Application 应用 - + Allow application screenshots 允许截屏 - + + Enable notifications + + + + + Enable notifications to show the VPN state in the status bar + + + + Auto start 自动运行 @@ -1099,80 +1693,85 @@ And if you don't like the app, all the more support it - the donation will 启动时自动运行运用程序 - + Launch the application every time the device is starts 每次设备启动时启动应用程序 - + Auto connect 自动连接 - + Connect to VPN on app start 应用开启时连接VPN - + Start minimized 最小化 - + Launch application minimized 开启应用软件时窗口最小化 - + Language 语言 - + Logging 日志 - + Enabled 开启 - + Disabled 禁用 - + Reset settings and remove all data from the application 重置并清理应用的所有数据 - + Reset settings and remove all data from the application? 重置并清理应用的所有数据? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. 所有配置恢复为默认值。服务器已安装的AmneziaVPN服务将被保留。 - + Continue 继续 - + Cancel 取消 + + + Cannot reset settings during active connection + + PageSettingsBackup - + Settings restored from backup file 从备份文件还原配置 @@ -1186,100 +1785,120 @@ And if you don't like the app, all the more support it - the donation will 备份您的配置 - + You can save your settings to a backup file to restore them the next time you install the application. 您可以将配置信息备份到文件中,以便在下次安装应用软件时恢复配置 - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. 备份将包含您添加到 AmneziaVPN 的所有服务器的密码和私钥。请将这些信息保存在安全的地方。 - + Make a backup 进行备份 - + Save backup file 保存备份 - - + + Backup files (*.backup) - + Backup file saved 备份文件已保存 - + Restore from backup 从备份还原 - + Open backup file 打开备份文件 - + Import settings from a backup file? 从备份文件导入设置? - + All current settings will be reset 当前所有设置将重置 - + Continue 继续 - + Cancel 取消 + + + Cannot restore backup settings during active connection + + PageSettingsConnection - + Connection 连接 - + When AmneziaDNS is not used or installed 当未使用或未安装AmneziaDNS时 - + Allows you to use the VPN only for certain Apps 只允许在某些应用程序中使用 VPN + + + KillSwitch + + + + + Disables your internet if your encrypted VPN connection drops out for any reason. + + + + + Cannot change killSwitch settings during active connection + + Use AmneziaDNS if installed on the server 使用AmneziaDNS,如其已安装在服务器上 - + Use AmneziaDNS 使用AmneziaDNS - + If AmneziaDNS is installed on the server 如果已在服务器安装AmneziaDNS - + DNS servers DNS服务器 @@ -1288,17 +1907,17 @@ And if you don't like the app, all the more support it - the donation will 如果未使用或未安装AmneziaDNS - + Site-based split tunneling 基于网站的隧道分离 - + Allows you to select which sites you want to access through the VPN 配置想要通过VPN访问网站 - + App-based split tunneling 基于应用的隧道分离 @@ -1322,62 +1941,62 @@ And if you don't like the app, all the more support it - the donation will PageSettingsDns - + Default server does not support custom DNS 默认服务器不支持自定义 DNS - + DNS servers DNS服务器 - + If AmneziaDNS is not used or installed 如果未使用或未安装AmneziaDNS - + Primary DNS 首选 DNS - + Secondary DNS 备用 DNS - + Restore default 恢复默认配置 - + Restore default DNS settings? 是否恢复默认DNS配置? - + Continue 继续 - + Cancel 取消 - + Settings have been reset 已重置 - + Save 保存 - + Settings saved 配置已保存 @@ -1385,72 +2004,102 @@ And if you don't like the app, all the more support it - the donation will PageSettingsLogging - - Logging is enabled. Note that logs will be automatically disabled after 14 days, and all log files will be deleted. - - - - + Logging 日志 - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. 默认情况下,日志功能是禁用的。如果应用程序出现故障,则启用日志保存功能。 - Save logs - 记录日志 + 记录日志 - Open folder with logs - 打开日志文件夹 + 打开日志文件夹 - + + Save 保存 - + + Logs files (*.log) - + + Logs file saved 日志文件已保存 - Save logs to file - 保存日志到文件 + 保存日志到文件 - + + Enable logs + + + + Clear logs? 清理日志? - + Continue 继续 - + Cancel 取消 - + Logs have been cleaned up 日志已清理 - + + Client logs + + + + + AmneziaVPN logs + + + + + Open logs folder + + + + + Export logs + + + + + Service logs + + + + + AmneziaVPN-service logs + + + + Clear logs 清理日志 @@ -1458,37 +2107,34 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerData - + All installed containers have been added to the application 所有已安装的容器,已被添加到应用软件 - + No new installed containers found 未发现新安装的容器 - Clear Amnezia cache - 清除 Amnezia 缓存 + 清除 Amnezia 缓存 - May be needed when changing other settings - 更改其他设置时可能需要缓存 + 更改其他设置时可能需要缓存 - Clear cached profiles? - 清除缓存? + 清除缓存? - + Do you want to reboot the server? 您想重新启动服务器吗? - + Do you want to clear server from Amnezia software? 您要清除服务器上的Amnezia软件吗? @@ -1498,79 +2144,97 @@ And if you don't like the app, all the more support it - the donation will - - - - - + + + + Continue 继续 - - - - - + + + + Cancel 取消 - + Check the server for previously installed Amnezia services 检查服务器上,是否存在之前安装的 Amnezia 服务 - + Add them to the application if they were not displayed 如果存在且未显示,则添加到应用软件 - + Reboot server 重新启动服务器 - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? 重新启动过程可能需要大约30秒。您确定要继续吗? - + + Cannot reboot server during active connection + + + + Remove server from application 移除本地服务器信息 - + Do you want to remove the server from application? 您想要从应用程序中移除服务器吗? - + + Cannot remove server during active connection + + + + All users whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + + Cannot clear server from Amnezia software during active connection + + + + Reset API config 重置 API 配置 - + Do you want to reset API config? 您想重置 API 配置吗? + + + Cannot reset API config during active connection + + Remove server? 移除本地服务器信息? - + All installed AmneziaVPN services will still remain on the server. 所有已安装的 AmneziaVPN 服务仍将保留在服务器上。 - + Clear server from Amnezia software 清理Amnezia中服务器信息 @@ -1586,27 +2250,32 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerInfo - + + Subscription is valid until + + + + Server name 服务器名 - + Save 保存 - + Protocols 协议 - + Services 服务 - + Management 管理 @@ -1618,20 +2287,65 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerProtocol - + settings 配置 - + + Clear %1 profile? + + + + + + + + + + connection settings + + + + + Click the "connect" button to create a connection configuration + + + + + server settings + + + + + Clear profile + + + + + The connection configuration will be deleted for this device only + + + + + Unable to clear %1 profile while there is an active connection + + + + Remove 移除 - + All users with whom you shared a connection will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 + + + Cannot remove active container + + All users who you shared a connection with will no longer be able to connect to it. 使用此共享连接的所有用户,将无法再连接它。 @@ -1641,7 +2355,7 @@ And if you don't like the app, all the more support it - the donation will 从服务器 - + Remove %1 from server? 从服务器移除 %1 ? @@ -1650,12 +2364,14 @@ And if you don't like the app, all the more support it - the donation will 与您共享连接的所有用户将无法再连接到此链接 - + + Continue 继续 - + + Cancel 取消 @@ -1663,7 +2379,7 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServersList - + Servers 服务器 @@ -1683,7 +2399,7 @@ And if you don't like the app, all the more support it - the donation will 网站级VPN分流 - + Default server does not support split tunneling function 默认服务器不支持分离隧道功能 @@ -1692,32 +2408,32 @@ And if you don't like the app, all the more support it - the donation will 仅使用VPN访问 - + Addresses from the list should not be accessed via VPN 不使用VPN访问 - + Split tunneling 隧道分离 - + Mode 规则 - + Remove 移除 - + Continue 继续 - + Cancel 取消 @@ -1730,75 +2446,120 @@ And if you don't like the app, all the more support it - the donation will 导入/导出网站 - + Cannot change split tunneling settings during active connection 无法在活动连接期间更改分割隧道设置 - + Only the sites listed here will be accessed through the VPN 只有这里列出的网站将通过VPN访问 - + website or IP 网站或IP - + Import / Export Sites 导入/导出网站 - + Import 导入 - + Save site list 保存网址 - + Save sites 保存网址 - - - + + + Sites files (*.json) - + Import a list of sites 导入网址列表 - + Replace site list 替换网址列表 - - + + Open sites file 打开网址文件 - + Add imported sites to existing ones 将导入的网址添加到现有网址中 + + PageSetupWizardApiServiceInfo + + + For the region + + + + + Price + + + + + Work period + + + + + Speed + + + + + Features + + + + + Connect + 连接 + + + + PageSetupWizardApiServicesList + + + VPN by Amnezia + + + + + Choose a VPN service that suits your needs. + + + PageSetupWizardConfigSource - Server connection - 服务器连接 + 服务器连接 Do not use connection code from public sources. It may have been created to intercept your data. @@ -1808,39 +2569,150 @@ It's okay as long as it's from someone you trust. 请确保连接码来源可信。 - Do not use connection codes from untrusted sources, as they may be created to intercept your data. - 请勿使用来自不受信任来源的连接代码,因为它们可能是为了拦截您的数据而创建的。 + 请勿使用来自不受信任来源的连接代码,因为它们可能是为了拦截您的数据而创建的。 - What do you have? - 你用什么方式创建连接? + 你用什么方式创建连接? - File with connection settings or backup - 包含连接配置或备份的文件 + 包含连接配置或备份的文件 - + + Connection + 连接 + + + + Settings + 设置 + + + + Enable logs + + + + + Support tag + + + + + Copied + + + + + Insert the key, add a configuration file or scan the QR-code + + + + + Insert key + + + + + Insert + 插入 + + + + Continue + 继续 + + + + Other connection options + + + + + Site Amnezia + + + + + VPN by Amnezia + + + + + Connect to classic paid and free VPN services from Amnezia + + + + + Self-hosted VPN + + + + + Configure Amnezia VPN on your own server + + + + + Restore from backup + 从备份还原 + + + + + + + + + Open backup file + 打开备份文件 + + + + Backup files (*.backup) + + + + File with connection settings 包含连接配置的文件 - + + + + + + Open config file 打开配置文件 - + QR code 二维码 - + + + + + + + I have nothing + 我没有 + + + + + + + Key as text - 授权码文本 + 授权码文本 @@ -1850,12 +2722,12 @@ It's okay as long as it's from someone you trust. 连接服务器 - + Configure your server 配置服务器 - + Server IP address [:port] 服务器IP [:端口] @@ -1868,12 +2740,12 @@ It's okay as long as it's from someone you trust. 密码 或 私钥 - + Continue 继续 - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties 您输入的所有数据将严格保密,不会与 Amnezia 或任何第三方共享或披露 @@ -1884,37 +2756,47 @@ and will not be shared or disclosed to the Amnezia or any third parties 不会向 Amnezia 或任何第三方分享或披露 - + 255.255.255.255:22 - + SSH Username SSH 用户名 - + Password or SSH private key 密码或 SSH 私钥 - + + How to run your VPN server + + + + + Where to get connection data, step-by-step instructions for buying a VPS + + + + Ip address cannot be empty IP不能为空 - + Enter the address in the format 255.255.255.255:88 按照这种格式输入 255.255.255.255:88 - + Login cannot be empty 账号不能为空 - + Password/private key cannot be empty 密码或私钥不能为空 @@ -1922,17 +2804,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - + What is the level of internet control in your region? 您所在地区的互联网管控力度如何? - + Choose a VPN protocol 选择 VPN 协议 - + Skip setup 跳过设置 @@ -1945,7 +2827,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 我想选择VPN协议 - + Continue 继续 @@ -1957,28 +2839,28 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardInstalling - - + + Usually it takes no more than 5 minutes 通常不超过5分钟 - + The server has already been added to the application 服务器已添加到应用软件中 - + Amnezia has detected that your server is currently Amnezia 检测到您的服务器当前 - + busy installing other software. Amnezia installation 正安装其他软件。Amnezia安装 - + Cancel installation 取消安装 @@ -1991,12 +2873,12 @@ and will not be shared or disclosed to the Amnezia or any third parties 正安装其他软件。Amnezia安装 - + will pause until the server finishes installing other software 将暂停,直到其他软件安装完成。 - + Installing 安装中 @@ -2004,45 +2886,50 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocolSettings - + Installing %1 正在安装 %1 - + More detailed 更多细节 - + Close 关闭 - + Network protocol 网络协议 - + Port 端口 - + Install 安装 + + + The port must be in the range of 1 to 65535 + + PageSetupWizardProtocols - + VPN protocol VPN 协议 - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. 选择你认为优先级最高的一项。稍后,您可以安装其他协议和附加服务,例如 DNS 代理和 SFTP。 @@ -2050,7 +2937,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardQrReader - + Point the camera at the QR code and hold for a couple of seconds. 将相机对准二维码并按住几秒钟 @@ -2058,34 +2945,29 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardStart - Settings restored from backup file - 从备份文件还原配置 + 从备份文件还原配置 - Free service for creating a personal VPN on your server. - 在您的服务器上架设私人免费VPN服务。 + 在您的服务器上架设私人免费VPN服务。 - Helps you access blocked content without revealing your privacy, even to VPN providers. - 帮助您访问受限内容,保护您的隐私,即使是VPN提供商也无法获取。 + 帮助您访问受限内容,保护您的隐私,即使是VPN提供商也无法获取。 - I have the data to connect - 我有连接配置 + 我有连接配置 - I have nothing - 我没有 + 我没有 - - https://amnezia.org/instructions/0_starter-guide - + + Let's get started + @@ -2111,7 +2993,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 插入 - + Continue 继续 @@ -2119,7 +3001,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardViewConfig - + New connection 新连接 @@ -2128,22 +3010,27 @@ and will not be shared or disclosed to the Amnezia or any third parties 请勿使用公共来源的连接码。它可以被创建来拦截您的数据。 - + Collapse content 折叠内容 - + Show content 显示内容 - + + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. + + + + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. 只使用您信任的来源提供的连接代码。公共来源的代码可能是为了拦截您的数据而创建的。 - + Connect 连接 @@ -2151,133 +3038,167 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + Save OpenVPN config 保存OpenVPN配置 - + Save WireGuard config 保存WireGuard配置 - + Save AmneziaWG config 保存 AmneziaWG 配置 - + Save Shadowsocks config 保存 Shadowsocks 配置 - + Save Cloak config 保存斗篷配置 - + + Save XRay config + + + + For the AmneziaVPN app AmneziaVPN 应用 - + OpenVPN native format OpenVPN原生格式 - + WireGuard native format WireGuard原生格式 - + AmneziaWG native format AmneziaWG 本地格式 - + Shadowsocks native format Shadowsocks原生格式 - + Cloak native format Cloak原生格式 - + + XRay native format + + + + Share VPN Access 共享 VPN 访问 - + Share full access to the server and VPN 共享服务器和VPN的完全访问权限 - + Use for your own devices, or share with those you trust to manage the server. 用于您自己的设备,或与您信任的人共享以管理服务器. - - + + Users 用户 - + Share VPN access without the ability to manage the server 共享 VPN 访问,无需管理服务器 - + Search 搜索 - - Creation date: - 创建日期: + + Creation date: %1 + - + + Latest handshake: %1 + + + + + Data received: %1 + + + + + Data sent: %1 + + + + + Allowed IPs: %1 + + + + Creation date: + 创建日期: + + + Rename 重新命名 - + Client name 客户名称 - + Save 保存 - + Revoke 撤销 - + Revoke the config for a user - %1? 撤销用户的配置- %1? - + The user will no longer be able to connect to your server. 该用户将无法再连接到您的服务器. - + Continue 继续 - + Cancel 取消 @@ -2290,7 +3211,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 访问VPN - + Connection 连接 @@ -2319,8 +3240,8 @@ and will not be shared or disclosed to the Amnezia or any third parties 服务器 - - + + Server 服务器 @@ -2333,7 +3254,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 访问配置文件的内容为: - + File with connection settings to 连接配置文件的内容为: @@ -2342,35 +3263,35 @@ and will not be shared or disclosed to the Amnezia or any third parties 协议 - - + + Protocol 协议 - + Connection to 连接到 - + Config revoked 配置已撤销 - + User name 用户名 - - + + Connection format 连接格式 - - + + Share 共享 @@ -2378,50 +3299,55 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShareFullAccess - + Full access to the server and VPN 对服务器和VPN的完全访问权限 - + We recommend that you use full access to the server only for your own additional devices. 我们建议您仅为自己的附加设备使用服务器的完全访问权限. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. 如果您与其他人共享完全访问权限,他们可以从服务器中删除和添加协议和服务,这将导致VPN对所有用户的工作出现问题。 - - + + Server 服务器 - + Accessing 访问 - + File with accessing settings to 访问配置文件的内容为 - + Share 共享 - + + Access error! + 访问错误 + + + Connection to 连接到 - + File with connection settings to 连接配置文件的内容为 @@ -2429,15 +3355,25 @@ and will not be shared or disclosed to the Amnezia or any third parties PageStart - + Logging was disabled after 14 days, log files were deleted + + + Settings restored from backup file + 从备份文件还原配置 + + + + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. + + PopupType - + Close 关闭 @@ -2471,12 +3407,12 @@ and will not be shared or disclosed to the Amnezia or any third parties 未发现密码 - + Could not open keystore 无法打开密钥库 - + Could not remove private key from keystore 无法从密钥库中删除私钥 @@ -2652,27 +3588,27 @@ and will not be shared or disclosed to the Amnezia or any third parties 无法打开密钥库 - + Could not create private key generator 无法创建私钥生成器 - + Could not generate new private key 无法生成新的私钥 - + Could not retrieve private key from keystore 无法从密钥库检索私钥 - + Could not create encryption cipher 无法创建加密密码 - + Could not encrypt data 无法加密数据 @@ -2680,10 +3616,17 @@ and will not be shared or disclosed to the Amnezia or any third parties QObject - + SFTP service SFTP 服务 + + + + + SOCKS5 proxy server + + No error @@ -2691,6 +3634,7 @@ and will not be shared or disclosed to the Amnezia or any third parties + Unknown error 未知错误 @@ -2700,70 +3644,85 @@ and will not be shared or disclosed to the Amnezia or any third parties 功能未实现 - + + Background service is not running + + + + Server check failed 服务器检测失败 - + Server port already used. Check for another software 检测服务器该端口是否被其他软件被占用 - + Server error: Docker container missing 服务器错误: Docker容器丢失 - + Server error: Docker failed 服务器错误: Docker失败 - + Installation canceled by user 用户取消安装 - + The user does not have permission to use sudo 用户没有root权限 - + + Server error: Packet manager error + + + + SSH request was denied SSH请求被拒绝 - + SSH request was interrupted SSH请求中断 - + SSH internal error SSH内部错误 - + Invalid private key or invalid passphrase entered 输入的私钥或密码无效 - + The selected private key format is not supported, use openssh ED25519 key types or PEM key types 不支持所选私钥格式,请使用 openssh ED25519 密钥类型或 PEM 密钥类型 - + Timeout connecting to server 连接服务器超时 - + SCP error: Generic failure + + + Unable to open config file + + Sftp error: End-of-file encountered Sftp错误: End-of-file encountered @@ -2817,52 +3776,82 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp 错误: 远程驱动器中没有媒介 - + VPN connection error VPN 连接错误 - + Error when retrieving configuration from API 从 API 检索配置时出错 - + This config has already been added to the application 该配置已添加到应用程序中 - - - QFile error: The file could not be opened - - - QFile error: An error occurred when reading from the file + In the response from the server, an empty config was received - QFile error: The file could not be accessed + SSL error occurred - QFile error: An unspecified error occurred + Server response timeout on api request - QFile error: A fatal error occurred + Missing AGW public key - QFile error: The operation was aborted + Failed to decrypt response payload + + + + + Missing list of available services + + + + + QFile error: The file could not be opened + + + + + QFile error: An error occurred when reading from the file + + + + + QFile error: The file could not be accessed + QFile error: An unspecified error occurred + + + + + QFile error: A fatal error occurred + + + + + QFile error: The operation was aborted + + + + ErrorCode: %1. 错误代码: %1. @@ -2871,57 +3860,57 @@ and will not be shared or disclosed to the Amnezia or any third parties 配置保存到磁盘失败 - + OpenVPN config missing OpenVPN配置丢失 - + OpenVPN management server error OpenVPN 管理服务器错误 - + OpenVPN executable missing OpenVPN 可执行文件丢失 - + Shadowsocks (ss-local) executable missing Shadowsocks (ss-local) 执行文件丢失 - + Cloak (ck-client) executable missing Cloak (ck-client) 执行文件丢失 - + Amnezia helper service error Amnezia 服务连接失败 - + OpenSSL failed OpenSSL错误 - + Can't connect: another VPN connection is active 无法连接:另一个VPN连接处于活跃状态 - + Can't setup OpenVPN TAP network adapter 无法设置 OpenVPN TAP 网络适配器 - + VPN pool error: no available addresses VPN 池错误:没有可用地址 - + The config does not contain any containers and credentials for connecting to the server 配置不包含任何用于连接服务器的容器和凭据 @@ -2930,43 +3919,61 @@ and will not be shared or disclosed to the Amnezia or any third parties 该配置不包含任何用于连接到服务器的容器和凭据。 - + Internal error - + IPsec - - + + Website in Tor network 在 Tor 网络中架设网站 - + AmneziaDNS AmneziaDNS - + SFTP file sharing service SFTP文件共享服务 - + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. OpenVPN 是最流行的 VPN 协议,具有灵活的配置选项。它使用自己的安全协议与 SSL/TLS 进行密钥交换。 - + Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. Shadowsocks - 掩盖VPN流量,使其类似于正常的网络流量,但在一些高度审查的地区可能会被分析系统识别. - + + XRay with REALITY - Suitable for countries with the highest level of internet censorship. Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods. + + + + + IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + + + + + The REALITY protocol, a pioneering development by the creators of XRay, is specifically designed to counteract the highest levels of internet censorship through its novel approach to evasion. +It uniquely identifies censors during the TLS handshake phase, seamlessly operating as a proxy for legitimate clients while diverting censors to genuine websites like google.com, thus presenting an authentic TLS certificate and data. +This advanced capability differentiates REALITY from similar technologies by its ability to disguise web traffic as coming from random, legitimate sites without the need for specific configurations. +Unlike older protocols such as VMess, VLESS, and the XTLS-Vision transport, REALITY's innovative "friend or foe" recognition at the TLS handshake enhances security and circumvents detection by sophisticated DPI systems employing active probing techniques. This makes REALITY a robust solution for maintaining internet freedom in environments with stringent censorship. + + + + After installation, Amnezia will create a file storage on your server. You will be able to access it using @@ -2989,12 +3996,12 @@ For more detailed information, you can OpenVPN over Cloak - OpenVPN与VPN结合,伪装成Web流量,并保护免受主动探测的侦测。非常适合在具有最高审查水平的地区绕过封锁 - + Create a file vault on your server to securely store and transfer files. 在您的服务器上创建一个文件保险库,用于安全存储和传输文件。 - + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. @@ -3016,7 +4023,7 @@ If there is a extreme level of Internet censorship in your region, we advise you - + A relatively new popular VPN protocol with a simplified architecture. WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. @@ -3041,32 +4048,31 @@ WireGuard非常容易被阻挡,因为其独特的数据包签名。与一些 Shadowsocks - 混淆 VPN 流量,使其与正常的 Web 流量相似,但在一些审查力度高的地区可以被分析系统识别。 - + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. OpenVPN over Cloak - OpenVPN与VPN结合,伪装成Web流量,并保护免受主动探测的侦测。非常适合在具有最高审查水平的地区绕过封锁. - + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. WireGuard - 新型流行的VPN协议,具有高性能、高速度和低功耗。建议用于审查力度较低的地区. - + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. AmneziaWG - Amnezia 的特殊协议,基于 WireGuard。它的速度像 WireGuard 一样快,但非常抗堵塞。推荐用于审查较严的地区。 - IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after signal loss. - IKEv2/IPsec - 现代稳定协议,相比其他协议较快一些,在信号丢失后恢复连接。 + IKEv2/IPsec - 现代稳定协议,相比其他协议较快一些,在信号丢失后恢复连接。 - + Deploy a WordPress site on the Tor network in two clicks. 只需点击两次即可架设 WordPress 网站到 Tor 网络. - + Replace the current DNS server with your own. This will increase your privacy level. 将当前的 DNS 服务器替换为您自己的。这将提高您的隐私保护级别。 @@ -3075,7 +4081,7 @@ WireGuard非常容易被阻挡,因为其独特的数据包签名。与一些 在您的服务器上创建文件仓库,以便安全地存储和传输文件 - + OpenVPN stands as one of the most popular and time-tested VPN protocols available. It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. @@ -3094,7 +4100,7 @@ It employs its unique security protocol, leveraging the strength of SSL/TLS for * 可以通过 TCP 和 UDP 网络协议运行. - + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. * Available in the AmneziaVPN only on desktop platforms @@ -3166,7 +4172,7 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * 通过 UDP 网络协议工作。 - + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. @@ -3187,7 +4193,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin * 通过 UDP 网络协议工作。 - + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. @@ -3228,7 +4234,7 @@ While it offers a blend of security, stability, and speed, it's essential t IPsec 容器 - + DNS Service DNS 服务 @@ -3286,35 +4292,169 @@ While it offers a blend of security, stability, and speed, it's essential t No match 不匹配 - - - Unknown error - 未知错误 - error 0x%1: %2 错误 0x%1: %2 + + + vmess:// url is invalid + + + + + Invalid streamSettings protocol: + + + + + Unknown transport method: + + + + + VMess string should start with 'vmess://' + + + + + VMess string should be a valid base64 string + + + + + JSON should not be empty + + + + + VLESS link should start with vless:// + + + + + link parse failed: %1 + + + + + empty host + + + + + missing port + + + + + missing uuid + + + + + Invalid ssd link: json: field %1 must exist + + + + + Invalid ssd link: json: field %1 must be valid port number + + + + + Invalid ssd link: json: field %1 must be of type 'string' + + + + + Invalid ssd link: json: field %1 must be an array + + + + + Skipping invalid ssd server: server must be an object + + + + + Skipping invalid ssd server: missing required field %1 + + + + + Skipping invalid ssd server: field %1 should be of type 'string' + + + + + Invalid ssd link: should begin with ssd:// + + + + + Invalid ssd link: base64 parse failed + + + + + Invalid ssd link: json parse failed + + + + + Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core + + + + + SS URI is too short + + + + + + Can't find the colon separator between method and password + + + + + Can't find the at separator between password and hostname + + + + + Can't find the colon separator between hostname and port + + SelectLanguageDrawer - + Choose language 选择语言 + + ServersListView + + + Unable change server while there is an active connection + 已建立连接时无法更改服务器配置 + + Settings - + Server #1 - - + + Server 服务器 @@ -3322,52 +4462,51 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted 备份文件已损坏 - + All settings have been reset to default values 所配置恢复为默认值 - Cached profiles cleared - 缓存的配置文件已清除 + 缓存的配置文件已清除 ShareConnectionDrawer - - + + Save AmneziaVPN config 保存配置 - + Share 共享 - + Copy 拷贝 - - + + Copied 已拷贝 - + Copy config string 复制配置字符串 - + Show connection settings 显示连接配置 @@ -3376,7 +4515,7 @@ While it offers a blend of security, stability, and speed, it's essential t 展示内容 - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" 要应用二维码到 Amnezia,请底部工具栏点击“+”→“连接方式”→“二维码、授权码或配置文件” @@ -3399,27 +4538,27 @@ While it offers a blend of security, stability, and speed, it's essential t 已移除网站: %1 - + Can't open file: %1 无法打开文件: %1 - + Failed to parse JSON data from file: %1 JSON解析失败,文件: %1 - + The JSON data is not an array in file: %1 文件中的JSON数据不是一个数组,文件: %1 - + Import completed 完成导入 - + Export completed 完成导出 @@ -3460,7 +4599,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty 输入不能为空 @@ -3468,7 +4607,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps @@ -3476,42 +4615,42 @@ While it offers a blend of security, stability, and speed, it's essential t VpnProtocol - + Unknown 未知 - + Disconnected 连接已断开 - + Preparing 准备中 - + Connecting... 连接中 - + Connected 已连接 - + Disconnecting... 断开中 - + Reconnecting... 重连中 - + Error 错误 @@ -3519,38 +4658,32 @@ While it offers a blend of security, stability, and speed, it's essential t amnezia::ContainerProps - + Low - + High 中或高 - Extreme - 极度 + 极度 - + I just want to increase the level of my privacy. 只是想提高隐私保护级别。 - + I want to bypass censorship. This option recommended in most cases. 想要绕过审查制度。大多数情况下推荐使用此选项。 - Most VPN protocols are blocked. Recommended if other options are not working. - 大多数 VPN 协议都被阻止。如果其他选项不起作用,推荐此选项。 - - - High - + 大多数 VPN 协议都被阻止。如果其他选项不起作用,推荐此选项。 Medium @@ -3572,12 +4705,12 @@ While it offers a blend of security, stability, and speed, it's essential t main2 - + Private key passphrase 私钥密码 - + Save 保存 diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp new file mode 100644 index 00000000..b8696201 --- /dev/null +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -0,0 +1,537 @@ +#include "apiConfigsController.h" + +#include +#include + +#include "amnezia_application.h" +#include "configurators/wireguard_configurator.h" +#include "core/api/apiDefs.h" +#include "core/api/apiUtils.h" +#include "core/controllers/gatewayController.h" +#include "core/qrCodeUtils.h" +#include "ui/controllers/systemController.h" +#include "version.h" + +namespace +{ + namespace configKey + { + constexpr char cloak[] = "cloak"; + constexpr char awg[] = "awg"; + + constexpr char apiEdnpoint[] = "api_endpoint"; + constexpr char accessToken[] = "api_key"; + constexpr char certificate[] = "certificate"; + constexpr char publicKey[] = "public_key"; + constexpr char protocol[] = "protocol"; + + constexpr char uuid[] = "installation_uuid"; + constexpr char osVersion[] = "os_version"; + constexpr char appVersion[] = "app_version"; + + constexpr char userCountryCode[] = "user_country_code"; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceInfo[] = "service_info"; + constexpr char serviceProtocol[] = "service_protocol"; + + constexpr char aesKey[] = "aes_key"; + constexpr char aesIv[] = "aes_iv"; + constexpr char aesSalt[] = "aes_salt"; + + constexpr char apiPayload[] = "api_payload"; + constexpr char keyPayload[] = "key_payload"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; + + constexpr char config[] = "config"; + } +} + +ApiConfigsController::ApiConfigsController(const QSharedPointer &serversModel, + const QSharedPointer &apiServicesModel, + const std::shared_ptr &settings, QObject *parent) + : QObject(parent), m_serversModel(serversModel), m_apiServicesModel(apiServicesModel), m_settings(settings) +{ +} + +bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName) +{ + if (fileName.isEmpty()) { + emit errorOccurred(ErrorCode::PermissionsError); + return false; + } + + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = serverCountryCode; + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/native_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } + + QJsonObject jsonConfig = QJsonDocument::fromJson(responseBody).object(); + QString nativeConfig = jsonConfig.value(configKey::config).toString(); + nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); + + SystemController::saveFile(fileName, nativeConfig); + return true; +} + +bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = serverCountryCode; + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_native_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { + emit errorOccurred(errorCode); + return false; + } + return true; +} + +void ApiConfigsController::prepareVpnKeyExport() +{ + auto serverConfigObject = m_serversModel->getServerConfig(m_serversModel->getProcessedServerIndex()); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); + m_vpnKey = vpnKey; + + vpnKey.replace("vpn://", ""); + + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(vpnKey.toUtf8()); + + emit vpnKeyExportReady(); +} + +void ApiConfigsController::copyVpnKeyToClipboard() +{ + auto clipboard = amnApp->getClipboard(); + clipboard->setText(m_vpnKey); +} + +bool ApiConfigsController::fillAvailableServices() +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + QJsonObject apiPayload; + apiPayload[configKey::osVersion] = QSysInfo::productType(); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/services"), apiPayload, responseBody); + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + errorCode = ErrorCode::ApiServicesMissingError; + } + } + + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } + + QJsonObject data = QJsonDocument::fromJson(responseBody).object(); + m_apiServicesModel->updateModel(data); + return true; +} + +bool ApiConfigsController::importServiceFromGateway() +{ + if (m_serversModel->isServerFromApiAlreadyExists(m_apiServicesModel->getCountryCode(), m_apiServicesModel->getSelectedServiceType(), + m_apiServicesModel->getSelectedServiceProtocol())) { + emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded); + return false; + } + + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto installationUuid = m_settings->getInstallationUuid(true); + auto userCountryCode = m_apiServicesModel->getCountryCode(); + auto serviceType = m_apiServicesModel->getSelectedServiceType(); + auto serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol(); + + ApiPayloadData apiPayloadData = generateApiPayloadData(serviceProtocol); + + QJsonObject apiPayload = fillApiPayload(serviceProtocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = userCountryCode; + apiPayload[configKey::serviceType] = serviceType; + apiPayload[configKey::uuid] = installationUuid; + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody); + + QJsonObject serverConfig; + if (errorCode == ErrorCode::NoError) { + fillServerConfig(serviceProtocol, apiPayloadData, responseBody, serverConfig); + + QJsonObject apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + apiConfig.insert(configKey::userCountryCode, m_apiServicesModel->getCountryCode()); + apiConfig.insert(configKey::serviceType, m_apiServicesModel->getSelectedServiceType()); + apiConfig.insert(configKey::serviceProtocol, m_apiServicesModel->getSelectedServiceProtocol()); + + serverConfig.insert(configKey::apiConfig, apiConfig); + + m_serversModel->addServer(serverConfig); + emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName())); + return true; + } else { + emit errorOccurred(errorCode); + return false; + } +} + +bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, + bool reloadServiceConfig) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverConfig = m_serversModel->getServerConfig(serverIndex); + auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + auto authData = serverConfig.value(configKey::authData).toObject(); + + auto installationUuid = m_settings->getInstallationUuid(true); + auto userCountryCode = apiConfig.value(configKey::userCountryCode).toString(); + auto serviceType = apiConfig.value(configKey::serviceType).toString(); + auto serviceProtocol = apiConfig.value(configKey::serviceProtocol).toString(); + + ApiPayloadData apiPayloadData = generateApiPayloadData(serviceProtocol); + + QJsonObject apiPayload = fillApiPayload(serviceProtocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = userCountryCode; + apiPayload[configKey::serviceType] = serviceType; + apiPayload[configKey::uuid] = installationUuid; + + if (!newCountryCode.isEmpty()) { + apiPayload[configKey::serverCountryCode] = newCountryCode; + } + if (!authData.isEmpty()) { + apiPayload[configKey::authData] = authData; + } + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody); + + QJsonObject newServerConfig; + if (errorCode == ErrorCode::NoError) { + fillServerConfig(serviceProtocol, apiPayloadData, responseBody, newServerConfig); + + QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject(); + newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode)); + newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType)); + newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol)); + newApiConfig.insert(apiDefs::key::vpnKey, apiConfig.value(apiDefs::key::vpnKey)); + + newServerConfig.insert(configKey::apiConfig, newApiConfig); + newServerConfig.insert(configKey::authData, authData); + // newServerConfig.insert( + + m_serversModel->editServer(newServerConfig, serverIndex); + if (reloadServiceConfig) { + emit reloadServerFromApiFinished(tr("API config reloaded")); + } else if (newCountryName.isEmpty()) { + emit updateServerFromApiFinished(); + } else { + emit changeApiCountryFinished(tr("Successfully changed the country of connection to %1").arg(newCountryName)); + } + return true; + } else { + emit errorOccurred(errorCode); + return false; + } +} + +bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) +{ + auto serverConfig = m_serversModel->getServerConfig(serverIndex); + auto installationUuid = m_settings->getInstallationUuid(true); + +#ifdef Q_OS_IOS + IosController::Instance()->requestInetAccess(); + QThread::msleep(10); +#endif + + if (serverConfig.value(config_key::configVersion).toInt()) { + QNetworkRequest request; + request.setTransferTimeout(apiDefs::requestTimeoutMsecs); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); + QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); + request.setUrl(endpoint); + + QString protocol = serverConfig.value(configKey::protocol).toString(); + + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::uuid] = installationUuid; + + QByteArray requestBody = QJsonDocument(apiPayload).toJson(); + + QNetworkReply *reply = amnApp->networkManager()->post(request, requestBody); + + QEventLoop wait; + connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); + + QList sslErrors; + connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); + wait.exec(); + + auto errorCode = 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; +} + +bool ApiConfigsController::deactivateDevice() +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverIndex = m_serversModel->getProcessedServerIndex(); + auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) { + return true; + } + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = apiConfigObject.value(configKey::serverCountryCode); + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + apiPayload[configKey::uuid] = m_settings->getInstallationUuid(true); + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { + emit errorOccurred(errorCode); + return false; + } + + serverConfigObject.remove(config_key::containers); + m_serversModel->editServer(serverConfigObject, serverIndex); + + return true; +} + +bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode) +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs); + + auto serverIndex = m_serversModel->getProcessedServerIndex(); + auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); + auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); + + if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) { + return true; + } + + QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString(); + ApiPayloadData apiPayloadData = generateApiPayloadData(protocol); + + QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData); + apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode); + apiPayload[configKey::serverCountryCode] = serverCountryCode; + apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType); + apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData); + apiPayload[configKey::uuid] = uuid; + + QByteArray responseBody; + ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) { + emit errorOccurred(errorCode); + return false; + } + + if (uuid == m_settings->getInstallationUuid(true)) { + serverConfigObject.remove(config_key::containers); + m_serversModel->editServer(serverConfigObject, serverIndex); + } + + return true; +} + +bool ApiConfigsController::isConfigValid() +{ + int serverIndex = m_serversModel->getDefaultServerIndex(); + QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex); + auto configSource = apiUtils::getConfigSource(serverConfigObject); + + if (configSource == apiDefs::ConfigSource::Telegram + && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + m_serversModel->removeApiConfig(serverIndex); + return updateServiceFromTelegram(serverIndex); + } else if (configSource == apiDefs::ConfigSource::AmneziaGateway + && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + return updateServiceFromGateway(serverIndex, "", ""); + } else if (configSource && m_serversModel->isApiKeyExpired(serverIndex)) { + qDebug() << "attempt to update api config by expires_at event"; + if (configSource == apiDefs::ConfigSource::AmneziaGateway) { + return updateServiceFromGateway(serverIndex, "", ""); + } else { + m_serversModel->removeApiConfig(serverIndex); + return updateServiceFromTelegram(serverIndex); + } + } + return true; +} + +ApiConfigsController::ApiPayloadData ApiConfigsController::generateApiPayloadData(const QString &protocol) +{ + ApiConfigsController::ApiPayloadData apiPayload; + if (protocol == configKey::cloak) { + apiPayload.certRequest = OpenVpnConfigurator::createCertRequest(); + } else if (protocol == configKey::awg) { + auto connData = WireguardConfigurator::genClientKeys(); + apiPayload.wireGuardClientPubKey = connData.clientPubKey; + apiPayload.wireGuardClientPrivKey = connData.clientPrivKey; + } + return apiPayload; +} + +QJsonObject ApiConfigsController::fillApiPayload(const QString &protocol, const ApiPayloadData &apiPayloadData) +{ + QJsonObject obj; + if (protocol == configKey::cloak) { + obj[configKey::certificate] = apiPayloadData.certRequest.request; + } else if (protocol == configKey::awg) { + obj[configKey::publicKey] = apiPayloadData.wireGuardClientPubKey; + } + + obj[configKey::osVersion] = QSysInfo::productType(); + obj[configKey::appVersion] = QString(APP_VERSION); + + return obj; +} + +void ApiConfigsController::fillServerConfig(const QString &protocol, const ApiPayloadData &apiPayloadData, + const QByteArray &apiResponseBody, QJsonObject &serverConfig) +{ + QString data = QJsonDocument::fromJson(apiResponseBody).object().value(config_key::config).toString(); + + data.replace("vpn://", ""); + QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + if (ba.isEmpty()) { + emit errorOccurred(ErrorCode::ApiConfigEmptyError); + return; + } + + QByteArray ba_uncompressed = qUncompress(ba); + if (!ba_uncompressed.isEmpty()) { + ba = ba_uncompressed; + } + + QString configStr = ba; + if (protocol == configKey::cloak) { + configStr.replace("", "\n"); + configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey); + } else if (protocol == configKey::awg) { + configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); + auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + auto containers = newServerConfig.value(config_key::containers).toArray(); + if (containers.isEmpty()) { + return; // todo process error + } + auto container = containers.at(0).toObject(); + QString containerName = ContainerProps::containerTypeToString(DockerContainer::Awg); + auto containerConfig = container.value(containerName).toObject(); + auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object(); + containerConfig[config_key::junkPacketCount] = protocolConfig.value(config_key::junkPacketCount); + containerConfig[config_key::junkPacketMinSize] = protocolConfig.value(config_key::junkPacketMinSize); + containerConfig[config_key::junkPacketMaxSize] = protocolConfig.value(config_key::junkPacketMaxSize); + containerConfig[config_key::initPacketJunkSize] = protocolConfig.value(config_key::initPacketJunkSize); + containerConfig[config_key::responsePacketJunkSize] = protocolConfig.value(config_key::responsePacketJunkSize); + containerConfig[config_key::initPacketMagicHeader] = protocolConfig.value(config_key::initPacketMagicHeader); + containerConfig[config_key::responsePacketMagicHeader] = protocolConfig.value(config_key::responsePacketMagicHeader); + containerConfig[config_key::underloadPacketMagicHeader] = protocolConfig.value(config_key::underloadPacketMagicHeader); + containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader); + container[containerName] = containerConfig; + containers.replace(0, container); + newServerConfig[config_key::containers] = containers; + configStr = QString(QJsonDocument(newServerConfig).toJson()); + } + + QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object(); + serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1); + serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2); + serverConfig[config_key::containers] = newServerConfig.value(config_key::containers); + serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName); + + if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { + serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion); + serverConfig[config_key::description] = newServerConfig.value(config_key::description); + serverConfig[config_key::name] = newServerConfig.value(config_key::name); + } + + auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString(); + serverConfig[config_key::defaultContainer] = defaultContainer; + + QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap(); + map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap()); + auto apiConfig = QJsonObject::fromVariantMap(map); + + if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { + apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject()); + } + + serverConfig[configKey::apiConfig] = apiConfig; + + return; +} + +QList ApiConfigsController::getQrCodes() +{ + return m_qrCodes; +} + +int ApiConfigsController::getQrCodesCount() +{ + return m_qrCodes.size(); +} + +QString ApiConfigsController::getVpnKey() +{ + return m_vpnKey; +} diff --git a/client/ui/controllers/api/apiConfigsController.h b/client/ui/controllers/api/apiConfigsController.h new file mode 100644 index 00000000..2fe981e4 --- /dev/null +++ b/client/ui/controllers/api/apiConfigsController.h @@ -0,0 +1,74 @@ +#ifndef APICONFIGSCONTROLLER_H +#define APICONFIGSCONTROLLER_H + +#include + +#include "configurators/openvpn_configurator.h" +#include "ui/models/api/apiServicesModel.h" +#include "ui/models/servers_model.h" + +class ApiConfigsController : public QObject +{ + Q_OBJECT +public: + ApiConfigsController(const QSharedPointer &serversModel, const QSharedPointer &apiServicesModel, + const std::shared_ptr &settings, QObject *parent = nullptr); + + Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) + Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady) + Q_PROPERTY(QString vpnKey READ getVpnKey NOTIFY vpnKeyExportReady) + +public slots: + bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); + bool revokeNativeConfig(const QString &serverCountryCode); + // bool exportVpnKey(const QString &fileName); + void prepareVpnKeyExport(); + void copyVpnKeyToClipboard(); + + bool fillAvailableServices(); + bool importServiceFromGateway(); + bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, + bool reloadServiceConfig = false); + bool updateServiceFromTelegram(const int serverIndex); + bool deactivateDevice(); + bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode); + + bool isConfigValid(); + +signals: + void errorOccurred(ErrorCode errorCode); + + void installServerFromApiFinished(const QString &message); + void changeApiCountryFinished(const QString &message); + void reloadServerFromApiFinished(const QString &message); + void updateServerFromApiFinished(); + + void vpnKeyExportReady(); + +private: + struct ApiPayloadData + { + OpenVpnConfigurator::ConnectionData certRequest; + + QString wireGuardClientPrivKey; + QString wireGuardClientPubKey; + }; + + ApiPayloadData generateApiPayloadData(const QString &protocol); + QJsonObject fillApiPayload(const QString &protocol, const ApiPayloadData &apiPayloadData); + void fillServerConfig(const QString &protocol, const ApiPayloadData &apiPayloadData, const QByteArray &apiResponseBody, + QJsonObject &serverConfig); + + QList getQrCodes(); + int getQrCodesCount(); + QString getVpnKey(); + + QList m_qrCodes; + QString m_vpnKey; + + QSharedPointer m_serversModel; + QSharedPointer m_apiServicesModel; + std::shared_ptr m_settings; +}; + +#endif // APICONFIGSCONTROLLER_H diff --git a/client/ui/controllers/api/apiSettingsController.cpp b/client/ui/controllers/api/apiSettingsController.cpp new file mode 100644 index 00000000..737bfd1a --- /dev/null +++ b/client/ui/controllers/api/apiSettingsController.cpp @@ -0,0 +1,93 @@ +#include "apiSettingsController.h" + +#include +#include + +#include "core/api/apiUtils.h" +#include "core/controllers/gatewayController.h" + +namespace +{ + namespace configKey + { + constexpr char userCountryCode[] = "user_country_code"; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceInfo[] = "service_info"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; + } + + const int requestTimeoutMsecs = 12 * 1000; // 12 secs +} + +ApiSettingsController::ApiSettingsController(const QSharedPointer &serversModel, + const QSharedPointer &apiAccountInfoModel, + const QSharedPointer &apiCountryModel, + const QSharedPointer &apiDevicesModel, + const std::shared_ptr &settings, QObject *parent) + : QObject(parent), + m_serversModel(serversModel), + m_apiAccountInfoModel(apiAccountInfoModel), + m_apiCountryModel(apiCountryModel), + m_apiDevicesModel(apiDevicesModel), + m_settings(settings) +{ +} + +ApiSettingsController::~ApiSettingsController() +{ +} + +bool ApiSettingsController::getAccountInfo(bool reload) +{ + if (reload) { + QEventLoop wait; + QTimer::singleShot(1000, &wait, &QEventLoop::quit); + wait.exec(); + } + + GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs); + + auto processedIndex = m_serversModel->getProcessedServerIndex(); + auto serverConfig = m_serversModel->getServerConfig(processedIndex); + auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + auto authData = serverConfig.value(configKey::authData).toObject(); + + QJsonObject apiPayload; + apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString(); + apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString(); + apiPayload[configKey::authData] = authData; + + QByteArray responseBody; + + if (apiUtils::getConfigType(serverConfig) == apiDefs::ConfigType::AmneziaPremiumV2) { + ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody); + if (errorCode != ErrorCode::NoError) { + emit errorOccurred(errorCode); + return false; + } + } + + QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object(); + m_apiAccountInfoModel->updateModel(accountInfo, serverConfig); + + if (reload) { + updateApiCountryModel(); + updateApiDevicesModel(); + } + + return true; +} + +void ApiSettingsController::updateApiCountryModel() +{ + m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); + m_apiCountryModel->updateIssuedConfigsInfo(m_apiAccountInfoModel->getIssuedConfigsInfo()); +} + +void ApiSettingsController::updateApiDevicesModel() +{ + m_apiDevicesModel->updateModel(m_apiAccountInfoModel->getIssuedConfigsInfo()); +} diff --git a/client/ui/controllers/api/apiSettingsController.h b/client/ui/controllers/api/apiSettingsController.h new file mode 100644 index 00000000..afe9a570 --- /dev/null +++ b/client/ui/controllers/api/apiSettingsController.h @@ -0,0 +1,37 @@ +#ifndef APISETTINGSCONTROLLER_H +#define APISETTINGSCONTROLLER_H + +#include + +#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 +{ + Q_OBJECT +public: + ApiSettingsController(const QSharedPointer &serversModel, const QSharedPointer &apiAccountInfoModel, + const QSharedPointer &apiCountryModel, const QSharedPointer &apiDevicesModel, + const std::shared_ptr &settings, QObject *parent = nullptr); + ~ApiSettingsController(); + +public slots: + bool getAccountInfo(bool reload); + void updateApiCountryModel(); + void updateApiDevicesModel(); + +signals: + void errorOccurred(ErrorCode errorCode); + +private: + QSharedPointer m_serversModel; + QSharedPointer m_apiAccountInfoModel; + QSharedPointer m_apiCountryModel; + QSharedPointer m_apiDevicesModel; + + std::shared_ptr m_settings; +}; + +#endif // APISETTINGSCONTROLLER_H diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index 76c352f4..9fc60493 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -5,7 +5,6 @@ #else #include #endif -#include #include "core/controllers/vpnConfigurationController.h" #include "version.h" @@ -16,7 +15,6 @@ ConnectionController::ConnectionController(const QSharedPointer &s const QSharedPointer &vpnConnection, const std::shared_ptr &settings, QObject *parent) : QObject(parent), - m_apiController(this), m_serversModel(serversModel), m_containersModel(containersModel), m_clientManagementModel(clientManagementModel), @@ -27,9 +25,7 @@ ConnectionController::ConnectionController(const QSharedPointer &s connect(this, &ConnectionController::connectToVpn, m_vpnConnection.get(), &VpnConnection::connectToVpn, Qt::QueuedConnection); connect(this, &ConnectionController::disconnectFromVpn, m_vpnConnection.get(), &VpnConnection::disconnectFromVpn, Qt::QueuedConnection); - connect(&m_apiController, &ApiController::configUpdated, this, - static_cast(&ConnectionController::openConnection)); - connect(&m_apiController, qOverload(&ApiController::errorOccurred), this, qOverload(&ConnectionController::connectionErrorOccurred)); + connect(this, &ConnectionController::connectButtonClicked, this, &ConnectionController::toggleConnection, Qt::QueuedConnection); m_state = Vpn::ConnectionState::Disconnected; } @@ -37,8 +33,7 @@ ConnectionController::ConnectionController(const QSharedPointer &s void ConnectionController::openConnection() { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) - if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) - { + if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) { emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning); return; } @@ -47,14 +42,23 @@ void ConnectionController::openConnection() int serverIndex = m_serversModel->getDefaultServerIndex(); QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing); + DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); - if (serverConfig.value(config_key::configVersion).toInt() - && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { - m_apiController.updateServerConfigFromApi(m_settings->getInstallationUuid(true), serverIndex, serverConfig); - } else { - openConnection(false, serverConfig, serverIndex); + if (!m_containersModel->isSupportedByCurrentPlatform(container)) { + emit connectionErrorOccurred(ErrorCode::NotSupportedOnThisPlatform); + return; } + + QSharedPointer serverController(new ServerController(m_settings)); + VpnConfigurationsController vpnConfigurationController(m_settings, serverController); + + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + auto dns = m_serversModel->getDnsPair(serverIndex); + + auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container); + emit connectToVpn(serverIndex, credentials, container, vpnConfiguration); } void ConnectionController::closeConnection() @@ -158,7 +162,7 @@ void ConnectionController::toggleConnection() } else if (isConnected()) { closeConnection(); } else { - openConnection(); + emit prepareConfig(); } } @@ -171,99 +175,3 @@ bool ConnectionController::isConnected() const { return m_isConnected; } - -bool ConnectionController::isProtocolConfigExists(const QJsonObject &containerConfig, const DockerContainer container) -{ - for (Proto protocol : ContainerProps::protocolsForContainer(container)) { - QString protocolConfig = - containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString(); - - if (protocolConfig.isEmpty()) { - return false; - } - } - return true; -} - -void ConnectionController::openConnection(const bool updateConfig, const QJsonObject &config, const int serverIndex) -{ - // Update config for this server as it was received from API - if (updateConfig) { - m_serversModel->editServer(config, serverIndex); - } - - if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { - emit noInstalledContainers(); - emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); - return; - } - - DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); - - if (!m_containersModel->isSupportedByCurrentPlatform(container)) { - emit connectionErrorOccurred(tr("The selected protocol is not supported on the current platform")); - return; - } - - if (container == DockerContainer::None) { - emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first")); - return; - } - - QSharedPointer serverController(new ServerController(m_settings)); - VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - - QJsonObject containerConfig = m_containersModel->getContainerConfig(container); - ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); - ErrorCode errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController); - if (errorCode != ErrorCode::NoError) { - emit connectionErrorOccurred(errorCode); - return; - } - - auto dns = m_serversModel->getDnsPair(serverIndex); - - auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, config, containerConfig, container, errorCode); - if (errorCode != ErrorCode::NoError) { - emit connectionErrorOccurred(tr("unable to create configuration")); - return; - } - - emit connectToVpn(serverIndex, credentials, container, vpnConfiguration); -} - -ErrorCode ConnectionController::updateProtocolConfig(const DockerContainer container, const ServerCredentials &credentials, - QJsonObject &containerConfig, QSharedPointer serverController) -{ - QFutureWatcher watcher; - - if (serverController.isNull()) { - serverController.reset(new ServerController(m_settings)); - } - - QFuture future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() { - ErrorCode errorCode = ErrorCode::NoError; - if (!isProtocolConfigExists(containerConfig, container)) { - VpnConfigurationsController vpnConfigurationController(m_settings, serverController); - errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - m_serversModel->updateContainerConfig(container, containerConfig); - - errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, - QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - } - return errorCode; - }); - - QEventLoop wait; - connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); - watcher.setFuture(future); - wait.exec(); - - return watcher.result(); -} diff --git a/client/ui/controllers/connectionController.h b/client/ui/controllers/connectionController.h index bc8f7e2f..cabeb601 100644 --- a/client/ui/controllers/connectionController.h +++ b/client/ui/controllers/connectionController.h @@ -1,7 +1,6 @@ #ifndef CONNECTIONCONTROLLER_H #define CONNECTIONCONTROLLER_H -#include "core/controllers/apiController.h" #include "protocols/vpnprotocol.h" #include "ui/models/clientManagementModel.h" #include "ui/models/containers_model.h" @@ -41,30 +40,22 @@ public slots: void onTranslationsUpdated(); - ErrorCode updateProtocolConfig(const DockerContainer container, const ServerCredentials &credentials, QJsonObject &containerConfig, - QSharedPointer serverController = nullptr); - signals: void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); void disconnectFromVpn(); void connectionStateChanged(); - void connectionErrorOccurred(const QString &errorMessage); void connectionErrorOccurred(ErrorCode errorCode); void reconnectWithUpdatedContainer(const QString &message); - void noInstalledContainers(); - void connectButtonClicked(); void preparingConfig(); + void prepareConfig(); private: Vpn::ConnectionState getCurrentConnectionState(); - bool isProtocolConfigExists(const QJsonObject &containerConfig, const DockerContainer container); - void openConnection(const bool updateConfig, const QJsonObject &config, const int serverIndex); - - ApiController m_apiController; + void continueConnection(); QSharedPointer m_serversModel; QSharedPointer m_containersModel; diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index a5605674..b47111ae 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -9,11 +9,8 @@ #include #include "core/controllers/vpnConfigurationController.h" +#include "core/qrCodeUtils.h" #include "systemController.h" -#ifdef Q_OS_ANDROID - #include "platforms/android/android_utils.h" -#endif -#include "qrcodegen.hpp" ExportController::ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const QSharedPointer &clientManagementModel, @@ -24,12 +21,6 @@ ExportController::ExportController(const QSharedPointer &serversMo m_clientManagementModel(clientManagementModel), m_settings(settings) { -#ifdef Q_OS_ANDROID - m_authResultNotifier.reset(new AuthResultNotifier); - m_authResultReceiver.reset(new AuthResultReceiver(m_authResultNotifier)); - connect(m_authResultNotifier.get(), &AuthResultNotifier::authFailed, this, [this]() { emit exportErrorOccurred(tr("Access error!")); }); - connect(m_authResultNotifier.get(), &AuthResultNotifier::authSuccessful, this, &ExportController::generateFullAccessConfig); -#endif } void ExportController::generateFullAccessConfig() @@ -59,30 +50,10 @@ void ExportController::generateFullAccessConfig() compressedConfig = qCompress(compressedConfig, 8); m_config = QString("vpn://%1").arg(QString(compressedConfig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))); - m_qrCodes = generateQrCodeImageSeries(compressedConfig); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(compressedConfig); emit exportConfigChanged(); } -#if defined(Q_OS_ANDROID) -void ExportController::generateFullAccessConfigAndroid() -{ - /* We use builtin keyguard for ssh key export protection on Android */ - QJniObject activity = AndroidUtils::getActivity(); - auto appContext = activity.callObjectMethod("getApplicationContext", "()Landroid/content/Context;"); - if (appContext.isValid()) { - auto intent = QJniObject::callStaticObjectMethod("org/amnezia/vpn/AuthHelper", "getAuthIntent", - "(Landroid/content/Context;)Landroid/content/Intent;", appContext.object()); - if (intent.isValid()) { - if (intent.object() != nullptr) { - QtAndroidPrivate::startActivity(intent.object(), 1, m_authResultReceiver.get()); - } - } else { - generateFullAccessConfig(); - } - } -} -#endif - void ExportController::generateConnectionConfig(const QString &clientName) { clearPreviousConfig(); @@ -121,7 +92,7 @@ void ExportController::generateConnectionConfig(const QString &clientName) compressedConfig = qCompress(compressedConfig, 8); m_config = QString("vpn://%1").arg(QString(compressedConfig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))); - m_qrCodes = generateQrCodeImageSeries(compressedConfig); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(compressedConfig); emit exportConfigChanged(); } @@ -133,7 +104,7 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); auto dns = m_serversModel->getDnsPair(serverIndex); - bool isApiConfig = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::IsServerFromApiRole)); + bool isApiConfig = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::IsServerFromTelegramApiRole)); QJsonObject containerConfig = m_containersModel->getContainerConfig(container); containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); @@ -150,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -179,7 +149,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName) m_config.append(line + "\n"); } - m_qrCodes = generateQrCodeImageSeries(m_config.toUtf8()); + m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8()); emit exportConfigChanged(); } @@ -197,8 +167,8 @@ void ExportController::generateWireGuardConfig(const QString &clientName) m_config.append(line + "\n"); } - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW); - m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -217,8 +187,8 @@ void ExportController::generateAwgConfig(const QString &clientName) m_config.append(line + "\n"); } - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW); - m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -251,8 +221,8 @@ void ExportController::generateShadowSocksConfig() m_nativeConfigString = "ss://" + m_nativeConfigString.toUtf8().toBase64(); - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_nativeConfigString.toUtf8(), qrcodegen::QrCode::Ecc::LOW); - m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + auto qr = qrCodeUtils::generateQrCode(m_nativeConfigString.toUtf8()); + m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1))); emit exportConfigChanged(); } @@ -277,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; @@ -342,32 +312,6 @@ void ExportController::renameClient(const int row, const QString &clientName, co } } -QList ExportController::generateQrCodeImageSeries(const QByteArray &data) -{ - double k = 850; - - quint8 chunksCount = std::ceil(data.size() / k); - QList chunks; - for (int i = 0; i < data.size(); i = i + k) { - QByteArray chunk; - QDataStream s(&chunk, QIODevice::WriteOnly); - s << amnezia::qrMagicCode << chunksCount << (quint8)std::round(i / k) << data.mid(i, k); - - QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(ba, qrcodegen::QrCode::Ecc::LOW); - QString svg = QString::fromStdString(toSvgString(qr, 1)); - chunks.append(svgToBase64(svg)); - } - - return chunks; -} - -QString ExportController::svgToBase64(const QString &image) -{ - return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); -} - int ExportController::getQrCodesCount() { return m_qrCodes.size(); diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index 023f22cf..5fb3e6b3 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -6,9 +6,6 @@ #include "ui/models/clientManagementModel.h" #include "ui/models/containers_model.h" #include "ui/models/servers_model.h" -#ifdef Q_OS_ANDROID - #include "platforms/android/authResultReceiver.h" -#endif class ExportController : public QObject { @@ -25,16 +22,13 @@ public: public slots: void generateFullAccessConfig(); -#if defined(Q_OS_ANDROID) - void generateFullAccessConfigAndroid(); -#endif void generateConnectionConfig(const QString &clientName); void generateOpenVpnConfig(const QString &clientName); void generateWireGuardConfig(const QString &clientName); void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); @@ -56,9 +50,6 @@ signals: void saveFile(const QString &fileName, const QString &data); private: - QList generateQrCodeImageSeries(const QByteArray &data); - QString svgToBase64(const QString &image); - int getQrCodesCount(); void clearPreviousConfig(); @@ -74,11 +65,6 @@ private: QString m_config; QString m_nativeConfigString; QList m_qrCodes; - -#ifdef Q_OS_ANDROID - QSharedPointer m_authResultNotifier; - QSharedPointer m_authResultReceiver; -#endif }; #endif // EXPORTCONTROLLER_H diff --git a/client/ui/controllers/focusController.cpp b/client/ui/controllers/focusController.cpp new file mode 100644 index 00000000..54bdc7d8 --- /dev/null +++ b/client/ui/controllers/focusController.cpp @@ -0,0 +1,209 @@ +#include "focusController.h" +#include "utils/qmlUtils.h" + +#include +#include + +FocusController::FocusController(QQmlApplicationEngine *engine, QObject *parent) + : QObject { parent }, + m_engine { engine }, + m_focusChain {}, + m_focusedItem { nullptr }, + m_rootObjects {}, + m_defaultFocusItem { nullptr }, + m_lvfc { nullptr } +{ + QObject::connect(m_engine, &QQmlApplicationEngine::objectCreated, this, [this](QObject *object, const QUrl &url) { + QQuickItem *newDefaultFocusItem = object->findChild("defaultFocusItem"); + if (newDefaultFocusItem && m_defaultFocusItem != newDefaultFocusItem) { + m_defaultFocusItem = newDefaultFocusItem; + } + }); + + QObject::connect(this, &FocusController::focusedItemChanged, this, + [this]() { m_focusedItem->forceActiveFocus(Qt::TabFocusReason); }); +} + +void FocusController::nextKeyTabItem() +{ + nextItem(Direction::Forward); +} + +void FocusController::previousKeyTabItem() +{ + nextItem(Direction::Backward); +} + +void FocusController::nextKeyUpItem() +{ + nextItem(Direction::Backward); +} + +void FocusController::nextKeyDownItem() +{ + nextItem(Direction::Forward); +} + +void FocusController::nextKeyLeftItem() +{ + nextItem(Direction::Backward); +} + +void FocusController::nextKeyRightItem() +{ + nextItem(Direction::Forward); +} + +void FocusController::setFocusItem(QQuickItem *item) +{ + if (m_focusedItem != item) { + m_focusedItem = item; + } + emit focusedItemChanged(); +} + +void FocusController::setFocusOnDefaultItem() +{ + setFocusItem(m_defaultFocusItem); +} + +void FocusController::pushRootObject(QObject *object) +{ + m_rootObjects.push(object); + dropListView(); + // setFocusOnDefaultItem(); +} + +void FocusController::dropRootObject(QObject *object) +{ + if (m_rootObjects.empty()) { + return; + } + + if (m_rootObjects.top() == object) { + m_rootObjects.pop(); + dropListView(); + setFocusOnDefaultItem(); + } else { + qWarning() << "===>> TRY TO DROP WRONG ROOT OBJECT: " << m_rootObjects.top() << " SHOULD BE: " << object; + } +} + +void FocusController::resetRootObject() +{ + m_rootObjects.clear(); +} + +void FocusController::reload(Direction direction) +{ + m_focusChain.clear(); + + QObject *rootObject = (m_rootObjects.empty() ? m_engine->rootObjects().value(0) : m_rootObjects.top()); + + if (!rootObject) { + qCritical() << "No ROOT OBJECT found!"; + resetRootObject(); + dropListView(); + return; + } + + m_focusChain.append(FocusControl::getSubChain(rootObject)); + + std::sort(m_focusChain.begin(), m_focusChain.end(), + direction == Direction::Forward ? FocusControl::isLess : FocusControl::isMore); + + if (m_focusChain.empty()) { + qWarning() << "Focus chain is empty!"; + resetRootObject(); + dropListView(); + return; + } +} + +void FocusController::nextItem(Direction direction) +{ + reload(direction); + + if (m_lvfc && FocusControl::isListView(m_focusedItem)) { + direction == Direction::Forward ? focusNextListViewItem() : focusPreviousListViewItem(); + + return; + } + + if (m_focusChain.empty()) { + qWarning() << "There are no items to navigate"; + setFocusOnDefaultItem(); + return; + } + + auto focusedItemIndex = m_focusChain.indexOf(m_focusedItem); + + if (focusedItemIndex == -1) { + focusedItemIndex = 0; + } else if (focusedItemIndex == (m_focusChain.size() - 1)) { + focusedItemIndex = 0; + } else { + focusedItemIndex++; + } + + const auto focusedItem = qobject_cast(m_focusChain.at(focusedItemIndex)); + + if (focusedItem == nullptr) { + qWarning() << "Failed to get item to focus on. Setting focus on default"; + setFocusOnDefaultItem(); + return; + } + + if (FocusControl::isListView(focusedItem)) { + m_lvfc = new ListViewFocusController(focusedItem, this); + m_focusedItem = focusedItem; + if (direction == Direction::Forward) { + m_lvfc->nextDelegate(); + focusNextListViewItem(); + } else { + m_lvfc->previousDelegate(); + focusPreviousListViewItem(); + } + return; + } + + setFocusItem(focusedItem); +} + +void FocusController::focusNextListViewItem() +{ + m_lvfc->reloadFocusChain(); + if (m_lvfc->isLastFocusItemInListView() || m_lvfc->isReturnNeeded()) { + dropListView(); + nextItem(Direction::Forward); + return; + } else if (m_lvfc->isLastFocusItemInDelegate()) { + m_lvfc->resetFocusChain(); + m_lvfc->nextDelegate(); + } + + m_lvfc->focusNextItem(); +} + +void FocusController::focusPreviousListViewItem() +{ + m_lvfc->reloadFocusChain(); + if (m_lvfc->isFirstFocusItemInListView() || m_lvfc->isReturnNeeded()) { + dropListView(); + nextItem(Direction::Backward); + return; + } else if (m_lvfc->isFirstFocusItemInDelegate()) { + m_lvfc->resetFocusChain(); + m_lvfc->previousDelegate(); + } + + m_lvfc->focusPreviousItem(); +} + +void FocusController::dropListView() +{ + if (m_lvfc) { + delete m_lvfc; + m_lvfc = nullptr; + } +} diff --git a/client/ui/controllers/focusController.h b/client/ui/controllers/focusController.h new file mode 100644 index 00000000..11074dae --- /dev/null +++ b/client/ui/controllers/focusController.h @@ -0,0 +1,57 @@ +#ifndef FOCUSCONTROLLER_H +#define FOCUSCONTROLLER_H + +#include "ui/controllers/listViewFocusController.h" + +#include + +/*! + * \brief The FocusController class makes focus control more straightforward + * \details Focus is handled only for visible and enabled items which have + * `isFocused` property from top left to bottom right. + * \note There are items handled differently (e.g. ListView) + */ +class FocusController : public QObject +{ + Q_OBJECT +public: + explicit FocusController(QQmlApplicationEngine *engine, QObject *parent = nullptr); + ~FocusController() override = default; + + Q_INVOKABLE void nextKeyTabItem(); + Q_INVOKABLE void previousKeyTabItem(); + Q_INVOKABLE void nextKeyUpItem(); + Q_INVOKABLE void nextKeyDownItem(); + Q_INVOKABLE void nextKeyLeftItem(); + Q_INVOKABLE void nextKeyRightItem(); + Q_INVOKABLE void setFocusItem(QQuickItem *item); + Q_INVOKABLE void setFocusOnDefaultItem(); + Q_INVOKABLE void pushRootObject(QObject *object); + Q_INVOKABLE void dropRootObject(QObject *object); + Q_INVOKABLE void resetRootObject(); + +private: + enum class Direction { + Forward, + Backward, + }; + + void reload(Direction direction); + void nextItem(Direction direction); + void focusNextListViewItem(); + void focusPreviousListViewItem(); + void dropListView(); + + QQmlApplicationEngine *m_engine; // Pointer to engine to get root object + QList m_focusChain; // List of current objects to be focused + QQuickItem *m_focusedItem; // Pointer to the active focus item + QStack m_rootObjects; // Pointer to stack of roots for focus chain + QQuickItem *m_defaultFocusItem; + + ListViewFocusController *m_lvfc; // ListView focus manager + +signals: + void focusedItemChanged(); +}; + +#endif // FOCUSCONTROLLER_H diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 60a90e4e..4ca12e21 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -4,12 +4,16 @@ #include #include #include -#include #include +#include -#include "utilities.h" -#include "core/serialization/serialization.h" +#include "core/api/apiDefs.h" +#include "core/api/apiUtils.h" #include "core/errorstrings.h" +#include "core/qrCodeUtils.h" +#include "core/serialization/serialization.h" +#include "systemController.h" +#include "utilities.h" #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" @@ -23,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"; @@ -39,22 +41,23 @@ namespace const QString amneziaConfigPatternUserName = "userName"; const QString amneziaConfigPatternPassword = "password"; const QString amneziaFreeConfigPattern = "api_key"; + const QString amneziaPremiumConfigPattern = "auth_data"; const QString backupPattern = "Servers/serversList"; if (config.contains(backupPattern)) { return ConfigTypes::Backup; } else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) + || config.contains(amneziaPremiumConfigPattern) || (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; } @@ -75,17 +78,18 @@ ImportController::ImportController(const QSharedPointer &serversMo bool ImportController::extractConfigFromFile(const QString &fileName) { - QFile file(fileName); - - if (file.open(QIODevice::ReadOnly)) { - QString data = file.readAll(); - - m_configFileName = QFileInfo(file.fileName()).fileName(); - return extractConfigFromData(data); + QString data; + if (!SystemController::readFile(fileName, data)) { + emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); + return false; } - - emit importErrorOccurred(tr("Unable to open file"), false); - return false; + m_configFileName = QFileInfo(QFile(fileName).fileName()).fileName(); +#ifdef Q_OS_ANDROID + if (m_configFileName.isEmpty()) { + m_configFileName = AndroidController::instance()->getFileName(fileName); + } +#endif + return extractConfigFromData(data); } bool ImportController::extractConfigFromData(QString data) @@ -96,36 +100,40 @@ bool ImportController::extractConfigFromData(QString data) if (config.startsWith("vless://")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("vmess://") && config.contains("@")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("vmess://")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("trojan://")) { m_configType = ConfigTypes::Xray; - m_config = extractXrayConfig(Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), + prefix); return m_config.empty() ? false : true; } if (config.startsWith("ss://") && !config.contains("plugin=")) { m_configType = ConfigTypes::ShadowSocks; - m_config = extractXrayConfig(Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg), - QJsonDocument::JsonFormat::Compact), prefix); + m_config = extractXrayConfig( + Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact), prefix); return m_config.empty() ? false : true; } @@ -142,11 +150,11 @@ bool ImportController::extractConfigFromData(QString data) m_configType = checkConfigFormat(config); if (m_configType == ConfigTypes::Invalid) { - data.replace("vpn://", ""); - QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - QByteArray ba_uncompressed = qUncompress(ba); - if (!ba_uncompressed.isEmpty()) { - ba = ba_uncompressed; + config.replace("vpn://", ""); + QByteArray ba = QByteArray::fromBase64(config.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + QByteArray baUncompressed = qUncompress(ba); + if (!baUncompressed.isEmpty()) { + ba = baUncompressed; } config = ba; @@ -173,6 +181,14 @@ bool ImportController::extractConfigFromData(QString data) } case ConfigTypes::Amnezia: { m_config = QJsonDocument::fromJson(config.toUtf8()).object(); + + if (apiUtils::isServerFromApi(m_config)) { + auto apiConfig = m_config.value(apiDefs::key::apiConfig).toObject(); + apiConfig[apiDefs::key::vpnKey] = data; + m_config[apiDefs::key::apiConfig] = apiConfig; + } + + processAmneziaConfig(m_config); if (!m_config.empty()) { checkForMaliciousStrings(m_config); return true; @@ -183,12 +199,12 @@ bool ImportController::extractConfigFromData(QString data) if (!m_serversModel->getServersCount()) { emit restoreAppConfig(config.toUtf8()); } else { - emit importErrorOccurred(tr("Invalid configuration file"), false); + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); } break; } case ConfigTypes::Invalid: { - emit importErrorOccurred(tr("Invalid configuration file"), false); + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); break; } } @@ -209,6 +225,21 @@ bool ImportController::extractConfigFromQr(const QByteArray &data) return true; } + m_configType = checkConfigFormat(data); + if (m_configType == ConfigTypes::Invalid) { + QByteArray ba = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + QByteArray baUncompressed = qUncompress(ba); + + if (!baUncompressed.isEmpty()) { + ba = baUncompressed; + } + + if (!ba.isEmpty()) { + m_config = QJsonDocument::fromJson(ba).object(); + return true; + } + } + return false; } @@ -237,24 +268,26 @@ void ImportController::processNativeWireGuardConfig() auto containers = m_config.value(config_key::containers).toArray(); if (!containers.isEmpty()) { auto container = containers.at(0).toObject(); - auto containerConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject(); - auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object(); + auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject(); + auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object(); - QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(3, 10)); - QString junkPacketMinSize = QString::number(50); - QString junkPacketMaxSize = QString::number(1000); - protocolConfig[config_key::junkPacketCount] = junkPacketCount; - protocolConfig[config_key::junkPacketMinSize] = junkPacketMinSize; - protocolConfig[config_key::junkPacketMaxSize] = junkPacketMaxSize; - protocolConfig[config_key::initPacketJunkSize] = "0"; - protocolConfig[config_key::responsePacketJunkSize] = "0"; - protocolConfig[config_key::initPacketMagicHeader] = "1"; - protocolConfig[config_key::responsePacketMagicHeader] = "2"; - protocolConfig[config_key::underloadPacketMagicHeader] = "3"; - protocolConfig[config_key::transportPacketMagicHeader] = "4"; + QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5)); + QString junkPacketMinSize = QString::number(10); + QString junkPacketMaxSize = QString::number(50); + clientProtocolConfig[config_key::junkPacketCount] = junkPacketCount; + clientProtocolConfig[config_key::junkPacketMinSize] = junkPacketMinSize; + clientProtocolConfig[config_key::junkPacketMaxSize] = junkPacketMaxSize; + clientProtocolConfig[config_key::initPacketJunkSize] = "0"; + clientProtocolConfig[config_key::responsePacketJunkSize] = "0"; + clientProtocolConfig[config_key::initPacketMagicHeader] = "1"; + clientProtocolConfig[config_key::responsePacketMagicHeader] = "2"; + clientProtocolConfig[config_key::underloadPacketMagicHeader] = "3"; + clientProtocolConfig[config_key::transportPacketMagicHeader] = "4"; - containerConfig[config_key::last_config] = QString(QJsonDocument(protocolConfig).toJson()); - container["wireguard"] = containerConfig; + clientProtocolConfig[config_key::isObfuscationEnabled] = true; + + serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(clientProtocolConfig).toJson()); + container["wireguard"] = serverProtocolConfig; containers.replace(0, container); m_config[config_key::containers] = containers; } @@ -309,7 +342,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) arr.push_back(containers); QString hostName; - const static QRegularExpression hostNameRegExp("remote (.*) [0-9]*"); + const static QRegularExpression hostNameRegExp("remote\\s+([^\\s]+)"); QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data); if (hostNameMatch.hasMatch()) { hostName = hostNameMatch.captured(1); @@ -353,20 +386,19 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) QJsonObject lastConfig; lastConfig[config_key::config] = data; - const static QRegularExpression hostNameAndPortRegExp("Endpoint = (.*):([0-9]*)"); - QRegularExpressionMatch hostNameAndPortMatch = hostNameAndPortRegExp.match(data); + auto url { QUrl::fromUserInput(configMap.value("Endpoint")) }; QString hostName; QString port; - if (hostNameAndPortMatch.hasCaptured(1)) { - hostName = hostNameAndPortMatch.captured(1); + if (!url.host().isEmpty()) { + hostName = url.host(); } else { - qDebug() << "Key parameter 'Endpoint' is missing"; + qDebug() << "Key parameter 'Endpoint' is missing or has an invalid format"; emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); return QJsonObject(); } - if (hostNameAndPortMatch.hasCaptured(2)) { - port = hostNameAndPortMatch.captured(2); + if (url.port() != -1) { + port = QString::number(url.port()); } else { port = protocols::wireguard::defaultPort; } @@ -395,7 +427,11 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) lastConfig[config_key::mtu] = configMap.value("MTU"); } - QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(",")); + if (!configMap.value("PersistentKeepalive").isEmpty()) { + lastConfig[config_key::persistent_keep_alive] = configMap.value("PersistentKeepalive"); + } + + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(", ")); lastConfig[config_key::allowed_ips] = allowedIpsJsonArray; @@ -419,6 +455,12 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) m_configType = ConfigTypes::Awg; } + if (!configMap.value("MTU").isEmpty()) { + lastConfig[config_key::mtu] = configMap.value("MTU"); + } else { + lastConfig[config_key::mtu] = protocolName == "awg" ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; + } + QJsonObject wireguardConfig; wireguardConfig[config_key::last_config] = QString(QJsonDocument(lastConfig).toJson()); wireguardConfig[config_key::isThirdPartyConfig] = true; @@ -488,7 +530,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, const QStri if (m_configType == ConfigTypes::ShadowSocks) { config[config_key::defaultContainer] = "amnezia-ssxray"; } else { - config[config_key::defaultContainer] = "amnezia-xray"; + config[config_key::defaultContainer] = "amnezia-xray"; } if (description.isEmpty()) { config[config_key::description] = m_settings->nextAvailableServerName(); @@ -550,7 +592,7 @@ bool ImportController::parseQrCodeChunk(const QString &code) qint16 magic; s >> magic; - if (magic == amnezia::qrMagicCode) { + if (magic == qrCodeUtils::qrMagicCode) { quint8 chunksCount; s >> chunksCount; if (m_totalQrCodeChunksCount != chunksCount) { @@ -646,3 +688,29 @@ void ImportController::checkForMaliciousStrings(const QJsonObject &serverConfig) } } } + +void ImportController::processAmneziaConfig(QJsonObject &config) +{ + auto containers = config.value(config_key::containers).toArray(); + for (auto i = 0; i < containers.size(); i++) { + auto container = containers.at(i).toObject(); + auto dockerContainer = ContainerProps::containerFromString(container.value(config_key::container).toString()); + if (dockerContainer == DockerContainer::Awg || dockerContainer == DockerContainer::WireGuard) { + auto containerConfig = container.value(ContainerProps::containerTypeToString(dockerContainer)).toObject(); + auto protocolConfig = containerConfig.value(config_key::last_config).toString(); + if (protocolConfig.isEmpty()) { + return; + } + + QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object(); + jsonConfig[config_key::mtu] = + dockerContainer == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; + + containerConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); + + container[ContainerProps::containerTypeToString(dockerContainer)] = containerConfig; + containers.replace(i, container); + config.insert(config_key::containers, containers); + } + } +} diff --git a/client/ui/controllers/importController.h b/client/ui/controllers/importController.h index ea1ba6b0..05e320a5 100644 --- a/client/ui/controllers/importController.h +++ b/client/ui/controllers/importController.h @@ -54,7 +54,6 @@ public slots: signals: void importFinished(); - void importErrorOccurred(const QString &errorMessage, bool goToPageHome); void importErrorOccurred(ErrorCode errorCode, bool goToPageHome); void qrDecodingFinished(); @@ -68,6 +67,8 @@ private: void checkForMaliciousStrings(const QJsonObject &protocolConfig); + void processAmneziaConfig(QJsonObject &config); + #if defined Q_OS_ANDROID || defined Q_OS_IOS void stopDecodingQr(); #endif diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp old mode 100644 new mode 100755 index 514091d4..7a6d8d40 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "core/controllers/serverController.h" #include "core/controllers/vpnConfigurationController.h" @@ -14,39 +15,26 @@ #include "ui/models/protocols/awgConfigModel.h" #include "ui/models/protocols/wireguardConfigModel.h" #include "utilities.h" - -#ifdef Q_OS_IOS - #include -#endif +#include "core/api/apiUtils.h" namespace { Logger logger("ServerController"); -#ifdef Q_OS_WINDOWS - QString getNextDriverLetter() + namespace configKey { - QProcess drivesProc; - drivesProc.start("wmic logicaldisk get caption"); - drivesProc.waitForFinished(); - QString drives = drivesProc.readAll(); - qDebug() << drives; + constexpr char serviceInfo[] = "service_info"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceProtocol[] = "service_protocol"; + constexpr char userCountryCode[] = "user_country_code"; - QString letters = "CFGHIJKLMNOPQRSTUVWXYZ"; - QString letter; - for (int i = letters.size() - 1; i > 0; i--) { - letter = letters.at(i); - if (!drives.contains(letter + ":")) - break; - } - if (letter == "C:") { - // set err info - qDebug() << "Can't find free drive letter"; - return ""; - } - return letter; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serverCountryName[] = "server_country_name"; + constexpr char availableCountries[] = "available_countries"; + + constexpr char apiConfig[] = "api_config"; + constexpr char authData[] = "auth_data"; } -#endif } InstallController::InstallController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, @@ -85,9 +73,9 @@ void InstallController::install(DockerContainer container, int port, TransportPr containerConfig.insert(config_key::transport_proto, ProtocolProps::transportProtoToString(transportProto, protocol)); if (container == DockerContainer::Awg) { - QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(3, 10)); - QString junkPacketMinSize = QString::number(50); - QString junkPacketMaxSize = QString::number(1000); + QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5)); + QString junkPacketMinSize = QString::number(10); + QString junkPacketMaxSize = QString::number(50); int s1 = QRandomGenerator::global()->bounded(15, 150); int s2 = QRandomGenerator::global()->bounded(15, 150); @@ -122,10 +110,10 @@ void InstallController::install(DockerContainer container, int port, TransportPr containerConfig[config_key::transportPacketMagicHeader] = transportPacketMagicHeader; } else if (container == DockerContainer::Sftp) { containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName); - containerConfig.insert(config_key::password, Utils::getRandomString(10)); + containerConfig.insert(config_key::password, Utils::getRandomString(16)); } else if (container == DockerContainer::Socks5Proxy) { containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName); - containerConfig.insert(config_key::password, Utils::getRandomString(10)); + containerConfig.insert(config_key::password, Utils::getRandomString(16)); } config.insert(config_key::container, ContainerProps::containerToString(container)); @@ -432,7 +420,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia containerConfig.insert(config_key::password, password); } else if (protocol == Proto::Socks5Proxy) { QString proxyConfig = serverController->getTextFileFromContainer(container, credentials, - protocols::socks5Proxy::proxyConfigPath, errorCode); + protocols::socks5Proxy::proxyConfigPath, errorCode); const static QRegularExpression usernameAndPasswordRegExp("users (\\w+):CL:(\\w+)"); QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig); @@ -591,25 +579,8 @@ void InstallController::removeProcessedContainer() void InstallController::removeApiConfig(const int serverIndex) { - auto serverConfig = m_serversModel->getServerConfig(serverIndex); - -#ifdef Q_OS_IOS - QString vpncName = QString("%1 (%2) %3") - .arg(serverConfig[config_key::description].toString()) - .arg(serverConfig[config_key::hostName].toString()) - .arg(serverConfig[config_key::vpnproto].toString()); - - AmneziaVPN::removeVPNC(vpncName.toStdString()); -#endif - - serverConfig.remove(config_key::dns1); - serverConfig.remove(config_key::dns2); - serverConfig.remove(config_key::containers); - serverConfig.remove(config_key::hostName); - - serverConfig.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None)); - - m_serversModel->editServer(serverConfig, serverIndex); + m_serversModel->removeApiConfig(serverIndex); + emit apiConfigRemoved(tr("Api config removed")); } void InstallController::clearCachedProfile(QSharedPointer serverController) @@ -671,7 +642,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw QString hostname = serverCredentials.hostName; #ifdef Q_OS_WINDOWS - mountPath = getNextDriverLetter() + ":"; + mountPath = Utils::getNextDriverLetter() + ":"; // QString cmd = QString("net use \\\\sshfs\\%1@x.x.x.x!%2 /USER:%1 %3") // .arg(labelTftpUserNameText()) // .arg(labelTftpPortText()) @@ -772,7 +743,7 @@ bool InstallController::checkSshConnection(QSharedPointer serv } else { if (output.contains(tr("Please login as the user"))) { output.replace("\n", ""); - emit installationErrorOccurred(output); + emit wrongInstallationUser(output); return false; } } @@ -801,6 +772,81 @@ void InstallController::addEmptyServer() emit installServerFinished(tr("Server added successfully")); } +bool InstallController::isConfigValid() +{ + int serverIndex = m_serversModel->getDefaultServerIndex(); + QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex); + + if (apiUtils::isServerFromApi(serverConfigObject)) { + return true; + } + + if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { + emit noInstalledContainers(); + return false; + } + + DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); + + if (container == DockerContainer::None) { + emit installationErrorOccurred(ErrorCode::NoInstalledContainersError); + return false; + } + + QSharedPointer serverController(new ServerController(m_settings)); + VpnConfigurationsController vpnConfigurationController(m_settings, serverController); + + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + QFutureWatcher watcher; + + QFuture future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() { + ErrorCode errorCode = ErrorCode::NoError; + + auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) { + for (Proto protocol : ContainerProps::protocolsForContainer(container)) { + QString protocolConfig = + containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString(); + + if (protocolConfig.isEmpty()) { + return false; + } + } + return true; + }; + + if (!isProtocolConfigExists(containerConfig, container)) { + VpnConfigurationsController vpnConfigurationController(m_settings, serverController); + errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + m_serversModel->updateContainerConfig(container, containerConfig); + + errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig, + QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController); + if (errorCode != ErrorCode::NoError) { + return errorCode; + } + } + return errorCode; + }); + + QEventLoop wait; + connect(&watcher, &QFutureWatcher::finished, &wait, &QEventLoop::quit); + watcher.setFuture(future); + wait.exec(); + + ErrorCode errorCode = watcher.result(); + + if (errorCode != ErrorCode::NoError) { + emit installationErrorOccurred(errorCode); + return false; + } + return true; +} + bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig, const QJsonObject &newConfig) { diff --git a/client/ui/controllers/installController.h b/client/ui/controllers/installController.h index 5e7fd41d..8e42b5b2 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -50,6 +50,8 @@ public slots: void addEmptyServer(); + bool isConfigValid(); + signals: void installContainerFinished(const QString &finishMessage, bool isServiceInstall); void installServerFinished(const QString &finishMessage); @@ -63,8 +65,8 @@ signals: void removeAllContainersFinished(const QString &finishedMessage); void removeProcessedContainerFinished(const QString &finishedMessage); - void installationErrorOccurred(const QString &errorMessage); void installationErrorOccurred(ErrorCode errorCode); + void wrongInstallationUser(const QString &message); void serverAlreadyExists(int serverIndex); @@ -77,6 +79,9 @@ signals: void currentContainerUpdated(); void cachedProfileCleared(const QString &message); + void apiConfigRemoved(const QString &message); + + void noInstalledContainers(); private: void installServer(const DockerContainer container, const QMap &installedContainers, @@ -95,6 +100,7 @@ private: QSharedPointer m_containersModel; QSharedPointer m_protocolModel; QSharedPointer m_clientManagementModel; + std::shared_ptr m_settings; ServerCredentials m_processedServerCredentials; diff --git a/client/ui/controllers/listViewFocusController.cpp b/client/ui/controllers/listViewFocusController.cpp new file mode 100644 index 00000000..9fa232ca --- /dev/null +++ b/client/ui/controllers/listViewFocusController.cpp @@ -0,0 +1,309 @@ +#include "listViewFocusController.h" +#include "utils/qmlUtils.h" + +#include + +ListViewFocusController::ListViewFocusController(QQuickItem *listView, QObject *parent) + : QObject { parent }, + m_listView { listView }, + m_focusChain {}, + m_currentSection { Section::Default }, + m_header { nullptr }, + m_footer { nullptr }, + m_focusedItem { nullptr }, + m_focusedItemIndex { -1 }, + m_delegateIndex { 0 }, + m_isReturnNeeded { false }, + m_currentSectionString { "Default", "Header", "Delegate", "Footer" } +{ + QVariant headerItemProperty = m_listView->property("headerItem"); + m_header = headerItemProperty.canConvert() ? headerItemProperty.value() : nullptr; + + QVariant footerItemProperty = m_listView->property("footerItem"); + m_footer = footerItemProperty.canConvert() ? footerItemProperty.value() : nullptr; +} + +ListViewFocusController::~ListViewFocusController() +{ +} + +void ListViewFocusController::viewAtCurrentIndex() const +{ + switch (m_currentSection) { + case Section::Default: [[fallthrough]]; + case Section::Header: { + QMetaObject::invokeMethod(m_listView, "positionViewAtBeginning"); + break; + } + case Section::Delegate: { + QMetaObject::invokeMethod(m_listView, "positionViewAtIndex", Q_ARG(int, m_delegateIndex), // Index + Q_ARG(int, 2)); // PositionMode (0 = Visible) + break; + } + case Section::Footer: { + QMetaObject::invokeMethod(m_listView, "positionViewAtEnd"); + break; + } + } +} + +int ListViewFocusController::size() const +{ + return m_listView->property("count").toInt(); +} + +int ListViewFocusController::currentIndex() const +{ + return m_delegateIndex; +} + +void ListViewFocusController::setDelegateIndex(int index) +{ + m_delegateIndex = index; + m_listView->setProperty("currentIndex", index); +} + +void ListViewFocusController::nextDelegate() +{ + switch (m_currentSection) { + case Section::Default: { + if (hasHeader()) { + m_currentSection = Section::Header; + viewAtCurrentIndex(); + break; + } + [[fallthrough]]; + } + case Section::Header: { + if (size() > 0) { + m_currentSection = Section::Delegate; + viewAtCurrentIndex(); + break; + } + [[fallthrough]]; + } + case Section::Delegate: + if (m_delegateIndex < (size() - 1)) { + setDelegateIndex(m_delegateIndex + 1); + viewAtCurrentIndex(); + break; + } else if (hasFooter()) { + m_currentSection = Section::Footer; + viewAtCurrentIndex(); + break; + } + [[fallthrough]]; + case Section::Footer: { + m_isReturnNeeded = true; + m_currentSection = Section::Default; + viewAtCurrentIndex(); + break; + } + default: { + qCritical() << "Current section is invalid!"; + break; + } + } +} + +void ListViewFocusController::previousDelegate() +{ + switch (m_currentSection) { + case Section::Default: { + if (hasFooter()) { + m_currentSection = Section::Footer; + break; + } + [[fallthrough]]; + } + case Section::Footer: { + if (size() > 0) { + m_currentSection = Section::Delegate; + setDelegateIndex(size() - 1); + break; + } + [[fallthrough]]; + } + case Section::Delegate: { + if (m_delegateIndex > 0) { + setDelegateIndex(m_delegateIndex - 1); + break; + } else if (hasHeader()) { + m_currentSection = Section::Header; + break; + } + [[fallthrough]]; + } + case Section::Header: { + m_isReturnNeeded = true; + m_currentSection = Section::Default; + break; + } + default: { + qCritical() << "Current section is invalid!"; + break; + } + } +} + +void ListViewFocusController::decrementIndex() +{ + m_delegateIndex--; +} + +QQuickItem *ListViewFocusController::itemAtIndex(const int index) const +{ + QQuickItem *item { nullptr }; + + QMetaObject::invokeMethod(m_listView, "itemAtIndex", Q_RETURN_ARG(QQuickItem *, item), Q_ARG(int, index)); + + return item; +} + +QQuickItem *ListViewFocusController::currentDelegate() const +{ + QQuickItem *result { nullptr }; + + switch (m_currentSection) { + case Section::Default: { + qWarning() << "No elements..."; + break; + } + case Section::Header: { + result = m_header; + break; + } + case Section::Delegate: { + result = itemAtIndex(m_delegateIndex); + break; + } + case Section::Footer: { + result = m_footer; + break; + } + } + return result; +} + +QQuickItem *ListViewFocusController::focusedItem() const +{ + return m_focusedItem; +} + +void ListViewFocusController::focusNextItem() +{ + if (m_isReturnNeeded) { + return; + } + + reloadFocusChain(); + + if (m_focusChain.empty()) { + qWarning() << "No elements found in the delegate. Going to next delegate..."; + nextDelegate(); + focusNextItem(); + return; + } + m_focusedItemIndex++; + m_focusedItem = qobject_cast(m_focusChain.at(m_focusedItemIndex)); + m_focusedItem->forceActiveFocus(Qt::TabFocusReason); +} + +void ListViewFocusController::focusPreviousItem() +{ + if (m_isReturnNeeded) { + return; + } + + if (m_focusChain.empty()) { + qInfo() << "Empty focusChain with current delegate: " << currentDelegate() << "Scanning for elements..."; + reloadFocusChain(); + } + if (m_focusChain.empty()) { + qWarning() << "No elements found in the delegate. Going to next delegate..."; + previousDelegate(); + focusPreviousItem(); + return; + } + if (m_focusedItemIndex == -1) { + m_focusedItemIndex = m_focusChain.size(); + } + m_focusedItemIndex--; + m_focusedItem = qobject_cast(m_focusChain.at(m_focusedItemIndex)); + m_focusedItem->forceActiveFocus(Qt::TabFocusReason); +} + +void ListViewFocusController::resetFocusChain() +{ + m_focusChain.clear(); + m_focusedItem = nullptr; + m_focusedItemIndex = -1; +} + +void ListViewFocusController::reloadFocusChain() +{ + m_focusChain = FocusControl::getItemsChain(currentDelegate()); +} + +bool ListViewFocusController::isFirstFocusItemInDelegate() const +{ + return m_focusedItem && (m_focusedItem == m_focusChain.first()); +} + +bool ListViewFocusController::isLastFocusItemInDelegate() const +{ + return m_focusedItem && (m_focusedItem == m_focusChain.last()); +} + +bool ListViewFocusController::hasHeader() const +{ + return m_header && !FocusControl::getItemsChain(m_header).isEmpty(); +} + +bool ListViewFocusController::hasFooter() const +{ + return m_footer && !FocusControl::getItemsChain(m_footer).isEmpty(); +} + +bool ListViewFocusController::isFirstFocusItemInListView() const +{ + switch (m_currentSection) { + case Section::Footer: { + return isFirstFocusItemInDelegate() && !hasHeader() && (size() == 0); + } + case Section::Delegate: { + return isFirstFocusItemInDelegate() && (m_delegateIndex == 0) && !hasHeader(); + } + case Section::Header: { + isFirstFocusItemInDelegate(); + } + case Section::Default: { + return true; + } + default: qWarning() << "Wrong section"; return true; + } +} + +bool ListViewFocusController::isLastFocusItemInListView() const +{ + switch (m_currentSection) { + case Section::Default: { + return !hasHeader() && (size() == 0) && !hasFooter(); + } + case Section::Header: { + return isLastFocusItemInDelegate() && (size() == 0) && !hasFooter(); + } + case Section::Delegate: { + return isLastFocusItemInDelegate() && (m_delegateIndex == size() - 1) && !hasFooter(); + } + case Section::Footer: { + return isLastFocusItemInDelegate(); + } + default: qWarning() << "Wrong section"; return true; + } +} + +bool ListViewFocusController::isReturnNeeded() const +{ + return m_isReturnNeeded; +} diff --git a/client/ui/controllers/listViewFocusController.h b/client/ui/controllers/listViewFocusController.h new file mode 100644 index 00000000..f6405716 --- /dev/null +++ b/client/ui/controllers/listViewFocusController.h @@ -0,0 +1,70 @@ +#ifndef LISTVIEWFOCUSCONTROLLER_H +#define LISTVIEWFOCUSCONTROLLER_H + +#include +#include +#include +#include +#include + +/*! + * \brief The ListViewFocusController class manages the focus of elements in ListView + * \details This class object moving focus to ListView's controls since ListView stores + * it's data implicitly and it could be got one by one. + * + * This class was made to store as less as possible data getting it from QML + * when it's needed. + */ +class ListViewFocusController : public QObject +{ + Q_OBJECT +public: + explicit ListViewFocusController(QQuickItem *listView, QObject *parent = nullptr); + ~ListViewFocusController(); + + void nextDelegate(); + void previousDelegate(); + void decrementIndex(); + void focusNextItem(); + void focusPreviousItem(); + void resetFocusChain(); + void reloadFocusChain(); + bool isFirstFocusItemInListView() const; + bool isFirstFocusItemInDelegate() const; + bool isLastFocusItemInListView() const; + bool isLastFocusItemInDelegate() const; + bool isReturnNeeded() const; + +private: + enum class Section { + Default, + Header, + Delegate, + Footer, + }; + + int size() const; + int currentIndex() const; + void setDelegateIndex(int index); + void viewAtCurrentIndex() const; + QQuickItem *itemAtIndex(const int index) const; + QQuickItem *currentDelegate() const; + QQuickItem *focusedItem() const; + + bool hasHeader() const; + bool hasFooter() const; + + QQuickItem *m_listView; + QList m_focusChain; + Section m_currentSection; + QQuickItem *m_header; + QQuickItem *m_footer; + QQuickItem *m_focusedItem; // Pointer to focused item on Delegate + qsizetype m_focusedItemIndex; + qsizetype m_delegateIndex; + bool m_isReturnNeeded; + + QList m_currentSectionString; +}; + +#endif // LISTVIEWFOCUSCONTROLLER_H diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 3c6583d3..d515df49 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -10,8 +10,6 @@ #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" - #include "platforms/android/android_utils.h" - #include #endif #if defined Q_OS_MAC #include "ui/macos_util.h" @@ -22,18 +20,8 @@ PageController::PageController(const QSharedPointer &serversModel, : QObject(parent), m_serversModel(serversModel), m_settings(settings) { #ifdef Q_OS_ANDROID - // Change color of navigation and status bar's auto initialPageNavigationBarColor = getInitialPageNavigationBarColor(); - AndroidUtils::runOnAndroidThreadSync([&initialPageNavigationBarColor]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - window.callMethod("addFlags", "(I)V", 0x80000000); - window.callMethod("clearFlags", "(I)V", 0x04000000); - window.callMethod("setStatusBarColor", "(I)V", 0xFF0E0E11); - window.callMethod("setNavigationBarColor", "(I)V", initialPageNavigationBarColor); - } - }); + AndroidController::instance()->setNavigationBarColor(initialPageNavigationBarColor); #endif #if defined Q_OS_MACX @@ -46,16 +34,16 @@ PageController::PageController(const QSharedPointer &serversModel, m_isTriggeredByConnectButton = false; } -QString PageController::getInitialPage() +bool PageController::isStartPageVisible() { if (m_serversModel->getServersCount()) { if (m_serversModel->getDefaultServerIndex() < 0) { auto defaultServerIndex = m_serversModel->index(0); m_serversModel->setData(defaultServerIndex, true, ServersModel::Roles::IsDefaultRole); } - return getPagePath(PageLoader::PageEnum::PageStart); + return false; } else { - return getPagePath(PageLoader::PageEnum::PageSetupWizardStart); + return true; } } @@ -93,7 +81,7 @@ void PageController::keyPressEvent(Qt::Key key) case Qt::Key_Escape: { if (m_drawerDepth) { emit closeTopDrawer(); - setDrawerDepth(getDrawerDepth() - 1); + decrementDrawerDepth(); } else { emit escapePressed(); } @@ -115,14 +103,7 @@ unsigned int PageController::getInitialPageNavigationBarColor() void PageController::updateNavigationBarColor(const int color) { #ifdef Q_OS_ANDROID - // Change color of navigation bar - AndroidUtils::runOnAndroidThreadSync([&color]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - window.callMethod("setNavigationBarColor", "(I)V", color); - } - }); + AndroidController::instance()->setNavigationBarColor(color); #endif } @@ -131,7 +112,7 @@ void PageController::showOnStartup() if (!m_settings->isStartMinimized()) { emit raiseMainWindow(); } else { -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) emit hideMainWindow(); #elif defined Q_OS_MACX setDockIconVisible(false); @@ -161,11 +142,25 @@ void PageController::setDrawerDepth(const int depth) } } -int PageController::getDrawerDepth() +int PageController::getDrawerDepth() const { return m_drawerDepth; } +int PageController::incrementDrawerDepth() +{ + return ++m_drawerDepth; +} + +int PageController::decrementDrawerDepth() +{ + if (m_drawerDepth == 0) { + return m_drawerDepth; + } else { + return --m_drawerDepth; + } +} + void PageController::onShowErrorMessage(ErrorCode errorCode) { const auto fullErrorMessage = errorString(errorCode); diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index c9d655ba..60621414 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -31,6 +31,12 @@ namespace PageLoader PageSettingsLogging, PageSettingsSplitTunneling, PageSettingsAppSplitTunneling, + PageSettingsApiServerInfo, + PageSettingsApiAvailableCountries, + PageSettingsApiSupport, + PageSettingsApiInstructions, + PageSettingsApiNativeConfigs, + PageSettingsApiDevices, PageServiceSftpSettings, PageServiceTorWebsiteSettings, @@ -47,17 +53,24 @@ namespace PageLoader PageSetupWizardTextKey, PageSetupWizardViewConfig, PageSetupWizardQrReader, + PageSetupWizardApiServicesList, + PageSetupWizardApiServiceInfo, PageProtocolOpenVpnSettings, PageProtocolShadowSocksSettings, PageProtocolCloakSettings, - PageProtocolXraySettings, + PageProtocolXraySettings, PageProtocolWireGuardSettings, PageProtocolAwgSettings, PageProtocolIKev2Settings, PageProtocolRaw, - PageShareFullAccess + PageProtocolWireGuardClientSettings, + PageProtocolAwgClientSettings, + + PageShareFullAccess, + + PageDevMenu }; Q_ENUM_NS(PageEnum) @@ -75,7 +88,7 @@ public: QObject *parent = nullptr); public slots: - QString getInitialPage(); + bool isStartPageVisible(); QString getPagePath(PageLoader::PageEnum page); void closeWindow(); @@ -93,9 +106,11 @@ public slots: void closeApplication(); void setDrawerDepth(const int depth); - int getDrawerDepth(); + int getDrawerDepth() const; + int incrementDrawerDepth(); + int decrementDrawerDepth(); - private slots: +private slots: void onShowErrorMessage(amnezia::ErrorCode errorCode); signals: @@ -110,7 +125,6 @@ signals: void closePage(); void restorePageHomeState(bool isContainerInstalled = false); - void replaceStartPage(); void showErrorMessage(amnezia::ErrorCode); void showErrorMessage(const QString &errorMessage); @@ -129,9 +143,6 @@ signals: void escapePressed(); void closeTopDrawer(); - void forceTabBarActiveFocus(); - void forceStackActiveFocus(); - private: QSharedPointer m_serversModel; diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index aceac551..f4e3d83d 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -88,7 +88,12 @@ void SettingsController::toggleLogging(bool enable) void SettingsController::openLogsFolder() { - Logger::openLogsFolder(); + Logger::openLogsFolder(false); +} + +void SettingsController::openServiceLogsFolder() +{ + Logger::openLogsFolder(true); } void SettingsController::exportLogsFile(const QString &fileName) @@ -100,12 +105,21 @@ void SettingsController::exportLogsFile(const QString &fileName) #endif } +void SettingsController::exportServiceLogsFile(const QString &fileName) +{ +#ifdef Q_OS_ANDROID + AndroidController::instance()->exportLogsFile(fileName); +#else + SystemController::saveFile(fileName, Logger::getServiceLogFile()); +#endif +} + void SettingsController::clearLogs() { #ifdef Q_OS_ANDROID AndroidController::instance()->clearLogs(); #else - Logger::clearLogs(); + Logger::clearLogs(false); Logger::clearServiceLogs(); #endif } @@ -117,12 +131,8 @@ void SettingsController::backupAppConfig(const QString &fileName) void SettingsController::restoreAppConfig(const QString &fileName) { - QFile file(fileName); - - file.open(QIODevice::ReadOnly); - - QByteArray data = file.readAll(); - + QByteArray data; + SystemController::readFile(fileName, data); restoreAppConfigFromData(data); } @@ -252,3 +262,73 @@ void SettingsController::requestNotificationPermission() AndroidController::instance()->requestNotificationPermission(); #endif } + +QString SettingsController::getInstallationUuid() +{ + return m_settings->getInstallationUuid(false); +} + +void SettingsController::enableDevMode() +{ + m_isDevModeEnabled = true; + emit devModeEnabled(); +} + +bool SettingsController::isDevModeEnabled() +{ + return m_isDevModeEnabled; +} + +void SettingsController::resetGatewayEndpoint() +{ + m_settings->resetGatewayEndpoint(); + emit gatewayEndpointChanged(m_settings->getGatewayEndpoint()); +} + +void SettingsController::setGatewayEndpoint(const QString &endpoint) +{ + m_settings->setGatewayEndpoint(endpoint); + emit gatewayEndpointChanged(endpoint); +} + +QString SettingsController::getGatewayEndpoint() +{ + return m_settings->isDevGatewayEnv() ? "Dev endpoint" : m_settings->getGatewayEndpoint(); +} + +bool SettingsController::isDevGatewayEnv() +{ + return m_settings->isDevGatewayEnv(); +} + +void SettingsController::toggleDevGatewayEnv(bool enabled) +{ + m_settings->toggleDevGatewayEnv(enabled); + if (enabled) { + m_settings->setDevGatewayEndpoint(); + } else { + m_settings->resetGatewayEndpoint(); + } + emit gatewayEndpointChanged(m_settings->getGatewayEndpoint()); + emit devGatewayEnvChanged(enabled); +} + +bool SettingsController::isOnTv() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->isOnTv(); +#else + return false; +#endif +} + +bool SettingsController::isHomeAdLabelVisible() +{ + return m_settings->isHomeAdLabelVisible(); +} + +void SettingsController::disableHomeAdLabel() +{ + m_settings->disableHomeAdLabel(); + emit isHomeAdLabelVisibleChanged(false); +} diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 43ad10e8..7781f6c7 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -25,6 +25,12 @@ public: Q_PROPERTY(bool isLoggingEnabled READ isLoggingEnabled WRITE toggleLogging NOTIFY loggingStateChanged) Q_PROPERTY(bool isNotificationPermissionGranted READ isNotificationPermissionGranted NOTIFY onNotificationStateChanged) + Q_PROPERTY(bool isDevModeEnabled READ isDevModeEnabled NOTIFY devModeEnabled) + Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged) + Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged) + + Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged) + public slots: void toggleAmneziaDns(bool enable); bool isAmneziaDnsEnabled(); @@ -39,7 +45,9 @@ public slots: void toggleLogging(bool enable); void openLogsFolder(); + void openServiceLogsFolder(); void exportLogsFile(const QString &fileName); + void exportServiceLogsFile(const QString &fileName); void clearLogs(); void backupAppConfig(const QString &fileName); @@ -70,6 +78,22 @@ public slots: bool isNotificationPermissionGranted(); void requestNotificationPermission(); + QString getInstallationUuid(); + + void enableDevMode(); + bool isDevModeEnabled(); + + void resetGatewayEndpoint(); + void setGatewayEndpoint(const QString &endpoint); + QString getGatewayEndpoint(); + bool isDevGatewayEnv(); + void toggleDevGatewayEnv(bool enabled); + + bool isOnTv(); + + bool isHomeAdLabelVisible(); + void disableHomeAdLabel(); + signals: void primaryDnsChanged(); void secondaryDnsChanged(); @@ -89,6 +113,12 @@ signals: void onNotificationStateChanged(); + void devModeEnabled(); + void gatewayEndpointChanged(const QString &endpoint); + void devGatewayEnvChanged(bool enabled); + + void isHomeAdLabelVisibleChanged(bool visible); + private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; @@ -101,6 +131,8 @@ private: QDateTime m_loggingDisableDate; + bool m_isDevModeEnabled = false; + void checkIfNeedDisableLogs(); }; diff --git a/client/ui/controllers/sitesController.cpp b/client/ui/controllers/sitesController.cpp index d54dbdd2..d40be458 100644 --- a/client/ui/controllers/sitesController.cpp +++ b/client/ui/controllers/sitesController.cpp @@ -44,7 +44,6 @@ void SitesController::addSite(QString hostname) QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, QStringList() << hostname)); } - QMetaObject::invokeMethod(m_vpnConnection.get(), "flushDns", Qt::QueuedConnection); }; const auto &resolveCallback = [this, processSite](const QHostInfo &hostInfo) { @@ -75,21 +74,18 @@ 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)); } void SitesController::importSites(const QString &fileName, bool replaceExisting) { - QFile file(fileName); - - if (!file.open(QIODevice::ReadOnly)) { + QByteArray jsonData; + if (!SystemController::readFile(fileName, jsonData)) { emit errorOccurred(tr("Can't open file: %1").arg(fileName)); return; } - QByteArray jsonData = file.readAll(); QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData); if (jsonDocument.isNull()) { emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName)); @@ -126,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")); } diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index e6a9a28e..52ca1294 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -24,7 +24,7 @@ SystemController::SystemController(const std::shared_ptr &settings, QO { } -void SystemController::saveFile(QString fileName, const QString &data) +void SystemController::saveFile(const QString &fileName, const QString &data) { #if defined Q_OS_ANDROID AndroidController::instance()->saveFile(fileName, data); @@ -62,6 +62,31 @@ void SystemController::saveFile(QString fileName, const QString &data) #endif } +bool SystemController::readFile(const QString &fileName, QByteArray &data) +{ +#ifdef Q_OS_ANDROID + int fd = AndroidController::instance()->getFd(fileName); + if (fd == -1) return false; + QFile file; + if(!file.open(fd, QIODevice::ReadOnly)) return false; + data = file.readAll(); + AndroidController::instance()->closeFd(); +#else + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) return false; + data = file.readAll(); +#endif + return true; +} + +bool SystemController::readFile(const QString &fileName, QString &data) +{ + QByteArray byteArray; + if(!readFile(fileName, byteArray)) return false; + data = byteArray; + return true; +} + QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) { @@ -125,3 +150,19 @@ void SystemController::setQmlRoot(QObject *qmlRoot) { m_qmlRoot = qmlRoot; } + +bool SystemController::isAuthenticated() +{ +#ifdef Q_OS_ANDROID + return AndroidController::instance()->requestAuthentication(); +#else + return true; +#endif +} + +void SystemController::sendTouch(float x, float y) +{ +#ifdef Q_OS_ANDROID + AndroidController::instance()->sendTouch(x, y); +#endif +} diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 274df234..8cb3a0d1 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -11,7 +11,9 @@ class SystemController : public QObject public: explicit SystemController(const std::shared_ptr &setting, QObject *parent = nullptr); - static void saveFile(QString fileName, const QString &data); + static void saveFile(const QString &fileName, const QString &data); + static bool readFile(const QString &fileName, QByteArray &data); + static bool readFile(const QString &fileName, QString &data); public slots: QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "", @@ -19,6 +21,9 @@ public slots: void setQmlRoot(QObject *qmlRoot); + bool isAuthenticated(); + void sendTouch(float x, float y); + signals: void fileDialogClosed(const bool isAccepted); diff --git a/client/ui/macos_util.h b/client/ui/macos_util.h index 15677e42..ff16390a 100644 --- a/client/ui/macos_util.h +++ b/client/ui/macos_util.h @@ -3,7 +3,7 @@ #ifndef Q_OS_IOS #include -#include +#include void setDockIconVisible(bool visible); void fixWidget(QWidget *widget); diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp new file mode 100644 index 00000000..191582a5 --- /dev/null +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -0,0 +1,143 @@ +#include "apiAccountInfoModel.h" + +#include + +#include "core/api/apiUtils.h" +#include "logger.h" + +namespace +{ + Logger logger("AccountInfoModel"); +} + +ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +int ApiAccountInfoModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) + return QVariant(); + + switch (role) { + case SubscriptionStatusRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return tr("Active"); + } + + return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active"); + } + case EndDateRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return ""; + } + + return QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } + case ConnectedDevicesRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return ""; + } + return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount); + } + case ServiceDescriptionRole: { + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { + return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. " + "Speeds up to 200 Mbps"); + } 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."); + } + } + case IsComponentVisibleRole: { + return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2; + } + case HasExpiredWorkerRole: { + for (int i = 0; i < m_issuedConfigsInfo.size(); i++) { + QJsonObject issuedConfigObject = m_issuedConfigsInfo.at(i).toObject(); + + auto lastDownloaded = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::lastDownloaded).toString()); + auto workerLastUpdated = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString()); + + if (lastDownloaded < workerLastUpdated) { + return true; + } + } + return false; + } + } + + return QVariant(); +} + +void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig) +{ + beginResetModel(); + + AccountInfoData accountInfoData; + + m_availableCountries = accountInfoObject.value(apiDefs::key::availableCountries).toArray(); + m_issuedConfigsInfo = accountInfoObject.value(apiDefs::key::issuedConfigs).toArray(); + + accountInfoData.activeDeviceCount = accountInfoObject.value(apiDefs::key::activeDeviceCount).toInt(); + accountInfoData.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt(); + accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString(); + + accountInfoData.configType = apiUtils::getConfigType(serverConfig); + + m_accountInfoData = accountInfoData; + + endResetModel(); +} + +QVariant ApiAccountInfoModel::data(const QString &roleString) +{ + QModelIndex modelIndex = index(0); + auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return data(modelIndex, it.key()); + } + } + + return {}; +} + +QJsonArray ApiAccountInfoModel::getAvailableCountries() +{ + return m_availableCountries; +} + +QJsonArray ApiAccountInfoModel::getIssuedConfigsInfo() +{ + return m_issuedConfigsInfo; +} + +QString ApiAccountInfoModel::getTelegramBotLink() +{ + if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { + return tr("amnezia_free_support_bot"); + } else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { + return tr("amnezia_premium_support_bot"); + } + return ""; +} + +QHash ApiAccountInfoModel::roleNames() const +{ + QHash roles; + roles[SubscriptionStatusRole] = "subscriptionStatus"; + roles[EndDateRole] = "endDate"; + roles[ConnectedDevicesRole] = "connectedDevices"; + roles[ServiceDescriptionRole] = "serviceDescription"; + roles[IsComponentVisibleRole] = "isComponentVisible"; + roles[HasExpiredWorkerRole] = "hasExpiredWorker"; + + return roles; +} diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h new file mode 100644 index 00000000..44eb7ee6 --- /dev/null +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -0,0 +1,56 @@ +#ifndef APIACCOUNTINFOMODEL_H +#define APIACCOUNTINFOMODEL_H + +#include +#include +#include + +#include "core/api/apiDefs.h" + +class ApiAccountInfoModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + SubscriptionStatusRole = Qt::UserRole + 1, + ConnectedDevicesRole, + ServiceDescriptionRole, + EndDateRole, + IsComponentVisibleRole, + HasExpiredWorkerRole + }; + + explicit ApiAccountInfoModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig); + QVariant data(const QString &roleString); + + QJsonArray getAvailableCountries(); + QJsonArray getIssuedConfigsInfo(); + QString getTelegramBotLink(); + +protected: + QHash roleNames() const override; + +private: + struct AccountInfoData + { + QString subscriptionEndDate; + int activeDeviceCount; + int maxDeviceCount; + + apiDefs::ConfigType configType; + }; + + AccountInfoData m_accountInfoData; + QJsonArray m_availableCountries; + QJsonArray m_issuedConfigsInfo; +}; + +#endif // APIACCOUNTINFOMODEL_H diff --git a/client/ui/models/api/apiCountryModel.cpp b/client/ui/models/api/apiCountryModel.cpp new file mode 100644 index 00000000..12f4658e --- /dev/null +++ b/client/ui/models/api/apiCountryModel.cpp @@ -0,0 +1,122 @@ +#include "apiCountryModel.h" + +#include + +#include "core/api/apiDefs.h" +#include "logger.h" + +namespace +{ + Logger logger("ApiCountryModel"); + + constexpr QLatin1String countryConfig("country_config"); +} + +ApiCountryModel::ApiCountryModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +int ApiCountryModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_countries.size(); +} + +QVariant ApiCountryModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) + return QVariant(); + + CountryInfo countryInfo = m_countries.at(index.row()); + IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.value(countryInfo.countryCode); + bool isIssued = issuedConfigInfo.sourceType == countryConfig; + + switch (role) { + case CountryCodeRole: { + return countryInfo.countryCode; + } + case CountryNameRole: { + return countryInfo.countryName; + } + case CountryImageCodeRole: { + return countryInfo.countryCode.toUpper(); + } + case IsIssuedRole: { + return isIssued; + } + case IsWorkerExpiredRole: { + return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated; + } + } + + return QVariant(); +} + +void ApiCountryModel::updateModel(const QJsonArray &countries, const QString ¤tCountryCode) +{ + beginResetModel(); + + m_countries.clear(); + for (int i = 0; i < countries.size(); i++) { + CountryInfo countryInfo; + QJsonObject countryObject = countries.at(i).toObject(); + + countryInfo.countryName = countryObject.value(apiDefs::key::serverCountryName).toString(); + countryInfo.countryCode = countryObject.value(apiDefs::key::serverCountryCode).toString(); + + if (countryInfo.countryCode == currentCountryCode) { + m_currentIndex = i; + emit currentIndexChanged(m_currentIndex); + } + m_countries.push_back(countryInfo); + } + + endResetModel(); +} + +void ApiCountryModel::updateIssuedConfigsInfo(const QJsonArray &issuedConfigs) +{ + beginResetModel(); + + m_issuedConfigs.clear(); + for (int i = 0; i < issuedConfigs.size(); i++) { + IssuedConfigInfo issuedConfigInfo; + QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject(); + + if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != countryConfig) { + continue; + } + + issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString(); + issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString(); + issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString(); + issuedConfigInfo.sourceType = issuedConfigObject.value(apiDefs::key::sourceType).toString(); + issuedConfigInfo.osVersion = issuedConfigObject.value(apiDefs::key::osVersion).toString(); + + m_issuedConfigs.insert(issuedConfigObject.value(apiDefs::key::serverCountryCode).toString(), issuedConfigInfo); + } + + endResetModel(); +} + +int ApiCountryModel::getCurrentIndex() +{ + return m_currentIndex; +} + +void ApiCountryModel::setCurrentIndex(const int i) +{ + m_currentIndex = i; + emit currentIndexChanged(m_currentIndex); +} + +QHash ApiCountryModel::roleNames() const +{ + QHash roles; + roles[CountryNameRole] = "countryName"; + roles[CountryCodeRole] = "countryCode"; + roles[CountryImageCodeRole] = "countryImageCode"; + roles[IsIssuedRole] = "isIssued"; + roles[IsWorkerExpiredRole] = "isWorkerExpired"; + return roles; +} diff --git a/client/ui/models/api/apiCountryModel.h b/client/ui/models/api/apiCountryModel.h new file mode 100644 index 00000000..08ac3685 --- /dev/null +++ b/client/ui/models/api/apiCountryModel.h @@ -0,0 +1,63 @@ +#ifndef APICOUNTRYMODEL_H +#define APICOUNTRYMODEL_H + +#include +#include +#include + +class ApiCountryModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + CountryNameRole = Qt::UserRole + 1, + CountryCodeRole, + CountryImageCodeRole, + IsIssuedRole, + IsWorkerExpiredRole + }; + + explicit ApiCountryModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_PROPERTY(int currentIndex READ getCurrentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public slots: + void updateModel(const QJsonArray &countries, const QString ¤tCountryCode); + void updateIssuedConfigsInfo(const QJsonArray &issuedConfigs); + + int getCurrentIndex(); + void setCurrentIndex(const int i); + +signals: + void currentIndexChanged(const int index); + +protected: + QHash roleNames() const override; + +private: + struct IssuedConfigInfo + { + QString installationUuid; + QString workerLastUpdated; + QString lastDownloaded; + QString sourceType; + QString osVersion; + }; + + struct CountryInfo + { + QString countryName; + QString countryCode; + }; + + QVector m_countries; + QHash m_issuedConfigs; + int m_currentIndex; +}; + +#endif // APICOUNTRYMODEL_H diff --git a/client/ui/models/api/apiDevicesModel.cpp b/client/ui/models/api/apiDevicesModel.cpp new file mode 100644 index 00000000..6c0d60d0 --- /dev/null +++ b/client/ui/models/api/apiDevicesModel.cpp @@ -0,0 +1,90 @@ +#include "apiDevicesModel.h" + +#include + +#include "core/api/apiDefs.h" +#include "logger.h" + +namespace +{ + Logger logger("ApiDevicesModel"); + + constexpr QLatin1String gatewayAccount("gateway_account"); +} + +ApiDevicesModel::ApiDevicesModel(std::shared_ptr settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent) +{ +} + +int ApiDevicesModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_issuedConfigs.size(); +} + +QVariant ApiDevicesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) + return QVariant(); + + IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.at(index.row()); + + switch (role) { + case OsVersionRole: { + return issuedConfigInfo.osVersion; + } + case SupportTagRole: { + return issuedConfigInfo.installationUuid; + } + case CountryCodeRole: { + return issuedConfigInfo.countryCode; + } + case LastUpdateRole: { + return QDateTime::fromString(issuedConfigInfo.lastDownloaded, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } + case IsCurrentDeviceRole: { + return issuedConfigInfo.installationUuid == m_settings->getInstallationUuid(false); + } + } + + return QVariant(); +} + +void ApiDevicesModel::updateModel(const QJsonArray &issuedConfigs) +{ + beginResetModel(); + + m_issuedConfigs.clear(); + for (int i = 0; i < issuedConfigs.size(); i++) { + IssuedConfigInfo issuedConfigInfo; + QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject(); + + if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != gatewayAccount) { + continue; + } + + issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString(); + issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString(); + issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString(); + issuedConfigInfo.sourceType = issuedConfigObject.value(apiDefs::key::sourceType).toString(); + issuedConfigInfo.osVersion = issuedConfigObject.value(apiDefs::key::osVersion).toString(); + + issuedConfigInfo.countryName = issuedConfigObject.value(apiDefs::key::serverCountryName).toString(); + issuedConfigInfo.countryCode = issuedConfigObject.value(apiDefs::key::serverCountryCode).toString(); + + m_issuedConfigs.push_back(issuedConfigInfo); + } + + endResetModel(); +} + +QHash ApiDevicesModel::roleNames() const +{ + QHash roles; + roles[OsVersionRole] = "osVersion"; + roles[SupportTagRole] = "supportTag"; + roles[CountryCodeRole] = "countryCode"; + roles[LastUpdateRole] = "lastUpdate"; + roles[IsCurrentDeviceRole] = "isCurrentDevice"; + return roles; +} diff --git a/client/ui/models/api/apiDevicesModel.h b/client/ui/models/api/apiDevicesModel.h new file mode 100644 index 00000000..e6a59dba --- /dev/null +++ b/client/ui/models/api/apiDevicesModel.h @@ -0,0 +1,52 @@ +#ifndef APIDEVICESMODEL_H +#define APIDEVICESMODEL_H + +#include +#include +#include + +#include "settings.h" + +class ApiDevicesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + OsVersionRole = Qt::UserRole + 1, + SupportTagRole, + CountryCodeRole, + LastUpdateRole, + IsCurrentDeviceRole + }; + + explicit ApiDevicesModel(std::shared_ptr settings, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void updateModel(const QJsonArray &issuedConfigs); + +protected: + QHash roleNames() const override; + +private: + struct IssuedConfigInfo + { + QString installationUuid; + QString workerLastUpdated; + QString lastDownloaded; + QString sourceType; + QString osVersion; + + QString countryName; + QString countryCode; + }; + + QVector m_issuedConfigs; + + std::shared_ptr m_settings; +}; +#endif // APIDEVICESMODEL_H diff --git a/client/ui/models/api/apiServicesModel.cpp b/client/ui/models/api/apiServicesModel.cpp new file mode 100644 index 00000000..65f17758 --- /dev/null +++ b/client/ui/models/api/apiServicesModel.cpp @@ -0,0 +1,264 @@ +#include "apiServicesModel.h" + +#include + +#include "logger.h" + +namespace +{ + Logger logger("ApiServicesModel"); + + namespace configKey + { + constexpr char userCountryCode[] = "user_country_code"; + constexpr char services[] = "services"; + constexpr char serviceInfo[] = "service_info"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceProtocol[] = "service_protocol"; + + constexpr char name[] = "name"; + constexpr char price[] = "price"; + constexpr char speed[] = "speed"; + constexpr char timelimit[] = "timelimit"; + constexpr char region[] = "region"; + + constexpr char availableCountries[] = "available_countries"; + + constexpr char storeEndpoint[] = "store_endpoint"; + + constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; + } + + namespace serviceType + { + constexpr char amneziaFree[] = "amnezia-free"; + constexpr char amneziaPremium[] = "amnezia-premium"; + } +} + +ApiServicesModel::ApiServicesModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +int ApiServicesModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_services.size(); +} + +QVariant ApiServicesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) + return QVariant(); + + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; + + switch (role) { + case NameRole: { + return apiServiceData.serviceInfo.name; + } + case CardDescriptionRole: { + auto speed = apiServiceData.serviceInfo.speed; + if (serviceType == serviceType::amneziaPremium) { + return tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos. " + "Access all websites and online resources. Speeds up to %1 Mbps.") + .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."); + if (!isServiceAvailable) { + description += tr("

Not available in your region. If you have VPN enabled, disable it, " + "return to the previous screen, and try again."); + } + return description; + } + } + case ServiceDescriptionRole: { + if (serviceType == serviceType::amneziaPremium) { + return tr("Amnezia Premium is classic VPN for for seamless work, downloading large files, and watching videos. " + "Access all websites and online resources."); + } 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."); + } + } + case IsServiceAvailableRole: { + if (serviceType == serviceType::amneziaFree) { + if (!isServiceAvailable) { + return false; + } + } + return true; + } + case SpeedRole: { + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); + } + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { + return ""; + } + return tr("%1 days").arg(timeLimit); + } + case RegionRole: { + return apiServiceData.serviceInfo.region; + } + case FeaturesRole: { + if (serviceType == serviceType::amneziaPremium) { + return tr(""); + } else { + return tr("VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. " + "Other sites will be opened from your real IP address, " + "more details on the website."); + } + } + case PriceRole: { + auto price = apiServiceData.serviceInfo.price; + if (price == "free") { + return tr("Free"); + } + return tr("%1 $/month").arg(price); + } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } + } + + return QVariant(); +} + +void ApiServicesModel::updateModel(const QJsonObject &data) +{ + beginResetModel(); + + m_services.clear(); + + m_countryCode = data.value(configKey::userCountryCode).toString(); + auto services = data.value(configKey::services).toArray(); + + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); + m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + auto serviceObject = service.toObject(); + m_services.push_back(getApiServicesData(serviceObject)); + } + } + + endResetModel(); +} + +void ApiServicesModel::setServiceIndex(const int index) +{ + m_selectedServiceIndex = index; +} + +QJsonObject ApiServicesModel::getSelectedServiceInfo() +{ + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; +} + +QString ApiServicesModel::getSelectedServiceType() +{ + auto service = m_services.at(m_selectedServiceIndex); + return service.type; +} + +QString ApiServicesModel::getSelectedServiceProtocol() +{ + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; +} + +QString ApiServicesModel::getSelectedServiceName() +{ + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; +} + +QJsonArray ApiServicesModel::getSelectedServiceCountries() +{ + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; +} + +QString ApiServicesModel::getCountryCode() +{ + return m_countryCode; +} + +QString ApiServicesModel::getStoreEndpoint() +{ + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; +} + +QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) +{ + QModelIndex modelIndex = index(m_selectedServiceIndex); + auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return data(modelIndex, it.key()); + } + } + + return {}; +} + +QHash ApiServicesModel::roleNames() const +{ + QHash roles; + roles[NameRole] = "name"; + roles[CardDescriptionRole] = "cardDescription"; + roles[ServiceDescriptionRole] = "serviceDescription"; + roles[IsServiceAvailableRole] = "isServiceAvailable"; + roles[SpeedRole] = "speed"; + roles[TimeLimitRole] = "timeLimit"; + roles[RegionRole] = "region"; + roles[FeaturesRole] = "features"; + roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; + + return roles; +} + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = data.value(configKey::storeEndpoint).toString(); + + if (data.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/api/apiServicesModel.h b/client/ui/models/api/apiServicesModel.h new file mode 100644 index 00000000..c96a49ab --- /dev/null +++ b/client/ui/models/api/apiServicesModel.h @@ -0,0 +1,91 @@ +#ifndef APISERVICESMODEL_H +#define APISERVICESMODEL_H + +#include +#include +#include + +class ApiServicesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + NameRole = Qt::UserRole + 1, + CardDescriptionRole, + ServiceDescriptionRole, + IsServiceAvailableRole, + SpeedRole, + TimeLimitRole, + RegionRole, + FeaturesRole, + PriceRole, + EndDateRole + }; + + explicit ApiServicesModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void updateModel(const QJsonObject &data); + + void setServiceIndex(const int index); + + QJsonObject getSelectedServiceInfo(); + QString getSelectedServiceType(); + QString getSelectedServiceProtocol(); + QString getSelectedServiceName(); + QJsonArray getSelectedServiceCountries(); + + QString getCountryCode(); + + QString getStoreEndpoint(); + + QVariant getSelectedServiceData(const QString roleString); + +protected: + QHash roleNames() const override; + +private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + + QString m_countryCode; + QVector m_services; + + int m_selectedServiceIndex; +}; + +#endif // APISERVICESMODEL_H diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index f2117f75..f07eae71 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -20,6 +20,7 @@ namespace constexpr char latestHandshake[] = "latestHandshake"; constexpr char dataReceived[] = "dataReceived"; constexpr char dataSent[] = "dataSent"; + constexpr char allowedIps[] = "allowedIps"; } } @@ -49,6 +50,7 @@ QVariant ClientManagementModel::data(const QModelIndex &index, int role) const case LatestHandshakeRole: return userData.value(configKey::latestHandshake).toString(); case DataReceivedRole: return userData.value(configKey::dataReceived).toString(); case DataSentRole: return userData.value(configKey::dataSent).toString(); + case AllowedIpsRole: return userData.value(configKey::allowedIps).toString(); } return QVariant(); @@ -75,6 +77,7 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co { beginResetModel(); m_clientsTable = QJsonArray(); + endResetModel(); ErrorCode error = ErrorCode::NoError; @@ -88,10 +91,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co const QByteArray clientsTableString = serverController->getTextFileFromContainer(container, credentials, clientsTableFile, error); if (error != ErrorCode::NoError) { logger.error() << "Failed to get the clientsTable file from the server"; - endResetModel(); return error; } + beginResetModel(); m_clientsTable = QJsonDocument::fromJson(clientsTableString).array(); if (m_clientsTable.isEmpty()) { @@ -103,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -141,6 +146,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co userData[configKey::dataSent] = client.dataSent; } + if (!client.allowedIps.isEmpty()) { + userData[configKey::allowedIps] = client.allowedIps; + } + obj[configKey::userData] = userData; m_clientsTable.replace(i, obj); break; @@ -232,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -266,8 +337,9 @@ ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const S const auto peerList = parts.filter("peer:"); const auto latestHandshakeList = parts.filter("latest handshake:"); const auto transferredDataList = parts.filter("transfer:"); + const auto allowedIpsList = parts.filter("allowed ips:"); - if (latestHandshakeList.isEmpty() || transferredDataList.isEmpty() || peerList.isEmpty()) { + if (allowedIpsList.isEmpty() || latestHandshakeList.isEmpty() || transferredDataList.isEmpty() || peerList.isEmpty()) { return error; } @@ -281,19 +353,20 @@ ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const S } }; - for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size(); ++i) { + for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size() && i < allowedIpsList.size(); ++i) { const auto transferredData = getStrValue(transferredDataList[i]).split(","); auto latestHandshake = getStrValue(latestHandshakeList[i]); auto serverBytesReceived = transferredData.front().trimmed(); auto serverBytesSent = transferredData.back().trimmed(); + auto allowedIps = getStrValue(allowedIpsList[i]); changeHandshakeFormat(latestHandshake); serverBytesReceived.chop(QStringLiteral(" received").length()); serverBytesSent.chop(QStringLiteral(" sent").length()); - data.push_back({ getStrValue(peerList[i]), latestHandshake, serverBytesSent, serverBytesReceived }); + data.push_back({ getStrValue(peerList[i]), latestHandshake, serverBytesSent, serverBytesReceived, allowedIps }); } return error; @@ -317,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + + return appendClient(clientId, clientName, container, credentials, serverController); } ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, @@ -413,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -454,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -478,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + break; } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -585,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + ErrorCode error = ErrorCode::NoError; + + // Get server config + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + // Get client ID to remove + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + // Remove client from server config + QJsonObject configObj = serverConfig.object(); + if (!configObj.contains("inbounds")) { + logger.error() << "Missing inbounds in xray config"; + return ErrorCode::InternalError; + } + + QJsonArray inbounds = configObj["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Empty inbounds array in xray config"; + return ErrorCode::InternalError; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings"; + return ErrorCode::InternalError; + } + + QJsonArray clients = settings["clients"].toArray(); + if (clients.isEmpty()) { + logger.error() << "Empty clients array in xray config"; + return ErrorCode::InternalError; + } + + for (int i = 0; i < clients.size(); ++i) { + QJsonObject clientObj = clients[i].toObject(); + if (clientObj.contains("id") && clientObj["id"].toString() == clientId) { + clients.removeAt(i); + break; + } + } + + // Update server config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + configObj["inbounds"] = inbounds; + + // Upload updated config + error = serverController->uploadTextFileToContainer( + container, + credentials, + QJsonDocument(configObj).toJson(), + serverConfigPath + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload updated xray config"; + return error; + } + + // Remove from local table + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + // Update clients table file on server + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable") + .arg(ContainerProps::containerTypeToString(container)); + + error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file"; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + error = serverController->runScript( + credentials, + serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container)) + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to restart xray container"; + return error; + } + + return error; +} + QHash ClientManagementModel::roleNames() const { QHash roles; @@ -593,5 +911,6 @@ QHash ClientManagementModel::roleNames() const roles[LatestHandshakeRole] = "latestHandshake"; roles[DataReceivedRole] = "dataReceived"; roles[DataSentRole] = "dataSent"; + roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index d64280a3..989120a9 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -17,7 +17,8 @@ public: CreationDateRole, LatestHandshakeRole, DataReceivedRole, - DataSentRole + DataSentRole, + AllowedIpsRole }; struct WgShowData @@ -26,6 +27,7 @@ public: QString latestHandshake; QString dataReceived; QString dataSent; + QString allowedIps; }; ClientManagementModel(std::shared_ptr settings, QObject *parent = nullptr); @@ -38,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -62,11 +66,15 @@ private: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/models/containers_model.cpp b/client/ui/models/containers_model.cpp index b8633a18..41d26bc7 100644 --- a/client/ui/models/containers_model.cpp +++ b/client/ui/models/containers_model.cpp @@ -41,6 +41,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const case IsCurrentlyProcessedRole: return container == static_cast(m_processedContainerIndex); case IsSupportedRole: return ContainerProps::isSupportedByCurrentPlatform(container); case IsShareableRole: return ContainerProps::isShareable(container); + case InstallPageOrderRole: return ContainerProps::installPageOrder(container); } return QVariant(); @@ -93,6 +94,26 @@ bool ContainersModel::isServiceContainer(const int containerIndex) return qvariant_cast(data(index(containerIndex), ServiceTypeRole) == ServiceType::Other); } +bool ContainersModel::hasInstalledServices() +{ + for (const auto &container : m_containers.keys()) { + if (ContainerProps::containerService(container) == ServiceType::Other) { + return true; + } + } + return false; +} + +bool ContainersModel::hasInstalledProtocols() +{ + for (const auto &container : m_containers.keys()) { + if (ContainerProps::containerService(container) == ServiceType::Vpn) { + return true; + } + } + return false; +} + QHash ContainersModel::roleNames() const { QHash roles; @@ -112,5 +133,7 @@ QHash ContainersModel::roleNames() const roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed"; roles[IsSupportedRole] = "isSupported"; roles[IsShareableRole] = "isShareable"; + + roles[InstallPageOrderRole] = "installPageOrder"; return roles; } diff --git a/client/ui/models/containers_model.h b/client/ui/models/containers_model.h index 9999307f..3bd0ddc1 100644 --- a/client/ui/models/containers_model.h +++ b/client/ui/models/containers_model.h @@ -31,7 +31,9 @@ public: IsCurrentlyProcessedRole, IsDefaultRole, IsSupportedRole, - IsShareableRole + IsShareableRole, + + InstallPageOrderRole }; int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -52,6 +54,9 @@ public slots: bool isSupportedByCurrentPlatform(const int containerIndex); bool isServiceContainer(const int containerIndex); + bool hasInstalledServices(); + bool hasInstalledProtocols(); + protected: QHash roleNames() const override; diff --git a/client/ui/models/languageModel.cpp b/client/ui/models/languageModel.cpp index 4bd01b54..0041fdd0 100644 --- a/client/ui/models/languageModel.cpp +++ b/client/ui/models/languageModel.cpp @@ -92,9 +92,9 @@ int LanguageModel::getCurrentLanguageIndex() int LanguageModel::getLineHeightAppend() { - int langIndex = getCurrentLanguageIndex(); - switch (langIndex) { - case 5: return 10; break; // Burmese + auto language = static_cast(getCurrentLanguageIndex()); + switch (language) { + case LanguageSettings::AvailableLanguageEnum::Burmese: return 10; break; default: return 0; break; } } @@ -103,3 +103,12 @@ QString LanguageModel::getCurrentLanguageName() { return m_availableLanguages[getCurrentLanguageIndex()].name; } + +QString LanguageModel::getCurrentSiteUrl() +{ + auto language = static_cast(getCurrentLanguageIndex()); + switch (language) { + case LanguageSettings::AvailableLanguageEnum::Russian: return "https://storage.googleapis.com/amnezia/amnezia.org"; + default: return "https://amnezia.org"; + } +} diff --git a/client/ui/models/languageModel.h b/client/ui/models/languageModel.h index ea29dda5..2c80880a 100644 --- a/client/ui/models/languageModel.h +++ b/client/ui/models/languageModel.h @@ -59,6 +59,7 @@ public slots: int getCurrentLanguageIndex(); int getLineHeightAppend(); QString getCurrentLanguageName(); + QString getCurrentSiteUrl(); signals: void updateTranslations(const QLocale &locale); diff --git a/client/ui/models/protocols/awgConfigModel.cpp b/client/ui/models/protocols/awgConfigModel.cpp index 6b9234f4..860c8395 100644 --- a/client/ui/models/protocols/awgConfigModel.cpp +++ b/client/ui/models/protocols/awgConfigModel.cpp @@ -21,28 +21,30 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in } switch (role) { - case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break; - case Roles::MtuRole: m_protocolConfig.insert(config_key::mtu, value.toString()); break; - case Roles::JunkPacketCountRole: m_protocolConfig.insert(config_key::junkPacketCount, value.toString()); break; - case Roles::JunkPacketMinSizeRole: m_protocolConfig.insert(config_key::junkPacketMinSize, value.toString()); break; - case Roles::JunkPacketMaxSizeRole: m_protocolConfig.insert(config_key::junkPacketMaxSize, value.toString()); break; - case Roles::InitPacketJunkSizeRole: - m_protocolConfig.insert(config_key::initPacketJunkSize, value.toString()); + case Roles::SubnetAddressRole: m_serverProtocolConfig.insert(config_key::subnet_address, value.toString()); break; + case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break; + + case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break; + case Roles::ClientJunkPacketCountRole: m_clientProtocolConfig.insert(config_key::junkPacketCount, value.toString()); break; + case Roles::ClientJunkPacketMinSizeRole: m_clientProtocolConfig.insert(config_key::junkPacketMinSize, value.toString()); break; + case Roles::ClientJunkPacketMaxSizeRole: m_clientProtocolConfig.insert(config_key::junkPacketMaxSize, value.toString()); break; + + case Roles::ServerJunkPacketCountRole: m_serverProtocolConfig.insert(config_key::junkPacketCount, value.toString()); break; + case Roles::ServerJunkPacketMinSizeRole: m_serverProtocolConfig.insert(config_key::junkPacketMinSize, value.toString()); break; + case Roles::ServerJunkPacketMaxSizeRole: m_serverProtocolConfig.insert(config_key::junkPacketMaxSize, value.toString()); break; + case Roles::ServerInitPacketJunkSizeRole: m_serverProtocolConfig.insert(config_key::initPacketJunkSize, value.toString()); break; + case Roles::ServerResponsePacketJunkSizeRole: + m_serverProtocolConfig.insert(config_key::responsePacketJunkSize, value.toString()); break; - case Roles::ResponsePacketJunkSizeRole: - m_protocolConfig.insert(config_key::responsePacketJunkSize, value.toString()); + case Roles::ServerInitPacketMagicHeaderRole: m_serverProtocolConfig.insert(config_key::initPacketMagicHeader, value.toString()); break; + case Roles::ServerResponsePacketMagicHeaderRole: + m_serverProtocolConfig.insert(config_key::responsePacketMagicHeader, value.toString()); break; - case Roles::InitPacketMagicHeaderRole: - m_protocolConfig.insert(config_key::initPacketMagicHeader, value.toString()); + case Roles::ServerUnderloadPacketMagicHeaderRole: + m_serverProtocolConfig.insert(config_key::underloadPacketMagicHeader, value.toString()); break; - case Roles::ResponsePacketMagicHeaderRole: - m_protocolConfig.insert(config_key::responsePacketMagicHeader, value.toString()); - break; - case Roles::UnderloadPacketMagicHeaderRole: - m_protocolConfig.insert(config_key::underloadPacketMagicHeader, value.toString()); - break; - case Roles::TransportPacketMagicHeaderRole: - m_protocolConfig.insert(config_key::transportPacketMagicHeader, value.toString()); + case Roles::ServerTransportPacketMagicHeaderRole: + m_serverProtocolConfig.insert(config_key::transportPacketMagicHeader, value.toString()); break; } @@ -57,17 +59,23 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const } switch (role) { - case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(); - case Roles::MtuRole: return m_protocolConfig.value(config_key::mtu).toString(); - case Roles::JunkPacketCountRole: return m_protocolConfig.value(config_key::junkPacketCount); - case Roles::JunkPacketMinSizeRole: return m_protocolConfig.value(config_key::junkPacketMinSize); - case Roles::JunkPacketMaxSizeRole: return m_protocolConfig.value(config_key::junkPacketMaxSize); - case Roles::InitPacketJunkSizeRole: return m_protocolConfig.value(config_key::initPacketJunkSize); - case Roles::ResponsePacketJunkSizeRole: return m_protocolConfig.value(config_key::responsePacketJunkSize); - case Roles::InitPacketMagicHeaderRole: return m_protocolConfig.value(config_key::initPacketMagicHeader); - case Roles::ResponsePacketMagicHeaderRole: return m_protocolConfig.value(config_key::responsePacketMagicHeader); - case Roles::UnderloadPacketMagicHeaderRole: return m_protocolConfig.value(config_key::underloadPacketMagicHeader); - case Roles::TransportPacketMagicHeaderRole: return m_protocolConfig.value(config_key::transportPacketMagicHeader); + case Roles::SubnetAddressRole: return m_serverProtocolConfig.value(config_key::subnet_address).toString(); + case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString(); + + case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu); + case Roles::ClientJunkPacketCountRole: return m_clientProtocolConfig.value(config_key::junkPacketCount); + case Roles::ClientJunkPacketMinSizeRole: return m_clientProtocolConfig.value(config_key::junkPacketMinSize); + case Roles::ClientJunkPacketMaxSizeRole: return m_clientProtocolConfig.value(config_key::junkPacketMaxSize); + + case Roles::ServerJunkPacketCountRole: return m_serverProtocolConfig.value(config_key::junkPacketCount); + case Roles::ServerJunkPacketMinSizeRole: return m_serverProtocolConfig.value(config_key::junkPacketMinSize); + case Roles::ServerJunkPacketMaxSizeRole: return m_serverProtocolConfig.value(config_key::junkPacketMaxSize); + case Roles::ServerInitPacketJunkSizeRole: return m_serverProtocolConfig.value(config_key::initPacketJunkSize); + case Roles::ServerResponsePacketJunkSizeRole: return m_serverProtocolConfig.value(config_key::responsePacketJunkSize); + case Roles::ServerInitPacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::initPacketMagicHeader); + case Roles::ServerResponsePacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::responsePacketMagicHeader); + case Roles::ServerUnderloadPacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::underloadPacketMagicHeader); + case Roles::ServerTransportPacketMagicHeaderRole: return m_serverProtocolConfig.value(config_key::transportPacketMagicHeader); } return QVariant(); @@ -80,52 +88,64 @@ void AwgConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; - QJsonObject protocolConfig = config.value(config_key::awg).toObject(); + QJsonObject serverProtocolConfig = config.value(config_key::awg).toObject(); - m_protocolConfig[config_key::last_config] = protocolConfig.value(config_key::last_config); - m_protocolConfig[config_key::port] = protocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); - m_protocolConfig[config_key::mtu] = protocolConfig.value(config_key::mtu).toString(protocols::awg::defaultMtu); - m_protocolConfig[config_key::junkPacketCount] = - protocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); - m_protocolConfig[config_key::junkPacketMinSize] = - protocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); - m_protocolConfig[config_key::junkPacketMaxSize] = - protocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); - m_protocolConfig[config_key::initPacketJunkSize] = - protocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); - m_protocolConfig[config_key::responsePacketJunkSize] = - protocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); - m_protocolConfig[config_key::initPacketMagicHeader] = - protocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); - m_protocolConfig[config_key::responsePacketMagicHeader] = - protocolConfig.value(config_key::responsePacketMagicHeader) - .toString(protocols::awg::defaultResponsePacketMagicHeader); - m_protocolConfig[config_key::underloadPacketMagicHeader] = - protocolConfig.value(config_key::underloadPacketMagicHeader) - .toString(protocols::awg::defaultUnderloadPacketMagicHeader); - m_protocolConfig[config_key::transportPacketMagicHeader] = - protocolConfig.value(config_key::transportPacketMagicHeader) - .toString(protocols::awg::defaultTransportPacketMagicHeader); + auto defaultTransportProto = ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::Awg), Proto::Awg); + m_serverProtocolConfig.insert(config_key::transport_proto, + serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config); + m_serverProtocolConfig[config_key::subnet_address] = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); + m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); + m_serverProtocolConfig[config_key::junkPacketCount] = + serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); + m_serverProtocolConfig[config_key::junkPacketMinSize] = + serverProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); + m_serverProtocolConfig[config_key::junkPacketMaxSize] = + serverProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + m_serverProtocolConfig[config_key::initPacketJunkSize] = + serverProtocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); + m_serverProtocolConfig[config_key::responsePacketJunkSize] = + serverProtocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); + m_serverProtocolConfig[config_key::initPacketMagicHeader] = + serverProtocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); + m_serverProtocolConfig[config_key::responsePacketMagicHeader] = + serverProtocolConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader); + m_serverProtocolConfig[config_key::underloadPacketMagicHeader] = + serverProtocolConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader); + m_serverProtocolConfig[config_key::transportPacketMagicHeader] = + serverProtocolConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + m_clientProtocolConfig[config_key::mtu] = clientProtocolConfig[config_key::mtu].toString(protocols::awg::defaultMtu); + m_clientProtocolConfig[config_key::junkPacketCount] = + clientProtocolConfig.value(config_key::junkPacketCount).toString(m_serverProtocolConfig[config_key::junkPacketCount].toString()); + m_clientProtocolConfig[config_key::junkPacketMinSize] = + clientProtocolConfig.value(config_key::junkPacketMinSize).toString(m_serverProtocolConfig[config_key::junkPacketMinSize].toString()); + m_clientProtocolConfig[config_key::junkPacketMaxSize] = + clientProtocolConfig.value(config_key::junkPacketMaxSize).toString(m_serverProtocolConfig[config_key::junkPacketMaxSize].toString()); endResetModel(); } QJsonObject AwgConfigModel::getConfig() { const AwgConfig oldConfig(m_fullConfig.value(config_key::awg).toObject()); - const AwgConfig newConfig(m_protocolConfig); + const AwgConfig newConfig(m_serverProtocolConfig); if (!oldConfig.hasEqualServerSettings(newConfig)) { - m_protocolConfig.remove(config_key::last_config); + m_serverProtocolConfig.remove(config_key::last_config); } else { - auto lastConfig = m_protocolConfig.value(config_key::last_config).toString(); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); QJsonObject jsonConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); - jsonConfig[config_key::mtu] = newConfig.mtu; + jsonConfig[config_key::mtu] = m_clientProtocolConfig[config_key::mtu]; + jsonConfig[config_key::junkPacketCount] = m_clientProtocolConfig[config_key::junkPacketCount]; + jsonConfig[config_key::junkPacketMinSize] = m_clientProtocolConfig[config_key::junkPacketMinSize]; + jsonConfig[config_key::junkPacketMaxSize] = m_clientProtocolConfig[config_key::junkPacketMaxSize]; - m_protocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); + m_serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); } - m_fullConfig.insert(config_key::awg, m_protocolConfig); + m_fullConfig.insert(config_key::awg, m_serverProtocolConfig); return m_fullConfig; } @@ -139,56 +159,75 @@ bool AwgConfigModel::isPacketSizeEqual(const int s1, const int s2) return (AwgConstant::messageInitiationSize + s1 == AwgConstant::messageResponseSize + s2); } +bool AwgConfigModel::isServerSettingsEqual() +{ + const AwgConfig oldConfig(m_fullConfig.value(config_key::awg).toObject()); + const AwgConfig newConfig(m_serverProtocolConfig); + + return oldConfig.hasEqualServerSettings(newConfig); +} + QHash AwgConfigModel::roleNames() const { QHash roles; + roles[SubnetAddressRole] = "subnetAddress"; roles[PortRole] = "port"; - roles[MtuRole] = "mtu"; - roles[JunkPacketCountRole] = "junkPacketCount"; - roles[JunkPacketMinSizeRole] = "junkPacketMinSize"; - roles[JunkPacketMaxSizeRole] = "junkPacketMaxSize"; - roles[InitPacketJunkSizeRole] = "initPacketJunkSize"; - roles[ResponsePacketJunkSizeRole] = "responsePacketJunkSize"; - roles[InitPacketMagicHeaderRole] = "initPacketMagicHeader"; - roles[ResponsePacketMagicHeaderRole] = "responsePacketMagicHeader"; - roles[UnderloadPacketMagicHeaderRole] = "underloadPacketMagicHeader"; - roles[TransportPacketMagicHeaderRole] = "transportPacketMagicHeader"; + + roles[ClientMtuRole] = "clientMtu"; + roles[ClientJunkPacketCountRole] = "clientJunkPacketCount"; + roles[ClientJunkPacketMinSizeRole] = "clientJunkPacketMinSize"; + roles[ClientJunkPacketMaxSizeRole] = "clientJunkPacketMaxSize"; + + roles[ServerJunkPacketCountRole] = "serverJunkPacketCount"; + roles[ServerJunkPacketMinSizeRole] = "serverJunkPacketMinSize"; + roles[ServerJunkPacketMaxSizeRole] = "serverJunkPacketMaxSize"; + roles[ServerInitPacketJunkSizeRole] = "serverInitPacketJunkSize"; + roles[ServerResponsePacketJunkSizeRole] = "serverResponsePacketJunkSize"; + roles[ServerInitPacketMagicHeaderRole] = "serverInitPacketMagicHeader"; + roles[ServerResponsePacketMagicHeaderRole] = "serverResponsePacketMagicHeader"; + roles[ServerUnderloadPacketMagicHeaderRole] = "serverUnderloadPacketMagicHeader"; + roles[ServerTransportPacketMagicHeaderRole] = "serverTransportPacketMagicHeader"; return roles; } -AwgConfig::AwgConfig(const QJsonObject &jsonConfig) +AwgConfig::AwgConfig(const QJsonObject &serverProtocolConfig) { - port = jsonConfig.value(config_key::port).toString(protocols::awg::defaultPort); - mtu = jsonConfig.value(config_key::mtu).toString(protocols::awg::defaultMtu); - junkPacketCount = jsonConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); - junkPacketMinSize = - jsonConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); - junkPacketMaxSize = - jsonConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); - initPacketJunkSize = - jsonConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); - responsePacketJunkSize = - jsonConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); - initPacketMagicHeader = - jsonConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); - responsePacketMagicHeader = jsonConfig.value(config_key::responsePacketMagicHeader) - .toString(protocols::awg::defaultResponsePacketMagicHeader); - underloadPacketMagicHeader = jsonConfig.value(config_key::underloadPacketMagicHeader) - .toString(protocols::awg::defaultUnderloadPacketMagicHeader); - transportPacketMagicHeader = jsonConfig.value(config_key::transportPacketMagicHeader) - .toString(protocols::awg::defaultTransportPacketMagicHeader); + auto lastConfig = serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + clientMtu = clientProtocolConfig[config_key::mtu].toString(protocols::awg::defaultMtu); + clientJunkPacketCount = clientProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); + clientJunkPacketMinSize = clientProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); + clientJunkPacketMaxSize = clientProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + + subnetAddress = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); + port = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); + serverJunkPacketCount = serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); + serverJunkPacketMinSize = serverProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); + serverJunkPacketMaxSize = serverProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + serverInitPacketJunkSize = serverProtocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize); + serverResponsePacketJunkSize = + serverProtocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize); + serverInitPacketMagicHeader = + serverProtocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader); + serverResponsePacketMagicHeader = + serverProtocolConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader); + serverUnderloadPacketMagicHeader = + serverProtocolConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader); + serverTransportPacketMagicHeader = + serverProtocolConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader); } bool AwgConfig::hasEqualServerSettings(const AwgConfig &other) const { - if (port != other.port || junkPacketCount != other.junkPacketCount || junkPacketMinSize != other.junkPacketMinSize - || junkPacketMaxSize != other.junkPacketMaxSize || initPacketJunkSize != other.initPacketJunkSize - || responsePacketJunkSize != other.responsePacketJunkSize || initPacketMagicHeader != other.initPacketMagicHeader - || responsePacketMagicHeader != other.responsePacketMagicHeader - || underloadPacketMagicHeader != other.underloadPacketMagicHeader - || transportPacketMagicHeader != other.transportPacketMagicHeader) { + if (subnetAddress != other.subnetAddress || port != other.port || serverJunkPacketCount != other.serverJunkPacketCount + || serverJunkPacketMinSize != other.serverJunkPacketMinSize || serverJunkPacketMaxSize != other.serverJunkPacketMaxSize + || serverInitPacketJunkSize != other.serverInitPacketJunkSize || serverResponsePacketJunkSize != other.serverResponsePacketJunkSize + || serverInitPacketMagicHeader != other.serverInitPacketMagicHeader + || serverResponsePacketMagicHeader != other.serverResponsePacketMagicHeader + || serverUnderloadPacketMagicHeader != other.serverUnderloadPacketMagicHeader + || serverTransportPacketMagicHeader != other.serverTransportPacketMagicHeader) { return false; } return true; @@ -196,7 +235,8 @@ bool AwgConfig::hasEqualServerSettings(const AwgConfig &other) const bool AwgConfig::hasEqualClientSettings(const AwgConfig &other) const { - if (mtu != other.mtu) { + if (clientMtu != other.clientMtu || clientJunkPacketCount != other.clientJunkPacketCount + || clientJunkPacketMinSize != other.clientJunkPacketMinSize || clientJunkPacketMaxSize != other.clientJunkPacketMaxSize) { return false; } return true; diff --git a/client/ui/models/protocols/awgConfigModel.h b/client/ui/models/protocols/awgConfigModel.h index 80375d38..c1f8bb27 100644 --- a/client/ui/models/protocols/awgConfigModel.h +++ b/client/ui/models/protocols/awgConfigModel.h @@ -15,17 +15,23 @@ struct AwgConfig { AwgConfig(const QJsonObject &jsonConfig); + QString subnetAddress; QString port; - QString mtu; - QString junkPacketCount; - QString junkPacketMinSize; - QString junkPacketMaxSize; - QString initPacketJunkSize; - QString responsePacketJunkSize; - QString initPacketMagicHeader; - QString responsePacketMagicHeader; - QString underloadPacketMagicHeader; - QString transportPacketMagicHeader; + + QString clientMtu; + QString clientJunkPacketCount; + QString clientJunkPacketMinSize; + QString clientJunkPacketMaxSize; + + QString serverJunkPacketCount; + QString serverJunkPacketMinSize; + QString serverJunkPacketMaxSize; + QString serverInitPacketJunkSize; + QString serverResponsePacketJunkSize; + QString serverInitPacketMagicHeader; + QString serverResponsePacketMagicHeader; + QString serverUnderloadPacketMagicHeader; + QString serverTransportPacketMagicHeader; bool hasEqualServerSettings(const AwgConfig &other) const; bool hasEqualClientSettings(const AwgConfig &other) const; @@ -38,17 +44,23 @@ class AwgConfigModel : public QAbstractListModel public: enum Roles { - PortRole = Qt::UserRole + 1, - MtuRole, - JunkPacketCountRole, - JunkPacketMinSizeRole, - JunkPacketMaxSizeRole, - InitPacketJunkSizeRole, - ResponsePacketJunkSizeRole, - InitPacketMagicHeaderRole, - ResponsePacketMagicHeaderRole, - UnderloadPacketMagicHeaderRole, - TransportPacketMagicHeaderRole + SubnetAddressRole = Qt::UserRole + 1, + PortRole, + + ClientMtuRole, + ClientJunkPacketCountRole, + ClientJunkPacketMinSizeRole, + ClientJunkPacketMaxSizeRole, + + ServerJunkPacketCountRole, + ServerJunkPacketMinSizeRole, + ServerJunkPacketMaxSizeRole, + ServerInitPacketJunkSizeRole, + ServerResponsePacketJunkSizeRole, + ServerInitPacketMagicHeaderRole, + ServerResponsePacketMagicHeaderRole, + ServerUnderloadPacketMagicHeaderRole, + ServerTransportPacketMagicHeaderRole }; explicit AwgConfigModel(QObject *parent = nullptr); @@ -65,12 +77,15 @@ public slots: bool isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4); bool isPacketSizeEqual(const int s1, const int s2); + bool isServerSettingsEqual(); + protected: QHash roleNames() const override; private: DockerContainer m_container; - QJsonObject m_protocolConfig; + QJsonObject m_serverProtocolConfig; + QJsonObject m_clientProtocolConfig; QJsonObject m_fullConfig; }; diff --git a/client/ui/models/protocols/cloakConfigModel.cpp b/client/ui/models/protocols/cloakConfigModel.cpp index 203f08b5..a9f06f4d 100644 --- a/client/ui/models/protocols/cloakConfigModel.cpp +++ b/client/ui/models/protocols/cloakConfigModel.cpp @@ -51,14 +51,11 @@ void CloakConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; QJsonObject protocolConfig = config.value(config_key::cloak).toObject(); - m_protocolConfig.insert(config_key::cipher, - protocolConfig.value(config_key::cipher).toString(protocols::cloak::defaultCipher)); - - m_protocolConfig.insert(config_key::port, - protocolConfig.value(config_key::port).toString(protocols::cloak::defaultPort)); - - m_protocolConfig.insert(config_key::site, - protocolConfig.value(config_key::site).toString(protocols::cloak::defaultRedirSite)); + auto defaultTransportProto = ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::Cloak), Proto::Cloak); + m_protocolConfig.insert(config_key::transport_proto, protocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_protocolConfig.insert(config_key::cipher, protocolConfig.value(config_key::cipher).toString(protocols::cloak::defaultCipher)); + m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString(protocols::cloak::defaultPort)); + m_protocolConfig.insert(config_key::site, protocolConfig.value(config_key::site).toString(protocols::cloak::defaultRedirSite)); endResetModel(); } diff --git a/client/ui/models/protocols/ikev2ConfigModel.cpp b/client/ui/models/protocols/ikev2ConfigModel.cpp index a11b6652..05494a07 100644 --- a/client/ui/models/protocols/ikev2ConfigModel.cpp +++ b/client/ui/models/protocols/ikev2ConfigModel.cpp @@ -35,8 +35,7 @@ QVariant Ikev2ConfigModel::data(const QModelIndex &index, int role) const switch (role) { case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(protocols::shadowsocks::defaultPort); - case Roles::CipherRole: - return m_protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher); + case Roles::CipherRole: return m_protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher); } return QVariant(); @@ -50,11 +49,8 @@ void Ikev2ConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; QJsonObject protocolConfig = config.value(config_key::shadowsocks).toObject(); - m_protocolConfig.insert(config_key::cipher, - protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher)); - - m_protocolConfig.insert(config_key::port, - protocolConfig.value(config_key::port).toString(protocols::shadowsocks::defaultPort)); + m_protocolConfig.insert(config_key::cipher, protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher)); + m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString(protocols::shadowsocks::defaultPort)); endResetModel(); } diff --git a/client/ui/models/protocols/openvpnConfigModel.cpp b/client/ui/models/protocols/openvpnConfigModel.cpp index 30d00306..a04c2b1a 100644 --- a/client/ui/models/protocols/openvpnConfigModel.cpp +++ b/client/ui/models/protocols/openvpnConfigModel.cpp @@ -19,9 +19,7 @@ bool OpenVpnConfigModel::setData(const QModelIndex &index, const QVariant &value } switch (role) { - case Roles::SubnetAddressRole: - m_protocolConfig.insert(amnezia::config_key::subnet_address, value.toString()); - break; + case Roles::SubnetAddressRole: m_protocolConfig.insert(amnezia::config_key::subnet_address, value.toString()); break; case Roles::TransportProtoRole: m_protocolConfig.insert(config_key::transport_proto, value.toString()); break; case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break; case Roles::AutoNegotiateEncryprionRole: m_protocolConfig.insert(config_key::ncp_disable, !value.toBool()); break; @@ -29,12 +27,8 @@ bool OpenVpnConfigModel::setData(const QModelIndex &index, const QVariant &value case Roles::CipherRole: m_protocolConfig.insert(config_key::cipher, value.toString()); break; case Roles::TlsAuthRole: m_protocolConfig.insert(config_key::tls_auth, value.toBool()); break; case Roles::BlockDnsRole: m_protocolConfig.insert(config_key::block_outside_dns, value.toBool()); break; - case Roles::AdditionalClientCommandsRole: - m_protocolConfig.insert(config_key::additional_client_config, value.toString()); - break; - case Roles::AdditionalServerCommandsRole: - m_protocolConfig.insert(config_key::additional_server_config, value.toString()); - break; + case Roles::AdditionalClientCommandsRole: m_protocolConfig.insert(config_key::additional_client_config, value.toString()); break; + case Roles::AdditionalServerCommandsRole: m_protocolConfig.insert(config_key::additional_server_config, value.toString()); break; } emit dataChanged(index, index, QList { role }); @@ -49,26 +43,21 @@ QVariant OpenVpnConfigModel::data(const QModelIndex &index, int role) const switch (role) { case Roles::SubnetAddressRole: - return m_protocolConfig.value(amnezia::config_key::subnet_address) - .toString(amnezia::protocols::openvpn::defaultSubnetAddress); + return m_protocolConfig.value(amnezia::config_key::subnet_address).toString(amnezia::protocols::openvpn::defaultSubnetAddress); case Roles::TransportProtoRole: return m_protocolConfig.value(config_key::transport_proto).toString(protocols::openvpn::defaultTransportProto); case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(protocols::openvpn::defaultPort); case Roles::AutoNegotiateEncryprionRole: return !m_protocolConfig.value(config_key::ncp_disable).toBool(protocols::openvpn::defaultNcpDisable); case Roles::HashRole: return m_protocolConfig.value(config_key::hash).toString(protocols::openvpn::defaultHash); - case Roles::CipherRole: - return m_protocolConfig.value(config_key::cipher).toString(protocols::openvpn::defaultCipher); - case Roles::TlsAuthRole: - return m_protocolConfig.value(config_key::tls_auth).toBool(protocols::openvpn::defaultTlsAuth); + case Roles::CipherRole: return m_protocolConfig.value(config_key::cipher).toString(protocols::openvpn::defaultCipher); + case Roles::TlsAuthRole: return m_protocolConfig.value(config_key::tls_auth).toBool(protocols::openvpn::defaultTlsAuth); case Roles::BlockDnsRole: return m_protocolConfig.value(config_key::block_outside_dns).toBool(protocols::openvpn::defaultBlockOutsideDns); case Roles::AdditionalClientCommandsRole: - return m_protocolConfig.value(config_key::additional_client_config) - .toString(protocols::openvpn::defaultAdditionalClientConfig); + return m_protocolConfig.value(config_key::additional_client_config).toString(protocols::openvpn::defaultAdditionalClientConfig); case Roles::AdditionalServerCommandsRole: - return m_protocolConfig.value(config_key::additional_server_config) - .toString(protocols::openvpn::defaultAdditionalServerConfig); + return m_protocolConfig.value(config_key::additional_server_config).toString(protocols::openvpn::defaultAdditionalServerConfig); case Roles::IsPortEditable: return m_container == DockerContainer::OpenVpn ? true : false; case Roles::IsTransportProtoEditable: return m_container == DockerContainer::OpenVpn ? true : false; case Roles::HasRemoveButton: return m_container == DockerContainer::OpenVpn ? true : false; @@ -84,14 +73,13 @@ void OpenVpnConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; QJsonObject protocolConfig = config.value(config_key::openvpn).toObject(); - m_protocolConfig.insert(config_key::subnet_address, - protocolConfig.value(amnezia::config_key::subnet_address) - .toString(amnezia::protocols::openvpn::defaultSubnetAddress)); + m_protocolConfig.insert( + config_key::subnet_address, + protocolConfig.value(amnezia::config_key::subnet_address).toString(amnezia::protocols::openvpn::defaultSubnetAddress)); QString transportProto; if (m_container == DockerContainer::OpenVpn) { - transportProto = - protocolConfig.value(config_key::transport_proto).toString(protocols::openvpn::defaultTransportProto); + transportProto = protocolConfig.value(config_key::transport_proto).toString(protocols::openvpn::defaultTransportProto); } else { transportProto = "tcp"; } @@ -100,23 +88,18 @@ void OpenVpnConfigModel::updateModel(const QJsonObject &config) m_protocolConfig.insert(config_key::ncp_disable, protocolConfig.value(config_key::ncp_disable).toBool(protocols::openvpn::defaultNcpDisable)); - m_protocolConfig.insert(config_key::cipher, - protocolConfig.value(config_key::cipher).toString(protocols::openvpn::defaultCipher)); - m_protocolConfig.insert(config_key::hash, - protocolConfig.value(config_key::hash).toString(protocols::openvpn::defaultHash)); + m_protocolConfig.insert(config_key::cipher, protocolConfig.value(config_key::cipher).toString(protocols::openvpn::defaultCipher)); + m_protocolConfig.insert(config_key::hash, protocolConfig.value(config_key::hash).toString(protocols::openvpn::defaultHash)); m_protocolConfig.insert(config_key::block_outside_dns, protocolConfig.value(config_key::block_outside_dns).toBool(protocols::openvpn::defaultBlockOutsideDns)); - m_protocolConfig.insert(config_key::port, - protocolConfig.value(config_key::port).toString(protocols::openvpn::defaultPort)); + m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString(protocols::openvpn::defaultPort)); + m_protocolConfig.insert(config_key::tls_auth, protocolConfig.value(config_key::tls_auth).toBool(protocols::openvpn::defaultTlsAuth)); m_protocolConfig.insert( - config_key::tls_auth, - protocolConfig.value(config_key::tls_auth).toBool(protocols::openvpn::defaultTlsAuth)); - m_protocolConfig.insert(config_key::additional_client_config, - protocolConfig.value(config_key::additional_client_config) - .toString(protocols::openvpn::defaultAdditionalClientConfig)); - m_protocolConfig.insert(config_key::additional_server_config, - protocolConfig.value(config_key::additional_server_config) - .toString(protocols::openvpn::defaultAdditionalServerConfig)); + config_key::additional_client_config, + protocolConfig.value(config_key::additional_client_config).toString(protocols::openvpn::defaultAdditionalClientConfig)); + m_protocolConfig.insert( + config_key::additional_server_config, + protocolConfig.value(config_key::additional_server_config).toString(protocols::openvpn::defaultAdditionalServerConfig)); endResetModel(); } diff --git a/client/ui/models/protocols/shadowsocksConfigModel.cpp b/client/ui/models/protocols/shadowsocksConfigModel.cpp index 2fe2d2a9..769bef20 100644 --- a/client/ui/models/protocols/shadowsocksConfigModel.cpp +++ b/client/ui/models/protocols/shadowsocksConfigModel.cpp @@ -35,8 +35,7 @@ QVariant ShadowSocksConfigModel::data(const QModelIndex &index, int role) const switch (role) { case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(protocols::shadowsocks::defaultPort); - case Roles::CipherRole: - return m_protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher); + case Roles::CipherRole: return m_protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher); case Roles::IsPortEditableRole: return m_container == DockerContainer::ShadowSocks ? true : false; case Roles::IsCipherEditableRole: return m_container == DockerContainer::ShadowSocks ? true : false; } @@ -52,11 +51,11 @@ void ShadowSocksConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; QJsonObject protocolConfig = config.value(config_key::shadowsocks).toObject(); - m_protocolConfig.insert(config_key::cipher, - protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher)); - - m_protocolConfig.insert(config_key::port, - protocolConfig.value(config_key::port).toString(protocols::shadowsocks::defaultPort)); + auto defaultTransportProto = ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::ShadowSocks), Proto::ShadowSocks); + m_protocolConfig.insert(config_key::transport_proto, + protocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_protocolConfig.insert(config_key::cipher, protocolConfig.value(config_key::cipher).toString(protocols::shadowsocks::defaultCipher)); + m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString(protocols::shadowsocks::defaultPort)); endResetModel(); } diff --git a/client/ui/models/protocols/wireguardConfigModel.cpp b/client/ui/models/protocols/wireguardConfigModel.cpp index 8903f40f..1c8e1341 100644 --- a/client/ui/models/protocols/wireguardConfigModel.cpp +++ b/client/ui/models/protocols/wireguardConfigModel.cpp @@ -21,8 +21,9 @@ bool WireGuardConfigModel::setData(const QModelIndex &index, const QVariant &val } switch (role) { - case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break; - case Roles::MtuRole: m_protocolConfig.insert(config_key::mtu, value.toString()); break; + case Roles::SubnetAddressRole: m_serverProtocolConfig.insert(config_key::subnet_address, value.toString()); break; + case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break; + case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break; } emit dataChanged(index, index, QList { role }); @@ -36,8 +37,9 @@ QVariant WireGuardConfigModel::data(const QModelIndex &index, int role) const } switch (role) { - case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString(); - case Roles::MtuRole: return m_protocolConfig.value(config_key::mtu).toString(); + case Roles::SubnetAddressRole: return m_serverProtocolConfig.value(config_key::subnet_address).toString(); + case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString(); + case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu); } return QVariant(); @@ -49,14 +51,19 @@ void WireGuardConfigModel::updateModel(const QJsonObject &config) m_container = ContainerProps::containerFromString(config.value(config_key::container).toString()); m_fullConfig = config; - QJsonObject protocolConfig = config.value(config_key::wireguard).toObject(); + QJsonObject serverProtocolConfig = config.value(config_key::wireguard).toObject(); - m_protocolConfig[config_key::last_config] = protocolConfig.value(config_key::last_config); - m_protocolConfig[config_key::port] = - protocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); + auto defaultTransportProto = + ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::WireGuard), Proto::WireGuard); + m_serverProtocolConfig.insert(config_key::transport_proto, + serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config); + m_serverProtocolConfig[config_key::subnet_address] = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); + m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); - m_protocolConfig[config_key::mtu] = - protocolConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + m_clientProtocolConfig[config_key::mtu] = clientProtocolConfig[config_key::mtu].toString(protocols::wireguard::defaultMtu); endResetModel(); } @@ -64,41 +71,54 @@ void WireGuardConfigModel::updateModel(const QJsonObject &config) QJsonObject WireGuardConfigModel::getConfig() { const WgConfig oldConfig(m_fullConfig.value(config_key::wireguard).toObject()); - const WgConfig newConfig(m_protocolConfig); + const WgConfig newConfig(m_serverProtocolConfig); if (!oldConfig.hasEqualServerSettings(newConfig)) { - m_protocolConfig.remove(config_key::last_config); + m_serverProtocolConfig.remove(config_key::last_config); } else { - auto lastConfig = m_protocolConfig.value(config_key::last_config).toString(); + auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); QJsonObject jsonConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); - jsonConfig[config_key::mtu] = newConfig.mtu; + jsonConfig[config_key::mtu] = m_clientProtocolConfig[config_key::mtu]; - m_protocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); + m_serverProtocolConfig[config_key::last_config] = QString(QJsonDocument(jsonConfig).toJson()); } - m_fullConfig.insert(config_key::wireguard, m_protocolConfig); + m_fullConfig.insert(config_key::wireguard, m_serverProtocolConfig); return m_fullConfig; } +bool WireGuardConfigModel::isServerSettingsEqual() +{ + const WgConfig oldConfig(m_fullConfig.value(config_key::wireguard).toObject()); + const WgConfig newConfig(m_serverProtocolConfig); + + return oldConfig.hasEqualServerSettings(newConfig); +} + QHash WireGuardConfigModel::roleNames() const { QHash roles; + roles[SubnetAddressRole] = "subnetAddress"; roles[PortRole] = "port"; - roles[MtuRole] = "mtu"; + roles[ClientMtuRole] = "clientMtu"; return roles; } -WgConfig::WgConfig(const QJsonObject &jsonConfig) +WgConfig::WgConfig(const QJsonObject &serverProtocolConfig) { - port = jsonConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); - mtu = jsonConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu); + auto lastConfig = serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); + clientMtu = clientProtocolConfig[config_key::mtu].toString(protocols::wireguard::defaultMtu); + + subnetAddress = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); + port = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); } bool WgConfig::hasEqualServerSettings(const WgConfig &other) const { - if (port != other.port) { + if (subnetAddress != other.subnetAddress || port != other.port) { return false; } return true; @@ -106,7 +126,7 @@ bool WgConfig::hasEqualServerSettings(const WgConfig &other) const bool WgConfig::hasEqualClientSettings(const WgConfig &other) const { - if (mtu != other.mtu) { + if (clientMtu != other.clientMtu) { return false; } return true; diff --git a/client/ui/models/protocols/wireguardConfigModel.h b/client/ui/models/protocols/wireguardConfigModel.h index 6cec76dd..b1ce2d61 100644 --- a/client/ui/models/protocols/wireguardConfigModel.h +++ b/client/ui/models/protocols/wireguardConfigModel.h @@ -10,8 +10,9 @@ struct WgConfig { WgConfig(const QJsonObject &jsonConfig); + QString subnetAddress; QString port; - QString mtu; + QString clientMtu; bool hasEqualServerSettings(const WgConfig &other) const; bool hasEqualClientSettings(const WgConfig &other) const; @@ -24,8 +25,9 @@ class WireGuardConfigModel : public QAbstractListModel public: enum Roles { - PortRole = Qt::UserRole + 1, - MtuRole + SubnetAddressRole = Qt::UserRole + 1, + PortRole, + ClientMtuRole }; explicit WireGuardConfigModel(QObject *parent = nullptr); @@ -39,12 +41,15 @@ public slots: void updateModel(const QJsonObject &config); QJsonObject getConfig(); + bool isServerSettingsEqual(); + protected: QHash roleNames() const override; private: DockerContainer m_container; - QJsonObject m_protocolConfig; + QJsonObject m_serverProtocolConfig; + QJsonObject m_clientProtocolConfig; QJsonObject m_fullConfig; }; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index d5b50481..84bbb2f7 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -47,8 +47,11 @@ void XrayConfigModel::updateModel(const QJsonObject &config) m_fullConfig = config; QJsonObject protocolConfig = config.value(config_key::xray).toObject(); - m_protocolConfig.insert(config_key::site, - protocolConfig.value(config_key::site).toString(protocols::xray::defaultSite)); + auto defaultTransportProto = ProtocolProps::transportProtoToString(ProtocolProps::defaultTransportProto(Proto::Xray), Proto::Xray); + m_protocolConfig.insert(config_key::transport_proto, + protocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); + m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString(protocols::xray::defaultPort)); + m_protocolConfig.insert(config_key::site, protocolConfig.value(config_key::site).toString(protocols::xray::defaultSite)); endResetModel(); } diff --git a/client/ui/models/protocols_model.cpp b/client/ui/models/protocols_model.cpp index 32447cd4..019b2d2f 100644 --- a/client/ui/models/protocols_model.cpp +++ b/client/ui/models/protocols_model.cpp @@ -16,9 +16,11 @@ QHash ProtocolsModel::roleNames() const QHash roles; roles[ProtocolNameRole] = "protocolName"; - roles[ProtocolPageRole] = "protocolPage"; + roles[ServerProtocolPageRole] = "serverProtocolPage"; + roles[ClientProtocolPageRole] = "clientProtocolPage"; roles[ProtocolIndexRole] = "protocolIndex"; roles[RawConfigRole] = "rawConfig"; + roles[IsClientProtocolExistsRole] = "isClientProtocolExists"; return roles; } @@ -34,8 +36,10 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const amnezia::Proto proto = ProtocolProps::protoFromString(m_content.keys().at(index.row())); return ProtocolProps::protocolHumanNames().value(proto); } - case ProtocolPageRole: - return static_cast(protocolPage(ProtocolProps::protoFromString(m_content.keys().at(index.row())))); + case ServerProtocolPageRole: + return static_cast(serverProtocolPage(ProtocolProps::protoFromString(m_content.keys().at(index.row())))); + case ClientProtocolPageRole: + return static_cast(clientProtocolPage(ProtocolProps::protoFromString(m_content.keys().at(index.row())))); case ProtocolIndexRole: return ProtocolProps::protoFromString(m_content.keys().at(index.row())); case RawConfigRole: { auto protocolConfig = m_content.value(ContainerProps::containerTypeToString(m_container)).toObject(); @@ -50,6 +54,15 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const } return rawConfig; } + case IsClientProtocolExistsRole: { + auto protocolConfig = m_content.value(ContainerProps::containerTypeToString(m_container)).toObject(); + auto lastConfigJsonDoc = + QJsonDocument::fromJson(protocolConfig.value(config_key::last_config).toString().toUtf8()); + auto lastConfigJson = lastConfigJsonDoc.object(); + + auto configString = lastConfigJson.value(config_key::config).toString(); + return !configString.isEmpty(); + } } return QVariant(); @@ -70,7 +83,7 @@ QJsonObject ProtocolsModel::getConfig() return config; } -PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const +PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const { switch (protocol) { case Proto::OpenVpn: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; @@ -90,3 +103,12 @@ PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; } } + +PageLoader::PageEnum ProtocolsModel::clientProtocolPage(Proto protocol) const +{ + switch (protocol) { + case Proto::WireGuard: return PageLoader::PageEnum::PageProtocolWireGuardClientSettings; + case Proto::Awg: return PageLoader::PageEnum::PageProtocolAwgClientSettings; + default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; + } +} diff --git a/client/ui/models/protocols_model.h b/client/ui/models/protocols_model.h index 5ee8a3dd..5c52ee86 100644 --- a/client/ui/models/protocols_model.h +++ b/client/ui/models/protocols_model.h @@ -13,9 +13,11 @@ class ProtocolsModel : public QAbstractListModel public: enum Roles { ProtocolNameRole = Qt::UserRole + 1, - ProtocolPageRole, + ServerProtocolPageRole, + ClientProtocolPageRole, ProtocolIndexRole, - RawConfigRole + RawConfigRole, + IsClientProtocolExistsRole }; ProtocolsModel(std::shared_ptr settings, QObject *parent = nullptr); @@ -33,7 +35,8 @@ protected: QHash roleNames() const override; private: - PageLoader::PageEnum protocolPage(Proto protocol) const; + PageLoader::PageEnum serverProtocolPage(Proto protocol) const; + PageLoader::PageEnum clientProtocolPage(Proto protocol) const; std::shared_ptr m_settings; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 3f167029..7cde28b4 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -1,8 +1,31 @@ #include "servers_model.h" +#include "core/api/apiDefs.h" #include "core/controllers/serverController.h" #include "core/networkUtilities.h" +#ifdef Q_OS_IOS + #include +#endif + +namespace +{ + namespace configKey + { + constexpr char apiConfig[] = "api_config"; + constexpr char serviceInfo[] = "service_info"; + constexpr char availableCountries[] = "available_countries"; + constexpr char serverCountryCode[] = "server_country_code"; + constexpr char serverCountryName[] = "server_country_name"; + constexpr char userCountryCode[] = "user_country_code"; + constexpr char serviceType[] = "service_type"; + constexpr char serviceProtocol[] = "service_protocol"; + + constexpr char publicKeyInfo[] = "public_key"; + constexpr char expiresAt[] = "expires_at"; + } +} + ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent) { m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); @@ -16,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -56,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -63,6 +95,7 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const } const QJsonObject server = m_servers.at(index.row()).toObject(); + const auto apiConfig = server.value(configKey::apiConfig).toObject(); const auto configVersion = server.value(config_key::configVersion).toInt(); switch (role) { case NameRole: { @@ -98,8 +131,23 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const case HasInstalledContainers: { return serverHasInstalledContainers(index.row()); } - case IsServerFromApiRole: { - return server.value(config_key::configVersion).toInt(); + case IsServerFromTelegramApiRole: { + return server.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::Telegram; + } + case IsServerFromGatewayApiRole: { + return server.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway; + } + case ApiConfigRole: { + return apiConfig; + } + case IsCountrySelectionAvailableRole: { + return !apiConfig.value(configKey::availableCountries).toArray().isEmpty(); + } + case ApiAvailableCountriesRole: { + return apiConfig.value(configKey::availableCountries).toArray(); + } + case ApiServerCountryCodeRole: { + return apiConfig.value(configKey::serverCountryCode).toString(); } case HasAmneziaDns: { QString primaryDns = server.value(config_key::dns1).toString(); @@ -146,10 +194,13 @@ const QString ServersModel::getDefaultServerName() QString ServersModel::getServerDescription(const QJsonObject &server, const int index) const { const auto configVersion = server.value(config_key::configVersion).toInt(); + const auto apiConfig = server.value(configKey::apiConfig).toObject(); QString description; - if (configVersion) { + if (configVersion && !apiConfig.value(configKey::serverCountryCode).toString().isEmpty()) { + return apiConfig.value(configKey::serverCountryName).toString(); + } else if (configVersion) { return server.value(config_key::description).toString(); } else if (data(index, HasWriteAccessRole).toBool()) { if (m_isAmneziaDnsEnabled && isAmneziaDnsContainerInstalled(index)) { @@ -208,6 +259,12 @@ void ServersModel::setProcessedServerIndex(const int index) { m_processedServerIndex = index; updateContainersModel(); + if (data(index, IsServerFromGatewayApiRole).toBool()) { + if (data(index, IsCountrySelectionAvailableRole).toBool()) { + emit updateApiCountryModel(); + } + emit updateApiServicesModel(); + } emit processedServerIndexChanged(m_processedServerIndex); } @@ -233,7 +290,8 @@ bool ServersModel::isDefaultServerCurrentlyProcessed() bool ServersModel::isDefaultServerFromApi() { - return qvariant_cast(data(m_defaultServerIndex, IsServerFromApiRole)); + return data(m_defaultServerIndex, IsServerFromTelegramApiRole).toBool() + || data(m_defaultServerIndex, IsServerFromGatewayApiRole).toBool(); } bool ServersModel::isProcessedServerHasWriteAccess() @@ -315,7 +373,12 @@ QHash ServersModel::roleNames() const roles[DefaultContainerRole] = "defaultContainer"; roles[HasInstalledContainers] = "hasInstalledContainers"; - roles[IsServerFromApiRole] = "isServerFromApi"; + roles[IsServerFromTelegramApiRole] = "isServerFromTelegramApi"; + roles[IsServerFromGatewayApiRole] = "isServerFromGatewayApi"; + roles[ApiConfigRole] = "apiConfig"; + roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable"; + roles[ApiAvailableCountriesRole] = "apiAvailableCountries"; + roles[ApiServerCountryCodeRole] = "apiServerCountryCode"; return roles; } @@ -399,8 +462,7 @@ void ServersModel::addContainerConfig(const int containerIndex, const QJsonObjec auto defaultContainer = server.value(config_key::defaultContainer).toString(); if (ContainerProps::containerFromString(defaultContainer) == DockerContainer::None - && ContainerProps::containerService(container) != ServiceType::Other - && ContainerProps::isSupportedByCurrentPlatform(container)) { + && ContainerProps::containerService(container) != ServiceType::Other && ContainerProps::isSupportedByCurrentPlatform(container)) { server.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); } @@ -565,7 +627,7 @@ void ServersModel::toggleAmneziaDns(bool enabled) bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc) { - for (const auto &server : qAsConst(m_servers)) { + for (const auto &server : std::as_const(m_servers)) { if (static_cast(server.toObject().value(config_key::crc).toInt()) == crc) { return true; } @@ -573,6 +635,19 @@ bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc) return false; } +bool ServersModel::isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol) +{ + for (const auto &server : std::as_const(m_servers)) { + const auto apiConfig = server.toObject().value(configKey::apiConfig).toObject(); + if (apiConfig.value(configKey::userCountryCode).toString() == userCountryCode + && apiConfig.value(configKey::serviceType).toString() == serviceType + && apiConfig.value(configKey::serviceProtocol).toString() == serviceProtocol) { + return true; + } + } + return false; +} + bool ServersModel::serverHasInstalledContainers(const int serverIndex) const { QJsonObject server = m_servers.at(serverIndex).toObject(); @@ -613,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -621,14 +708,89 @@ bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() auto containers = server.value(config_key::containers).toArray(); for (auto i = 0; i < containers.size(); i++) { auto container = containers.at(i).toObject(); + if (container.value(config_key::container).toString() != ContainerProps::containerToString(defaultContainer)) { + continue; + } if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) { - auto containerConfig = container.value(ContainerProps::containerTypeToString(defaultContainer)).toObject(); - return !(containerConfig.value(config_key::last_config).toString().contains("AllowedIPs = 0.0.0.0/0, ::/0")); + QJsonObject serverProtocolConfig = container.value(ContainerProps::containerTypeToString(defaultContainer)).toObject(); + QString clientProtocolConfigString = serverProtocolConfig.value(config_key::last_config).toString(); + QJsonObject clientProtocolConfig = QJsonDocument::fromJson(clientProtocolConfigString.toUtf8()).object(); + return (clientProtocolConfigString.contains("AllowedIPs") && !clientProtocolConfigString.contains("AllowedIPs = 0.0.0.0/0, ::/0")) + || (!clientProtocolConfig.value(config_key::allowed_ips).toArray().isEmpty() + && !clientProtocolConfig.value(config_key::allowed_ips).toArray().contains("0.0.0.0/0")); } else if (defaultContainer == DockerContainer::Cloak || defaultContainer == DockerContainer::OpenVpn || defaultContainer == DockerContainer::ShadowSocks) { - auto containerConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)).toObject(); - return !(containerConfig.value(config_key::last_config).toString().contains("redirect-gateway")); + auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)).toObject(); + QString clientProtocolConfigString = serverProtocolConfig.value(config_key::last_config).toString(); + return !clientProtocolConfigString.isEmpty() && !clientProtocolConfigString.contains("redirect-gateway"); } } return false; } + +bool ServersModel::isServerFromApi(const int serverIndex) +{ + return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool(); +} + +bool ServersModel::isApiKeyExpired(const int serverIndex) +{ + auto serverConfig = m_servers.at(serverIndex).toObject(); + auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + + auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); + serverConfig.insert(configKey::apiConfig, apiConfig); + editServer(serverConfig, serverIndex); + + return false; + } + + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { + return true; + } + return false; +} + +void ServersModel::removeApiConfig(const int serverIndex) +{ + auto serverConfig = getServerConfig(serverIndex); + +#ifdef Q_OS_IOS + QString vpncName = QString("%1 (%2) %3") + .arg(serverConfig[config_key::description].toString()) + .arg(serverConfig[config_key::hostName].toString()) + .arg(serverConfig[config_key::vpnproto].toString()); + + AmneziaVPN::removeVPNC(vpncName.toStdString()); +#endif + + serverConfig.remove(config_key::dns1); + serverConfig.remove(config_key::dns2); + serverConfig.remove(config_key::containers); + serverConfig.remove(config_key::hostName); + + auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); + apiConfig.remove(configKey::publicKeyInfo); + serverConfig.insert(configKey::apiConfig, apiConfig); + + serverConfig.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None)); + + editServer(serverConfig, serverIndex); +} + +const QString ServersModel::getDefaultServerImagePathCollapsed() +{ + const auto server = m_servers.at(m_defaultServerIndex).toObject(); + const auto apiConfig = server.value(configKey::apiConfig).toObject(); + const auto countryCode = apiConfig.value(configKey::serverCountryCode).toString(); + + if (countryCode.isEmpty()) { + return ""; + } + return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode.toUpper()); +} diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index e6b9226b..4b790c7a 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -3,8 +3,8 @@ #include -#include "settings.h" #include "core/controllers/serverController.h" +#include "settings.h" class ServersModel : public QAbstractListModel { @@ -30,7 +30,13 @@ public: DefaultContainerRole, HasInstalledContainers, - IsServerFromApiRole, + + IsServerFromTelegramApiRole, + IsServerFromGatewayApiRole, + ApiConfigRole, + IsCountrySelectionAvailableRole, + ApiAvailableCountriesRole, + ApiServerCountryCodeRole, HasAmneziaDns }; @@ -40,6 +46,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -49,8 +56,10 @@ public: Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged) Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged) Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDefaultContainerChanged) + Q_PROPERTY(QString defaultServerImagePathCollapsed READ getDefaultServerImagePathCollapsed NOTIFY defaultServerDefaultContainerChanged) Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDefaultContainerChanged) - Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerDefaultContainerChanged) + Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY + defaultServerDefaultContainerChanged) Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged) Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) @@ -60,6 +69,7 @@ public slots: const int getDefaultServerIndex(); const QString getDefaultServerName(); const QString getDefaultServerDescriptionCollapsed(); + const QString getDefaultServerImagePathCollapsed(); const QString getDefaultServerDescriptionExpanded(); const QString getDefaultServerDefaultContainerName(); bool isDefaultServerCurrentlyProcessed(); @@ -101,18 +111,27 @@ public slots: QPair getDnsPair(const int serverIndex); bool isServerFromApiAlreadyExists(const quint16 crc); + bool isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol); QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); + bool isServerFromApi(const int serverIndex); + bool isApiKeyExpired(const int serverIndex); + void removeApiConfig(const int serverIndex); + protected: QHash roleNames() const override; signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); @@ -121,6 +140,9 @@ signals: void defaultServerContainersUpdated(const QJsonArray &containers); void defaultServerDefaultContainerChanged(const int containerIndex); + void updateApiCountryModel(); + void updateApiServicesModel(); + private: ServerCredentials serverCredentials(int index) const; diff --git a/client/ui/property_helper.h b/client/ui/property_helper.h deleted file mode 100644 index 927105b3..00000000 --- a/client/ui/property_helper.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef PROPERTY_HELPER_H -#define PROPERTY_HELPER_H - -#include - -#define AUTO_PROPERTY(TYPE, NAME) \ - Q_PROPERTY(TYPE NAME READ NAME WRITE set_ ## NAME NOTIFY NAME ## Changed ) \ - public: \ - TYPE NAME() const { return m_ ## NAME ; } \ - void set_ ## NAME(TYPE value) { \ - if (m_ ## NAME == value) return; \ - m_ ## NAME = value; \ - emit NAME ## Changed(value); \ - } \ - Q_SIGNAL void NAME ## Changed(TYPE value);\ - private: \ - TYPE m_ ## NAME{}; - -#define READONLY_PROPERTY(TYPE, NAME) \ - Q_PROPERTY(TYPE NAME READ NAME CONSTANT ) \ - public: \ - TYPE NAME() const { return m_ ## NAME ; } \ - private: \ - void NAME(TYPE value) {m_ ## NAME = value; } \ - TYPE m_ ## NAME{}; - -#endif // PROPERTY_HELPER_H diff --git a/client/ui/qml/Components/AdLabel.qml b/client/ui/qml/Components/AdLabel.qml new file mode 100644 index 00000000..91e1a42c --- /dev/null +++ b/client/ui/qml/Components/AdLabel.qml @@ -0,0 +1,73 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes +import Qt5Compat.GraphicalEffects + +import Style 1.0 + +import "../Config" +import "../Controls2" +import "../Controls2/TextTypes" + +Rectangle { + id: root + + property real contentHeight: ad.implicitHeight + ad.anchors.topMargin + ad.anchors.bottomMargin + + border.width: 1 + border.color: AmneziaStyle.color.goldenApricot + color: AmneziaStyle.color.transparent + radius: 13 + + visible: false + // visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi + // && ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/premium") + } + } + + RowLayout { + id: ad + anchors.fill: parent + anchors.margins: 16 + + Image { + source: "qrc:/images/controls/amnezia.svg" + sourceSize: Qt.size(36, 36) + + layer { + effect: ColorOverlay { + color: AmneziaStyle.color.paleGray + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.rightMargin: 10 + Layout.leftMargin: 10 + + text: qsTr("Amnezia Premium - for access to all websites and online resources") + color: AmneziaStyle.color.pearlGray + + lineHeight: 18 + font.pixelSize: 15 + } + + ImageButtonType { + image: "qrc:/images/controls/close.svg" + imageColor: AmneziaStyle.color.paleGray + + onClicked: function() { + SettingsController.disableHomeAdLabel() + } + } + } +} diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index 58da7ebf..b90891a0 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -6,14 +6,42 @@ import Qt5Compat.GraphicalEffects import ConnectionState 1.0 import PageEnum 1.0 +import Style 1.0 Button { id: root - property string defaultButtonColor: "#D7D8DB" - property string progressButtonColor: "#D7D8DB" - property string connectedButtonColor: "#FBB26A" + property string defaultButtonColor: AmneziaStyle.color.paleGray + property string progressButtonColor: AmneziaStyle.color.paleGray + property string connectedButtonColor: AmneziaStyle.color.goldenApricot + property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv()) + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + implicitWidth: 190 implicitHeight: 190 @@ -49,14 +77,14 @@ Button { verticalOffset: 0 radius: 10 samples: 25 - color: root.activeFocus ? "#D7D8DB" : "#FBB26A" + color: root.buttonActiveFocus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.goldenApricot source: backgroundCircle } ShapePath { - fillColor: "transparent" - strokeColor: "#D7D8DB" - strokeWidth: root.activeFocus ? 1 : 0 + fillColor: AmneziaStyle.color.transparent + strokeColor: AmneziaStyle.color.paleGray + strokeWidth: root.buttonActiveFocus ? 1 : 0 capStyle: ShapePath.RoundCap PathAngleArc { @@ -70,24 +98,24 @@ Button { } ShapePath { - fillColor: "transparent" + fillColor: AmneziaStyle.color.transparent strokeColor: { if (ConnectionController.isConnectionInProgress) { - return "#261E1A" + return AmneziaStyle.color.darkCharcoal } else if (ConnectionController.isConnected) { return connectedButtonColor } else { return defaultButtonColor } } - strokeWidth: root.activeFocus ? 2 : 3 + strokeWidth: root.buttonActiveFocus ? 2 : 3 capStyle: ShapePath.RoundCap PathAngleArc { centerX: backgroundCircle.width / 2 centerY: backgroundCircle.height / 2 - radiusX: 93 - (root.activeFocus ? 2 : 0) - radiusY: 93 - (root.activeFocus ? 2 : 0) + radiusX: 93 - (root.buttonActiveFocus ? 2 : 0) + radiusY: 93 - (root.buttonActiveFocus ? 2 : 0) startAngle: 0 sweepAngle: 360 } @@ -113,8 +141,8 @@ Button { visible: ConnectionController.isConnectionInProgress ShapePath { - fillColor: "transparent" - strokeColor: "#D7D8DB" + fillColor: AmneziaStyle.color.transparent + strokeColor: AmneziaStyle.color.paleGray strokeWidth: 3 capStyle: ShapePath.RoundCap diff --git a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml index 23fe0d2a..c9124d81 100644 --- a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml +++ b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml @@ -14,7 +14,7 @@ DrawerType2 { width: parent.width height: parent.height - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: content anchors.top: parent.top @@ -26,14 +26,6 @@ DrawerType2 { root.expandedHeight = content.implicitHeight + 32 } - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -44,11 +36,6 @@ DrawerType2 { headerText: qsTr("Add new connection") } - Item { - id: focusItem - KeyNavigation.tab: ip.rightButton - } - LabelWithButtonType { id: ip Layout.fillWidth: true @@ -59,10 +46,8 @@ DrawerType2 { clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardCredentials) - root.close() + root.closeTriggered() } - - KeyNavigation.tab: qrCode.rightButton } DividerType {} @@ -76,10 +61,8 @@ DrawerType2 { clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardConfigSource) - root.close() + root.closeTriggered() } - - KeyNavigation.tab: focusItem } DividerType {} diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index b0e074d0..337918f1 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -17,55 +17,15 @@ ListView { property var rootWidth property var selectedText - property bool a: true - width: rootWidth - height: menuContent.contentItem.height + height: contentItem.height clip: true - interactive: false + snapMode: ListView.SnapToItem - property FlickableType parentFlickable - property var lastItemTabClicked + ScrollBar.vertical: ScrollBarType {} - property int currentFocusIndex: 0 - - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } - } - - Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } else { - currentFocusIndex = 0 - if (lastItemTabClicked && typeof lastItemTabClicked === "function") { - lastItemTabClicked() - } - } - } - - onVisibleChanged: { - if (visible) { - currentFocusIndex = 0 - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - } - - onCurrentFocusIndexChanged: { - if (parentFlickable) { - parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex)) - } - } + property bool isFocusable: true ButtonGroup { id: containersRadioButtonGroup @@ -75,12 +35,6 @@ ListView { implicitWidth: rootWidth implicitHeight: content.implicitHeight - onActiveFocusChanged: { - if (activeFocus) { - containerRadioButton.forceActiveFocus() - } - } - ColumnLayout { id: content @@ -111,13 +65,13 @@ ListView { } if (checked) { - containersDropDown.close() + containersDropDown.closeTriggered() ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index)) } else { ContainersModel.setProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index)) InstallController.setShouldCreateServer(false) PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) - containersDropDown.close() + containersDropDown.closeTriggered() } } diff --git a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml index 29a83334..097274a4 100644 --- a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml +++ b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml @@ -14,9 +14,9 @@ DrawerType2 { property bool isAppSplitTinnelingEnabled: Qt.platform.os === "windows" || Qt.platform.os === "android" anchors.fill: parent - expandedHeight: parent.height * 0.7 + expandedHeight: parent.height * 0.9 - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: content anchors.top: parent.top @@ -24,14 +24,6 @@ DrawerType2 { anchors.right: parent.right spacing: 0 - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -43,32 +35,25 @@ DrawerType2 { descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others") } - Item { - id: focusItem - KeyNavigation.tab: splitTunnelingSwitch.visible ? splitTunnelingSwitch : siteBasedSplitTunnelingSwitch.rightButton - } - LabelWithButtonType { id: splitTunnelingSwitch Layout.fillWidth: true Layout.topMargin: 16 - visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi") + visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling text: qsTr("Split tunneling on the server") descriptionText: qsTr("Enabled \nCan't be disabled for current server") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: siteBasedSplitTunnelingSwitch.visible ? siteBasedSplitTunnelingSwitch.rightButton : focusItem - clickedFunction: function() { -// PageController.goToPage(PageEnum.PageSettingsSplitTunneling) -// root.close() + PageController.goToPage(PageEnum.PageSettingsSplitTunneling) + root.closeTriggered() } } DividerType { - visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi") + visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling } LabelWithButtonType { @@ -80,13 +65,9 @@ DrawerType2 { descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: appSplitTunnelingSwitch.visible ? - appSplitTunnelingSwitch.rightButton : - focusItem - clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) - root.close() + root.closeTriggered() } } @@ -103,11 +84,9 @@ DrawerType2 { descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: focusItem - clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) - root.close() + root.closeTriggered() } } diff --git a/client/ui/qml/Components/InstalledAppsDrawer.qml b/client/ui/qml/Components/InstalledAppsDrawer.qml index 6d6f1feb..ce8ef837 100644 --- a/client/ui/qml/Components/InstalledAppsDrawer.qml +++ b/client/ui/qml/Components/InstalledAppsDrawer.qml @@ -8,6 +8,7 @@ import "../Controls2/TextTypes" import SortFilterProxyModel 0.2 import InstalledAppsModel 1.0 +import Style 1.0 DrawerType2 { id: root @@ -25,7 +26,7 @@ DrawerType2 { id: installedAppsModel } - expandedContent: Item { + expandedStateContent: Item { id: container implicitHeight: expandedHeight @@ -42,7 +43,7 @@ DrawerType2 { BackButtonType { backButtonImage: "qrc:/images/controls/arrow-left.svg" backButtonFunction: function() { - root.close() + root.closeTriggered() } } @@ -68,6 +69,8 @@ DrawerType2 { clip: true interactive: true + property bool isFocusable: true + model: SortFilterProxyModel { id: proxyInstalledAppsModel sourceModel: installedAppsModel @@ -78,10 +81,7 @@ DrawerType2 { } } - ScrollBar.vertical: ScrollBar { - id: scrollBar - policy: ScrollBar.AlwaysOn - } + ScrollBar.vertical: ScrollBarType {} ButtonGroup { id: buttonGroup @@ -133,9 +133,9 @@ DrawerType2 { anchors.rightMargin: 16 anchors.leftMargin: 16 - backgroundColor: "#2C2D30" + backgroundColor: AmneziaStyle.color.slateGray - textFieldPlaceholderText: qsTr("application name") + textField.placeholderText: qsTr("application name") } BasicButtonType { @@ -154,7 +154,7 @@ DrawerType2 { PageController.showBusyIndicator(true) AppSplitTunnelingController.addApps(installedAppsModel.getSelectedAppsInfo()) PageController.showBusyIndicator(false) - root.close() + root.closeTriggered() } } } diff --git a/client/ui/qml/Components/QuestionDrawer.qml b/client/ui/qml/Components/QuestionDrawer.qml index 57e6db83..0c14e52d 100644 --- a/client/ui/qml/Components/QuestionDrawer.qml +++ b/client/ui/qml/Components/QuestionDrawer.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "../Controls2" import "../Controls2/TextTypes" @@ -18,7 +20,7 @@ DrawerType2 { property var yesButtonFunction property var noButtonFunction - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: content anchors.top: parent.top @@ -31,14 +33,6 @@ DrawerType2 { root.expandedHeight = content.implicitHeight + 32 } - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2TextType { Layout.fillWidth: true Layout.topMargin: 16 @@ -57,11 +51,6 @@ DrawerType2 { text: descriptionText } - Item { - id: focusItem - KeyNavigation.tab: yesButton - } - BasicButtonType { id: yesButton Layout.fillWidth: true @@ -76,8 +65,6 @@ DrawerType2 { yesButtonFunction() } } - - KeyNavigation.tab: noButton } BasicButtonType { @@ -86,11 +73,11 @@ DrawerType2 { Layout.rightMargin: 16 Layout.leftMargin: 16 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: noButtonText @@ -100,8 +87,6 @@ DrawerType2 { noButtonFunction() } } - - KeyNavigation.tab: focusItem } } } diff --git a/client/ui/qml/Components/RenameServerDrawer.qml b/client/ui/qml/Components/RenameServerDrawer.qml new file mode 100644 index 00000000..d65b9bba --- /dev/null +++ b/client/ui/qml/Components/RenameServerDrawer.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" + +import "../Config" + +DrawerType2 { + property string serverNameText + + id: root + objectName: "serverNameEditDrawer" + + expandedStateContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + TextFieldWithHeaderType { + id: serverName + + Layout.fillWidth: true + headerText: qsTr("Server name") + textField.text: root.serverNameText + textField.maximumLength: 30 + checkEmptyText: true + } + + BasicButtonType { + id: saveButton + + Layout.fillWidth: true + + text: qsTr("Save") + + clickedFunc: function() { + if (serverName.textField.text === "") { + return + } + + if (serverName.textField.text !== root.serverNameText) { + ServersModel.setProcessedServerData("name", serverName.textField.text); + } + root.closeTriggered() + } + } + } +} diff --git a/client/ui/qml/Components/SelectLanguageDrawer.qml b/client/ui/qml/Components/SelectLanguageDrawer.qml index dcae22d9..2c026848 100644 --- a/client/ui/qml/Components/SelectLanguageDrawer.qml +++ b/client/ui/qml/Components/SelectLanguageDrawer.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "../Controls2" import "../Controls2/TextTypes" import "../Config" @@ -9,7 +11,7 @@ import "../Config" DrawerType2 { id: root - expandedContent: Item { + expandedStateContent: Item { id: container implicitHeight: root.height * 0.9 @@ -18,19 +20,6 @@ DrawerType2 { root.expandedHeight = container.implicitHeight } - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -41,167 +30,148 @@ DrawerType2 { BackButtonType { id: backButton + + Layout.fillWidth: true + backButtonImage: "qrc:/images/controls/arrow-left.svg" - backButtonFunction: function() { root.close() } - KeyNavigation.tab: listView + backButtonFunction: function() { root.closeTriggered() } + } + + Header2Type { + id: header + + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Choose language") } } - FlickableType { + ListView { + id: listView + anchors.top: backButtonLayout.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - contentHeight: content.implicitHeight - ColumnLayout { - id: content + property bool isFocusable: true + property int selectedIndex: LanguageModel.currentLanguageIndex - anchors.fill: parent + clip: true + reuseItems: true - Header2Type { - id: header - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.rightMargin: 16 - Layout.leftMargin: 16 + ScrollBar.vertical: ScrollBarType {} - headerText: qsTr("Choose language") - } + model: LanguageModel - ListView { - id: listView + ButtonGroup { + id: buttonGroup + } - Layout.fillWidth: true - height: listView.contentItem.height + delegate: Item { + implicitWidth: root.width + implicitHeight: delegateContent.implicitHeight - clip: true - interactive: false + ColumnLayout { + id: delegateContent - model: LanguageModel - currentIndex: LanguageModel.currentLanguageIndex + anchors.fill: parent - ButtonGroup { - id: buttonGroup - } + RadioButton { + id: radioButton - property int currentFocusIndex: 0 + implicitWidth: parent.width + implicitHeight: radioButtonContent.implicitHeight - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } - } + hoverEnabled: true - Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } else { - listViewFocusItem.forceActiveFocus() - focusItem.forceActiveFocus() - } - } + property bool isFocusable: true - Item { - id: listViewFocusItem Keys.onTabPressed: { - root.forceActiveFocus() + FocusController.nextKeyTabItem() } - } - onVisibleChanged: { - if (visible) { - listViewFocusItem.forceActiveFocus() - focusItem.forceActiveFocus() + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() } - } - delegate: Item { - implicitWidth: root.width - implicitHeight: delegateContent.implicitHeight + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - onActiveFocusChanged: { - if (activeFocus) { - radioButton.forceActiveFocus() + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + indicator: Rectangle { + width: parent.width - 1 + height: parent.height + color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack + border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent + border.width: radioButton.focus ? 1 : 0 + + Behavior on color { + PropertyAnimation { duration: 200 } + } + Behavior on border.color { + PropertyAnimation { duration: 200 } } } - ColumnLayout { - id: delegateContent - + RowLayout { + id: radioButtonContent anchors.fill: parent - RadioButton { - id: radioButton + anchors.rightMargin: 16 + anchors.leftMargin: 16 - implicitWidth: parent.width - implicitHeight: radioButtonContent.implicitHeight + spacing: 0 - hoverEnabled: true + z: 1 - indicator: Rectangle { - width: parent.width - 1 - height: parent.height - color: radioButton.hovered ? "#2C2D30" : "#1C1D21" - border.color: radioButton.focus ? "#D7D8DB" : "transparent" - border.width: radioButton.focus ? 1 : 0 + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.bottomMargin: 20 - Behavior on color { - PropertyAnimation { duration: 200 } - } - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - } + text: languageName + } - RowLayout { - id: radioButtonContent - anchors.fill: parent + Image { + source: "qrc:/images/controls/check.svg" + visible: radioButton.checked - anchors.rightMargin: 16 - anchors.leftMargin: 16 + width: 24 + height: 24 - spacing: 0 - - z: 1 - - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 20 - Layout.bottomMargin: 20 - - text: languageName - } - - Image { - source: "qrc:/images/controls/check.svg" - visible: radioButton.checked - - width: 24 - height: 24 - - Layout.rightMargin: 8 - } - } - - ButtonGroup.group: buttonGroup - checked: listView.currentIndex === index - - onClicked: { - listView.currentIndex = index - LanguageModel.changeLanguage(languageIndex) - root.close() - } + Layout.rightMargin: 8 } } - Keys.onEnterPressed: radioButton.clicked() - Keys.onReturnPressed: radioButton.clicked() + ButtonGroup.group: buttonGroup + checked: listView.selectedIndex === index + + onClicked: { + listView.selectedIndex = index + LanguageModel.changeLanguage(languageIndex) + root.closeTriggered() + } } } + + Keys.onEnterPressed: radioButton.clicked() + Keys.onReturnPressed: radioButton.clicked() } } } diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml new file mode 100644 index 00000000..d0567a8c --- /dev/null +++ b/client/ui/qml/Components/ServersListView.qml @@ -0,0 +1,143 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ProtocolEnum 1.0 +import ContainerProps 1.0 +import ContainersModelFilters 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" + +ListView { + id: root + + property int selectedIndex: ServersModel.defaultIndex + + anchors.top: serversMenuHeader.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 16 + + model: ServersModel + + ScrollBar.vertical: ScrollBarType {} + + property bool isFocusable: true + + Connections { + target: ServersModel + function onDefaultServerIndexChanged(serverIndex) { + root.selectedIndex = serverIndex + } + } + + clip: true + reuseItems: true + + delegate: Item { + id: menuContentDelegate + objectName: "menuContentDelegate" + + property variant delegateData: model + property VerticalRadioButton serverRadioButtonProperty: serverRadioButton + + implicitWidth: root.width + implicitHeight: serverRadioButtonContent.implicitHeight + + ColumnLayout { + id: serverRadioButtonContent + objectName: "serverRadioButtonContent" + + anchors.fill: parent + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + spacing: 0 + + RowLayout { + objectName: "serverRadioButtonRowLayout" + + Layout.fillWidth: true + + VerticalRadioButton { + id: serverRadioButton + objectName: "serverRadioButton" + + Layout.fillWidth: true + + text: name + descriptionText: serverDescription + + checked: index === root.selectedIndex + checkable: !ConnectionController.isConnected + + ButtonGroup.group: serversRadioButtonGroup + + onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection")) + return + } + + root.selectedIndex = index + + ServersModel.defaultIndex = index + } + + Keys.onEnterPressed: serverRadioButton.clicked() + Keys.onReturnPressed: serverRadioButton.clicked() + } + + ImageButtonType { + id: serverInfoButton + objectName: "serverInfoButton" + + image: "qrc:/images/controls/settings.svg" + imageColor: AmneziaStyle.color.paleGray + + implicitWidth: 56 + implicitHeight: 56 + + z: 1 + + onClicked: function() { + ServersModel.processedIndex = index + + if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { + PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) + } else { + PageController.showBusyIndicator(true) + let result = ApiSettingsController.getAccountInfo(false) + PageController.showBusyIndicator(false) + if (!result) { + return + } + + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } else { + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } + + drawer.closeTriggered() + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 0 + } + } + } +} diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index eda29885..9e672130 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -20,47 +20,14 @@ ListView { height: root.contentItem.height clip: true - interactive: false + reuseItems: true - activeFocusOnTab: true - Keys.onTabPressed: { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - } else { - currentIndex = 0 - lastItemTabClickedSignal() - } - } - - onCurrentIndexChanged: { - if (visible) { - if (fl.contentHeight > fl.height) { - var item = this.currentItem - if (item.y < fl.height) { - fl.contentY = item.y - } else if (item.y + item.height > fl.contentY + fl.height) { - fl.contentY = item.y + item.height - fl.height - } - } - } - } - - onVisibleChanged: { - if (visible) { - this.currentIndex = 0 - } - } + property bool isFocusable: false delegate: Item { implicitWidth: root.width implicitHeight: delegateContent.implicitHeight - onActiveFocusChanged: { - if (activeFocus) { - containerRadioButton.rightButton.forceActiveFocus() - } - } - ColumnLayout { id: delegateContent @@ -93,20 +60,11 @@ ListView { PageController.goToPage(PageEnum.PageProtocolRaw) break } - case ContainerEnum.Sftp: { - SftpConfigModel.updateModel(config) - PageController.goToPage(PageEnum.PageServiceSftpSettings) - break - } - case ContainerEnum.TorWebSite: { - PageController.goToPage(PageEnum.PageServiceTorWebsiteSettings) - break - } case ContainerEnum.Dns: { PageController.goToPage(PageEnum.PageServiceDnsSettings) break } - default: { // go to the settings page of the container with multiple protocols + default: { ProtocolsModel.updateModel(config) PageController.goToPage(PageEnum.PageSettingsServerProtocol) } diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index edd5d42e..dd59180b 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -9,6 +9,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -21,7 +22,9 @@ DrawerType2 { property string headerText property string configContentHeaderText - property string contentVisible + property string shareButtonText: qsTr("Share") + property string copyButtonText: qsTr("Copy") + property bool isSelfHostedConfig: true property string configExtension: ".vpn" property string configCaption: qsTr("Save AmneziaVPN config") @@ -35,17 +38,9 @@ DrawerType2 { configFileName = "amnezia_config" } - expandedContent: Item { + expandedStateContent: Item { implicitHeight: root.expandedHeight - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - header.forceActiveFocus() - } - } - Header2Type { id: header anchors.top: parent.top @@ -56,36 +51,37 @@ DrawerType2 { anchors.rightMargin: 16 headerText: root.headerText - - KeyNavigation.tab: shareButton } - FlickableType { + ListView { + id: listView + anchors.top: header.bottom anchors.bottom: parent.bottom - contentHeight: content.height + 32 + anchors.left: parent.left + anchors.right: parent.right - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ScrollBar.vertical: ScrollBarType {} - anchors.leftMargin: 16 - anchors.rightMargin: 16 + model: 1 - visible: root.contentVisible + clip: true + reuseItems: true + + header: ColumnLayout { + width: listView.width BasicButtonType { id: shareButton Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" - - KeyNavigation.tab: copyConfigTextButton + text: root.shareButtonText + leftImageSource: "qrc:/images/controls/share-2.svg" clickedFunc: function() { var fileName = "" @@ -110,39 +106,41 @@ DrawerType2 { id: copyConfigTextButton Layout.fillWidth: true Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 - text: qsTr("Copy") - imageSource: "qrc:/images/controls/copy.svg" + text: root.copyButtonText + leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } Keys.onEnterPressed: { copyConfigTextButton.clicked() } - - KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton } BasicButtonType { id: copyNativeConfigStringButton Layout.fillWidth: true Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 visible: false - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Copy config string") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" KeyNavigation.tab: showSettingsButton } @@ -152,21 +150,23 @@ DrawerType2 { Layout.fillWidth: true Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + visible: root.isSelfHostedConfig + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Show connection settings") clickedFunc: function() { - configContentDrawer.open() + configContentDrawer.openTriggered() } - - KeyNavigation.tab: header } DrawerType2 { @@ -177,30 +177,11 @@ DrawerType2 { anchors.fill: parent expandedHeight: parent.height * 0.9 - onClosed: { - if (!GC.isMobile()) { - header.forceActiveFocus() - } - } - - expandedContent: Item { + expandedStateContent: Item { id: configContentContainer implicitHeight: configContentDrawer.expandedHeight - Connections { - target: configContentDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - Connections { target: copyNativeConfigStringButton function onClicked() { @@ -230,9 +211,7 @@ DrawerType2 { anchors.right: parent.right anchors.topMargin: 16 - backButtonFunction: function() { configContentDrawer.close() } - - KeyNavigation.tab: focusItem + backButtonFunction: function() { configContentDrawer.closeTriggered() } } FlickableType { @@ -281,9 +260,9 @@ DrawerType2 { readOnly: true activeFocusOnTab: false - color: "#D7D8DB" - selectionColor: "#633303" - selectedTextColor: "#D7D8DB" + color: AmneziaStyle.color.paleGray + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray font.pixelSize: 16 font.weight: Font.Medium @@ -294,13 +273,19 @@ DrawerType2 { wrapMode: Text.Wrap background: Rectangle { - color: "transparent" + color: AmneziaStyle.color.transparent } } } } } } + } + + delegate: ColumnLayout { + width: listView.width + + property bool isQrCodeVisible: root.isSelfHostedConfig ? ExportController.qrCodesCount > 0 : ApiConfigsController.qrCodesCount > 0 Rectangle { id: qrCodeContainer @@ -308,8 +293,10 @@ DrawerType2 { Layout.fillWidth: true Layout.preferredHeight: width Layout.topMargin: 20 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - visible: ExportController.qrCodesCount > 0 + visible: isQrCodeVisible color: "white" @@ -317,20 +304,49 @@ DrawerType2 { anchors.fill: parent smooth: false - source: ExportController.qrCodesCount ? ExportController.qrCodes[0] : "" + source: root.isSelfHostedConfig ? (isQrCodeVisible ? ExportController.qrCodes[0] : "") : + (isQrCodeVisible ? ApiConfigsController.qrCodes[0] : "") + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } Timer { property int index: 0 interval: 1000 - running: ExportController.qrCodesCount > 0 + running: isQrCodeVisible repeat: true onTriggered: { - if (ExportController.qrCodesCount > 0) { + if (isQrCodeVisible) { index++ - if (index >= ExportController.qrCodesCount) { + let qrCodesCount = root.isSelfHostedConfig ? ExportController.qrCodesCount : ApiConfigsController.qrCodesCount + if (index >= qrCodesCount) { index = 0 } - parent.source = ExportController.qrCodes[index] + + parent.source = root.isSelfHostedConfig ? ExportController.qrCodes[index] : ApiConfigsController.qrCodes[index] } } } @@ -345,8 +361,10 @@ DrawerType2 { Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - visible: ExportController.qrCodesCount > 0 + visible: isQrCodeVisible horizontalAlignment: Text.AlignHCenter text: qsTr("To read the QR code in the Amnezia app, select \"Add server\" → \"I have data to connect\" → \"QR code, key or settings file\"") diff --git a/client/ui/qml/Components/TransportProtoSelector.qml b/client/ui/qml/Components/TransportProtoSelector.qml index 12e48635..323892fa 100644 --- a/client/ui/qml/Components/TransportProtoSelector.qml +++ b/client/ui/qml/Components/TransportProtoSelector.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "../Controls2" import "../Controls2/TextTypes" @@ -14,7 +16,7 @@ Rectangle { implicitWidth: transportProtoButtonGroup.implicitWidth implicitHeight: transportProtoButtonGroup.implicitHeight - color: "#1C1D21" + color: AmneziaStyle.color.onyxBlack radius: 16 onFocusChanged: { @@ -37,8 +39,6 @@ Rectangle { implicitWidth: (rootWidth - 32) / 2 text: "UDP" - KeyNavigation.tab: tcpButton - onClicked: { root.currentIndex = 0 } diff --git a/client/ui/qml/Controls2/BackButtonType.qml b/client/ui/qml/Controls2/BackButtonType.qml index 42ab8340..40136ad5 100644 --- a/client/ui/qml/Controls2/BackButtonType.qml +++ b/client/ui/qml/Controls2/BackButtonType.qml @@ -2,7 +2,9 @@ import QtQuick import QtQuick.Layouts import Qt5Compat.GraphicalEffects -Item { +import Style 1.0 + +FocusScope { id: root property string backButtonImage: "qrc:/images/controls/arrow-left.svg" @@ -13,12 +15,6 @@ Item { visible: backButtonImage !== "" - onActiveFocusChanged: { - if (activeFocus) { - backButton.forceActiveFocus() - } - } - RowLayout { id: content @@ -28,7 +24,7 @@ Item { ImageButtonType { id: backButton image: backButtonImage - imageColor: "#D7D8DB" + imageColor: AmneziaStyle.color.paleGray implicitWidth: 40 implicitHeight: 40 @@ -46,7 +42,7 @@ Item { id: background Layout.fillWidth: true - color: "transparent" + color: AmneziaStyle.color.transparent } } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 646ccd57..b60e96cf 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -3,26 +3,29 @@ import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects +import Style 1.0 + import "TextTypes" Button { id: root - property string hoveredColor: "#C1C2C5" - property string defaultColor: "#D7D8DB" - property string disabledColor: "#494B50" - property string pressedColor: "#979799" + property string hoveredColor: AmneziaStyle.color.lightGray + property string defaultColor: AmneziaStyle.color.paleGray + property string disabledColor: AmneziaStyle.color.charcoalGray + property string pressedColor: AmneziaStyle.color.mutedGray - property string textColor: "#0E0E11" + property string textColor: AmneziaStyle.color.midnightBlack - property string borderColor: "#D7D8DB" - property string borderFocusedColor: "#D7D8DB" + property string borderColor: AmneziaStyle.color.paleGray + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderWidth: 0 property int borderFocusedWidth: 1 - property string imageSource + property string leftImageSource property string rightImageSource property string leftImageColor: textColor + property bool changeLeftImageSize: true property bool squareLeftSide: false @@ -30,10 +33,37 @@ Button { property var clickedFunc + property alias buttonTextLabel: buttonText + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + implicitHeight: 56 hoverEnabled: true - focusPolicy: Qt.TabFocus onFocusChanged: { if (root.activeFocus) { @@ -46,8 +76,8 @@ Button { background: Rectangle { id: focusBorder - color: "transparent" - border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + color: AmneziaStyle.color.transparent + border.color: root.activeFocus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.activeFocus ? root.borderFocusedWidth : 0 anchors.fill: parent @@ -123,22 +153,29 @@ Button { anchors.centerIn: parent Image { - Layout.preferredHeight: 20 - Layout.preferredWidth: 20 - - source: root.imageSource - visible: root.imageSource === "" ? false : true + id: leftImage + source: root.leftImageSource + visible: root.leftImageSource === "" ? false : true layer { - enabled: true + enabled: leftImageColor !== "" ? true : false effect: ColorOverlay { color: leftImageColor } } + + Component.onCompleted: { + if (root.changeLeftImageSize) { + leftImage.Layout.preferredHeight = 20 + leftImage.Layout.preferredWidth = 20 + } + } } ButtonTextType { - color: textColor + id: buttonText + + color: root.textColor text: root.text visible: root.text === "" ? false : true diff --git a/client/ui/qml/Controls2/BusyIndicatorType.qml b/client/ui/qml/Controls2/BusyIndicatorType.qml index 7e92998c..480f25c1 100644 --- a/client/ui/qml/Controls2/BusyIndicatorType.qml +++ b/client/ui/qml/Controls2/BusyIndicatorType.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Shapes +import Style 1.0 + Popup { id: root anchors.centerIn: parent @@ -12,11 +14,11 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { - color: "transparent" + color: AmneziaStyle.color.transparent } BusyIndicator { @@ -40,8 +42,8 @@ Popup { layer.samples: 4 ShapePath { - fillColor: "transparent" - strokeColor: "#787878" + fillColor: AmneziaStyle.color.transparent + strokeColor: AmneziaStyle.color.charcoalGray strokeWidth: 3 capStyle: ShapePath.RoundCap diff --git a/client/ui/qml/Controls2/CardType.qml b/client/ui/qml/Controls2/CardType.qml index 32f89122..8e689541 100644 --- a/client/ui/qml/Controls2/CardType.qml +++ b/client/ui/qml/Controls2/CardType.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + RadioButton { id: root @@ -9,17 +11,18 @@ RadioButton { property string bodyText property string footerText - property string hoveredColor: Qt.rgba(1, 1, 1, 0.05) - property string defaultColor: Qt.rgba(1, 1, 1, 0) - property string disabledColor: Qt.rgba(1, 1, 1, 0) - property string pressedColor: Qt.rgba(1, 1, 1, 0.05) - property string selectedColor: Qt.rgba(1, 1, 1, 0) + property string hoveredColor: AmneziaStyle.color.barelyTranslucentWhite + property string defaultColor: AmneziaStyle.color.transparent + property string disabledColor: AmneziaStyle.color.transparent + property string pressedColor: AmneziaStyle.color.barelyTranslucentWhite + property string selectedColor: AmneziaStyle.color.transparent - property string textColor: "#0E0E11" + property string textColor: AmneziaStyle.color.midnightBlack - property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3) - property string selectedBorderColor: "#FBB26A" - property string defaultBodredColor: "transparent" + property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot + property string selectedBorderColor: AmneziaStyle.color.goldenApricot + property string defaultBodredColor: AmneziaStyle.color.transparent + property string focusBorderColor: AmneziaStyle.color.paleGray property int borderWidth: 0 implicitWidth: content.implicitWidth @@ -27,6 +30,32 @@ RadioButton { hoverEnabled: true + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + indicator: Rectangle { anchors.fill: parent radius: 16 @@ -50,6 +79,8 @@ RadioButton { return pressedBorderColor } else if (root.checked) { return selectedBorderColor + } else if (root.activeFocus) { + return focusBorderColor } } return defaultBodredColor @@ -57,7 +88,7 @@ RadioButton { border.width: { if (root.enabled) { - if(root.checked) { + if(root.checked || root.activeFocus) { return 1 } return root.pressed ? 1 : 0 @@ -82,7 +113,7 @@ RadioButton { Text { text: root.headerText wrapMode: Text.WordWrap - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 25 font.weight: 700 font.family: "PT Root UI VF" @@ -97,7 +128,7 @@ RadioButton { Text { text: root.bodyText wrapMode: Text.WordWrap - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 16 font.weight: 400 font.family: "PT Root UI VF" @@ -113,7 +144,7 @@ RadioButton { text: root.footerText wrapMode: Text.WordWrap visible: root.footerText !== "" - color: "#878B91" + color: AmneziaStyle.color.mutedGray font.pixelSize: 13 font.weight: 400 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml new file mode 100644 index 00000000..4277d735 --- /dev/null +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -0,0 +1,203 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "TextTypes" + +Button { + id: root + + property string headerText + property string bodyText + property string footerText + + property string hoveredColor: AmneziaStyle.color.slateGray + property string defaultColor: AmneziaStyle.color.onyxBlack + + property string textColor: AmneziaStyle.color.midnightBlack + + property string rightImageSource + property string rightImageColor: AmneziaStyle.color.paleGray + + property string leftImageSource + + property real textOpacity: 1.0 + + property alias focusItem: rightImage + + property FlickableType parentFlickable + + hoverEnabled: true + + background: Rectangle { + id: backgroundRect + + anchors.fill: parent + radius: 16 + + color: defaultColor + + Behavior on color { + PropertyAnimation { duration: 200 } + } + } + + function ensureVisible(item) { + if (item.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + + onFocusChanged: { + ensureVisible(root) + } + + focusItem.onFocusChanged: { + root.ensureVisible(focusItem) + } + + contentItem: Item { + anchors.left: parent.left + anchors.right: parent.right + + implicitHeight: content.implicitHeight + + RowLayout { + id: content + + anchors.fill: parent + + Image { + id: leftImage + source: leftImageSource + + visible: leftImageSource !== "" + + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.topMargin: 24 + Layout.bottomMargin: 24 + Layout.leftMargin: 24 + } + + ColumnLayout { + + ListItemTitleType { + text: root.headerText + visible: text !== "" + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: root.bodyText !== "" ? 0 : 16 + + opacity: root.textOpacity + } + + CaptionTextType { + text: root.bodyText + visible: text !== "" + + color: AmneziaStyle.color.mutedGray + textFormat: Text.RichText + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: root.footerText !== "" ? 0 : 16 + + opacity: root.textOpacity + } + + ButtonTextType { + text: root.footerText + visible: text !== "" + + color: AmneziaStyle.color.mutedGray + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 16 + + opacity: root.textOpacity + } + } + + ImageButtonType { + id: rightImage + + implicitWidth: 40 + implicitHeight: 40 + + hoverEnabled: false + image: rightImageSource + imageColor: rightImageColor + visible: rightImageSource ? true : false + + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.topMargin: 16 + Layout.bottomMargin: 16 + Layout.rightMargin: 16 + + Rectangle { + id: rightImageBackground + + anchors.fill: parent + radius: 12 + color: "transparent" + + Behavior on color { + PropertyAnimation { duration: 200 } + } + } + + onClicked: { + root.clicked() + } + } + } + } + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + enabled: root.enabled + + onEntered: { + backgroundRect.color = root.hoveredColor + + if (rightImageSource) { + rightImageBackground.color = rightImage.hoveredColor + } + root.textOpacity = 0.8 + } + + onExited: { + backgroundRect.color = root.defaultColor + + if (rightImageSource) { + rightImageBackground.color = rightImage.defaultColor + } + root.textOpacity = 1 + } + + onPressedChanged: { + if (rightImageSource) { + rightImageBackground.color = pressed ? rightImage.pressedColor : entered ? rightImage.hoveredColor : rightImage.defaultColor + } + root.textOpacity = 0.7 + } + + onClicked: { + root.clicked() + } + } +} diff --git a/client/ui/qml/Controls2/CheckBoxType.qml b/client/ui/qml/Controls2/CheckBoxType.qml index ac77e900..a26a68f1 100644 --- a/client/ui/qml/Controls2/CheckBoxType.qml +++ b/client/ui/qml/Controls2/CheckBoxType.qml @@ -3,32 +3,34 @@ import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects +import Style 1.0 + import "TextTypes" CheckBox { id: root property string descriptionText - property string descriptionTextColor: "#878B91" - property string descriptionTextDisabledColor: "#494B50" + property string descriptionTextColor: AmneziaStyle.color.mutedGray + property string descriptionTextDisabledColor: AmneziaStyle.color.charcoalGray - property string textColor: "#D7D8DB" - property string textDisabledColor: "#878B91" + property string textColor: AmneziaStyle.color.paleGray + property string textDisabledColor: AmneziaStyle.color.mutedGray - property string hoveredColor: Qt.rgba(1, 1, 1, 0.05) - property string defaultColor: "transparent" - property string pressedColor: Qt.rgba(1, 1, 1, 0.05) + property string hoveredColor: AmneziaStyle.color.barelyTranslucentWhite + property string defaultColor: AmneziaStyle.color.transparent + property string pressedColor: AmneziaStyle.color.barelyTranslucentWhite - property string defaultBorderColor: "#D7D8DB" - property string checkedBorderColor: "#FBB26A" - property string checkedBorderDisabledColor: "#402102" + property string defaultBorderColor: AmneziaStyle.color.paleGray + property string checkedBorderColor: AmneziaStyle.color.goldenApricot + property string checkedBorderDisabledColor: AmneziaStyle.color.deepBrown - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray - property string checkedImageColor: "#FBB26A" - property string pressedImageColor: "#A85809" - property string defaultImageColor: "transparent" - property string checkedDisabledImageColor: "#84603D" + property string checkedImageColor: AmneziaStyle.color.goldenApricot + property string pressedImageColor: AmneziaStyle.color.burntOrange + property string defaultImageColor: AmneziaStyle.color.transparent + property string checkedDisabledImageColor: AmneziaStyle.color.mutedBrown property string imageSource: "qrc:/images/controls/check.svg" @@ -45,8 +47,8 @@ CheckBox { focusPolicy: Qt.NoFocus background: Rectangle { - color: "transparent" - border.color: root.focus ? borderFocusedColor : "transparent" + color: AmneziaStyle.color.transparent + border.color: root.focus ? borderFocusedColor : AmneziaStyle.color.transparent border.width: 1 radius: 16 } @@ -77,7 +79,7 @@ CheckBox { anchors.centerIn: parent width: 24 height: 24 - color: "transparent" + color: AmneziaStyle.color.transparent border.color: root.checked ? (root.enabled ? checkedBorderColor : diff --git a/client/ui/qml/Controls2/ContextMenuType.qml b/client/ui/qml/Controls2/ContextMenuType.qml index 867fcb10..fd341e6d 100644 --- a/client/ui/qml/Controls2/ContextMenuType.qml +++ b/client/ui/qml/Controls2/ContextMenuType.qml @@ -20,7 +20,8 @@ Menu { MenuItem { text: qsTr("&Paste") shortcut: StandardKey.Paste - enabled: textObj.canPaste + // Fix calling paste from clipboard when launching app on android + enabled: Qt.platform.os === "android" ? true : textObj.canPaste onTriggered: textObj.paste() } diff --git a/client/ui/qml/Controls2/DividerType.qml b/client/ui/qml/Controls2/DividerType.qml index bf01e7a1..2eb535ec 100644 --- a/client/ui/qml/Controls2/DividerType.qml +++ b/client/ui/qml/Controls2/DividerType.qml @@ -1,6 +1,8 @@ import QtQuick import QtQuick.Layouts +import Style 1.0 + Rectangle { Layout.fillWidth: true @@ -8,5 +10,5 @@ Rectangle { Layout.rightMargin: 16 height: 1 - color: "#2C2D30" + color: AmneziaStyle.color.slateGray } diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index 3a5d981c..e67e36a1 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -2,54 +2,65 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" Item { id: root - readonly property string drawerExpanded: "expanded" - readonly property string drawerCollapsed: "collapsed" + readonly property string drawerExpandedStateName: "expanded" + readonly property string drawerCollapsedStateName: "collapsed" - readonly property bool isOpened: drawerContent.state === root.drawerExpanded || (drawerContent.state === root.drawerCollapsed && dragArea.drag.active === true) - readonly property bool isClosed: drawerContent.state === root.drawerCollapsed && dragArea.drag.active === false + readonly property bool isOpened: isExpandedStateActive() || (isCollapsedStateActive() && (dragArea.drag.active === true)) + readonly property bool isClosed: isCollapsedStateActive() && (dragArea.drag.active === false) - readonly property bool isExpanded: drawerContent.state === root.drawerExpanded - readonly property bool isCollapsed: drawerContent.state === root.drawerCollapsed + property Component collapsedStateContent + property Component expandedStateContent - property Component collapsedContent - property Component expandedContent - - property string defaultColor: "#1C1D21" - property string borderColor: "#2C2D30" + property string defaultColor: AmneziaStyle.color.onyxBlack + property string borderColor: AmneziaStyle.color.slateGray property real expandedHeight property real collapsedHeight: 0 property int depthIndex: 0 - signal entered - signal exited + signal cursorEntered + signal cursorExited signal pressed(bool pressed, bool entered) signal aboutToHide signal aboutToShow - signal close - signal open + signal closeTriggered + signal openTriggered signal closed signal opened + function isExpandedStateActive() { + return isStateActive(drawerExpandedStateName) + } + + function isCollapsedStateActive() { + return isStateActive(drawerCollapsedStateName) + } + + function isStateActive(stateName) { + return drawerContent.state === stateName + } + Connections { target: PageController function onCloseTopDrawer() { if (depthIndex === PageController.getDrawerDepth()) { - if (isCollapsed) { + if (isCollapsedStateActive()) { return } aboutToHide() - drawerContent.state = root.drawerCollapsed + drawerContent.state = root.drawerCollapsedStateName depthIndex = 0 closed() } @@ -59,30 +70,52 @@ Item { Connections { target: root - function onClose() { - if (isCollapsed) { + function onCloseTriggered() { + if (isCollapsedStateActive()) { return } aboutToHide() - drawerContent.state = root.drawerCollapsed - depthIndex = 0 - PageController.setDrawerDepth(PageController.getDrawerDepth() - 1) closed() } - function onOpen() { - if (isExpanded) { + function onClosed() { + drawerContent.state = root.drawerCollapsedStateName + + if (root.isCollapsedStateActive()) { + var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor() + if (initialPageNavigationBarColor !== 0xFF1C1D21) { + PageController.updateNavigationBarColor(initialPageNavigationBarColor) + } + } + + depthIndex = 0 + PageController.decrementDrawerDepth() + FocusController.dropRootObject(root) + } + + function onOpenTriggered() { + if (root.isExpandedStateActive()) { return } - aboutToShow() + root.aboutToShow() - drawerContent.state = root.drawerExpanded - depthIndex = PageController.getDrawerDepth() + 1 - PageController.setDrawerDepth(depthIndex) - opened() + root.opened() + } + + function onOpened() { + drawerContent.state = root.drawerExpandedStateName + + if (isExpandedStateActive()) { + if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) { + PageController.updateNavigationBarColor(0xFF1C1D21) + } + } + + depthIndex = PageController.incrementDrawerDepth() + FocusController.pushRootObject(root) } } @@ -90,7 +123,7 @@ Item { id: background anchors.fill: parent - color: root.isCollapsed ? "transparent" : Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: root.isCollapsedStateActive() ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack Behavior on color { PropertyAnimation { duration: 200 } @@ -100,18 +133,17 @@ Item { MouseArea { id: emptyArea anchors.fill: parent - enabled: root.isExpanded - visible: enabled + onClicked: { - root.close() + root.closeTriggered() } } MouseArea { id: dragArea + objectName: "dragArea" anchors.fill: drawerContentBackground - cursorShape: root.isCollapsed ? Qt.PointingHandCursor : Qt.ArrowCursor hoverEnabled: true enabled: drawerContent.implicitHeight > 0 @@ -123,35 +155,36 @@ Item { /** If drag area is released at any point other than min or max y, transition to the other state */ onReleased: { - if (root.isCollapsed && drawerContent.y < dragArea.drag.maximumY) { - root.open() + if (isCollapsedStateActive() && drawerContent.y < dragArea.drag.maximumY) { + root.openTriggered() return } - if (root.isExpanded && drawerContent.y > dragArea.drag.minimumY) { - root.close() + if (isExpandedStateActive() && drawerContent.y > dragArea.drag.minimumY) { + root.closeTriggered() return } } onEntered: { - root.entered() + root.cursorEntered() } onExited: { - root.exited() + root.cursorExited() } onPressedChanged: { root.pressed(pressed, entered) } onClicked: { - if (root.isCollapsed) { - root.open() + if (isCollapsedStateActive()) { + root.openTriggered() } } } Rectangle { id: drawerContentBackground + objectName: "drawerContentBackground" anchors { left: drawerContent.left; right: drawerContent.right; top: drawerContent.top } height: root.height @@ -172,53 +205,80 @@ Item { Item { id: drawerContent + objectName: "drawerContent" Drag.active: dragArea.drag.active anchors.right: root.right anchors.left: root.left - y: root.height - drawerContent.height - state: root.drawerCollapsed - implicitHeight: root.isCollapsed ? collapsedHeight : expandedHeight - - onStateChanged: { - if (root.isCollapsed) { - var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor() - if (initialPageNavigationBarColor !== 0xFF1C1D21) { - PageController.updateNavigationBarColor(initialPageNavigationBarColor) - } - return - } - if (root.isExpanded) { - if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) { - PageController.updateNavigationBarColor(0xFF1C1D21) - } - return - } - } + state: root.drawerCollapsedStateName states: [ State { - name: root.drawerCollapsed + name: root.drawerCollapsedStateName PropertyChanges { target: drawerContent + implicitHeight: collapsedHeight y: root.height - root.collapsedHeight } + PropertyChanges { + target: background + color: AmneziaStyle.color.transparent + } + PropertyChanges { + target: dragArea + cursorShape: Qt.PointingHandCursor + } + PropertyChanges { + target: emptyArea + enabled: false + visible: false + } + PropertyChanges { + target: collapsedLoader + // visible: true + } + PropertyChanges { + target: expandedLoader + visible: false + + } }, State { - name: root.drawerExpanded + name: root.drawerExpandedStateName PropertyChanges { target: drawerContent + implicitHeight: expandedHeight y: dragArea.drag.minimumY - + } + PropertyChanges { + target: background + color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + } + PropertyChanges { + target: dragArea + cursorShape: Qt.ArrowCursor + } + PropertyChanges { + target: emptyArea + enabled: true + visible: true + } + PropertyChanges { + target: collapsedLoader + // visible: false + } + PropertyChanges { + target: expandedLoader + visible: true } } ] transitions: [ Transition { - from: root.drawerCollapsed - to: root.drawerExpanded + from: root.drawerCollapsedStateName + to: root.drawerExpandedStateName PropertyAnimation { target: drawerContent properties: "y" @@ -226,8 +286,8 @@ Item { } }, Transition { - from: root.drawerExpanded - to: root.drawerCollapsed + from: root.drawerExpandedStateName + to: root.drawerCollapsedStateName PropertyAnimation { target: drawerContent properties: "y" @@ -239,7 +299,7 @@ Item { Loader { id: collapsedLoader - sourceComponent: root.collapsedContent + sourceComponent: root.collapsedStateContent anchors.right: parent.right anchors.left: parent.left @@ -248,8 +308,7 @@ Item { Loader { id: expandedLoader - visible: root.isExpanded - sourceComponent: root.expandedContent + sourceComponent: root.expandedStateContent anchors.right: parent.right anchors.left: parent.left diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index c1dc1124..ae6dac85 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" import "../Config" @@ -9,31 +11,31 @@ Item { id: root property string text - property string textColor: "#d7d8db" - property string textDisabledColor: "#878B91" + property string textColor: AmneziaStyle.color.paleGray + property string textDisabledColor: AmneziaStyle.color.mutedGray property int textMaximumLineCount: 2 property int textElide: Qt.ElideRight property string descriptionText - property string descriptionTextColor: "#878B91" - property string descriptionTextDisabledColor: "#494B50" + property string descriptionTextColor: AmneziaStyle.color.mutedGray + property string descriptionTextDisabledColor: AmneziaStyle.color.charcoalGray property string headerText property string headerBackButtonImage property var rootButtonClickedFunction property string rootButtonImage: "qrc:/images/controls/chevron-down.svg" - property string rootButtonImageColor: "#D7D8DB" - property string rootButtonBackgroundColor: "#1C1D21" - property string rootButtonBackgroundHoveredColor: "#1C1D21" - property string rootButtonBackgroundPressedColor: "#1C1D21" + property string rootButtonImageColor: AmneziaStyle.color.paleGray + property string rootButtonBackgroundColor: AmneziaStyle.color.onyxBlack + property string rootButtonBackgroundHoveredColor: AmneziaStyle.color.onyxBlack + property string rootButtonBackgroundPressedColor: AmneziaStyle.color.onyxBlack - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 - property string rootButtonHoveredBorderColor: "#494B50" - property string rootButtonDefaultBorderColor: "#2C2D30" - property string rootButtonPressedBorderColor: "#D7D8DB" + property string rootButtonHoveredBorderColor: AmneziaStyle.color.charcoalGray + property string rootButtonDefaultBorderColor: AmneziaStyle.color.slateGray + property string rootButtonPressedBorderColor: AmneziaStyle.color.paleGray property int rootButtonTextLeftMargins: 16 property int rootButtonTextTopMargin: 16 @@ -43,40 +45,63 @@ Item { property Item drawerParent property Component listView - signal open - signal close + signal openTriggered + signal closeTriggered - function popupClosedFunc() { - if (!GC.isMobile()) { - this.forceActiveFocus() - } + readonly property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() } - property var parentFlickable - onFocusChanged: { - if (root.activeFocus) { - if (root.parentFlickable) { - root.parentFlickable.ensureVisible(root) - } - } + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() } implicitWidth: rootButtonContent.implicitWidth implicitHeight: rootButtonContent.implicitHeight - onOpen: { - menu.open() + onOpenTriggered: { + menu.openTriggered() } - onClose: { - menu.close() + onCloseTriggered: { + menu.closeTriggered() + } + + Keys.onEnterPressed: { + if (menu.isClosed) { + menu.openTriggered() + } + } + + Keys.onReturnPressed: { + if (menu.isClosed) { + menu.openTriggered() + } } Rectangle { id: focusBorder - color: "transparent" - border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + color: AmneziaStyle.color.transparent + border.color: root.activeFocus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.activeFocus ? root.borderFocusedWidth : 0 anchors.fill: rootButtonContent radius: 16 @@ -96,7 +121,7 @@ Item { } return root.hovered ? root.rootButtonBackgroundHoveredColor : root.rootButtonBackgroundColor } else { - return "transparent" + return AmneziaStyle.color.transparent } } @@ -171,7 +196,7 @@ Item { if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") { rootButtonClickedFunction() } else { - menu.open() + menu.openTriggered() } } } @@ -184,93 +209,38 @@ Item { anchors.fill: parent expandedHeight: drawerParent.height * drawerHeight - onClosed: { - root.popupClosedFunc() - } - - expandedContent: Item { + expandedStateContent: Item { id: container implicitHeight: menu.expandedHeight - Connections { - target: menu - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + anchors.fill: parent anchors.topMargin: 16 BackButtonType { id: backButton backButtonImage: root.headerBackButtonImage - backButtonFunction: function() { menu.close() } - KeyNavigation.tab: listViewLoader.item + backButtonFunction: function() { menu.closeTriggered() } + } + + Header2Type { + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + Layout.fillWidth: true + + headerText: root.headerText + } + + Loader { + id: listViewLoader + sourceComponent: root.listView + + Layout.fillHeight: true } } - - FlickableType { - id: flickable - anchors.top: header.bottom - anchors.topMargin: 16 - contentHeight: col.implicitHeight - - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - spacing: 16 - - Header2Type { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - headerText: root.headerText - - width: parent.width - } - - Loader { - id: listViewLoader - sourceComponent: root.listView - - onLoaded: { - listViewLoader.item.parentFlickable = flickable - listViewLoader.item.lastItemTabClicked = function() { - focusItem.forceActiveFocus() - } - } - } - } - } - } - } - - Keys.onEnterPressed: { - if (menu.isClosed) { - menu.open() - } - } - - Keys.onReturnPressed: { - if (menu.isClosed) { - menu.open() } } } diff --git a/client/ui/qml/Controls2/FlickableType.qml b/client/ui/qml/Controls2/FlickableType.qml index bcd14487..b3e5cabf 100644 --- a/client/ui/qml/Controls2/FlickableType.qml +++ b/client/ui/qml/Controls2/FlickableType.qml @@ -7,10 +7,11 @@ Flickable { function ensureVisible(item) { if (item.y < fl.contentY) { - fl.contentY = item.y + fl.contentY = item.y - 40 // 40 is a top margin } else if (item.y + item.height > fl.contentY + fl.height) { fl.contentY = item.y + item.height - fl.height + 40 // 40 is a bottom margin } + fl.returnToBounds() } clip: true @@ -24,7 +25,7 @@ Flickable { Keys.onUpPressed: scrollBar.decrease() Keys.onDownPressed: scrollBar.increase() - ScrollBar.vertical: ScrollBar { + ScrollBar.vertical: ScrollBarType { id: scrollBar policy: fl.height >= fl.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn } diff --git a/client/ui/qml/Controls2/Header2Type.qml b/client/ui/qml/Controls2/Header2Type.qml index a08e711e..c435cbe2 100644 --- a/client/ui/qml/Controls2/Header2Type.qml +++ b/client/ui/qml/Controls2/Header2Type.qml @@ -1,6 +1,8 @@ import QtQuick import QtQuick.Layouts +import Style 1.0 + import "TextTypes" Item { @@ -37,7 +39,7 @@ Item { implicitHeight: 40 image: root.actionButtonImage - imageColor: "#D7D8DB" + imageColor: AmneziaStyle.color.paleGray visible: image ? true : false @@ -57,7 +59,7 @@ Item { text: root.descriptionText - color: "#878B91" + color: AmneziaStyle.color.mutedGray visible: root.descriptionText !== "" } diff --git a/client/ui/qml/Controls2/HeaderType.qml b/client/ui/qml/Controls2/HeaderType.qml index 0bba92e9..1366148d 100644 --- a/client/ui/qml/Controls2/HeaderType.qml +++ b/client/ui/qml/Controls2/HeaderType.qml @@ -1,6 +1,8 @@ import QtQuick import QtQuick.Layouts +import Style 1.0 + import "TextTypes" Item { @@ -17,8 +19,6 @@ Item { property string descriptionText - focus: true - implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight @@ -46,7 +46,7 @@ Item { Layout.alignment: Qt.AlignRight image: root.actionButtonImage - imageColor: "#D7D8DB" + imageColor: AmneziaStyle.color.paleGray visible: image ? true : false @@ -66,7 +66,7 @@ Item { text: root.descriptionText - color: "#878B91" + color: AmneziaStyle.color.mutedGray visible: root.descriptionText !== "" } diff --git a/client/ui/qml/Controls2/HorizontalRadioButton.qml b/client/ui/qml/Controls2/HorizontalRadioButton.qml index 6a0b8125..89cc1658 100644 --- a/client/ui/qml/Controls2/HorizontalRadioButton.qml +++ b/client/ui/qml/Controls2/HorizontalRadioButton.qml @@ -2,29 +2,57 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" RadioButton { id: root - property string hoveredColor: Qt.rgba(1, 1, 1, 0.05) - property string defaultColor: Qt.rgba(1, 1, 1, 0) - property string checkedColor: Qt.rgba(1, 1, 1, 0) - property string disabledColor: "transparent" + property string hoveredColor: AmneziaStyle.color.barelyTranslucentWhite + property string defaultColor: AmneziaStyle.color.transparent + property string checkedColor: AmneziaStyle.color.transparent + property string disabledColor: AmneziaStyle.color.transparent - property string textColor: "#D7D8DB" - property string textDisabledColor: "#878B91" + property string textColor: AmneziaStyle.color.paleGray + property string textDisabledColor: AmneziaStyle.color.mutedGray - property string pressedBorderColor: "#494B50" - property string checkedBorderColor: "#FBB26A" - property string defaultBodredColor: "transparent" - property string checkedDisabledBorderColor: "#84603D" - property string borderFocusedColor: "#D7D8DB" + property string pressedBorderColor: AmneziaStyle.color.charcoalGray + property string checkedBorderColor: AmneziaStyle.color.goldenApricot + property string defaultBodredColor: AmneziaStyle.color.transparent + property string checkedDisabledBorderColor: AmneziaStyle.color.mutedBrown + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderWidth: 0 implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + indicator: Rectangle { anchors.fill: parent radius: 16 diff --git a/client/ui/qml/Controls2/ImageButtonType.qml b/client/ui/qml/Controls2/ImageButtonType.qml index a08b613a..d5f646a7 100644 --- a/client/ui/qml/Controls2/ImageButtonType.qml +++ b/client/ui/qml/Controls2/ImageButtonType.qml @@ -2,42 +2,61 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + Button { id: root property string image - property string hoveredColor: Qt.rgba(1, 1, 1, 0.08) - property string defaultColor: "transparent" - property string pressedColor: Qt.rgba(1, 1, 1, 0.12) - property string disableColor: "#2C2D30" + property string hoveredColor: AmneziaStyle.color.translucentWhite + property string defaultColor: AmneziaStyle.color.transparent + property string pressedColor: AmneziaStyle.color.sheerWhite + property string disableColor: AmneziaStyle.color.slateGray - property string imageColor: "#878B91" - property string disableImageColor: "#2C2D30" + property string imageColor: AmneziaStyle.color.mutedGray + property string disableImageColor: AmneziaStyle.color.slateGray property alias backgroundColor: background.color property alias backgroundRadius: background.radius - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 hoverEnabled: true - focus: true - focusPolicy: Qt.TabFocus icon.source: image icon.color: root.enabled ? imageColor : disableImageColor - property Flickable parentFlickable + property bool isFocusable: true - onFocusChanged: { - if (root.activeFocus) { - if (root.parentFlickable) { - root.parentFlickable.ensureVisible(this) - } - } + Keys.onTabPressed: { + FocusController.nextKeyTabItem() } + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + Keys.onEnterPressed: root.clicked() + Keys.onReturnPressed: root.clicked() + Behavior on icon.color { PropertyAnimation { duration: 200 } } @@ -46,7 +65,7 @@ Button { id: background anchors.fill: parent - border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.activeFocus ? root.borderFocusedWidth : 0 color: { diff --git a/client/ui/qml/Controls2/LabelWithButtonType.qml b/client/ui/qml/Controls2/LabelWithButtonType.qml index c66ce831..087415f7 100644 --- a/client/ui/qml/Controls2/LabelWithButtonType.qml +++ b/client/ui/qml/Controls2/LabelWithButtonType.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" Item { @@ -18,26 +20,53 @@ Item { property string buttonImageSource property string rightImageSource property string leftImageSource - property bool isLeftImageHoverEnabled: true //todo separete this qml file to 3 + property bool isLeftImageHoverEnabled: true + property bool isSmallLeftImage: false property alias rightButton: rightImage property alias eyeButton: eyeImage property FlickableType parentFlickable - property string textColor: "#d7d8db" - property string textDisabledColor: "#878B91" - property string descriptionColor: "#878B91" - property string descriptionDisabledColor: "#494B50" + property string textColor: AmneziaStyle.color.paleGray + property string textDisabledColor: AmneziaStyle.color.mutedGray + property string descriptionColor: AmneziaStyle.color.mutedGray + property string descriptionDisabledColor: AmneziaStyle.color.charcoalGray property real textOpacity: 1.0 - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 - property string rightImageColor: "#d7d8db" + property string rightImageColor: AmneziaStyle.color.paleGray property bool descriptionOnTop: false property bool hideDescription: true + property bool isFocusable: !(eyeImage.visible || rightImage.visible) // TODO: this component already has focusable items + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + implicitWidth: content.implicitWidth + content.anchors.topMargin + content.anchors.bottomMargin implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin @@ -112,12 +141,12 @@ Item { visible: leftImageSource ? true : false - Layout.preferredHeight: rightImageSource || !isLeftImageHoverEnabled ? leftImage.implicitHeight : 56 - Layout.preferredWidth: rightImageSource || !isLeftImageHoverEnabled ? leftImage.implicitWidth : 56 - Layout.rightMargin: rightImageSource || !isLeftImageHoverEnabled ? 16 : 0 + Layout.preferredHeight: (rightImageSource || !isLeftImageHoverEnabled || isSmallLeftImage) ? 40 : 56 + Layout.preferredWidth: (rightImageSource || !isLeftImageHoverEnabled || isSmallLeftImage)? 40 : 56 + Layout.rightMargin: isSmallLeftImage ? 8 : (rightImageSource || !isLeftImageHoverEnabled) ? 16 : 0 radius: 12 - color: "transparent" + color: AmneziaStyle.color.transparent Behavior on color { PropertyAnimation { duration: 200 } @@ -220,7 +249,7 @@ Item { id: eyeImageBackground anchors.fill: parent radius: 12 - color: "transparent" + color: AmneziaStyle.color.transparent Behavior on color { PropertyAnimation { duration: 200 } @@ -257,7 +286,7 @@ Item { id: rightImageBackground anchors.fill: parent radius: 12 - color: "transparent" + color: AmneziaStyle.color.transparent Behavior on color { PropertyAnimation { duration: 200 } @@ -274,9 +303,9 @@ Item { Rectangle { id: background anchors.fill: root - color: "transparent" + color: AmneziaStyle.color.transparent - border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.activeFocus ? root.borderFocusedWidth : 0 diff --git a/client/ui/qml/Controls2/LabelWithImageType.qml b/client/ui/qml/Controls2/LabelWithImageType.qml new file mode 100644 index 00000000..57d60d8f --- /dev/null +++ b/client/ui/qml/Controls2/LabelWithImageType.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "TextTypes" + +RowLayout { + property string imageSource + property string leftText + property var rightText + property bool isRightTextUndefined: rightText === undefined + + visible: !isRightTextUndefined + + Image { + Layout.preferredHeight: 18 + Layout.preferredWidth: 18 + source: imageSource + } + + ListItemTitleType { + Layout.fillWidth: true + Layout.rightMargin: 10 + Layout.alignment: Qt.AlignRight + + text: leftText + } + + ParagraphTextType { + visible: rightText !== "" + + Layout.alignment: Qt.AlignLeft + + text: isRightTextUndefined ? "" : rightText + } +} diff --git a/client/ui/qml/Controls2/ListViewType.qml b/client/ui/qml/Controls2/ListViewType.qml new file mode 100644 index 00000000..0de43d77 --- /dev/null +++ b/client/ui/qml/Controls2/ListViewType.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls + +ListView { + id: root + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + ScrollBar.vertical: ScrollBarType {} + + clip: true + reuseItems: true + snapMode: ListView.SnapToItem +} diff --git a/client/ui/qml/Controls2/ListViewWithLabelsType.qml b/client/ui/qml/Controls2/ListViewWithLabelsType.qml index 5b614c43..536bc25a 100644 --- a/client/ui/qml/Controls2/ListViewWithLabelsType.qml +++ b/client/ui/qml/Controls2/ListViewWithLabelsType.qml @@ -17,7 +17,7 @@ ListView { property bool dividerVisible: false - currentIndex: 0 + property int selectedIndex: 0 width: rootWidth height: menuContent.contentItem.height @@ -45,7 +45,7 @@ ListView { rightImageSource: imageSource clickedFunction: function() { - menuContent.currentIndex = index + menuContent.selectedIndex = index menuContent.selectedText = name if (menuContent.clickedFunction && typeof menuContent.clickedFunction === "function") { menuContent.clickedFunction() @@ -62,7 +62,7 @@ ListView { } Component.onCompleted: { - if (menuContent.currentIndex === index) { + if (menuContent.selectedIndex === index) { menuContent.selectedText = name } } diff --git a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml index 8551c7d5..bd7ca32e 100644 --- a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml +++ b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" ListView { @@ -18,155 +20,134 @@ ListView { property var clickedFunction - currentIndex: 0 + property int selectedIndex: 0 width: rootWidth height: root.contentItem.height clip: true - interactive: false + reuseItems: true - property FlickableType parentFlickable - property var lastItemTabClicked + property bool isFocusable: true - property int currentFocusIndex: 0 - - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } - } - - Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - } else { - currentFocusIndex = 0 - } - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } - - Item { - id: focusItem - Keys.onTabPressed: { - root.forceActiveFocus() - } - } - - onVisibleChanged: { - if (visible) { - focusItem.forceActiveFocus() - } - } - - onCurrentFocusIndexChanged: { - if (parentFlickable) { - parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex)) - } - } + ScrollBar.vertical: ScrollBarType {} ButtonGroup { id: buttonGroup } function triggerCurrentItem() { - var item = root.itemAtIndex(currentIndex) - var radioButton = item.children[0].children[0] - radioButton.clicked() + var item = root.itemAtIndex(selectedIndex) + item.selectable.clicked() } - delegate: Item { + delegate: ColumnLayout { + id: content + + property alias selectable: radioButton + implicitWidth: rootWidth - implicitHeight: content.implicitHeight - onActiveFocusChanged: { - if (activeFocus) { - radioButton.forceActiveFocus() + RadioButton { + id: radioButton + + implicitWidth: parent.width + implicitHeight: radioButtonContent.implicitHeight + + hoverEnabled: true + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() } - } - ColumnLayout { - id: content + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } - anchors.fill: parent + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - RadioButton { - id: radioButton + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } - implicitWidth: parent.width - implicitHeight: radioButtonContent.implicitHeight + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } - hoverEnabled: true + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } - indicator: Rectangle { - width: parent.width - 1 - height: parent.height - color: radioButton.hovered ? "#2C2D30" : "#1C1D21" - border.color: radioButton.focus ? "#D7D8DB" : "transparent" - border.width: radioButton.focus ? 1 : 0 + indicator: Rectangle { + width: parent.width - 1 + height: parent.height + color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack + border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent + border.width: radioButton.focus ? 1 : 0 - Behavior on color { - PropertyAnimation { duration: 200 } - } - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: false - } + Behavior on color { + PropertyAnimation { duration: 200 } + } + Behavior on border.color { + PropertyAnimation { duration: 200 } } - RowLayout { - id: radioButtonContent + MouseArea { anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: false + } + } - anchors.rightMargin: 16 - anchors.leftMargin: 16 + RowLayout { + id: radioButtonContent + anchors.fill: parent - z: 1 + anchors.rightMargin: 16 + anchors.leftMargin: 16 - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 20 - Layout.bottomMargin: 20 + z: 1 - text: name - maximumLineCount: root.textMaximumLineCount - elide: root.textElide + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.bottomMargin: 20 - } + text: name + maximumLineCount: root.textMaximumLineCount + elide: root.textElide - Image { - source: imageSource - visible: radioButton.checked - - width: 24 - height: 24 - - Layout.rightMargin: 8 - } } - ButtonGroup.group: buttonGroup - checked: root.currentIndex === index + Image { + source: imageSource + visible: radioButton.checked - onClicked: { - root.currentIndex = index - root.selectedText = name - if (clickedFunction && typeof clickedFunction === "function") { - clickedFunction() - } + width: 24 + height: 24 + + Layout.rightMargin: 8 + } + } + + ButtonGroup.group: buttonGroup + checked: root.selectedIndex === index + + onClicked: { + root.selectedIndex = index + root.selectedText = name + if (clickedFunction && typeof clickedFunction === "function") { + clickedFunction() } } } Component.onCompleted: { - if (root.currentIndex === index) { + if (root.selectedIndex === index) { root.selectedText = name } } diff --git a/client/ui/qml/Controls2/PageType.qml b/client/ui/qml/Controls2/PageType.qml index 0a2a2998..d7f3317f 100644 --- a/client/ui/qml/Controls2/PageType.qml +++ b/client/ui/qml/Controls2/PageType.qml @@ -9,53 +9,21 @@ Item { property StackView stackView: StackView.view - property var defaultActiveFocusItem: null - onVisibleChanged: { - if (visible && !GC.isMobile()) { + if (visible) { timer.start() } } - function lastItemTabClicked(focusItem) { - if (GC.isMobile()) { - return - } - - if (focusItem) { - focusItem.forceActiveFocus() - PageController.forceTabBarActiveFocus() - } else { - if (defaultActiveFocusItem) { - defaultActiveFocusItem.forceActiveFocus() - } - PageController.forceTabBarActiveFocus() - } - } - -// MouseArea { -// id: globalMouseArea -// z: 99 -// anchors.fill: parent - -// enabled: true - -// onPressed: function(mouse) { -// forceActiveFocus() -// mouse.accepted = false -// } -// } - // Set a timer to set focus after a short delay Timer { id: timer - interval: 100 // Milliseconds + interval: 200 // Milliseconds onTriggered: { - if (defaultActiveFocusItem) { - defaultActiveFocusItem.forceActiveFocus() - } + FocusController.resetRootObject() + FocusController.setFocusOnDefaultItem() } repeat: false // Stop the timer after one trigger - running: !GC.isMobile() // Start the timer + running: true // Start the timer } } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index fb0f0c3a..dfb6f273 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -2,7 +2,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" +import "../Config" Popup { id: root @@ -22,15 +25,15 @@ Popup { Overlay.modal: Rectangle { visible: root.closeButtonVisible - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } onOpened: { - focusItem.forceActiveFocus() + timer.start() } onClosed: { - PageController.forceStackActiveFocus() + FocusController.dropRootObject(root) } background: Rectangle { @@ -40,6 +43,17 @@ Popup { radius: 4 } + Timer { + id: timer + interval: 200 // Milliseconds + onTriggered: { + FocusController.pushRootObject(root) + FocusController.setFocusItem(closeButton) + } + repeat: false // Stop the timer after one trigger + running: true // Start the timer + } + contentItem: Item { implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight @@ -70,11 +84,6 @@ Popup { } } - Item { - id: focusItem - KeyNavigation.tab: closeButton - } - BasicButtonType { id: closeButton visible: closeButtonVisible @@ -82,15 +91,14 @@ Popup { implicitHeight: 32 defaultColor: "white" - hoveredColor: "#C1C2C5" - pressedColor: "#AEB0B7" - disabledColor: "#494B50" + hoveredColor: AmneziaStyle.color.lightGray + pressedColor: AmneziaStyle.color.lightGray + disabledColor: AmneziaStyle.color.charcoalGray - textColor: "#0E0E11" + textColor: AmneziaStyle.color.midnightBlack borderWidth: 0 text: qsTr("Close") - KeyNavigation.tab: focusItem clickedFunc: function() { root.close() diff --git a/client/ui/qml/Controls2/ProgressBarType.qml b/client/ui/qml/Controls2/ProgressBarType.qml index e642c3eb..83e49771 100644 --- a/client/ui/qml/Controls2/ProgressBarType.qml +++ b/client/ui/qml/Controls2/ProgressBarType.qml @@ -2,20 +2,22 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + ProgressBar { id: root implicitHeight: 4 background: Rectangle { - color: "#633303" + color: AmneziaStyle.color.richBrown } contentItem: Item { Rectangle { width: root.visualPosition * parent.width height: parent.height - color: "#FBB26A" + color: AmneziaStyle.color.goldenApricot } } } diff --git a/client/ui/qml/Controls2/ScrollBarType.qml b/client/ui/qml/Controls2/ScrollBarType.qml new file mode 100644 index 00000000..26e8edb6 --- /dev/null +++ b/client/ui/qml/Controls2/ScrollBarType.qml @@ -0,0 +1,11 @@ +import QtQuick +import QtQuick.Controls + +import "./" +import "../Controls2" + +ScrollBar { + id: root + + policy: parent.height >= parent.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn +} diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index 9433832f..0651390f 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -2,41 +2,70 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" Switch { id: root property alias descriptionText: description.text - property string descriptionTextColor: "#878B91" - property string descriptionTextDisabledColor: "#494B50" + property string descriptionTextColor: AmneziaStyle.color.mutedGray + property string descriptionTextDisabledColor: AmneziaStyle.color.charcoalGray - property string textColor: "#D7D8DB" - property string textDisabledColor: "#878B91" + property string textColor: AmneziaStyle.color.paleGray + property string textDisabledColor: AmneziaStyle.color.mutedGray - property string checkedIndicatorColor: "#633303" - property string defaultIndicatorColor: "transparent" - property string checkedDisabledIndicatorColor: "#402102" + property string checkedIndicatorColor: AmneziaStyle.color.richBrown + property string defaultIndicatorColor: AmneziaStyle.color.transparent + property string checkedDisabledIndicatorColor: AmneziaStyle.color.deepBrown - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 - property string checkedIndicatorBorderColor: "#633303" - property string defaultIndicatorBorderColor: "#494B50" - property string checkedDisabledIndicatorBorderColor: "#402102" + property string checkedIndicatorBorderColor: AmneziaStyle.color.richBrown + property string defaultIndicatorBorderColor: AmneziaStyle.color.charcoalGray + property string checkedDisabledIndicatorBorderColor: AmneziaStyle.color.deepBrown - property string checkedInnerCircleColor: "#FBB26A" - property string defaultInnerCircleColor: "#D7D8DB" - property string checkedDisabledInnerCircleColor: "#84603D" - property string defaultDisabledInnerCircleColor: "#494B50" + property string checkedInnerCircleColor: AmneziaStyle.color.goldenApricot + property string defaultInnerCircleColor: AmneziaStyle.color.paleGray + property string checkedDisabledInnerCircleColor: AmneziaStyle.color.mutedBrown + property string defaultDisabledInnerCircleColor: AmneziaStyle.color.charcoalGray - property string hoveredIndicatorBackgroundColor: Qt.rgba(1, 1, 1, 0.08) - property string defaultIndicatorBackgroundColor: "transparent" + property string hoveredIndicatorBackgroundColor: AmneziaStyle.color.translucentWhite + property string defaultIndicatorBackgroundColor: AmneziaStyle.color.transparent + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } hoverEnabled: enabled ? true : false focusPolicy: Qt.TabFocus property FlickableType parentFlickable: null + onFocusChanged: { if (root.activeFocus) { if (root.parentFlickable) { @@ -129,13 +158,15 @@ Switch { enabled: false } - Keys.onEnterPressed: { - root.checked = !root.checked - root.checkedChanged() - } + Keys.onEnterPressed: event => handleSwitch(event) + Keys.onReturnPressed: event => handleSwitch(event) + Keys.onSpacePressed: event => handleSwitch(event) - Keys.onReturnPressed: { - root.checked = !root.checked - root.checkedChanged() + function handleSwitch(event) { + if (!event.isAutoRepeat) { + root.checked = !root.checked + root.checkedChanged() + } + event.accepted = true } } diff --git a/client/ui/qml/Controls2/TabButtonType.qml b/client/ui/qml/Controls2/TabButtonType.qml index 11df83d8..0e48d975 100644 --- a/client/ui/qml/Controls2/TabButtonType.qml +++ b/client/ui/qml/Controls2/TabButtonType.qml @@ -1,32 +1,59 @@ import QtQuick import QtQuick.Controls +import Style 1.0 + TabButton { id: root - property string hoveredColor: "#633303" - property string defaultColor: "#2C2D30" - property string selectedColor: "#FBB26A" + property string hoveredColor: AmneziaStyle.color.richBrown + property string defaultColor: AmneziaStyle.color.slateGray + property string selectedColor: AmneziaStyle.color.goldenApricot - property string textColor: "#D7D8DB" + property string textColor: AmneziaStyle.color.paleGray - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 property bool isSelected: false + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + implicitHeight: 48 hoverEnabled: true - focusPolicy: Qt.TabFocus background: Rectangle { id: background anchors.fill: parent - color: "transparent" + color: AmneziaStyle.color.transparent - border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.activeFocus ? root.borderFocusedWidth : 0 Rectangle { diff --git a/client/ui/qml/Controls2/TabImageButtonType.qml b/client/ui/qml/Controls2/TabImageButtonType.qml index f93f4924..b49ad8eb 100644 --- a/client/ui/qml/Controls2/TabImageButtonType.qml +++ b/client/ui/qml/Controls2/TabImageButtonType.qml @@ -1,24 +1,51 @@ import QtQuick import QtQuick.Controls +import Style 1.0 + TabButton { id: root - property string hoveredColor: "#633303" - property string defaultColor: "#D7D8DB" - property string selectedColor: "#FBB26A" + property string hoveredColor: AmneziaStyle.color.richBrown + property string defaultColor: AmneziaStyle.color.paleGray + property string selectedColor: AmneziaStyle.color.goldenApricot property string image property bool isSelected: false - property string borderFocusedColor: "#D7D8DB" + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 property var clickedFunc hoverEnabled: true - focusPolicy: Qt.TabFocus icon.source: image icon.color: isSelected ? selectedColor : defaultColor @@ -26,10 +53,10 @@ TabButton { background: Rectangle { id: background anchors.fill: parent - color: "transparent" + color: AmneziaStyle.color.transparent radius: 10 - border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.activeFocus ? root.borderFocusedWidth : 0 } @@ -39,7 +66,7 @@ TabButton { cursorShape: Qt.PointingHandCursor enabled: false } - + Keys.onEnterPressed: { if (root.clickedFunc && typeof root.clickedFunc === "function") { root.clickedFunc() diff --git a/client/ui/qml/Controls2/TextAreaType.qml b/client/ui/qml/Controls2/TextAreaType.qml index 653ab477..9359fa16 100644 --- a/client/ui/qml/Controls2/TextAreaType.qml +++ b/client/ui/qml/Controls2/TextAreaType.qml @@ -1,6 +1,8 @@ import QtQuick import QtQuick.Controls +import Style 1.0 + Rectangle { id: root @@ -9,12 +11,12 @@ Rectangle { property alias textArea: textArea property alias textAreaText: textArea.text - property string borderHoveredColor: "#494B50" - property string borderNormalColor: "#2C2D30" - property string borderFocusedColor: "#d7d8db" + property string borderHoveredColor: AmneziaStyle.color.charcoalGray + property string borderNormalColor: AmneziaStyle.color.slateGray + property string borderFocusedColor: AmneziaStyle.color.paleGray height: 148 - color: "#1C1D21" + color: AmneziaStyle.color.onyxBlack border.width: 1 border.color: getBorderColor(borderNormalColor) radius: 16 @@ -52,10 +54,10 @@ Rectangle { anchors.topMargin: 16 anchors.bottomMargin: 16 - color: "#D7D8DB" - selectionColor: "#633303" - selectedTextColor: "#D7D8DB" - placeholderTextColor: "#878B91" + color: AmneziaStyle.color.paleGray + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray + placeholderTextColor: AmneziaStyle.color.mutedGray font.pixelSize: 16 font.weight: Font.Medium diff --git a/client/ui/qml/Controls2/TextAreaWithFooterType.qml b/client/ui/qml/Controls2/TextAreaWithFooterType.qml new file mode 100644 index 00000000..cf7b9146 --- /dev/null +++ b/client/ui/qml/Controls2/TextAreaWithFooterType.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "TextTypes" + +Rectangle { + id: root + + property string placeholderText + property string text + property string headerText + property alias textArea: textArea + property alias textAreaText: textArea.text + + property string borderHoveredColor: AmneziaStyle.color.charcoalGray + property string borderNormalColor: AmneziaStyle.color.slateGray + property string borderFocusedColor: AmneziaStyle.color.paleGray + + property string firstButtonImage + property string secondButtonImage + + property var firstButtonClickedFunc + property var secondButtonClickedFunc + + height: 148 + color: AmneziaStyle.color.onyxBlack + border.width: 1 + border.color: getBorderColor(borderNormalColor) + radius: 16 + + property FlickableType parentFlickable: null + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + + MouseArea { + id: parentMouse + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: textArea.forceActiveFocus() + hoverEnabled: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 0 + + LabelTextType { + Layout.fillWidth: true + text: root.headerText + } + + TextArea { + id: textArea + + Layout.fillWidth: true + Layout.fillHeight: true + + leftPadding: 0 + Layout.bottomMargin: 16 + + color: AmneziaStyle.color.paleGray + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray + placeholderTextColor: AmneziaStyle.color.mutedGray + + font.pixelSize: 16 + font.weight: Font.Medium + font.family: "PT Root UI VF" + + placeholderText: root.placeholderText + text: root.text + + onCursorVisibleChanged: { + if (textArea.cursorVisible) { + fl.interactive = true + } else { + fl.interactive = false + } + } + + wrapMode: Text.Wrap + + MouseArea { + id: textAreaMouse + anchors.fill: parent + acceptedButtons: Qt.RightButton + hoverEnabled: true + onClicked: { + fl.interactive = true + contextMenu.open() + } + } + + onFocusChanged: { + root.border.color = getBorderColor(borderNormalColor) + } + + ContextMenuType { + id: contextMenu + textObj: textArea + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: -8 + spacing: 0 + ImageButtonType { + id: firstButton + visible: root.firstButtonImage !== "" + + imageColor: AmneziaStyle.color.paleGray + + image: root.firstButtonImage + onClicked: function() { + if (root.firstButtonClickedFunc && typeof root.firstButtonClickedFunc === "function") { + root.firstButtonClickedFunc() + } + } + } + + ImageButtonType { + id: secondButton + visible: root.secondButtonImage !== "" + + imageColor: AmneziaStyle.color.paleGray + + image: root.secondButtonImage + onClicked: function() { + if (root.secondButtonClickedFunc && typeof root.secondButtonClickedFunc === "function") { + root.secondButtonClickedFunc() + } + } + } + + Item { + Layout.fillWidth: true + } + + ImageButtonType { + id: resetButton + imageColor: AmneziaStyle.color.paleGray + + visible: root.textAreaText !== "" + image: "qrc:/images/controls/close.svg" + + onClicked: function() { + root.textAreaText = "" + textArea.focus = true + } + } + } + } + + onPressed: { + root.border.color = getBorderColor(borderFocusedColor) + } + + onExited: { + root.border.color = getBorderColor(borderNormalColor) + } + + onEntered: { + root.border.color = getBorderColor(borderHoveredColor) + } + } + + + function getBorderColor(noneFocusedColor) { + return textArea.focus ? root.borderFocusedColor : noneFocusedColor + } +} diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 3a6ac1fa..c4ed91b3 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -2,14 +2,16 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Style 1.0 + import "TextTypes" Item { id: root property string headerText - property string headerTextDisabledColor: "#494B50" - property string headerTextColor: "#878b91" + property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray + property string headerTextColor: AmneziaStyle.color.mutedGray property alias errorText: errorField.text property bool checkEmptyText: false @@ -20,24 +22,23 @@ Item { property var clickedFunc property alias textField: textField - property alias textFieldText: textField.text - property string textFieldTextColor: "#d7d8db" - property string textFieldTextDisabledColor: "#878B91" + property string textFieldTextColor: AmneziaStyle.color.paleGray + property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray - property string textFieldPlaceholderText property bool textFieldEditable: true - property string borderColor: "#2C2D30" - property string borderFocusedColor: "#d7d8db" + property string borderColor: AmneziaStyle.color.slateGray + property string borderFocusedColor: AmneziaStyle.color.paleGray - property string backgroundColor: "#1c1d21" - property string backgroundDisabledColor: "transparent" - property string bgBorderHoveredColor: "#494B50" + property string backgroundColor: AmneziaStyle.color.onyxBlack + property string backgroundDisabledColor: AmneziaStyle.color.transparent + property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight property FlickableType parentFlickable + Connections { target: textField function onFocusChanged() { @@ -82,18 +83,26 @@ Item { TextField { id: textField - activeFocusOnTab: false + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } enabled: root.textFieldEditable color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText - placeholderText: root.textFieldPlaceholderText - placeholderTextColor: "#494B50" + placeholderTextColor: AmneziaStyle.color.charcoalGray - selectionColor: "#633303" - selectedTextColor: "#D7D8DB" + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray font.pixelSize: 16 font.weight: 400 @@ -117,8 +126,8 @@ Item { } onActiveFocusChanged: { - if (checkEmptyText && textFieldText === "") { - errorText = qsTr("The field can't be empty") + if (root.checkEmptyText && text === "") { + root.errorText = qsTr("The field can't be empty") } } @@ -147,7 +156,9 @@ Item { text: root.errorText visible: root.errorText !== "" - color: "#EB5757" + color: AmneziaStyle.color.vibrantRed + + Layout.fillWidth: true } } @@ -179,7 +190,7 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText - imageSource: root.buttonImageSource + leftImageSource: root.buttonImageSource anchors.top: content.top anchors.bottom: content.bottom @@ -205,9 +216,9 @@ Item { clickedFunc() } - if (KeyNavigation.tab) { - KeyNavigation.tab.forceActiveFocus(); - } + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } } Keys.onReturnPressed: { @@ -215,8 +226,8 @@ Item { clickedFunc() } - if (KeyNavigation.tab) { - KeyNavigation.tab.forceActiveFocus(); - } + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } } } diff --git a/client/ui/qml/Controls2/TextTypes/ButtonTextType.qml b/client/ui/qml/Controls2/TextTypes/ButtonTextType.qml index 94b48081..28056758 100644 --- a/client/ui/qml/Controls2/TextTypes/ButtonTextType.qml +++ b/client/ui/qml/Controls2/TextTypes/ButtonTextType.qml @@ -1,10 +1,12 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 24 lineHeightMode: Text.FixedHeight - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 16 font.weight: 600 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TextTypes/CaptionTextType.qml b/client/ui/qml/Controls2/TextTypes/CaptionTextType.qml index 4da2c960..ba511289 100644 --- a/client/ui/qml/Controls2/TextTypes/CaptionTextType.qml +++ b/client/ui/qml/Controls2/TextTypes/CaptionTextType.qml @@ -1,10 +1,12 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 16 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#0E0E11" + color: AmneziaStyle.color.midnightBlack font.pixelSize: 13 font.weight: 400 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TextTypes/Header1TextType.qml b/client/ui/qml/Controls2/TextTypes/Header1TextType.qml index 754ee2e4..40a0b35e 100644 --- a/client/ui/qml/Controls2/TextTypes/Header1TextType.qml +++ b/client/ui/qml/Controls2/TextTypes/Header1TextType.qml @@ -1,14 +1,16 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 38 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#D7D8DB" - font.pixelSize: 36 + color: AmneziaStyle.color.paleGray + font.pixelSize: 32 font.weight: 700 font.family: "PT Root UI VF" - font.letterSpacing: -1.08 + font.letterSpacing: -1.0 wrapMode: Text.WordWrap } diff --git a/client/ui/qml/Controls2/TextTypes/Header2TextType.qml b/client/ui/qml/Controls2/TextTypes/Header2TextType.qml index 5e27cc67..74412cfd 100644 --- a/client/ui/qml/Controls2/TextTypes/Header2TextType.qml +++ b/client/ui/qml/Controls2/TextTypes/Header2TextType.qml @@ -1,10 +1,12 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 30 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 25 font.weight: 700 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TextTypes/LabelTextType.qml b/client/ui/qml/Controls2/TextTypes/LabelTextType.qml index 8c76616d..9a9a1963 100644 --- a/client/ui/qml/Controls2/TextTypes/LabelTextType.qml +++ b/client/ui/qml/Controls2/TextTypes/LabelTextType.qml @@ -1,10 +1,12 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 16 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#878B91" + color: AmneziaStyle.color.mutedGray font.pixelSize: 13 font.weight: 400 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TextTypes/ListItemTitleType.qml b/client/ui/qml/Controls2/TextTypes/ListItemTitleType.qml index e6119c65..40cd7835 100644 --- a/client/ui/qml/Controls2/TextTypes/ListItemTitleType.qml +++ b/client/ui/qml/Controls2/TextTypes/ListItemTitleType.qml @@ -1,10 +1,12 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 21.6 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 18 font.weight: 400 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TextTypes/ParagraphTextType.qml b/client/ui/qml/Controls2/TextTypes/ParagraphTextType.qml index 6613e2ed..109b2876 100644 --- a/client/ui/qml/Controls2/TextTypes/ParagraphTextType.qml +++ b/client/ui/qml/Controls2/TextTypes/ParagraphTextType.qml @@ -1,10 +1,11 @@ import QtQuick +import Style 1.0 Text { lineHeight: 24 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 16 font.weight: 400 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TextTypes/SmallTextType.qml b/client/ui/qml/Controls2/TextTypes/SmallTextType.qml index da7510fc..6c28607a 100644 --- a/client/ui/qml/Controls2/TextTypes/SmallTextType.qml +++ b/client/ui/qml/Controls2/TextTypes/SmallTextType.qml @@ -1,10 +1,12 @@ import QtQuick +import Style 1.0 + Text { lineHeight: 20 + LanguageModel.getLineHeightAppend() lineHeightMode: Text.FixedHeight - color: "#D7D8DB" + color: AmneziaStyle.color.paleGray font.pixelSize: 14 font.weight: 400 font.family: "PT Root UI VF" diff --git a/client/ui/qml/Controls2/TopCloseButtonType.qml b/client/ui/qml/Controls2/TopCloseButtonType.qml index e29b0be4..3a652da6 100644 --- a/client/ui/qml/Controls2/TopCloseButtonType.qml +++ b/client/ui/qml/Controls2/TopCloseButtonType.qml @@ -14,18 +14,18 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { - color: "transparent" + color: AmneziaStyle.color.transparent } ImageButtonType { id: button image: "qrc:/images/svg/close_black_24dp.svg" - imageColor: "#D7D8DB" + imageColor: AmneziaStyle.color.paleGray implicitWidth: 40 implicitHeight: 40 diff --git a/client/ui/qml/Controls2/VerticalRadioButton.qml b/client/ui/qml/Controls2/VerticalRadioButton.qml index bc696cfa..bee8ef7b 100644 --- a/client/ui/qml/Controls2/VerticalRadioButton.qml +++ b/client/ui/qml/Controls2/VerticalRadioButton.qml @@ -3,6 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects +import Style 1.0 + import "TextTypes" RadioButton { @@ -12,29 +14,54 @@ RadioButton { property int textElide: Qt.ElideRight property string descriptionText - property string hoveredColor: Qt.rgba(1, 1, 1, 0.05) - property string defaultColor: Qt.rgba(1, 1, 1, 0) - property string disabledColor: Qt.rgba(1, 1, 1, 0) - property string selectedColor: Qt.rgba(1, 1, 1, 0) + property string hoveredColor: AmneziaStyle.color.barelyTranslucentWhite + property string defaultColor: AmneziaStyle.color.transparent + property string disabledColor: AmneziaStyle.color.transparent + property string selectedColor: AmneziaStyle.color.transparent - property string textColor: "#D7D8DB" - property string selectedTextColor: "#FBB26A" + property string textColor: AmneziaStyle.color.paleGray + property string selectedTextColor: AmneziaStyle.color.goldenApricot - property string borderFocusedColor: "#D7D8DB" + property string borderFocusedColor: AmneziaStyle.color.paleGray property int borderFocusedWidth: 1 property string imageSource property bool showImage + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + hoverEnabled: true - focusPolicy: Qt.TabFocus indicator: Rectangle { id: background anchors.verticalCenter: parent.verticalCenter - border.color: root.focus ? root.borderFocusedColor : "transparent" + border.color: root.focus ? root.borderFocusedColor : AmneziaStyle.color.transparent border.width: root.focus ? root.borderFocusedWidth : 0 implicitWidth: 56 @@ -137,7 +164,7 @@ RadioButton { CaptionTextType { id: description - color: "#878B91" + color: AmneziaStyle.color.mutedGray text: root.descriptionText visible: root.descriptionText !== "" diff --git a/client/ui/qml/Controls2/WarningType.qml b/client/ui/qml/Controls2/WarningType.qml index f996403e..24e5ecad 100644 --- a/client/ui/qml/Controls2/WarningType.qml +++ b/client/ui/qml/Controls2/WarningType.qml @@ -3,14 +3,16 @@ import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects +import Style 1.0 + import "TextTypes" Rectangle { id: root - property string textColor: "#D7D8DB" - property string backGroundColor: "#1C1D21" - property string imageColor: "#D7D8DB" + property string textColor: AmneziaStyle.color.paleGray + property string backGroundColor: AmneziaStyle.color.onyxBlack + property string imageColor: AmneziaStyle.color.paleGray property string textString property int textFormat: Text.PlainText diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml new file mode 100644 index 00000000..4e2e80f0 --- /dev/null +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -0,0 +1,32 @@ +pragma Singleton + +import QtQuick + +QtObject { + property QtObject color: QtObject { + readonly property color transparent: 'transparent' + readonly property color paleGray: '#D7D8DB' + readonly property color lightGray: '#C1C2C5' + readonly property color mutedGray: '#878B91' + readonly property color charcoalGray: '#494B50' + readonly property color slateGray: '#2C2D30' + readonly property color onyxBlack: '#1C1D21' + readonly property color midnightBlack: '#0E0E11' + readonly property color goldenApricot: '#FBB26A' + readonly property color burntOrange: '#A85809' + readonly property color mutedBrown: '#84603D' + readonly property color richBrown: '#633303' + readonly property color deepBrown: '#402102' + readonly property color vibrantRed: '#EB5757' + readonly property color darkCharcoal: '#261E1A' + readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12) + readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08) + readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05) + readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8) + readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3) + readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8) + readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) + readonly property color pearlGray: '#EAEAEC' + readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26) + } +} diff --git a/client/ui/qml/Modules/Style/qmldir b/client/ui/qml/Modules/Style/qmldir new file mode 100644 index 00000000..1d985b43 --- /dev/null +++ b/client/ui/qml/Modules/Style/qmldir @@ -0,0 +1,3 @@ +module Style + +singleton AmneziaStyle 1.0 AmneziaStyle.qml \ No newline at end of file diff --git a/client/ui/qml/Pages2/PageDeinstalling.qml b/client/ui/qml/Pages2/PageDeinstalling.qml index 15633fa0..f5fdb29a 100644 --- a/client/ui/qml/Pages2/PageDeinstalling.qml +++ b/client/ui/qml/Pages2/PageDeinstalling.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" diff --git a/client/ui/qml/Pages2/PageDevMenu.qml b/client/ui/qml/Pages2/PageDevMenu.qml new file mode 100644 index 00000000..d7afde1d --- /dev/null +++ b/client/ui/qml/Pages2/PageDevMenu.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + } + + ListView { + id: listView + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + + property bool isFocusable: true + + ScrollBar.vertical: ScrollBarType {} + + header: ColumnLayout { + width: listView.width + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: "Dev menu" + } + } + + model: 1 + clip: true + spacing: 16 + + delegate: ColumnLayout { + width: listView.width + + TextFieldWithHeaderType { + id: passwordTextField + + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Gateway endpoint") + textField.text: SettingsController.gatewayEndpoint + + buttonImageSource: textField.text !== "" ? "qrc:/images/controls/refresh-cw.svg" : "" + + clickedFunc: function() { + SettingsController.resetGatewayEndpoint() + } + + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== SettingsController.gatewayEndpoint) { + SettingsController.gatewayEndpoint = textField.text + } + } + } + } + + footer: ColumnLayout { + width: listView.width + + SwitcherType { + id: switcher + + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + text: qsTr("Dev gateway environment") + checked: SettingsController.isDevGatewayEnv + onToggled: function() { + SettingsController.isDevGatewayEnv = checked + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 21098cb2..f7233a89 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import SortFilterProxyModel 0.2 @@ -8,6 +9,7 @@ import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 import ContainersModelFilters 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -18,13 +20,13 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { + objectName: "pageControllerConnections" + target: PageController function onRestorePageHomeState(isContainerInstalled) { - drawer.open() + drawer.openTriggered() if (isContainerInstalled) { containersDropDown.rootButtonClickedFunction() } @@ -32,34 +34,43 @@ PageType { } Item { + objectName: "homeColumnItem" + anchors.fill: parent anchors.bottomMargin: drawer.collapsedHeight ColumnLayout { - anchors.fill: parent - anchors.topMargin: 34 - anchors.bottomMargin: 34 + objectName: "homeColumnLayout" - Item { - id: focusItem - KeyNavigation.tab: loggingButton.visible ? - loggingButton : - connectButton + anchors.fill: parent + anchors.topMargin: 12 + anchors.bottomMargin: 16 + + AdLabel { + id: adLabel + + Layout.fillWidth: true + Layout.preferredHeight: adLabel.contentHeight + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 22 } BasicButtonType { id: loggingButton + objectName: "loggingButton" + property bool isLoggingEnabled: SettingsController.isLoggingEnabled Layout.alignment: Qt.AlignHCenter implicitHeight: 36 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#878B91" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.mutedGray borderWidth: 0 visible: isLoggingEnabled ? true : false @@ -68,8 +79,6 @@ PageType { Keys.onEnterPressed: loggingButton.clicked() Keys.onReturnPressed: loggingButton.clicked() - KeyNavigation.tab: connectButton - onClicked: { PageController.goToPage(PageEnum.PageSettingsLogging) } @@ -77,89 +86,80 @@ PageType { ConnectButton { id: connectButton + objectName: "connectButton" + Layout.fillHeight: true Layout.alignment: Qt.AlignCenter - KeyNavigation.tab: splitTunnelingButton } BasicButtonType { id: splitTunnelingButton + objectName: "splitTunnelingButton" + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - Layout.bottomMargin: 34 leftPadding: 16 rightPadding: 16 implicitHeight: 36 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#878B91" - leftImageColor: "transparent" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.mutedGray borderWidth: 0 + buttonTextLabel.lineHeight: 20 + buttonTextLabel.font.pixelSize: 14 + buttonTextLabel.font.weight: 500 + property bool isSplitTunnelingEnabled: SitesModel.isTunnelingEnabled || AppSplitTunnelingModel.isTunnelingEnabled || - (ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi")) + ServersModel.isDefaultServerDefaultContainerHasSplitTunneling text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") - imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageColor: "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() Keys.onReturnPressed: splitTunnelingButton.clicked() - KeyNavigation.tab: drawer - onClicked: { - homeSplitTunnelingDrawer.open() + homeSplitTunnelingDrawer.openTriggered() } HomeSplitTunnelingDrawer { id: homeSplitTunnelingDrawer - parent: root + objectName: "homeSplitTunnelingDrawer" - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + parent: root } } } } - DrawerType2 { id: drawer + objectName: "drawerProtocol" + anchors.fill: parent - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + collapsedStateContent: Item { + objectName: "ProtocolDrawerCollapsedContent" - collapsedContent: Item { implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77 Component.onCompleted: { drawer.expandedHeight = implicitHeight } - Connections { - target: drawer - enabled: !GC.isMobile() - function onActiveFocusChanged() { - if (drawer.activeFocus && !drawer.isOpened) { - collapsedButtonChevron.forceActiveFocus() - } - } - } + ColumnLayout { id: collapsed + objectName: "collapsedColumnLayout" anchors.left: parent.left anchors.right: parent.right + spacing: 0 Component.onCompleted: { drawer.collapsedHeight = collapsed.implicitHeight @@ -174,6 +174,8 @@ PageType { } RowLayout { + objectName: "rowLayout" + Layout.topMargin: 14 Layout.leftMargin: 24 Layout.rightMargin: 24 @@ -182,9 +184,11 @@ PageType { spacing: 0 Connections { + objectName: "drawerConnections" + target: drawer - function onEntered() { - if (drawer.isCollapsed) { + function onCursorEntered() { + if (drawer.isCollapsedStateActive) { collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor collapsedButtonHeader.opacity = 0.8 } else { @@ -192,8 +196,8 @@ PageType { } } - function onExited() { - if (drawer.isCollapsed) { + function onCursorExited() { + if (drawer.isCollapsedStateActive) { collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor collapsedButtonHeader.opacity = 1 } else { @@ -202,7 +206,7 @@ PageType { } function onPressed(pressed, entered) { - if (drawer.isCollapsed) { + if (drawer.isCollapsedStateActive) { collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor collapsedButtonHeader.opacity = 0.7 } else { @@ -213,6 +217,8 @@ PageType { Header1TextType { id: collapsedButtonHeader + objectName: "collapsedButtonHeader" + Layout.maximumWidth: drawer.width - 48 - 18 - 12 maximumLineCount: 2 @@ -221,8 +227,6 @@ PageType { text: ServersModel.defaultServerName horizontalAlignment: Qt.AlignHCenter - KeyNavigation.tab: tabBar - Behavior on opacity { PropertyAnimation { duration: 200 } } @@ -230,14 +234,15 @@ PageType { ImageButtonType { id: collapsedButtonChevron + objectName: "collapsedButtonChevron" Layout.leftMargin: 8 - visible: drawer.isCollapsed + visible: drawer.isCollapsedStateActive() hoverEnabled: false image: "qrc:/images/controls/chevron-down.svg" - imageColor: "#d7d8db" + imageColor: AmneziaStyle.color.paleGray icon.width: 18 icon.height: 18 @@ -248,37 +253,75 @@ PageType { Keys.onEnterPressed: collapsedButtonChevron.clicked() Keys.onReturnPressed: collapsedButtonChevron.clicked() - Keys.onTabPressed: lastItemTabClicked() - onClicked: { - if (drawer.isCollapsed) { - drawer.open() + if (drawer.isCollapsedStateActive()) { + drawer.openTriggered() } } } } - LabelTextType { - id: collapsedServerMenuDescription - Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44 + RowLayout { + objectName: "rowLayoutLabel" Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded - } - } + Layout.topMargin: 8 + Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16 + spacing: 0 - Connections { - target: drawer - enabled: !GC.isMobile() - function onIsCollapsedChanged() { - if (!drawer.isCollapsed) { - focusItem1.forceActiveFocus() + BasicButtonType { + enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsedStateActive + hoverEnabled: enabled + + implicitHeight: 36 + + leftPadding: 16 + rightPadding: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.transparent + textColor: AmneziaStyle.color.mutedGray + + buttonTextLabel.lineHeight: 16 + buttonTextLabel.font.pixelSize: 13 + buttonTextLabel.font.weight: 400 + + text: drawer.isCollapsedStateActive ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded + leftImageSource: ServersModel.defaultServerImagePathCollapsed + leftImageColor: "" + changeLeftImageSize: false + + rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : "" + + onClicked: { + ServersModel.processedIndex = ServersModel.defaultIndex + + if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { + PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries) + } else { + PageController.showBusyIndicator(true) + let result = ApiSettingsController.getAccountInfo(false) + PageController.showBusyIndicator(false) + if (!result) { + return + } + + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } else { + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } + } } } } ColumnLayout { id: serversMenuHeader + objectName: "serversMenuHeader" anchors.top: collapsed.bottom anchors.right: parent.right @@ -290,45 +333,41 @@ PageType { visible: !ServersModel.isDefaultServerFromApi - Item { - id: focusItem1 - KeyNavigation.tab: containersDropDown - } - DropDownType { id: containersDropDown + objectName: "containersDropDown" - rootButtonImageColor: "#0E0E11" - rootButtonBackgroundColor: "#D7D8DB" - rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) - rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) - rootButtonHoveredBorderColor: "transparent" - rootButtonDefaultBorderColor: "transparent" + rootButtonImageColor: AmneziaStyle.color.midnightBlack + rootButtonBackgroundColor: AmneziaStyle.color.paleGray + rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray + rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray + rootButtonHoveredBorderColor: AmneziaStyle.color.transparent + rootButtonDefaultBorderColor: AmneziaStyle.color.transparent rootButtonTextTopMargin: 8 rootButtonTextBottomMargin: 8 + enabled: drawer.isOpened + text: ServersModel.defaultServerDefaultContainerName - textColor: "#0E0E11" + textColor: AmneziaStyle.color.midnightBlack headerText: qsTr("VPN protocol") headerBackButtonImage: "qrc:/images/controls/arrow-left.svg" rootButtonClickedFunction: function() { - containersDropDown.open() + containersDropDown.openTriggered() } drawerParent: root - KeyNavigation.tab: serversMenuContent listView: HomeContainersListView { id: containersListView + objectName: "containersListView" + rootWidth: root.width - onVisibleChanged: { - if (containersDropDown.visible && !GC.isMobile()) { - focusItem1.forceActiveFocus() - } - } Connections { + objectName: "rowLayoutConnections" + target: ServersModel function onDefaultServerIndexChanged() { @@ -370,167 +409,21 @@ PageType { ButtonGroup { id: serversRadioButtonGroup + objectName: "serversRadioButtonGroup" } - ListView { + ServersListView { id: serversMenuContent + objectName: "serversMenuContent" - anchors.top: serversMenuHeader.bottom - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.topMargin: 16 - - model: ServersModel - currentIndex: ServersModel.defaultIndex - - ScrollBar.vertical: ScrollBar { - id: scrollBar - policy: serversMenuContent.height >= serversMenuContent.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn - } - - - activeFocusOnTab: true - focus: true - - property int focusItemIndex: 0 - onActiveFocusChanged: { - if (activeFocus) { - serversMenuContent.focusItemIndex = 0 - serversMenuContent.itemAtIndex(focusItemIndex).forceActiveFocus() - } - } - - onFocusItemIndexChanged: { - const focusedElement = serversMenuContent.itemAtIndex(focusItemIndex) - if (focusedElement) { - if (focusedElement.y + focusedElement.height > serversMenuContent.height) { - serversMenuContent.contentY = focusedElement.y + focusedElement.height - serversMenuContent.height - } else { - serversMenuContent.contentY = 0 - } - } - } - - Keys.onUpPressed: scrollBar.decrease() - Keys.onDownPressed: scrollBar.increase() + isFocusable: false Connections { target: drawer - enabled: !GC.isMobile() - function onIsCollapsedChanged() { - if (drawer.isCollapsed) { - const item = serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex) - if (item) { item.serverRadioButtonProperty.focus = false } - } - } - } - Connections { - target: ServersModel - function onDefaultServerIndexChanged(serverIndex) { - serversMenuContent.currentIndex = serverIndex - } - } - - clip: true - - delegate: Item { - id: menuContentDelegate - - property variant delegateData: model - property VerticalRadioButton serverRadioButtonProperty: serverRadioButton - - implicitWidth: serversMenuContent.width - implicitHeight: serverRadioButtonContent.implicitHeight - - onActiveFocusChanged: { - if (activeFocus) { - serverRadioButton.forceActiveFocus() - } - } - - ColumnLayout { - id: serverRadioButtonContent - - anchors.fill: parent - anchors.rightMargin: 16 - anchors.leftMargin: 16 - - spacing: 0 - - RowLayout { - Layout.fillWidth: true - VerticalRadioButton { - id: serverRadioButton - - Layout.fillWidth: true - - text: name - descriptionText: serverDescription - - checked: index === serversMenuContent.currentIndex - checkable: !ConnectionController.isConnected - - ButtonGroup.group: serversRadioButtonGroup - - onClicked: { - if (ConnectionController.isConnected) { - PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection")) - return - } - - serversMenuContent.currentIndex = index - - ServersModel.defaultIndex = index - } - - MouseArea { - anchors.fill: serverRadioButton - cursorShape: Qt.PointingHandCursor - enabled: false - } - - Keys.onTabPressed: serverInfoButton.forceActiveFocus() - Keys.onEnterPressed: serverRadioButton.clicked() - Keys.onReturnPressed: serverRadioButton.clicked() - } - - ImageButtonType { - id: serverInfoButton - image: "qrc:/images/controls/settings.svg" - imageColor: "#D7D8DB" - - implicitWidth: 56 - implicitHeight: 56 - - z: 1 - - Keys.onTabPressed: { - if (serversMenuContent.focusItemIndex < serversMenuContent.count - 1) { - serversMenuContent.focusItemIndex++ - serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex).forceActiveFocus() - } else { - focusItem1.forceActiveFocus() - serversMenuContent.contentY = 0 - } - } - Keys.onEnterPressed: serverInfoButton.clicked() - Keys.onReturnPressed: serverInfoButton.clicked() - - onClicked: function() { - ServersModel.processedIndex = index - PageController.goToPage(PageEnum.PageSettingsServerInfo) - drawer.close() - } - } - } - - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 0 - Layout.rightMargin: 0 - } + // this item shouldn't be focused when drawer is closed + function onIsOpenedChanged() { + serversMenuContent.isFocusable = drawer.isOpened } } } diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml new file mode 100644 index 00000000..c22fdf0c --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -0,0 +1,310 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + + +PageType { + id: root + + ColumnLayout { + id: backButtonLayout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + } + } + + ListView { + id: listview + + anchors.top: backButtonLayout.bottom + anchors.bottom: saveButton.top + + width: parent.width + + clip: true + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + model: AwgConfigModel + + delegate: Item { + id: delegateItem + implicitWidth: listview.width + implicitHeight: col.implicitHeight + + property alias mtuTextField: mtuTextField + property bool isSaveButtonEnabled: mtuTextField.errorText === "" && + junkPacketMaxSizeTextField.errorText === "" && + junkPacketMinSizeTextField.errorText === "" && + junkPacketCountTextField.errorText === "" + + ColumnLayout { + id: col + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("AmneziaWG settings") + } + + TextFieldWithHeaderType { + id: mtuTextField + Layout.fillWidth: true + Layout.topMargin: 40 + + headerText: qsTr("MTU") + textField.text: clientMtu + textField.validator: IntValidator { bottom: 576; top: 65535 } + + textField.onEditingFinished: { + if (textField.text !== clientMtu) { + clientMtu = textField.text + } + } + checkEmptyText: true + KeyNavigation.tab: junkPacketCountTextField.textField + } + + TextFieldWithHeaderType { + id: junkPacketCountTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: "Jc - Junk packet count" + textField.text: clientJunkPacketCount + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== clientJunkPacketCount) { + clientJunkPacketCount = textField.text + } + } + + checkEmptyText: true + + KeyNavigation.tab: junkPacketMinSizeTextField.textField + } + + TextFieldWithHeaderType { + id: junkPacketMinSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: "Jmin - Junk packet minimum size" + textField.text: clientJunkPacketMinSize + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== clientJunkPacketMinSize) { + clientJunkPacketMinSize = textField.text + } + } + + checkEmptyText: true + + KeyNavigation.tab: junkPacketMaxSizeTextField.textField + } + + TextFieldWithHeaderType { + id: junkPacketMaxSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: "Jmax - Junk packet maximum size" + textField.text: clientJunkPacketMaxSize + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== clientJunkPacketMaxSize) { + clientJunkPacketMaxSize = textField.text + } + } + + checkEmptyText: true + + } + + Header2TextType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Server settings") + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 8 + + enabled: false + + headerText: qsTr("Port") + textField.text: port + } + + TextFieldWithHeaderType { + id: initPacketJunkSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "S1 - Init packet junk size" + textField.text: serverInitPacketJunkSize + } + + TextFieldWithHeaderType { + id: responsePacketJunkSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "S2 - Response packet junk size" + textField.text: serverResponsePacketJunkSize + } + + TextFieldWithHeaderType { + id: initPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H1 - Init packet magic header" + textField.text: serverInitPacketMagicHeader + } + + TextFieldWithHeaderType { + id: responsePacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H2 - Response packet magic header" + textField.text: serverResponsePacketMagicHeader + } + + TextFieldWithHeaderType { + id: underloadPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H3 - Underload packet magic header" + textField.text: serverUnderloadPacketMagicHeader + } + + TextFieldWithHeaderType { + id: transportPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: false + + headerText: "H4 - Transport packet magic header" + textField.text: serverTransportPacketMagicHeader + } + } + } + } + + BasicButtonType { + id: saveButton + + anchors.right: root.right + anchors.left: root.left + anchors.bottom: root.bottom + + anchors.topMargin: 24 + anchors.bottomMargin: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + enabled: listview.currentItem.isSaveButtonEnabled + + text: qsTr("Save") + + onActiveFocusChanged: { + if(activeFocus) { + listview.positionViewAtEnd() + } + } + + clickedFunc: function() { + forceActiveFocus() + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("Only the settings for this device will be changed") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(AwgConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index ec4aa010..8c629b68 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -2,9 +2,12 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtCore + import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,18 +19,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.portTextField.textField - - Item { - id: focusItem - onFocusChanged: { - if (activeFocus) { - fl.ensureVisible(focusItem) - } - } - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -39,360 +30,358 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.portTextField.textField } } - FlickableType { - id: fl + ListView { + id: listview + + property bool isFocusable: true + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom - contentHeight: content.implicitHeight - Column { - id: content + width: parent.width - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } - enabled: ServersModel.isProcessedServerHasWriteAccess() + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } - ListView { - id: listview + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - width: parent.width - height: listview.contentItem.height + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } - clip: true - interactive: false + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } - model: AwgConfigModel + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } - delegate: Item { - id: _delegate + clip: true - implicitWidth: listview.width - implicitHeight: col.implicitHeight + model: AwgConfigModel - property alias portTextField:portTextField + delegate: Item { + id: delegateItem + implicitWidth: listview.width + implicitHeight: col.implicitHeight - ColumnLayout { - id: col + property alias vpnAddressSubnetTextField: vpnAddressSubnetTextField + property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ColumnLayout { + id: col - anchors.leftMargin: 16 - anchors.rightMargin: 16 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - spacing: 0 + anchors.leftMargin: 16 + anchors.rightMargin: 16 - HeaderType { - Layout.fillWidth: true + spacing: 0 - headerText: qsTr("AmneziaWG settings") + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("AmneziaWG settings") + } + + TextFieldWithHeaderType { + id: vpnAddressSubnetTextField + + Layout.fillWidth: true + Layout.topMargin: 40 + + enabled: delegateItem.isEnabled + + headerText: qsTr("VPN address subnet") + textField.text: subnetAddress + + textField.onEditingFinished: { + if (textField.text !== subnetAddress) { + subnetAddress = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: delegateItem.isEnabled + + headerText: qsTr("Port") + textField.text: port + textField.maximumLength: 5 + textField.validator: IntValidator { bottom: 1; top: 65535 } + + textField.onEditingFinished: { + if (textField.text !== port) { + port = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: junkPacketCountTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("Jc - Junk packet count") + textField.text: serverJunkPacketCount + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text === "") { + textField.text = "0" } - TextFieldWithHeaderType { - id: portTextField - Layout.fillWidth: true - Layout.topMargin: 40 + if (textField.text !== serverJunkPacketCount) { + serverJunkPacketCount = textField.text + } + } - headerText: qsTr("Port") - textFieldText: port - textField.maximumLength: 5 - textField.validator: IntValidator { bottom: 1; top: 65535 } - parentFlickable: fl + checkEmptyText: true + } - textField.onEditingFinished: { - if (textFieldText !== port) { - port = textFieldText - } + TextFieldWithHeaderType { + id: junkPacketMinSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("Jmin - Junk packet minimum size") + textField.text: serverJunkPacketMinSize + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverJunkPacketMinSize) { + serverJunkPacketMinSize = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: junkPacketMaxSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("Jmax - Junk packet maximum size") + textField.text: serverJunkPacketMaxSize + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverJunkPacketMaxSize) { + serverJunkPacketMaxSize = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: initPacketJunkSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("S1 - Init packet junk size") + textField.text: serverInitPacketJunkSize + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverInitPacketJunkSize) { + serverInitPacketJunkSize = textField.text + } + } + + checkEmptyText: true + + onActiveFocusChanged: { + if(activeFocus) { + listview.positionViewAtEnd() + } + } + } + + TextFieldWithHeaderType { + id: responsePacketJunkSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("S2 - Response packet junk size") + textField.text: serverResponsePacketJunkSize + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverResponsePacketJunkSize) { + serverResponsePacketJunkSize = textField.text + } + } + + checkEmptyText: true + + onActiveFocusChanged: { + if(activeFocus) { + listview.positionViewAtEnd() + } + } + } + + TextFieldWithHeaderType { + id: initPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("H1 - Init packet magic header") + textField.text: serverInitPacketMagicHeader + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverInitPacketMagicHeader) { + serverInitPacketMagicHeader = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: responsePacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("H2 - Response packet magic header") + textField.text: serverResponsePacketMagicHeader + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverResponsePacketMagicHeader) { + serverResponsePacketMagicHeader = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: transportPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("H4 - Transport packet magic header") + textField.text: serverTransportPacketMagicHeader + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverTransportPacketMagicHeader) { + serverTransportPacketMagicHeader = textField.text + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: underloadPacketMagicHeaderTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: qsTr("H3 - Underload packet magic header") + textField.text: serverUnderloadPacketMagicHeader + textField.validator: IntValidator { bottom: 0 } + + textField.onEditingFinished: { + if (textField.text !== serverUnderloadPacketMagicHeader) { + serverUnderloadPacketMagicHeader = textField.text + } + } + + checkEmptyText: true + } + + BasicButtonType { + id: saveRestartButton + + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 24 + + enabled: underloadPacketMagicHeaderTextField.errorText === "" && + transportPacketMagicHeaderTextField.errorText === "" && + responsePacketMagicHeaderTextField.errorText === "" && + initPacketMagicHeaderTextField.errorText === "" && + responsePacketJunkSizeTextField.errorText === "" && + initPacketJunkSizeTextField.errorText === "" && + junkPacketMaxSizeTextField.errorText === "" && + junkPacketMinSizeTextField.errorText === "" && + junkPacketCountTextField.errorText === "" && + portTextField.errorText === "" && + vpnAddressSubnetTextField.errorText === "" + + text: qsTr("Save") + + onActiveFocusChanged: { + if(activeFocus) { + listview.positionViewAtEnd() + } + } + + clickedFunc: function() { + forceActiveFocus() + + if (delegateItem.isEnabled) { + if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text, + transportPacketMagicHeaderTextField.textField.text, + responsePacketMagicHeaderTextField.textField.text, + initPacketMagicHeaderTextField.textField.text)) { + PageController.showErrorMessage(qsTr("The values of the H1-H4 fields must be unique")) + return } - checkEmptyText: true - - KeyNavigation.tab: mtuTextField.textField - } - - TextFieldWithHeaderType { - id: mtuTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: qsTr("MTU") - textFieldText: mtu - textField.validator: IntValidator { bottom: 576; top: 65535 } - - textField.onEditingFinished: { - if (textFieldText === "") { - textFieldText = "0" - } - if (textFieldText !== mtu) { - mtu = textFieldText - } - } - checkEmptyText: true - KeyNavigation.tab: junkPacketCountTextField.textField - } - - TextFieldWithHeaderType { - id: junkPacketCountTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "Jc - Junk packet count" - textFieldText: junkPacketCount - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText === "") { - textFieldText = "0" - } - - if (textFieldText !== junkPacketCount) { - junkPacketCount = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: junkPacketMinSizeTextField.textField - } - - TextFieldWithHeaderType { - id: junkPacketMinSizeTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "Jmin - Junk packet minimum size" - textFieldText: junkPacketMinSize - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== junkPacketMinSize) { - junkPacketMinSize = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: junkPacketMaxSizeTextField.textField - } - - TextFieldWithHeaderType { - id: junkPacketMaxSizeTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "Jmax - Junk packet maximum size" - textFieldText: junkPacketMaxSize - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== junkPacketMaxSize) { - junkPacketMaxSize = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: initPacketJunkSizeTextField.textField - } - - TextFieldWithHeaderType { - id: initPacketJunkSizeTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "S1 - Init packet junk size" - textFieldText: initPacketJunkSize - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== initPacketJunkSize) { - initPacketJunkSize = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: responsePacketJunkSizeTextField.textField - } - - TextFieldWithHeaderType { - id: responsePacketJunkSizeTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "S2 - Response packet junk size" - textFieldText: responsePacketJunkSize - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== responsePacketJunkSize) { - responsePacketJunkSize = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: initPacketMagicHeaderTextField.textField - } - - TextFieldWithHeaderType { - id: initPacketMagicHeaderTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "H1 - Init packet magic header" - textFieldText: initPacketMagicHeader - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== initPacketMagicHeader) { - initPacketMagicHeader = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: responsePacketMagicHeaderTextField.textField - } - - TextFieldWithHeaderType { - id: responsePacketMagicHeaderTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "H2 - Response packet magic header" - textFieldText: responsePacketMagicHeader - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== responsePacketMagicHeader) { - responsePacketMagicHeader = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: transportPacketMagicHeaderTextField.textField - } - - TextFieldWithHeaderType { - id: transportPacketMagicHeaderTextField - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: "H4 - Transport packet magic header" - textFieldText: transportPacketMagicHeader - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl - - textField.onEditingFinished: { - if (textFieldText !== transportPacketMagicHeader) { - transportPacketMagicHeader = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: underloadPacketMagicHeaderTextField.textField - } - - TextFieldWithHeaderType { - id: underloadPacketMagicHeaderTextField - Layout.fillWidth: true - Layout.topMargin: 16 - parentFlickable: fl - - headerText: "H3 - Underload packet magic header" - textFieldText: underloadPacketMagicHeader - textField.validator: IntValidator { bottom: 0 } - - textField.onEditingFinished: { - if (textFieldText !== underloadPacketMagicHeader) { - underloadPacketMagicHeader = textFieldText - } - } - - checkEmptyText: true - - KeyNavigation.tab: saveRestartButton - } - - BasicButtonType { - id: saveRestartButton - parentFlickable: fl - - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.bottomMargin: 24 - - enabled: underloadPacketMagicHeaderTextField.errorText === "" && - transportPacketMagicHeaderTextField.errorText === "" && - responsePacketMagicHeaderTextField.errorText === "" && - initPacketMagicHeaderTextField.errorText === "" && - responsePacketJunkSizeTextField.errorText === "" && - initPacketJunkSizeTextField.errorText === "" && - junkPacketMaxSizeTextField.errorText === "" && - junkPacketMinSizeTextField.errorText === "" && - junkPacketCountTextField.errorText === "" && - portTextField.errorText === "" - - text: qsTr("Save") - - Keys.onTabPressed: lastItemTabClicked(focusItem) - - clickedFunc: function() { - if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text, - transportPacketMagicHeaderTextField.textField.text, - responsePacketMagicHeaderTextField.textField.text, - initPacketMagicHeaderTextField.textField.text)) { - PageController.showErrorMessage(qsTr("The values of the H1-H4 fields must be unique")) - return - } - - if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text), - parseInt(responsePacketJunkSizeTextField.textField.text))) { - PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)")) - return - } - - var headerText = qsTr("Save settings?") - var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - forceActiveFocus() - - if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) - return - } - - PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(AwgConfigModel.getConfig()) - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - saveRestartButton.forceActiveFocus() - } - } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text), + parseInt(responsePacketJunkSizeTextField.textField.text))) { + PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)")) + return } } + + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(AwgConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveRestartButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } } diff --git a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml index 5ef5771e..686ccd7b 100644 --- a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -15,13 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.trafficFromField.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -33,7 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.trafficFromField.textField } } @@ -55,11 +48,13 @@ PageType { ListView { id: listview + property int selectedIndex: 0 + width: parent.width height: listview.contentItem.height clip: true - interactive: false + reuseItems: true model: CloakConfigModel @@ -94,23 +89,21 @@ PageType { Layout.topMargin: 32 headerText: qsTr("Disguised as traffic from") - textFieldText: site + textField.text: site textField.onEditingFinished: { - if (textFieldText !== site) { - var tmpText = textFieldText + if (textField.text !== site) { + var tmpText = textField.text tmpText = tmpText.toLocaleLowerCase() var indexHttps = tmpText.indexOf("https://") if (indexHttps === 0) { - tmpText = textFieldText.substring(8) + tmpText = textField.text.substring(8) } else { - site = textFieldText + site = textField.text } } } - - KeyNavigation.tab: portTextField.textField } TextFieldWithHeaderType { @@ -120,17 +113,15 @@ PageType { Layout.topMargin: 16 headerText: qsTr("Port") - textFieldText: port + textField.text: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } textField.onEditingFinished: { - if (textFieldText !== port) { - port = textFieldText + if (textField.text !== port) { + port = textField.text } } - - KeyNavigation.tab: cipherDropDown } DropDownType { @@ -142,7 +133,6 @@ PageType { headerText: qsTr("Cipher") drawerParent: root - KeyNavigation.tab: saveRestartButton listView: ListViewWithRadioButtonType { id: cipherListView @@ -160,7 +150,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.close() + cipherDropDown.closeTriggered() } Component.onCompleted: { @@ -168,7 +158,7 @@ PageType { for (var i = 0; i < cipherListView.model.count; i++) { if (cipherListView.model.get(i).name === cipherDropDown.text) { - currentIndex = i + selectedIndex = i } } } @@ -183,7 +173,6 @@ PageType { Layout.bottomMargin: 24 text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 4779965f..9cc628b7 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,18 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.vpnAddressSubnetTextField.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - onActiveFocusChanged: { - if (activeFocus) { - fl.ensureVisible(focusItem) - } - } - } - ColumnLayout { id: backButtonLayout @@ -39,7 +28,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.vpnAddressSubnetTextField.textField } } @@ -100,14 +88,13 @@ PageType { Layout.topMargin: 32 headerText: qsTr("VPN address subnet") - textFieldText: subnetAddress + textField.text: subnetAddress parentFlickable: fl - KeyNavigation.tab: transportProtoSelector textField.onEditingFinished: { - if (textFieldText !== subnetAddress) { - subnetAddress = textFieldText + if (textField.text !== subnetAddress) { + subnetAddress = textField.text } } } @@ -131,8 +118,6 @@ PageType { return transportProto === "tcp" ? 1 : 0 } - KeyNavigation.tab: portTextField.enabled ? portTextField.textField : autoNegotiateEncryprionSwitcher - onCurrentIndexChanged: { if (transportProto === "tcp" && currentIndex === 0) { transportProto = "udp" @@ -152,17 +137,15 @@ PageType { enabled: isPortEditable headerText: qsTr("Port") - textFieldText: port + textField.text: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } textField.onEditingFinished: { - if (textFieldText !== port) { - port = textFieldText + if (textField.text !== port) { + port = textField.text } } - - KeyNavigation.tab: autoNegotiateEncryprionSwitcher } SwitcherType { @@ -180,10 +163,6 @@ PageType { autoNegotiateEncryprion = checked } } - - KeyNavigation.tab: hashDropDown.enabled ? - hashDropDown : - tlsAuthCheckBox } DropDownType { @@ -197,10 +176,6 @@ PageType { headerText: qsTr("Hash") drawerParent: root - parentFlickable: fl - KeyNavigation.tab: cipherDropDown.enabled ? - cipherDropDown : - tlsAuthCheckBox listView: ListViewWithRadioButtonType { id: hashListView @@ -223,7 +198,7 @@ PageType { clickedFunction: function() { hashDropDown.text = selectedText hash = hashDropDown.text - hashDropDown.close() + hashDropDown.closeTriggered() } Component.onCompleted: { @@ -249,9 +224,6 @@ PageType { headerText: qsTr("Cipher") drawerParent: root - parentFlickable: fl - - KeyNavigation.tab: tlsAuthCheckBox listView: ListViewWithRadioButtonType { id: cipherListView @@ -274,7 +246,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.close() + cipherDropDown.closeTriggered() } Component.onCompleted: { @@ -294,7 +266,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 32 Layout.preferredHeight: checkboxLayout.implicitHeight - color: "#1C1D21" + color: AmneziaStyle.color.onyxBlack radius: 16 Connections { @@ -319,8 +291,6 @@ PageType { text: qsTr("TLS auth") checked: tlsAuth - KeyNavigation.tab: blockDnsCheckBox - onCheckedChanged: { if (checked !== tlsAuth) { console.log("tlsAuth changed to: " + checked) @@ -338,8 +308,6 @@ PageType { text: qsTr("Block DNS requests outside of VPN") checked: blockDns - KeyNavigation.tab: additionalClientCommandsSwitcher - onCheckedChanged: { if (checked !== blockDns) { blockDns = checked @@ -354,9 +322,6 @@ PageType { Layout.fillWidth: true Layout.topMargin: 32 parentFlickable: fl - KeyNavigation.tab: additionalClientCommandsTextArea.visible ? - additionalClientCommandsTextArea.textArea : - additionalServerCommandsSwitcher checked: additionalClientCommands !== "" @@ -375,7 +340,7 @@ PageType { Layout.topMargin: 16 visible: additionalClientCommandsSwitcher.checked - KeyNavigation.tab: additionalServerCommandsSwitcher + parentFlickable: fl textAreaText: additionalClientCommands @@ -393,9 +358,6 @@ PageType { Layout.fillWidth: true Layout.topMargin: 16 parentFlickable: fl - KeyNavigation.tab: additionalServerCommandsTextArea.visible ? - additionalServerCommandsTextArea.textArea : - saveRestartButton checked: additionalServerCommands !== "" @@ -418,7 +380,6 @@ PageType { textAreaText: additionalServerCommands placeholderText: qsTr("Commands:") parentFlickable: fl - KeyNavigation.tab: saveRestartButton textArea.onEditingFinished: { if (additionalServerCommands !== textAreaText) { additionalServerCommands = textAreaText @@ -435,7 +396,6 @@ PageType { text: qsTr("Save") parentFlickable: fl - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolRaw.qml b/client/ui/qml/Pages2/PageProtocolRaw.qml index f51035b1..03b4e297 100644 --- a/client/ui/qml/Pages2/PageProtocolRaw.qml +++ b/client/ui/qml/Pages2/PageProtocolRaw.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -18,13 +19,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -36,7 +30,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listView } HeaderType { @@ -74,13 +67,6 @@ PageType { activeFocusOnTab: true focus: true - onActiveFocusChanged: { - if (focus) { - listView.currentIndex = 0 - listView.currentItem.focusItem.forceActiveFocus() - } - } - delegate: Item { implicitWidth: parent.width implicitHeight: delegateContent.implicitHeight @@ -100,11 +86,9 @@ PageType { text: qsTr("Show connection options") clickedFunction: function() { - configContentDrawer.open() + configContentDrawer.openTriggered() } - KeyNavigation.tab: removeButton - MouseArea { anchors.fill: button cursorShape: Qt.PointingHandCursor @@ -119,31 +103,12 @@ PageType { expandedHeight: root.height * 0.9 - onClosed: { - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } - } - parent: root anchors.fill: parent - expandedContent: Item { + expandedStateContent: Item { implicitHeight: configContentDrawer.expandedHeight - Connections { - target: configContentDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem1.forceActiveFocus() - } - } - - Item { - id: focusItem1 - KeyNavigation.tab: backButton1 - } - BackButtonType { id: backButton1 @@ -153,10 +118,8 @@ PageType { anchors.topMargin: 16 backButtonFunction: function() { - configContentDrawer.close() + configContentDrawer.closeTriggered() } - - KeyNavigation.tab: focusItem1 } FlickableType { @@ -191,9 +154,9 @@ PageType { leftPadding: 0 height: 24 - color: "#D7D8DB" - selectionColor: "#633303" - selectedTextColor: "#D7D8DB" + color: AmneziaStyle.color.paleGray + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray font.pixelSize: 16 font.weight: Font.Medium @@ -204,7 +167,7 @@ PageType { wrapMode: Text.Wrap background: Rectangle { - color: "transparent" + color: AmneziaStyle.color.transparent } } } @@ -223,9 +186,8 @@ PageType { visible: ServersModel.isProcessedServerHasWriteAccess() text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() - textColor: "#EB5757" + textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunction: function() { var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") diff --git a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml index 2cf18544..5786012b 100644 --- a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -15,15 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.focusItemId.enabled ? - listview.currentItem.focusItemId.textField : - focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -35,9 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.focusItemId.enabled ? - listview.currentItem.focusItemId.textField : - focusItem } } @@ -104,17 +93,15 @@ PageType { enabled: isPortEditable headerText: qsTr("Port") - textFieldText: port + textField.text: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } textField.onEditingFinished: { - if (textFieldText !== port) { - port = textFieldText + if (textField.text !== port) { + port = textField.text } } - - KeyNavigation.tab: cipherDropDown } DropDownType { @@ -128,9 +115,9 @@ PageType { headerText: qsTr("Cipher") drawerParent: root - KeyNavigation.tab: saveRestartButton listView: ListViewWithRadioButtonType { + id: cipherListView rootWidth: root.width @@ -146,7 +133,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.close() + cipherDropDown.closeTriggered() } Component.onCompleted: { @@ -171,7 +158,6 @@ PageType { enabled: isPortEditable | isCipherEditable text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml new file mode 100644 index 00000000..a30c17e7 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml @@ -0,0 +1,175 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + + +PageType { + id: root + + Item { + id: focusItem + onFocusChanged: { + if (activeFocus) { + fl.ensureVisible(focusItem) + } + } + KeyNavigation.tab: backButton + } + + ColumnLayout { + id: backButtonLayout + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + + BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.mtuTextField.textField + } + } + + FlickableType { + id: fl + anchors.top: backButtonLayout.bottom + anchors.bottom: parent.bottom + contentHeight: content.implicitHeight + saveButton.implicitHeight + saveButton.anchors.bottomMargin + saveButton.anchors.topMargin + + Column { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + ListView { + id: listview + + width: parent.width + height: listview.contentItem.height + + clip: true + interactive: false + + model: WireGuardConfigModel + + delegate: Item { + id: delegateItem + implicitWidth: listview.width + implicitHeight: col.implicitHeight + + property alias mtuTextField: mtuTextField + property bool isSaveButtonEnabled: mtuTextField.errorText === "" + + ColumnLayout { + id: col + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("WG settings") + } + + TextFieldWithHeaderType { + id: mtuTextField + Layout.fillWidth: true + Layout.topMargin: 40 + + headerText: qsTr("MTU") + textField.text: clientMtu + textField.validator: IntValidator { bottom: 576; top: 65535 } + + textField.onEditingFinished: { + if (textField.text !== clientMtu) { + clientMtu = textField.text + } + } + checkEmptyText: true + KeyNavigation.tab: saveButton + } + + Header2TextType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Server settings") + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 8 + + enabled: false + + headerText: qsTr("Port") + textField.text: port + } + } + } + } + } + } + + BasicButtonType { + id: saveButton + + anchors.right: root.right + anchors.left: root.left + anchors.bottom: root.bottom + + anchors.topMargin: 24 + anchors.bottomMargin: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + enabled: listview.currentItem.isSaveButtonEnabled + + text: qsTr("Save") + + clickedFunc: function() { + forceActiveFocus() + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("Only the settings for this device will be changed") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(WireGuardConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml index 4e6a851e..10523b74 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -15,13 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -33,7 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -63,15 +56,11 @@ PageType { model: WireGuardConfigModel - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - listview.itemAtIndex(0)?.focusItemId.forceActiveFocus() - } - } - delegate: Item { - property alias focusItemId: portTextField.textField + id: delegateItem + + property alias focusItemId: vpnAddressSubnetTextField + property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() implicitWidth: listview.width implicitHeight: col.implicitHeight @@ -94,20 +83,18 @@ PageType { } TextFieldWithHeaderType { - id: portTextField + id: vpnAddressSubnetTextField Layout.fillWidth: true Layout.topMargin: 40 - headerText: qsTr("Port") - textFieldText: port - textField.maximumLength: 5 - textField.validator: IntValidator { bottom: 1; top: 65535 } + enabled: delegateItem.isEnabled - KeyNavigation.tab: mtuTextField.textField + headerText: qsTr("VPN address subnet") + textField.text: subnetAddress textField.onEditingFinished: { - if (textFieldText !== port) { - port = textFieldText + if (textField.text !== subnetAddress) { + subnetAddress = textField.text } } @@ -115,24 +102,23 @@ PageType { } TextFieldWithHeaderType { - id: mtuTextField + id: portTextField Layout.fillWidth: true Layout.topMargin: 16 - headerText: qsTr("MTU") - textFieldText: mtu - textField.validator: IntValidator { bottom: 576; top: 65535 } + enabled: delegateItem.isEnabled - KeyNavigation.tab: saveButton + headerText: qsTr("Port") + textField.text: port + textField.maximumLength: 5 + textField.validator: IntValidator { bottom: 1; top: 65535 } textField.onEditingFinished: { - if (textFieldText === "") { - textFieldText = "0" - } - if (textFieldText !== mtu) { - mtu = textFieldText + if (textField.text !== port) { + port = textField.text } } + checkEmptyText: true } @@ -142,24 +128,34 @@ PageType { Layout.topMargin: 24 Layout.bottomMargin: 24 - enabled: mtuTextField.errorText === "" && - portTextField.errorText === "" + enabled: portTextField.errorText === "" && + vpnAddressSubnetTextField.errorText === "" text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) - - onClicked: { + onClicked: function() { forceActiveFocus() - if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) - return - } + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.updateContainer(WireGuardConfigModel.getConfig()) - focusItem.forceActiveFocus() + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + + PageController.goToPage(PageEnum.PageSetupWizardInstalling); + InstallController.updateContainer(WireGuardConfigModel.getConfig()) + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + saveRestartButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } Keys.onEnterPressed: saveButton.clicked() diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 6a8094d7..90705d3e 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,13 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -34,7 +28,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -64,13 +57,6 @@ PageType { model: XrayConfigModel - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - listview.itemAtIndex(0)?.focusItemId.forceActiveFocus() - } - } - delegate: Item { property alias focusItemId: textFieldWithHeaderType.textField @@ -100,20 +86,18 @@ PageType { Layout.topMargin: 32 headerText: qsTr("Disguised as traffic from") - textFieldText: site - - KeyNavigation.tab: basicButton + textField.text: site textField.onEditingFinished: { - if (textFieldText !== site) { - var tmpText = textFieldText + if (textField.text !== site) { + var tmpText = textField.text tmpText = tmpText.toLocaleLowerCase() var indexHttps = tmpText.indexOf("https://") if (indexHttps === 0) { - tmpText = textFieldText.substring(8) + tmpText = textField.text.substring(8) } else { - site = textFieldText + site = textField.text } } } @@ -127,8 +111,6 @@ PageType { text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) - onClicked: { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageServiceDnsSettings.qml b/client/ui/qml/Pages2/PageServiceDnsSettings.qml index 34d8b786..cef29813 100644 --- a/client/ui/qml/Pages2/PageServiceDnsSettings.qml +++ b/client/ui/qml/Pages2/PageServiceDnsSettings.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -15,13 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -33,7 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: removeButton } } @@ -57,7 +50,7 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: "Amnezia DNS" + headerText: "AmneziaDNS" descriptionText: qsTr("A DNS service is installed on your server, and it is only accessible via VPN.\n") + qsTr("The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab.") } @@ -69,9 +62,7 @@ PageType { width: parent.width text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() - textColor: "#EB5757" - - Keys.onTabPressed: root.lastItemTabClicked() + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) @@ -81,7 +72,7 @@ PageType { var yesButtonFunction = function() { if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected && SettingsController.isAmneziaDnsEnabled()) { - PageController.showNotificationMessage(qsTr("Cannot remove Amnezia DNS from running server")) + PageController.showNotificationMessage(qsTr("Cannot remove AmneziaDNS from running server")) } else { PageController.goToPage(PageEnum.PageDeinstalling) @@ -103,8 +94,6 @@ PageType { enabled: false } } - - DividerType {} } } } diff --git a/client/ui/qml/Pages2/PageServiceSftpSettings.qml b/client/ui/qml/Pages2/PageServiceSftpSettings.qml index 836db69b..2deb315c 100644 --- a/client/ui/qml/Pages2/PageServiceSftpSettings.qml +++ b/client/ui/qml/Pages2/PageServiceSftpSettings.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -15,8 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { target: InstallController @@ -25,11 +24,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -41,7 +35,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -73,7 +66,7 @@ PageType { onFocusChanged: { if (focus) { - listview.currentItem.focusItem.forceActiveFocus() + listview.currentItem.listViewFocusItem.forceActiveFocus() } } @@ -81,7 +74,7 @@ PageType { implicitWidth: listview.width implicitHeight: col.implicitHeight - property alias focusItem: hostLabel.rightButton + property alias listViewFocusItem: hostLabel.rightButton ColumnLayout { id: col @@ -106,7 +99,6 @@ PageType { Layout.topMargin: 32 parentFlickable: fl - KeyNavigation.tab: portLabel.rightButton text: qsTr("Host") descriptionText: ServersModel.getProcessedServerData("hostName") @@ -114,7 +106,7 @@ PageType { descriptionOnTop: true rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -135,10 +127,9 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: usernameLabel.rightButton rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -159,10 +150,9 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: passwordLabel.eyeButton rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -183,17 +173,9 @@ PageType { descriptionOnTop: true parentFlickable: fl - eyeButton.KeyNavigation.tab: passwordLabel.rightButton - rightButton.Keys.onTabPressed: { - if (mountButton.visible) { - mountButton.forceActiveFocus() - } else { - detailedInstructionsButton.forceActiveFocus() - } - } rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray buttonImageSource: hideDescription ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg" @@ -216,15 +198,14 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 parentFlickable: fl - KeyNavigation.tab: detailedInstructionsButton text: qsTr("Mount folder on device") @@ -232,7 +213,7 @@ PageType { PageController.showBusyIndicator(true) InstallController.mountSftpDrive(port, password, username) PageController.showBusyIndicator(false) - } + } } ParagraphTextType { @@ -280,57 +261,20 @@ PageType { Layout.leftMargin: 8 implicitHeight: 32 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#FBB26A" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot text: qsTr("Detailed instructions") parentFlickable: fl - KeyNavigation.tab: removeButton clickedFunc: function() { // Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") } } - - BasicButtonType { - id: removeButton - Layout.topMargin: 24 - Layout.bottomMargin: 16 - Layout.leftMargin: 8 - implicitHeight: 32 - - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - textColor: "#EB5757" - - parentFlickable: fl - Keys.onTabPressed: lastItemTabClicked() - - text: qsTr("Remove SFTP and all data stored there") - - clickedFunc: function() { - var headerText = qsTr("Remove SFTP and all data stored there?") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeProcessedContainer() - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - removeButton.forceActiveFocus() - } - } - - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } - } } } } diff --git a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml index 95343f63..1b77267a 100644 --- a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml +++ b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,8 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview - Connections { target: InstallController @@ -26,11 +25,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -42,7 +36,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -98,7 +91,6 @@ PageType { Layout.topMargin: 32 parentFlickable: fl - KeyNavigation.tab: portLabel.rightButton text: qsTr("Host") descriptionText: ServersModel.getProcessedServerData("hostName") @@ -106,7 +98,7 @@ PageType { descriptionOnTop: true rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -127,10 +119,9 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: usernameLabel.rightButton rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -151,10 +142,9 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: passwordLabel.eyeButton rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -175,11 +165,9 @@ PageType { descriptionOnTop: true parentFlickable: fl - eyeButton.KeyNavigation.tab: passwordLabel.rightButton - rightButton.KeyNavigation.tab: changeSettingsButton rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray buttonImageSource: hideDescription ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg" @@ -199,13 +187,7 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.9 - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { property string tempPort: port property string tempUsername: username property string tempPassword: password @@ -221,9 +203,6 @@ PageType { Connections { target: changeSettingsDrawer function onOpened() { - if (!GC.isMobile()) { - drawerFocusItem.forceActiveFocus() - } tempPort = port tempUsername = username tempPassword = password @@ -232,17 +211,12 @@ PageType { port = tempPort username = tempUsername password = tempPassword - portTextField.textFieldText = port - usernameTextField.textFieldText = username - passwordTextField.textFieldText = password + portTextField.textField.text = port + usernameTextField.textField.text = username + passwordTextField.textField.text = password } } - Item { - id: drawerFocusItem - KeyNavigation.tab: portTextField.textField - } - HeaderType { Layout.fillWidth: true @@ -257,18 +231,16 @@ PageType { parentFlickable: fl headerText: qsTr("Port") - textFieldText: port + textField.text: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } textField.onEditingFinished: { - textFieldText = textField.text.replace(/^\s+|\s+$/g, '') - if (textFieldText !== port) { - port = textFieldText + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== port) { + port = textField.text } } - - KeyNavigation.tab: usernameTextField.textField } TextFieldWithHeaderType { @@ -279,18 +251,16 @@ PageType { parentFlickable: fl headerText: qsTr("Username") - textFieldPlaceholderText: "username" - textFieldText: username + textField.placeholderText: "username" + textField.text: username textField.maximumLength: 32 textField.onEditingFinished: { - textFieldText = textField.text.replace(/^\s+|\s+$/g, '') - if (textFieldText !== username) { - username = textFieldText + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== username) { + username = textField.text } } - - KeyNavigation.tab: passwordTextField.textField } TextFieldWithHeaderType { @@ -303,12 +273,12 @@ PageType { parentFlickable: fl headerText: qsTr("Password") - textFieldPlaceholderText: "password" - textFieldText: password + textField.placeholderText: "password" + textField.text: password textField.maximumLength: 32 textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal - buttonImageSource: textFieldText !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") + buttonImageSource: textField.text !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" clickedFunc: function() { @@ -316,13 +286,11 @@ PageType { } textField.onFocusChanged: { - textFieldText = textField.text.replace(/^\s+|\s+$/g, '') - if (textFieldText !== password) { - password = textFieldText + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (textField.text !== password) { + password = textField.text } } - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -333,7 +301,6 @@ PageType { Layout.bottomMargin: 24 text: qsTr("Change connection settings") - Keys.onTabPressed: lastItemTabClicked(drawerFocusItem) clickedFunc: function() { forceActiveFocus() @@ -342,20 +309,20 @@ PageType { portTextField.errorText = qsTr("The port must be in the range of 1 to 65535") return } - if (usernameTextField.textFieldText && passwordTextField.textFieldText === "") { + if (usernameTextField.textField.text && passwordTextField.textField.text === "") { passwordTextField.errorText = qsTr("Password cannot be empty") return - } else if (usernameTextField.textFieldText === "" && passwordTextField.textFieldText) { + } else if (usernameTextField.textField.text === "" && passwordTextField.textField.text) { usernameTextField.errorText = qsTr("Username cannot be empty") return } PageController.goToPage(PageEnum.PageSetupWizardInstalling) InstallController.updateContainer(Socks5ProxyConfigModel.getConfig()) - tempPort = portTextField.textFieldText - tempUsername = usernameTextField.textFieldText - tempPassword = passwordTextField.textFieldText - changeSettingsDrawer.close() + tempPort = portTextField.textField.text + tempUsername = usernameTextField.textField.text + tempPassword = passwordTextField.textField.text + changeSettingsDrawer.closeTriggered() } } } @@ -371,11 +338,10 @@ PageType { Layout.rightMargin: 16 text: qsTr("Change connection settings") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() - changeSettingsDrawer.open() + changeSettingsDrawer.openTriggered() } } } diff --git a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml index df8db486..249c70c7 100644 --- a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,8 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { target: InstallController @@ -26,11 +25,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -42,7 +36,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: websiteName.rightButton } } @@ -82,12 +75,10 @@ PageType { } descriptionOnTop: true - textColor: "#FBB26A" + textColor: AmneziaStyle.color.goldenApricot rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: "#D7D8DB" - - KeyNavigation.tab: removeButton + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { GC.copyToClipBoard(descriptionText) @@ -126,41 +117,6 @@ PageType { text: qsTr("When configuring WordPress set the this onion address as domain.") } - - BasicButtonType { - id: removeButton - Layout.topMargin: 24 - Layout.bottomMargin: 16 - Layout.leftMargin: 8 - implicitHeight: 32 - - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - textColor: "#EB5757" - - text: qsTr("Remove website") - - Keys.onTabPressed: lastItemTabClicked(focusItem) - - clickedFunc: function() { - var headerText = qsTr("The site with all data will be removed from the tor network.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeProcessedContainer() - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - removeButton.forceActiveFocus() - } - } - - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } - } } } } diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index f8056c63..a47bb535 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QtQuick.Dialogs import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -13,8 +14,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: header - FlickableType { id: fl anchors.top: parent.top @@ -38,8 +37,6 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Settings") - - KeyNavigation.tab: account.rightButton } LabelWithButtonType { @@ -54,8 +51,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsServersList) } - - KeyNavigation.tab: connection.rightButton } DividerType {} @@ -71,8 +66,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsConnection) } - - KeyNavigation.tab: application.rightButton } DividerType {} @@ -88,8 +81,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsApplication) } - - KeyNavigation.tab: backup.rightButton } DividerType {} @@ -105,8 +96,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsBackup) } - - KeyNavigation.tab: about.rightButton } DividerType {} @@ -122,12 +111,28 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAbout) } - KeyNavigation.tab: close - } DividerType {} + LabelWithButtonType { + id: devConsole + visible: SettingsController.isDevModeEnabled + Layout.fillWidth: true + + text: qsTr("Dev console") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + leftImageSource: "qrc:/images/controls/bug.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageDevMenu) + } + } + + DividerType { + visible: SettingsController.isDevModeEnabled + } + LabelWithButtonType { id: close visible: GC.isDesktop() @@ -136,9 +141,7 @@ PageType { text: qsTr("Close application") leftImageSource: "qrc:/images/controls/x-circle.svg" - isLeftImageHoverEnabled: false - - Keys.onTabPressed: lastItemTabClicked(header) + isLeftImageHoverEnabled: false clickedFunction: function() { PageController.closeApplication() diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index 08ee6406..37327313 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -13,19 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - - onFocusChanged: { - if (focusItem.activeFocus) { - fl.contentY = 0 - } - } - } - BackButtonType { id: backButton @@ -34,21 +22,106 @@ PageType { anchors.right: parent.right anchors.topMargin: 20 - KeyNavigation.tab: telegramButton + onActiveFocusChanged: { + if(backButton.enabled && backButton.activeFocus) { + listView.positionViewAtBeginning() + } + } } - FlickableType { - id: fl + QtObject { + id: telegramGroup + + readonly property string title: qsTr("Telegram group") + readonly property string description: qsTr("To discuss features") + readonly property string imageSource: "qrc:/images/controls/telegram.svg" + readonly property var handler: function() { + Qt.openUrlExternally(qsTr("https://t.me/amnezia_vpn_en")) + } + } + + QtObject { + id: mail + + readonly property string title: qsTr("support@amnezia.org") + readonly property string description: qsTr("For reviews and bug reports") + readonly property string imageSource: "qrc:/images/controls/mail.svg" + readonly property var handler: function() { + Qt.openUrlExternally(qsTr("mailto:support@amnezia.org")) + } + } + + QtObject { + id: github + + readonly property string title: qsTr("GitHub") + readonly property string description: qsTr("Discover the source code") + readonly property string imageSource: "qrc:/images/controls/github.svg" + readonly property var handler: function() { + Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client")) + } + } + + QtObject { + id: website + + readonly property string title: qsTr("Website") + readonly property string description: qsTr("Visit official website") + readonly property string imageSource: "qrc:/images/controls/amnezia.svg" + readonly property var handler: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) + } + } + + property list contacts: [ + telegramGroup, + mail, + github, + website + ] + + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.height + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + ScrollBar.vertical: ScrollBarType {} + + model: contacts + + clip: true + + header: ColumnLayout { + width: listView.width Image { id: image @@ -84,7 +157,7 @@ PageType { font.pixelSize: 14 text: qsTr("Amnezia is a free and open-source application. You can support the developers if you like it.") - color: "#CCCAC8" + color: AmneziaStyle.color.paleGray } ParagraphTextType { @@ -95,79 +168,29 @@ PageType { text: qsTr("Contacts") } + } + + delegate: ColumnLayout { + width: listView.width LabelWithButtonType { id: telegramButton Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 6 - text: qsTr("Telegram group") - descriptionText: qsTr("To discuss features") - leftImageSource: "qrc:/images/controls/telegram.svg" + text: title + descriptionText: description + leftImageSource: imageSource - KeyNavigation.tab: mailButton - parentFlickable: fl - - clickedFunction: function() { - Qt.openUrlExternally(qsTr("https://t.me/amnezia_vpn_en")) - } + clickedFunction: handler } DividerType {} - LabelWithButtonType { - id: mailButton - Layout.fillWidth: true + } - text: qsTr("Mail") - descriptionText: qsTr("For reviews and bug reports") - leftImageSource: "qrc:/images/controls/mail.svg" - - KeyNavigation.tab: githubButton - parentFlickable: fl - - clickedFunction: function() { - } - - } - - DividerType {} - - LabelWithButtonType { - id: githubButton - Layout.fillWidth: true - - text: qsTr("GitHub") - leftImageSource: "qrc:/images/controls/github.svg" - - KeyNavigation.tab: websiteButton - parentFlickable: fl - - clickedFunction: function() { - Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client")) - } - - } - - DividerType {} - - LabelWithButtonType { - id: websiteButton - Layout.fillWidth: true - - text: qsTr("Website") - leftImageSource: "qrc:/images/controls/amnezia.svg" - - KeyNavigation.tab: checkUpdatesButton - parentFlickable: fl - - clickedFunction: function() { - Qt.openUrlExternally(qsTr("https://amnezia.org")) - } - - } - - DividerType {} + footer: ColumnLayout { + width: listView.width CaptionTextType { Layout.fillWidth: true @@ -176,53 +199,61 @@ PageType { horizontalAlignment: Text.AlignHCenter text: qsTr("Software version: %1").arg(SettingsController.getAppVersion()) - color: "#878B91" + color: AmneziaStyle.color.mutedGray + + MouseArea { + property int clickCount: 0 + anchors.fill: parent + onClicked: { + if (clickCount > 10) { + SettingsController.enableDevMode() + } else { + clickCount++ + } + } + } } BasicButtonType { id: checkUpdatesButton + Layout.alignment: Qt.AlignHCenter Layout.topMargin: 8 Layout.bottomMargin: 16 implicitHeight: 32 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#FBB26A" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot text: qsTr("Check for updates") - KeyNavigation.tab: privacyPolicyButton - parentFlickable: fl - clickedFunc: function() { Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") } } BasicButtonType { - id: privacyPolicyButton - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 16 - Layout.topMargin: -15 - implicitHeight: 25 + id: privacyPolicyButton - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#FBB26A" + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 16 + Layout.topMargin: -15 + implicitHeight: 25 - text: qsTr("Privacy Policy") + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot - Keys.onTabPressed: lastItemTabClicked() - parentFlickable: fl + text: qsTr("Privacy Policy") - clickedFunc: function() { - Qt.openUrlExternally("https://amnezia.org/en/policy") - } + clickedFunc: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/policy") + } } } } diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml new file mode 100644 index 00000000..6e67ef1f --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -0,0 +1,171 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property var processedServer + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + + SortFilterProxyModel { + id: proxyServersModel + objectName: "proxyServersModel" + + sourceModel: ServersModel + filters: [ + ValueFilter { + roleName: "isCurrentlyProcessed" + value: true + } + ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } + } + + ListViewType { + id: menuContent + + anchors.fill: parent + + model: ApiCountryModel + + currentIndex: 0 + + ButtonGroup { + id: containersRadioButtonGroup + } + + header: ColumnLayout { + width: menuContent.width + + spacing: 4 + + BackButtonType { + id: backButton + objectName: "backButton" + + Layout.topMargin: 20 + } + + HeaderType { + id: headerContent + objectName: "headerContent" + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 10 + + actionButtonImage: "qrc:/images/controls/settings.svg" + + headerText: root.processedServer.name + descriptionText: qsTr("Location for connection") + + actionButtonFunction: function() { + PageController.showBusyIndicator(true) + let result = ApiSettingsController.getAccountInfo(false) + PageController.showBusyIndicator(false) + if (!result) { + return + } + + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } + } + } + + delegate: ColumnLayout { + id: content + + width: menuContent.width + height: content.implicitHeight + + RowLayout { + VerticalRadioButton { + id: containerRadioButton + + Layout.fillWidth: true + Layout.leftMargin: 16 + + text: countryName + + ButtonGroup.group: containersRadioButtonGroup + + imageSource: "qrc:/images/controls/download.svg" + + checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected + + onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + + if (index !== ApiCountryModel.currentIndex) { + PageController.showBusyIndicator(true) + var prevIndex = ApiCountryModel.currentIndex + ApiCountryModel.currentIndex = index + if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, countryCode, countryName)) { + ApiCountryModel.currentIndex = prevIndex + } + PageController.showBusyIndicator(false) + } + } + + MouseArea { + anchors.fill: containerRadioButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + + Keys.onEnterPressed: { + if (checkable) { + checked = true + } + containerRadioButton.clicked() + } + Keys.onReturnPressed: { + if (checkable) { + checked = true + } + containerRadioButton.clicked() + } + } + + Image { + Layout.rightMargin: 32 + Layout.alignment: Qt.AlignRight + + source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" + } + } + + DividerType { + Layout.fillWidth: true + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml new file mode 100644 index 00000000..c6a2f98c --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -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) + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiInstructions.qml b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml new file mode 100644 index 00000000..7961594b --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiInstructions.qml @@ -0,0 +1,124 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + QtObject { + id: windows + + readonly property string title: qsTr("Windows") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#windows") + } + + QtObject { + id: macos + + readonly property string title: qsTr("macOS") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#macos") + } + + QtObject { + id: android + + readonly property string title: qsTr("Android") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#android") + } + + QtObject { + id: androidTv + + readonly property string title: qsTr("AndroidTV") + readonly property string link: qsTr("https://docs.amnezia.org/ru/documentation/instructions/android_tv_connect/") + } + + QtObject { + id: ios + + readonly property string title: qsTr("iOS") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#ios") + } + + QtObject { + id: linux + + readonly property string title: qsTr("Linux") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#linux") + } + + QtObject { + id: routers + + readonly property string title: qsTr("Routers") + readonly property string link: qsTr("https://docs.amnezia.org/documentation/instructions/connect-amnezia-premium#routers") + } + + property list instructionsModel: [ + windows, + macos, + android, + androidTv, + ios, + linux, + routers + ] + + ListViewType { + id: listView + + anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 24 + + model: instructionsModel + + header: ColumnLayout { + width: listView.width + + BackButtonType { + id: backButton + } + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("How to connect on another device") + descriptionText: qsTr("Setup guides on the Amnezia website") + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 6 + + text: title + rightImageSource: "qrc:/images/controls/external-link.svg" + + clickedFunction: function() { + Qt.openUrlExternally(link) + } + } + + DividerType {} + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml new file mode 100644 index 00000000..44b2d2fa --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -0,0 +1,220 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtCore + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property string configExtension: ".conf" + property string configCaption: qsTr("Save AmneziaVPN config") + + ListViewType { + id: listView + + anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 24 + + model: ApiCountryModel + + header: ColumnLayout { + width: listView.width + + BackButtonType { + id: backButton + } + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Configuration Files") + descriptionText: qsTr("For router setup or the AmneziaWG app") + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + 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" + + clickedFunction: function() { + if (isIssued) { + moreOptionsDrawer.countryName = countryName + moreOptionsDrawer.countryCode = countryCode + moreOptionsDrawer.openTriggered() + } else { + issueConfig(countryCode) + } + } + } + + DividerType {} + } + } + + DrawerType2 { + id: moreOptionsDrawer + + property string countryName + property string countryCode + + anchors.fill: parent + expandedHeight: parent.height * 0.4375 + + expandedStateContent: Item { + implicitHeight: moreOptionsDrawer.expandedHeight + + BackButtonType { + id: moreOptionsDrawerBackButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + + backButtonFunction: function() { + moreOptionsDrawer.closeTriggered() + } + } + + FlickableType { + anchors.top: moreOptionsDrawerBackButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + contentHeight: moreOptionsDrawerContent.height + + ColumnLayout { + id: moreOptionsDrawerContent + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Header2Type { + Layout.fillWidth: true + Layout.margins: 16 + + headerText: moreOptionsDrawer.countryName + qsTr(" configuration file") + } + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Generate a new configuration file") + descriptionText: qsTr("The previously created one will stop working") + + clickedFunction: function() { + showQuestion(true, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Revoke the current configuration file") + + clickedFunction: function() { + showQuestion(false, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName) + } + } + + DividerType {} + } + } + } + } + + function issueConfig(countryCode) { + var fileName = "" + if (GC.isMobile()) { + fileName = countryCode + configExtension + } else { + fileName = SystemController.getFileName(configCaption, + qsTr("Config files (*" + configExtension + ")"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode, + true, + configExtension) + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + let result = ApiConfigsController.exportNativeConfig(countryCode, fileName) + if (result) { + ApiSettingsController.getAccountInfo(true) + } + + PageController.showBusyIndicator(false) + if (result) { + PageController.showNotificationMessage(qsTr("Config file saved")) + } + } + } + + function revokeConfig(countryCode) { + PageController.showBusyIndicator(true) + let result = ApiConfigsController.revokeNativeConfig(countryCode) + if (result) { + ApiSettingsController.getAccountInfo(true) + } + PageController.showBusyIndicator(false) + + if (result) { + PageController.showNotificationMessage(qsTr("The config has been revoked")) + } + } + + function showQuestion(isConfigIssue, countryCode, countryName) { + var headerText + if (isConfigIssue) { + headerText = qsTr("Generate a new %1 configuration file?").arg(countryName) + } else { + headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName) + } + + var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it") + var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (isConfigIssue) { + issueConfig(countryCode) + } else { + revokeConfig(countryCode) + } + moreOptionsDrawer.closeTriggered() + } + var noButtonFunction = function() { + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml new file mode 100644 index 00000000..689502c1 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -0,0 +1,402 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property list labelsModel: [ + statusObject, + endDateObject, + deviceCountObject + ] + + QtObject { + id: statusObject + + readonly property string title: qsTr("Subscription Status") + readonly property string contentKey: "subscriptionStatus" + readonly property string objectImageSource: "qrc:/images/controls/info.svg" + } + + QtObject { + id: endDateObject + + readonly property string title: qsTr("Valid Until") + readonly property string contentKey: "endDate" + readonly property string objectImageSource: "qrc:/images/controls/history.svg" + } + + QtObject { + id: deviceCountObject + + readonly property string title: qsTr("Active Connections") + readonly property string contentKey: "connectedDevices" + readonly property string objectImageSource: "qrc:/images/controls/monitor.svg" + } + + property var processedServer + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + + SortFilterProxyModel { + id: proxyServersModel + objectName: "proxyServersModel" + + sourceModel: ServersModel + filters: [ + ValueFilter { + roleName: "isCurrentlyProcessed" + value: true + } + ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } + } + + ListViewType { + id: listView + + anchors.fill: parent + + model: labelsModel + + header: ColumnLayout { + width: listView.width + + spacing: 4 + + BackButtonType { + id: backButton + objectName: "backButton" + + Layout.topMargin: 20 + } + + HeaderType { + id: headerContent + objectName: "headerContent" + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 10 + + actionButtonImage: "qrc:/images/controls/edit-3.svg" + + headerText: root.processedServer.name + descriptionText: ApiAccountInfoModel.data("serviceDescription") + + actionButtonFunction: function() { + serverNameEditDrawer.openTriggered() + } + } + + RenameServerDrawer { + id: serverNameEditDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.35 + + serverNameText: root.processedServer.name + } + } + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + Connections { + target: ApiAccountInfoModel + + function onModelReset() { + delegateItem.rightText = ApiAccountInfoModel.data(contentKey) + } + } + + LabelWithImageType { + id: delegateItem + + Layout.fillWidth: true + Layout.margins: 16 + + imageSource: objectImageSource + leftText: title + rightText: ApiAccountInfoModel.data(contentKey) + + visible: rightText !== "" + } + } + + footer: ColumnLayout { + id: footer + + width: listView.width + spacing: 0 + + readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible") + + WarningType { + id: warning + + Layout.topMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.fillWidth: true + + backGroundColor: AmneziaStyle.color.translucentRichBrown + + textString: qsTr("Configurations have been updated for some countries. Download and install the updated configuration files") + + iconPath: "qrc:/images/controls/alert-circle.svg" + + visible: ApiAccountInfoModel.data("hasExpiredWorker") + } + + LabelWithButtonType { + id: vpnKey + + Layout.fillWidth: true + Layout.topMargin: warning.visible ? 16 : 32 + + visible: false //footer.isVisibleForAmneziaFree + + text: qsTr("Subscription Key") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + shareConnectionDrawer.headerText = qsTr("Amnezia Premium subscription key") + + shareConnectionDrawer.openTriggered() + shareConnectionDrawer.isSelfHostedConfig = false; + shareConnectionDrawer.shareButtonText = qsTr("Save VPN key as a file") + shareConnectionDrawer.copyButtonText = qsTr("Copy VPN key") + + + PageController.showBusyIndicator(true) + + ApiConfigsController.prepareVpnKeyExport() + + PageController.showBusyIndicator(false) + } + } + + DividerType { + visible: false //footer.isVisibleForAmneziaFree + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: warning.visible ? 16 : 32 + + visible: footer.isVisibleForAmneziaFree + + text: qsTr("Configuration Files") + + descriptionText: qsTr("Manage configuration files") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ApiSettingsController.updateApiCountryModel() + PageController.goToPage(PageEnum.PageSettingsApiNativeConfigs) + } + } + + DividerType { + 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 + + text: qsTr("Support") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsApiSupport) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + visible: footer.isVisibleForAmneziaFree + + text: qsTr("How to connect on another device") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsApiInstructions) + } + } + + DividerType { + visible: footer.isVisibleForAmneziaFree + } + + BasicButtonType { + id: resetButton + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 24 + Layout.bottomMargin: 16 + Layout.leftMargin: 8 + implicitHeight: 32 + + defaultColor: "transparent" + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + textColor: AmneziaStyle.color.vibrantRed + + text: qsTr("Reload API config") + + clickedFunc: function() { + var headerText = qsTr("Reload API config?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Cannot reload API config during active connection")) + } else { + PageController.showBusyIndicator(true) + ApiConfigsController.updateServiceFromGateway(ServersModel.processedIndex, "", "", true) + PageController.showBusyIndicator(false) + } + } + var noButtonFunction = function() { + } + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } + + BasicButtonType { + id: revokeButton + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 16 + Layout.leftMargin: 8 + implicitHeight: 32 + + visible: footer.isVisibleForAmneziaFree + + defaultColor: "transparent" + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + textColor: AmneziaStyle.color.vibrantRed + + text: qsTr("Unlink this device") + + clickedFunc: function() { + var headerText = qsTr("Are you sure you want to unlink this device?") + var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing Connect.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) + } else { + PageController.showBusyIndicator(true) + if (ApiConfigsController.deactivateDevice()) { + ApiSettingsController.getAccountInfo(true) + } + PageController.showBusyIndicator(false) + } + } + var noButtonFunction = function() { + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } + + BasicButtonType { + id: removeButton + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 16 + Layout.leftMargin: 8 + implicitHeight: 32 + + defaultColor: "transparent" + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + textColor: AmneziaStyle.color.vibrantRed + + text: qsTr("Remove from application") + + clickedFunc: function() { + var headerText = qsTr("Remove from application?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) + } else { + PageController.showBusyIndicator(true) + if (ApiConfigsController.deactivateDevice()) { + InstallController.removeProcessedServer() + } + PageController.showBusyIndicator(false) + } + } + var noButtonFunction = function() { + } + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } + } + } + + ShareConnectionDrawer { + id: shareConnectionDrawer + + anchors.fill: parent + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApiSupport.qml b/client/ui/qml/Pages2/PageSettingsApiSupport.qml new file mode 100644 index 00000000..2ca13151 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApiSupport.qml @@ -0,0 +1,127 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + QtObject { + id: telegram + + readonly property string title: qsTr("Telegram") + readonly property string description: "@" + ApiAccountInfoModel.getTelegramBotLink() + readonly property string link: "https://t.me/" + ApiAccountInfoModel.getTelegramBotLink() + } + + QtObject { + id: techSupport + + readonly property string title: qsTr("Email") + readonly property string description: qsTr("support@amnezia.org") + readonly property string link: "mailto:support@amnezia.org" + } + + QtObject { + id: paymentSupport + + readonly property string title: qsTr("Email Billing & Orders") + readonly property string description: qsTr("help@vpnpay.io") + readonly property string link: "mailto:help@vpnpay.io" + } + + QtObject { + id: site + + readonly property string title: qsTr("Website") + readonly property string description: qsTr("amnezia.org") + readonly property string link: LanguageModel.getCurrentSiteUrl() + } + + property list supportModel: [ + telegram, + techSupport, + paymentSupport, + site + ] + + ListViewType { + id: listView + + anchors.fill: parent + anchors.topMargin: 20 + anchors.bottomMargin: 24 + + model: supportModel + + header: ColumnLayout { + width: listView.width + + BackButtonType { + id: backButton + } + + HeaderType { + id: header + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Support") + descriptionText: qsTr("Our technical support specialists are available to assist you at any time") + } + } + + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + text: title + descriptionText: description + rightImageSource: "qrc:/images/controls/external-link.svg" + clickedFunction: function() { + Qt.openUrlExternally(link) + } + } + DividerType {} + } + + + footer: ColumnLayout { + width: listView.width + + LabelWithButtonType { + id: supportUuid + Layout.fillWidth: true + + text: qsTr("Support tag") + descriptionText: SettingsController.getInstallationUuid() + + descriptionOnTop: true + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index 5a77e2fd..b6920a8f 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -10,6 +10,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -20,8 +21,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - property bool pageEnabled Component.onCompleted: { @@ -47,13 +46,15 @@ PageType { QtObject { id: onlyForwardApps - property string name: qsTr("Only the apps from the list should have access via VPN") - property int type: routeMode.onlyForwardApps + + readonly property string name: qsTr("Only the apps from the list should have access via VPN") + readonly property int type: routeMode.onlyForwardApps } QtObject { id: allExceptApps - property string name: qsTr("Apps from the list should not have access via VPN") - property int type: routeMode.allExceptApps + + readonly property string name: qsTr("Apps from the list should not have access via VPN") + readonly property int type: routeMode.allExceptApps } function getRouteModesModelIndex() { @@ -65,11 +66,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -81,7 +77,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: switcher } RowLayout { @@ -102,10 +97,6 @@ PageType { enabled: root.pageEnabled - KeyNavigation.tab: selector.enabled ? - selector : - searchField.textField - checked: AppSplitTunnelingModel.isTunnelingEnabled onToggled: { AppSplitTunnelingModel.toggleSplitTunneling(checked) @@ -129,25 +120,23 @@ PageType { enabled: Qt.platform.os === "android" && root.pageEnabled - KeyNavigation.tab: searchField.textField - listView: ListViewWithRadioButtonType { rootWidth: root.width model: root.routeModesModel - currentIndex: getRouteModesModelIndex() + selectedIndex: getRouteModesModelIndex() clickedFunction: function() { selector.text = selectedText - selector.close() - if (AppSplitTunnelingModel.routeMode !== root.routeModesModel[currentIndex].type) { - AppSplitTunnelingModel.routeMode = root.routeModesModel[currentIndex].type + selector.closeTriggered() + if (AppSplitTunnelingModel.routeMode !== root.routeModesModel[selectedIndex].type) { + AppSplitTunnelingModel.routeMode = root.routeModesModel[selectedIndex].type } } Component.onCompleted: { - if (root.routeModesModel[currentIndex].type === AppSplitTunnelingModel.routeMode) { + if (root.routeModesModel[selectedIndex].type === AppSplitTunnelingModel.routeMode) { selector.text = selectedText } else { selector.text = root.routeModesModel[0].name @@ -157,7 +146,7 @@ PageType { Connections { target: AppSplitTunnelingModel function onRouteModeChanged() { - currentIndex = getRouteModesModelIndex() + selectedIndex = getRouteModesModelIndex() } } } @@ -214,7 +203,7 @@ PageType { text: appPath rightImageSource: "qrc:/images/controls/trash.svg" - rightImageColor: "#D7D8DB" + rightImageColor: AmneziaStyle.color.paleGray clickedFunction: function() { var headerText = qsTr("Remove ") + appPath + "?" @@ -241,7 +230,7 @@ PageType { Rectangle { anchors.fill: addAppButton anchors.bottomMargin: -24 - color: "#0E0E11" + color: AmneziaStyle.color.midnightBlack opacity: 0.8 } @@ -263,10 +252,9 @@ PageType { Layout.fillWidth: true - textFieldPlaceholderText: qsTr("application name") + textField.placeholderText: qsTr("application name") buttonImageSource: "qrc:/images/controls/plus.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) rightButtonClickedOnEnter: true clickedFunc: function() { @@ -275,12 +263,12 @@ PageType { if (Qt.platform.os === "windows") { var fileName = SystemController.getFileName(qsTr("Open executable file"), - qsTr("Executable file (*.*)")) + qsTr("Executable files (*.*)")) if (fileName !== "") { AppSplitTunnelingController.addApp(fileName) } } else if (Qt.platform.os === "android"){ - installedAppDrawer.open() + installedAppDrawer.openTriggered() } PageController.showBusyIndicator(false) diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index 2243915f..6f77a521 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -13,19 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - - onFocusChanged: { - if (focusItem.activeFocus) { - fl.contentY = 0 - } - } - } - BackButtonType { id: backButton @@ -33,8 +21,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: GC.isMobile() ? switcher : switcherAutoStart } FlickableType { @@ -50,6 +36,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right + spacing: 0 + HeaderType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -74,8 +62,8 @@ PageType { } } - KeyNavigation.tab: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted ? - labelWithButtonNotification.rightButton : labelWithButtonLanguage.rightButton + // KeyNavigation.tab: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted ? + // labelWithButtonNotification.rightButton : labelWithButtonLanguage.rightButton parentFlickable: fl } @@ -92,7 +80,6 @@ PageType { descriptionText: qsTr("Enable notifications to show the VPN state in the status bar") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: labelWithButtonLanguage.rightButton parentFlickable: fl clickedFunction: function() { @@ -114,7 +101,6 @@ PageType { text: qsTr("Auto start") descriptionText: qsTr("Launch the application every time the device is starts") - KeyNavigation.tab: switcherAutoConnect parentFlickable: fl checked: SettingsController.isAutoStartEnabled() @@ -139,7 +125,6 @@ PageType { text: qsTr("Auto connect") descriptionText: qsTr("Connect to VPN on app start") - KeyNavigation.tab: switcherStartMinimized parentFlickable: fl checked: SettingsController.isAutoConnectEnabled() @@ -164,7 +149,6 @@ PageType { text: qsTr("Start minimized") descriptionText: qsTr("Launch application minimized") - KeyNavigation.tab: labelWithButtonLanguage.rightButton parentFlickable: fl checked: SettingsController.isStartMinimizedEnabled() @@ -187,11 +171,10 @@ PageType { descriptionText: LanguageModel.currentLanguageName rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: labelWithButtonLogging.rightButton parentFlickable: fl clickedFunction: function() { - selectLanguageDrawer.open() + selectLanguageDrawer.openTriggered() } } @@ -205,7 +188,6 @@ PageType { descriptionText: SettingsController.isLoggingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: labelWithButtonReset.rightButton parentFlickable: fl clickedFunction: function() { @@ -221,9 +203,8 @@ PageType { text: qsTr("Reset settings and remove all data from the application") rightImageSource: "qrc:/images/controls/chevron-right.svg" - textColor: "#EB5757" + textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: lastItemTabClicked() parentFlickable: fl clickedFunction: function() { @@ -238,17 +219,10 @@ PageType { } else { SettingsController.clearSettings() - PageController.replaceStartPage() - } - - if (!GC.isMobile()) { - root.defaultActiveFocusItem.forceActiveFocus() + PageController.goToPageHome() } } var noButtonFunction = function() { - if (!GC.isMobile()) { - root.defaultActiveFocusItem.forceActiveFocus() - } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -264,11 +238,5 @@ PageType { width: root.width height: root.height - - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } } } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index ffb5659d..d2dd4f2a 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -6,6 +6,7 @@ import QtQuick.Dialogs import QtCore import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,8 +17,6 @@ import "../Controls2/TextTypes" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { target: SettingsController @@ -27,7 +26,6 @@ PageType { function onRestoreBackupFinished() { PageController.showNotificationMessage(qsTr("Settings restored from backup file")) - //goToStartPage() PageController.goToPageHome() } @@ -36,11 +34,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -48,8 +41,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: makeBackupButton } FlickableType { @@ -93,6 +84,8 @@ PageType { text: qsTr("Make a backup") + parentFlickable: fl + clickedFunc: function() { var fileName = "" if (GC.isMobile()) { @@ -111,8 +104,6 @@ PageType { PageController.showNotificationMessage(qsTr("Backup file saved")) } } - - KeyNavigation.tab: restoreBackupButton } BasicButtonType { @@ -120,15 +111,17 @@ PageType { Layout.fillWidth: true Layout.topMargin: -8 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Restore from backup") + parentFlickable: fl + clickedFunc: function() { var filePath = SystemController.getFileName(qsTr("Open backup file"), qsTr("Backup files (*.backup)")) @@ -136,8 +129,6 @@ PageType { restoreBackup(filePath) } } - - Keys.onTabPressed: lastItemTabClicked() } } } diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index 218ffe28..69671f27 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -11,15 +12,8 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem - property bool isAppSplitTinnelingEnabled: Qt.platform.os === "windows" || Qt.platform.os === "android" - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -27,8 +21,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: amneziaDnsSwitch } FlickableType { @@ -66,8 +58,6 @@ PageType { SettingsController.toggleAmneziaDns(checked) } } - - KeyNavigation.tab: dnsServersButton.rightButton } DividerType {} @@ -80,11 +70,11 @@ PageType { descriptionText: qsTr("When AmneziaDNS is not used or installed") rightImageSource: "qrc:/images/controls/chevron-right.svg" + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsDns) } - - KeyNavigation.tab: splitTunnelingButton.rightButton } DividerType {} @@ -97,19 +87,11 @@ PageType { descriptionText: qsTr("Allows you to select which sites you want to access through the VPN") rightImageSource: "qrc:/images/controls/chevron-right.svg" + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) } - - Keys.onTabPressed: { - if (splitTunnelingButton2.visible) { - return splitTunnelingButton2.rightButton.forceActiveFocus() - } else if (killSwitchSwitcher.visible) { - return killSwitchSwitcher.forceActiveFocus() - } else { - lastItemTabClicked() - } - } } DividerType { @@ -126,17 +108,11 @@ PageType { descriptionText: qsTr("Allows you to use the VPN only for certain Apps") rightImageSource: "qrc:/images/controls/chevron-right.svg" + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) } - - Keys.onTabPressed: { - if (killSwitchSwitcher.visible) { - return killSwitchSwitcher.forceActiveFocus() - } else { - lastItemTabClicked() - } - } } DividerType { @@ -153,6 +129,8 @@ PageType { text: qsTr("KillSwitch") descriptionText: qsTr("Disables your internet if your encrypted VPN connection drops out for any reason.") + parentFlickable: fl + checked: SettingsController.isKillSwitchEnabled() checkable: !ConnectionController.isConnected onCheckedChanged: { @@ -162,11 +140,9 @@ PageType { } onClicked: { if (!checkable) { - PageController.showNotificationMessage(qsTr("Cannot change killSwitch settings during active connection")) + PageController.showNotificationMessage(qsTr("Cannot change KillSwitch settings during active connection")) } } - - Keys.onTabPressed: lastItemTabClicked() } DividerType { diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml index 2082e671..d78c5aa8 100644 --- a/client/ui/qml/Pages2/PageSettingsDns.qml +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -13,13 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: primaryDns.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -27,8 +21,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: root.defaultActiveFocusItem } FlickableType { @@ -37,7 +29,7 @@ PageType { anchors.bottom: parent.bottom contentHeight: content.height - property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi") + property var isServerFromApi: ServersModel.isServerFromApi(ServersModel.defaultIndex) enabled: !isServerFromApi @@ -75,12 +67,10 @@ PageType { Layout.fillWidth: true headerText: qsTr("Primary DNS") - textFieldText: SettingsController.primaryDns + textField.text: SettingsController.primaryDns textField.validator: RegularExpressionValidator { regularExpression: InstallController.ipAddressRegExp() } - - KeyNavigation.tab: secondaryDns.textField } TextFieldWithHeaderType { @@ -89,23 +79,21 @@ PageType { Layout.fillWidth: true headerText: qsTr("Secondary DNS") - textFieldText: SettingsController.secondaryDns + textField.text: SettingsController.secondaryDns textField.validator: RegularExpressionValidator { regularExpression: InstallController.ipAddressRegExp() } - - KeyNavigation.tab: restoreDefaultButton } BasicButtonType { id: restoreDefaultButton Layout.fillWidth: true - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Restore default") @@ -117,25 +105,16 @@ PageType { var yesButtonFunction = function() { SettingsController.primaryDns = "1.1.1.1" - primaryDns.textFieldText = SettingsController.primaryDns + primaryDns.textField.text = SettingsController.primaryDns SettingsController.secondaryDns = "1.0.0.1" - secondaryDns.textFieldText = SettingsController.secondaryDns + secondaryDns.textField.text = SettingsController.secondaryDns PageController.showNotificationMessage(qsTr("Settings have been reset")) - - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } } var noButtonFunction = function() { - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -146,16 +125,14 @@ PageType { text: qsTr("Save") clickedFunc: function() { - if (primaryDns.textFieldText !== SettingsController.primaryDns) { - SettingsController.primaryDns = primaryDns.textFieldText + if (primaryDns.textField.text !== SettingsController.primaryDns) { + SettingsController.primaryDns = primaryDns.textField.text } - if (secondaryDns.textFieldText !== SettingsController.secondaryDns) { - SettingsController.secondaryDns = secondaryDns.textFieldText + if (secondaryDns.textField.text !== SettingsController.secondaryDns) { + SettingsController.secondaryDns = secondaryDns.textField.text } PageController.showNotificationMessage(qsTr("Settings saved")) } - - Keys.onTabPressed: lastItemTabClicked(focusItem) } } } diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index 64e4d4ba..2c760e37 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -6,6 +6,7 @@ import QtQuick.Dialogs import QtCore import PageEnum 1.0 +import Style 1.0 import "../Controls2" import "../Config" @@ -15,25 +16,6 @@ import "../Controls2/TextTypes" PageType { id: root - Connections { - target: SettingsController - - function onLoggingStateChanged() { - if (SettingsController.isLoggingEnabled) { - var message = qsTr("Logging is enabled. Note that logs will be automatically \ -disabled after 14 days, and all log files will be deleted.") - PageController.showNotificationMessage(message) - } - } - } - - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -41,29 +23,27 @@ disabled after 14 days, and all log files will be deleted.") anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: switcher } - FlickableType { - id: fl + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.height + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 + ScrollBar.vertical: ScrollBarType {} - spacing: 16 + header: ColumnLayout { + width: listView.width HeaderType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 headerText: qsTr("Logging") descriptionText: qsTr("Enabling this function will save application's logs automatically. " + @@ -72,13 +52,16 @@ disabled after 14 days, and all log files will be deleted.") SwitcherType { id: switcher + Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - text: qsTr("Save logs") + text: qsTr("Enable logs") checked: SettingsController.isLoggingEnabled - KeyNavigation.tab: openFolderButton + onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { SettingsController.isLoggingEnabled = checked @@ -86,131 +69,160 @@ disabled after 14 days, and all log files will be deleted.") } } - RowLayout { + DividerType {} + + LabelWithButtonType { Layout.fillWidth: true + Layout.topMargin: -8 - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: GC.isMobile() ? 0 : root.width / 3 - visible: !GC.isMobile() + text: qsTr("Clear logs") + leftImageSource: "qrc:/images/controls/trash.svg" + isSmallLeftImage: true - ImageButtonType { - id: openFolderButton - Layout.alignment: Qt.AlignHCenter + clickedFunction: function() { + var headerText = qsTr("Clear logs?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - implicitWidth: 56 - implicitHeight: 56 - - image: "qrc:/images/controls/folder-open.svg" - KeyNavigation.tab: saveButton - - onClicked: SettingsController.openLogsFolder() - Keys.onReturnPressed: openFolderButton.clicked() - Keys.onEnterPressed: openFolderButton.clicked() + var yesButtonFunction = function() { + PageController.showBusyIndicator(true) + SettingsController.clearLogs() + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) } - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true + var noButtonFunction = function() { - text: qsTr("Open folder with logs") - color: "#D7D8DB" } + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } + } + } - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) + model: logTypes + clip: true + reuseItems: true + snapMode: ListView.SnapOneItem - ImageButtonType { - id: saveButton - Layout.alignment: Qt.AlignHCenter + delegate: ColumnLayout { + id: delegateContent - implicitWidth: 56 - implicitHeight: 56 + width: listView.width - image: "qrc:/images/controls/save.svg" - KeyNavigation.tab: clearButton + enabled: isVisible - Keys.onReturnPressed: saveButton.clicked() - Keys.onEnterPressed: saveButton.clicked() - onClicked: { - 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")) - } - } - } + ListItemTitleType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true + text: title + } - text: qsTr("Save logs to file") - color: "#D7D8DB" - } - } + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 - ColumnLayout { - Layout.alignment: Qt.AlignBaseline - Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) + color: AmneziaStyle.color.mutedGray - ImageButtonType { - id: clearButton - Layout.alignment: Qt.AlignHCenter + text: description + } - implicitWidth: 56 - implicitHeight: 56 + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 - image: "qrc:/images/controls/delete.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) + visible: !GC.isMobile() - Keys.onReturnPressed: clearButton.clicked() - Keys.onEnterPressed: clearButton.clicked() - onClicked: function() { - var headerText = qsTr("Clear logs?") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") + text: qsTr("Open logs folder") + leftImageSource: "qrc:/images/controls/folder-open.svg" + isSmallLeftImage: true - var yesButtonFunction = function() { - PageController.showBusyIndicator(true) - SettingsController.clearLogs() - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + clickedFunction: openLogsHandler + } - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } - } + DividerType {} - CaptionTextType { - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: -8 + Layout.bottomMargin: -8 - text: qsTr("Clear logs") - color: "#D7D8DB" - } - } + text: qsTr("Export logs") + leftImageSource: "qrc:/images/controls/save.svg" + isSmallLeftImage: true + + clickedFunction: exportLogsHandler + } + + DividerType {} + } + } + + property list logTypes: [ + clientLogs, + serviceLogs + ] + + QtObject { + id: clientLogs + + readonly property string title: qsTr("Client logs") + readonly property string description: qsTr("AmneziaVPN logs") + readonly property bool isVisible: true + readonly property var openLogsHandler: function() { + SettingsController.openLogsFolder() + } + readonly property var exportLogsHandler: 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")) + } + } + } + + QtObject { + id: serviceLogs + + readonly property string title: qsTr("Service logs") + readonly property string description: qsTr("AmneziaVPN-service logs") + readonly property bool isVisible: !GC.isMobile() + readonly property var openLogsHandler: function() { + SettingsController.openServiceLogsFolder() + } + readonly property var exportLogsHandler: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN-service.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service", + true, + ".log") + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportServiceLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerData.qml b/client/ui/qml/Pages2/PageSettingsServerData.qml index ec41fb9f..977e669e 100644 --- a/client/ui/qml/Pages2/PageSettingsServerData.qml +++ b/client/ui/qml/Pages2/PageSettingsServerData.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ProtocolEnum 1.0 +import Style 1.0 import "../Controls2" import "../Controls2/TextTypes" @@ -35,16 +36,6 @@ PageType { PageController.showErrorMessage(message) } - function onRemoveProcessedServerFinished(finishedMessage) { - if (!ServersModel.getServersCount()) { - PageController.replaceStartPage() - } else { - PageController.goToStartPage() - PageController.goToPage(PageEnum.PageSettingsServersList) - } - PageController.showNotificationMessage(finishedMessage) - } - function onRebootProcessedServerFinished(finishedMessage) { PageController.showNotificationMessage(finishedMessage) } @@ -99,8 +90,6 @@ PageType { text: qsTr("Check the server for previously installed Amnezia services") descriptionText: qsTr("Add them to the application if they were not displayed") - KeyNavigation.tab: labelWithButton2 - clickedFunction: function() { PageController.showBusyIndicator(true) InstallController.scanServerForInstalledContainers() @@ -118,9 +107,7 @@ PageType { Layout.fillWidth: true text: qsTr("Reboot server") - textColor: "#EB5757" - - KeyNavigation.tab: labelWithButton3 + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { var headerText = qsTr("Do you want to reboot the server?") @@ -159,17 +146,7 @@ PageType { Layout.fillWidth: true text: qsTr("Remove server from application") - textColor: "#EB5757" - - Keys.onTabPressed: { - if (content.isServerWithWriteAccess) { - labelWithButton4.forceActiveFocus() - } else { - labelWithButton5.visible ? - labelWithButton5.forceActiveFocus() : - lastItemTabClickedSignal() - } - } + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { var headerText = qsTr("Do you want to remove the server from application?") @@ -207,11 +184,7 @@ PageType { Layout.fillWidth: true text: qsTr("Clear server from Amnezia software") - textColor: "#EB5757" - - Keys.onTabPressed: labelWithButton5.visible ? - labelWithButton5.forceActiveFocus() : - root.lastItemTabClickedSignal() + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { var headerText = qsTr("Do you want to clear server from Amnezia software?") @@ -246,13 +219,11 @@ PageType { LabelWithButtonType { id: labelWithButton5 - visible: ServersModel.getProcessedServerData("isServerFromApi") + visible: ServersModel.getProcessedServerData("isServerFromTelegramApi") Layout.fillWidth: true text: qsTr("Reset API config") - textColor: "#EB5757" - - Keys.onTabPressed: root.lastItemTabClickedSignal() + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { var headerText = qsTr("Do you want to reset API config?") @@ -284,7 +255,7 @@ PageType { } DividerType { - visible: ServersModel.getProcessedServerData("isServerFromApi") + visible: ServersModel.getProcessedServerData("isServerFromTelegramApi") } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 3b8968e6..d350ebef 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 import ProtocolProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -18,18 +19,32 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem + readonly property int pageSettingsServerProtocols: 0 + readonly property int pageSettingsServerServices: 1 + readonly property int pageSettingsServerData: 2 + + property var processedServer Connections { target: PageController function onGoToPageSettingsServerServices() { - tabBar.currentIndex = 1 + tabBar.setCurrentIndex(root.pageSettingsServerServices) + } + } + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) } } SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -37,133 +52,61 @@ PageType { value: true } ] - } - Item { - id: focusItem - KeyNavigation.tab: header + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } ColumnLayout { + objectName: "mainLayout" + anchors.fill: parent + anchors.topMargin: 20 - spacing: 16 + spacing: 4 - Repeater { - id: header - model: proxyServersModel + BackButtonType { + id: backButton + objectName: "backButton" + } - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() - } + HeaderType { + id: headerContent + objectName: "headerContent" - delegate: ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 10 - property alias focusItem: backButton + actionButtonImage: "qrc:/images/controls/edit-3.svg" - id: content - - Layout.topMargin: 20 - - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton - } - - HeaderType { - id: headerContent - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - actionButtonImage: "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar - - actionButtonFunction: function() { - serverNameEditDrawer.open() - } - } - - DrawerType2 { - id: serverNameEditDrawer - - parent: root - - anchors.fill: parent - expandedHeight: root.height * 0.35 - - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Connections { - target: serverNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - serverName.textField.forceActiveFocus() - } - } - - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } - - TextFieldWithHeaderType { - id: serverName - - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true - - KeyNavigation.tab: saveButton - } - - BasicButtonType { - id: saveButton - - Layout.fillWidth: true - - text: qsTr("Save") - KeyNavigation.tab: focusItem1 - - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } - - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } - } - } + headerText: root.processedServer.name + descriptionText: { + if (root.processedServer.isServerFromTelegramApi) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName } } + + actionButtonFunction: function() { + serverNameEditDrawer.openTriggered() + } + } + + RenameServerDrawer { + id: serverNameEditDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.35 + + serverNameText: root.processedServer.name } TabBar { @@ -171,83 +114,68 @@ PageType { Layout.fillWidth: true - currentIndex: (ServersModel.getProcessedServerData("isServerFromApi") - && !ServersModel.getProcessedServerData("hasInstalledContainers")) ? 2 : 0 + currentIndex: (ServersModel.getProcessedServerData("isServerFromTelegramApi") + && !ServersModel.getProcessedServerData("hasInstalledContainers")) ? + root.pageSettingsServerData : root.pageSettingsServerProtocols background: Rectangle { - color: "transparent" + color: AmneziaStyle.color.transparent } - activeFocusOnTab: true - onFocusChanged: { - if (activeFocus) { - protocolsTab.forceActiveFocus() - } - } TabButtonType { id: protocolsTab visible: protocolsPage.installedProtocolsCount width: protocolsPage.installedProtocolsCount ? undefined : 0 - isSelected: tabBar.currentIndex === 0 + isSelected: TabBar.tabBar.currentIndex === root.pageSettingsServerProtocols text: qsTr("Protocols") - KeyNavigation.tab: servicesTab - Keys.onReturnPressed: tabBar.currentIndex = 0 - Keys.onEnterPressed: tabBar.currentIndex = 0 + Keys.onReturnPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerProtocols) + Keys.onEnterPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerProtocols) } + TabButtonType { id: servicesTab visible: servicesPage.installedServicesCount width: servicesPage.installedServicesCount ? undefined : 0 - isSelected: tabBar.currentIndex === 1 + isSelected: TabBar.tabBar.currentIndex === root.pageSettingsServerServices text: qsTr("Services") - KeyNavigation.tab: dataTab - Keys.onReturnPressed: tabBar.currentIndex = 1 - Keys.onEnterPressed: tabBar.currentIndex = 1 + Keys.onReturnPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerServices) + Keys.onEnterPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerServices) } + TabButtonType { id: dataTab - isSelected: tabBar.currentIndex === 2 + isSelected: tabBar.currentIndex === root.pageSettingsServerData text: qsTr("Management") - Keys.onReturnPressed: tabBar.currentIndex = 2 - Keys.onEnterPressed: tabBar.currentIndex = 2 - KeyNavigation.tab: stackView.currentIndex === 0 ? - protocolsPage : - stackView.currentIndex === 1 ? - servicesPage : - dataPage + Keys.onReturnPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerData) + Keys.onEnterPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerData) } } StackLayout { - id: stackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + id: nestedStackView + + Layout.fillWidth: true currentIndex: tabBar.currentIndex PageSettingsServerProtocols { id: protocolsPage stackView: root.stackView - - onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } + PageSettingsServerServices { id: servicesPage stackView: root.stackView - - onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } + PageSettingsServerData { id: dataPage stackView: root.stackView - - onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } } - } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index a0c668be..ade94ebb 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -20,13 +21,6 @@ PageType { property bool isClearCacheVisible: ServersModel.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ContainersModel.getProcessedContainerIndex()) - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -38,7 +32,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: protocols } HeaderType { @@ -56,30 +49,36 @@ PageType { height: protocols.contentItem.height clip: true interactive: true - model: ProtocolsModel - property int currentFocusIndex: 0 - - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - protocols.itemAtIndex(currentFocusIndex).focusItem.forceActiveFocus() - } - } + property bool isFocusable: true Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - protocols.itemAtIndex(currentFocusIndex).focusItem.forceActiveFocus() - } else { - clearCacheButton.forceActiveFocus() - } + FocusController.nextKeyTabItem() } - delegate: Item { - property var focusItem: button.rightButton + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + model: ProtocolsModel + + delegate: Item { implicitWidth: protocols.width implicitHeight: delegateContent.implicitHeight @@ -88,13 +87,49 @@ PageType { anchors.fill: parent + property bool isClientSettingsVisible: protocolIndex === ProtocolEnum.WireGuard || protocolIndex === ProtocolEnum.Awg + property bool isServerSettingsVisible: ServersModel.isProcessedServerHasWriteAccess() + LabelWithButtonType { - id: button + id: clientSettings Layout.fillWidth: true - text: protocolName + text: protocolName + qsTr(" connection settings") rightImageSource: "qrc:/images/controls/chevron-right.svg" + visible: delegateContent.isClientSettingsVisible + + clickedFunction: function() { + if (isClientProtocolExists) { + switch (protocolIndex) { + case ProtocolEnum.WireGuard: WireGuardConfigModel.updateModel(ProtocolsModel.getConfig()); break; + case ProtocolEnum.Awg: AwgConfigModel.updateModel(ProtocolsModel.getConfig()); break; + } + PageController.goToPage(clientProtocolPage); + } else { + PageController.showNotificationMessage(qsTr("Click the \"connect\" button to create a connection configuration")) + } + } + + MouseArea { + anchors.fill: clientSettings + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + + DividerType { + visible: delegateContent.isClientSettingsVisible + } + + LabelWithButtonType { + id: serverSettings + + Layout.fillWidth: true + + text: protocolName + qsTr(" server settings") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + visible: delegateContent.isServerSettingsVisible clickedFunction: function() { switch (protocolIndex) { @@ -104,125 +139,131 @@ PageType { case ProtocolEnum.WireGuard: WireGuardConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Awg: AwgConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Xray: XrayConfigModel.updateModel(ProtocolsModel.getConfig()); break; + case ProtocolEnum.Sftp: SftpConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Ipsec: Ikev2ConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Socks5Proxy: Socks5ProxyConfigModel.updateModel(ProtocolsModel.getConfig()); break; } - PageController.goToPage(protocolPage); + PageController.goToPage(serverProtocolPage); } MouseArea { - anchors.fill: button + anchors.fill: serverSettings cursorShape: Qt.PointingHandCursor enabled: false } } - DividerType {} + DividerType { + visible: delegateContent.isServerSettingsVisible + } } } - } - LabelWithButtonType { - id: clearCacheButton + footer: ColumnLayout { + width: header.width - Layout.fillWidth: true + LabelWithButtonType { + id: clearCacheButton - visible: root.isClearCacheVisible - KeyNavigation.tab: removeButton + Layout.fillWidth: true - text: qsTr("Clear %1 profile").arg(ContainersModel.getProcessedContainerName()) + visible: root.isClearCacheVisible - clickedFunction: function() { - var headerText = qsTr("Clear %1 profile?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") + text: qsTr("Clear profile") - var yesButtonFunction = function() { - if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - var message = qsTr("Unable to clear %1 profile while there is an active connection").arg(ContainersModel.getProcessedContainerName()) - PageController.showNotificationMessage(message) - return + clickedFunction: function() { + var headerText = qsTr("Clear %1 profile?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The connection configuration will be deleted for this device only") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + var message = qsTr("Unable to clear %1 profile while there is an active connection").arg(ContainersModel.getProcessedContainerName()) + PageController.showNotificationMessage(message) + return + } + + PageController.showBusyIndicator(true) + InstallController.clearCachedProfile() + PageController.showBusyIndicator(false) + } + var noButtonFunction = function() { + // if (!GC.isMobile()) { + // focusItem.forceActiveFocus() + // } + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - PageController.showBusyIndicator(true) - InstallController.clearCachedProfile() - PageController.showBusyIndicator(false) - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() + MouseArea { + anchors.fill: clearCacheButton + cursorShape: Qt.PointingHandCursor + enabled: false } } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } + DividerType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - MouseArea { - anchors.fill: clearCacheButton - cursorShape: Qt.PointingHandCursor - enabled: false - } - } - - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - visible: root.isClearCacheVisible - } - - LabelWithButtonType { - id: removeButton - - Layout.fillWidth: true - - visible: ServersModel.isProcessedServerHasWriteAccess() - Keys.onTabPressed: lastItemTabClicked(focusItem) - - text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() - textColor: "#EB5757" - - clickedFunction: function() { - var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected - && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - PageController.showNotificationMessage(qsTr("Cannot remove active container")) - } else - { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeProcessedContainer() - } + visible: root.isClearCacheVisible } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() + + LabelWithButtonType { + id: removeButton + + Layout.fillWidth: true + + visible: ServersModel.isProcessedServerHasWriteAccess() + + text: qsTr("Remove ") + textColor: AmneziaStyle.color.vibrantRed + + clickedFunction: function() { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected + && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Cannot remove active container")) + } else + { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeProcessedContainer() + } + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false } } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } + DividerType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - MouseArea { - anchors.fill: removeButton - cursorShape: Qt.PointingHandCursor - enabled: false + visible: ServersModel.isProcessedServerHasWriteAccess() + } } } - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - visible: ServersModel.isProcessedServerHasWriteAccess() - } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml index 5ce8fdbb..ba72957e 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 import ContainersModelFilters 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -20,52 +21,45 @@ PageType { property var installedProtocolsCount - onFocusChanged: settingsContainersListView.forceActiveFocus() - signal lastItemTabClickedSignal() + function resetView() { + settingsContainersListView.positionViewAtBeginning() + } - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + SettingsContainersListView { + id: settingsContainersListView - Column { - id: content + anchors.fill: parent - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Connections { + target: ServersModel - SettingsContainersListView { - id: settingsContainersListView - - Connections { - target: ServersModel - - function onProcessedServerIndexChanged() { - settingsContainersListView.updateContainersModelFilters() - } - } - - function updateContainersModelFilters() { - if (ServersModel.isProcessedServerHasWriteAccess()) { - proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() - } else { - proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() - } - root.installedProtocolsCount = proxyContainersModel.count - } - - model: SortFilterProxyModel { - id: proxyContainersModel - sourceModel: ContainersModel - sorters: [ - RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } - ] - } - - Component.onCompleted: updateContainersModelFilters() + function onProcessedServerIndexChanged() { + settingsContainersListView.updateContainersModelFilters() } } + + function updateContainersModelFilters() { + if (ServersModel.isProcessedServerHasWriteAccess()) { + proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() + } else { + proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() + } + root.installedProtocolsCount = proxyContainersModel.count + } + + model: SortFilterProxyModel { + id: proxyContainersModel + sourceModel: ContainersModel + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder }, + RoleSorter { roleName: "installPageOrder"; sortOrder: Qt.AscendingOrder } + ] + } + + Component.onCompleted: { + settingsContainersListView.isFocusable = true + settingsContainersListView.interactive = true + updateContainersModelFilters() + } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerServices.qml b/client/ui/qml/Pages2/PageSettingsServerServices.qml index 72a4d3f7..a46d4051 100644 --- a/client/ui/qml/Pages2/PageSettingsServerServices.qml +++ b/client/ui/qml/Pages2/PageSettingsServerServices.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 import ContainersModelFilters 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -20,52 +21,40 @@ PageType { property var installedServicesCount - onFocusChanged: settingsContainersListView.forceActiveFocus() - signal lastItemTabClickedSignal() + SettingsContainersListView { + id: settingsContainersListView - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + anchors.fill: parent - Column { - id: content + Connections { + target: ServersModel - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - SettingsContainersListView { - id: settingsContainersListView - - Connections { - target: ServersModel - - function onProcessedServerIndexChanged() { - settingsContainersListView.updateContainersModelFilters() - } - } - - function updateContainersModelFilters() { - if (ServersModel.isProcessedServerHasWriteAccess()) { - proxyContainersModel.filters = ContainersModelFilters.getWriteAccessServicesListFilters() - } else { - proxyContainersModel.filters = ContainersModelFilters.getReadAccessServicesListFilters() - } - root.installedServicesCount = proxyContainersModel.count - } - - model: SortFilterProxyModel { - id: proxyContainersModel - sourceModel: ContainersModel - sorters: [ - RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } - ] - } - - Component.onCompleted: updateContainersModelFilters() + function onProcessedServerIndexChanged() { + settingsContainersListView.updateContainersModelFilters() } } + + function updateContainersModelFilters() { + if (ServersModel.isProcessedServerHasWriteAccess()) { + proxyContainersModel.filters = ContainersModelFilters.getWriteAccessServicesListFilters() + } else { + proxyContainersModel.filters = ContainersModelFilters.getReadAccessServicesListFilters() + } + root.installedServicesCount = proxyContainersModel.count + } + + model: SortFilterProxyModel { + id: proxyContainersModel + sourceModel: ContainersModel + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } + ] + } + + Component.onCompleted: { + settingsContainersListView.isFocusable = true + settingsContainersListView.interactive = true + updateContainersModelFilters() + } } } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index 596505a9..554b6cbb 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -7,6 +7,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -17,13 +18,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -35,7 +29,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: servers } HeaderType { @@ -47,91 +40,76 @@ PageType { } } - FlickableType { - id: fl + ListView { + id: servers + objectName: "servers" + + width: parent.width anchors.top: header.bottom anchors.topMargin: 16 - contentHeight: col.implicitHeight + anchors.left: parent.left + anchors.right: parent.right - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + height: 500 - ListView { - id: servers - width: parent.width - height: servers.contentItem.height + property bool isFocusable: true - model: ServersModel + model: ServersModel - clip: true - interactive: false + clip: true + reuseItems: true - activeFocusOnTab: true - focus: true - Keys.onTabPressed: { - if (currentIndex < servers.count - 1) { - servers.incrementCurrentIndex() - } else { - servers.currentIndex = 0 - focusItem.forceActiveFocus() - root.lastItemTabClicked() - } + delegate: Item { + implicitWidth: servers.width + implicitHeight: delegateContent.implicitHeight - fl.ensureVisible(this.currentItem) - } + ColumnLayout { + id: delegateContent - onVisibleChanged: { - if (visible) { - currentIndex = 0 - } - } + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - delegate: Item { - implicitWidth: servers.width - implicitHeight: delegateContent.implicitHeight + LabelWithButtonType { + id: server + Layout.fillWidth: true - onFocusChanged: { - if (focus) { - server.rightButton.forceActiveFocus() - } - } + text: name - ColumnLayout { - id: delegateContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - LabelWithButtonType { - id: server - Layout.fillWidth: true - - text: name - parentFlickable: fl - descriptionText: { - var servicesNameString = "" - var servicesName = ServersModel.getAllInstalledServicesName(index) - for (var i = 0; i < servicesName.length; i++) { - servicesNameString += servicesName[i] + " · " - } - - return servicesNameString + hostName - } - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - clickedFunction: function() { - ServersModel.processedIndex = index - PageController.goToPage(PageEnum.PageSettingsServerInfo) - } + descriptionText: { + var servicesNameString = "" + var servicesName = ServersModel.getAllInstalledServicesName(index) + for (var i = 0; i < servicesName.length; i++) { + servicesNameString += servicesName[i] + " · " } - DividerType {} + if (ServersModel.isServerFromApi(index)) { + return servicesNameString + serverDescription + } else { + return servicesNameString + hostName + } + } + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ServersModel.processedIndex = index + + if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { + PageController.showBusyIndicator(true) + let result = ApiSettingsController.getAccountInfo(false) + PageController.showBusyIndicator(false) + if (!result) { + return + } + + PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } else { + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } + + DividerType {} } } } diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index ce4c391f..f5978687 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -10,6 +10,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ProtocolEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -20,22 +21,15 @@ import "../Components" PageType { id: root - property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi") + property var isServerFromTelegramApi: ServersModel.getDefaultServerData("isServerFromTelegramApi") - defaultActiveFocusItem: searchField.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - property bool pageEnabled Component.onCompleted: { if (ConnectionController.isConnected) { PageController.showNotificationMessage(qsTr("Cannot change split tunneling settings during active connection")) root.pageEnabled = false - } else if (ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && isServerFromApi) { + } else if (ServersModel.isDefaultServerDefaultContainerHasSplitTunneling) { PageController.showNotificationMessage(qsTr("Default server does not support split tunneling function")) root.pageEnabled = false } else { @@ -98,7 +92,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: switcher } RowLayout { @@ -128,8 +121,6 @@ PageType { onToggled: { onToggledFunc() } Keys.onEnterPressed: { onToggledFunc() } Keys.onReturnPressed: { onToggledFunc() } - - KeyNavigation.tab: selector } } @@ -153,18 +144,18 @@ PageType { model: root.routeModesModel - currentIndex: getRouteModesModelIndex() + selectedIndex: getRouteModesModelIndex() clickedFunction: function() { selector.text = selectedText - selector.close() - if (SitesModel.routeMode !== root.routeModesModel[currentIndex].type) { - SitesModel.routeMode = root.routeModesModel[currentIndex].type + selector.closeTriggered() + if (SitesModel.routeMode !== root.routeModesModel[selectedIndex].type) { + SitesModel.routeMode = root.routeModesModel[selectedIndex].type } } Component.onCompleted: { - if (root.routeModesModel[currentIndex].type === SitesModel.routeMode) { + if (root.routeModesModel[selectedIndex].type === SitesModel.routeMode) { selector.text = selectedText } else { selector.text = root.routeModesModel[0].name @@ -174,132 +165,93 @@ PageType { Connections { target: SitesModel function onRouteModeChanged() { - currentIndex = getRouteModesModelIndex() + selectedIndex = getRouteModesModelIndex() } } } - - KeyNavigation.tab: { - return sites.count > 0 ? - sites : - searchField.textField - } } } - FlickableType { - id: fl + ListView { + id: listView + anchors.top: header.bottom anchors.topMargin: 16 - contentHeight: col.implicitHeight + addSiteButton.implicitHeight + addSiteButton.anchors.bottomMargin + addSiteButton.anchors.topMargin + anchors.bottom: addSiteButton.top + + width: parent.width enabled: root.pageEnabled - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + property bool isFocusable: true - ListView { - id: sites - width: parent.width - height: sites.contentItem.height - - model: SortFilterProxyModel { - id: proxySitesModel - sourceModel: SitesModel - filters: [ - AnyOf { - RegExpFilter { - roleName: "url" - pattern: ".*" + searchField.textField.text + ".*" - caseSensitivity: Qt.CaseInsensitive - } - RegExpFilter { - roleName: "ip" - pattern: ".*" + searchField.textField.text + ".*" - caseSensitivity: Qt.CaseInsensitive - } - } - ] - } - - clip: true - interactive: false - - activeFocusOnTab: true - focus: true - Keys.onTabPressed: { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - } else { - currentIndex = 0 - searchField.textField.forceActiveFocus() + model: SortFilterProxyModel { + id: proxySitesModel + sourceModel: SitesModel + filters: [ + AnyOf { + RegExpFilter { + roleName: "url" + pattern: ".*" + searchField.textField.text + ".*" + caseSensitivity: Qt.CaseInsensitive + } + RegExpFilter { + roleName: "ip" + pattern: ".*" + searchField.textField.text + ".*" + caseSensitivity: Qt.CaseInsensitive } - - fl.ensureVisible(currentItem) } + ] + } - delegate: Item { - implicitWidth: sites.width - implicitHeight: delegateContent.implicitHeight + clip: true - onActiveFocusChanged: { - if (activeFocus) { + reuseItems: true + + delegate: ColumnLayout { + id: delegateContent + + width: listView.width + + LabelWithButtonType { + id: site + Layout.fillWidth: true + + text: url + descriptionText: ip + rightImageSource: "qrc:/images/controls/trash.svg" + rightImageColor: AmneziaStyle.color.paleGray + + clickedFunction: function() { + var headerText = qsTr("Remove ") + url + "?" + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + SitesController.removeSite(proxySitesModel.mapToSource(index)) + if (!GC.isMobile()) { + site.rightButton.forceActiveFocus() + } + } + var noButtonFunction = function() { + if (!GC.isMobile()) { site.rightButton.forceActiveFocus() } } - ColumnLayout { - id: delegateContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - LabelWithButtonType { - id: site - Layout.fillWidth: true - - text: url - descriptionText: ip - rightImageSource: "qrc:/images/controls/trash.svg" - rightImageColor: "#D7D8DB" - - clickedFunction: function() { - var headerText = qsTr("Remove ") + url + "?" - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - SitesController.removeSite(proxySitesModel.mapToSource(index)) - if (!GC.isMobile()) { - site.rightButton.forceActiveFocus() - } - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - site.rightButton.forceActiveFocus() - } - } - - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } - } - - DividerType {} - } + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } + DividerType {} } } + Rectangle { anchors.fill: addSiteButton anchors.bottomMargin: -24 - color: "#0E0E11" + color: AmneziaStyle.color.midnightBlack opacity: 0.8 } @@ -322,14 +274,13 @@ PageType { Layout.fillWidth: true rightButtonClickedOnEnter: true - textFieldPlaceholderText: qsTr("website or IP") + textField.placeholderText: qsTr("website or IP") buttonImageSource: "qrc:/images/controls/plus.svg" - KeyNavigation.tab: GC.isMobile() ? focusItem : addSiteButtonImage clickedFunc: function() { PageController.showBusyIndicator(true) - SitesController.addSite(textFieldText) - textFieldText = "" + SitesController.addSite(textField.text) + textField.text = "" PageController.showBusyIndicator(false) } } @@ -340,16 +291,14 @@ PageType { implicitHeight: 56 image: "qrc:/images/controls/more-vertical.svg" - imageColor: "#D7D8DB" + imageColor: AmneziaStyle.color.paleGray onClicked: function () { - moreActionsDrawer.open() + moreActionsDrawer.openTriggered() } Keys.onReturnPressed: addSiteButtonImage.clicked() Keys.onEnterPressed: addSiteButtonImage.clicked() - - Keys.onTabPressed: lastItemTabClicked(focusItem) } } @@ -359,38 +308,13 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.4375 - onClosed: { - if (root.defaultActiveFocusItem && !GC.isMobile()) { - root.defaultActiveFocusItem.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: moreActionsDrawerContent anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - Connections { - target: moreActionsDrawer - - function onOpened() { - focusItem1.forceActiveFocus() - } - - function onActiveFocusChanged() { - if (!GC.isMobile()) { - focusItem1.forceActiveFocus() - } - } - } - - Item { - id: focusItem1 - KeyNavigation.tab: importSitesButton.rightButton - } - Header2Type { Layout.fillWidth: true Layout.margins: 16 @@ -406,10 +330,8 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - importSitesDrawer.open() + importSitesDrawer.openTriggered() } - - KeyNavigation.tab: exportSitesButton } DividerType {} @@ -419,8 +341,6 @@ PageType { Layout.fillWidth: true text: qsTr("Save site list") - KeyNavigation.tab: focusItem1 - clickedFunction: function() { var fileName = "" if (GC.isMobile()) { @@ -435,7 +355,7 @@ PageType { if (fileName !== "") { PageController.showBusyIndicator(true) SitesController.exportSites(fileName) - moreActionsDrawer.close() + moreActionsDrawer.closeTriggered() PageController.showBusyIndicator(false) } } @@ -451,28 +371,9 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.4375 - onClosed: { - if (!GC.isMobile()) { - moreActionsDrawer.forceActiveFocus() - } - } - - expandedContent: Item { + expandedStateContent: Item { implicitHeight: importSitesDrawer.expandedHeight - Connections { - target: importSitesDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem2.forceActiveFocus() - } - } - - Item { - id: focusItem2 - KeyNavigation.tab: importSitesDrawerBackButton - } - BackButtonType { id: importSitesDrawerBackButton @@ -481,10 +382,8 @@ PageType { anchors.right: parent.right anchors.topMargin: 16 - KeyNavigation.tab: importSitesButton2 - backButtonFunction: function() { - importSitesDrawer.close() + importSitesDrawer.closeTriggered() } } @@ -515,7 +414,6 @@ PageType { Layout.fillWidth: true text: qsTr("Replace site list") - KeyNavigation.tab: importSitesButton3 clickedFunction: function() { var fileName = SystemController.getFileName(qsTr("Open sites file"), @@ -532,7 +430,6 @@ PageType { id: importSitesButton3 Layout.fillWidth: true text: qsTr("Add imported sites to existing ones") - KeyNavigation.tab: focusItem2 clickedFunction: function() { var fileName = SystemController.getFileName(qsTr("Open sites file"), @@ -547,8 +444,8 @@ PageType { PageController.showBusyIndicator(true) SitesController.importSites(fileName, replaceExistingSites) PageController.showBusyIndicator(false) - importSitesDrawer.close() - moreActionsDrawer.close() + importSitesDrawer.closeTriggered() + moreActionsDrawer.closeTriggered() } DividerType {} diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml new file mode 100644 index 00000000..134e73b6 --- /dev/null +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -0,0 +1,146 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + FlickableType { + id: fl + anchors.top: parent.top + anchors.bottom: parent.bottom + contentHeight: content.height + continueButton.implicitHeight + continueButton.anchors.bottomMargin + continueButton.anchors.topMargin + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 0 + + BackButtonType { + id: backButton + Layout.topMargin: 20 + } + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 32 + + headerText: ApiServicesModel.getSelectedServiceData("name") + descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") + } + + LabelWithImageType { + Layout.fillWidth: true + Layout.margins: 16 + + imageSource: "qrc:/images/controls/map-pin.svg" + leftText: qsTr("For the region") + rightText: ApiServicesModel.getSelectedServiceData("region") + } + + LabelWithImageType { + Layout.fillWidth: true + Layout.margins: 16 + + imageSource: "qrc:/images/controls/tag.svg" + leftText: qsTr("Price") + rightText: ApiServicesModel.getSelectedServiceData("price") + } + + LabelWithImageType { + Layout.fillWidth: true + Layout.margins: 16 + + imageSource: "qrc:/images/controls/history.svg" + leftText: qsTr("Work period") + rightText: ApiServicesModel.getSelectedServiceData("timeLimit") + + visible: rightText !== "" + } + + LabelWithImageType { + Layout.fillWidth: true + Layout.margins: 16 + + imageSource: "qrc:/images/controls/gauge.svg" + leftText: qsTr("Speed") + rightText: ApiServicesModel.getSelectedServiceData("speed") + } + + LabelWithImageType { + Layout.fillWidth: true + Layout.margins: 16 + + imageSource: "qrc:/images/controls/info.svg" + leftText: qsTr("Features") + rightText: "" + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + onLinkActivated: function(link) { + Qt.openUrlExternally(link) + } + textFormat: Text.RichText + text: { + var text = ApiServicesModel.getSelectedServiceData("features") + return text.replace("%1", LanguageModel.getCurrentSiteUrl()) + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } + } + + BasicButtonType { + id: continueButton + + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + + anchors.topMargin: 32 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.bottomMargin: 32 + + text: qsTr("Connect") + + clickedFunc: function() { + var endpoint = ApiServicesModel.getStoreEndpoint() + if (endpoint !== undefined && endpoint !== "") { + Qt.openUrlExternally(endpoint) + PageController.closePage() + PageController.closePage() + } else { + PageController.showBusyIndicator(true) + ApiConfigsController.importServiceFromGateway() + PageController.showBusyIndicator(false) + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml new file mode 100644 index 00000000..c3e3edbc --- /dev/null +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -0,0 +1,100 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" + +PageType { + id: root + + ColumnLayout { + id: header + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 0 + + BackButtonType { + id: backButton + Layout.topMargin: 20 + } + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + + headerText: qsTr("VPN by Amnezia") + descriptionText: qsTr("Choose a VPN service that suits your needs.") + } + } + + ListView { + id: servicesListView + + anchors.top: header.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 16 + spacing: 0 + + property bool isFocusable: true + + clip: true + reuseItems: true + + model: ApiServicesModel + + ScrollBar.vertical: ScrollBarType {} + + delegate: Item { + implicitWidth: servicesListView.width + implicitHeight: delegateContent.implicitHeight + + enabled: isServiceAvailable + + ColumnLayout { + id: delegateContent + + anchors.fill: parent + + CardWithIconsType { + id: card + + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + + headerText: name + bodyText: cardDescription + footerText: price + + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + onClicked: { + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + } + } + + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index f7b8949b..38a1da52 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QtQuick.Dialogs import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -17,122 +18,323 @@ PageType { target: ImportController function onQrDecodingFinished() { - PageController.closePage() + if (Qt.platform.os === "ios") { + PageController.closePage() + } PageController.goToPage(PageEnum.PageSetupWizardViewConfig) } } - defaultActiveFocusItem: focusItem + ListView { + id: listView - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.height + anchors.fill: parent - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ScrollBar.vertical: ScrollBarType {} - spacing: 0 + model: variants - Item { - id: focusItem - KeyNavigation.tab: backButton - } + clip: true - BackButtonType { - id: backButton - Layout.topMargin: 20 - KeyNavigation.tab: fileButton.rightButton - } + reuseItems: true + + header: ColumnLayout { + width: listView.width HeaderType { + id: moreButton + + property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() + Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: 24 Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Server connection") - descriptionText: qsTr("Do not use connection codes from untrusted sources, as they may be created to intercept your data.") - } + headerText: qsTr("Connection") - Header2TextType { - Layout.fillWidth: true - Layout.topMargin: 48 - Layout.rightMargin: 16 - Layout.leftMargin: 16 + actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : "" + actionButtonFunction: function() { + moreActionsDrawer.openTriggered() + } - text: qsTr("What do you have?") - } + DrawerType2 { + id: moreActionsDrawer - LabelWithButtonType { - id: fileButton - Layout.fillWidth: true - Layout.topMargin: 16 + parent: root - text: !ServersModel.getServersCount() ? qsTr("File with connection settings or backup") : qsTr("File with connection settings") - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/folder-open.svg" + anchors.fill: parent + expandedHeight: root.height * 0.5 - KeyNavigation.tab: qrButton.visible ? qrButton.rightButton : textButton.rightButton + expandedStateContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 - clickedFunction: function() { - var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.json *.backup)" : - "Config files (*.vpn *.ovpn *.conf *.json)" - var fileName = SystemController.getFileName(qsTr("Open config file"), nameFilter) - if (fileName !== "") { - if (ImportController.extractConfigFromFile(fileName)) { - PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: qsTr("Settings") + } + + SwitcherType { + id: switcher + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Enable logs") + + visible: PageController.isStartPageVisible() + checked: SettingsController.isLoggingEnabled + onCheckedChanged: { + if (checked !== SettingsController.isLoggingEnabled) { + SettingsController.isLoggingEnabled = checked + } + } + } + + LabelWithButtonType { + id: supportUuid + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Support tag") + descriptionText: SettingsController.getInstallationUuid() + + descriptionOnTop: true + + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + + visible: SettingsController.getInstallationUuid() !== "" + clickedFunction: function() { + GC.copyToClipBoard(descriptionText) + PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } + } } } } } - DividerType {} + ParagraphTextType { + objectName: "insertKeyLabel" - LabelWithButtonType { - id: qrButton Layout.fillWidth: true - visible: SettingsController.isCameraPresent() + Layout.topMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 24 - text: qsTr("QR code") - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/qr-code.svg" + text: qsTr("Insert the key, add a configuration file or scan the QR-code") + } - KeyNavigation.tab: textButton.rightButton + TextFieldWithHeaderType { + id: textKey - clickedFunction: function() { - ImportController.startDecodingQr() - if (Qt.platform.os === "ios") { - PageController.goToPage(PageEnum.PageSetupWizardQrReader) + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Insert key") + buttonText: qsTr("Insert") + + clickedFunc: function() { + textField.text = "" + textField.paste() + } + } + + BasicButtonType { + id: continueButton + + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + visible: textKey.textField.text !== "" + + text: qsTr("Continue") + + clickedFunc: function() { + if (ImportController.extractConfigFromData(textKey.textField.text)) { + PageController.goToPage(PageEnum.PageSetupWizardViewConfig) } } } - DividerType { - visible: SettingsController.isCameraPresent() - } - - LabelWithButtonType { - id: textButton + ParagraphTextType { Layout.fillWidth: true + Layout.topMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 24 + + color: AmneziaStyle.color.charcoalGray + text: qsTr("Other connection options") + } + } + + delegate: ColumnLayout { + width: listView.width + + CardWithIconsType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + + visible: isVisible + + headerText: title + bodyText: description - text: qsTr("Key as text") rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/text-cursor.svg" + leftImageSource: imageSource - Keys.onTabPressed: lastItemTabClicked(focusItem) + onClicked: { handler() } + } + } - clickedFunction: function() { - PageController.goToPage(PageEnum.PageSetupWizardTextKey) + footer: ColumnLayout { + width: listView.width + + BasicButtonType { + id: siteLink2 + Layout.topMargin: 24 + Layout.bottomMargin: 16 + Layout.alignment: Qt.AlignHCenter + implicitHeight: 32 + + visible: Qt.platform.os !== "ios" + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot + + text: qsTr("Site Amnezia") + + rightImageSource: "qrc:/images/controls/external-link.svg" + + clickedFunc: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) } } + } + } - DividerType {} + property list variants: [ + amneziaVpn, + selfHostVpn, + backupRestore, + fileOpen, + qrScan, + siteLink + ] + + QtObject { + id: amneziaVpn + + property string title: qsTr("VPN by Amnezia") + property string description: qsTr("Connect to classic paid and free VPN services from Amnezia") + property string imageSource: "qrc:/images/controls/amnezia.svg" + property bool isVisible: true + property var handler: function() { + PageController.showBusyIndicator(true) + var result = ApiConfigsController.fillAvailableServices() + PageController.showBusyIndicator(false) + if (result) { + PageController.goToPage(PageEnum.PageSetupWizardApiServicesList) + } + } + } + + QtObject { + id: selfHostVpn + + property string title: qsTr("Self-hosted VPN") + property string description: qsTr("Configure Amnezia VPN on your own server") + property string imageSource: "qrc:/images/controls/server.svg" + property bool isVisible: true + property var handler: function() { + PageController.goToPage(PageEnum.PageSetupWizardCredentials) + } + } + + QtObject { + id: backupRestore + + property string title: qsTr("Restore from backup") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/archive-restore.svg" + property bool isVisible: PageController.isStartPageVisible() + property var handler: function() { + var filePath = SystemController.getFileName(qsTr("Open backup file"), + qsTr("Backup files (*.backup)")) + if (filePath !== "") { + PageController.showBusyIndicator(true) + SettingsController.restoreAppConfig(filePath) + PageController.showBusyIndicator(false) + } + } + } + + QtObject { + id: fileOpen + + property string title: qsTr("File with connection settings") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/folder-search-2.svg" + property bool isVisible: true + property var handler: function() { + var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.json *.backup)" : + "Config files (*.vpn *.ovpn *.conf *.json)" + var fileName = SystemController.getFileName(qsTr("Open config file"), nameFilter) + if (fileName !== "") { + if (ImportController.extractConfigFromFile(fileName)) { + PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + } + } + } + } + + QtObject { + id: qrScan + + property string title: qsTr("QR code") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/scan-line.svg" + property bool isVisible: SettingsController.isCameraPresent() + property var handler: function() { + ImportController.startDecodingQr() + if (Qt.platform.os === "ios") { + PageController.goToPage(PageEnum.PageSetupWizardQrReader) + } + } + } + + QtObject { + id: siteLink + + property string title: qsTr("I have nothing") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/help-circle.svg" + property bool isVisible: PageController.isStartPageVisible() && Qt.platform.os !== "ios" + property var handler: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index ea522363..ca7e3a7c 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -12,13 +13,6 @@ import "../Controls2/TextTypes" PageType { id: root - defaultActiveFocusItem: hostname.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -27,103 +21,123 @@ PageType { anchors.right: parent.right anchors.topMargin: 20 - KeyNavigation.tab: hostname.textField + onFocusChanged: { + if (this.activeFocus) { + listView.positionViewAtBeginning() + } + } } - FlickableType { - id: fl + ListView { + id: listView anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.height + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: 16 - anchors.leftMargin: 16 + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } - spacing: 16 + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + ScrollBar.vertical: ScrollBarType {} + + header: ColumnLayout { + width: listView.width HeaderType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 headerText: qsTr("Configure your server") } + } + + model: inputFields + spacing: 16 + clip: true + reuseItems: true + + delegate: ColumnLayout { + width: listView.width TextFieldWithHeaderType { - id: hostname + id: delegate Layout.fillWidth: true - headerText: qsTr("Server IP address [:port]") - textFieldPlaceholderText: qsTr("255.255.255.255:22") - textField.validator: RegularExpressionValidator { - regularExpression: InstallController.ipAddressPortRegExp() + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: title + textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal + textField.placeholderText: placeholderContent + textField.text: textField.text + + rightButtonClickedOnEnter: true + + clickedFunc: function () { + clickedHandler() } textField.onFocusChanged: { textField.text = textField.text.replace(/^\s+|\s+$/g, '') } - KeyNavigation.tab: username.textField - } - - TextFieldWithHeaderType { - id: username - - Layout.fillWidth: true - headerText: qsTr("SSH Username") - textFieldPlaceholderText: "root" - - textField.onFocusChanged: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') + textField.onTextChanged: { + if (hideContent) { + buttonImageSource = textField.text !== "" ? (hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + } } - - KeyNavigation.tab: secretData.textField } + } - TextFieldWithHeaderType { - id: secretData - - property bool hidePassword: true - - Layout.fillWidth: true - headerText: qsTr("Password or SSH private key") - textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal - buttonImageSource: textFieldText !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") - : "" - - clickedFunc: function() { - hidePassword = !hidePassword - } - - textField.onFocusChanged: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - } - - KeyNavigation.tab: continueButton - } + footer: ColumnLayout { + width: listView.width BasicButtonType { id: continueButton Layout.fillWidth: true - Layout.topMargin: 24 + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 text: qsTr("Continue") - Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { - forceActiveFocus() - if (!isCredentialsFilled()) { + if (!root.isCredentialsFilled()) { return } InstallController.setShouldCreateServer(true) - InstallController.setProcessedServerCredentials(hostname.textField.text, username.textField.text, secretData.textField.text) + var _hostname = listView.itemAtIndex(vars.hostnameIndex).children[0].textField.text + var _username = listView.itemAtIndex(vars.usernameIndex).children[0].textField.text + var _secretData = listView.itemAtIndex(vars.secretDataIndex).children[0].textField.text + + InstallController.setProcessedServerCredentials(_hostname, _username, _secretData) PageController.showBusyIndicator(true) var isConnectionOpened = InstallController.checkSshConnection() @@ -138,31 +152,101 @@ PageType { LabelTextType { Layout.fillWidth: true - Layout.topMargin: 12 + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 text: qsTr("All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties") } + + CardWithIconsType { + id: siteLink + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + headerText: qsTr("How to run your VPN server") + bodyText: qsTr("Where to get connection data, step-by-step instructions for buying a VPS") + + rightImageSource: "qrc:/images/controls/chevron-right.svg" + leftImageSource: "qrc:/images/controls/help-circle.svg" + + onClicked: { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/starter-guide") + } + } } } function isCredentialsFilled() { var hasEmptyField = false - if (hostname.textFieldText === "") { - hostname.errorText = qsTr("Ip address cannot be empty") + var hostnameItem = listView.itemAtIndex(vars.hostnameIndex).children[0] + if (hostnameItem.textField.text === "") { + hostnameItem.errorText = qsTr("Ip address cannot be empty") hasEmptyField = true - } else if (!hostname.textField.acceptableInput) { - hostname.errorText = qsTr("Enter the address in the format 255.255.255.255:88") + } else if (!hostnameItem.textField.acceptableInput) { + hostnameItem.errorText = qsTr("Enter the address in the format 255.255.255.255:88") } - if (username.textFieldText === "") { - username.errorText = qsTr("Login cannot be empty") + var usernameItem = listView.itemAtIndex(vars.usernameIndex).children[0] + if (usernameItem.textField.text === "") { + usernameItem.errorText = qsTr("Login cannot be empty") hasEmptyField = true } - if (secretData.textFieldText === "") { - secretData.errorText = qsTr("Password/private key cannot be empty") + + var secretDataItem = listView.itemAtIndex(vars.secretDataIndex).children[0] + if (secretDataItem.textField.text === "") { + secretDataItem.errorText = qsTr("Password/private key cannot be empty") hasEmptyField = true } + return !hasEmptyField } + + property list inputFields: [ + hostnameObject, + usernameObject, + secretDataObject + ] + + QtObject { + id: hostnameObject + + property string title: qsTr("Server IP address [:port]") + readonly property string placeholderContent: qsTr("255.255.255.255:22") + property bool hideContent: false + readonly property var clickedHandler: undefined + } + + QtObject { + id: usernameObject + + property string title: qsTr("SSH Username") + readonly property string placeholderContent: "root" + property bool hideContent: false + readonly property var clickedHandler: undefined + } + + QtObject { + id: secretDataObject + + property string title: qsTr("Password or SSH private key") + readonly property string placeholderContent: "" + property bool hideContent: true + readonly property var clickedHandler: function() { + hideContent = !hideContent + } + } + + QtObject { + id: vars + + readonly property int hostnameIndex: 0 + readonly property int usernameIndex: 1 + readonly property int secretDataIndex: 2 + } } diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index acc902f3..353eeb32 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -7,6 +7,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 import ProtocolProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -16,7 +17,6 @@ PageType { id: root property bool isEasySetup: true - defaultActiveFocusItem: focusItem SortFilterProxyModel { id: proxyContainersModel @@ -33,14 +33,6 @@ PageType { } } - Item { - id: focusItem - implicitWidth: 1 - implicitHeight: 54 - - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -48,8 +40,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: continueButton } FlickableType { @@ -75,7 +65,7 @@ PageType { implicitWidth: parent.width headerTextMaximumLineCount: 10 - headerText: qsTr("What is the level of internet control in your region?") + headerText: qsTr("Choose Installation Type") } ButtonGroup { @@ -97,6 +87,8 @@ PageType { property int containerDefaultPort property int containerDefaultTransportProto + property bool isFocusable: true + delegate: Item { implicitWidth: containers.width implicitHeight: delegateContent.implicitHeight @@ -147,7 +139,8 @@ PageType { CardType { implicitWidth: parent.width - headerText: qsTr("Choose a VPN protocol") + headerText: qsTr("Manual") + bodyText: qsTr("Choose a VPN protocol") ButtonGroup.group: buttonGroup @@ -162,7 +155,7 @@ PageType { implicitWidth: parent.width text: qsTr("Continue") - KeyNavigation.tab: setupLaterButton + parentFlickable: fl clickedFunc: function() { @@ -185,11 +178,11 @@ PageType { anchors.topMargin: 8 anchors.bottomMargin: 24 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 Keys.onTabPressed: lastItemTabClicked(focusItem) diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index 632bb727..1128761d 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import SortFilterProxyModel 0.2 import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -46,11 +47,7 @@ PageType { ServersModel.processedIndex = ServersModel.defaultIndex } - PageController.goToStartPage() - if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSetupWizardStart)) { - PageController.replaceStartPage() - } - + PageController.goToPageHome() PageController.showNotificationMessage(finishedMessage) } diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index f27873c6..50d1ea81 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -7,6 +7,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 import ProtocolProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -48,6 +49,32 @@ PageType { interactive: false model: proxyContainersModel + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + delegate: Item { implicitWidth: processedContainerListView.width implicitHeight: (delegateContent.implicitHeight > root.height) ? delegateContent.implicitHeight : root.height @@ -61,19 +88,12 @@ PageType { anchors.rightMargin: 16 anchors.leftMargin: 16 - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton Layout.topMargin: 20 Layout.rightMargin: -16 Layout.leftMargin: -16 - - KeyNavigation.tab: showDetailsButton } HeaderType { @@ -93,52 +113,29 @@ PageType { implicitHeight: 32 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#FBB26A" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot text: qsTr("More detailed") KeyNavigation.tab: transportProtoSelector clickedFunc: function() { - showDetailsDrawer.open() + showDetailsDrawer.openTriggered() } } DrawerType2 { id: showDetailsDrawer parent: root - onClosed: { - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } - } anchors.fill: parent expandedHeight: parent.height * 0.9 - expandedContent: Item { - Connections { - target: showDetailsDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem2.forceActiveFocus() - } - } - + expandedStateContent: Item { implicitHeight: showDetailsDrawer.expandedHeight - Item { - id: focusItem2 - KeyNavigation.tab: showDetailsBackButton - onFocusChanged: { - if (focusItem2.activeFocus) { - fl.contentY = 0 - } - } - } - BackButtonType { id: showDetailsBackButton @@ -147,10 +144,8 @@ PageType { anchors.right: parent.right anchors.topMargin: 16 - KeyNavigation.tab: showDetailsCloseButton - backButtonFunction: function() { - showDetailsDrawer.close() + showDetailsDrawer.closeTriggered() } } @@ -194,7 +189,7 @@ PageType { Rectangle { Layout.fillHeight: true - color: "transparent" + color: AmneziaStyle.color.transparent } BasicButtonType { @@ -204,10 +199,9 @@ PageType { parentFlickable: fl text: qsTr("Close") - Keys.onTabPressed: lastItemTabClicked(focusItem2) clickedFunc: function() { - showDetailsDrawer.close() + showDetailsDrawer.closeTriggered() } } } @@ -228,8 +222,6 @@ PageType { Layout.fillWidth: true rootWidth: root.width - - KeyNavigation.tab: (port.visible && port.enabled) ? port.textField : installButton } TextFieldWithHeaderType { @@ -241,13 +233,11 @@ PageType { headerText: qsTr("Port") textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } - - KeyNavigation.tab: installButton } Rectangle { Layout.fillHeight: true - color: "transparent" + color: AmneziaStyle.color.transparent } BasicButtonType { @@ -258,16 +248,16 @@ PageType { text: qsTr("Install") - Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { - if (!port.textField.acceptableInput) { + if (!port.textField.acceptableInput && + ContainerProps.containerTypeToString(dockerContainer) !== "torwebsite" && + ContainerProps.containerTypeToString(dockerContainer) !== "ikev2") { port.errorText = qsTr("The port must be in the range of 1 to 65535") return } PageController.goToPage(PageEnum.PageSetupWizardInstalling); - InstallController.install(dockerContainer, port.textFieldText, transportProtoSelector.currentIndex) + InstallController.install(dockerContainer, port.textField.text, transportProtoSelector.currentIndex) } } @@ -277,7 +267,7 @@ PageType { if (ProtocolProps.defaultPort(defaultContainerProto) < 0) { port.visible = false } else { - port.textFieldText = ProtocolProps.getPortForInstall(defaultContainerProto) + port.textField.text = ProtocolProps.getPortForInstall(defaultContainerProto) } transportProtoSelector.currentIndex = ProtocolProps.defaultTransportProto(defaultContainerProto) @@ -285,11 +275,6 @@ PageType { var protocolSelectorVisible = ProtocolProps.defaultTransportProtoChangeable(defaultContainerProto) transportProtoSelector.visible = protocolSelectorVisible transportProtoHeader.visible = protocolSelectorVisible - - if (port.visible && port.enabled) - defaultActiveFocusItem = port.textField - else - defaultActiveFocusItem = focusItem } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml index cb922b4c..6b6b6038 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml @@ -6,6 +6,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ProtocolEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -14,13 +15,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - SortFilterProxyModel { id: proxyContainersModel sourceModel: ContainersModel @@ -34,128 +28,72 @@ PageType { value: true } ] + sorters: RoleSorter { + roleName: "installPageOrder" + sortOrder: Qt.AscendingOrder + } } - ColumnLayout { - id: backButtonLayout + BackButtonType { + id: backButton anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - BackButtonType { - id: backButton - KeyNavigation.tab: containers - } } - FlickableType { - id: fl - anchors.top: backButtonLayout.bottom + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + content.anchors.topMargin + content.anchors.bottomMargin + anchors.right: parent.right + anchors.left: parent.left - Column { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 20 + ScrollBar.vertical: ScrollBarType {} - Item { - width: parent.width - height: header.implicitHeight + header: ColumnLayout { + width: listView.width - HeaderType { - id: header + HeaderType { + id: header - anchors.fill: parent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 - anchors.leftMargin: 16 - anchors.rightMargin: 16 + headerText: qsTr("VPN protocol") + descriptionText: qsTr("Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP.") + } + } - width: parent.width + model: proxyContainersModel + clip: true + spacing: 0 + reuseItems: true + snapMode: ListView.SnapToItem - headerText: qsTr("VPN protocol") - descriptionText: qsTr("Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP.") + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + + text: name + descriptionText: description + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(index)) + PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) } } - ListView { - id: containers - width: parent.width - height: containers.contentItem.height - // currentIndex: -1 - clip: true - interactive: false - model: proxyContainersModel - - function ensureCurrentItemVisible() { - if (currentIndex >= 0) { - if (currentItem.y < fl.contentY) { - fl.contentY = currentItem.y - } else if (currentItem.y + currentItem.height + header.height > fl.contentY + fl.height) { - fl.contentY = currentItem.y + currentItem.height + header.height - fl.height + 40 // 40 is a bottom margin - } - } - } - - activeFocusOnTab: true - Keys.onTabPressed: { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - } else { - this.currentIndex = 0 - focusItem.forceActiveFocus() - } - - ensureCurrentItemVisible() - } - - onVisibleChanged: { - if (visible) { - currentIndex = 0 - } - } - - delegate: Item { - implicitWidth: containers.width - implicitHeight: delegateContent.implicitHeight - - onActiveFocusChanged: { - if (activeFocus) { - container.rightButton.forceActiveFocus() - } - } - - ColumnLayout { - id: delegateContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - LabelWithButtonType { - id: container - Layout.fillWidth: true - - text: name - descriptionText: description - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - clickedFunction: function() { - ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(index)) - PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) - } - } - - DividerType {} - } - } - } + DividerType {} } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardQrReader.qml b/client/ui/qml/Pages2/PageSetupWizardQrReader.qml index 1fa71592..83ead6f2 100644 --- a/client/ui/qml/Pages2/PageSetupWizardQrReader.qml +++ b/client/ui/qml/Pages2/PageSetupWizardQrReader.qml @@ -5,6 +5,7 @@ import QtQuick.Dialogs import PageEnum 1.0 import QRCodeReader 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -60,7 +61,7 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 34 - color: "transparent" + color: AmneziaStyle.color.transparent //radius: 16 QRCodeReader { diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index 4473b730..2d6790ba 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -13,183 +14,35 @@ import "../Components" PageType { id: root - property bool isControlsDisabled: false + ColumnLayout { + id: content - defaultActiveFocusItem: focusItem + anchors.fill: parent + spacing: 0 - Connections { - target: PageController + Image { + id: image + source: "qrc:/images/amneziaBigLogo.png" - function onGoToPageViewConfig() { - PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.topMargin: 32 + Layout.preferredWidth: 360 + Layout.preferredHeight: 287 } - function onClosePage() { - if (stackView.depth <= 1) { - PageController.hideWindow() - return + BasicButtonType { + id: startButton + Layout.fillWidth: true + Layout.bottomMargin: 48 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.alignment: Qt.AlignBottom + + text: qsTr("Let's get started") + + clickedFunc: function() { + PageController.goToPage(PageEnum.PageSetupWizardConfigSource) } - stackView.pop() - } - - function onGoToPage(page, slide) { - var pagePath = PageController.getPagePath(page) - if (slide) { - stackView.push(pagePath, { "objectName" : pagePath }, StackView.PushTransition) - } else { - stackView.push(pagePath, { "objectName" : pagePath }, StackView.Immediate) - } - } - - function onGoToStartPage() { - while (stackView.depth > 1) { - stackView.pop() - } - } - - function onDisableControls(disabled) { - isControlsDisabled = disabled - } - - function onDisableTabBar(disabled) { - isControlsDisabled = disabled - } - - function onEscapePressed() { - if (isControlsDisabled) { - return - } - - PageController.closePage() - } - } - - Connections { - target: SettingsController - - function onRestoreBackupFinished() { - PageController.showNotificationMessage(qsTr("Settings restored from backup file")) - PageController.replaceStartPage() - } - } - - Connections { - target: InstallController - - function onInstallationErrorOccurred(error) { - PageController.showBusyIndicator(false) - PageController.showErrorMessage(error) - - var currentPageName = stackView.currentItem.objectName - - if (currentPageName === PageController.getPagePath(PageEnum.PageSetupWizardInstalling)) { - PageController.closePage() - } - } - } - - Connections { - target: ImportController - - function onRestoreAppConfig(data) { - PageController.showBusyIndicator(true) - SettingsController.restoreAppConfigFromData(data) - PageController.showBusyIndicator(false) - } - - function onImportErrorOccurred(error, goToPageHome) { - PageController.showErrorMessage(error) - } - } - - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.height - - ColumnLayout { - id: content - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 - - Image { - id: image - source: "qrc:/images/amneziaBigLogo.png" - - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 32 - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.preferredWidth: 344 - Layout.preferredHeight: 279 - } - - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 50 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - text: qsTr("Free service for creating a personal VPN on your server.") + - qsTr(" Helps you access blocked content without revealing your privacy, even to VPN providers.") - } - - Item { - id: focusItem - KeyNavigation.tab: startButton - } - - BasicButtonType { - id: startButton - Layout.fillWidth: true - Layout.topMargin: 32 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - text: qsTr("I have the data to connect") - - clickedFunc: function() { - connectionTypeSelection.open() - } - - KeyNavigation.tab: startButton2 - } - - BasicButtonType { - id: startButton2 - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" - borderWidth: 1 - - text: qsTr("I have nothing") - - clickedFunc: function() { - Qt.openUrlExternally(qsTr("https://amnezia.org/instructions/0_starter-guide")) - } - - Keys.onTabPressed: lastItemTabClicked(focusItem) - } - } - } - - ConnectionTypeSelectionDrawer { - id: connectionTypeSelection - - onClosed: { - PageController.forceTabBarActiveFocus() - root.defaultActiveFocusItem.forceActiveFocus() } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml index 5c9da47a..3cf154e4 100644 --- a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml +++ b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -12,14 +13,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: textKey.textField - - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - FlickableType { id: fl anchors.top: parent.top @@ -38,7 +31,6 @@ PageType { BackButtonType { id: backButton Layout.topMargin: 20 - KeyNavigation.tab: textKey.textField } HeaderType { @@ -59,15 +51,13 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Key") - textFieldPlaceholderText: "vpn://" + textField.placeholderText: "vpn://" buttonText: qsTr("Insert") clickedFunc: function() { textField.text = "" textField.paste() } - - KeyNavigation.tab: continueButton } } } @@ -83,10 +73,9 @@ PageType { anchors.bottomMargin: 32 text: qsTr("Continue") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { - if (ImportController.extractConfigFromData(textKey.textFieldText)) { + if (ImportController.extractConfigFromData(textKey.textField.text)) { PageController.goToPage(PageEnum.PageSetupWizardViewConfig) } } diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index b0d5ce44..14096742 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QtQuick.Dialogs import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -15,13 +16,6 @@ PageType { property bool showContent: false - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -29,14 +23,12 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: showContentButton } Connections { target: ImportController - function onImportErrorOccurred(errorMessage, goToPageHome) { + function onImportErrorOccurred(error, goToPageHome) { if (goToPageHome) { PageController.goToStartPage() } else { @@ -50,10 +42,7 @@ PageType { ServersModel.processedIndex = ServersModel.defaultIndex } - PageController.goToStartPage() - if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSetupWizardStart)) { - PageController.replaceStartPage() - } + PageController.goToPageHome() } } @@ -102,14 +91,15 @@ PageType { Layout.leftMargin: -8 implicitHeight: 32 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#FBB26A" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot text: showContent ? qsTr("Collapse content") : qsTr("Show content") - KeyNavigation.tab: connectButton + + parentFlickable: fl clickedFunc: function() { showContent = !showContent @@ -135,8 +125,8 @@ PageType { iconPath: "qrc:/images/controls/alert-circle.svg" - textColor: "#EB5757" - imageColor: "#EB5757" + textColor: AmneziaStyle.color.vibrantRed + imageColor: AmneziaStyle.color.vibrantRed } WarningType { @@ -155,7 +145,7 @@ PageType { implicitHeight: configContent.implicitHeight radius: 10 - color: "#1C1D21" + color: AmneziaStyle.color.onyxBlack visible: showContent @@ -176,7 +166,7 @@ PageType { Rectangle { anchors.fill: columnContent anchors.bottomMargin: -24 - color: "#0E0E11" + color: AmneziaStyle.color.midnightBlack opacity: 0.8 } @@ -188,8 +178,6 @@ PageType { anchors.rightMargin: 16 anchors.leftMargin: 16 - Keys.onTabPressed: lastItemTabClicked(focusItem) - BasicButtonType { id: connectButton Layout.fillWidth: true diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index a853cb67..af208544 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -7,6 +7,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -17,8 +18,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: clientNameTextField.textField - enum ConfigType { AmneziaConnection, OpenVpn, @@ -46,31 +45,30 @@ PageType { shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text - shareConnectionDrawer.open() - shareConnectionDrawer.contentVisible = false + shareConnectionDrawer.openTriggered() PageController.showBusyIndicator(true) switch (type) { case PageShare.ConfigType.AmneziaConnection: { - ExportController.generateConnectionConfig(clientNameTextField.textFieldText); + ExportController.generateConnectionConfig(clientNameTextField.textField.text); break; } case PageShare.ConfigType.OpenVpn: { - ExportController.generateOpenVpnConfig(clientNameTextField.textFieldText) + ExportController.generateOpenVpnConfig(clientNameTextField.textField.text) shareConnectionDrawer.configCaption = qsTr("Save OpenVPN config") shareConnectionDrawer.configExtension = ".ovpn" shareConnectionDrawer.configFileName = "amnezia_for_openvpn" break } case PageShare.ConfigType.WireGuard: { - ExportController.generateWireGuardConfig(clientNameTextField.textFieldText) + ExportController.generateWireGuardConfig(clientNameTextField.textField.text) shareConnectionDrawer.configCaption = qsTr("Save WireGuard config") shareConnectionDrawer.configExtension = ".conf" shareConnectionDrawer.configFileName = "amnezia_for_wireguard" break } case PageShare.ConfigType.Awg: { - ExportController.generateAwgConfig(clientNameTextField.textFieldText) + ExportController.generateAwgConfig(clientNameTextField.textField.text) shareConnectionDrawer.configCaption = qsTr("Save AmneziaWG config") shareConnectionDrawer.configExtension = ".conf" shareConnectionDrawer.configFileName = "amnezia_for_awg" @@ -91,7 +89,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textField.text) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray" @@ -103,7 +101,7 @@ PageType { } function onExportErrorOccurred(error) { - shareConnectionDrawer.close() + shareConnectionDrawer.closeTriggered() PageController.showErrorMessage(error) } @@ -118,38 +116,38 @@ PageType { QtObject { id: amneziaConnectionFormat - property string name: qsTr("For the AmneziaVPN app") - property var type: PageShare.ConfigType.AmneziaConnection + readonly property string name: qsTr("For the AmneziaVPN app") + readonly property int type: PageShare.ConfigType.AmneziaConnection } QtObject { id: openVpnConnectionFormat - property string name: qsTr("OpenVPN native format") - property var type: PageShare.ConfigType.OpenVpn + readonly property string name: qsTr("OpenVPN native format") + readonly property int type: PageShare.ConfigType.OpenVpn } QtObject { id: wireGuardConnectionFormat - property string name: qsTr("WireGuard native format") - property var type: PageShare.ConfigType.WireGuard + readonly property string name: qsTr("WireGuard native format") + readonly property int type: PageShare.ConfigType.WireGuard } QtObject { id: awgConnectionFormat - property string name: qsTr("AmneziaWG native format") - property var type: PageShare.ConfigType.Awg + readonly property string name: qsTr("AmneziaWG native format") + readonly property int type: PageShare.ConfigType.Awg } QtObject { id: shadowSocksConnectionFormat - property string name: qsTr("Shadowsocks native format") - property var type: PageShare.ConfigType.ShadowSocks + readonly property string name: qsTr("Shadowsocks native format") + readonly property int type: PageShare.ConfigType.ShadowSocks } QtObject { id: cloakConnectionFormat - property string name: qsTr("Cloak native format") - property var type: PageShare.ConfigType.Cloak + readonly property string name: qsTr("Cloak native format") + readonly property int type: PageShare.ConfigType.Cloak } QtObject { id: xrayConnectionFormat - property string name: qsTr("XRay native format") - property var type: PageShare.ConfigType.Xray + readonly property string name: qsTr("XRay native format") + readonly property int type: PageShare.ConfigType.Xray } FlickableType { @@ -171,16 +169,6 @@ PageType { spacing: 0 - Item { - id: focusItem - KeyNavigation.tab: header.actionButton - onFocusChanged: { - if (focusItem.activeFocus) { - a.contentY = 0 - } - } - } - HeaderType { id: header Layout.fillWidth: true @@ -190,25 +178,19 @@ PageType { actionButtonImage: "qrc:/images/controls/more-vertical.svg" actionButtonFunction: function() { - shareFullAccessDrawer.open() + shareFullAccessDrawer.openTriggered() } - KeyNavigation.tab: connectionRadioButton - DrawerType2 { id: shareFullAccessDrawer parent: root anchors.fill: parent - expandedHeight: root.height * 0.45 - onClosed: { - if (!GC.isMobile()) { - clientNameTextField.textField.forceActiveFocus() - } - } + expandedHeight: root.height - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { + id: shareFullAccessDrawerContent anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -216,12 +198,8 @@ PageType { spacing: 0 - Connections { - target: shareFullAccessDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } + onImplicitHeightChanged: { + shareFullAccessDrawer.expandedHeight = shareFullAccessDrawerContent.implicitHeight + 32 } Header2Type { @@ -234,24 +212,17 @@ PageType { descriptionText: qsTr("Use for your own devices, or share with those you trust to manage the server.") } - Item { - id: focusItem - KeyNavigation.tab: shareFullAccessButton.rightButton - } - LabelWithButtonType { id: shareFullAccessButton Layout.fillWidth: true text: qsTr("Share") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: focusItem clickedFunction: function() { PageController.goToPage(PageEnum.PageShareFullAccess) - shareFullAccessDrawer.close() + shareFullAccessDrawer.closeTriggered() } - } } } @@ -267,7 +238,7 @@ PageType { implicitWidth: accessTypeSelectorContent.implicitWidth implicitHeight: accessTypeSelectorContent.implicitHeight - color: "#1C1D21" + color: AmneziaStyle.color.onyxBlack radius: 16 RowLayout { @@ -282,13 +253,8 @@ PageType { implicitWidth: (root.width - 32) / 2 text: qsTr("Connection") - KeyNavigation.tab: usersRadioButton - onClicked: { accessTypeSelector.currentIndex = 0 - if (!GC.isMobile()) { - clientNameTextField.textField.forceActiveFocus() - } } } @@ -299,15 +265,12 @@ PageType { implicitWidth: (root.width - 32) / 2 text: qsTr("Users") - KeyNavigation.tab: accessTypeSelector.currentIndex === 0 ? clientNameTextField.textField : serverSelector - onClicked: { accessTypeSelector.currentIndex = 1 PageController.showBusyIndicator(true) ExportController.updateClientManagementModel(ContainersModel.getProcessedContainerIndex(), ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) - focusItem.forceActiveFocus() } } } @@ -321,7 +284,7 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share VPN access without the ability to manage the server") - color: "#878B91" + color: AmneziaStyle.color.mutedGray } TextFieldWithHeaderType { @@ -332,13 +295,10 @@ PageType { visible: accessTypeSelector.currentIndex === 0 headerText: qsTr("User name") - textFieldText: "New client" + textField.text: "New client" textField.maximumLength: 20 checkEmptyText: true - - KeyNavigation.tab: serverSelector - } DropDownType { @@ -379,31 +339,30 @@ PageType { clickedFunction: function() { handler() - if (serverSelector.currentIndex !== serverSelectorListView.currentIndex) { - serverSelector.currentIndex = serverSelectorListView.currentIndex + if (serverSelector.currentIndex !== serverSelectorListView.selectedIndex) { + serverSelector.currentIndex = serverSelectorListView.selectedIndex serverSelector.severSelectorIndexChanged() } - serverSelector.close() + serverSelector.closeTriggered() } Component.onCompleted: { if (ServersModel.isDefaultServerHasWriteAccess() && ServersModel.getDefaultServerData("hasInstalledContainers")) { - serverSelectorListView.currentIndex = proxyServersModel.mapFromSource(ServersModel.defaultIndex) + serverSelectorListView.selectedIndex = proxyServersModel.mapFromSource(ServersModel.defaultIndex) } else { - serverSelectorListView.currentIndex = 0 + serverSelectorListView.selectedIndex = 0 } + serverSelectorListView.positionViewAtIndex(selectedIndex, ListView.Beginning) serverSelectorListView.triggerCurrentItem() } function handler() { serverSelector.text = selectedText - ServersModel.processedIndex = proxyServersModel.mapToSource(currentIndex) + ServersModel.processedIndex = proxyServersModel.mapToSource(selectedIndex) } } - - KeyNavigation.tab: protocolSelector } DropDownType { @@ -439,12 +398,10 @@ PageType { ] } - currentIndex: 0 - clickedFunction: function() { handler() - protocolSelector.close() + protocolSelector.closeTriggered() } Connections { @@ -452,7 +409,8 @@ PageType { function onSeverSelectorIndexChanged() { var defaultContainer = proxyContainersModel.mapFromSource(ServersModel.getProcessedServerData("defaultContainer")) - protocolSelectorListView.currentIndex = defaultContainer + protocolSelectorListView.selectedIndex = defaultContainer + protocolSelectorListView.positionViewAtIndex(selectedIndex, ListView.Beginning) protocolSelectorListView.triggerCurrentItem() } } @@ -467,7 +425,7 @@ PageType { protocolSelector.text = selectedText - ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(currentIndex)) + ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(selectedIndex)) fillConnectionTypeModel() @@ -482,7 +440,7 @@ PageType { function fillConnectionTypeModel() { root.connectionTypesModel = [amneziaConnectionFormat] - var index = proxyContainersModel.mapToSource(currentIndex) + var index = proxyContainersModel.mapToSource(selectedIndex) if (index === ContainerProps.containerFromString("amnezia-openvpn")) { root.connectionTypesModel.push(openVpnConnectionFormat) @@ -502,12 +460,6 @@ PageType { } } } - - KeyNavigation.tab: accessTypeSelector.currentIndex === 0 ? - exportTypeSelector : - isSearchBarVisible ? - searchTextField.textField : - usersHeader.actionButton } DropDownType { @@ -528,9 +480,11 @@ PageType { headerText: qsTr("Connection format") listView: ListViewWithRadioButtonType { + id: exportTypeSelectorListView + onCurrentIndexChanged: { - exportTypeSelector.currentIndex = currentIndex - exportTypeSelector.text = selectedText + exportTypeSelector.currentIndex = exportTypeSelectorListView.selectedIndex + exportTypeSelector.text = exportTypeSelectorListView.selectedText } rootWidth: root.width @@ -541,19 +495,16 @@ PageType { currentIndex: 0 clickedFunction: function() { - exportTypeSelector.text = selectedText - exportTypeSelector.currentIndex = currentIndex - exportTypeSelector.close() + exportTypeSelector.text = exportTypeSelectorListView.selectedText + exportTypeSelector.currentIndex = exportTypeSelectorListView.selectedIndex + exportTypeSelector.closeTriggered() } Component.onCompleted: { - exportTypeSelector.text = selectedText - exportTypeSelector.currentIndex = currentIndex + exportTypeSelector.text = exportTypeSelectorListView.selectedText + exportTypeSelector.currentIndex = exportTypeSelectorListView.selectedIndex } } - - KeyNavigation.tab: shareButton - } BasicButtonType { @@ -567,18 +518,16 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) parentFlickable: a clickedFunc: function(){ - if (clientNameTextField.textFieldText !== "") { + if (clientNameTextField.textField.text !== "") { ExportController.generateConfig(root.connectionTypesModel[exportTypeSelector.currentIndex].type) } } - } Header2Type { @@ -594,11 +543,6 @@ PageType { actionButtonFunction: function() { root.isSearchBarVisible = true } - - Keys.onTabPressed: clientsListView.model.count > 0 ? - clientsListView.forceActiveFocus() : - lastItemTabClicked(focusItem) - } RowLayout { @@ -610,37 +554,15 @@ PageType { id: searchTextField Layout.fillWidth: true - textFieldPlaceholderText: qsTr("Search") - - Connections { - target: root - function onIsSearchBarVisibleChanged() { - if (root.isSearchBarVisible) { - searchTextField.textField.forceActiveFocus() - } else { - searchTextField.textFieldText = "" - if (!GC.isMobile()) { - usersHeader.actionButton.forceActiveFocus() - } - } - } - } + textField.placeholderText: qsTr("Search") Keys.onEscapePressed: { root.isSearchBarVisible = false } function navigateTo() { - if (GC.isMobile()) { - focusItem.forceActiveFocus() - return; - } - - if (searchTextField.textFieldText === "") { + if (searchTextField.textField.text === "") { root.isSearchBarVisible = false - usersHeader.actionButton.forceActiveFocus() - } else { - closeSearchButton.forceActiveFocus() } } @@ -652,17 +574,7 @@ PageType { ImageButtonType { id: closeSearchButton image: "qrc:/images/controls/close.svg" - imageColor: "#D7D8DB" - - Keys.onTabPressed: { - if (!GC.isMobile()) { - if (clientsListView.model.count > 0) { - clientsListView.forceActiveFocus() - } else { - lastItemTabClicked(focusItem) - } - } - } + imageColor: AmneziaStyle.color.paleGray function clickedFunc() { root.isSearchBarVisible = false @@ -681,56 +593,26 @@ PageType { visible: accessTypeSelector.currentIndex === 1 + property bool isFocusable: true + model: SortFilterProxyModel { id: proxyClientManagementModel sourceModel: ClientManagementModel filters: RegExpFilter { roleName: "clientName" - pattern: ".*" + searchTextField.textFieldText + ".*" + pattern: ".*" + searchTextField.textField.text + ".*" caseSensitivity: Qt.CaseInsensitive } } clip: true interactive: false - - activeFocusOnTab: true - focus: true - Keys.onTabPressed: { - if (!GC.isMobile()) { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - currentItem.focusItem.forceActiveFocus() - } else { - this.currentIndex = 0 - lastItemTabClicked(focusItem) - } - } - } - - onActiveFocusChanged: { - if (focus && !GC.isMobile()) { - currentIndex = 0 - currentItem.focusItem.forceActiveFocus() - } - } - - onCurrentIndexChanged: { - if (currentItem) { - if (currentItem.y < a.contentY) { - a.contentY = currentItem.y - } else if (currentItem.y + currentItem.height + clientsListView.y > a.contentY + a.height) { - a.contentY = currentItem.y + clientsListView.y + currentItem.height - a.height - } - } - } + reuseItems: true delegate: Item { implicitWidth: clientsListView.width implicitHeight: delegateContent.implicitHeight - property alias focusItem: clientFocusItem.rightButton - ColumnLayout { id: delegateContent @@ -749,7 +631,7 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - clientInfoDrawer.open() + clientInfoDrawer.openTriggered() } } @@ -760,16 +642,11 @@ PageType { parent: root - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + width: root.width + height: root.height - anchors.fill: parent - - expandedContent: ColumnLayout { - id: expandedContent + expandedStateContent: ColumnLayout { + id: expandedStateContent anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -777,68 +654,76 @@ PageType { anchors.leftMargin: 16 anchors.rightMargin: 16 - spacing: 8 - onImplicitHeightChanged: { - clientInfoDrawer.expandedHeight = expandedContent.implicitHeight + 32 + clientInfoDrawer.expandedHeight = expandedStateContent.implicitHeight + 32 } - Connections { - target: clientInfoDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem1.forceActiveFocus() - } - } - - Header2Type { - Layout.fillWidth: true - - headerText: clientName - } - - ColumnLayout - { - id: textColumn - property string textColor: "#878B91" + Header2TextType { + Layout.maximumWidth: parent.width Layout.bottomMargin: 24 - ParagraphTextType { - color: textColumn.textColor - visible: creationDate - Layout.fillWidth: true - - text: qsTr("Creation date: %1").arg(creationDate) - } - - ParagraphTextType { - color: textColumn.textColor - visible: latestHandshake - Layout.fillWidth: true - - text: qsTr("Latest handshake: %1").arg(latestHandshake) - } - - ParagraphTextType { - color: textColumn.textColor - visible: dataReceived - Layout.fillWidth: true - - text: qsTr("Data received: %1").arg(dataReceived) - } - - ParagraphTextType { - color: textColumn.textColor - visible: dataSent - Layout.fillWidth: true - - text: qsTr("Data sent: %1").arg(dataSent) - } + text: clientName + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight } - Item { - id: focusItem1 - KeyNavigation.tab: renameButton + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: creationDate + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight + + text: qsTr("Creation date: %1").arg(creationDate) + } + + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: latestHandshake + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight + + text: qsTr("Latest handshake: %1").arg(latestHandshake) + } + + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: dataReceived + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight + + text: qsTr("Data received: %1").arg(dataReceived) + } + + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: dataSent + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight + + text: qsTr("Data sent: %1").arg(dataSent) + } + + ParagraphTextType { + color: AmneziaStyle.color.mutedGray + visible: allowedIps + Layout.maximumWidth: parent.width + + wrapMode: Text.Wrap + + text: qsTr("Allowed IPs: %1").arg(allowedIps) } BasicButtonType { @@ -846,19 +731,17 @@ PageType { Layout.fillWidth: true Layout.topMargin: 24 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Rename") - KeyNavigation.tab: revokeButton - clickedFunc: function() { - clientNameEditDrawer.open() + clientNameEditDrawer.openTriggered() } DrawerType2 { @@ -869,13 +752,7 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.35 - onClosed: { - if (!GC.isMobile()) { - focusItem1.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -883,28 +760,13 @@ PageType { anchors.leftMargin: 16 anchors.rightMargin: 16 - Connections { - target: clientNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - clientNameEditor.textField.forceActiveFocus() - } - } - - Item { - id: focusItem2 - KeyNavigation.tab: clientNameEditor.textField - } - TextFieldWithHeaderType { id: clientNameEditor Layout.fillWidth: true headerText: qsTr("Client name") - textFieldText: clientName + textField.text: clientName textField.maximumLength: 20 checkEmptyText: true - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -913,21 +775,20 @@ PageType { Layout.fillWidth: true text: qsTr("Save") - KeyNavigation.tab: focusItem2 clickedFunc: function() { - if (clientNameEditor.textFieldText === "") { + if (clientNameEditor.textField.text === "") { return } - if (clientNameEditor.textFieldText !== clientName) { + if (clientNameEditor.textField.text !== clientName) { PageController.showBusyIndicator(true) ExportController.renameClient(index, - clientNameEditor.textFieldText, + clientNameEditor.textField.text, ContainersModel.getProcessedContainerIndex(), ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) - clientNameEditDrawer.close() + clientNameEditDrawer.closeTriggered() } } } @@ -938,16 +799,16 @@ PageType { BasicButtonType { id: revokeButton Layout.fillWidth: true + Layout.topMargin: 8 - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Revoke") - KeyNavigation.tab: focusItem1 clickedFunc: function() { var headerText = qsTr("Revoke the config for a user - %1?").arg(clientName) @@ -956,12 +817,12 @@ PageType { var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { - clientInfoDrawer.close() + clientInfoDrawer.closeTriggered() root.revokeConfig(index) } var noButtonFunction = function() { if (!GC.isMobile()) { - focusItem1.forceActiveFocus() + // focusItem1.forceActiveFocus() } } @@ -980,18 +841,5 @@ PageType { id: shareConnectionDrawer anchors.fill: parent - onClosed: { - if (!GC.isMobile()) { - clientNameTextField.textField.forceActiveFocus() - } - } - } - - MouseArea { - anchors.fill: parent - onPressed: function(mouse) { - forceActiveFocus() - mouse.accepted = false - } } } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index abbaf5be..70fd6292 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -7,6 +7,7 @@ import SortFilterProxyModel 0.2 import PageEnum 1.0 import ContainerProps 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -17,13 +18,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -31,8 +25,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: serverSelector } FlickableType { @@ -66,7 +58,7 @@ PageType { text: qsTr("We recommend that you use full access to the server only for your own additional devices.\n") + qsTr("If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. ") - color: "#878B91" + color: AmneziaStyle.color.mutedGray } DropDownType { @@ -84,8 +76,6 @@ PageType { descriptionText: qsTr("Server") headerText: qsTr("Server") - KeyNavigation.tab: shareButton - listView: ListViewWithRadioButtonType { id: serverSelectorListView @@ -112,7 +102,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Accessing ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text - serverSelector.close() + serverSelector.closeTriggered() } Component.onCompleted: { @@ -134,27 +124,25 @@ PageType { Layout.topMargin: 40 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" - - Keys.onTabPressed: lastItemTabClicked(focusItem) + leftImageSource: "qrc:/images/controls/share-2.svg" clickedFunc: function() { + PageController.showBusyIndicator(true) + + if (Qt.platform.os === "android" && !SystemController.isAuthenticated()) { + PageController.showBusyIndicator(false) + ExportController.exportErrorOccurred(qsTr("Access error!")) + return + } else { + ExportController.generateFullAccessConfig() + } + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text - shareConnectionDrawer.open() - shareConnectionDrawer.contentVisible = false - PageController.showBusyIndicator(true) - - if (Qt.platform.os === "android") { - ExportController.generateFullAccessConfigAndroid(); - } else { - ExportController.generateFullAccessConfig(); - } + shareConnectionDrawer.openTriggered() PageController.showBusyIndicator(false) - - shareConnectionDrawer.contentVisible = true } } } @@ -164,10 +152,5 @@ PageType { id: shareConnectionDrawer anchors.fill: parent - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 5585631e..0a21497d 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QtQuick.Shapes import PageEnum 1.0 +import Style 1.0 import "./" import "../Controls2" @@ -14,17 +15,23 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: homeTabButton - property bool isControlsDisabled: false property bool isTabBarDisabled: false Connections { + objectName: "pageControllerConnection" + target: PageController function onGoToPageHome() { - tabBar.setCurrentIndex(0) - tabBarStackView.goToTabBarPage(PageEnum.PageHome) + if (PageController.isStartPageVisible()) { + tabBar.visible = false + tabBarStackView.goToTabBarPage(PageEnum.PageSetupWizardStart) + } else { + tabBar.visible = true + tabBar.setCurrentIndex(0) + tabBarStackView.goToTabBarPage(PageEnum.PageHome) + } } function onGoToPageSettings() { @@ -64,7 +71,6 @@ PageType { } function onGoToStartPage() { - connectionTypeSelection.close() while (tabBarStackView.depth > 1) { tabBarStackView.pop() } @@ -77,26 +83,18 @@ PageType { var pageName = tabBarStackView.currentItem.objectName if ((pageName === PageController.getPagePath(PageEnum.PageShare)) || - (pageName === PageController.getPagePath(PageEnum.PageSettings))) { + (pageName === PageController.getPagePath(PageEnum.PageSettings)) || + (pageName === PageController.getPagePath(PageEnum.PageSetupWizardConfigSource))) { PageController.goToPageHome() - tabBar.previousIndex = 0 } else { PageController.closePage() } } - - function onForceTabBarActiveFocus() { - homeTabButton.focus = true - tabBar.forceActiveFocus() - } - - function onForceStackActiveFocus() { - homeTabButton.focus = true - tabBarStackView.forceActiveFocus() - } } Connections { + objectName: "installControllerConnections" + target: InstallController function onInstallationErrorOccurred(error) { @@ -117,6 +115,10 @@ PageType { } } + function onWrongInstallationUser(message) { + onInstallationErrorOccurred(message) + } + function onUpdateContainerFinished(message) { PageController.showNotificationMessage(message) PageController.closePage() @@ -125,14 +127,19 @@ PageType { function onCachedProfileCleared(message) { PageController.showNotificationMessage(message) } - } - Connections { - target: ConnectionController - - function onReconnectWithUpdatedContainer(message) { + function onApiConfigRemoved(message) { PageController.showNotificationMessage(message) - PageController.closePage() + } + + function onRemoveProcessedServerFinished(finishedMessage) { + if (!ServersModel.getServersCount()) { + PageController.goToPageHome() + } else { + PageController.goToStartPage() + PageController.goToPage(PageEnum.PageSettingsServersList) + } + PageController.showNotificationMessage(finishedMessage) } function onNoInstalledContainers() { @@ -145,53 +152,141 @@ PageType { } Connections { + objectName: "connectionControllerConnections" + + target: ConnectionController + + function onReconnectWithUpdatedContainer(message) { + PageController.showNotificationMessage(message) + PageController.closePage() + } + } + + Connections { + objectName: "importControllerConnections" + target: ImportController function onImportErrorOccurred(error, goToPageHome) { PageController.showErrorMessage(error) } + + function onRestoreAppConfig(data) { + PageController.showBusyIndicator(true) + SettingsController.restoreAppConfigFromData(data) + PageController.showBusyIndicator(false) + } } Connections { + objectName: "settingsControllerConnections" + target: SettingsController function onLoggingDisableByWatcher() { PageController.showNotificationMessage(qsTr("Logging was disabled after 14 days, log files were deleted")) } + + function onRestoreBackupFinished() { + PageController.showNotificationMessage(qsTr("Settings restored from backup file")) + PageController.goToPageHome() + } + + function onLoggingStateChanged() { + if (SettingsController.isLoggingEnabled) { + var message = qsTr("Logging is enabled. Note that logs will be automatically" + + "disabled after 14 days, and all log files will be deleted.") + PageController.showNotificationMessage(message) + } + } + } + + Connections { + target: ApiSettingsController + + function onErrorOccurred(error) { + PageController.showErrorMessage(error) + } + } + + Connections { + target: ApiConfigsController + + function onInstallServerFromApiFinished(message) { + if (!ConnectionController.isConnected) { + ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); + ServersModel.processedIndex = ServersModel.defaultIndex + } + + PageController.goToPageHome() + PageController.showNotificationMessage(message) + } + + function onChangeApiCountryFinished(message) { + PageController.goToPageHome() + PageController.showNotificationMessage(message) + } + + function onReloadServerFromApiFinished(message) { + PageController.goToPageHome() + PageController.showNotificationMessage(message) + } } StackViewType { id: tabBarStackView + objectName: "tabBarStackView" anchors.top: parent.top anchors.right: parent.right anchors.left: parent.left anchors.bottom: tabBar.top - width: parent.width - height: root.height - tabBar.implicitHeight - enabled: !root.isControlsDisabled function goToTabBarPage(page) { - connectionTypeSelection.close() - var pagePath = PageController.getPagePath(page) tabBarStackView.clear(StackView.Immediate) tabBarStackView.replace(pagePath, { "objectName" : pagePath }, StackView.Immediate) } Component.onCompleted: { - var pagePath = PageController.getPagePath(PageEnum.PageHome) - ServersModel.processedIndex = ServersModel.defaultIndex + var pagePath + if (PageController.isStartPageVisible()) { + tabBar.visible = false + pagePath = PageController.getPagePath(PageEnum.PageSetupWizardStart) + } else { + tabBar.visible = true + pagePath = PageController.getPagePath(PageEnum.PageHome) + ServersModel.processedIndex = ServersModel.defaultIndex + } + tabBarStackView.push(pagePath, { "objectName" : pagePath }) } + + Keys.onPressed: function(event) { + console.debug(">>>> ", event.key, " Event is caught by StartPage") + switch (event.key) { + case Qt.Key_Tab: + case Qt.Key_Down: + case Qt.Key_Right: + FocusController.nextKeyTabItem() + break + case Qt.Key_Backtab: + case Qt.Key_Up: + case Qt.Key_Left: + FocusController.previousKeyTabItem() + break + default: + PageController.keyPressEvent(event.key) + event.accepted = true + } + } } TabBar { id: tabBar - - property int previousIndex: 0 + objectName: "tabBar" anchors.right: parent.right anchors.left: parent.left @@ -202,9 +297,13 @@ PageType { leftPadding: 96 rightPadding: 96 + height: visible ? homeTabButton.implicitHeight + tabBar.topPadding + tabBar.bottomPadding : 0 + enabled: !root.isControlsDisabled && !root.isTabBarDisabled background: Shape { + objectName: "backgroundShape" + width: parent.width height: parent.height @@ -213,90 +312,78 @@ PageType { startY: 0 PathLine { x: width; y: 0 } - PathLine { x: width; y: height - 1 } - PathLine { x: 0; y: height - 1 } + PathLine { x: width; y: tabBar.height - 1 } + PathLine { x: 0; y: tabBar.height - 1 } PathLine { x: 0; y: 0 } strokeWidth: 1 - strokeColor: "#2C2D30" - fillColor: "#1C1D21" + strokeColor: AmneziaStyle.color.slateGray + fillColor: AmneziaStyle.color.onyxBlack } } TabImageButtonType { id: homeTabButton + objectName: "homeTabButton" + isSelected: tabBar.currentIndex === 0 image: "qrc:/images/controls/home.svg" clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageHome) ServersModel.processedIndex = ServersModel.defaultIndex tabBar.currentIndex = 0 - tabBar.previousIndex = 0 } - - KeyNavigation.tab: shareTabButton - Keys.onEnterPressed: this.clicked() - Keys.onReturnPressed: this.clicked() } TabImageButtonType { id: shareTabButton + objectName: "shareTabButton" Connections { target: ServersModel function onModelReset() { - var hasServerWithWriteAccess = ServersModel.hasServerWithWriteAccess() - shareTabButton.visible = hasServerWithWriteAccess - shareTabButton.width = hasServerWithWriteAccess ? undefined : 0 + if (!SettingsController.isOnTv()) { + var hasServerWithWriteAccess = ServersModel.hasServerWithWriteAccess() + shareTabButton.visible = hasServerWithWriteAccess + shareTabButton.width = hasServerWithWriteAccess ? undefined : 0 + } } } - visible: ServersModel.hasServerWithWriteAccess() - width: ServersModel.hasServerWithWriteAccess() ? undefined : 0 + visible: !SettingsController.isOnTv() && ServersModel.hasServerWithWriteAccess() + width: !SettingsController.isOnTv() && ServersModel.hasServerWithWriteAccess() ? undefined : 0 isSelected: tabBar.currentIndex === 1 image: "qrc:/images/controls/share-2.svg" clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageShare) tabBar.currentIndex = 1 - tabBar.previousIndex = 1 } - - KeyNavigation.tab: settingsTabButton } TabImageButtonType { id: settingsTabButton + objectName: "settingsTabButton" + isSelected: tabBar.currentIndex === 2 - image: "qrc:/images/controls/settings-2.svg" + image: "qrc:/images/controls/settings.svg" clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageSettings) tabBar.currentIndex = 2 - tabBar.previousIndex = 2 } - - KeyNavigation.tab: plusTabButton } TabImageButtonType { id: plusTabButton + objectName: "plusTabButton" + isSelected: tabBar.currentIndex === 3 image: "qrc:/images/controls/plus.svg" clickedFunc: function () { - connectionTypeSelection.open() + tabBarStackView.goToTabBarPage(PageEnum.PageSetupWizardConfigSource) + tabBar.currentIndex = 3 } - - Keys.onTabPressed: PageController.forceStackActiveFocus() - } - } - - ConnectionTypeSelectionDrawer { - id: connectionTypeSelection - - onAboutToHide: { - PageController.forceTabBarActiveFocus() - tabBar.setCurrentIndex(tabBar.previousIndex) } } } diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index 7e31bb09..7cd5790b 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -5,14 +5,17 @@ import QtQuick.Layouts import QtQuick.Dialogs import PageEnum 1.0 +import Style 1.0 import "Config" import "Controls2" import "Components" +import "Pages2" Window { id: root objectName: "mainWindow" + visible: true width: GC.screenWidth height: GC.screenHeight @@ -21,42 +24,43 @@ Window { maximumWidth: 600 maximumHeight: 800 - color: "#0E0E11" + color: AmneziaStyle.color.midnightBlack onClosing: function() { - console.debug("QML onClosing signal") PageController.closeWindow() } title: "AmneziaVPN" - StackViewType { - id: rootStackView + Item { // This item is needed for focus handling + id: defaultFocusItem + objectName: "defaultFocusItem" - width: root.width - height: root.height focus: true - Component.onCompleted: { - var pagePath = PageController.getInitialPage() - rootStackView.push(pagePath, { "objectName" : pagePath }) - } - Keys.onPressed: function(event) { - PageController.keyPressEvent(event.key) - event.accepted = true + switch (event.key) { + case Qt.Key_Tab: + case Qt.Key_Down: + case Qt.Key_Right: + FocusController.nextKeyTabItem() + break + case Qt.Key_Backtab: + case Qt.Key_Up: + case Qt.Key_Left: + FocusController.previousKeyTabItem() + break + default: + PageController.keyPressEvent(event.key) + event.accepted = true + } } } Connections { - target: PageController + objectName: "pageControllerConnections" - function onReplaceStartPage() { - var pagePath = PageController.getInitialPage() - rootStackView.clear() - PageController.updateNavigationBarColor(PageController.getInitialPageNavigationBarColor()) - rootStackView.replace(pagePath, { "objectName" : pagePath }) - } + target: PageController function onRaiseMainWindow() { root.show() @@ -81,7 +85,7 @@ Window { } function onShowPassphraseRequestDrawer() { - privateKeyPassphraseDrawer.open() + privateKeyPassphraseDrawer.openTriggered() } function onGoToPageSettingsBackup() { @@ -95,6 +99,8 @@ Window { } Connections { + objectName: "settingsControllerConnections" + target: SettingsController function onChangeSettingsFinished(finishedMessage) { @@ -102,7 +108,16 @@ Window { } } + PageStart { + objectName: "pageStart" + + width: root.width + height: root.height + } + Item { + objectName: "popupNotificationItem" + anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom @@ -126,6 +141,8 @@ Window { } Item { + objectName: "popupErrorMessageItem" + anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom @@ -138,6 +155,8 @@ Window { } Item { + objectName: "privateKeyPassphraseDrawerItem" + anchors.fill: parent DrawerType2 { @@ -146,7 +165,7 @@ Window { anchors.fill: parent expandedHeight: root.height * 0.35 - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -157,12 +176,12 @@ Window { Connections { target: privateKeyPassphraseDrawer function onOpened() { - passphrase.textFieldText = "" + passphrase.textField.text = "" passphrase.textField.forceActiveFocus() } function onAboutToHide() { - if (passphrase.textFieldText !== "") { + if (passphrase.textField.text !== "") { PageController.showBusyIndicator(true) } } @@ -185,8 +204,6 @@ Window { clickedFunc: function() { hidePassword = !hidePassword } - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -194,18 +211,18 @@ Window { Layout.fillWidth: true - defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) - disabledColor: "#878B91" - textColor: "#D7D8DB" + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.paleGray borderWidth: 1 text: qsTr("Save") clickedFunc: function() { - privateKeyPassphraseDrawer.close() - PageController.passphraseRequestDrawerClosed(passphrase.textFieldText) + privateKeyPassphraseDrawer.closeTriggered() + PageController.passphraseRequestDrawerClosed(passphrase.textField.text) } } } @@ -213,6 +230,8 @@ Window { } Item { + objectName: "questionDrawerItem" + anchors.fill: parent QuestionDrawer { @@ -223,6 +242,8 @@ Window { } Item { + objectName: "busyIndicatorItem" + anchors.fill: parent BusyIndicatorType { @@ -239,26 +260,26 @@ Window { questionDrawer.noButtonText = noButtonText questionDrawer.yesButtonFunction = function() { - questionDrawer.close() + questionDrawer.closeTriggered() if (yesButtonFunction && typeof yesButtonFunction === "function") { yesButtonFunction() } } questionDrawer.noButtonFunction = function() { - questionDrawer.close() + questionDrawer.closeTriggered() if (noButtonFunction && typeof noButtonFunction === "function") { noButtonFunction() } } - questionDrawer.open() + questionDrawer.openTriggered() } FileDialog { id: mainFileDialog + objectName: "mainFileDialog" property bool isSaveMode: false - objectName: "mainFileDialog" fileMode: isSaveMode ? FileDialog.SaveFile : FileDialog.OpenFile onAccepted: SystemController.fileDialogClosed(true) diff --git a/client/utilities.cpp b/client/utilities.cpp old mode 100644 new mode 100755 index a2f3d021..61944e51 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -10,18 +10,72 @@ #include #include "utilities.h" -#include "version.h" + +#ifdef Q_OS_WINDOWS +QString printErrorMessage(DWORD errorCode) { + LPVOID lpMsgBuf; + + DWORD dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + + DWORD dwLanguageId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + + FormatMessageW( + dwFlags, + NULL, + errorCode, + dwLanguageId, + (LPWSTR)&lpMsgBuf, + 0, + NULL + ); + + QString errorMsg = QString::fromWCharArray((LPCWSTR)lpMsgBuf); + LocalFree(lpMsgBuf); + return errorMsg.trimmed(); +} + +QString Utils::getNextDriverLetter() +{ + DWORD drivesBitmask = GetLogicalDrives(); + if (drivesBitmask == 0) { + DWORD error = GetLastError(); + qDebug() << "GetLogicalDrives failed. Error code:" << error; + return ""; + } + + QString letters = "FGHIJKLMNOPQRSTUVWXYZ"; + QString availableLetter; + + for (int i = letters.size() - 1; i >= 0; --i) { + QChar letterChar = letters.at(i); + int driveIndex = letterChar.toLatin1() - 'A'; + + if ((drivesBitmask & (1 << driveIndex)) == 0) { + availableLetter = letterChar; + break; + } + } + + if (availableLetter.isEmpty()) { + qDebug() << "Can't find free drive letter"; + return ""; + } + + return availableLetter; +} +#endif QString Utils::getRandomString(int len) { - const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); - + const QString possibleCharacters = QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); QString randomString; + for (int i = 0; i < len; ++i) { - quint32 index = QRandomGenerator::global()->generate() % possibleCharacters.length(); - QChar nextChar = possibleCharacters.at(index); - randomString.append(nextChar); + randomString.append(possibleCharacters.at(QRandomGenerator::system()->bounded(possibleCharacters.length()))); } + return randomString; } @@ -69,22 +123,6 @@ QString Utils::JsonToString(const QJsonArray &array, QJsonDocument::JsonFormat f return doc.toJson(format); } -QString Utils::systemLogPath() -{ -#ifdef Q_OS_WIN - QStringList locationList = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); - QString primaryLocation = "ProgramData"; - foreach (const QString &location, locationList) { - if (location.contains(primaryLocation)) { - return QString("%1/%2/log").arg(location).arg(APPLICATION_NAME); - } - } - return QString(); -#else - return QString("/var/log/%1").arg(APPLICATION_NAME); -#endif -} - bool Utils::initializePath(const QString &path) { QDir dir; @@ -125,35 +163,44 @@ QString Utils::usrExecutable(const QString &baseName) bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) { #ifdef Q_OS_WIN - QProcess process; - process.setReadChannel(QProcess::StandardOutput); - process.setProcessChannelMode(QProcess::MergedChannels); - process.start("wmic.exe", - QStringList() << "/OUTPUT:STDOUT" - << "PROCESS" - << "get" - << "Caption"); - process.waitForStarted(); - process.waitForFinished(); - QString processData(process.readAll()); - QStringList processList = processData.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); - foreach (const QString &rawLine, processList) { - const QString line = rawLine.simplified(); - if (line.isEmpty()) { - continue; - } + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) { + qWarning() << "Utils::processIsRunning error CreateToolhelp32Snapshot"; + return false; + } - if (line == fileName) { + PROCESSENTRY32W pe32; + pe32.dwSize = sizeof(PROCESSENTRY32W); + + if (!Process32FirstW(hSnapshot, &pe32)) { + CloseHandle(hSnapshot); + qWarning() << "Utils::processIsRunning error Process32FirstW"; + return false; + } + + do { + QString exeFile = QString::fromWCharArray(pe32.szExeFile); + + if (exeFile.compare(fileName, Qt::CaseInsensitive) == 0) { + CloseHandle(hSnapshot); return true; } - } + } while (Process32NextW(hSnapshot, &pe32)); + + CloseHandle(hSnapshot); return false; -#elif defined(Q_OS_IOS) + +#elif defined(Q_OS_IOS) || defined(Q_OS_ANDROID) return false; #else QProcess process; + QStringList arguments; + if (fullFlag) { + arguments << "-f"; + } + arguments << fileName; process.setProcessChannelMode(QProcess::MergedChannels); - process.start("pgrep", QStringList({ fullFlag ? "-f" : "", fileName })); + process.start("pgrep", arguments); process.waitForFinished(); if (process.exitStatus() == QProcess::NormalExit) { if (fullFlag) { @@ -166,15 +213,47 @@ bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) #endif } -void Utils::killProcessByName(const QString &name) +bool Utils::killProcessByName(const QString &name) { qDebug().noquote() << "Kill process" << name; #ifdef Q_OS_WIN - QProcess::execute("taskkill", QStringList() << "/IM" << name << "/F"); -#elif defined Q_OS_IOS - return; + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) + return false; + + PROCESSENTRY32W pe32; + pe32.dwSize = sizeof(PROCESSENTRY32W); + + bool success = false; + + if (Process32FirstW(hSnapshot, &pe32)) { + do { + QString exeFile = QString::fromWCharArray(pe32.szExeFile); + + if (exeFile.compare(name, Qt::CaseInsensitive) == 0) { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID); + if (hProcess != NULL) { + if (TerminateProcess(hProcess, 0)) { + success = true; + } else { + DWORD error = GetLastError(); + qCritical() << "Can't terminate process" << exeFile << "(PID:" << pe32.th32ProcessID << "). Error:" << printErrorMessage(error); + } + CloseHandle(hProcess); + } else { + DWORD error = GetLastError(); + qCritical() << "Can't open process for termination" << exeFile << "(PID:" << pe32.th32ProcessID << "). Error:" << printErrorMessage(error); + } + } + } while (Process32NextW(hSnapshot, &pe32)); + } + + CloseHandle(hSnapshot); + return success; +#elif defined Q_OS_IOS || defined(Q_OS_ANDROID) + return false; #else - QProcess::execute(QString("pkill %1").arg(name)); + return QProcess::execute("pkill", { name }) == 0; #endif } @@ -260,3 +339,22 @@ bool Utils::signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent) } #endif + +void Utils::logException(const std::exception &e) +{ + qCritical() << e.what(); + try { + std::rethrow_if_nested(e); + } catch (const std::exception &nested) { + logException(nested); + } catch (...) {} +} + +void Utils::logException(const std::exception_ptr &eptr) +{ + try { + if (eptr) std::rethrow_exception(eptr); + } catch (const std::exception &e) { + logException(e); + } catch (...) {} +} diff --git a/client/utilities.h b/client/utilities.h old mode 100644 new mode 100755 index b85c5b3b..4a1985b1 --- a/client/utilities.h +++ b/client/utilities.h @@ -7,7 +7,8 @@ #include #ifdef Q_OS_WIN - #include "Windows.h" +#include +#include #endif class Utils : public QObject @@ -23,20 +24,23 @@ public: static QJsonObject JsonFromString(const QString &string); static QString executable(const QString &baseName, bool absPath); static QString usrExecutable(const QString &baseName); - static QString systemLogPath(); static bool createEmptyFile(const QString &path); static bool initializePath(const QString &path); static bool processIsRunning(const QString &fileName, const bool fullFlag = false); - static void killProcessByName(const QString &name); + static bool killProcessByName(const QString &name); static QString openVpnExecPath(); static QString wireguardExecPath(); static QString certUtilPath(); static QString tun2socksPath(); + static void logException(const std::exception &e); + static void logException(const std::exception_ptr &eptr = std::current_exception()); + #ifdef Q_OS_WIN static bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent); + static QString getNextDriverLetter(); #endif }; diff --git a/client/utils/qmlUtils.cpp b/client/utils/qmlUtils.cpp new file mode 100644 index 00000000..b9557b14 --- /dev/null +++ b/client/utils/qmlUtils.cpp @@ -0,0 +1,128 @@ +#include "qmlUtils.h" + +#include +#include +#include + +namespace FocusControl +{ + QPointF getItemCenterPointOnScene(QQuickItem *item) + { + const auto x0 = item->x() + (item->width() / 2); + const auto y0 = item->y() + (item->height() / 2); + return item->parentItem()->mapToScene(QPointF { x0, y0 }); + } + + bool isEnabled(QObject *obj) + { + const auto item = qobject_cast(obj); + return item && item->isEnabled(); + } + + bool isVisible(QObject *item) + { + const auto res = item->property("visible").toBool(); + return res; + } + + bool isFocusable(QObject *item) + { + const auto res = item->property("isFocusable").toBool(); + return res; + } + + bool isListView(QObject *item) + { + return item->inherits("QQuickListView"); + } + + bool isOnTheScene(QObject *object) + { + QQuickItem *item = qobject_cast(object); + if (!item) { + qWarning() << "Couldn't recognize object as item"; + return false; + } + + if (!item->isVisible()) { + return false; + } + + QRectF itemRect = item->mapRectToScene(item->childrenRect()); + + QQuickWindow *window = item->window(); + if (!window) { + qWarning() << "Couldn't get the window on the Scene check"; + return false; + } + + const auto contentItem = window->contentItem(); + if (!contentItem) { + qWarning() << "Couldn't get the content item on the Scene check"; + return false; + } + QRectF windowRect = contentItem->childrenRect(); + const auto res = (windowRect.contains(itemRect) || isListView(item)); + return res; + } + + bool isMore(QObject *item1, QObject *item2) + { + return !isLess(item1, item2); + } + + bool isLess(QObject *item1, QObject *item2) + { + const auto p1 = getItemCenterPointOnScene(qobject_cast(item1)); + const auto p2 = getItemCenterPointOnScene(qobject_cast(item2)); + return (p1.y() == p2.y()) ? (p1.x() < p2.x()) : (p1.y() < p2.y()); + } + + QList getSubChain(QObject *object) + { + QList res; + if (!object) { + return res; + } + + const auto children = object->children(); + + for (const auto child : children) { + if (child && isFocusable(child) && isOnTheScene(child) && isEnabled(child)) { + res.append(child); + } else { + res.append(getSubChain(child)); + } + } + return res; + } + + QList getItemsChain(QObject *object) + { + QList res; + if (!object) { + return res; + } + + const auto children = object->children(); + + for (const auto child : children) { + if (child && isFocusable(child) && isEnabled(child) && isVisible(child)) { + res.append(child); + } else { + res.append(getItemsChain(child)); + } + } + return res; + } + + void printItems(const QList &items, QObject *current_item) + { + for (const auto &item : items) { + QQuickItem *i = qobject_cast(item); + QPointF coords { getItemCenterPointOnScene(i) }; + QString prefix = current_item == i ? "==>" : " "; + qDebug() << prefix << " Item: " << i << " with coords: " << coords; + } + } +} // namespace FocusControl \ No newline at end of file diff --git a/client/utils/qmlUtils.h b/client/utils/qmlUtils.h new file mode 100644 index 00000000..535377c4 --- /dev/null +++ b/client/utils/qmlUtils.h @@ -0,0 +1,30 @@ +#ifndef FOCUSCONTROL_H +#define FOCUSCONTROL_H + +#include +#include + +namespace FocusControl +{ + bool isEnabled(QObject *item); + bool isVisible(QObject *item); + bool isFocusable(QObject *item); + bool isListView(QObject *item); + bool isOnTheScene(QObject *object); + bool isMore(QObject *item1, QObject *item2); + bool isLess(QObject *item1, QObject *item2); + + /*! + * \brief Make focus chain of elements which are on the scene + */ + QList getSubChain(QObject *object); + + /*! + * \brief Make focus chain of elements which could be not on the scene + */ + QList getItemsChain(QObject *object); + + void printItems(const QList &items, QObject *current_item); +} // namespace FocusControl + +#endif // FOCUSCONTROL_H diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index daff1187..042c51c7 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -1,16 +1,16 @@ #include "qtimer.h" #include +#include #include #include #include -#include +#include "core/controllers/serverController.h" #include #include #include #include -#include "core/controllers/serverController.h" #ifdef AMNEZIA_DESKTOP #include "core/ipcclient.h" @@ -34,8 +34,7 @@ VpnConnection::VpnConnection(std::shared_ptr settings, QObject *parent { m_checkTimer.setInterval(1000); #ifdef Q_OS_IOS - connect(IosController::Instance(), &IosController::connectionStateChanged, this, - &VpnConnection::onConnectionStateChanged); + connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged); connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged); #endif @@ -57,14 +56,15 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) { #ifdef AMNEZIA_DESKTOP - QString proto = m_settings->defaultContainerName(m_settings->defaultServerIndex()); - + auto container = m_settings->defaultContainer(m_settings->defaultServerIndex()); + if (IpcClient::Interface()) { if (state == Vpn::ConnectionState::Connected) { IpcClient::Interface()->resetIpStack(); IpcClient::Interface()->flushDns(); - if (!m_vpnConfiguration.value(config_key::configVersion).toInt()) { + if (!m_vpnConfiguration.value(config_key::configVersion).toInt() && container != DockerContainer::Awg + && container != DockerContainer::WireGuard) { QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); @@ -72,7 +72,7 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state) if (m_settings->isSitesSplitTunnelingEnabled()) { IpcClient::Interface()->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0"); - // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); + // qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size(); if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) { QTimer::singleShot(1000, m_vpnProtocol.data(), [this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); }); @@ -215,10 +215,9 @@ ErrorCode VpnConnection::lastError() const void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration) { - qDebug() << QString("ConnectToVpn, Server index is %1, container is %2, route mode is") + qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2") .arg(serverIndex) - .arg(ContainerProps::containerToString(container)) - << m_settings->routeMode(); + .arg(ContainerProps::containerToString(container)); #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) if (!m_IpcClient) { m_IpcClient = new IpcClient(this); @@ -234,7 +233,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede } #endif - m_remoteAddress = credentials.hostName; + m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName); emit connectionStateChanged(Vpn::ConnectionState::Connecting); m_vpnConfiguration = vpnConfiguration; @@ -291,43 +290,78 @@ void VpnConnection::appendKillSwitchConfig() void VpnConnection::appendSplitTunnelingConfig() { - if (m_vpnConfiguration.value(config_key::configVersion).toInt()) { - auto protocolName = m_vpnConfiguration.value(config_key::vpnproto).toString(); - if (protocolName == ProtocolProps::protoToString(Proto::Awg)) { - auto configData = m_vpnConfiguration.value(protocolName + "_config_data").toObject(); - QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configData.value("allowed_ips").toString().split(",")); - QJsonArray defaultAllowedIP = QJsonArray::fromStringList(QString("0.0.0.0/0, ::/0").split(",")); + bool allowSiteBasedSplitTunneling = true; - if (allowedIpsJsonArray != defaultAllowedIP) { - allowedIpsJsonArray.append(m_vpnConfiguration.value(config_key::dns1).toString()); - allowedIpsJsonArray.append(m_vpnConfiguration.value(config_key::dns2).toString()); - - m_vpnConfiguration.insert(config_key::splitTunnelType, Settings::RouteMode::VpnOnlyForwardSites); - m_vpnConfiguration.insert(config_key::splitTunnelSites, allowedIpsJsonArray); + // this block is for old native configs and for old self-hosted configs + auto protocolName = m_vpnConfiguration.value(config_key::vpnproto).toString(); + if (protocolName == ProtocolProps::protoToString(Proto::Awg) || protocolName == ProtocolProps::protoToString(Proto::WireGuard)) { + allowSiteBasedSplitTunneling = false; + auto configData = m_vpnConfiguration.value(protocolName + "_config_data").toObject(); + if (configData.value(config_key::allowed_ips).isString()) { + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configData.value(config_key::allowed_ips).toString().split(", ")); + configData.insert(config_key::allowed_ips, allowedIpsJsonArray); + m_vpnConfiguration.insert(protocolName + "_config_data", configData); + } else if (configData.value(config_key::allowed_ips).isUndefined()) { + auto nativeConfig = configData.value(config_key::config).toString(); + auto nativeConfigLines = nativeConfig.split("\n"); + for (auto &line : nativeConfigLines) { + if (line.contains("AllowedIPs")) { + auto allowedIpsString = line.split(" = "); + if (allowedIpsString.size() < 1) { + break; + } + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(allowedIpsString.at(1).split(", ")); + configData.insert(config_key::allowed_ips, allowedIpsJsonArray); + m_vpnConfiguration.insert(protocolName + "_config_data", configData); + break; + } } } - } else { - Settings::RouteMode routeMode = Settings::RouteMode::VpnAllSites; - QJsonArray sitesJsonArray; - if (m_settings->isSitesSplitTunnelingEnabled()) { - routeMode = m_settings->routeMode(); - auto sites = m_settings->getVpnIps(routeMode); + if (configData.value(config_key::persistent_keep_alive).isUndefined()) { + auto nativeConfig = configData.value(config_key::config).toString(); + auto nativeConfigLines = nativeConfig.split("\n"); + for (auto &line : nativeConfigLines) { + if (line.contains("PersistentKeepalive")) { + auto persistentKeepaliveString = line.split(" = "); + if (persistentKeepaliveString.size() < 1) { + break; + } + configData.insert(config_key::persistent_keep_alive, persistentKeepaliveString.at(1)); + m_vpnConfiguration.insert(protocolName + "_config_data", configData); + break; + } + } + } + + QJsonArray allowedIpsJsonArray = configData.value(config_key::allowed_ips).toArray(); + if (allowedIpsJsonArray.contains("0.0.0.0/0") && allowedIpsJsonArray.contains("::/0")) { + allowSiteBasedSplitTunneling = true; + } + } + + Settings::RouteMode sitesRouteMode = Settings::RouteMode::VpnAllSites; + QJsonArray sitesJsonArray; + if (m_settings->isSitesSplitTunnelingEnabled()) { + sitesRouteMode = m_settings->routeMode(); + + if (allowSiteBasedSplitTunneling) { + auto sites = m_settings->getVpnIps(sitesRouteMode); for (const auto &site : sites) { sitesJsonArray.append(site); } // Allow traffic to Amnezia DNS - if (routeMode == Settings::VpnOnlyForwardSites) { + if (sitesRouteMode == Settings::VpnOnlyForwardSites) { sitesJsonArray.append(m_vpnConfiguration.value(config_key::dns1).toString()); sitesJsonArray.append(m_vpnConfiguration.value(config_key::dns2).toString()); } } - - m_vpnConfiguration.insert(config_key::splitTunnelType, routeMode); - m_vpnConfiguration.insert(config_key::splitTunnelSites, sitesJsonArray); } + m_vpnConfiguration.insert(config_key::splitTunnelType, sitesRouteMode); + m_vpnConfiguration.insert(config_key::splitTunnelSites, sitesJsonArray); + Settings::AppsRouteMode appsRouteMode = Settings::AppsRouteMode::VpnAllApps; QJsonArray appsJsonArray; if (m_settings->isAppsSplitTunnelingEnabled()) { @@ -341,6 +375,13 @@ void VpnConnection::appendSplitTunnelingConfig() m_vpnConfiguration.insert(config_key::appSplitTunnelType, appsRouteMode); m_vpnConfiguration.insert(config_key::splitTunnelApps, appsJsonArray); + + qDebug() << QString("Site split tunneling is %1, route mode is %2") + .arg(m_settings->isSitesSplitTunnelingEnabled() ? "enabled" : "disabled") + .arg(sitesRouteMode); + qDebug() << QString("App split tunneling is %1, route mode is %2") + .arg(m_settings->isAppsSplitTunnelingEnabled() ? "enabled" : "disabled") + .arg(appsRouteMode); } #ifdef Q_OS_ANDROID @@ -359,8 +400,7 @@ void VpnConnection::createAndroidConnections() connect(AndroidController::instance(), &AndroidController::connectionStateChanged, androidVpnProtocol, &AndroidVpnProtocol::setConnectionState); - connect(AndroidController::instance(), &AndroidController::statisticsUpdated, androidVpnProtocol, - &AndroidVpnProtocol::setBytesChanged); + connect(AndroidController::instance(), &AndroidController::statisticsUpdated, androidVpnProtocol, &AndroidVpnProtocol::setBytesChanged); } AndroidVpnProtocol *VpnConnection::createDefaultAndroidVpnProtocol() diff --git a/common/logger/logger.cpp b/common/logger/logger.cpp new file mode 100644 index 00000000..747590b9 --- /dev/null +++ b/common/logger/logger.cpp @@ -0,0 +1,330 @@ +#include "logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utilities.h" +#include "version.h" + +#ifdef AMNEZIA_DESKTOP + #include +#endif + +#ifdef Q_OS_IOS + #include +#endif + +QFile Logger::m_file; +QTextStream Logger::m_textStream; +QString Logger::m_logFileName = QString("%1.log").arg(APPLICATION_NAME); +QString Logger::m_serviceLogFileName = QString("%1.log").arg(SERVICE_NAME); + +void debugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (msg.simplified().isEmpty()) { + return; + } + + // Skip annoying messages from Qt + if (msg.contains("OpenType support missing for")) { + return; + } + + if (msg.startsWith("Unknown property") || msg.startsWith("Could not create pixmap") || msg.startsWith("Populating font") + || msg.startsWith("stale focus object")) { + return; + } + + Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; + + std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; +} + +Logger &Logger::Instance() +{ + static Logger s; + return s; +} + +bool Logger::init(bool isServiceLogger) +{ + QString path = isServiceLogger ? systemLogDir() : userLogsDir(); + QString logFileName = isServiceLogger ? m_serviceLogFileName : m_logFileName ; + QDir appDir(path); + if (!appDir.mkpath(path)) { + return false; + } + + m_file.setFileName(appDir.filePath(logFileName)); + if (!m_file.open(QIODevice::Append)) { + qWarning() << "Cannot open log file:" << logFileName; + return false; + } + + m_file.setTextModeEnabled(true); + m_textStream.setDevice(&m_file); + qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); + +#if !defined(QT_DEBUG) || defined(Q_OS_IOS) + qInstallMessageHandler(debugMessageHandler); +#endif + + return true; +} + +void Logger::deInit() +{ + qInstallMessageHandler(nullptr); + qSetMessagePattern("%{message}"); + m_textStream.setDevice(nullptr); + m_file.close(); +} + +bool Logger::setServiceLogsEnabled(bool enabled) +{ +#ifdef AMNEZIA_DESKTOP + IpcClient *m_IpcClient = new IpcClient; + + if (!m_IpcClient->isSocketConnected()) { + if (!IpcClient::init(m_IpcClient)) { + qWarning() << "Error occurred when init IPC client"; + return false; + } + } + + if (m_IpcClient->Interface()) { + m_IpcClient->Interface()->setLogsEnabled(enabled); + } else { + qWarning() << "Error occurred setting up service logs"; + return false; + } +#endif + + return true; +} + +QString Logger::userLogsDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/log"; +} + +QString Logger::systemLogDir() +{ +#ifdef Q_OS_WIN + QStringList locationList = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + QString primaryLocation = "ProgramData"; + foreach (const QString &location, locationList) { + if (location.contains(primaryLocation)) { + return QString("%1/%2/log").arg(location).arg(APPLICATION_NAME); + } + } + return QString(); +#else + return QString("/var/log/%1").arg(APPLICATION_NAME); +#endif +} + +QString Logger::userLogsFilePath() +{ + return userLogsDir() + QDir::separator() + m_logFileName; +} + +QString Logger::serviceLogsFilePath() +{ + return systemLogDir() + QDir::separator() + m_serviceLogFileName; +} + +QString Logger::getLogFile() +{ + m_file.flush(); + QFile file(userLogsFilePath()); + + file.open(QIODevice::ReadOnly); + QString qtLog = file.readAll(); + +#ifdef Q_OS_IOS + return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); +#else + return qtLog; +#endif +} + +QString Logger::getServiceLogFile() +{ + m_file.flush(); + QFile file(serviceLogsFilePath()); + + file.open(QIODevice::ReadOnly); + QString qtLog = file.readAll(); + +#ifdef Q_OS_IOS + return QString().fromStdString(AmneziaVPN::swiftUpdateLogData(qtLog.toStdString())); +#else + return qtLog; +#endif +} + +bool Logger::openLogsFolder(bool isServiceLogger) +{ + QString path = isServiceLogger ? systemLogDir() : userLogsDir(); +#ifdef Q_OS_WIN + path = "file:///" + path; +#endif + if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path))) { + qWarning() << "Can't open url:" << path; + return false; + } + return true; +} + +void Logger::clearLogs(bool isServiceLogger) +{ + bool isLogActive = m_file.isOpen(); + m_file.close(); + + QFile file(isServiceLogger ? serviceLogsFilePath() : userLogsFilePath()); + + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.resize(0); + file.close(); + +#ifdef Q_OS_IOS + AmneziaVPN::swiftDeleteLog(); +#endif + + if (isLogActive) { + init(isServiceLogger); + } +} + +void Logger::clearServiceLogs() +{ +#ifdef AMNEZIA_DESKTOP + IpcClient *m_IpcClient = new IpcClient; + + if (!m_IpcClient->isSocketConnected()) { + if (!IpcClient::init(m_IpcClient)) { + qWarning() << "Error occurred when init IPC client"; + return; + } + } + + if (m_IpcClient->Interface()) { + m_IpcClient->Interface()->clearLogs(); + } else { + qWarning() << "Error occurred cleaning up service logs"; + } +#endif +} + +void Logger::cleanUp() +{ + clearLogs(false); + QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + dir.removeRecursively(); + + clearLogs(true); +} + +Logger::Log::Log(Logger *logger, LogLevel logLevel) : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) +{ +} + +Logger::Log::~Log() +{ + qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); + delete m_data; +} + +Logger::Log Logger::error() +{ + return Log(this, LogLevel::Error); +} +Logger::Log Logger::warning() +{ + return Log(this, LogLevel::Warning); +} +Logger::Log Logger::info() +{ + return Log(this, LogLevel::Info); +} +Logger::Log Logger::debug() +{ + return Log(this, LogLevel::Debug); +} +QString Logger::sensitive(const QString &input) +{ +#ifdef Q_DEBUG + return input; +#else + Q_UNUSED(input); + return QString(8, 'X'); +#endif +} + +#define CREATE_LOG_OP_REF(x) \ + Logger::Log &Logger::Log::operator<<(x t) \ + { \ + m_data->m_ts << t << ' '; \ + return *this; \ + } + +CREATE_LOG_OP_REF(uint64_t); +CREATE_LOG_OP_REF(const char *); +CREATE_LOG_OP_REF(const QString &); +CREATE_LOG_OP_REF(const QByteArray &); +CREATE_LOG_OP_REF(const void *); + +#undef CREATE_LOG_OP_REF + +Logger::Log &Logger::Log::operator<<(const QStringList &t) +{ + m_data->m_ts << '[' << t.join(",") << ']' << ' '; + return *this; +} + +Logger::Log &Logger::Log::operator<<(const QJsonObject &t) +{ + m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; + return *this; +} + +Logger::Log &Logger::Log::operator<<(QTextStreamFunction t) +{ + m_data->m_ts << t; + return *this; +} + +void Logger::Log::addMetaEnum(quint64 value, const QMetaObject *meta, const char *name) +{ + QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); + + QString out; + QTextStream ts(&out); + + if (const char *scope = me.scope()) { + ts << scope << "::"; + } + + const char *key = me.valueToKey(static_cast(value)); + const bool scoped = me.isScoped(); + if (scoped || !key) { + ts << me.enumName() << (!key ? "(" : "::"); + } + + if (key) { + ts << key; + } else { + ts << value << ")"; + } + + m_data->m_ts << out; +} diff --git a/common/logger/logger.h b/common/logger/logger.h new file mode 100644 index 00000000..7dff7ede --- /dev/null +++ b/common/logger/logger.h @@ -0,0 +1,114 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include +#include +#include + +#include "mozilla/shared/loglevel.h" + +class Logger : public QObject +{ + Q_OBJECT + +public: + static Logger &Instance(); + + static bool init(bool isServiceLogger); + static void deInit(); + + static bool setServiceLogsEnabled(bool enabled); + + static bool openLogsFolder(bool isServiceLogger); + + static void clearLogs(bool isServiceLogger); + static void clearServiceLogs(); + static void cleanUp(); + + static QString userLogsFilePath(); + static QString serviceLogsFilePath(); + static QString systemLogDir(); + + static QString getLogFile(); + static QString getServiceLogFile(); + + // compat with Mozilla logger + Logger(const QString &className) + { + m_className = className; + } + const QString &className() const + { + return m_className; + } + + class Log + { + public: + Log(Logger *logger, LogLevel level); + ~Log(); + + Log &operator<<(uint64_t t); + Log &operator<<(const char *t); + Log &operator<<(const QString &t); + Log &operator<<(const QStringList &t); + Log &operator<<(const QByteArray &t); + Log &operator<<(const QJsonObject &t); + Log &operator<<(QTextStreamFunction t); + Log &operator<<(const void *t); + + // Q_ENUM + template typename std::enable_if::Value, Log &>::type operator<<(T t) + { + const QMetaObject *meta = qt_getEnumMetaObject(t); + const char *name = qt_getEnumName(t); + addMetaEnum(typename QFlags::Int(t), meta, name); + return *this; + } + + private: + void addMetaEnum(quint64 value, const QMetaObject *meta, const char *name); + + Logger *m_logger; + LogLevel m_logLevel; + + struct Data + { + Data() : m_ts(&m_buffer, QIODevice::WriteOnly) + { + } + + QString m_buffer; + QTextStream m_ts; + }; + + Data *m_data; + }; + + Log error(); + Log warning(); + Log info(); + Log debug(); + QString sensitive(const QString &input); + +private: + Logger() {}; + Logger(Logger const &) = delete; + Logger &operator=(Logger const &) = delete; + + static QString userLogsDir(); + + static QFile m_file; + static QTextStream m_textStream; + static QString m_logFileName; + static QString m_serviceLogFileName; + + friend void debugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + // compat with Mozilla logger + QString m_className; +}; + +#endif // LOGGER_H diff --git a/deploy/data/linux/post_install.sh b/deploy/data/linux/post_install.sh index b3345bac..324462d9 100755 --- a/deploy/data/linux/post_install.sh +++ b/deploy/data/linux/post_install.sh @@ -19,6 +19,11 @@ date > $LOG_FILE echo "Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + if sudo systemctl is-active --quiet $APP_NAME; then sudo systemctl stop $APP_NAME >> $LOG_FILE sudo systemctl disable $APP_NAME >> $LOG_FILE @@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE echo "user desktop creation loop ended" >> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh index 5849a90e..98090d20 100755 --- a/deploy/data/linux/post_uninstall.sh +++ b/deploy/data/linux/post_uninstall.sh @@ -13,6 +13,11 @@ date >> $LOG_FILE echo "Uninstall Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do sudo unlink $dir >> $LOG_FILE done @@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then fi +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service after uninstall status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 79f2d042..c0f031fe 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -7,7 +7,6 @@ class IpcInterface { SLOT( int createPrivilegedProcess() ); // return local pid - //SIGNAL(sendMessage(const QByteArray &message)); // Route functions SLOT( int routeAddList(const QString &gw, const QStringList &ips) ); @@ -21,6 +20,7 @@ class IpcInterface SLOT( void cleanUp() ); SLOT( void setLogsEnabled(bool enabled) ); + SLOT( void clearLogs() ); SLOT( bool createTun(const QString &dev, const QString &subnet) ); SLOT( bool deleteTun(const QString &dev) ); diff --git a/ipc/ipc_process_interface.rep b/ipc/ipc_process_interface.rep index ba42332c..6b3bb654 100644 --- a/ipc/ipc_process_interface.rep +++ b/ipc/ipc_process_interface.rep @@ -3,7 +3,6 @@ class IpcProcessInterface { - //SLOT( start(const QString &program, const QStringList &args) ); SLOT( start() ); SLOT( close() ); diff --git a/ipc/ipc_process_tun2socks.rep b/ipc/ipc_process_tun2socks.rep new file mode 100644 index 00000000..e355035e --- /dev/null +++ b/ipc/ipc_process_tun2socks.rep @@ -0,0 +1,11 @@ +#include +#include + +class IpcProcessTun2Socks +{ + SLOT( start() ); + SLOT( stop() ); + + SIGNAL( setConnectionState(int state) ); + SIGNAL( stateChanged(QProcess::ProcessState newState) ); +}; diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index c734912b..17f34499 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -1,32 +1,33 @@ #include "ipcserver.h" -#include #include -#include #include +#include +#include -#include "router.h" #include "logger.h" +#include "router.h" +#include "../core/networkUtilities.h" #include "../client/protocols/protocols_defs.h" #ifdef Q_OS_WIN -#include "tapcontroller_win.h" -#include "../client/platforms/windows/daemon/windowsfirewall.h" -#include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsdaemon.h" + #include "../client/platforms/windows/daemon/windowsfirewall.h" + #include "tapcontroller_win.h" #endif #ifdef Q_OS_LINUX -#include "../client/platforms/linux/daemon/linuxfirewall.h" + #include "../client/platforms/linux/daemon/linuxfirewall.h" #endif #ifdef Q_OS_MACOS -#include "../client/platforms/macos/daemon/macosfirewall.h" + #include "../client/platforms/macos/daemon/macosfirewall.h" #endif -IpcServer::IpcServer(QObject *parent): - IpcInterfaceSource(parent) +IpcServer::IpcServer(QObject *parent) : IpcInterfaceSource(parent) -{} +{ +} int IpcServer::createPrivilegedProcess() { @@ -34,10 +35,6 @@ int IpcServer::createPrivilegedProcess() qDebug() << "IpcServer::createPrivilegedProcess"; #endif -#ifdef Q_OS_WIN - WindowsFirewall::instance()->init(); -#endif - m_localpid++; ProcessDescriptor pd(this); @@ -58,23 +55,10 @@ int IpcServer::createPrivilegedProcess() } }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, [pd](QRemoteObjectNode::ErrorCode errorCode) { - qDebug() << "QRemoteObjectHost::error" << errorCode; - }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::error, this, + [pd](QRemoteObjectNode::ErrorCode errorCode) { qDebug() << "QRemoteObjectHost::error" << errorCode; }); - QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { - qDebug() << "QRemoteObjectHost::destroyed"; - }); - -// connect(pd.ipcProcess.data(), &IpcServerProcess::finished, this, [this, pid=m_localpid](int exitCode, QProcess::ExitStatus exitStatus){ -// qDebug() << "IpcServerProcess finished" << exitCode << exitStatus; -//// if (m_processes.contains(pid)) { -//// m_processes[pid].ipcProcess.reset(); -//// m_processes[pid].serverNode.reset(); -//// m_processes[pid].localServer.reset(); -//// m_processes.remove(pid); -//// } -// }); + QObject::connect(pd.serverNode.data(), &QRemoteObjectHost::destroyed, this, [pd]() { qDebug() << "QRemoteObjectHost::destroyed"; }); m_processes.insert(m_localpid, pd); @@ -105,7 +89,7 @@ bool IpcServer::routeDeleteList(const QString &gw, const QStringList &ips) qDebug() << "IpcServer::routeDeleteList"; #endif - return Router::routeDeleteList(gw ,ips); + return Router::routeDeleteList(gw, ips); } void IpcServer::flushDns() @@ -158,10 +142,15 @@ void IpcServer::cleanUp() qDebug() << "IpcServer::cleanUp"; #endif - Logger::deinit(); + Logger::deInit(); Logger::cleanUp(); } +void IpcServer::clearLogs() +{ + Logger::clearLogs(true); +} + bool IpcServer::createTun(const QString &dev, const QString &subnet) { return Router::createTun(dev, subnet); @@ -172,7 +161,7 @@ bool IpcServer::deleteTun(const QString &dev) return Router::deleteTun(dev); } -bool IpcServer::updateResolvers(const QString& ifname, const QList& resolvers) +bool IpcServer::updateResolvers(const QString &ifname, const QList &resolvers) { return Router::updateResolvers(ifname, resolvers); } @@ -193,18 +182,18 @@ void IpcServer::setLogsEnabled(bool enabled) #endif if (enabled) { - Logger::init(); - } - else { - Logger::deinit(); + Logger::init(true); + } else { + Logger::deInit(); } } - bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { #ifdef Q_OS_WIN - return WindowsFirewall::instance()->enableKillSwitch(vpnAdapterIndex); + auto firewallManager = WindowsFirewall::create(this); + Q_ASSERT(firewallManager != nullptr); + return firewallManager->enableInterface(vpnAdapterIndex); #endif #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) @@ -216,13 +205,11 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd QStringList allownets; QStringList blocknets; - if (splitTunnelType == 0) - { + if (splitTunnelType == 0) { blockAll = true; allowNets = true; - allownets.append(configStr.value(amnezia::config_key::hostName).toString()); - } else if (splitTunnelType == 1) - { + allownets.append(configStr.value("vpnServer").toString()); + } else if (splitTunnelType == 1) { blockNets = true; for (auto v : splitTunnelSites) { blocknets.append(v.toString()); @@ -230,7 +217,7 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd } else if (splitTunnelType == 2) { blockAll = true; allowNets = true; - allownets.append(configStr.value(amnezia::config_key::hostName).toString()); + allownets.append(configStr.value("vpnServer").toString()); for (auto v : splitTunnelSites) { allownets.append(v.toString()); } @@ -239,6 +226,8 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd #ifdef Q_OS_LINUX // double-check + ensure our firewall is installed and enabled + if (!LinuxFirewall::isInstalled()) + LinuxFirewall::install(); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), blockAll); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), allowNets); @@ -264,18 +253,17 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd // double-check + ensure our firewall is installed and enabled. This is necessary as // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) - if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + if (!MacOSFirewall::isInstalled()) + MacOSFirewall::install(); MacOSFirewall::ensureRootAnchorPriority(); MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), blockAll); MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), allowNets); - MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, - QStringLiteral("allownets"), allownets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), allowNets, QStringLiteral("allownets"), allownets); MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), blockNets); - MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, - QStringLiteral("blocknets"), blocknets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), blockNets, QStringLiteral("blocknets"), blocknets); MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); @@ -294,7 +282,9 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd bool IpcServer::disableKillSwitch() { #ifdef Q_OS_WIN - return WindowsFirewall::instance()->disableKillSwitch(); + auto firewallManager = WindowsFirewall::create(this); + Q_ASSERT(firewallManager != nullptr); + return firewallManager->disableKillSwitch(); #endif #ifdef Q_OS_LINUX @@ -326,10 +316,8 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) // Use APP split tunnel if (splitTunnelType == 0 || splitTunnelType == 2) { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("0.0.0.0"), 0)); - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress("::"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("0.0.0.0"), 0)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress("::"), 0)); } if (splitTunnelType == 1) { @@ -337,15 +325,14 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) QString ipRange = v.toString(); if (ipRange.split('/').size() > 1) { config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); + IPAddress(QHostAddress(ipRange.split('/')[0]), atoi(ipRange.split('/')[1].toLocal8Bit()))); } else { - config.m_allowedIPAddressRanges.append( - IPAddress(QHostAddress(ipRange), 32)); + config.m_allowedIPAddressRanges.append(IPAddress(QHostAddress(ipRange), 32)); } } } - config.m_excludedAddresses.append(configStr.value(amnezia::config_key::hostName).toString()); + config.m_excludedAddresses.append(configStr.value("vpnServer").toString()); if (splitTunnelType == 2) { for (auto v : splitTunnelSites) { QString ipRange = v.toString(); @@ -353,7 +340,7 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) } } - for (const QJsonValue& i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { + for (const QJsonValue &i : configStr.value(amnezia::config_key::splitTunnelApps).toArray()) { if (!i.isString()) { break; } @@ -362,12 +349,13 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) // killSwitch toggle if (QVariant(configStr.value(amnezia::config_key::killSwitchOption).toString()).toBool()) { - WindowsFirewall::instance()->enablePeerTraffic(config); + auto firewallManager = WindowsFirewall::create(this); + Q_ASSERT(firewallManager != nullptr); + firewallManager->enablePeerTraffic(config); } WindowsDaemon::instance()->prepareActivation(config, inetAdapterIndex); WindowsDaemon::instance()->activateSplitTunnel(config, vpnAdapterIndex); - return true; #endif return true; } diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index bd474481..9810046b 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -9,8 +9,10 @@ #include "ipc.h" #include "ipcserverprocess.h" +#include "ipctun2socksprocess.h" #include "rep_ipc_interface_source.h" +#include "rep_ipc_process_tun2socks_source.h" class IpcServer : public IpcInterfaceSource { @@ -26,6 +28,7 @@ public: virtual bool checkAndInstallDriver() override; virtual QStringList getTapList() override; virtual void cleanUp() override; + virtual void clearLogs() override; virtual void setLogsEnabled(bool enabled) override; virtual bool createTun(const QString &dev, const QString &subnet) override; virtual bool deleteTun(const QString &dev) override; @@ -43,10 +46,12 @@ private: ProcessDescriptor (QObject *parent = nullptr) { serverNode = QSharedPointer(new QRemoteObjectHost(parent)); ipcProcess = QSharedPointer(new IpcServerProcess(parent)); + tun2socksProcess = QSharedPointer(new IpcProcessTun2Socks(parent)); localServer = QSharedPointer(new QLocalServer(parent)); } QSharedPointer ipcProcess; + QSharedPointer tun2socksProcess; QSharedPointer serverNode; QSharedPointer localServer; }; diff --git a/ipc/ipctun2socksprocess.cpp b/ipc/ipctun2socksprocess.cpp new file mode 100644 index 00000000..2125f6ab --- /dev/null +++ b/ipc/ipctun2socksprocess.cpp @@ -0,0 +1,79 @@ +#include "ipctun2socksprocess.h" +#include "ipc.h" +#include +#include + +#include "../protocols/protocols_defs.h" + +#ifndef Q_OS_IOS + +IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) : + IpcProcessTun2SocksSource(parent), + m_t2sProcess(QSharedPointer(new QProcess())) +{ + qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()"; + +} + +IpcProcessTun2Socks::~IpcProcessTun2Socks() +{ + qDebug() << "IpcProcessTun2Socks::~IpcProcessTun2Socks()"; +} + +void IpcProcessTun2Socks::start() +{ + connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged); + qDebug() << "IpcProcessTun2Socks::start()"; + m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast(amnezia::PermittedProcess::Tun2Socks))); + + QString XrayConStr = "socks5://127.0.0.1:10808"; + +#ifdef Q_OS_WIN + QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr, "-tun-post-up", + QString("cmd /c netsh interface ip set address name=\"tun2\" static %1 255.255.255.255") + .arg(amnezia::protocols::xray::defaultLocalAddr)}); +#endif +#ifdef Q_OS_LINUX + QStringList arguments({"-device", "tun://tun2", "-proxy", XrayConStr}); +#endif +#ifdef Q_OS_MAC + QStringList arguments({"-device", "utun22", "-proxy", XrayConStr}); +#endif + + m_t2sProcess->setArguments(arguments); + + if (Utils::processIsRunning(Utils::executable("tun2socks", false))) { + qDebug().noquote() << "kill previos tun2socks"; + Utils::killProcessByName(Utils::executable("tun2socks", false)); + } + + m_t2sProcess->start(); + + connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() { + QString line = m_t2sProcess.data()->readAllStandardOutput(); + if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) { + emit setConnectionState(Vpn::ConnectionState::Connected); + } + }); + + connect(m_t2sProcess.data(), QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus; + emit setConnectionState(Vpn::ConnectionState::Disconnected); + if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { + emit setConnectionState(Vpn::ConnectionState::Error); + } + + }); + + m_t2sProcess->start(); + m_t2sProcess->waitForStarted(); +} + +void IpcProcessTun2Socks::stop() +{ + qDebug() << "IpcProcessTun2Socks::stop()"; + m_t2sProcess->disconnect(); + m_t2sProcess->kill(); + m_t2sProcess->waitForFinished(3000); +} +#endif diff --git a/ipc/ipctun2socksprocess.h b/ipc/ipctun2socksprocess.h new file mode 100644 index 00000000..8ce9be1a --- /dev/null +++ b/ipc/ipctun2socksprocess.h @@ -0,0 +1,52 @@ +#ifndef IPCTUN2SOCKSPROCESS_H +#define IPCTUN2SOCKSPROCESS_H + +#include + +#ifndef Q_OS_IOS +#include "rep_ipc_process_tun2socks_source.h" + +namespace Vpn +{ +Q_NAMESPACE + enum ConnectionState { + Unknown, + Disconnected, + Preparing, + Connecting, + Connected, + Disconnecting, + Reconnecting, + Error + }; +Q_ENUM_NS(ConnectionState) +} + + +class IpcProcessTun2Socks : public IpcProcessTun2SocksSource +{ + Q_OBJECT +public: + explicit IpcProcessTun2Socks(QObject *parent = nullptr); + virtual ~IpcProcessTun2Socks(); + + void start() override; + void stop() override; + +signals: + +private: + QSharedPointer m_t2sProcess; +}; + +#else +class IpcProcessTun2Socks : public QObject +{ + Q_OBJECT + +public: + explicit IpcProcessTun2Socks(QObject *parent = nullptr); +}; +#endif + +#endif // IPCTUN2SOCKSPROCESS_H diff --git a/metadata/img-readme/download-alt.svg b/metadata/img-readme/download-alt.svg new file mode 100644 index 00000000..f97c9c3d --- /dev/null +++ b/metadata/img-readme/download-alt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 00000000..386ae4fe --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website.svg b/metadata/img-readme/download-website.svg new file mode 100644 index 00000000..d0cf8375 --- /dev/null +++ b/metadata/img-readme/download-website.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/testiny.png b/metadata/img-readme/testiny.png new file mode 100644 index 00000000..4f38a3a9 Binary files /dev/null and b/metadata/img-readme/testiny.png differ diff --git a/metadata/img-readme/uipic4.png b/metadata/img-readme/uipic4.png new file mode 100644 index 00000000..8ce91ba1 Binary files /dev/null and b/metadata/img-readme/uipic4.png differ diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 234dfafe..28174774 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -18,8 +18,9 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.h + ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.h ${CMAKE_CURRENT_LIST_DIR}/localserver.h - ${CMAKE_CURRENT_LIST_DIR}/logger.h + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.h ${CMAKE_CURRENT_LIST_DIR}/router.h ${CMAKE_CURRENT_LIST_DIR}/systemservice.h ${CMAKE_CURRENT_BINARY_DIR}/version.h @@ -30,8 +31,9 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserverprocess.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipctun2socksprocess.cpp ${CMAKE_CURRENT_LIST_DIR}/localserver.cpp - ${CMAKE_CURRENT_LIST_DIR}/logger.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger/logger.cpp ${CMAKE_CURRENT_LIST_DIR}/main.cpp ${CMAKE_CURRENT_LIST_DIR}/router.cpp ${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp @@ -125,6 +127,7 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsutils.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowspingsender.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsnetworkwatcher.h + ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/daemonerrors.h ) set(SOURCES ${SOURCES} @@ -238,6 +241,7 @@ include_directories( ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/../../client ${CMAKE_CURRENT_LIST_DIR}/../../ipc + ${CMAKE_CURRENT_LIST_DIR}/../../common/logger ${CMAKE_CURRENT_BINARY_DIR} ) @@ -278,6 +282,7 @@ endif() qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep) qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep) +qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_tun2socks.rep) # copy deploy artifacts required to run the application to the debug build folder if(WIN32) diff --git a/service/server/localserver.cpp b/service/server/localserver.cpp index 3e1b0954..8a5079cb 100644 --- a/service/server/localserver.cpp +++ b/service/server/localserver.cpp @@ -37,6 +37,7 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent), if (!m_isRemotingEnabled) { m_isRemotingEnabled = true; m_serverNode.enableRemoting(&m_ipcServer); + m_serverNode.enableRemoting(&m_tun2socks); } }); diff --git a/service/server/localserver.h b/service/server/localserver.h index 4a6648a5..3c565d3b 100644 --- a/service/server/localserver.h +++ b/service/server/localserver.h @@ -38,6 +38,7 @@ public: ~LocalServer(); QSharedPointer m_server; IpcServer m_ipcServer; + IpcProcessTun2Socks m_tun2socks; QRemoteObjectHost m_serverNode; bool m_isRemotingEnabled = false; #ifdef Q_OS_LINUX diff --git a/service/server/logger.cpp b/service/server/logger.cpp deleted file mode 100644 index ab658796..00000000 --- a/service/server/logger.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "logger.h" - -#include -#include -#include -#include - -#include - -#include "version.h" -#include "utilities.h" - -QFile Logger::m_file; -QTextStream Logger::m_textStream; -QString Logger::m_logFileName = QString("%1.log").arg(SERVICE_NAME); - -void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) -{ - if (msg.simplified().isEmpty()) { - return; - } - - Logger::m_textStream << qFormatLogMessage(type, context, msg) << Qt::endl << Qt::flush; - - std::cout << qFormatLogMessage(type, context, msg).toStdString() << std::endl << std::flush; -} - -bool Logger::init() -{ - if (m_file.isOpen()) return true; - - QString path = Utils::systemLogPath(); - QDir appDir(path); - if (!appDir.mkpath(path)) { - return false; - } - - qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} %{type} %{message}"); - - m_file.setFileName(appDir.filePath(m_logFileName)); - if (!m_file.open(QIODevice::Append)) { - qWarning() << "Cannot open log file:" << m_logFileName; - return false; - } - m_file.setTextModeEnabled(true); - m_textStream.setDevice(&m_file); - qInstallMessageHandler(debugMessageHandler); - - return true; -} - -void Logger::deinit() -{ - m_file.close(); - m_textStream.setDevice(nullptr); - qInstallMessageHandler(nullptr); -} - -QString Logger::serviceLogFileNamePath() -{ - return m_file.fileName(); -} - -void Logger::clearLogs() -{ - bool isLogActive = m_file.isOpen(); - m_file.close(); - - - QString path = Utils::systemLogPath(); - QDir appDir(path); - QFile file; - file.setFileName(appDir.filePath(m_logFileName)); - - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.resize(0); - file.close(); - - if (isLogActive) { - init(); - } -} - -void Logger::cleanUp() -{ - clearLogs(); - deinit(); - - QString path = Utils::systemLogPath(); - QDir appDir(path); - - { - QFile file; - file.setFileName(appDir.filePath(m_logFileName)); - file.remove(); - } - { - QFile file; - file.setFileName(appDir.filePath("openvpn.log")); - file.remove(); - } - -#ifdef Q_OS_WINDOWS - QDir dir(Utils::systemLogPath()); - dir.removeRecursively(); -#endif -} - - -Logger::Log::Log(Logger* logger, LogLevel logLevel) - : m_logger(logger), m_logLevel(logLevel), m_data(new Data()) {} - -Logger::Log::~Log() { - qDebug() << "Amnezia" << m_logger->className() << m_data->m_buffer.trimmed(); - delete m_data; -} - -Logger::Log Logger::error() { return Log(this, LogLevel::Error); } -Logger::Log Logger::warning() { return Log(this, LogLevel::Warning); } -Logger::Log Logger::info() { return Log(this, LogLevel::Info); } -Logger::Log Logger::debug() { return Log(this, LogLevel::Debug); } -QString Logger::sensitive(const QString& input) { -#ifdef Q_DEBUG - return input; -#else - Q_UNUSED(input); - return QString(8, 'X'); -#endif -} - - -#define CREATE_LOG_OP_REF(x) \ -Logger::Log& Logger::Log::operator<<(x t) { \ - m_data->m_ts << t << ' '; \ - return *this; \ -} - -CREATE_LOG_OP_REF(uint64_t); -CREATE_LOG_OP_REF(const char*); -CREATE_LOG_OP_REF(const QString&); -CREATE_LOG_OP_REF(const QByteArray&); -CREATE_LOG_OP_REF(const void*); - -#undef CREATE_LOG_OP_REF - -Logger::Log& Logger::Log::operator<<(const QStringList& t) { - m_data->m_ts << '[' << t.join(",") << ']' << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(const QJsonObject& t) { - m_data->m_ts << QJsonDocument(t).toJson(QJsonDocument::Indented) << ' '; - return *this; -} - -Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { - m_data->m_ts << t; - return *this; -} - -void Logger::Log::addMetaEnum(quint64 value, const QMetaObject* meta, - const char* name) { - QMetaEnum me = meta->enumerator(meta->indexOfEnumerator(name)); - - QString out; - QTextStream ts(&out); - - if (const char* scope = me.scope()) { - ts << scope << "::"; - } - - const char* key = me.valueToKey(static_cast(value)); - const bool scoped = me.isScoped(); - if (scoped || !key) { - ts << me.enumName() << (!key ? "(" : "::"); - } - - if (key) { - ts << key; - } else { - ts << value << ")"; - } - - m_data->m_ts << out; -} diff --git a/service/server/logger.h b/service/server/logger.h deleted file mode 100644 index bb920931..00000000 --- a/service/server/logger.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include -#include -#include -#include - -#include "mozilla/shared/loglevel.h" - -class Logger -{ -public: - static bool init(); - static void deinit(); - - static QString serviceLogFileNamePath(); - - static void clearLogs(); - static void cleanUp(); - - // compat with Mozilla logger - Logger(const QString &className) { m_className = className; } - const QString& className() const { return m_className; } - - class Log { - public: - Log(Logger* logger, LogLevel level); - ~Log(); - - Log& operator<<(uint64_t t); - Log& operator<<(const char* t); - Log& operator<<(const QString& t); - Log& operator<<(const QStringList& t); - Log& operator<<(const QByteArray& t); - Log& operator<<(const QJsonObject& t); - Log& operator<<(QTextStreamFunction t); - Log& operator<<(const void* t); - - // Q_ENUM - template - typename std::enable_if::Value, Log&>::type - operator<<(T t) { - const QMetaObject* meta = qt_getEnumMetaObject(t); - const char* name = qt_getEnumName(t); - addMetaEnum(typename QFlags::Int(t), meta, name); - return *this; - } - - private: - void addMetaEnum(quint64 value, const QMetaObject* meta, const char* name); - - Logger* m_logger; - LogLevel m_logLevel; - - struct Data { - Data() : m_ts(&m_buffer, QIODevice::WriteOnly) {} - - QString m_buffer; - QTextStream m_ts; - }; - - Data* m_data; - }; - - Log error(); - Log warning(); - Log info(); - Log debug(); - QString sensitive(const QString& input); - -private: - friend void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - - static QFile m_file; - static QString m_logFileName; - static QTextStream m_textStream; - - // compat with Mozilla logger - QString m_className; -}; - -#endif // LOGGER_H diff --git a/service/server/main.cpp b/service/server/main.cpp index 495192c3..cee33d72 100644 --- a/service/server/main.cpp +++ b/service/server/main.cpp @@ -44,9 +44,7 @@ int runApplication(int argc, char** argv) int main(int argc, char **argv) { - Utils::initializePath(Utils::systemLogPath()); - - Logger::init(); + Utils::initializePath(Logger::systemLogDir()); if (argc >= 2) { qInfo() << "Started as console application"; diff --git a/service/server/router_linux.cpp b/service/server/router_linux.cpp index fb7a108c..852c878f 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -152,21 +152,29 @@ bool RouterLinux::routeDeleteList(const QString &gw, const QStringList &ips) return cnt; } +bool RouterLinux::isServiceActive(const QString &serviceName) { + QProcess process; + process.start("systemctl", { "is-active", "--quiet", serviceName }); + process.waitForFinished(); + + return process.exitCode() == 0; +} + void RouterLinux::flushDns() { QProcess p; p.setProcessChannelMode(QProcess::MergedChannels); //check what the dns manager use - if (QFileInfo::exists("/usr/bin/nscd") - || QFileInfo::exists("/usr/sbin/nscd") - || QFileInfo::exists("/usr/lib/systemd/system/nscd.service")) - { - p.start("systemctl restart nscd"); - } - else - { - p.start("systemctl restart systemd-resolved"); + if (isServiceActive("nscd.service")) { + qDebug() << "Restarting nscd.service"; + p.start("systemctl", { "restart", "nscd" }); + } else if (isServiceActive("systemd-resolved.service")) { + qDebug() << "Restarting systemd-resolved.service"; + p.start("systemctl", { "restart", "systemd-resolved" }); + } else { + qDebug() << "No suitable DNS manager found."; + return; } p.waitForFinished(); diff --git a/service/server/router_linux.h b/service/server/router_linux.h index 04a26fd2..2094f596 100644 --- a/service/server/router_linux.h +++ b/service/server/router_linux.h @@ -43,6 +43,7 @@ private: RouterLinux(RouterLinux const &) = delete; RouterLinux& operator= (RouterLinux const&) = delete; + bool isServiceActive(const QString &serviceName); QList m_addedRoutes; DnsUtilsLinux *m_dnsUtil; };