From 1c1e74d06f6e6738a1976d2c3af7bf9b6f1e2286 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 6 Dec 2024 12:40:04 +0000 Subject: [PATCH 1/8] ru readme --- README_RU.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 README_RU.md diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 000000000..8b4539075 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,191 @@ +# Amnezia VPN +## _The best client for self-hosted VPN_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. + +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) + +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) + +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). + + + + +[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) + +
+ + + +## Features + +- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. +- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. +- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). +- Windows, MacOS, Linux, Android, iOS releases. +- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). + +## Links + +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium + +## Tech + +AmneziaVPN uses several open-source projects to work: + +- [OpenSSL](https://www.openssl.org/) +- [OpenVPN](https://openvpn.net/) +- [Shadowsocks](https://shadowsocks.org/) +- [Qt](https://www.qt.io/) +- [LibSsh](https://libssh.org) - forked from Qt Creator +- and more... + +## Checking out the source code + +Make sure to pull all submodules after checking out the repo. + +```bash +git submodule update --init --recursive +``` + +## Development + +Want to contribute? Welcome! + +### Help with translations + +Download the most actual translation files. + +Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. +Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". + +Unzip this file. +Each *.ts file contains strings for one corresponding language. + +Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. +You can do it via a web-interface or any other method you're familiar with. + +### Building sources and deployment + +Check deploy folder for build scripts. + +### How to build an iOS app from source code on MacOS + +1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. + +2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: + - MacOS + - iOS + - Qt 5 Compatibility Module + - Qt Shader Tools + - Additional Libraries: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + +3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) + +4. You also need to install go >= v1.16. If you don't have it installed already, +download go from the [official website](https://golang.org/dl/) or use Homebrew. +The latest version is recommended. Install gomobile +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Build the project +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment + + +If you get `gomobile: command not found` make sure to set PATH to the location +of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Open the XCode project. You can then run /test/archive/ship the app. + +If the build fails with the following error +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with +key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +if the above error persists on your M1 Mac, then most probably you need to install arch based CMake +``` +arch -arm64 brew install cmake +``` + +Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that +require them. In this case, simply restart the build. + +## How to build the Android app + +_Tested on Mac OS_ + +The Android app has the following requirements: +* JDK 11 +* Android platform SDK 33 +* CMake 3.25.0 + +After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. + +- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. +- Set path to JDK 11 +- Set path to Android SDK (`$ANDROID_HOME`) + +In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  +Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  +Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. + +That's it! You should be ready to compile the project from QT Creator! + +### Development flow + +After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. +If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! + +You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. + +## License + +GPL v3.0 + +## Donate + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
+USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
+USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
+XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns +## Acknowledgments + +This project is tested with BrowserStack. +We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. From ea910ba30054d5a88a9bfc62f35b04e1ece2986f Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Dec 2024 22:15:01 +0200 Subject: [PATCH 2/8] Update README_RU.md --- README_RU.md | 181 +++++++++------------------------------------------ 1 file changed, 30 insertions(+), 151 deletions(-) diff --git a/README_RU.md b/README_RU.md index 8b4539075..6ebdb97f7 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,182 +1,60 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ +## _Лучший клиент для создания VPN на собственном сервере_ -[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) -[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) - -[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) +### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting) > [!TIP] -> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). +> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). -[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
-## Features +## Особенности -- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. -- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. -- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. -- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). -- Windows, MacOS, Linux, Android, iOS releases. -- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). +- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN. +- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2. +- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них. +- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS. +- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). -## Links +## Ссылки -- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) -- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация - [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit -- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) -- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) -- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) -- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) -- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\) -## Tech +## Технологии -AmneziaVPN uses several open-source projects to work: +AmneziaVPN использует несколько проектов с открытым исходным кодом: - [OpenSSL](https://www.openssl.org/) - [OpenVPN](https://openvpn.net/) - [Shadowsocks](https://shadowsocks.org/) - [Qt](https://www.qt.io/) -- [LibSsh](https://libssh.org) - forked from Qt Creator -- and more... - -## Checking out the source code - -Make sure to pull all submodules after checking out the repo. - -```bash -git submodule update --init --recursive -``` - -## Development - -Want to contribute? Welcome! - -### Help with translations - -Download the most actual translation files. - -Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. -Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". - -Unzip this file. -Each *.ts file contains strings for one corresponding language. - -Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. -You can do it via a web-interface or any other method you're familiar with. - -### Building sources and deployment - -Check deploy folder for build scripts. - -### How to build an iOS app from source code on MacOS - -1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. - -2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: - - MacOS - - iOS - - Qt 5 Compatibility Module - - Qt Shader Tools - - Additional Libraries: - - Qt Image Formats - - Qt Multimedia - - Qt Remote Objects - -3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) - -4. You also need to install go >= v1.16. If you don't have it installed already, -download go from the [official website](https://golang.org/dl/) or use Homebrew. -The latest version is recommended. Install gomobile -```bash -export PATH=$PATH:~/go/bin -go install golang.org/x/mobile/cmd/gomobile@latest -gomobile init -``` +- [LibSsh](https://libssh.org) +- и другие... -5. Build the project -```bash -export QT_BIN_DIR="/Qt//ios/bin" -export QT_MACOS_ROOT_DIR="/Qt//macos" -export QT_IOS_BIN=$QT_BIN_DIR -export PATH=$PATH:~/go/bin -mkdir build-ios -$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -``` -Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment - - -If you get `gomobile: command not found` make sure to set PATH to the location -of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. -```bash -export PATH=$(PATH):/path/to/GOPATH/bin -``` - -6. Open the XCode project. You can then run /test/archive/ship the app. - -If the build fails with the following error -``` -make: *** -[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] -Error 1 -``` -Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with -key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. - -if the above error persists on your M1 Mac, then most probably you need to install arch based CMake -``` -arch -arm64 brew install cmake -``` - -Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that -require them. In this case, simply restart the build. - -## How to build the Android app - -_Tested on Mac OS_ - -The Android app has the following requirements: -* JDK 11 -* Android platform SDK 33 -* CMake 3.25.0 - -After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. - -- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. -- Set path to JDK 11 -- Set path to Android SDK (`$ANDROID_HOME`) - -In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  -Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  -Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. - -That's it! You should be ready to compile the project from QT Creator! - -### Development flow - -After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. -If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! - -You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. - -## License +## Лицензия GPL v3.0 -## Donate +## Донаты Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) @@ -185,7 +63,8 @@ USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns -## Acknowledgments -This project is tested with BrowserStack. -We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. From 569d63ef0f750f9938dfdda2fc69c9559a7be4c8 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 7 Dec 2024 15:53:40 +0200 Subject: [PATCH 3/8] Add files via upload --- metadata/img-readme/download-website-ru.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 metadata/img-readme/download-website-ru.svg diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 000000000..386ae4fe5 --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + From d67201ede9dc2f39b525ec55e04ac0225e551462 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:34:18 +0200 Subject: [PATCH 4/8] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b4539075..8f8878081 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ + +### _The best client for self-hosted VPN_ + [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) +### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md) + + [Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) From c5aa070bf4cd7ff1de931dc22a887aea3104ae92 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:49:26 +0200 Subject: [PATCH 5/8] Update README_RU.md --- README_RU.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README_RU.md b/README_RU.md index 6ebdb97f7..fe9dd2860 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,6 +1,11 @@ # Amnezia VPN -## _Лучший клиент для создания VPN на собственном сервере_ +### _Лучший клиент для создания VPN на собственном сервере_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский [AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) @@ -10,8 +15,8 @@ > [!TIP] > Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). - - + + [Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) From 6ea6ab1bd983fd2be880e9e14a9184bda9b79349 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 8 Dec 2024 08:14:22 +0300 Subject: [PATCH 6/8] chore: added clang-format config files (#1293) --- .clang-format | 39 +++++++++++++++++++++++++++++++++++++++ .clang-format-ignore | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-format-ignore diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..5c459fd2f --- /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 000000000..4019357f0 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,20 @@ +/client/3rd +/client/3rd-prebuild +/client/android +/client/cmake +/client/core/serialization +/client/daemon +/client/fonts +/client/images +/client/ios +/client/mozilla +/client/platforms/dummy +/client/platforms/linux +/client/platforms/macos +/client/platforms/windows +/client/server_scripts +/client/translations +/deploy +/docs +/metadata +/service/src From 2db99715b1fc5a7ef1f8b800c72d6e5b3422ce1f Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Dec 2024 09:32:49 +0300 Subject: [PATCH 7/8] feature: added subscription expiration date for premium v2 (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: added subscription expiration date for premium v2 * feature: added a check for the presence of the “services” field in the response body of the getServicesList() function * feature: added prohibition to change location when connection is active * bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend --- client/core/controllers/apiController.cpp | 7 + client/core/defs.h | 1 + client/core/errorstrings.cpp | 3 +- .../ui/controllers/connectionController.cpp | 2 +- client/ui/models/apiServicesModel.cpp | 112 ++++++--- client/ui/models/apiServicesModel.h | 40 +++- client/ui/models/servers_model.cpp | 33 ++- client/ui/models/servers_model.h | 5 + .../Pages2/PageSettingsApiLanguageList.qml | 6 + .../qml/Pages2/PageSettingsApiServerInfo.qml | 7 +- .../ui/qml/Pages2/PageSettingsServerInfo.qml | 219 +++++++++--------- 11 files changed, 280 insertions(+), 155 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index c50165e7e..6562632a9 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -379,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); + + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + return ErrorCode::ApiServicesMissingError; + } + } + return errorCode; } diff --git a/client/core/defs.h b/client/core/defs.h index d00d347b1..c0db2e127 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 495346063..70f433c62 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -63,7 +63,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; - + case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f8516f6e3..f9491d4e6 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -55,7 +55,7 @@ void ConnectionController::openConnection() && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by end_date event"; + qDebug() << "attempt to update api config by expires_at event"; if (configVersion == ApiConfigSources::Telegram) { emit updateApiConfigFromTelegram(); } else { diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 2a87bde3f..81a10f873 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -27,6 +27,9 @@ namespace constexpr char storeEndpoint[] = "store_endpoint"; constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; } namespace serviceType @@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject service = m_services.at(index.row()).toObject(); - QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); - auto serviceType = service.value(configKey::serviceType).toString(); + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; switch (role) { case NameRole: { - return serviceInfo.value(configKey::name).toString(); + return apiServiceData.serviceInfo.name; } case CardDescriptionRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); + auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); } else if (serviceType == serviceType::amneziaFree){ QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { description += tr("

Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); } return description; @@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case IsServiceAvailableRole: { if (serviceType == serviceType::amneziaFree) { - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { return false; } } return true; } case SpeedRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); - return tr("%1 MBit/s").arg(speed); + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); } - case WorkPeriodRole: { - auto timelimit = serviceInfo.value(configKey::timelimit).toString(); - if (timelimit == "0") { + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { return ""; } - return tr("%1 days").arg(timelimit); + return tr("%1 days").arg(timeLimit); } case RegionRole: { - return serviceInfo.value(configKey::region).toString(); + return apiServiceData.serviceInfo.region; } case FeaturesRole: { if (serviceType == serviceType::amneziaPremium) { @@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } } case PriceRole: { - auto price = serviceInfo.value(configKey::price).toString(); + auto price = apiServiceData.serviceInfo.price; if (price == "free") { return tr("Free"); } return tr("%1 $/month").arg(price); } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } } return QVariant(); @@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data) { beginResetModel(); + m_services.clear(); + m_countryCode = data.value(configKey::userCountryCode).toString(); - m_services = data.value(configKey::services).toArray(); - if (m_services.isEmpty()) { - QJsonObject service; - service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo)); - service.insert(configKey::serviceType, data.value(configKey::serviceType)); + auto services = data.value(configKey::services).toArray(); - m_services.push_back(service); + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + m_services.push_back(getApiServicesData(service.toObject())); + } } endResetModel(); @@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index) QJsonObject ApiServicesModel::getSelectedServiceInfo() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceInfo).toObject(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; } QString ApiServicesModel::getSelectedServiceType() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceType).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.type; } QString ApiServicesModel::getSelectedServiceProtocol() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceProtocol).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; } QString ApiServicesModel::getSelectedServiceName() { - auto modelIndex = index(m_selectedServiceIndex, 0); - return data(modelIndex, ApiServicesModel::Roles::NameRole).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; } QJsonArray ApiServicesModel::getSelectedServiceCountries() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::availableCountries).toArray(); + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; } QString ApiServicesModel::getCountryCode() @@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode() QString ApiServicesModel::getStoreEndpoint() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::storeEndpoint).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; } QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) @@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; - roles[WorkPeriodRole] = "workPeriod"; + roles[TimeLimitRole] = "timeLimit"; roles[RegionRole] = "region"; roles[FeaturesRole] = "features"; roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; return roles; } + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString(); + + if (serviceInfo.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 499189401..c96a49abc 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -3,6 +3,7 @@ #include #include +#include class ApiServicesModel : public QAbstractListModel { @@ -15,10 +16,11 @@ class ApiServicesModel : public QAbstractListModel ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ public slots: QHash roleNames() const override; private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + QString m_countryCode; - QJsonArray m_services; + QVector m_services; int m_selectedServiceIndex; }; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index c87499a73..b72b10c31 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -22,7 +22,7 @@ namespace constexpr char serviceProtocol[] = "service_protocol"; constexpr char publicKeyInfo[] = "public_key"; - constexpr char endDate[] = "end_date"; + constexpr char expiresAt[] = "expires_at"; } } @@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); - const QString endDate = publicKeyInfo.value(configKey::endDate).toString(); - if (endDate.isEmpty()) { - publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); serverConfig.insert(configKey::apiConfig, apiConfig); editServer(serverConfig, serverIndex); @@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) return false; } - auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC(); - if (endDateDateTime < QDateTime::currentDateTimeUtc()) { + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 0f18ea301..78bc22cc0 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ class ServersModel : public QAbstractListModel int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -115,6 +116,7 @@ public slots: QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); @@ -127,6 +129,9 @@ public slots: signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 120313cd1..600db85d1 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -54,8 +54,14 @@ PageType { imageSource: "qrc:/images/controls/download.svg" checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + if (index !== ApiCountryModel.currentIndex) { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 2d6c1d9be..167e56e5a 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -56,12 +56,15 @@ PageType { } LabelWithImageType { + property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable") + Layout.fillWidth: true Layout.margins: 16 imageSource: "qrc:/images/controls/history.svg" - leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period") + rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate") + : ApiServicesModel.getSelectedServiceData("workPeriod") visible: rightText !== "" } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 95ae5c8a6..ffcfb4411 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -25,6 +25,8 @@ PageType { property int pageSettingsApiServerInfo: 3 property int pageSettingsApiLanguageList: 4 + property var processedServer + defaultActiveFocusItem: focusItem Connections { @@ -35,8 +37,18 @@ PageType { } } + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -44,147 +56,139 @@ PageType { value: true } ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } Item { id: focusItem - KeyNavigation.tab: header + //KeyNavigation.tab: header } ColumnLayout { anchors.fill: parent - spacing: 16 - - Repeater { - id: header - model: proxyServersModel - - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() - } + spacing: 4 - delegate: ColumnLayout { + BackButtonType { + id: backButton - property alias focusItem: backButton + Layout.topMargin: 20 + KeyNavigation.tab: headerContent.actionButton - id: content + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } - Layout.topMargin: 20 + HeaderType { + id: headerContent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton + actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" + : "qrc:/images/controls/edit-3.svg" - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } + headerText: root.processedServer.name + descriptionText: { + if (root.processedServer.isServerFromGatewayApi) { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate") + } else { + return ApiServicesModel.getSelectedServiceData("serviceDescription") } + } else if (root.processedServer.isServerFromTelegramApi) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName } + } - HeaderType { - id: headerContent - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) { - return serverDescription - } else if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar + KeyNavigation.tab: tabBar - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.open() } + } + } - DrawerType2 { - id: serverNameEditDrawer + DrawerType2 { + id: serverNameEditDrawer - parent: root + parent: root - anchors.fill: parent - expandedHeight: root.height * 0.35 + anchors.fill: parent + expandedHeight: root.height * 0.35 - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } + 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() - } - } + 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 - } + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } - TextFieldWithHeaderType { - id: serverName + TextFieldWithHeaderType { + id: serverName - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true + Layout.fillWidth: true + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true - KeyNavigation.tab: saveButton - } + KeyNavigation.tab: saveButton + } - BasicButtonType { - id: saveButton + BasicButtonType { + id: saveButton - Layout.fillWidth: true + Layout.fillWidth: true - text: qsTr("Save") - KeyNavigation.tab: focusItem1 + text: qsTr("Save") + KeyNavigation.tab: focusItem1 - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return + } - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } + if (serverName.textFieldText !== root.processedServer.name) { + ServersModel.setProcessedServerData("name", serverName.textFieldText); } + serverNameEditDrawer.close() } } } @@ -257,8 +261,7 @@ PageType { StackLayout { id: nestedStackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + Layout.fillWidth: true currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? From d06924c59dd8684c28b6257efe5d1a11db34b19b Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Tue, 10 Dec 2024 03:17:16 +0100 Subject: [PATCH 8/8] feature/xray user management (#972) * feature: implement client management functionality for Xray --------- Co-authored-by: aiamnezia Co-authored-by: vladimir.kuznetsov --- client/configurators/xray_configurator.cpp | 147 ++++++++- client/configurators/xray_configurator.h | 4 + client/ui/controllers/exportController.cpp | 9 +- client/ui/controllers/exportController.h | 2 +- client/ui/models/clientManagementModel.cpp | 351 +++++++++++++++++++-- client/ui/models/clientManagementModel.h | 6 + client/ui/qml/Pages2/PageShare.qml | 2 +- 7 files changed, 485 insertions(+), 36 deletions(-) diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c5..514aa8211 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) +{ + // 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 ""; + } + + // 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 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); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } xrayShortId.replace("\n", ""); - if (errorCode != ErrorCode::NoError) { + // 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", xrayUuid); + 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 2acfdf71d..8ed4e7752 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ class XrayConfigurator : public ConfiguratorBase QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2690b5b1e..8681406e9 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index b031ea393..a2c9fcfaf 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -28,7 +28,7 @@ public slots: void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7445d60fb..f07eae712 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + + return appendClient(clientId, clientName, container, credentials, serverController); } ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, @@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + ErrorCode error = ErrorCode::NoError; + + // Get server config + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + // Get client ID to remove + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + // Remove client from server config + QJsonObject configObj = serverConfig.object(); + if (!configObj.contains("inbounds")) { + logger.error() << "Missing inbounds in xray config"; + return ErrorCode::InternalError; + } + + QJsonArray inbounds = configObj["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Empty inbounds array in xray config"; + return ErrorCode::InternalError; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings"; + return ErrorCode::InternalError; + } + + QJsonArray clients = settings["clients"].toArray(); + if (clients.isEmpty()) { + logger.error() << "Empty clients array in xray config"; + return ErrorCode::InternalError; + } + + for (int i = 0; i < clients.size(); ++i) { + QJsonObject clientObj = clients[i].toObject(); + if (clientObj.contains("id") && clientObj["id"].toString() == clientId) { + clients.removeAt(i); + break; + } + } + + // Update server config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + configObj["inbounds"] = inbounds; + + // Upload updated config + error = serverController->uploadTextFileToContainer( + container, + credentials, + QJsonDocument(configObj).toJson(), + serverConfigPath + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload updated xray config"; + return error; + } + + // Remove from local table + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + // Update clients table file on server + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable") + .arg(ContainerProps::containerTypeToString(container)); + + error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file"; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + error = serverController->runScript( + credentials, + serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container)) + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to restart xray container"; + return error; + } + + return error; +} + QHash ClientManagementModel::roleNames() const { QHash roles; @@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const roles[DataSentRole] = "dataSent"; roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 60132abe2..989120a9e 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -40,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -64,11 +66,15 @@ public slots: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 995fa3e76..d6ce78488 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -92,7 +92,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray"