Compare commits

..

77 Commits

Author SHA1 Message Date
aiamnezia f7e99d1d5d fix: teardown xray fast fix 2026-06-30 16:37:32 +04:00
aiamnezia 661fca64dd Merge branch 'dev' into feature/local-proxy-integration 2026-06-30 15:37:39 +04:00
vkamn 0f6847219b chore: bump version (#2772) 2026-06-26 13:04:36 +08:00
yp 5d16645b84 fix: android icons (#2725)
* update icons android

* remove comment
2026-06-24 00:08:21 +08:00
Yaroslav Gurov 203a092dc9 fix: blobs for macos-ne and codesigning of qt blobs (#2754) 2026-06-24 00:07:42 +08:00
yp d8b8590bc4 fix: XRay validation audit (#2749)
* flow default and config fixes

* host/SNI/path validation backend + flow-default flag

* input validation, numeric limits and live Save on settings pages

* MinMaxRowType clamping + DropDownType fit-content drawer
2026-06-24 00:07:26 +08:00
yp 9b8bfaa6f8 fix: regression testing vs 4.8.15.4 (#2730)
* fixed revoke

* fixed async update xray/mtproxy/telemt

* fixed connect premium config

* fixed autostart app hide

* fixed clear profile

* (6) fixed xtls-rprx-vision→empty

* (7) fixed appendClient abort & fix restore admin

* (8) fixed async|clientsUpdated

* fixed increment name server N

* remove comment & reset file

* chore: add tr to nextAvailableServerName

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-23 23:05:58 +08:00
Yaroslav Gurov 890103a16a fix: update amneziawg (#2743)
* chore(conan): update amneziawg

* fix(conan): use cmake 4.2+ to support MSVC26

* fix(ci/cd): use the latest cmake generator available on windows
2026-06-17 19:56:53 +07:00
yp 56ab82f87f fix: Use shared OpenSSL on Android (#2736) 2026-06-16 10:57:32 +07:00
lunardunno 3984acbb44 feat: updating install_docker.sh script (#2661)
* Updating install_docker.sh script

Implementing a Docker service status check.
The Docker reinstall step has been removed due to the implementation of Docker service checking.
Implementing locale checking and assignment.
Implementation of execution of some actions through commands with sudo, to reduce delays caused by differences in the values ​​of the PATH variable for the root user and the user included in the sudo group.
Implementation of a verification step for the install containerization app to avoid installing unsupported podman-docker applications.

* adding message handling to install controller

Adding handling for "Containerization app is not supported" and "Service status not active" messages to the controller.

* Error Codes added

Error Codes added for ServerContainerizationNotSupported & DockerServiceNotActive

* Adding extended descriptions of new errors

* fix last line in errorCodes.h

* fix last line in errorStrings.cpp

* Changing the names of errors

* various changes in the script

The messages output for processing by the server controller have been changed: "Container runtime is not supported" and "Container runtime service is not running."
The redundant check and output of the "Packet manager not found" message, as well as the interruption of script execution, have been eliminated, as this situation is handled by the server controller at an earlier stage (check_server_is_busy.sh) and only there.
Added installation of the whish package if it is missing from the OS, for subsequent re-execution of the install_docker.sh and check_server_is_busy.sh scripts.
Implemented an alternative method for detecting the package manager if the whish package is initially missing from the OS.
The algorithm for setting the $pm variable (package manager) has been changed.

* processed phrases have been changed

The phrases processed by the server controller have been changed.

* Attempting to use "command -v"

Switching to using "command -v" instead of "which".

* "which" as main, "command" as backup.

* "which" as main, "command" as backup for check user

* which  LOCK_CMD with sudo

Run the "which" with sudo to check the $LOCK_CMD variable in case the user's PATH variable has incorrect values ​​if the user is not root and is only a member of the sudo group.

* suppressing sudo password prompt

* suppressing sudo password prompt

* suppressing sudo password prompt install_docker.sh

* Changing the phrase for check stdout

"sudo:" with "not found" instead of "command not found"

* Changing phrases for check stdout check_user_in_sudo.sh‎

* sudo|docker and not found, in one line

* check only sudoers
2026-06-15 22:28:38 +07:00
yp cc404378f9 fix: remove only amnezia- prefixed docker volumes (#2728) 2026-06-15 13:12:19 +07:00
yp 594635e5cf fix: script remove docker volume (#2686)
* move sudo docker volume rm -f

* fix: remove unnecessary function

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-06-04 22:58:39 +08:00
vkamn f9b106cf5b fix: various fixes (#2693)
* fix: fixed country model update

* fix: fixed context menu crush on ios

* fix: fixed passphrase dialog freeze

* fix: fixed country switch

* fix: fixed start minimized

* fix: fixed black screen after remove container

* refactor: return cloak and ss only for view

* fix: fixed default server change after improt while connected

* fix: divider visibility

* fix: fixed revoke admin user

* fix: fixed language restore after backup

* fix: link hover for tor settings page

* fix: fixed openvpn connecntion status

* fix: fixed free color status

* fix: fixed client config update

* chore: bump version
2026-06-04 22:45:53 +08:00
yp a9861d18b7 fix: wrong index on xray pages (#2669)
* test crash xray

* fixed save config xray

* reset file

* fixed text port & reset file

* fixed textFieldWithHeaderType.textField
2026-06-01 12:22:54 +08:00
lunardunno c14138f031 fix: deleting volumes when cleaning the server (#2673)
* Deleting volumes when cleaning the server

* force the remove volumes
2026-06-01 11:54:34 +08:00
yyy-amnezia 60686fde24 fix: link OpenVPNAdapter statically (#2645)
* fix(ios): link OpenVPNAdapter statically

* chore(conan): simplify openvpnadapter merge process and make everything via XCrun

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-05-30 13:59:35 +08:00
aiamnezia be4df3594b Merge branch 'dev' into feature/local-proxy-integration 2026-05-29 05:44:48 +04:00
aiamnezia 5c8c143833 Merge branch 'dev' into feature/local-proxy-integration 2026-05-29 05:15:31 +04:00
Yaroslav Gurov bd0747296e fix: networkextension proper framework linking (#2668) 2026-05-28 23:09:49 +08:00
Yaroslav Gurov ba61019a50 fix: enable bundled openssl for every platform except NE-based ones (#2660)
* fix: enable bundled openssl for every platform except NE-based ones

* fix(conan): trigger CI/CD on crutial cmake changes

* fix: install dylibs/dlls from conan and use proper RPATH

* fix: adjust windows runtime deps
2026-05-28 19:17:27 +08:00
vkamn 113f967006 fix: various fixes (#2664)
* fix: fixed nextAvailableServerName

* fix: fixed password request for ssh key
2026-05-28 15:14:04 +08:00
yp bcee58b08b feat: add captcha (#2508)
* test capcha

* add test AMNEZIA_GATEWAY_PLAINTEXT_MOCK

* ref

* remove first QNetworkReply::NoError

* fixed macros

* fixed http code

* add test server

* fix cmake

* add CAPTCHA refreshed

* fixed captcha

* update QML Captha

* fixed crash app & up vercion & fix qml captha

* ver 4.9.0.1

* remove m_gatewayCaptchaStickyBase & outEffectiveRequestBase

* reset code PR

* remove mock & temp var AMNEZIA_LOCAL_GATEWAY

* ref code & remove AMNEZIA_LOCAL_GATEWAY

* remove check httpStatusCode & error

* add 408 status code

* fix update captca

* remove fallback на transport

* chore: add loader after captcha solved

* chore: remove logs from api utils

* chore: minor fixes

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 13:51:26 +08:00
MrMirDan 52de1acebf refactoring: move tests to separate repo (#2550)
* fix: AUTOMOC and AUTOUIC added

* update: native exports test

* update: export test

* update: vless serialization/deserialization test added

* update: language model and controller test

* update: sites UiController and Model test

* update: sites test

* update: app ui model and controller test

* update: allowed dns ui model and controller test

* update: env vars and removed some lines

* update: news ui model and controller test (incomplete)

* update: api services ui model and controller test (incompleted)

* update: job for tests (Linux)

* update: job for tests (Windows)

* update: proper artifact names

* update: added envs

* update: added 'get sources' and changed steps order

* update: tests jobs remake

* update: 'get sources' step and windows shell

* update: using ctest

* search for exe files

* changed path to run tests

* update: 'Build' step

* update: changed path to deploy qt dependencies

* update: dependencies only for tests executables

* update: ctest dir

* update: include ctest

* update: set dir for tests exe

* update: qt path

* update: serialization test

* update: removed api tests from cmake

* update: changed tests dir

* added ctest to client cmake

* update: installing msvc and additional checks

* removed mcvs install

* update: path to ssh.dll

* fixed issue with ssh path

* update: removed unneccessary step and line

* update: linux job step 'Run tests'

* update: linux 'Install dependencies'

* update: modified qtest include

* update: changed QVERIFY to QVERIFY2

* update: some qverify2 messages

* update: linux additional dependencies

* update: offscreen for linux tests

* update: MacOS tests job

* update: Android tests job

* update: rewrited env's, qtest include and clear clients in some tests

* update: added local vars file for tests

* proper path for some vars

* some fixes due merge

* update: windows tests deploy

* python and conan installation to tests jobs

* chore: minor fixes after merge with dev

* chore: move selfhosted admin tests to separate folder

* refactor: some rename

* chore: fixes after merge

* refactor: moved tests to separate repo

* refactor: remove tests from core controller

* chore: add more protected getters to core controller

* chore: add more protected getters

* chore: remove ctest

* chore: return xray model default values

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 12:31:27 +08:00
yp 027a12a1df fix: extended VLESS configuration (#2643)
* fixed vless

* fixed default var

* fixed save button

* remove comment

* fix: fixed header link in xray settings page

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 12:21:46 +08:00
yp 0a659a2d74 fix: various fixes for MTProxy & Telemt (#2653)
* fix color & fix enabled

* fixed remove base secret

* fix mtproxy/telemt 'base secret'

* fixed button back

* fixed loader

* fixed reload loader

* fixed dd secret

* fixed qml

* fix: fixed header link in mtproxy/telemt page

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-05-28 11:46:26 +08:00
vkamn 6f119cd083 fix: various fixes (#2662)
* fix: fixed dns processing

* fix: fixed proceesed index/id selection

* refactor: stop using the server index as state

* fix: fixed autostart and start minimized

* fix: fixed typo

* fix: add socks5 extractConfigFromContainer

* fix: remove unused currentContainerUpdated

* fix: fixed clear cached profile order
2026-05-28 10:57:08 +08:00
Yaroslav Gurov 1753aed3fc fix: use shared openssl on Android (#2657)
* feat(conan): clone openssl and patch it for Android

* fix(conan): build shared libssl for Android
2026-05-26 21:59:47 +08:00
Yaroslav Gurov c714d98bd1 chore: extend prebuilts support for MacOS NE by clang 21+ (#2633)
* chore: extend prebuilts support for MacOS NE by clang 21+

* chore: trigger prebuilts on workflow file changes
2026-05-25 22:05:56 +08:00
vkamn 4787f3915b chore: defines for update controller (#2634) 2026-05-21 20:40:04 +08:00
Yaroslav Gurov 7a383116b2 fix: force os.sdk to empty on Darwin (#2632) 2026-05-21 18:56:53 +08:00
Yaroslav Gurov d3de5f0f48 fix(conan): openvpn support in MSVC+Ninja setup (#2616) 2026-05-21 18:09:52 +08:00
vkamn 8749d683e3 chore: minor fixes (#2630) 2026-05-21 11:38:41 +08:00
vkamn 9de9d082bc chore: bump version (#2629) 2026-05-21 10:41:25 +08:00
Yaroslav Gurov a4233fef41 fix: add ssh init and finalize for statically-linked libssh (#2627) 2026-05-21 10:19:08 +08:00
Yaroslav Gurov 4890dd1d74 chore: compare changes against base branch of the PR (#2626) 2026-05-20 21:26:41 +08:00
Yaroslav Gurov 564630827e сhore: apple ci cd macos versions (#2625)
* chore(ci/cd): use macos-26 for xcode>26.4

* chore(ci/cd): bump conan version

* chore(conan): remove redundant VirtualBuildEnv
2026-05-20 21:19:56 +08:00
Yaroslav Gurov fbe15d965b chore: bump apple hev-socks5-tunnel dep (#2624)
* chore(conan): bump hev-socks5-tunnel

* chore(conan): bump xcode-versions

* chore(conan): upload prebuilts only in case of pushing to dev
2026-05-20 20:31:35 +08:00
vkamn b29515c380 chore: rename artifacts (#2622) 2026-05-20 19:07:30 +08:00
vkamn 0658a8f565 revert: regional country codes UI (#2567) (#2621) 2026-05-20 13:55:28 +08:00
vkamn 482ec04b4a chore: bump version (#2620)
* chore: bump version

* chore: bump android qt version
2026-05-20 12:37:38 +08:00
vkamn d40d24fcf9 fix: fixed validateAndPrepareConfig for non admin configs (#2617)
* fix: fixed validateAndPrepareConfig for non admin configs

* fix: fix fetchGatewayUrl lambda context
2026-05-20 12:37:22 +08:00
aiamnezia 24f545d001 Merge branch 'dev' into feature/local-proxy-integration 2026-04-21 16:14:49 +04:00
aiamnezia 2b0d86c626 fix: waiting starting xray before setting running flag 2026-04-13 11:04:19 +04:00
aiamnezia fe773a108e fix: waiting VPN disconnect before proxy turning on 2026-04-13 10:19:01 +04:00
aiamnezia a0997156f6 fix: restoring server_uuid within config update 2026-04-13 09:40:23 +04:00
aiamnezia 9a6e975622 fix: stop xray before closing the app 2026-04-13 09:24:55 +04:00
aiamnezia 78f09634a4 fix: prevent stopping Xray due to changing local proxy port
- Added a check in XrayController::stop() to skip the stop operation if Xray is not currently running, with a debug log for clarity.
2026-04-13 07:31:26 +04:00
aiamnezia be692001b0 fix: fix local proxy settings and restart logic
- Added local proxy restart token management in Settings.
- Implemented logic to handle local proxy state on application quit.
- Updated ProxyServer to restart based on configuration changes.
- Enhanced API configuration updates to bump restart token when necessary.
- Improved UI components to reflect local proxy availability and state.
- Added new error handling and notifications for local proxy operations.
2026-04-13 07:14:42 +04:00
aiamnezia 850b8ea03b Merge branch 'dev' into feature/local-proxy-integration 2026-04-13 03:07:45 +04:00
aiamnezia c645d07a87 chore: fix build 2026-03-24 16:39:59 +04:00
aiamnezia 7f8786720e Merge branch 'dev' into feature/local-proxy-integration 2026-03-24 15:48:49 +04:00
aiamnezia 25c70b5bf6 fix: fix android build 2026-02-13 17:21:15 +04:00
aiamnezia 7bf16f075c feat: add mutual exclusion between local proxy and vpn 2026-02-13 17:07:35 +04:00
aiamnezia 6518d4866e fix: Fix local proxy UI 2026-02-13 16:46:49 +04:00
aiamnezia 4c2010244b feat: Update local proxy settings page 2026-01-27 15:09:02 +04:00
aiamnezia e946ee2430 feat: enhance ConfigManager with dynamic proxy port resolution and availability check
- Added functionality to resolve and validate the local proxy port within a specified range.
- Implemented a method to check if a port is available before applying it to the configuration.
- Updated ProxyService to handle the new port resolution logic and cache the parsed configuration.
2026-01-27 12:45:38 +04:00
aiamnezia 5fab8363e7 feat: made API port static and proxy port configurable 2026-01-27 11:09:27 +04:00
aiamnezia 35c2e1564b chore: add debug 2026-01-23 15:33:25 +04:00
aiamnezia ca5bca085b feat: remove logs for local proxy 2026-01-19 16:25:59 +04:00
aiamnezia 33b2a3d2fc Merge branch 'dev' into feature/local-proxy-integration 2026-01-16 17:48:48 +04:00
aiamnezia 917f5858a8 fix: update ProxyService to use new configuration fetching method 2026-01-15 23:35:39 +04:00
aiamnezia e98c11079a feat: enhance ConfigManager with fetch capabilities for Xray configuration 2026-01-15 23:05:36 +04:00
aiamnezia 84d908d4d8 feat: enhance local proxy error handling and configuration management
- Added signal for local proxy start failure to Settings.
- Updated CoreController to emit failure signals with descriptive messages when local proxy fails to start.
- Refactored XrayController to accept JSON configuration directly, improving configuration handling.
- Removed unused config file loading logic to streamline the XrayController's functionality.
2026-01-15 21:04:10 +04:00
aiamnezia 412e69af9b feat: update ConfigManager and ProxyServer to utilize Settings
- Modified ConfigManager to accept a Settings object for improved configuration management.
- Updated ProxyServer to initialize with Settings, enhancing dependency injection.
2025-12-31 21:16:45 +04:00
aiamnezia 4492b0af7e fix: fix local proxy page 2025-12-31 20:15:40 +04:00
aiamnezia 806b1d75af refactor: streamline HTTP API for Xray control
- Removed outdated config management routes and consolidated Xray control endpoints.
- Updated response structures to ensure consistent error handling across API calls.
2025-12-31 19:30:29 +04:00
aiamnezia 9740e7557a feat: add dynamic local proxy lifecycle in depence of settings 2025-12-31 18:48:57 +04:00
aiamnezia 5ab82e2196 chore: fix android build 2025-12-30 16:59:15 +04:00
aiamnezia ae44de7101 chore: fix win build 2025-12-30 16:05:05 +04:00
aiamnezia 2be6079e21 refactor: enhance XrayController to use IPC for process management
- Replaced direct process management with IPC calls for starting and stopping the Xray process.
- Improved error handling for IPC communication and config file loading.
- Removed unused methods and variables related to direct process handling.
- Updated logging to reflect changes in process management.
2025-12-30 15:33:59 +04:00
aiamnezia 2a653f8876 feat: implement local proxy settings UI and functionality
- Added new QML pages for managing local proxy settings and connection types.
- Updated SettingsController to handle local proxy enablement and port configuration.
- Enhanced server model to include processed server UUID for local proxy management.
2025-12-30 14:19:32 +04:00
aiamnezia 41ab51a5ef feat: add server UUID management and local proxy settings
- Implemented UUID migration for servers to ensure each server has a unique identifier.
- Added methods for managing local proxy settings, including owner UUID, port, and HTTP enablement.
- Updated server model to include server UUID role for better data handling.
2025-12-30 11:02:59 +04:00
aiamnezia 300558c33c Merge branch 'dev' into feature/local-proxy-integration 2025-12-19 14:18:26 +04:00
aiamnezia 774deb87f5 fix build scripts 2025-08-08 09:44:50 +04:00
aiamnezia bae7fbd222 Add qtwebsockets in deploy.yml 2025-08-08 09:14:35 +04:00
aiamnezia 97e4b95673 Add qthttpserver deploy.yml 2025-08-08 09:07:28 +04:00
aiamnezia 2ae97c5cda feat: Added local proxy server 2025-08-08 06:44:18 +04:00
246 changed files with 7102 additions and 8418 deletions
+60 -33
View File
@@ -18,11 +18,14 @@ jobs:
- uses: dorny/paths-filter@v3
id: filter
with:
base: ${{ github.event.before }}
filters: |
recipes:
- 'recipes/**'
- 'conanfile.py'
- '.github/workflows/deploy.yml'
- 'cmake/conan_provider.cmake'
- 'cmake/platform_settings.cmake'
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux:
runs-on: ubuntu-latest
@@ -40,7 +43,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
shell: bash
@@ -50,9 +53,11 @@ jobs:
done
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -83,7 +88,7 @@ jobs:
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -98,7 +103,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Install system packages'
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
@@ -118,7 +123,7 @@ jobs:
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-Linux.run
path: deploy/build/AmneziaVPN_*_linux_x64.run
archive: false
retention-days: 7
@@ -149,15 +154,17 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
run: cmake -S . -B build -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -194,7 +201,7 @@ jobs:
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -229,7 +236,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build project'
shell: cmd
@@ -242,27 +249,31 @@ jobs:
- name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-win64.msi
path: deploy/build/AmneziaVPN_*_windows_x64.msi
archive: false
retention-days: 7
- name: 'Upload IFW installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-win64.exe
path: deploy/build/AmneziaVPN_*_windows_x64.exe
archive: false
retention-days: 7
# ------------------------------------------------------
Bake-Prebuilts-iOS:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [26.0]
xcode-version: [26.0, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
@@ -279,15 +290,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -344,7 +357,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.1'
xcode-version: '26.0'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v3
@@ -352,7 +365,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
arch: 'clang_64'
dir: ${{ runner.temp }}
set-env: 'true'
@@ -364,7 +377,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'ios'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -376,7 +389,7 @@ jobs:
python-version: 3.14
- name: 'Install deps'
run: pip install "conan==2.26.2" jsonschema jinja2
run: pip install "conan==2.28.0" jsonschema jinja2
- name: 'Build project'
env:
@@ -394,14 +407,17 @@ jobs:
# ------------------------------------------------------
Bake-Prebuilts-MacOS:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [16.2, 16.4]
xcode-version: [16.2, 16.4, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
@@ -418,15 +434,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -488,7 +506,7 @@ jobs:
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -502,7 +520,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build project'
env:
@@ -518,20 +536,24 @@ jobs:
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN-*-Darwin.pkg
path: deploy/build/AmneziaVPN_*_macos_x64.pkg
archive: false
retention-days: 7
# ------------------------------------------------------
Bake-Prebuilts-MacOS-NE:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [16.2, 16.4]
xcode-version: [16.2, 16.4, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
@@ -548,15 +570,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -635,7 +659,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Build project'
run: |
@@ -671,7 +695,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Setup Android SDK'
uses: android-actions/setup-android@v4
@@ -696,9 +720,11 @@ jobs:
done
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -712,8 +738,8 @@ jobs:
env:
ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.1
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
QT_VERSION: 6.10.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools qthttpserver qtwebsockets'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
@@ -806,7 +832,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
run: pip install "conan==2.28.0"
- name: 'Decode keystore secret to file'
env:
@@ -894,3 +920,4 @@ jobs:
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
+3 -1
View File
@@ -10,7 +10,9 @@ deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
.cursorignore
.cursor/
.venv/
# Qt-es
/.qmake.cache
+4 -4
View File
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.2)
set(AMNEZIAVPN_VERSION 4.9.0.3)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -18,9 +18,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
# trigger conan to kick off `conan install`
find_package(OpenSSL REQUIRED)
return()
endif()
@@ -28,7 +28,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 2120)
set(APP_ANDROID_VERSION_CODE 2126)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
+31 -6
View File
@@ -14,6 +14,10 @@ set(PACKAGES
Core5Compat Concurrent LinguistTools
)
if(NOT ANDROID AND NOT IOS)
list(APPEND PACKAGES HttpServer)
endif()
execute_process(
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMAND git rev-parse --short HEAD
@@ -47,6 +51,10 @@ set(LIBS ${LIBS}
Qt6::Core5Compat Qt6::Concurrent
)
if(NOT ANDROID AND NOT IOS)
list(APPEND LIBS Qt6::HttpServer)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
endif()
@@ -193,10 +201,6 @@ elseif(APPLE)
include(cmake/macos.cmake)
endif()
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
add_subdirectory(tests)
endif()
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
@@ -216,11 +220,32 @@ endif()
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET client_deps
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
if(APPLE)
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/AmneziaVPN.app/Contents/Frameworks)
else()
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
endif()
install(RUNTIME_DEPENDENCY_SET client_deps
PRE_EXCLUDE_REGEXES
[[api-ms-win-.*]]
[[ext-ms-.*]]
[[kernel32\.dll]]
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]
[[^/usr/lib.*]]
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
)
set(deploy_tool_options "")
+6 -3
View File
@@ -109,9 +109,6 @@ void AmneziaApplication::init()
// install filter on main window
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
win->installEventFilter(this);
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->setDefaultAlphaBuffer(true);
#endif
#ifdef Q_OS_ANDROID
QObject::connect(win, &QQuickWindow::sceneGraphError,
[](QQuickWindow::SceneGraphError, const QString &msg) {
@@ -122,7 +119,13 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
win->show();
}
#endif
}
},
Qt::QueuedConnection);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#2A2A2E"
android:centerColor="#17171A"
android:endColor="#0E0E11" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_amnezia_round"
android:insetLeft="19.5%"
android:insetTop="19.5%"
android:insetRight="19.5%"
android:insetBottom="19.5%" />
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE8E8EC"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#38FFFFFF" />
</shape>
@@ -8,75 +8,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<org.amnezia.vpn.PairingQrScanOverlayView
android:id="@+id/pairingScanOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<LinearLayout
android:id="@+id/pairingChrome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="@android:color/transparent"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingTop="28dp"
android:paddingEnd="16dp"
android:paddingBottom="12dp"
android:visibility="gone">
<ImageButton
android:id="@+id/pairingBack"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pairing_qr_camera_back"
android:padding="12dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_pairing_back" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/pairingTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pairing_qr_camera_title"
android:textColor="#FFE8E8EC"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/pairingSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/pairing_qr_camera_subtitle"
android:textColor="#FFB8B8C0"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/torchButton"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="32dp"
android:background="@drawable/torch_fab_bg"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:text="🔦"
android:textSize="26sp"
android:contentDescription="@string/camera_torch" />
</FrameLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

-8
View File
@@ -24,13 +24,5 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
<string name="cameraPermissionDialogTitle">Доступ к камере</string>
<string name="cameraPermissionDialogMessage">Чтобы отсканировать QR-код для добавления устройства, Amnezia VPN нужен доступ к камере.</string>
<string name="cameraPermissionContinue">Продолжить</string>
<string name="camera_torch">Фонарик</string>
<string name="pairing_qr_camera_title">Добавить устройство по QR</string>
<string name="pairing_qr_camera_subtitle">Отсканируйте QR сессии на устройстве, которое хотите добавить. Перед отправкой подписки будет подтверждение.</string>
<string name="pairing_qr_camera_back">Назад</string>
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/icon</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0E0E11</color>
</resources>
-8
View File
@@ -24,13 +24,5 @@
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string>
<string name="cameraPermissionDialogTitle">Camera access</string>
<string name="cameraPermissionDialogMessage">To scan a QR code for device pairing, Amnezia VPN needs access to the camera.</string>
<string name="cameraPermissionContinue">Continue</string>
<string name="camera_torch">Flashlight</string>
<string name="pairing_qr_camera_title">Add device via QR</string>
<string name="pairing_qr_camera_subtitle">Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent.</string>
<string name="pairing_qr_camera_back">Back</string>
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources>
@@ -42,9 +42,6 @@ import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
@@ -76,18 +73,12 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val CHECK_CAMERA_PERMISSION_ACTION_CODE = 5
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
class AmneziaActivity : QtActivity(), LifecycleOwner {
private val lifecycleRegistry = LifecycleRegistry(this)
override val lifecycle: Lifecycle
get() = lifecycleRegistry
class AmneziaActivity : QtActivity() {
private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>()
@@ -108,8 +99,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private var lastPairingQrReaderStartUptimeMs: Long = 0L
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
@@ -216,7 +205,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -274,7 +262,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
override fun onStart() {
super.onStart()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Log.d(TAG, "Start Amnezia activity")
mainScope.launch {
qtInitialized.await()
@@ -298,7 +285,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
qtInitialized.await()
QtAndroidController.onServiceDisconnected()
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
super.onStop()
}
@@ -371,7 +357,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
@@ -382,7 +367,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
override fun onResume() {
super.onResume()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
@@ -499,7 +483,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
mainScope.cancel()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
super.onDestroy()
}
@@ -897,66 +880,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@Suppress("unused")
fun isCameraPermissionGranted(): Boolean =
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
@Suppress("unused")
fun requestCameraPermissionForQrPairing() {
if (isCameraPermissionGranted()) {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onCameraPermissionResult(true)
}
return
}
runOnUiThread {
AlertDialog.Builder(this)
.setTitle(R.string.cameraPermissionDialogTitle)
.setMessage(R.string.cameraPermissionDialogMessage)
.setNegativeButton(R.string.cancel) { _, _ ->
mainScope.launch {
qtInitialized.await()
QtAndroidController.onCameraPermissionResult(false)
}
}
.setPositiveButton(R.string.cameraPermissionContinue) { _, _ ->
requestPermission(
Manifest.permission.CAMERA,
CHECK_CAMERA_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onCameraPermissionResult(true)
}
},
onFail = {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onCameraPermissionResult(false)
}
},
onAny = {}
)
)
}
.show()
}
}
@Suppress("unused")
fun openApplicationDetailsSettings() {
try {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
startActivity(this)
}
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "openApplicationDetailsSettings: $e")
}
}
@Suppress("unused")
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
@@ -1005,19 +928,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
}
}
@Suppress("unused")
fun startPairingQrCodeReader() {
val now = SystemClock.uptimeMillis()
if (now - lastPairingQrReaderStartUptimeMs < 1200L) {
return
}
lastPairingQrReaderStartUptimeMs = now
Intent(this, CameraActivity::class.java).also {
it.putExtra(CameraActivity.EXTRA_PAIRING_QR_CAMERA, true)
startActivity(it)
}
}
@Suppress("unused")
fun setSaveLogs(enabled: Boolean) {
Log.v(TAG, "Set save logs: $enabled")
@@ -1269,7 +1179,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION"
else -> actionCode.toString()
}
}
@@ -2,384 +2,47 @@ package org.amnezia.vpn
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP
import android.graphics.RectF
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringAction.FLAG_AE
import androidx.camera.core.FocusMeteringAction.FLAG_AF
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.camera.view.TransformExperimental
import androidx.camera.view.transform.CoordinateTransform
import androidx.camera.view.transform.ImageProxyTransformFactory
import androidx.camera.view.transform.OutputTransform
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions.Builder
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import org.amnezia.vpn.databinding.CameraPreviewBinding
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.roundToInt
private const val TAG = "CameraActivity"
@OptIn(TransformExperimental::class)
class CameraActivity : ComponentActivity() {
companion object {
const val EXTRA_PAIRING_QR_CAMERA = "org.amnezia.vpn.extra.PAIRING_QR_CAMERA"
}
private lateinit var viewBinding: CameraPreviewBinding
private var cameraProvider: ProcessCameraProvider? = null
private var boundCamera: Camera? = null
private var boundImageAnalysis: ImageAnalysis? = null
private var torchOn: Boolean = false
private var imageAnalysisExecutor: ExecutorService? = null
private val qrHandledOrClosing = AtomicBoolean(false)
private var pairingQrDeliveredToQt = false
private var pairingQrUserDismissedCamera = false
private var barcodeScanner: BarcodeScanner? = null
private val cachedPreviewOutputTransform = AtomicReference<OutputTransform?>(null)
private var previewTransformLayoutListener: View.OnLayoutChangeListener? = null
private var previewStreamStateObserver: Observer<PreviewView.StreamState>? = null
@Volatile
private var pairingGeomHeaderBottomPx = 0f
@Volatile
private var pairingGeomStatusBarTopPx = 0f
@Volatile
private var pairingGeomDensity = 1f
private lateinit var cameraProvider: ProcessCameraProvider
@ExperimentalGetImage
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = CameraPreviewBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
viewBinding.viewFinder.scaleType = PreviewView.ScaleType.FILL_CENTER
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
WindowCompat.setDecorFitsSystemWindows(window, false)
val density = resources.displayMetrics.density
val padH = (8 * density).toInt()
val padTopBase = (28 * density).toInt()
val padBottom = (12 * density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.pairingChrome) { v, windowInsets ->
val bars = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
v.setPadding(padH, padTopBase + bars.top, (16 * density).toInt(), padBottom)
v.post { onPairingLayoutGeometryChanged() }
windowInsets
}
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
viewBinding.pairingChrome.visibility = View.VISIBLE
viewBinding.root.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
viewBinding.root.post { onPairingLayoutGeometryChanged() }
}
viewBinding.root.post {
onPairingLayoutGeometryChanged()
applyPairingTorchButtonChrome()
}
}
viewBinding.pairingBack.setOnClickListener { releaseCameraAndFinish() }
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
releaseCameraAndFinish()
}
}
)
viewBinding.torchButton.setOnClickListener {
torchOn = !torchOn
try {
boundCamera?.cameraControl?.enableTorch(torchOn)
} catch (e: Exception) {
Log.e(TAG, "Torch: $e")
}
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
applyPairingTorchButtonChrome()
}
}
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (!intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
return
}
if (!::viewBinding.isInitialized) {
return
}
cleanupCameraResources()
qrHandledOrClosing.set(false)
pairingQrDeliveredToQt = false
pairingQrUserDismissedCamera = false
torchOn = false
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
viewBinding.pairingChrome.visibility = View.VISIBLE
viewBinding.root.post {
onPairingLayoutGeometryChanged()
applyPairingTorchButtonChrome()
}
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
}
override fun onDestroy() {
cleanupCameraResources()
val pairing = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
if (pairing && !pairingQrDeliveredToQt && !pairingQrUserDismissedCamera) {
try {
QtAndroidController.onPairingQrCameraClosed()
} catch (t: Throwable) {
Log.e(TAG, "onPairingQrCameraClosed: $t")
}
}
super.onDestroy()
}
/** Idempotent: safe from back, successful decode, or process death. */
private fun cleanupCameraResources() {
qrHandledOrClosing.set(true)
try {
boundImageAnalysis?.clearAnalyzer()
} catch (_: Exception) {
}
boundImageAnalysis = null
try {
barcodeScanner?.close()
} catch (_: Exception) {
}
barcodeScanner = null
try {
boundCamera?.cameraControl?.enableTorch(false)
} catch (_: Exception) {
}
boundCamera = null
try {
cameraProvider?.unbindAll()
} catch (_: Exception) {
}
imageAnalysisExecutor?.let { ex ->
try {
ex.shutdown()
} catch (_: Exception) {
}
}
imageAnalysisExecutor = null
previewTransformLayoutListener?.let { listener ->
if (::viewBinding.isInitialized) {
viewBinding.viewFinder.removeOnLayoutChangeListener(listener)
}
}
previewTransformLayoutListener = null
previewStreamStateObserver?.let { obs ->
if (::viewBinding.isInitialized) {
viewBinding.viewFinder.previewStreamState.removeObserver(obs)
}
}
previewStreamStateObserver = null
cachedPreviewOutputTransform.set(null)
}
private fun refreshCachedPreviewOutputTransform() {
if (!::viewBinding.isInitialized) {
return
}
val vf = viewBinding.viewFinder
try {
val out = vf.outputTransform
cachedPreviewOutputTransform.set(out)
} catch (t: Throwable) {
Log.e(TAG, "refreshCachedPreviewOutputTransform: $t")
cachedPreviewOutputTransform.set(null)
}
}
private fun scheduleCachedPreviewOutputTransformRefresh() {
if (!::viewBinding.isInitialized) {
return
}
viewBinding.viewFinder.post { refreshCachedPreviewOutputTransform() }
}
private fun onPairingLayoutGeometryChanged() {
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
return
}
val root = viewBinding.root
val chrome = viewBinding.pairingChrome
val w = root.width
val h = root.height
if (w <= 0 || h <= 0) {
return
}
val density = resources.displayMetrics.density
val headerBottom = if (chrome.visibility == View.VISIBLE) chrome.bottom.toFloat() else 0f
val insets = ViewCompat.getRootWindowInsets(root)
val statusTop = insets?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
val safeBottom = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom?.toFloat() ?: 0f
pairingGeomHeaderBottomPx = headerBottom
pairingGeomStatusBarTopPx = statusTop
pairingGeomDensity = density
viewBinding.pairingScanOverlay.setPairingHeaderBottomPx(headerBottom)
val hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, headerBottom, statusTop, density)
val torchCy = PairingQrScanGeometry.pairingIosStyleTorchCenterYPx(
hole.bottom,
h.toFloat(),
headerBottom,
safeBottom,
density
)
val torchSizePx = (56f * density).roundToInt().coerceAtLeast(1)
val topMargin = (torchCy - torchSizePx / 2f).roundToInt().coerceAtLeast(0)
val wantGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
viewBinding.torchButton.post {
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
return@post
}
val btn = viewBinding.torchButton
val lp = btn.layoutParams as FrameLayout.LayoutParams
if (lp.gravity == wantGravity && lp.topMargin == topMargin && lp.bottomMargin == 0) {
return@post
}
lp.gravity = wantGravity
lp.topMargin = topMargin
lp.bottomMargin = 0
btn.layoutParams = lp
}
}
private fun applyPairingTorchButtonChrome() {
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
return
}
val btn = viewBinding.torchButton
val d = resources.displayMetrics.density
val alpha = if (torchOn) (0.42f * 255f).toInt() else (0.22f * 255f).toInt()
val bg = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(Color.argb(alpha, 255, 255, 255))
if (torchOn) {
setStroke((2f * d).roundToInt(), Color.rgb(255, 191, 115))
} else {
setStroke(0, 0)
}
}
btn.background = bg
}
private fun pairingHoleRectInImageSpace(
viewFinder: PreviewView,
imageProxy: ImageProxy,
imageWidth: Int,
imageHeight: Int
): RectF {
val vw = viewFinder.width
val vh = viewFinder.height
fun geomFallback(): RectF =
PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
vw,
vh,
pairingGeomHeaderBottomPx,
pairingGeomStatusBarTopPx,
pairingGeomDensity,
imageWidth,
imageHeight
)
if (vw <= 0 || vh <= 0 || imageWidth <= 0 || imageHeight <= 0) {
return geomFallback()
}
return try {
val previewOut = cachedPreviewOutputTransform.get()
if (previewOut == null) {
geomFallback()
} else {
val imageFactory = ImageProxyTransformFactory().apply {
setUsingRotationDegrees(true)
}
val imageOut = imageFactory.getOutputTransform(imageProxy)
val holeView = PairingQrScanGeometry.pairingIosStyleHoleRectF(
vw,
vh,
pairingGeomHeaderBottomPx,
pairingGeomStatusBarTopPx,
pairingGeomDensity
)
if (holeView.width() <= 0f || holeView.height() <= 0f) {
return geomFallback()
}
val hole = RectF(holeView)
CoordinateTransform(previewOut, imageOut).mapRect(hole)
hole
}
} catch (t: Throwable) {
Log.e(TAG, "pairingHoleRectInImageSpace: $t")
geomFallback()
}
}
private fun releaseCameraAndFinish() {
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
pairingQrUserDismissedCamera = true
try {
QtAndroidController.onPairingQrCameraUserDismissed()
} catch (t: Throwable) {
Log.e(TAG, "onPairingQrCameraUserDismissed: $t")
}
}
cleanupCameraResources()
finish()
}
private fun checkPermissions(onSuccess: () -> Unit, onFail: () -> Unit) {
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
onSuccess()
@@ -404,41 +67,26 @@ class CameraActivity : ComponentActivity() {
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
bindCameraUseCases()
bindPreview()
bindImageAnalysis()
}, ContextCompat.getMainExecutor(this))
}
@SuppressLint("ClickableViewAccessibility")
@ExperimentalGetImage
private fun bindCameraUseCases() {
val provider = cameraProvider ?: return
imageAnalysisExecutor?.shutdown()
imageAnalysisExecutor = Executors.newSingleThreadExecutor()
private fun bindPreview() {
val viewFinder = viewBinding.viewFinder
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val camera = provider.bindToLifecycle(
this,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
boundCamera = camera
boundImageAnalysis = imageAnalysis
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview)
viewFinder.setOnTouchListener { _, motionEvent ->
when (motionEvent.action) {
ACTION_DOWN -> true
ACTION_UP -> {
val point = viewFinder
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.x)
val action = FocusMeteringAction
.Builder(point, FLAG_AF or FLAG_AE).build()
@@ -450,121 +98,58 @@ class CameraActivity : ComponentActivity() {
else -> false
}
}
}
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
previewTransformLayoutListener?.let { viewFinder.removeOnLayoutChangeListener(it) }
val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
viewFinder.post {
scheduleCachedPreviewOutputTransformRefresh()
onPairingLayoutGeometryChanged()
}
}
previewTransformLayoutListener = layoutListener
viewFinder.addOnLayoutChangeListener(layoutListener)
previewStreamStateObserver?.let { viewFinder.previewStreamState.removeObserver(it) }
val streamObserver = Observer<PreviewView.StreamState> { state ->
if (state == PreviewView.StreamState.STREAMING) {
viewFinder.post {
scheduleCachedPreviewOutputTransformRefresh()
onPairingLayoutGeometryChanged()
}
}
}
previewStreamStateObserver = streamObserver
viewFinder.previewStreamState.observe(this, streamObserver)
scheduleCachedPreviewOutputTransformRefresh()
}
@ExperimentalGetImage
private fun bindImageAnalysis() {
val imageAnalysis = ImageAnalysis.Builder().build()
try {
barcodeScanner?.close()
} catch (_: Exception) {
}
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis)
barcodeScanner = BarcodeScanning.getClient(
val barcodeScanner = BarcodeScanning.getClient(
Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
.setZoomSuggestionOptions(
ZoomSuggestionOptions.Builder { zoomLevel ->
camera.cameraControl.setZoomRatio(zoomLevel)
true
}.apply {
camera.cameraInfo.zoomState.value?.maxZoomRatio?.let { maxZoomRation ->
setMaxSupportedZoomRatio(maxZoomRation)
}
}.build()
).build()
)
// optimization
val checkedBarcodes = hashSetOf<String>()
val analysisExecutor = imageAnalysisExecutor!!
val mainExecutor = ContextCompat.getMainExecutor(this)
val pairingQrMode = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
imageAnalysis.setAnalyzer(analysisExecutor) { imageProxy ->
if (qrHandledOrClosing.get()) {
imageProxy.close()
return@setAnalyzer
}
val mediaImage = imageProxy.image
if (mediaImage == null) {
imageProxy.close()
return@setAnalyzer
}
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val viewW = viewFinder.width
val viewH = viewFinder.height
val pairingRoi = if (pairingQrMode) {
pairingHoleRectInImageSpace(viewFinder, imageProxy, image.width, image.height)
} else {
null
}
val scanner = barcodeScanner ?: run {
imageProxy.close()
return@setAnalyzer
}
scanner.process(image)
.addOnSuccessListener(mainExecutor) { barcodes ->
if (qrHandledOrClosing.get()) {
return@addOnSuccessListener
}
val barcode = if (pairingQrMode) {
val roi = pairingRoi
?: PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
viewW,
viewH,
pairingGeomHeaderBottomPx,
pairingGeomStatusBarTopPx,
pairingGeomDensity,
image.width,
image.height
)
barcodes.firstOrNull {
PairingQrScanGeometry.barcodeMatchesPairingHole(
roi,
image.width,
image.height,
it
)
}
} else {
barcodes.firstOrNull()
}
barcode?.displayValue?.let { code ->
if (code.isNotEmpty() && code !in checkedBarcodes) {
checkedBarcodes.add(code)
if (QtAndroidController.decodeQrCode(code)) {
if (qrHandledOrClosing.compareAndSet(false, true)) {
if (pairingQrMode) {
pairingQrDeliveredToQt = true
}
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)) { imageProxy ->
imageProxy.image?.let { InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees) }
?.let { image ->
barcodeScanner.process(image).addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.let { barcode ->
barcode.displayValue?.let { code ->
if (code.isNotEmpty() && code !in checkedBarcodes) {
if (QtAndroidController.decodeQrCode(code)) {
barcodeScanner.close()
stopCamera()
}
checkedBarcodes.add(code)
}
}
}
}.addOnFailureListener {
Log.e(TAG, "Processing QR code image failed: ${it.message}")
}.addOnCompleteListener {
imageProxy.close()
}
}
.addOnFailureListener(mainExecutor) {
Log.e(TAG, "Processing QR code image failed: ${it.message}")
}
.addOnCompleteListener(mainExecutor) {
imageProxy.close()
}
}
}
private fun stopCamera() {
cleanupCameraResources()
cameraProvider.unbindAll()
finish()
}
}
@@ -1,101 +0,0 @@
package org.amnezia.vpn
import android.graphics.Path
import android.graphics.RectF
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.max
import kotlin.math.min
object PairingQrScanBracketPaths {
private fun Path.addCornerMinorArc(
cx: Float,
cy: Float,
r: Float,
sx: Float,
sy: Float,
ex: Float,
ey: Float
) {
var asRad = atan2((sy - cy).toDouble(), (sx - cx).toDouble())
var aeRad = atan2((ey - cy).toDouble(), (ex - cx).toDouble())
while (aeRad - asRad > PI) {
aeRad -= 2.0 * PI
}
while (aeRad - asRad < -PI) {
aeRad += 2.0 * PI
}
val minor = aeRad - asRad
val startDeg = Math.toDegrees(asRad).toFloat()
val sweepDeg = Math.toDegrees(minor).toFloat()
addArc(RectF(cx - r, cy - r, cx + r, cy + r), startDeg, sweepDeg)
}
fun bracketStrokePath(corner: Int, x0: Float, y0: Float, s: Float, R: Float, L: Float, t: Float): Path {
val r = max(1.5f, R - t * 0.5f)
val p = Path()
val yy = y0 + t * 0.5f
val yyb = y0 + s - t * 0.5f
val xx = x0 + t * 0.5f
val xxb = x0 + s - t * 0.5f
when (corner) {
0 -> {
val cTLx = x0 + R
val cTLy = y0 + R
val sTLx = x0 + R
val sTLy = yy
val eTLx = xx
val eTLy = y0 + R
p.moveTo(x0 + R + L, yy)
p.lineTo(sTLx, sTLy)
p.addCornerMinorArc(cTLx, cTLy, r, sTLx, sTLy, eTLx, eTLy)
val yEndTL = min(y0 + R + L, y0 + s - R - t * 0.5f)
p.lineTo(xx, max(yEndTL, y0 + R + 2f))
}
1 -> {
val cTRx = x0 + s - R
val cTRy = y0 + R
val sTRx = x0 + s - R
val sTRy = yy
val eTRx = xxb
val eTRy = y0 + R
p.moveTo(x0 + s - R - L, yy)
p.lineTo(sTRx, sTRy)
p.addCornerMinorArc(cTRx, cTRy, r, sTRx, sTRy, eTRx, eTRy)
val yEndTR = min(y0 + R + L, y0 + s - R - t * 0.5f)
p.lineTo(xxb, max(yEndTR, y0 + R + 2f))
}
2 -> {
val cBLx = x0 + R
val cBLy = y0 + s - R
val sBLx = x0 + R
val sBLy = yyb
val eBLx = xx
val eBLy = y0 + s - R
p.moveTo(x0 + R + L, yyb)
p.lineTo(sBLx, sBLy)
p.addCornerMinorArc(cBLx, cBLy, r, sBLx, sBLy, eBLx, eBLy)
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
val yLegBL = y0 + s + y0 - yEndTopRef
p.lineTo(xx, yLegBL)
}
3 -> {
val cBRx = x0 + s - R
val cBRy = y0 + s - R
val sBRx = x0 + s - R
val sBRy = yyb
val eBRx = xxb
val eBRy = y0 + s - R
p.moveTo(x0 + s - R - L, yyb)
p.lineTo(sBRx, sBRy)
p.addCornerMinorArc(cBRx, cBRy, r, sBRx, sBRy, eBRx, eBRy)
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
val yLegBR = y0 + s + y0 - yEndTopRef
p.lineTo(xxb, yLegBR)
}
}
return p
}
}
@@ -1,152 +0,0 @@
package org.amnezia.vpn
import android.graphics.Rect
import android.graphics.RectF
import com.google.mlkit.vision.barcode.common.Barcode
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
object PairingQrScanGeometry {
fun viewRectToInputImageRectFillCenter(
viewW: Int,
viewH: Int,
imageW: Int,
imageH: Int,
viewRect: RectF
): RectF {
val scale = max(viewW / imageW.toFloat(), viewH / imageH.toFloat())
val drawLeft = (viewW - imageW * scale) / 2f
val drawTop = (viewH - imageH * scale) / 2f
return RectF(
(viewRect.left - drawLeft) / scale,
(viewRect.top - drawTop) / scale,
(viewRect.right - drawLeft) / scale,
(viewRect.bottom - drawTop) / scale
)
}
fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float {
val d = density
var holeR = min(28f * d, max(10f * d, sidePx * 0.056f))
val half = 0.5f * sidePx
holeR = min(holeR, max(6f * d, half - 2f * d))
return max(holeR, 1f)
}
fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float {
val bf = RectF(box)
val inter = RectF(roi)
if (!inter.intersect(bf)) return 0f
val interArea = inter.width() * inter.height()
val boxArea = bf.width() * bf.height()
return if (boxArea <= 0f) 0f else interArea / boxArea
}
fun barcodeMatchesPairingHole(
roiInImageSpace: RectF,
imageW: Int,
imageH: Int,
barcode: Barcode,
minOverlapFraction: Float = PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK
): Boolean {
if (imageW <= 0 || imageH <= 0) {
return false
}
val roi = RectF(roiInImageSpace)
val iw = imageW.toFloat()
val ih = imageH.toFloat()
roi.left = max(0f, roi.left)
roi.top = max(0f, roi.top)
roi.right = min(iw, roi.right)
roi.bottom = min(ih, roi.bottom)
if (roi.width() <= 0f || roi.height() <= 0f) {
return false
}
val corners = barcode.cornerPoints
if (corners != null && corners.size >= 4) {
for (p in corners) {
if (!roi.contains(p.x.toFloat(), p.y.toFloat())) {
return false
}
}
return true
}
val box = barcode.boundingBox ?: return false
val cx = box.centerX().toFloat()
val cy = box.centerY().toFloat()
if (!roi.contains(cx, cy)) {
return false
}
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
}
private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f
fun pairingIosStyleHoleRectF(
viewW: Int,
viewH: Int,
headerBottomPx: Float,
statusBarTopPx: Float,
density: Float
): RectF {
val w = viewW.toFloat()
val h = viewH.toFloat()
val d = density
if (w < 32f || h < 32f) {
return RectF()
}
var hdrBottom = headerBottomPx
if (hdrBottom < 8f * d) {
hdrBottom = 132f * d + statusBarTopPx
}
val sqSz = floor(min(w, h) * 0.72).toFloat()
var sqX = (w - sqSz) / 2f
var sqY = (h - sqSz) / 2f
sqY = max(sqY, hdrBottom + 8f * d)
val kBottomBand = 80f * d
val maxHoleBottom = h - kBottomBand
if (sqY + sqSz > maxHoleBottom) {
sqY = maxHoleBottom - sqSz
sqY = max(sqY, hdrBottom + 8f * d)
}
sqX = max(8f * d, min(sqX, w - sqSz - 8f * d))
sqY = max(hdrBottom + 4f * d, min(sqY, h - sqSz - 8f * d))
return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz)
}
fun pairingIosStyleTorchCenterYPx(
holeBottomPx: Float,
bandBottomPx: Float,
headerBottomPx: Float,
safeBottomPx: Float,
density: Float
): Float {
val torchH = 56f * density
val d = density
var torchCy = (holeBottomPx + bandBottomPx) * 0.5f
val minC = holeBottomPx + torchH * 0.5f + 6f * d
val maxC = bandBottomPx - torchH * 0.5f - max(6f * d, safeBottomPx)
torchCy = max(minC, min(maxC, torchCy))
if (minC > maxC) {
torchCy = (minC + maxC) * 0.5f
}
val hdr = headerBottomPx + torchH * 0.5f + 10f * d
return max(torchCy, hdr)
}
fun pairingIosStyleHoleInImageCoords(
viewW: Int,
viewH: Int,
headerBottomPx: Float,
statusBarTopPx: Float,
density: Float,
imageW: Int,
imageH: Int
): RectF {
val hv = pairingIosStyleHoleRectF(viewW, viewH, headerBottomPx, statusBarTopPx, density)
return viewRectToInputImageRectFillCenter(viewW, viewH, imageW, imageH, hv)
}
}
@@ -1,115 +0,0 @@
package org.amnezia.vpn
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import kotlin.math.max
class PairingQrScanOverlayView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
init {
isClickable = false
isFocusable = false
}
@Suppress("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean = false
private val dimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0x8C000000.toInt()
style = Paint.Style.FILL
}
private val bracketPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0xFFE8E8EC.toInt()
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
}
private var hole = RectF()
private val bracketPaths = arrayOfNulls<Path>(4)
private val dimPath = Path()
private var pairingHeaderBottomPx = 0f
fun setPairingHeaderBottomPx(px: Float) {
if (pairingHeaderBottomPx == px) {
return
}
pairingHeaderBottomPx = px
recomputePairingHole()
invalidate()
}
private fun recomputePairingHole() {
val w = width
val h = height
if (w <= 0 || h <= 0) {
return
}
val topInset = ViewCompat.getRootWindowInsets(this)
?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
val d = resources.displayMetrics.density
hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, pairingHeaderBottomPx, topInset, d)
rebuildBracketPaths()
}
private fun rebuildBracketPaths() {
val s = hole.width()
if (s <= 0f) {
bracketPaths.fill(null)
return
}
val x0 = hole.left
val y0 = hole.top
val t = bracketPaint.strokeWidth
val d = resources.displayMetrics.density
val l = max(28f * d, s * 0.13f)
val r = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(s, d)
for (i in 0..3) {
bracketPaths[i] = PairingQrScanBracketPaths.bracketStrokePath(i, x0, y0, s, r, l, t)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
bracketPaint.strokeWidth = max(3f, 5f * resources.displayMetrics.density)
recomputePairingHole()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
val side = hole.width()
if (side > 0f) {
val d = resources.displayMetrics.density
val rx = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(side, d)
dimPath.rewind()
dimPath.fillType = Path.FillType.EVEN_ODD
dimPath.addRect(0f, 0f, w, h, Path.Direction.CW)
dimPath.addRoundRect(hole, rx, rx, Path.Direction.CW)
canvas.drawPath(dimPath, dimPaint)
} else {
canvas.drawRect(0f, 0f, w, h, dimPaint)
}
for (i in 0..3) {
bracketPaths[i]?.let { canvas.drawPath(it, bracketPaint) }
}
}
}
@@ -34,10 +34,4 @@ object QtAndroidController {
external fun onActivityPaused()
external fun onActivityResumed()
external fun onCameraPermissionResult(granted: Boolean)
external fun onPairingQrCameraClosed()
external fun onPairingQrCameraUserDismissed()
}
-4
View File
@@ -28,7 +28,6 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
@@ -45,8 +44,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
@@ -57,7 +54,6 @@ target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
+1 -2
View File
@@ -49,7 +49,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
)
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
@@ -153,5 +152,5 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
)
+10 -5
View File
@@ -45,7 +45,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.h
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
${CLIENT_ROOT_DIR}/core/controllers/updateController.h
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
@@ -66,8 +65,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/utilities.h
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
${CLIENT_ROOT_DIR}/core/utils/constants.h
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
)
# Mozilla headres
@@ -125,7 +122,6 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
@@ -161,7 +157,6 @@ set(SOURCES ${SOURCES}
if(NOT IOS AND NOT MACOS_NE)
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
)
endif()
@@ -206,6 +201,11 @@ file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/configurat
file(GLOB_RECURSE CORE_MODELS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.h)
file(GLOB_RECURSE CORE_MODELS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.cpp)
if(NOT ANDROID AND NOT IOS)
file(GLOB LOCAL_PROXY_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.h)
file(GLOB LOCAL_PROXY_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.cpp)
endif()
file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.h
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
@@ -251,6 +251,11 @@ set(SOURCES ${SOURCES}
${UI_CONTROLLERS_CPP}
)
if(NOT ANDROID AND NOT IOS)
list(APPEND HEADERS ${LOCAL_PROXY_H})
list(APPEND SOURCES ${LOCAL_PROXY_CPP})
endif()
if(WIN32)
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolWindows.h
+280 -152
View File
@@ -4,6 +4,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QThread>
#include <QUuid>
#include "logger.h"
@@ -137,117 +138,322 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
return protocolConfig;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
{
// Generate new UUID for client
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return errorCode;
}
// Get flow value from settings (default xtls-rprx-vision)
QString flowValue = "xtls-rprx-vision";
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
if (!xrayCfg->serverConfig.flow.isEmpty()) {
flowValue = xrayCfg->serverConfig.flow;
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript,
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
dnsSettings.secondaryDns)));
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
}
return errorCode;
}
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
QString &outPublicKey, QString &outShortId) const
{
outPublicKey.clear();
outShortId.clear();
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
out.replace(QLatin1Char('\n'), QString());
out.replace(QLatin1Char('\r'), QString());
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
};
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
}
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const
{
QJsonObject streamSettings = buildStreamSettings(srv, QString());
if (srv.security != QLatin1String("reality")) {
return streamSettings;
}
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
}
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
if (!merged.contains(QStringLiteral("dest"))) {
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
}
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
return streamSettings;
}
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
bool appendNewClient, QString *outClientId)
{
ErrorCode errorCode = ErrorCode::NoError;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
if (srv.isThirdPartyConfig) {
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
if (outClientId && xrayCfg->hasClientConfig()) {
*outClientId = xrayCfg->clientConfig->id;
}
return ErrorCode::NoError;
}
logger.info() << "Xray applyServerSettings: start"
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
const QString flowValue = srv.flow;
QString realityPublicKey;
QString realityShortId;
if (srv.security == QLatin1String("reality")) {
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
}
// Get current server config
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return "";
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
return errorCode;
}
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
// 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 "";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
return ErrorCode::XrayServerConfigInvalid;
}
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
if (!srv.port.isEmpty()) {
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
settings[amnezia::protocols::xray::clients] = QJsonArray {};
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
QString clientId;
// Create configuration for new client
QJsonObject clientConfig {
{amnezia::protocols::xray::id, clientId},
};
clientConfig[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientConfig[amnezia::protocols::xray::flow] = flowValue;
if (appendNewClient) {
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QJsonObject clientEntry;
clientEntry[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientEntry[amnezia::protocols::xray::flow] = flowValue;
}
clients.append(clientEntry);
} else {
if (clients.isEmpty()) {
logger.error() << "Server config has no VLESS clients";
return ErrorCode::XrayServerNoVlessClients;
}
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
if (clientId.isEmpty()) {
logger.error() << "Server config VLESS client has empty id";
return ErrorCode::XrayServerNoVlessClients;
}
QJsonArray updatedClients;
for (const QJsonValue &v : clients) {
QJsonObject c = v.toObject();
if (flowValue.isEmpty()) {
c.remove(amnezia::protocols::xray::flow);
} else {
c[amnezia::protocols::xray::flow] = flowValue;
}
updatedClients.append(c);
}
clients = updatedClients;
}
clients.append(clientConfig);
// Update config
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_sshSession->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
if (outClientId) {
*outClientId = clientId;
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
containerConfig.protocolConfig = updated;
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
return ErrorCode::NoError;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
ContainerConfig mutableConfig = containerConfig;
QString clientId;
const ErrorCode applyError =
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId);
errorCode = applyError;
if (applyError != ErrorCode::NoError || clientId.isEmpty()) {
return QString();
}
return clientId;
}
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
DockerContainer container,
const XrayServerConfig &srv, const QString &clientId,
ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey,
const QString &prefetchedRealityShortId) const
{
QString xrayPublicKey = prefetchedRealityPublicKey;
QString xrayShortId = prefetchedRealityShortId;
if (srv.security == QLatin1String("reality")) {
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
if (errorCode != ErrorCode::NoError) {
return {};
}
}
}
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = clientId;
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] =
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
QJsonObject outboundSettings;
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
QJsonObject outbound;
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
outbound[amnezia::protocols::xray::settings] = outboundSettings;
QJsonObject streamObj = buildStreamSettings(srv, clientId);
if (srv.security == QLatin1String("reality")) {
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
rs[amnezia::protocols::xray::shortId] = xrayShortId;
rs[amnezia::protocols::xray::spiderX] = QString();
streamObj[amnezia::protocols::xray::realitySettings] = rs;
}
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
QJsonObject inboundObj;
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
QJsonObject clientJson;
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = clientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{
QJsonObject streamSettings;
@@ -353,9 +559,12 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
if (pad.obfsMode) {
if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) {
QJsonObject br;
br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt())
: pad.bytesMax.toInt();
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
if (toV < fromV)
toV = fromV;
br[QStringLiteral("from")] = fromV;
br[QStringLiteral("to")] = toV;
xo[QStringLiteral("xPaddingBytes")] = br;
}
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
@@ -419,6 +628,13 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
if (xrayCfg->serverConfig.isThirdPartyConfig && xrayCfg->hasClientConfig()) {
logger.info() << "Xray createConfig: returning existing third-party client config without server SSH";
return *xrayCfg;
}
}
const XrayServerConfig *serverConfig = nullptr;
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayCfg->serverConfig;
@@ -441,93 +657,5 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
return XrayProtocolConfig{};
}
// Fetch server keys (Reality only)
QString xrayPublicKey;
QString xrayShortId;
if (srv.security == "reality") {
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
xrayPublicKey.replace("\n", "");
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
xrayShortId.replace("\n", "");
}
// Build outbound
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = xrayClientId;
userObj[amnezia::protocols::xray::encryption] = "none";
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
QJsonObject outboundSettings;
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
QJsonObject outbound;
outbound["protocol"] = "vless";
outbound[amnezia::protocols::xray::settings] = outboundSettings;
// Build streamSettings
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
// Inject Reality keys
if (srv.security == "reality") {
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
rs[amnezia::protocols::xray::shortId] = xrayShortId;
rs[amnezia::protocols::xray::spiderX] = "";
streamObj[amnezia::protocols::xray::realitySettings] = rs;
}
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
// Build full client config
QJsonObject inboundObj;
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
inboundObj["protocol"] = "socks";
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
QJsonObject clientJson;
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
// Return
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
qDebug() << "config:" << config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
}
+26 -1
View File
@@ -23,12 +23,37 @@ public:
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig) override;
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
bool appendNewClient,
QString *outClientId = nullptr);
private:
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::XrayServerConfig &srv,
const QString &clientId,
amnezia::ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey = {},
const QString &prefetchedRealityShortId = {}) const;
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
QString &outPublicKey,
QString &outShortId) const;
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const;
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const;
};
@@ -90,7 +90,7 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType));
}
auto future = gatewayController->postAsync(QString("%1v1/news"), payload, nullptr, gatewayController);
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
@@ -1,204 +0,0 @@
#include "pairingController.h"
#include <QJsonDocument>
#include <QSysInfo>
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "version.h"
using namespace amnezia;
namespace
{
constexpr qsizetype kPairingMaxQrUuidChars = 128;
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
constexpr qsizetype kPairingMaxServiceTypeChars = 64;
constexpr qsizetype kPairingMaxUserCountryCodeChars = 32;
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
{
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
if (apiStatus != ErrorCode::NoError) {
return apiStatus;
}
const QString config = obj.value(apiDefs::key::config).toString();
if (!config.isEmpty()) {
outPayload.config = config;
outPayload.serviceInfo = obj.value(apiDefs::key::serviceInfo).toObject();
outPayload.supportedProtocols = obj.value(apiDefs::key::supportedProtocols).toArray();
return ErrorCode::NoError;
}
if (obj.contains(QStringLiteral("detail"))) {
return ErrorCode::ApiConfigEmptyError;
}
const QString msg = obj.value(QStringLiteral("message")).toString();
if (msg.contains(QStringLiteral("timeout"), Qt::CaseInsensitive)) {
return ErrorCode::ApiConfigTimeoutError;
}
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingRateLimitedError;
}
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingServiceUnavailableError;
}
if (!msg.isEmpty()) {
return ErrorCode::ApiConfigDownloadError;
}
return ErrorCode::ApiConfigEmptyError;
}
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
{
const QString msgProbe = obj.value(QStringLiteral("message")).toString();
if (msgProbe.contains(QStringLiteral("limit"), Qt::CaseInsensitive)
&& (msgProbe.contains(QStringLiteral("device"), Qt::CaseInsensitive)
|| msgProbe.contains(QStringLiteral("maximum"), Qt::CaseInsensitive)
|| msgProbe.contains(QStringLiteral("max"), Qt::CaseInsensitive))) {
return ErrorCode::ApiConfigLimitError;
}
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
if (apiStatus != ErrorCode::NoError) {
return apiStatus;
}
if (obj.value(QStringLiteral("message")).toString() == QLatin1String("OK")) {
return ErrorCode::NoError;
}
if (obj.contains(QStringLiteral("detail"))) {
return ErrorCode::ApiPairingForbiddenError;
}
const QString msg = obj.value(QStringLiteral("message")).toString();
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
return ErrorCode::ApiPairingSessionExpiredError;
}
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
return ErrorCode::ApiNotFoundError;
}
if (msg.contains(QStringLiteral("Conflict"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("already"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingConflictError;
}
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingRateLimitedError;
}
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
return ErrorCode::ApiPairingServiceUnavailableError;
}
if (!msg.isEmpty()) {
return ErrorCode::ApiConfigDownloadError;
}
return ErrorCode::ApiConfigEmptyError;
}
ErrorCode interpretGenerateQrJson(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
{
return applyGatewayOrOpenApiGenerateError(obj, outPayload);
}
ErrorCode interpretScanQrJson(const QJsonObject &obj)
{
return applyGatewayOrOpenApiScanError(obj);
}
} // namespace
ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload)
{
outPayload = QrPairingConfigPayload {};
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretGenerateQrJson(obj, outPayload);
}
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName)
{
if (outOptionalDisplayName) {
outOptionalDisplayName->clear();
}
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
const ErrorCode err = interpretScanQrJson(obj);
if (err != ErrorCode::NoError) {
return err;
}
if (outOptionalDisplayName) {
const QString deviceName = obj.value(QStringLiteral("device_name")).toString().trimmed();
if (!deviceName.isEmpty()) {
*outOptionalDisplayName = deviceName;
}
}
return ErrorCode::NoError;
}
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
const QString &serviceType, const QString &userCountryCode)
{
if (qrUuid.size() > kPairingMaxQrUuidChars) {
return ErrorCode::ApiConfigEmptyError;
}
if (vpnConfig.size() > kPairingMaxVpnConfigChars) {
return ErrorCode::ApiPairingPayloadTooLargeError;
}
if (apiKey.size() > kPairingMaxApiKeyChars) {
return ErrorCode::ApiPairingPayloadTooLargeError;
}
const QString st = serviceType.trimmed();
const QString cc = userCountryCode.trimmed();
if (st.isEmpty() || cc.isEmpty()) {
return ErrorCode::ApiPairingMissingMetadataError;
}
if (st.size() > kPairingMaxServiceTypeChars || cc.size() > kPairingMaxUserCountryCodeChars) {
return ErrorCode::ApiPairingPayloadTooLargeError;
}
return ErrorCode::NoError;
}
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
: m_appSettingsRepository(appSettingsRepository)
{
}
int PairingController::pairingLongPollTimeoutMsecs() const
{
return 60 * 1000;
}
QJsonObject PairingController::buildGenerateQrPayload(const QString &qrUuid) const
{
QJsonObject o;
o[apiDefs::key::qrUuid] = qrUuid;
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
o[apiDefs::key::appVersion] = QString(APP_VERSION);
o[apiDefs::key::osVersion] = QSysInfo::productType();
return o;
}
QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey,
const QString &serviceType, const QString &userCountryCode) const
{
QJsonObject auth;
auth[apiDefs::key::apiKey] = apiKey;
QJsonObject o;
o[apiDefs::key::qrUuid] = qrUuid;
o[apiDefs::key::config] = vpnConfig;
o[apiDefs::key::serviceInfo] = serviceInfo;
o[apiDefs::key::supportedProtocols] = supportedProtocols;
o[apiDefs::key::authData] = auth;
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
o[apiDefs::key::appVersion] = QString(APP_VERSION);
o[apiDefs::key::osVersion] = QSysInfo::productType();
o[apiDefs::key::serviceType] = serviceType.trimmed();
o[apiDefs::key::userCountryCode] = userCountryCode.trimmed();
return o;
}
@@ -1,41 +0,0 @@
#ifndef PAIRINGCONTROLLER_H
#define PAIRINGCONTROLLER_H
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "core/utils/errorCodes.h"
class SecureAppSettingsRepository;
class PairingController
{
public:
struct QrPairingConfigPayload
{
QString config;
QJsonObject serviceInfo;
QJsonArray supportedProtocols;
};
explicit PairingController(SecureAppSettingsRepository *appSettingsRepository);
int pairingLongPollTimeoutMsecs() const;
QJsonObject buildGenerateQrPayload(const QString &qrUuid) const;
QJsonObject buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
const QString &userCountryCode) const;
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName = nullptr);
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
const QString &serviceType, const QString &userCountryCode);
private:
SecureAppSettingsRepository *m_appSettingsRepository;
};
#endif // PAIRINGCONTROLLER_H
@@ -5,6 +5,7 @@
#include <QEventLoop>
#include <QFutureWatcher>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPromise>
#include <QSet>
#include <QSysInfo>
@@ -216,7 +217,8 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
}
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData)
const QString &serviceProtocol, const ProtocolData &protocolData,
CaptchaInfo &captchaInfo)
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
@@ -233,6 +235,19 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
captchaInfo.hint = jsonObj.value("hint").toString();
captchaInfo.isRequired = true;
}
return errorCode;
}
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -242,9 +257,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -312,71 +327,6 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols,
int *duplicateServerIndex)
{
if (vpnConfigKey.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
QString normalizedKey = vpnConfigKey;
normalizedKey.replace(QStringLiteral("vpn://"), QString());
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
const auto apiV2 = m_serversRepository->apiV2Config(m_serversRepository->serverIdAt(i));
QString existingVpnKey = apiV2.has_value() ? apiV2->vpnKey() : QString();
existingVpnKey.replace(QStringLiteral("vpn://"), QString());
if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
if (duplicateServerIndex) {
*duplicateServerIndex = i;
}
return ErrorCode::ApiConfigAlreadyAdded;
}
}
QByteArray configString =
QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray configUncompressed = qUncompress(configString);
if (!configUncompressed.isEmpty()) {
configString = configUncompressed;
}
if (configString.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
QJsonObject serverJson = QJsonDocument::fromJson(configString).object();
if (serverJson.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
if (serverJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
QJsonObject apiConfig = serverJson.value(apiDefs::key::apiConfig).toObject();
if (!serviceInfo.isEmpty()) {
apiConfig.insert(apiDefs::key::serviceInfo, serviceInfo);
}
if (!supportedProtocols.isEmpty()) {
apiConfig.insert(apiDefs::key::supportedProtocols, supportedProtocols);
}
serverJson[apiDefs::key::apiConfig] = apiConfig;
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverJson);
if (apiV2ServerConfig.apiConfig.vpnKey.isEmpty()) {
QString fullKey = vpnConfigKey.trimmed();
if (!fullKey.startsWith(QStringLiteral("vpn://"))) {
fullKey = QStringLiteral("vpn://") + fullKey;
}
apiV2ServerConfig.apiConfig.vpnKey = fullKey;
}
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
@@ -525,6 +475,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
if (apiV2->nameOverriddenByUser) {
newApiV2->name = apiV2->name;
newApiV2->displayName = apiV2->displayName;
newApiV2->nameOverriddenByUser = true;
}
@@ -999,7 +950,7 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload, nullptr, gatewayController);
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
[promise, watcher, gatewayController]() {
@@ -1020,3 +971,74 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
return promise->future();
}
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
const QString &serviceType,
const QString &serviceProtocol,
const ProtocolData &protocolData,
const QString &captchaId,
const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut)
{
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject()};
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload["captcha_id"] = captchaId;
QString normalizedSolution;
normalizedSolution.reserve(captchaSolution.size());
for (const QChar &ch : captchaSolution) {
const ushort u = ch.unicode();
if (u >= '0' && u <= '9') {
normalizedSolution += ch;
} else if (u >= 0xFF10 && u <= 0xFF19) {
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
}
}
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
if (retryCaptchaOut
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
const QJsonObject jsonObj = jsonDoc.object();
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
retryCaptchaOut->isRequired = true;
}
}
}
return errorCode;
}
QJsonObject serverConfigJson;
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}
@@ -1,7 +1,6 @@
#ifndef SUBSCRIPTIONCONTROLLER_H
#define SUBSCRIPTIONCONTROLLER_H
#include <QJsonArray>
#include <QJsonObject>
#include <QByteArray>
#include <QFuture>
@@ -43,6 +42,13 @@ public:
QJsonObject toJsonObject() const;
};
struct CaptchaInfo {
QString captchaId;
QString captchaImageBase64;
QString hint;
bool isRequired = false;
};
explicit SubscriptionController(SecureServersRepository* serversRepository,
SecureAppSettingsRepository* appSettingsRepository);
@@ -50,13 +56,11 @@ public:
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData);
const QString &serviceProtocol, const ProtocolData &protocolData,
CaptchaInfo &captchaInfo);
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email);
ErrorCode importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, int *duplicateServerIndex = nullptr);
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase,
@@ -102,6 +106,11 @@ public:
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol);
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &captchaId, const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut = nullptr);
private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool isApiKeyExpired(const QString &serverId) const;
@@ -6,9 +6,7 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/utilities.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
#include "version.h"
#include "core/utils/containerEnum.h"
@@ -51,14 +49,96 @@ void ConnectionController::setConnectionState(Vpn::ConnectionState state)
}
}
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container)
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
{
const auto kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
return ErrorCode::LegacyApiV1NotSupportedError;
case serverConfigUtils::ConfigType::Invalid:
default:
return ErrorCode::InternalError;
}
}
ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) const
{
if (serverId.isEmpty()) {
return ErrorCode::InternalError;
}
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
DockerContainer container = DockerContainer::None;
const ErrorCode errorCode = defaultContainerForServer(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (container == DockerContainer::None) {
if (serverConfigUtils::isApiV2Subscription(kind)) {
return ErrorCode::NoError;
}
return ErrorCode::NoInstalledContainersError;
}
if (ContainerUtils::isUnsupportedContainer(container)) {
return ErrorCode::LegacyContainerNotSupportedError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
return ErrorCode::NoError;
}
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container)
{
ContainerConfig containerConfigModel;
QPair<QString, QString> dns;
QString hostName;
@@ -67,13 +147,15 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
bool isApiConfig = false;
const auto kind = m_serversRepository->serverKind(serverId);
const QString primaryDns = m_appSettingsRepository->primaryDns();
const QString secondaryDns = m_appSettingsRepository->secondaryDns();
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 };
dns = cfg->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
break;
@@ -83,7 +165,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 };
dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
break;
@@ -93,7 +175,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 };
dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
break;
@@ -105,7 +187,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 };
dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName;
description = cfg->description;
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
@@ -120,20 +202,6 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
return ErrorCode::InternalError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) {
if (m_appSettingsRepository->useAmneziaDns()) {
dns.first = protocols::dns::amneziaDnsIp;
} else {
dns.first = m_appSettingsRepository->primaryDns();
}
}
if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) {
dns.second = m_appSettingsRepository->secondaryDns();
}
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
containerConfigModel, container);
@@ -150,6 +218,13 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
return errorCode;
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
emit localProxyStoppedBecauseVpnTurnedOn(tr("Local proxy stopped because VPN was turned on"));
}
#endif
emit openConnectionRequested(serverId, container, vpnConfiguration);
return ErrorCode::NoError;
}
@@ -34,6 +34,8 @@ public:
QJsonObject& vpnConfiguration,
DockerContainer& container);
ErrorCode isConnectionSupported(const QString &serverId) const;
ErrorCode openConnection(const QString &serverId);
void closeConnection();
@@ -67,12 +69,15 @@ signals:
void closeConnectionRequested();
void setConnectionStateRequested(Vpn::ConnectionState state);
void killSwitchModeChangedRequested(bool enabled);
void localProxyStoppedBecauseVpnTurnedOn(const QString &message);
#ifdef Q_OS_ANDROID
void restoreConnectionRequested();
#endif
private:
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
VpnConnection* m_vpnConnection;
+82 -12
View File
@@ -1,7 +1,10 @@
#include "coreController.h"
#include <QCoreApplication>
#include <QDirIterator>
#include <QDebug>
#include <QTranslator>
#include <QStandardPaths>
#include <QTimer>
#include "core/utils/selfhosted/sshSession.h"
@@ -35,6 +38,10 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initAppleController();
initLogging();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
initLocalProxy();
#endif
m_translator = new QTranslator(this);
if (m_appSettingsRepository) {
updateTranslator(m_appSettingsRepository->getAppLanguage());
@@ -48,6 +55,69 @@ void CoreController::setQmlContextProperty(const QString &name, QObject *value)
}
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
void CoreController::initLocalProxy()
{
constexpr quint16 kLocalProxyApiPort = 49490;
m_proxyServer.reset(new ProxyServer(m_serversRepository, m_appSettingsRepository, this));
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
}
});
auto syncLocalProxy = [this]() {
if (!m_proxyServer) {
return;
}
const bool httpEnabled = m_appSettingsRepository->isLocalProxyHttpEnabled();
if (!httpEnabled) {
qInfo() << "Local proxy: HTTP API disabled";
m_proxyServer->stop();
return;
}
if (!m_proxyServer->start(kLocalProxyApiPort)) {
qWarning() << "Local proxy: failed to start on port" << kLocalProxyApiPort;
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
emit m_appSettingsRepository->localProxyStartFailed(tr("Local proxy failed to start. Check if the port is available."));
return;
}
if (!m_proxyServer->syncSettings()) {
qWarning() << "Local proxy: failed to start proxy core (Xray)";
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
emit m_appSettingsRepository->localProxyStartFailed(tr("Couldn’t start the proxy due to an internal error. Try restarting the app."));
return;
}
qInfo() << "Local proxy: running on 127.0.0.1:" << kLocalProxyApiPort;
};
syncLocalProxy();
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this, syncLocalProxy);
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, [this](const QString &serverId) {
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()
&& m_appSettingsRepository->localProxyOwnerId() == serverId) {
m_appSettingsRepository->bumpLocalProxyRestartToken();
}
});
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this, [this](const QString &serverId, int) {
if (m_appSettingsRepository && m_appSettingsRepository->localProxyOwnerId() == serverId) {
m_appSettingsRepository->setLocalProxyOwnerId(QString());
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
}
});
}
#endif
void CoreController::initModels()
{
m_containersModel = new ContainersModel(this);
@@ -153,7 +223,6 @@ void CoreController::initCoreControllers()
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
m_pairingController = new PairingController(m_appSettingsRepository);
m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
m_updateController = new UpdateController(m_appSettingsRepository, this);
@@ -179,7 +248,8 @@ void CoreController::initControllers()
#ifdef Q_OS_WINDOWS
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
m_connectionController, this);
setQmlContextProperty("InstallController", m_installUiController);
m_importController = new ImportUiController(m_importCoreController, this);
@@ -191,12 +261,17 @@ void CoreController::initControllers()
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
setQmlContextProperty("LanguageUiController", m_languageUiController);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, m_languageUiController, this);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this);
setQmlContextProperty("PageController", m_pageController);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_connectionController, &ConnectionController::localProxyStoppedBecauseVpnTurnedOn, m_pageController,
&PageController::showNotificationMessage);
#endif
m_serversUiController = new ServersUiController(m_serversController, m_settingsController, m_serversModel, m_containersModel, m_defaultServerContainersModel, this);
setQmlContextProperty("ServersUiController", m_serversUiController);
@@ -213,20 +288,18 @@ void CoreController::initControllers()
setQmlContextProperty("SystemController", m_systemController);
m_networkReachabilityController = new NetworkReachabilityController(this);
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel,
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
m_apiCountryModel, m_apiDevicesModel, m_settingsController,
m_connectionController, this);
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
m_pairingUiController = new PairingUiController(m_pairingController, m_serversController, m_subscriptionController, m_appSettingsRepository, this);
setQmlContextProperty("PairingUiController", m_pairingUiController);
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
@@ -346,9 +419,6 @@ void CoreController::openConnectionByIndex(int serverIndex)
if (serverId.isEmpty()) {
return;
}
if (m_serversModel) {
m_serversModel->setProcessedServerIndex(serverIndex);
}
if (m_serversController) {
m_serversController->setDefaultServer(serverId);
}
+38 -26
View File
@@ -10,8 +10,6 @@
#endif
#include "ui/controllers/api/subscriptionUiController.h"
#include "ui/controllers/api/pairingUiController.h"
#include "core/controllers/api/pairingController.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/controllers/appSplitTunnelingUiController.h"
#include "ui/controllers/allowedDnsUiController.h"
@@ -80,37 +78,16 @@
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "core/local-proxy/proxyserver.h"
#include "ui/utils/notificationHandler.h"
#endif
class CoreSignalHandlers;
class TestMultipleImports;
class TestAdminSelfHostedExport;
class TestServerEdit;
class TestDefaultServerChange;
class TestServerEdgeCases;
class TestSignalOrder;
class TestServersModelSync;
class TestComplexOperations;
class TestSettingsSignals;
class TestUiServersModelAndController;
class TestSelfHostedServerSetup;
class CoreController : public QObject
{
Q_OBJECT
friend class CoreSignalHandlers;
friend class TestMultipleImports;
friend class TestAdminSelfHostedExport;
friend class TestServerEdit;
friend class TestDefaultServerChange;
friend class TestServerEdgeCases;
friend class TestSignalOrder;
friend class TestServersModelSync;
friend class TestComplexOperations;
friend class TestSettingsSignals;
friend class TestUiServersModelAndController;
friend class TestSelfHostedServerSetup;
public:
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
@@ -127,6 +104,36 @@ signals:
void translationsUpdated();
void websiteUrlChanged(const QString &newUrl);
protected:
SecureServersRepository* serversRepositoryProtected() const { return m_serversRepository; }
SecureAppSettingsRepository* appSettingsRepositoryProtected() const { return m_appSettingsRepository; }
ServersModel* serversModelProtected() const { return m_serversModel; }
ContainersModel* containersModelProtected() const { return m_containersModel; }
ApiServicesModel* apiServicesModelProtected() const { return m_apiServicesModel; }
NewsModel* newsModelProtected() const { return m_newsModel; }
AllowedDnsModel* allowedDnsModelProtected() const { return m_allowedDnsModel; }
AppSplitTunnelingModel* appSplitTunnelingModelProtected() const { return m_appSplitTunnelingModel; }
IpSplitTunnelingModel* ipSplitTunnelingModelProtected() const { return m_ipSplitTunnelingModel; }
LanguageModel* languageModelProtected() const { return m_languageModel; }
InstallUiController* installUiControllerProtected() const { return m_installUiController; }
ImportController* importCoreControllerProtected() const { return m_importCoreController; }
ExportController* exportControllerProtected() const { return m_exportController; }
InstallController* installControllerProtected() const { return m_installController; }
ServersController* serversControllerProtected() const { return m_serversController; }
SettingsUiController* settingsUiControllerProtected() const { return m_settingsUiController; }
SettingsController* settingsControllerProtected() const { return m_settingsController; }
AllowedDnsUiController* allowedDnsUiControllerProtected() const { return m_allowedDnsUiController; }
AllowedDnsController* allowedDnsControllerProtected() const { return m_allowedDnsController; }
LanguageUiController* languageUiControllerProtected() const { return m_languageUiController; }
IpSplitTunnelingController* ipSplitTunnelingControllerProtected() const { return m_ipSplitTunnelingController; }
IpSplitTunnelingUiController* ipSplitTunnelingUiControllerProtected() const { return m_ipSplitTunnelingUiController; }
AppSplitTunnelingController* appSplitTunnelingControllerProtected() const { return m_appSplitTunnelingController; }
AppSplitTunnelingUiController* appSplitTunnelingUiControllerProtected() const { return m_appSplitTunnelingUiController; }
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
private:
void initRepositories();
void initCoreControllers();
@@ -136,6 +143,9 @@ private:
void initAppleController();
void initLogging();
void initSignalHandlers();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
void initLocalProxy();
#endif
void setQmlContextProperty(const QString &name, QObject *value);
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
@@ -170,7 +180,6 @@ private:
UpdateUiController* m_updateUiController;
SubscriptionUiController* m_subscriptionUiController;
PairingUiController* m_pairingUiController;
ApiNewsUiController* m_apiNewsUiController;
ServicesCatalogUiController* m_servicesCatalogUiController;
@@ -182,7 +191,6 @@ private:
AllowedDnsController* m_allowedDnsController;
ServicesCatalogController* m_servicesCatalogController;
SubscriptionController* m_subscriptionController;
PairingController* m_pairingController;
NewsController* m_newsController;
UpdateController* m_updateController;
InstallController* m_installController;
@@ -223,6 +231,10 @@ private:
TelemtConfigModel* m_telemtConfigModel;
CoreSignalHandlers* m_signalHandlers;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
QScopedPointer<ProxyServer> m_proxyServer;
#endif
};
#endif // CORECONTROLLER_H
+36 -29
View File
@@ -1,6 +1,7 @@
#include "coreSignalHandlers.h"
#include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/errorCodes.h"
@@ -21,7 +22,6 @@
#include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/importUiController.h"
#include "ui/controllers/api/subscriptionUiController.h"
#include "ui/controllers/api/pairingUiController.h"
#include "ui/controllers/updateUiController.h"
#include "ui/models/serversModel.h"
#include "core/controllers/serversController.h"
@@ -34,7 +34,6 @@
#include "core/controllers/connectionController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h"
@@ -99,9 +98,6 @@ void CoreSignalHandlers::initErrorMessagesHandler()
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
connect(m_coreController->m_pairingUiController, &PairingUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
connect(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage));
}
@@ -129,9 +125,9 @@ void CoreSignalHandlers::initInstallControllerHandler()
{
connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy);
connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation);
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
m_coreController->m_installUiController, [this](int serverIndex) {
if (serverIndex >= 0) {
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged,
m_coreController->m_installUiController, [this](const QString &serverId) {
if (!serverId.isEmpty()) {
m_coreController->m_installUiController->clearProcessedServerCredentials();
}
});
@@ -149,7 +145,9 @@ void CoreSignalHandlers::initExportControllerHandler()
});
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](const QString &serverId, int row, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, row, container);
QtConcurrent::run([this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
});
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
@@ -160,15 +158,17 @@ void CoreSignalHandlers::initExportControllerHandler()
void CoreSignalHandlers::initImportControllerHandler()
{
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
if (!m_coreController->m_connectionController->isConnected()) {
int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
if (m_coreController->m_connectionUiController->isConnected()) {
return;
}
const int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
});
}
@@ -180,17 +180,14 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
if (processedServerId.isEmpty()) {
return;
}
QJsonArray availableCountries;
QString serverCountryCode;
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
if (apiV2.has_value()) {
availableCountries = apiV2->apiConfig.availableCountries;
serverCountryCode = apiV2->apiConfig.serverCountryCode;
if (!apiV2.has_value()) {
return;
}
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode);
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
});
}
@@ -208,13 +205,15 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
{
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
QtConcurrent::run([this, serverId, containerConfig, container]() {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
});
});
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
});
}, Qt::DirectConnection);
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
&InstallController::clearCachedProfile);
@@ -241,13 +240,16 @@ void CoreSignalHandlers::initLanguageHandler()
connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->changeLanguage(m_coreController->m_languageUiController->getSystemLanguageEnum());
});
connect(m_coreController->m_settingsUiController, &SettingsUiController::appLanguageChanged, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->onAppLanguageChanged(m_coreController->m_settingsController->getAppLanguage());
});
}
void CoreSignalHandlers::initAutoConnectHandler()
{
if (m_coreController->m_settingsUiController->isAutoConnectEnabled()
&& !m_coreController->m_serversController->getDefaultServerId().isEmpty()) {
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); });
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->toggleConnection(); });
}
}
@@ -288,6 +290,8 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
connect(m_coreController->m_usersController, &UsersController::clientRenamed,
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
m_coreController->m_exportController, &ExportController::revokeFinished);
}
void CoreSignalHandlers::initSitesModelUpdateHandler()
@@ -352,6 +356,9 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
{
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
connect(m_coreController->m_connectionUiController, &ConnectionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()
+65 -205
View File
@@ -10,7 +10,6 @@
#include <QJsonObject>
#include <QNetworkReply>
#include <QPromise>
#include <QTimer>
#include <QUrl>
#include "QBlockCipher.h"
@@ -22,28 +21,17 @@
#include "core/utils/networkUtilities.h"
#include "core/utils/utilities.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#endif
namespace
{
void execNetworkWaitLoop(QEventLoop &wait)
{
#ifdef Q_OS_IOS
wait.exec();
#else
wait.exec(QEventLoop::ExcludeUserInputEvents);
#endif
}
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
@@ -51,29 +39,18 @@ namespace
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeRequestTimeout = 408;
constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QString normalizedGatewayBase(const QString &endpoint)
{
QString e = endpoint.trimmed();
if (e.isEmpty()) {
return e;
}
if (!e.endsWith(QLatin1Char('/'))) {
e.append(QLatin1Char('/'));
}
return e;
}
} // namespace
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent)
: QObject(parent),
m_gatewayEndpoint(normalizedGatewayBase(gatewayEndpoint)),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
@@ -161,8 +138,6 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
Q_UNUSED(replyError);
DecryptionResult result;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
@@ -179,29 +154,6 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
return result;
}
GatewayController::DecryptionResult GatewayController::resolveResponseBody(const QByteArray &responseBody,
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
DecryptionResult result = tryDecryptResponseBody(responseBody, replyError, key, iv, salt);
if (result.isDecryptionSuccessful || !m_isDevEnvironment) {
return result;
}
const QByteArray trimmed = responseBody.trimmed();
if (trimmed.isEmpty() || trimmed.front() != '{') {
return result;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(trimmed, &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
result.decryptedBody = trimmed;
result.isDecryptionSuccessful = true;
}
return result;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
@@ -216,7 +168,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait);
wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
@@ -225,18 +177,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater();
if (encRequestData.isPlaintextLocalGateway) {
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
if (errorCode) {
return errorCode;
}
responseBody = encryptedResponseBody;
return ErrorCode::NoError;
}
auto decryptionResult =
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
@@ -252,7 +194,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
@@ -267,8 +209,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
responseBody = decryptionResult.decryptedBody;
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
if (errorCode) {
return errorCode;
}
@@ -278,19 +221,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
return ErrorCode::ApiConfigDecryptionError;
}
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject &apiPayload,
QNetworkReply **activeReplyOut,
const QSharedPointer<GatewayController> &keepAlive)
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
const QSharedPointer<GatewayController> life = keepAlive;
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
@@ -299,22 +237,12 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
if (activeReplyOut) {
*activeReplyOut = reply;
}
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, life]() mutable {
if (!life) {
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
GatewayController *const ctl = life.data();
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
@@ -322,20 +250,8 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
reply->deleteLater();
if (encRequestData.isPlaintextLocalGateway) {
const auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode,
encryptedResponseBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
} else {
promise->addResult(qMakePair(ErrorCode::NoError, encryptedResponseBody));
}
promise->finish();
return;
}
auto decryptionResult =
ctl->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
@@ -343,7 +259,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->finish();
return;
}
@@ -360,13 +276,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
promise->finish();
};
if (sslErrors->isEmpty() && ctl->shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList primaryBaseUrls;
QStringList fallbackBaseUrls;
if (ctl->m_isDevEnvironment) {
if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
@@ -393,27 +309,19 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
life->getProxyUrlsAsync(life, proxyStorageUrls, 0,
[life, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
life->getProxyUrlAsync(life, proxyUrls, 0,
[life, encRequestData, endpoint, processResponse](
const QString &proxyUrl) {
life->bypassProxyAsync(
life, endpoint, proxyUrl, encRequestData,
[processResponse](const QByteArray &decryptedBody,
bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors,
QNetworkReply::NetworkError replyError,
const QString &replyErrorString,
int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError,
replyErrorString, httpStatusCode);
});
});
});
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
});
});
});
} else {
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
@@ -476,7 +384,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait);
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll();
@@ -529,10 +437,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
{
if (m_isDevEnvironment) {
return false;
}
const QByteArray &responseBody = decryptedResponseBody;
int apiHttpStatus = -1;
@@ -558,15 +462,19 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << "the response contains an html tag";
return true;
}
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
return false;
}
if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
}
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
@@ -613,7 +521,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait);
wait.exec(QEventLoop::ExcludeUserInputEvents);
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
@@ -635,7 +543,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait);
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
@@ -664,14 +572,9 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
}
}
void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls,
const int currentProxyStorageIndex, const std::function<void(const QStringList &)> &onComplete)
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete)
{
if (!life) {
onComplete({});
return;
}
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({});
return;
@@ -684,23 +587,17 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
QNetworkReply *reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (!life) {
onComplete({});
reply->deleteLater();
return;
}
GatewayController *const ctl = life.data();
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
QByteArray key = ctl->m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!ctl->m_isDevEnvironment) {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
@@ -717,21 +614,15 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
} catch (...) {
Utils::logException();
qCritical() << "error decrypting payload";
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
if (life) {
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
} else {
onComplete({});
}
});
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
return;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray) {
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
}
QStringList shuffled = endpoints;
std::random_device randomDevice;
@@ -746,26 +637,16 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
if (life) {
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
} else {
onComplete({});
}
});
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls,
const int currentProxyIndex, const std::function<void(const QString &)> &onComplete)
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
std::function<void(const QString &)> onComplete)
{
if (!life) {
onComplete(QString());
return;
}
if (currentProxyIndex >= proxyUrls.size()) {
onComplete(QString());
onComplete("");
return;
}
@@ -776,16 +657,13 @@ void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController>
QNetworkReply *reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, [life, proxyUrls, currentProxyIndex, onComplete, reply]() {
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
// *(state->sslErrors) = e;
// });
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
reply->deleteLater();
if (!life) {
onComplete(QString());
return;
}
GatewayController *const ctl = life.data();
if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl);
@@ -793,28 +671,15 @@ void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController>
}
qDebug() << "go to the next proxy endpoint";
QTimer::singleShot(0, ctl, [life, proxyUrls, currentProxyIndex, onComplete]() {
if (life) {
life->getProxyUrlAsync(life, proxyUrls, currentProxyIndex + 1, onComplete);
} else {
onComplete(QString());
}
});
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::bypassProxyAsync(
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl,
const EncryptedRequestData &encRequestData,
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)>
&onComplete)
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (!life) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
return;
}
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
@@ -825,9 +690,9 @@ void GatewayController::bypassProxyAsync(
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, reply, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [life, sslErrors, onComplete, encRequestData, reply]() {
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
@@ -835,13 +700,8 @@ void GatewayController::bypassProxyAsync(
reply->deleteLater();
if (!life) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
return;
}
auto decryptionResult = life->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv,
encRequestData.salt);
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode);
+6 -14
View File
@@ -1,8 +1,6 @@
#ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H
#include <functional>
#include <QFuture>
#include <QNetworkReply>
#include <QObject>
@@ -27,9 +25,7 @@ public:
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload,
QNetworkReply **activeReplyOut = nullptr,
const QSharedPointer<GatewayController> &keepAlive = {});
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
struct EncryptedRequestData
@@ -40,7 +36,6 @@ private:
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode;
bool isPlaintextLocalGateway = false;
};
struct DecryptionResult
@@ -52,8 +47,6 @@ private:
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
DecryptionResult resolveResponseBody(const QByteArray &responseBody, QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
@@ -61,13 +54,12 @@ private:
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls, int currentProxyStorageIndex,
const std::function<void(const QStringList &)> &onComplete);
void getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, int currentProxyIndex,
const std::function<void(const QString &)> &onComplete);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, const EncryptedRequestData &encRequestData,
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> &onComplete);
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
@@ -48,6 +48,7 @@ signals:
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(const QString &serverId, DockerContainer container);
void revokeClientRequested(const QString &serverId, int row, DockerContainer container);
void revokeFinished(ErrorCode errorCode);
void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container);
public slots:
@@ -486,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = configKey::amneziaOpenvpn;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp("dhcp-option DNS (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)");
QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(data);
@@ -645,7 +645,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = containerName;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp(
"DNS = "
@@ -699,7 +699,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
? configKey::amneziaSsxray
: configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}
@@ -20,6 +20,7 @@
#include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/installers/telemtInstaller.h"
#include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h"
@@ -71,6 +72,16 @@ namespace
}
return false;
}
QString buildRemoveContainerScript(const amnezia::ScriptVars &vars, bool removeDataVolume)
{
QString script = SshSession::replaceVars(amnezia::scriptData(SharedScriptType::remove_container), vars);
if (removeDataVolume) {
script += QLatin1String("\nsudo docker volume rm -f $CONTAINER_NAME-data 2>/dev/null || true");
script = SshSession::replaceVars(script, vars);
}
return script;
}
}
InstallController::InstallController(SecureServersRepository *serversRepository,
@@ -119,9 +130,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
sshSession.runScript(credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
amnezia::genBaseVars(credentials, container, QString(), QString())));
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start";
@@ -146,8 +158,8 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return startupContainerWorker(credentials, container, config, sshSession);
}
ErrorCode InstallController::updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig)
{
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
@@ -179,7 +191,17 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
SshSession sshSession(this);
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired;
qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired;
bool xrayServerSettingsChanged = false;
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
xrayServerSettingsChanged =
!oldXrayConfig->serverConfig.hasEqualServerSettings(newXrayConfig->serverConfig);
}
}
ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) {
@@ -191,13 +213,30 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
}
}
const bool skipXrayInboundSync =
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
XrayConfigurator xrayConfigurator(&sshSession);
qDebug() << "InstallController::updateServerConfig applying Xray server inbound sync, reinstall="
<< reinstallRequired;
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
if (errorCode != ErrorCode::NoError) {
qDebug() << "InstallController::updateServerConfig Xray inbound sync failed, error="
<< static_cast<int>(errorCode);
}
}
if (errorCode == ErrorCode::NoError) {
if (container == DockerContainer::MtProxy) {
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
} else if (container == DockerContainer::Telemt) {
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
}
clearCachedProfile(serverId, container);
if (reinstallRequired) {
clearCachedProfile(serverId, container);
}
adminConfig->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
}
@@ -205,6 +244,41 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
return errorCode;
}
ErrorCode InstallController::updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig)
{
switch (m_serversRepository->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
auto config = m_serversRepository->selfHostedAdminConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
auto config = m_serversRepository->selfHostedUserConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::SelfHostedUser);
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
auto config = m_serversRepository->nativeConfig(serverId);
if (!config.has_value()) {
return ErrorCode::InternalError;
}
config->updateContainerConfig(container, newConfig);
m_serversRepository->editServer(serverId, config->toJson(), serverConfigUtils::ConfigType::Native);
return ErrorCode::NoError;
}
default:
return ErrorCode::InternalError;
}
}
void InstallController::clearCachedProfile(const QString &serverId, DockerContainer container)
{
if (ContainerUtils::containerService(container) == ServiceType::Other) {
@@ -216,9 +290,9 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
return;
}
adminConfig->clearCachedClientProfile(container);
const ContainerConfig containerConfigModel = adminConfig->containerConfig(container);
adminConfig->clearCachedClientProfile(container);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
emit clientRevocationRequested(serverId, containerConfigModel, container);
@@ -226,38 +300,75 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
{
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
const auto kind = m_serversRepository->serverKind(serverId);
DockerContainer container = DockerContainer::None;
ContainerConfig containerConfig;
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
containerConfig = cfg->containerConfig(container);
break;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
containerConfig = cfg->containerConfig(container);
break;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
containerConfig = cfg->containerConfig(container);
break;
}
default:
return ErrorCode::InternalError;
}
DockerContainer container = adminConfig->defaultContainer;
if (container == DockerContainer::None) {
return ErrorCode::NoInstalledContainersError;
}
ContainerConfig containerConfig = adminConfig->containerConfig(container);
if (containerConfig.protocolConfig.hasClientConfig()) {
return ErrorCode::NoError;
}
if (kind != serverConfigUtils::ConfigType::SelfHostedAdmin) {
return ErrorCode::InternalError;
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession;
auto isProtocolConfigExists = [](const ContainerConfig &cfg) {
return cfg.protocolConfig.hasClientConfig();
};
if (!isProtocolConfigExists(containerConfig)) {
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
adminConfig->updateContainerConfig(container, containerConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
const QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
const ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
adminConfig->updateContainerConfig(container, containerConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
return ErrorCode::NoError;
}
@@ -290,7 +401,7 @@ void InstallController::addEmptyServer(const ServerCredentials &credentials)
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
serverConfig.description = m_serversRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None;
@@ -601,12 +712,19 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
}
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto* newXrayConfig = newConfig.getXrayProtocolConfig();
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) {
if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
? QString(protocols::xray::defaultPort)
: oldXrayConfig->serverConfig.port;
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
? QString(protocols::xray::defaultPort)
: newXrayConfig->serverConfig.port;
if (oldPort != newPort) {
return true;
}
}
}
@@ -720,8 +838,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
@@ -734,8 +852,19 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))
if (stdOut.contains("Container runtime is not supported"))
return ErrorCode::ServerContainerRuntimeNotSupported;
QRegularExpression notFoundRegex(
R"(^.*(?:sudo:|docker:).*not found.*$)",
QRegularExpression::MultilineOption);
if (notFoundRegex.match(stdOut).hasMatch()) {
return ErrorCode::ServerDockerFailedError;
}
if (stdOut.contains("Container runtime service not running"))
return ErrorCode::ContainerRuntimeServiceNotRunning;
return error;
}
@@ -772,7 +901,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
return ErrorCode::ServerUserDirectoryNotAccessible;
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
return ErrorCode::ServerUserNotAllowedInSudoers;
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
return ErrorCode::ServerUserPasswordRequired;
@@ -905,10 +1034,11 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
return ErrorCode::InternalError;
}
SshSession sshSession(this);
ErrorCode errorCode = sshSession.runScript(
credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
amnezia::genBaseVars(credentials, container, QString(), QString())));
const amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
ErrorCode errorCode =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
if (errorCode == ErrorCode::NoError) {
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
@@ -1093,7 +1223,7 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials,
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
serverConfig.description = m_serversRepository->nextAvailableServerName();
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
serverConfig.containers.insert(iterator.key(), iterator.value());
@@ -1163,28 +1293,26 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
return ErrorCode::NoError;
}
ErrorCode InstallController::checkSshConnection(const ServerCredentials &credentials, QString &output,
ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback)
{
SshSession sshSession(this);
ErrorCode errorCode = ErrorCode::NoError;
ServerCredentials processedCredentials = credentials;
if (processedCredentials.secretData.contains("BEGIN") && processedCredentials.secretData.contains("PRIVATE KEY")) {
if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
if (!passphraseCallback) {
return ErrorCode::SshPrivateKeyError;
}
QString decryptedPrivateKey;
errorCode = sshSession.getDecryptedPrivateKey(processedCredentials, decryptedPrivateKey, passphraseCallback);
errorCode = sshSession.getDecryptedPrivateKey(credentials, decryptedPrivateKey, passphraseCallback);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
processedCredentials.secretData = decryptedPrivateKey;
credentials.secretData = decryptedPrivateKey;
}
output = sshSession.checkSshConnection(processedCredentials, errorCode);
output = sshSession.checkSshConnection(credentials, errorCode);
return errorCode;
}
@@ -1388,7 +1516,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = containerAndPortMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) {
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
continue;
}
@@ -1413,7 +1541,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = torOrDnsRegMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) {
if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
continue;
}
@@ -34,7 +34,12 @@ public:
~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
ErrorCode updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
ErrorCode rebootServer(const QString &serverId);
ErrorCode removeAllContainers(const QString &serverId);
@@ -64,7 +69,8 @@ public:
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig);
ErrorCode checkSshConnection(const ServerCredentials &credentials, QString &output, std::function<QString()> passphraseCallback = nullptr);
ErrorCode checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback = nullptr);
bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);
@@ -698,7 +698,7 @@ ErrorCode UsersController::revokeXray(const int row,
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = sshSession->runScript(
credentials,
credentials,
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
@@ -758,14 +758,17 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
ContainerConfig containerCfg = adminConfig->containerConfig(container);
QString containerClientId = containerCfg.protocolConfig.clientId();
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
if (isAdminMatch) {
emit adminConfigRevoked(serverId, container);
}
emit clientRevoked(index);
emit clientsUpdated(m_clientsTable);
}
emit clientsUpdated(m_clientsTable);
emit revokeFinished(errorCode);
return errorCode;
}
@@ -37,6 +37,7 @@ signals:
void clientAdded(const QJsonObject &client);
void clientRenamed(int row, const QString &newName);
void clientRevoked(int row);
void revokeFinished(ErrorCode errorCode);
void adminConfigRevoked(const QString &serverId, DockerContainer container);
public slots:
@@ -44,6 +44,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
@@ -51,6 +52,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
@@ -58,6 +60,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
@@ -67,6 +70,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) return false;
cfg->name = name;
cfg->displayName = name;
cfg->nameOverriddenByUser = true;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
+131 -1
View File
@@ -8,10 +8,19 @@
#include "version.h"
#include "ui/utils/qAutoStart.h"
#include "logger.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "core/local-proxy/portavailabilityhelper.h"
#endif
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
namespace {
constexpr int kDefaultProxyPort = 10808;
constexpr int kLocalProxyPortMin = 1024;
constexpr int kLocalProxyPortMax = 65535;
}
QString getPlatformName()
{
#if defined(Q_OS_WINDOWS)
@@ -38,6 +47,11 @@ SettingsController::SettingsController(SecureServersRepository* serversRepositor
{
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
m_isDevModeEnabled = m_appSettingsRepository->isDevGatewayEnv();
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this,
&SettingsController::localProxySettingsUpdated);
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxyStartFailed, this,
&SettingsController::localProxyStartFailed);
}
void SettingsController::toggleAmneziaDns(bool enable)
@@ -217,6 +231,11 @@ void SettingsController::toggleAutoStart(bool enable)
bool SettingsController::isStartMinimizedEnabled() const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!isAutoStartEnabled()) {
return false;
}
#endif
return m_appSettingsRepository->isStartMinimized();
}
@@ -358,6 +377,117 @@ void SettingsController::disablePremV1MigrationReminder()
QString SettingsController::nextAvailableServerName() const
{
return m_appSettingsRepository->nextAvailableServerName();
return m_serversRepository->nextAvailableServerName();
}
bool SettingsController::isLocalProxySupported() const
{
#ifdef AMNEZIA_DESKTOP
return true;
#else
return false;
#endif
}
bool SettingsController::isLocalProxyHttpEnabled() const
{
return m_appSettingsRepository->isLocalProxyHttpEnabled();
}
int SettingsController::localProxyPort() const
{
return static_cast<int>(m_appSettingsRepository->localProxyPort());
}
QString SettingsController::localProxyOwnerId() const
{
return m_appSettingsRepository->localProxyOwnerId();
}
bool SettingsController::isLocalProxyPortUserDefined() const
{
return m_appSettingsRepository->isLocalProxyPortUserDefined();
}
bool SettingsController::setLocalProxyPort(int port)
{
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax) {
return false;
}
if (m_appSettingsRepository->localProxyPort() == static_cast<quint16>(port)) {
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
return true;
}
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
return true;
}
bool SettingsController::isLocalProxyPortBusy(int port) const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
return !PortAvailabilityHelper::isPortAvailable(port);
#else
Q_UNUSED(port);
return false;
#endif
}
int SettingsController::findFirstAvailableLocalProxyPort(int startPort) const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
const auto port = PortAvailabilityHelper::findFirstAvailablePort(startPort, kLocalProxyPortMax);
return port ? *port : -1;
#else
Q_UNUSED(startPort);
return -1;
#endif
}
bool SettingsController::enableLocalProxy(const QString &ownerId, int port)
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
Q_UNUSED(ownerId);
Q_UNUSED(port);
return false;
#else
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerId.isEmpty()) {
return false;
}
if (m_appSettingsRepository->isLocalProxyHttpEnabled() && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
return false;
}
int selectedPort = port;
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
if (isUserDefinedPort) {
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
return false;
}
} else if (selectedPort != kDefaultProxyPort && !PortAvailabilityHelper::isPortAvailable(selectedPort)) {
return false;
}
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(selectedPort)) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(selectedPort));
}
m_appSettingsRepository->setLocalProxyPortUserDefined(isUserDefinedPort);
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
m_appSettingsRepository->setLocalProxyHttpEnabled(true);
return true;
#endif
}
void SettingsController::disableLocalProxy()
{
if (m_appSettingsRepository->isLocalProxyHttpEnabled()) {
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
}
}
@@ -89,6 +89,18 @@ public:
QLocale getAppLanguage() const;
void setAppLanguage(const QLocale &locale);
// Local proxy
bool isLocalProxySupported() const;
bool isLocalProxyHttpEnabled() const;
int localProxyPort() const;
QString localProxyOwnerId() const;
bool isLocalProxyPortUserDefined() const;
bool setLocalProxyPort(int port);
bool isLocalProxyPortBusy(int port) const;
int findFirstAvailableLocalProxyPort(int startPort) const;
bool enableLocalProxy(const QString &ownerId, int port);
void disableLocalProxy();
signals:
void siteSplitTunnelingRouteModeChanged(RouteMode mode);
void siteSplitTunnelingToggled(bool enabled);
@@ -96,6 +108,9 @@ signals:
void appSplitTunnelingToggled(bool enabled);
void appSplitTunnelingClearAppsList();
void localProxySettingsUpdated();
void localProxyStartFailed(const QString &message);
private:
QString getPlatform() const;
+17 -51
View File
@@ -13,7 +13,6 @@
#include "version.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/errorStrings.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
namespace
@@ -21,13 +20,13 @@ namespace
Logger logger("UpdateController");
#if defined(Q_OS_WINDOWS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.exe");
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
#elif defined(Q_OS_MACOS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg");
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run");
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
#endif
}
@@ -57,10 +56,6 @@ void UpdateController::checkForUpdates()
if (m_updateCheckRunning || !m_appSettingsRepository) {
return;
}
if (m_appSettingsRepository->isDevGatewayEnv()) {
return;
}
m_updateCheckRunning = true;
fetchGatewayUrl();
@@ -97,11 +92,6 @@ void UpdateController::doGetAsync(const QString &endpoint, std::function<void(bo
void UpdateController::fetchGatewayUrl()
{
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
finishUpdateCheck();
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
7000,
@@ -114,19 +104,11 @@ void UpdateController::fetchGatewayUrl()
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
finishUpdateCheck();
return;
}
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload, nullptr, gatewayController)
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) {
if (err == ErrorCode::ApiNotFoundError) {
logger.debug() << "Update check: updater_endpoint not found on gateway";
} else {
logger.error() << errorString(err);
}
logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
finishUpdateCheck();
return;
}
@@ -201,7 +183,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
.arg(operation, reply->errorString(), QString::number(error));
});
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
QStringList errorStrings;
for (const QSslError &err : errors) {
@@ -213,21 +195,13 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
QString UpdateController::composeDownloadUrl() const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
return m_baseUrl + "/" + fileName;
#else
@@ -237,7 +211,7 @@ QString UpdateController::composeDownloadUrl() const
void UpdateController::runInstaller()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
if (m_downloadUrl.isEmpty()) {
logger.error() << "Download URL is empty";
return;
@@ -269,23 +243,15 @@ void UpdateController::runInstaller()
#if defined(Q_OS_WINDOWS)
runWindowsInstaller(kInstallerLocalPath);
#elif defined(Q_OS_MACOS)
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
runMacInstaller(kInstallerLocalPath);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
runLinuxInstaller(kInstallerLocalPath);
#endif
} else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << QString::fromUtf8(reply->readAll());
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
<< reply->errorString();
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
reply->deleteLater();
});
@@ -309,7 +275,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
}
#endif
#if defined(Q_OS_MACOS)
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
int UpdateController::runMacInstaller(const QString &installerPath)
{
// Create temporary directory for extraction
+27 -6
View File
@@ -1,15 +1,17 @@
#include "socks5Installer.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/utilities.h"
#include <QRegularExpression>
using namespace amnezia;
using namespace ProtocolUtils;
@@ -33,10 +35,29 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, ContainerConfig &config)
{
Q_UNUSED(container);
Q_UNUSED(credentials);
Q_UNUSED(sshSession);
Q_UNUSED(config);
if (container != DockerContainer::Socks5Proxy || !sshSession) {
return ErrorCode::NoError;
}
Socks5ProxyProtocolConfig *socks5Config = config.getSocks5ProxyProtocolConfig();
if (!socks5Config) {
return ErrorCode::NoError;
}
ErrorCode readError = ErrorCode::NoError;
const QByteArray configRaw = sshSession->getTextFileFromContainer(
container, credentials, QString::fromUtf8(protocols::socks5Proxy::proxyConfigPath), readError);
if (readError != ErrorCode::NoError || configRaw.trimmed().isEmpty()) {
return ErrorCode::NoError;
}
const QString proxyConfig = QString::fromUtf8(configRaw);
static const QRegularExpression usernameAndPasswordRegExp(QStringLiteral("users (\\w+):CL:(\\w+)"));
const QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
if (usernameAndPasswordMatch.hasMatch()) {
socks5Config->userName = usernameAndPasswordMatch.captured(1);
socks5Config->password = usernameAndPasswordMatch.captured(2);
}
return ErrorCode::NoError;
}
+444
View File
@@ -0,0 +1,444 @@
#include "configmanager.h"
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/containers/containerUtils.h"
#include "portavailabilityhelper.h"
#include "proxylogger.h"
#include "version.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QSaveFile>
#include <QSysInfo>
#include <QStandardPaths>
#include <QUuid>
using namespace amnezia;
ConfigManager::ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository)
: m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository)
{
ProxyLogger::getInstance().debug("ConfigManager initialized (repository-backed)");
}
namespace {
namespace gateway_key {
constexpr char vless[] = "vless";
} // namespace gateway_key
constexpr quint16 kDefaultProxyPort = 10808;
constexpr int kProxyPortMin = 1024;
constexpr int kProxyPortMax = 65535;
int resolveProxyPort(SecureAppSettingsRepository *appSettings)
{
if (!appSettings) {
return kDefaultProxyPort;
}
const quint16 port = appSettings->localProxyPort();
if (port < kProxyPortMin || port > kProxyPortMax) {
return kDefaultProxyPort;
}
return static_cast<int>(port);
}
} // namespace
bool ConfigManager::applyProxyPortToConfig(QJsonObject &config, int port) const
{
if (!config.contains("inbounds") || !config.value("inbounds").isArray()) {
return false;
}
QJsonArray inbounds = config.value("inbounds").toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return false;
}
QJsonObject firstInbound = inbounds.at(0).toObject();
firstInbound.insert("port", port);
inbounds[0] = firstInbound;
config.insert("inbounds", inbounds);
return true;
}
QString ConfigManager::serializeConfig(const QJsonObject &config) const
{
return QString::fromUtf8(QJsonDocument(config).toJson(QJsonDocument::Compact));
}
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfig(QString &errorDescription) const
{
errorDescription.clear();
if (!m_serversRepository || !m_appSettingsRepository) {
const QString message = QStringLiteral("Local proxy repositories are not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
const QString message = QStringLiteral("Local proxy owner server id is not configured");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
.arg(ownerServer->value(configKey::name).toString());
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
if (!serializedConfig || serializedConfig->isEmpty()) {
const QString message = QStringLiteral("Server %1 lacks Xray last_config payload")
.arg(ownerServer->value(configKey::name).toString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
ConfigData data;
data.ownerId = ownerId;
data.serverName = ownerServer->value(configKey::name).toString();
data.parsedConfig = doc.object();
const int proxyPort = resolveProxyPort(m_appSettingsRepository);
if (applyProxyPortToConfig(data.parsedConfig, proxyPort)) {
data.serializedConfig = serializeConfig(data.parsedConfig);
} else {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
data.serializedConfig = *serializedConfig;
}
return data;
}
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfigWithFetch(QString &errorDescription) const
{
errorDescription.clear();
if (!m_serversRepository || !m_appSettingsRepository) {
const QString message = QStringLiteral("Local proxy repositories are not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
const QString message = QStringLiteral("Local proxy owner server id is not configured");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
.arg(ownerServer->value(configKey::name).toString());
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
if (!serializedConfig || serializedConfig->isEmpty()) {
auto fetchedConfig = fetchSerializedXrayConfigFromGateway(*ownerServer, errorDescription);
if (!fetchedConfig || fetchedConfig->isEmpty()) {
return std::nullopt;
}
serializedConfig = fetchedConfig;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
ConfigData data;
data.ownerId = ownerId;
data.serverName = ownerServer->value(configKey::name).toString();
data.parsedConfig = doc.object();
int selectedPort = resolveProxyPort(m_appSettingsRepository);
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
const bool canAutoSelect = !isUserDefinedPort && selectedPort == kDefaultProxyPort;
if (canAutoSelect) {
const auto freePort = PortAvailabilityHelper::findFirstAvailablePort(kDefaultProxyPort + 1, kProxyPortMax);
if (!freePort) {
errorDescription = QStringLiteral("No available local proxy port in range %1-%2")
.arg(kDefaultProxyPort + 1)
.arg(kProxyPortMax);
ProxyLogger::getInstance().error(errorDescription);
return std::nullopt;
}
selectedPort = *freePort;
} else {
errorDescription = QStringLiteral("Local proxy port %1 is already in use")
.arg(selectedPort);
ProxyLogger::getInstance().error(errorDescription);
return std::nullopt;
}
}
if (applyProxyPortToConfig(data.parsedConfig, selectedPort)) {
data.serializedConfig = serializeConfig(data.parsedConfig);
} else {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
data.serializedConfig = *serializedConfig;
}
return data;
}
bool ConfigManager::writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const
{
errorDescription.clear();
configPath.clear();
const QString directory = tempDirectory();
if (!QDir().mkpath(directory)) {
const QString message = QStringLiteral("Failed to create temp config directory: %1").arg(directory);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
const QString path = tempConfigPath();
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
const QString message = QStringLiteral("Failed to open temp config file %1: %2").arg(path, file.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
if (file.write(serializedConfig.toUtf8()) == -1) {
const QString message = QStringLiteral("Failed to write temp config file %1: %2").arg(path, file.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
if (!file.commit()) {
const QString message = QStringLiteral("Failed to commit temp config file %1").arg(path);
ProxyLogger::getInstance().error(message);
errorDescription = message;
return false;
}
ProxyLogger::getInstance().info(QStringLiteral("Xray config saved to %1").arg(path));
configPath = path;
return true;
}
bool ConfigManager::removeTempConfig() const
{
const QString path = tempConfigPath();
QFile file(path);
if (!file.exists()) {
return true;
}
if (!file.remove()) {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to remove temp config file %1: %2").arg(path, file.errorString()));
return false;
}
ProxyLogger::getInstance().debug(QStringLiteral("Removed temp config file %1").arg(path));
return true;
}
QString ConfigManager::tempConfigPath() const
{
return QDir(tempDirectory()).filePath(QStringLiteral("xray_active.json"));
}
std::optional<QString> ConfigManager::extractSerializedXrayConfig(const QJsonObject &server) const
{
const QJsonArray containers = server.value(configKey::containers).toArray();
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
const QString protoKey = QString(configKey::xray);
for (const QJsonValue &value : containers) {
const QJsonObject container = value.toObject();
if (container.value(configKey::container).toString() != targetContainer) {
continue;
}
const QJsonObject proto = container.value(protoKey).toObject();
const QString serialized = proto.value(configKey::lastConfig).toString();
if (!serialized.isEmpty()) {
return serialized;
}
}
return std::nullopt;
}
std::optional<QString> ConfigManager::fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const
{
errorDescription.clear();
if (!m_appSettingsRepository) {
const QString message = QStringLiteral("App settings repository is not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject();
if (apiConfig.isEmpty()) {
const QString message = QStringLiteral("Server API config is missing");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const QString userCountryCode = apiConfig.value(apiDefs::key::userCountryCode).toString();
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
if (userCountryCode.isEmpty() || serviceType.isEmpty()) {
const QString message = QStringLiteral("Server API config lacks service identifiers");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
QJsonObject apiPayload;
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
const QString appLanguage = m_appSettingsRepository->getAppLanguage().name().split("_").first();
if (!appLanguage.isEmpty()) {
apiPayload[apiDefs::key::appLanguage] = appLanguage;
}
apiPayload[apiDefs::key::uuid] = m_appSettingsRepository->getInstallationUuid(true);
apiPayload[apiDefs::key::userCountryCode] = userCountryCode;
apiPayload[apiDefs::key::serviceType] = serviceType;
apiPayload[apiDefs::key::serviceProtocol] = QString(gateway_key::vless);
apiPayload[apiDefs::key::publicKey] = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QJsonObject authData = server.value(apiDefs::key::authData).toObject();
if (!authData.isEmpty()) {
apiPayload[apiDefs::key::authData] = authData;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const amnezia::ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode != amnezia::ErrorCode::NoError) {
const QString message = QStringLiteral("Gateway request failed with error code %1").arg(static_cast<int>(errorCode));
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
QJsonParseError responseError;
const QJsonDocument responseDoc = QJsonDocument::fromJson(responseBody, &responseError);
if (responseError.error != QJsonParseError::NoError || !responseDoc.isObject()) {
const QString message = QStringLiteral("Failed to parse gateway response: %1").arg(responseError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
QString data = responseDoc.object().value(configKey::config).toString();
if (data.isEmpty()) {
const QString message = QStringLiteral("Gateway response lacks config payload");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
data.replace("vpn://", "");
QByteArray decoded = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (decoded.isEmpty()) {
const QString message = QStringLiteral("Gateway config payload is empty");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QByteArray uncompressed = qUncompress(decoded);
if (!uncompressed.isEmpty()) {
decoded = uncompressed;
}
QJsonParseError configError;
const QJsonDocument configDoc = QJsonDocument::fromJson(decoded, &configError);
if (configError.error != QJsonParseError::NoError || !configDoc.isObject()) {
const QString message = QStringLiteral("Failed to parse gateway config JSON: %1").arg(configError.errorString());
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const auto serializedConfig = extractSerializedXrayConfig(configDoc.object());
if (!serializedConfig || serializedConfig->isEmpty()) {
const QString message = QStringLiteral("Gateway response lacks Xray last_config payload");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
ProxyLogger::getInstance().info("Fetched Xray config from gateway");
return serializedConfig;
}
QString ConfigManager::tempDirectory() const
{
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if (baseDir.isEmpty()) {
return QDir::temp().filePath(QStringLiteral("amnezia_local_proxy"));
}
return QDir(baseDir).filePath(QStringLiteral("local_proxy"));
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <optional>
#include <QJsonObject>
#include <QString>
class SecureServersRepository;
class SecureAppSettingsRepository;
class ConfigManager {
public:
struct ConfigData {
QString ownerId;
QString serverName;
QString serializedConfig;
QJsonObject parsedConfig;
};
ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository);
std::optional<ConfigData> buildConfig(QString &errorDescription) const;
std::optional<ConfigData> buildConfigWithFetch(QString &errorDescription) const;
bool writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const;
bool removeTempConfig() const;
QString tempConfigPath() const;
private:
std::optional<QString> extractSerializedXrayConfig(const QJsonObject &server) const;
std::optional<QString> fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const;
QString tempDirectory() const;
bool applyProxyPortToConfig(QJsonObject &config, int port) const;
QString serializeConfig(const QJsonObject &config) const;
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
};
+190
View File
@@ -0,0 +1,190 @@
#include "httpapi.h"
#include "proxylogger.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QHostAddress>
#include <optional>
namespace {
std::optional<int> extractInboundPort(const QJsonObject &config)
{
if (!config.contains("inbounds") || !config["inbounds"].isArray()) {
return std::nullopt;
}
const QJsonArray inbounds = config["inbounds"].toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return std::nullopt;
}
const QJsonObject firstInbound = inbounds.at(0).toObject();
if (!firstInbound.contains("port")) {
return std::nullopt;
}
return firstInbound.value("port").toInt();
}
QJsonValue proxyPortValue(const std::optional<int> &port)
{
if (port.has_value()) {
return QJsonValue(*port);
}
return QJsonValue::Null;
}
QHttpServerResponse makeServiceUnavailablePingResponse()
{
QJsonObject payload{
{"status", "error"},
{"proxyPort", QJsonValue::Null}
};
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
}
QHttpServerResponse makeServiceUnavailableStatusResponse()
{
QJsonObject payload{
{"status", "error"}
};
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
}
} // namespace
HttpApi::HttpApi(QWeakPointer<IProxyService> service, QObject* parent)
: QObject(parent)
, m_tcpServer(new QTcpServer(this))
, m_service(service)
{
ProxyLogger::getInstance().debug("HttpApi initialized");
}
HttpApi::~HttpApi()
{
stop();
}
bool HttpApi::start(quint16 port)
{
ProxyLogger::getInstance().info(QString("Starting HTTP API server on port %1").arg(port));
if (!m_tcpServer->listen(QHostAddress::LocalHost, port)) {
ProxyLogger::getInstance().error(QString("Failed to start HTTP API server on port %1").arg(port));
return false;
}
setupRoutes();
m_server.bind(m_tcpServer.data());
ProxyLogger::getInstance().info(QString("HTTP API server is running on localhost:%1").arg(m_tcpServer->serverPort()));
ProxyLogger::getInstance().debug("Available endpoints:\n"
" POST /api/v1/up\n"
" POST /api/v1/down\n"
" GET /api/v1/ping");
return true;
}
void HttpApi::stop()
{
ProxyLogger::getInstance().info("Stopping HTTP API server");
if (m_tcpServer) {
m_tcpServer->close();
}
}
void HttpApi::setupRoutes()
{
ProxyLogger::getInstance().debug("Setting up HTTP API routes");
m_server.route("/api/v1/up", QHttpServerRequest::Method::Post,
[this] {
ProxyLogger::getInstance().debug("Handling POST /api/v1/up request");
return handlePostUp();
});
m_server.route("/api/v1/down", QHttpServerRequest::Method::Post,
[this] {
ProxyLogger::getInstance().debug("Handling POST /api/v1/down request");
return handlePostDown();
});
m_server.route("/api/v1/ping", QHttpServerRequest::Method::Get,
[this] {
ProxyLogger::getInstance().debug("Handling GET /api/v1/ping request");
return handleGetPing();
});
}
QHttpServerResponse HttpApi::handlePostUp()
{
if (auto service = m_service.lock()) {
const bool started = service->startXray();
QJsonObject response;
response["status"] = started ? "ok" : "error";
const auto port = started ? extractInboundPort(service->getConfig()) : std::optional<int>{};
response["proxyPort"] = proxyPortValue(port);
if (started) {
if (port.has_value()) {
ProxyLogger::getInstance().info(QString("Xray process started on port %1").arg(*port));
} else {
ProxyLogger::getInstance().warning("Xray started but inbound port is unknown (local proxy owner may be missing)");
}
} else {
ProxyLogger::getInstance().warning("Failed to start Xray process via HTTP API");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailablePingResponse();
}
QHttpServerResponse HttpApi::handlePostDown()
{
if (auto service = m_service.lock()) {
const bool stopped = service->stopXray();
QJsonObject response;
response["status"] = stopped ? "ok" : "error";
if (!stopped) {
ProxyLogger::getInstance().warning("Failed to stop Xray process via HTTP API");
} else {
ProxyLogger::getInstance().info("Xray process stopped via HTTP API");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailableStatusResponse();
}
QHttpServerResponse HttpApi::handleGetPing() const
{
if (auto service = m_service.lock()) {
QJsonObject response;
response["status"] = "ok";
const bool isRunning = service->isXrayRunning();
if (isRunning) {
const auto port = extractInboundPort(service->getConfig());
response["proxyPort"] = proxyPortValue(port);
if (port.has_value()) {
ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(*port));
} else {
ProxyLogger::getInstance().warning("Unable to detect inbound port while Xray is running");
}
} else {
response["proxyPort"] = QJsonValue::Null;
ProxyLogger::getInstance().debug("Xray is not running");
}
return QHttpServerResponse(response);
}
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
return makeServiceUnavailablePingResponse();
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <QObject>
#include <QScopedPointer>
#include <QHttpServer>
#include <QHttpServerRequest>
#include <QHttpServerResponse>
#include <QTcpServer>
#include <QWeakPointer>
#include "iproxyservice.h"
class HttpApi : public QObject {
Q_OBJECT
public:
explicit HttpApi(QWeakPointer<IProxyService> service, QObject* parent = nullptr);
~HttpApi();
bool start(quint16 port);
void stop();
private:
void setupRoutes();
QHttpServerResponse handlePostUp();
QHttpServerResponse handlePostDown();
QHttpServerResponse handleGetPing() const;
QHttpServer m_server;
QScopedPointer<QTcpServer> m_tcpServer;
QWeakPointer<IProxyService> m_service;
};
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <QJsonObject>
#include <QString>
class IProxyService {
public:
virtual ~IProxyService() = default;
virtual QJsonObject getConfig() = 0;
virtual bool startXray() = 0;
virtual bool stopXray() = 0;
virtual bool isXrayRunning() const = 0;
virtual qint64 getXrayProcessId() const = 0;
virtual QString getXrayError() const = 0;
};
@@ -0,0 +1,43 @@
#include "portavailabilityhelper.h"
#include <QHostAddress>
#include <QTcpServer>
namespace {
constexpr int kProxyPortMin = 1024;
constexpr int kProxyPortMax = 65535;
}
bool PortAvailabilityHelper::isPortAvailable(int port)
{
if (port < kProxyPortMin || port > kProxyPortMax) {
return false;
}
QTcpServer server;
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
server.close();
return success;
}
std::optional<int> PortAvailabilityHelper::findFirstAvailablePort(int startPort, int endPort)
{
if (startPort < kProxyPortMin) {
startPort = kProxyPortMin;
}
if (endPort > kProxyPortMax) {
endPort = kProxyPortMax;
}
if (startPort > endPort) {
return std::nullopt;
}
for (int port = startPort; port <= endPort; ++port) {
if (isPortAvailable(port)) {
return port;
}
}
return std::nullopt;
}
@@ -0,0 +1,11 @@
#pragma once
#include <optional>
class PortAvailabilityHelper
{
public:
static bool isPortAvailable(int port);
static std::optional<int> findFirstAvailablePort(int startPort, int endPort);
};
+133
View File
@@ -0,0 +1,133 @@
#include "proxylogger.h"
#include <QDir>
#include <QTextStream>
ProxyLogger::ProxyLogger() : m_maxFileSize(0), m_currentLevel(LogLevel::Info)
{
}
ProxyLogger::~ProxyLogger()
{
}
ProxyLogger& ProxyLogger::getInstance()
{
static ProxyLogger instance;
return instance;
}
void ProxyLogger::init(const QString& logPath, qint64 maxFileSize)
{
QMutexLocker locker(&m_mutex);
m_logPath = logPath;
m_maxFileSize = maxFileSize;
// Create logs directory if it doesn't exist
QDir dir = QFileInfo(m_logPath).dir();
if (!dir.exists()) {
dir.mkpath(".");
}
}
void ProxyLogger::setLogLevel(LogLevel level)
{
m_currentLevel = level;
}
void ProxyLogger::log(LogLevel level, const QString& message)
{
logInternal(level, message);
}
void ProxyLogger::debug(const QString& message)
{
logInternal(LogLevel::Debug, message);
}
void ProxyLogger::info(const QString& message)
{
logInternal(LogLevel::Info, message);
}
void ProxyLogger::warning(const QString& message)
{
logInternal(LogLevel::Warning, message);
}
void ProxyLogger::error(const QString& message)
{
logInternal(LogLevel::Error, message);
}
void ProxyLogger::logInternal(LogLevel level, const QString& message)
{
if (m_logPath.isEmpty()) {
return;
}
if (level < m_currentLevel) {
return;
}
QMutexLocker locker(&m_mutex);
checkRotation();
QFile file(m_logPath);
if (!openLogFile(file)) {
return;
}
QTextStream stream(&file);
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
stream << QString("[%1] [%2] %3\n").arg(timestamp, levelToString(level), message);
stream.flush();
file.close();
}
QString ProxyLogger::levelToString(LogLevel level)
{
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
default: return "UNKNOWN";
}
}
qint64 ProxyLogger::getCurrentFileSize() const
{
QFile file(m_logPath);
if (file.exists()) {
return file.size();
}
return -1;
}
void ProxyLogger::checkRotation()
{
if (m_maxFileSize > 0 && getCurrentFileSize() >= m_maxFileSize) {
// Delete the oldest file
QFile::remove(QString("%1.%2").arg(m_logPath).arg(MAX_BACKUP_FILES));
// Shift existing files
for (int i = MAX_BACKUP_FILES - 1; i >= 1; --i) {
QString oldName = QString("%1.%2").arg(m_logPath).arg(i);
QString newName = QString("%1.%2").arg(m_logPath).arg(i + 1);
QFile::rename(oldName, newName);
}
// Rename current file
QFile::rename(m_logPath, m_logPath + ".1");
}
}
bool ProxyLogger::openLogFile(QFile& file)
{
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qDebug() << "Failed to open log file:" << m_logPath;
return false;
}
return true;
}
+54
View File
@@ -0,0 +1,54 @@
#ifndef LOCAL_PROXY_LOGGER_H
#define LOCAL_PROXY_LOGGER_H
#include <QObject>
#include <QFile>
#include <QDateTime>
#include <QMutex>
#include <QString>
class ProxyLogger
{
public:
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
static ProxyLogger& getInstance();
void init(const QString& logPath, qint64 maxFileSize = 1024 * 1024 * 10); // 10MB by default
void setLogLevel(LogLevel level);
// Main logging method
void log(LogLevel level, const QString& message);
// Helper methods for convenience
void debug(const QString& message);
void info(const QString& message);
void warning(const QString& message);
void error(const QString& message);
private:
ProxyLogger();
~ProxyLogger();
ProxyLogger(const ProxyLogger&) = delete;
ProxyLogger& operator=(const ProxyLogger&) = delete;
void logInternal(LogLevel level, const QString& message);
void checkRotation();
QString levelToString(LogLevel level);
bool openLogFile(QFile& file);
qint64 getCurrentFileSize() const;
QString m_logPath;
qint64 m_maxFileSize;
LogLevel m_currentLevel;
QMutex m_mutex;
static const int MAX_BACKUP_FILES = 3;
};
#endif // LOCAL_PROXY_LOGGER_H
+114
View File
@@ -0,0 +1,114 @@
#include "proxyserver.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include <QDebug>
ProxyServer::ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
: QObject(parent)
, m_appSettingsRepository(appSettingsRepository)
, m_service(new ProxyService(serversRepository, appSettingsRepository, this))
{
m_lastRestartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
}
ProxyServer::~ProxyServer()
{
stop();
}
bool ProxyServer::start(quint16 port)
{
if (m_isRunning) {
if (m_currentApiPort == port) {
qInfo() << "Local proxy: already running on port" << port;
return true;
}
qInfo() << "Local proxy: restarting on new port" << port;
stop();
}
m_api.reset(new HttpApi(m_service.toWeakRef()));
const bool apiStarted = m_api->start(port);
if (!apiStarted) {
qWarning() << "Local proxy: port is busy:" << port;
m_api.reset();
m_isRunning = false;
m_currentApiPort = 0;
return false;
}
m_isRunning = true;
m_currentApiPort = port;
return true;
}
void ProxyServer::stop()
{
stopXrayProcess();
if (m_api) {
m_api->stop();
m_api.reset();
}
m_isRunning = false;
m_currentApiPort = 0;
m_currentProxyPort = 0;
}
bool ProxyServer::startXrayProcess()
{
return m_service->startXray();
}
void ProxyServer::stopXrayProcess()
{
m_service->stopXray();
}
bool ProxyServer::syncSettings()
{
if (!m_isRunning) {
qDebug() << "Local proxy: syncSettings called but server is not running";
return false;
}
const quint16 newProxyPort = m_appSettingsRepository ? m_appSettingsRepository->localProxyPort() : 0;
const int restartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
const bool xrayRunning = m_service->isXrayRunning();
if (!xrayRunning) {
qInfo() << "Local proxy: starting Xray on port" << newProxyPort;
const bool started = startXrayProcess();
if (started) {
m_currentProxyPort = newProxyPort;
m_lastRestartToken = restartToken;
}
return started;
}
if (m_lastRestartToken != restartToken) {
qInfo() << "Local proxy: restarting Xray due to config change token";
const bool restarted = m_service->restartXray();
if (restarted) {
m_currentProxyPort = newProxyPort;
m_lastRestartToken = restartToken;
}
return restarted;
}
if (m_currentProxyPort != newProxyPort) {
qInfo() << "Local proxy: proxy port changed from" << m_currentProxyPort << "to" << newProxyPort;
const bool restarted = m_service->restartXray();
if (restarted) {
m_currentProxyPort = newProxyPort;
m_lastRestartToken = restartToken;
}
return restarted;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <QObject>
#include <QScopedPointer>
#include <QSharedPointer>
#include "httpapi.h"
#include "proxyservice.h"
class SecureServersRepository;
class SecureAppSettingsRepository;
class ProxyServer : public QObject
{
Q_OBJECT
public:
ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~ProxyServer();
bool start(quint16 port = 49490);
void stop();
bool syncSettings();
private:
bool startXrayProcess();
void stopXrayProcess();
SecureAppSettingsRepository *m_appSettingsRepository;
QScopedPointer<HttpApi> m_api;
QSharedPointer<ProxyService> m_service;
bool m_isRunning {false};
quint16 m_currentApiPort {0};
quint16 m_currentProxyPort {0};
int m_lastRestartToken {0};
};
+118
View File
@@ -0,0 +1,118 @@
#include "proxyservice.h"
#include "proxylogger.h"
namespace {
void logConfigError(const QString &errorMessage)
{
if (!errorMessage.isEmpty()) {
ProxyLogger::getInstance().error(errorMessage);
}
}
} // namespace
ProxyService::ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
: QObject(parent)
, m_configManager(new ConfigManager(serversRepository, appSettingsRepository))
, m_xrayController(new XrayController())
{
ProxyLogger::getInstance().debug("ProxyService initialized");
}
QJsonObject ProxyService::getConfig()
{
if (!m_cachedConfig.isEmpty()) {
return m_cachedConfig;
}
QString error;
const auto configData = m_configManager->buildConfigWithFetch(error);
if (!configData) {
logConfigError(error);
return {};
}
m_cachedConfig = configData->parsedConfig;
return m_cachedConfig;
}
bool ProxyService::startXray()
{
ProxyLogger::getInstance().info("Starting Xray");
if (m_xrayController->isXrayRunning()) {
ProxyLogger::getInstance().info("Xray is already running");
return true;
}
QString error;
const auto configData = m_configManager->buildConfigWithFetch(error);
if (!configData) {
logConfigError(error);
return false;
}
const bool success = m_xrayController->start(configData->serializedConfig);
if (success) {
m_cachedConfig = configData->parsedConfig;
ProxyLogger::getInstance().info("Xray started successfully");
emit xrayStatusChanged(true);
return true;
}
ProxyLogger::getInstance().error(QStringLiteral("Failed to start Xray: %1").arg(m_xrayController->getError()));
return false;
}
bool ProxyService::stopXray()
{
ProxyLogger::getInstance().info("Stopping Xray");
const bool stopped = m_xrayController->stop();
if (stopped) {
ProxyLogger::getInstance().info("Xray stopped");
emit xrayStatusChanged(false);
return true;
}
ProxyLogger::getInstance().warning(QStringLiteral("Failed to stop Xray: %1").arg(m_xrayController->getError()));
return false;
}
bool ProxyService::isXrayRunning() const
{
return m_xrayController->isXrayRunning();
}
qint64 ProxyService::getXrayProcessId() const
{
return m_xrayController->getProcessId();
}
QString ProxyService::getXrayError() const
{
return m_xrayController->getError();
}
void ProxyService::clearCache()
{
m_cachedConfig = QJsonObject();
ProxyLogger::getInstance().debug("ProxyService cache cleared");
}
bool ProxyService::restartXray()
{
ProxyLogger::getInstance().info("Restarting Xray with updated config");
clearCache();
if (m_xrayController->isXrayRunning()) {
if (!stopXray()) {
ProxyLogger::getInstance().error("Failed to stop Xray during restart, aborting");
return false;
}
}
return startXray();
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "configmanager.h"
#include "iproxyservice.h"
#include "xraycontroller.h"
#include <QObject>
#include <QScopedPointer>
#include <QJsonObject>
class SecureServersRepository;
class SecureAppSettingsRepository;
class ProxyService : public QObject, public IProxyService {
Q_OBJECT
public:
ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~ProxyService() = default;
QJsonObject getConfig() override;
bool startXray() override;
bool stopXray() override;
bool isXrayRunning() const override;
qint64 getXrayProcessId() const override;
QString getXrayError() const override;
void clearCache();
bool restartXray();
signals:
void xrayStatusChanged(bool running);
private:
QScopedPointer<ConfigManager> m_configManager;
QScopedPointer<XrayController> m_xrayController;
QJsonObject m_cachedConfig;
};
+105
View File
@@ -0,0 +1,105 @@
#include "xraycontroller.h"
#include "proxylogger.h"
#include "core/utils/ipcClient.h"
namespace {
const QString kIpcUnavailableError = QStringLiteral("Failed to communicate with IPC service");
}
XrayController::XrayController(QObject *parent)
: QObject(parent)
{
ProxyLogger::getInstance().debug("XrayController initialized");
}
XrayController::~XrayController()
{
stop();
}
bool XrayController::start(const QString &configJson)
{
if (m_isRunning) {
ProxyLogger::getInstance().info("Xray is already running");
return true;
}
ProxyLogger::getInstance().info("Request to start Xray via IPC");
m_lastError.clear();
if (configJson.trimmed().isEmpty()) {
m_lastError = QStringLiteral("Config content is empty");
ProxyLogger::getInstance().error(m_lastError);
return false;
}
const bool ipcResult = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(configJson);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
ProxyLogger::getInstance().warning("Failed to start Xray via IPC");
return false;
}
return true;
}, []() {
return false;
});
if (!ipcResult) {
m_lastError = kIpcUnavailableError;
ProxyLogger::getInstance().error(m_lastError);
return false;
}
ProxyLogger::getInstance().info("Xray start command sent to IPC service");
m_isRunning = true;
return true;
}
bool XrayController::stop()
{
if (!m_isRunning) {
ProxyLogger::getInstance().debug("Skipping Xray stop via IPC: local proxy Xray is not running");
return true;
}
ProxyLogger::getInstance().info("Stopping Xray via IPC");
const bool ipcResult = IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStop = iface->xrayStop();
if (!xrayStop.waitForFinished() || !xrayStop.returnValue()) {
ProxyLogger::getInstance().warning("Failed to stop Xray via IPC");
return false;
}
return true;
}, []() {
return false;
});
if (!ipcResult) {
m_lastError = kIpcUnavailableError;
ProxyLogger::getInstance().warning(m_lastError);
return false;
}
m_isRunning = false;
return true;
}
bool XrayController::isXrayRunning() const
{
return m_isRunning;
}
qint64 XrayController::getProcessId() const
{
return -1;
}
QString XrayController::getError() const
{
return m_lastError;
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include <QString>
class XrayController : public QObject {
Q_OBJECT
public:
explicit XrayController(QObject* parent = nullptr);
~XrayController();
bool start(const QString& configJson);
bool stop();
bool isXrayRunning() const;
qint64 getProcessId() const;
QString getError() const;
private:
bool m_isRunning {false};
QString m_lastError;
};
+15 -4
View File
@@ -13,6 +13,7 @@
#include "core/utils/api/apiUtils.h"
#include "core/models/api/apiConfig.h"
#include "core/models/api/authData.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -67,6 +68,20 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
return containers.value(container);
}
QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject ApiV2ServerConfig::toJson() const
{
QJsonObject obj;
@@ -80,9 +95,6 @@ QJsonObject ApiV2ServerConfig::toJson() const
if (!description.isEmpty()) {
obj[configKey::description] = description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
obj[configKey::configVersion] = configVersion;
@@ -134,7 +146,6 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
config.name = json.value(configKey::name).toString();
config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false);
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.configVersion = json.value(configKey::configVersion).toInt(2);
config.hostName = json.value(configKey::hostName).toString();
@@ -3,6 +3,7 @@
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -43,6 +44,9 @@ struct ApiV2ServerConfig {
bool isExternalPremium() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static ApiV2ServerConfig fromJson(const QJsonObject& json);
};
@@ -23,9 +23,7 @@ LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
{
LegacyApiServerConfig config;
config.name = json.value(configKey::name).toString();
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
config.crc = json.value(configKey::crc).toInt(0);
@@ -32,7 +32,7 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
c.bytesMin = json.value(configKey::xPaddingBytesMin).toString();
c.bytesMax = json.value(configKey::xPaddingBytesMax).toString();
c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true);
c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite);
c.key = json.value(configKey::xPaddingKey).toString();
c.header = json.value(configKey::xPaddingHeader).toString();
c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement);
c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod);
@@ -108,35 +108,114 @@ QJsonObject XrayXhttpConfig::toJson() const
return obj;
}
namespace
{
XrayXhttpConfig clearedXhttpConfig()
{
XrayXhttpConfig c;
c.mode = QString();
c.host = QString();
c.path = QString();
c.headersTemplate = QString();
c.uplinkMethod = QString();
c.disableGrpc = false;
c.disableSse = false;
c.sessionPlacement = QString();
c.sessionKey = QString();
c.seqPlacement = QString();
c.seqKey = QString();
c.uplinkDataPlacement = QString();
c.uplinkDataKey = QString();
c.uplinkChunkSize = QString();
c.scMaxBufferedPosts = QString();
c.scMaxEachPostBytesMin = QString();
c.scMaxEachPostBytesMax = QString();
c.scMinPostsIntervalMsMin = QString();
c.scMinPostsIntervalMsMax = QString();
c.scStreamUpServerSecsMin = QString();
c.scStreamUpServerSecsMax = QString();
return c;
}
} // namespace
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
{
XrayXhttpConfig c;
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
c.path = json.value(configKey::xhttpPath).toString();
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
if (json.isEmpty()) {
return clearedXhttpConfig();
}
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement);
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
XrayXhttpConfig c = clearedXhttpConfig();
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
if (json.contains(configKey::xhttpMode)) {
c.mode = json.value(configKey::xhttpMode).toString();
}
if (json.contains(configKey::xhttpHost)) {
c.host = json.value(configKey::xhttpHost).toString();
}
if (json.contains(configKey::xhttpPath)) {
c.path = json.value(configKey::xhttpPath).toString();
}
if (json.contains(configKey::xhttpHeadersTemplate)) {
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
}
if (json.contains(configKey::xhttpUplinkMethod)) {
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
}
if (json.contains(configKey::xhttpDisableGrpc)) {
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool();
}
if (json.contains(configKey::xhttpDisableSse)) {
c.disableSse = json.value(configKey::xhttpDisableSse).toBool();
}
if (json.contains(configKey::xhttpSessionPlacement)) {
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString();
}
if (json.contains(configKey::xhttpSessionKey)) {
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
}
if (json.contains(configKey::xhttpSeqPlacement)) {
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString();
}
if (json.contains(configKey::xhttpSeqKey)) {
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
}
if (json.contains(configKey::xhttpUplinkDataPlacement)) {
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString();
}
if (json.contains(configKey::xhttpUplinkDataKey)) {
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
}
if (json.contains(configKey::xhttpUplinkChunkSize)) {
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString();
}
if (json.contains(configKey::xhttpScMaxBufferedPosts)) {
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
}
if (json.contains(configKey::xhttpScMaxEachPostBytesMin)) {
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString();
}
if (json.contains(configKey::xhttpScMaxEachPostBytesMax)) {
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString();
}
if (json.contains(configKey::xhttpScMinPostsIntervalMsMin)) {
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString();
}
if (json.contains(configKey::xhttpScMinPostsIntervalMsMax)) {
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString();
}
if (json.contains(configKey::xhttpScStreamUpServerSecsMin)) {
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString();
}
if (json.contains(configKey::xhttpScStreamUpServerSecsMax)) {
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString();
}
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
if (json.contains(QLatin1String("xPadding"))) {
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject());
}
if (json.contains(QLatin1String("xmux"))) {
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("xmux")).toObject());
}
return c;
}
@@ -156,12 +235,27 @@ QJsonObject XrayMkcpConfig::toJson() const
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
{
XrayMkcpConfig c;
c.tti = json.value(configKey::mkcpTti).toString();
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
if (json.isEmpty()) {
return c;
}
if (json.contains(configKey::mkcpTti)) {
c.tti = json.value(configKey::mkcpTti).toString();
}
if (json.contains(configKey::mkcpUplinkCapacity)) {
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
}
if (json.contains(configKey::mkcpDownlinkCapacity)) {
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
}
if (json.contains(configKey::mkcpReadBufferSize)) {
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
}
if (json.contains(configKey::mkcpWriteBufferSize)) {
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
}
if (json.contains(configKey::mkcpCongestion)) {
c.congestion = json.value(configKey::mkcpCongestion).toBool();
}
return c;
}
@@ -208,8 +302,14 @@ QJsonObject XrayServerConfig::toJson() const
if (!transport.isEmpty()) {
obj[configKey::xrayTransport] = transport;
}
obj["xhttp"] = xhttp.toJson();
obj["mkcp"] = mkcp.toJson();
const QJsonObject xhttpObj = xhttp.toJson();
if (!xhttpObj.isEmpty()) {
obj[QStringLiteral("xhttp")] = xhttpObj;
}
const QJsonObject mkcpObj = mkcp.toJson();
if (!mkcpObj.isEmpty()) {
obj[QStringLiteral("mkcp")] = mkcpObj;
}
return obj;
}
@@ -225,20 +325,39 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
c.site = json.value(configKey::site).toString();
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
// New: Security
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
if (json.contains(configKey::xraySecurity)) {
c.security = json.value(configKey::xraySecurity).toString();
}
if (json.contains(configKey::xrayFlow)) {
c.flow = json.value(configKey::xrayFlow).toString();
}
if (json.contains(configKey::xrayFingerprint)) {
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
}
}
if (json.contains(configKey::xraySni)) {
c.sni = json.value(configKey::xraySni).toString();
}
if (json.contains(configKey::xrayAlpn)) {
c.alpn = json.value(configKey::xrayAlpn).toString();
}
if (json.contains(configKey::xrayTransport)) {
c.transport = json.value(configKey::xrayTransport).toString();
}
if (json.contains(QLatin1String("xhttp"))) {
const QJsonObject xhttpJson = json.value(QLatin1String("xhttp")).toObject();
if (!xhttpJson.isEmpty()) {
c.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
}
}
if (json.contains(QLatin1String("mkcp"))) {
const QJsonObject mkcpJson = json.value(QLatin1String("mkcp")).toObject();
if (!mkcpJson.isEmpty()) {
c.mkcp = XrayMkcpConfig::fromJson(mkcpJson);
}
}
c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
// New: Transport
c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
return c;
}
@@ -246,12 +365,17 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
{
return port == other.port
&& transportProto == other.transportProto
&& subnetAddress == other.subnetAddress
&& site == other.site
&& security == other.security
&& flow == other.flow
&& transport == other.transport
&& fingerprint == other.fingerprint
&& sni == other.sni;
&& sni == other.sni
&& alpn == other.alpn
&& xhttp.toJson() == other.xhttp.toJson()
&& mkcp.toJson() == other.mkcp.toJson();
}
QJsonObject XrayClientConfig::toJson() const
@@ -344,6 +468,17 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
}
}
}
const QJsonArray outbounds = parsed.value(protocols::xray::outbounds).toArray();
if (!outbounds.isEmpty()) {
const QJsonObject settings = outbounds[0].toObject().value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonArray users = vnext[0].toObject().value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
clientCfg.id = users[0].toObject().value(protocols::xray::id).toString();
}
}
}
c.clientConfig = clientCfg;
} else {
c.clientConfig = XrayClientConfig::fromJson(parsed);
@@ -351,9 +486,154 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
}
}
c.needsClientHydration =
c.hasClientConfig()
&& (!json.contains(configKey::xrayTransport) || c.serverConfig.isThirdPartyConfig);
if (c.needsClientHydration) {
c.hydrateServerConfigFromClientNative();
}
return c;
}
bool XrayProtocolConfig::hydrateServerConfigFromClientNative()
{
if (!clientConfig.has_value() || clientConfig->nativeConfig.isEmpty()) {
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
return false;
}
const QJsonObject root = doc.object();
const QJsonArray outbounds = root.value(protocols::xray::outbounds).toArray();
if (outbounds.isEmpty()) {
return false;
}
const QJsonObject outbound = outbounds[0].toObject();
const QJsonObject streamSettings = outbound.value(protocols::xray::streamSettings).toObject();
if (streamSettings.isEmpty()) {
return false;
}
XrayServerConfig &srv = serverConfig;
const QJsonObject settings = outbound.value(protocols::xray::settings).toObject();
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
if (!vnext.isEmpty()) {
const QJsonObject vnextEntry = vnext[0].toObject();
if (vnextEntry.contains(protocols::xray::port)) {
srv.port = QString::number(vnextEntry.value(protocols::xray::port).toInt());
}
const QJsonArray users = vnextEntry.value(protocols::xray::users).toArray();
if (!users.isEmpty()) {
srv.flow = users[0].toObject().value(protocols::xray::flow).toString();
}
}
const QString networkVal = streamSettings.value(protocols::xray::network).toString(QStringLiteral("tcp"));
if (networkVal == QLatin1String("xhttp")) {
srv.transport = QStringLiteral("xhttp");
} else if (networkVal == QLatin1String("kcp")) {
srv.transport = QStringLiteral("mkcp");
} else {
srv.transport = QStringLiteral("raw");
}
if (streamSettings.contains(protocols::xray::security)) {
srv.security = streamSettings.value(protocols::xray::security).toString();
}
if (srv.security == QLatin1String("reality")) {
const QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
srv.sni = rs.value(protocols::xray::serverName).toString();
srv.site = srv.sni.isEmpty() ? srv.site : srv.sni;
const QString fp = rs.value(protocols::xray::fingerprint).toString();
if (!fp.isEmpty()) {
srv.fingerprint = fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)
? QString::fromLatin1(protocols::xray::defaultFingerprint)
: fp;
}
}
if (srv.security == QLatin1String("tls")) {
const QJsonObject tls = streamSettings.value(QStringLiteral("tlsSettings")).toObject();
srv.sni = tls.value(protocols::xray::serverName).toString();
const QString fp = tls.value(protocols::xray::fingerprint).toString();
if (!fp.isEmpty()) {
srv.fingerprint = fp;
}
QStringList alpnList;
for (const QJsonValue &v : tls.value(QStringLiteral("alpn")).toArray()) {
alpnList << v.toString();
}
if (!alpnList.isEmpty()) {
srv.alpn = alpnList.join(QLatin1Char(','));
}
}
if (srv.transport == QLatin1String("xhttp")) {
const QJsonObject xhttpObj = streamSettings.value(QStringLiteral("xhttpSettings")).toObject();
QJsonObject xhttpJson;
const QString mode = xhttpObj.value(QStringLiteral("mode")).toString();
if (!mode.isEmpty()) {
if (mode == QLatin1String("auto")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Auto");
} else if (mode == QLatin1String("packet-up")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Packet-up");
} else if (mode == QLatin1String("stream-up")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-up");
} else if (mode == QLatin1String("stream-one")) {
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-one");
} else {
xhttpJson[configKey::xhttpMode] = mode;
}
}
if (xhttpObj.contains(QStringLiteral("host"))) {
xhttpJson[configKey::xhttpHost] = xhttpObj.value(QStringLiteral("host")).toString();
}
if (xhttpObj.contains(QStringLiteral("path"))) {
xhttpJson[configKey::xhttpPath] = xhttpObj.value(QStringLiteral("path")).toString();
}
if (xhttpObj.contains(QStringLiteral("uplinkHTTPMethod"))) {
xhttpJson[configKey::xhttpUplinkMethod] = xhttpObj.value(QStringLiteral("uplinkHTTPMethod")).toString();
}
xhttpJson[configKey::xhttpDisableGrpc] = xhttpObj.value(QStringLiteral("noGRPCHeader")).toBool(true);
xhttpJson[configKey::xhttpDisableSse] = xhttpObj.value(QStringLiteral("noSSEHeader")).toBool(true);
srv.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
}
if (srv.transport == QLatin1String("mkcp")) {
const QJsonObject kcpObj = streamSettings.value(QStringLiteral("kcpSettings")).toObject();
XrayMkcpConfig mk;
if (kcpObj.contains(QStringLiteral("tti"))) {
mk.tti = QString::number(kcpObj.value(QStringLiteral("tti")).toInt());
}
if (kcpObj.contains(QStringLiteral("uplinkCapacity"))) {
mk.uplinkCapacity = QString::number(kcpObj.value(QStringLiteral("uplinkCapacity")).toInt());
}
if (kcpObj.contains(QStringLiteral("downlinkCapacity"))) {
mk.downlinkCapacity = QString::number(kcpObj.value(QStringLiteral("downlinkCapacity")).toInt());
}
if (kcpObj.contains(QStringLiteral("readBufferSize"))) {
mk.readBufferSize = QString::number(kcpObj.value(QStringLiteral("readBufferSize")).toInt());
}
if (kcpObj.contains(QStringLiteral("writeBufferSize"))) {
mk.writeBufferSize = QString::number(kcpObj.value(QStringLiteral("writeBufferSize")).toInt());
}
if (kcpObj.contains(QStringLiteral("congestion"))) {
mk.congestion = kcpObj.value(QStringLiteral("congestion")).toBool(true);
}
srv.mkcp = mk;
}
needsClientHydration = false;
return true;
}
bool XrayProtocolConfig::hasClientConfig() const
{
return clientConfig.has_value();
@@ -75,6 +75,7 @@ struct XrayXhttpConfig {
XrayXmuxConfig xmux;
QJsonObject toJson() const;
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
static XrayXhttpConfig fromJson(const QJsonObject &json);
};
@@ -99,15 +100,13 @@ struct XrayServerConfig {
QString site;
bool isThirdPartyConfig = false;
// New: Security
QString security = protocols::xray::defaultSecurity;
QString flow = protocols::xray::defaultFlow;
QString fingerprint = protocols::xray::defaultFingerprint;
QString sni = protocols::xray::defaultSni;
QString alpn = protocols::xray::defaultAlpn;
QString security;
QString flow;
QString fingerprint;
QString sni;
QString alpn;
// New: Transport
QString transport = protocols::xray::defaultTransport;
QString transport;
XrayXhttpConfig xhttp;
XrayMkcpConfig mkcp;
@@ -139,6 +138,10 @@ struct XrayProtocolConfig {
bool hasClientConfig() const;
void setClientConfig(const XrayClientConfig &config);
void clearClientConfig();
bool needsClientHydration = false;
bool hydrateServerConfigFromClientNative();
};
} // namespace amnezia
@@ -9,6 +9,7 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -28,6 +29,25 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container);
}
void NativeServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject NativeServerConfig::toJson() const
{
QJsonObject obj;
@@ -35,9 +55,6 @@ QJsonObject NativeServerConfig::toJson() const
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
@@ -70,7 +87,6 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
NativeServerConfig config;
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
@@ -3,6 +3,7 @@
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -25,6 +26,11 @@ struct NativeServerConfig {
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static NativeServerConfig fromJson(const QJsonObject& json);
};
@@ -87,9 +87,6 @@ QJsonObject SelfHostedAdminServerConfig::toJson() const
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
@@ -132,7 +129,6 @@ SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObj
SelfHostedAdminServerConfig config;
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
@@ -8,6 +8,7 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -42,6 +43,26 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
return containers.value(container);
}
void SelfHostedUserServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &primaryDns,
const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject SelfHostedUserServerConfig::toJson() const
{
QJsonObject obj;
@@ -49,9 +70,6 @@ QJsonObject SelfHostedUserServerConfig::toJson() const
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
@@ -84,7 +102,6 @@ SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObjec
SelfHostedUserServerConfig config;
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
@@ -3,6 +3,7 @@
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include <optional>
#include "core/utils/containerEnum.h"
@@ -30,6 +31,11 @@ struct SelfHostedUserServerConfig {
std::optional<ServerCredentials> credentials() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
void updateContainerConfig(DockerContainer container, const ContainerConfig &config);
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static SelfHostedUserServerConfig fromJson(const QJsonObject &json);
};
+24 -13
View File
@@ -39,33 +39,44 @@ QString OpenVpnProtocol::defaultConfigPath()
return p;
}
void OpenVpnProtocol::stop()
void OpenVpnProtocol::cleanupResources()
{
qDebug() << "OpenVpnProtocol::stop()";
setConnectionState(Vpn::ConnectionState::Disconnecting);
// TODO: need refactoring
// sendTermSignal() will even return true while server connected ???
if ((m_connectionState == Vpn::ConnectionState::Preparing) || (m_connectionState == Vpn::ConnectionState::Connecting)
|| (m_connectionState == Vpn::ConnectionState::Connected)
|| (m_connectionState == Vpn::ConnectionState::Reconnecting)) {
if (m_openVpnProcess || openVpnProcessIsRunning()) {
if (!sendTermSignal()) {
killOpenVpnProcess();
}
QThread::msleep(10);
m_managementServer.stop();
}
m_managementServer.stop();
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
if (!reply.waitForFinished(1000) && !reply.returnValue()) {
qWarning() << "OpenVpnProtocol::stop(): Failed to disable killswitch";
qWarning() << "OpenVpnProtocol::cleanupResources(): Failed to disable killswitch";
}
});
#endif
}
setConnectionState(Vpn::ConnectionState::Disconnected);
void OpenVpnProtocol::stop()
{
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|| m_connectionState == Vpn::ConnectionState::Connecting
|| m_connectionState == Vpn::ConnectionState::Connected
|| m_connectionState == Vpn::ConnectionState::Reconnecting;
if (wasActive) {
setConnectionState(Vpn::ConnectionState::Disconnecting);
}
cleanupResources();
if (wasActive || m_connectionState == Vpn::ConnectionState::Disconnecting) {
setConnectionState(Vpn::ConnectionState::Disconnected);
}
}
ErrorCode OpenVpnProtocol::prepare()
@@ -168,7 +179,7 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
OpenVpnProtocol::stop();
cleanupResources();
if (!QFileInfo::exists(configPath())) {
setLastError(ErrorCode::OpenVpnConfigMissing);
+1
View File
@@ -29,6 +29,7 @@ protected slots:
void onReadyReadDataFromManagementServer();
private:
void cleanupResources();
QString configPath() const;
bool openVpnProcessIsRunning() const;
bool sendTermSignal();
+13
View File
@@ -68,6 +68,8 @@ ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
m_teardownDone = false;
// Inject SOCKS5 auth into the inbound before starting xray.
// Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds;
@@ -120,6 +122,17 @@ void XrayProtocol::stop()
{
qDebug() << "XrayProtocol::stop()";
// The destructor calls stop() as a safety net, but stop() is also called
// explicitly on disconnect. Because stop() spins the event loop via
// waitForFinished(), the local proxy may start its own xray on the shared
// global engine in between. Running the teardown (xrayStop) twice would then
// kill the proxy's just-started xray. Run the engine teardown only once.
if (m_teardownDone) {
qDebug() << "XrayProtocol::stop() teardown already done, skipping";
return;
}
m_teardownDone = true;
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
+5
View File
@@ -38,6 +38,11 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
// Guards against tearing down the shared xray engine more than once.
// The destructor also calls stop(); without this the second stop() would
// call xrayStop() on the global engine that the local proxy may already own.
bool m_teardownDone = false;
};
#endif // XRAYPROTOCOL_H
@@ -5,6 +5,8 @@
#include <QJsonObject>
#include <QUuid>
#include <limits>
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
@@ -426,26 +428,6 @@ void SecureAppSettingsRepository::clearSettings()
emit settingsCleared();
}
QString SecureAppSettingsRepository::nextAvailableServerName() const
{
int i = 0;
bool nameExist = false;
do {
i++;
nameExist = false;
QJsonArray servers = QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
for (const QJsonValue &server : servers) {
if (server.toObject().value(configKey::description).toString() == QString("Server") + " " + QString::number(i)) {
nameExist = true;
break;
}
}
} while (nameExist);
return QString("Server") + " " + QString::number(i);
}
void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
{
m_settings->setValue("Conf/installationUuid", uuid);
@@ -460,3 +442,59 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
{
setValue("Xray/savedConfigs", data);
}
QString SecureAppSettingsRepository::localProxyOwnerId() const
{
return value("Conf/localProxyOwnerId", "").toString();
}
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
{
setValue("Conf/localProxyOwnerId", serverId);
emit localProxySettingsChanged();
}
quint16 SecureAppSettingsRepository::localProxyPort() const
{
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
}
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
{
setValue("Conf/localProxyPort", port);
emit localProxySettingsChanged();
}
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
{
return value("Conf/localProxyPortUserDefined", false).toBool();
}
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
{
setValue("Conf/localProxyPortUserDefined", userDefined);
}
bool SecureAppSettingsRepository::isLocalProxyHttpEnabled() const
{
return value("Conf/localProxyHttpEnabled", false).toBool();
}
void SecureAppSettingsRepository::setLocalProxyHttpEnabled(bool enabled)
{
setValue("Conf/localProxyHttpEnabled", enabled);
emit localProxySettingsChanged();
}
int SecureAppSettingsRepository::localProxyRestartToken() const
{
return value("Conf/localProxyRestartToken", 0).toInt();
}
void SecureAppSettingsRepository::bumpLocalProxyRestartToken()
{
const int current = localProxyRestartToken();
const int next = (current == std::numeric_limits<int>::max()) ? 0 : (current + 1);
setValue("Conf/localProxyRestartToken", next);
emit localProxySettingsChanged();
}
@@ -90,11 +90,21 @@ public:
bool restoreAppConfig(const QByteArray &cfg);
void clearSettings();
QString nextAvailableServerName() const;
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);
// Local proxy
QString localProxyOwnerId() const;
void setLocalProxyOwnerId(const QString &serverId);
quint16 localProxyPort() const;
void setLocalProxyPort(quint16 port);
bool isLocalProxyPortUserDefined() const;
void setLocalProxyPortUserDefined(bool userDefined);
bool isLocalProxyHttpEnabled() const;
void setLocalProxyHttpEnabled(bool enabled);
int localProxyRestartToken() const;
void bumpLocalProxyRestartToken();
signals:
void appLanguageChanged(QLocale locale);
void allowedDnsServersChanged(const QStringList &servers);
@@ -108,6 +118,8 @@ signals:
void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);
void settingsCleared();
void localProxySettingsChanged();
void localProxyStartFailed(const QString &message);
private:
void setVpnSites(RouteMode mode, const QVariantMap &sites);
@@ -3,6 +3,7 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonValue>
#include <QSet>
#include <QUuid>
#include "core/utils/serverConfigUtils.h"
@@ -32,6 +33,45 @@ QJsonObject embedStorageServerId(const QString &serverId, const QJsonObject &pay
return o;
}
QString storedServerDisplayName(const SecureServersRepository *repository, const QString &serverId)
{
using Kind = serverConfigUtils::ConfigType;
switch (repository->serverKind(serverId)) {
case Kind::SelfHostedAdmin:
if (const auto cfg = repository->selfHostedAdminConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::SelfHostedUser:
if (const auto cfg = repository->selfHostedUserConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::Native:
if (const auto cfg = repository->nativeConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium:
if (const auto cfg = repository->apiV2Config(serverId)) {
return cfg->displayName;
}
break;
case Kind::AmneziaPremiumV1:
case Kind::AmneziaFreeV2:
if (const auto cfg = repository->legacyApiConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::Invalid:
default:
break;
}
return {};
}
} // namespace
SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent)
@@ -153,6 +193,28 @@ void SecureServersRepository::clearServers()
syncToStorage();
}
QString SecureServersRepository::nextAvailableServerName() const
{
QSet<QString> usedNames;
usedNames.reserve(m_orderedServerIds.size());
for (const QString &serverId : m_orderedServerIds) {
const QString displayName = storedServerDisplayName(this, serverId);
if (!displayName.isEmpty()) {
usedNames.insert(displayName);
}
}
int i = 0;
QString candidate;
do {
++i;
candidate = tr("Server") + QLatin1Char(' ') + QString::number(i);
} while (usedNames.contains(candidate));
return candidate;
}
QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind)
{
const QString id = normalizedOrGeneratedServerId(serverId);
@@ -247,6 +309,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
}
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
if (it == m_serverJsonById.constEnd()) {
return std::nullopt;
}
return it.value();
}
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
@@ -31,6 +31,8 @@ public:
void removeServer(const QString &serverId);
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
@@ -48,6 +50,8 @@ public:
void clearServers();
QString nextAvailableServerName() const;
void invalidateCache();
signals:
+25 -59
View File
@@ -2,7 +2,6 @@
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/configKeys.h"
#include <QLatin1Char>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
@@ -77,26 +76,6 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
return endDate <= nowUtc.addDays(withinDays);
}
amnezia::ErrorCode apiUtils::errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj)
{
if (!jsonObj.contains(QStringLiteral("http_status"))) {
return amnezia::ErrorCode::NoError;
}
const int st = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
switch (st) {
case 200: return amnezia::ErrorCode::NoError;
case 400: return amnezia::ErrorCode::ApiConfigEmptyError;
case 403: return amnezia::ErrorCode::ApiPairingForbiddenError;
case 404: return amnezia::ErrorCode::ApiNotFoundError;
case 408: return amnezia::ErrorCode::ApiConfigTimeoutError;
case 409: return amnezia::ErrorCode::ApiPairingConflictError;
case 429: return amnezia::ErrorCode::ApiPairingRateLimitedError;
case 500: return amnezia::ErrorCode::ApiConfigDownloadError;
case 503: return amnezia::ErrorCode::ApiPairingServiceUnavailableError;
default: return amnezia::ErrorCode::ApiConfigDownloadError;
}
}
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody)
@@ -105,15 +84,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodePaymentRequired = 402;
const int httpStatusCodeTooManyRequests = 429;
const int httpStatusCodeRequestTimeout = 408;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError;
}
if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError;
@@ -124,14 +102,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiUpdateRequestError;
}
qDebug() << QString::fromUtf8(responseBody);
qDebug() << replyError;
qDebug() << httpStatusCode;
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
return amnezia::ErrorCode::ApiRateLimitError;
}
if (httpStatusFromBody == httpStatusCodeConflict) {
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
@@ -141,6 +119,9 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
}
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
return amnezia::ErrorCode::ApiConfigTimeoutError;
}
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
}
@@ -151,28 +132,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
const QString message = apiErrorMessageFromJson(jsonObj);
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRefreshError;
}
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaInvalidError;
}
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiCaptchaRequiredError;
}
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
}
const QString msg = apiErrorMessageFromJson(jsonObj);
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
if (httpStatusFromBody >= 300) {
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiNotFoundError;
}
if (httpStatusCode == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
if (httpStatusCode == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
}
qDebug() << "something went wrong";
@@ -272,18 +253,3 @@ QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
return vpnKeyText;
}
QString apiUtils::countryCodeBaseForFlag(const QString &fullCountryCode)
{
const QString trimmed = fullCountryCode.trimmed();
if (trimmed.isEmpty()) {
return QString();
}
const int dashIdx = trimmed.indexOf(QLatin1Char('-'));
const QString base = dashIdx < 0 ? trimmed : trimmed.left(dashIdx);
const QString normalized = base.trimmed();
if (normalized.isEmpty()) {
return QString();
}
return normalized.toUpper();
}
-5
View File
@@ -23,13 +23,8 @@ namespace apiUtils
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody);
amnezia::ErrorCode errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj);
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
// ISO2-style segment for flagKit assets (e.g. US-WEST -> US). Do not use in API request bodies.
QString countryCodeBaseForFlag(const QString &fullCountryCode);
}
#endif // APIUTILS_H
-1
View File
@@ -22,7 +22,6 @@ namespace apiDefs
constexpr QLatin1String availableCountries("available_countries");
constexpr QLatin1String installationUuid("installation_uuid");
constexpr QLatin1String uuid("installation_uuid");
constexpr QLatin1String qrUuid("qr_uuid");
constexpr QLatin1String osVersion("os_version");
constexpr QLatin1String userCountryCode("user_country_code");
constexpr QLatin1String serverCountryCode("server_country_code");
+2
View File
@@ -15,6 +15,8 @@ namespace amnezia
Awg2,
WireGuard,
OpenVpn,
Cloak,
ShadowSocks,
Ipsec,
Xray,
SSXray,
@@ -21,6 +21,8 @@ QString ContainerUtils::containerToString(DockerContainer c)
{
if (c == DockerContainer::None)
return "none";
if (c == DockerContainer::Cloak)
return "amnezia-openvpn-cloak";
if (c == DockerContainer::Awg)
return "amnezia-awg";
if (c == DockerContainer::Awg2)
@@ -62,6 +64,8 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
{
return { { DockerContainer::None, "Not installed" },
{ DockerContainer::OpenVpn, "OpenVPN" },
{ DockerContainer::ShadowSocks, "OpenVPN over SS" },
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
{ DockerContainer::WireGuard, "WireGuard" },
{ DockerContainer::Awg, "AmneziaWG" },
{ DockerContainer::Awg2, "AmneziaWG" },
@@ -83,6 +87,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
return { { DockerContainer::OpenVpn,
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("This protocol is no longer supported.") },
{ DockerContainer::Cloak,
QObject::tr("This protocol is no longer supported.") },
{ DockerContainer::WireGuard,
QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power "
"consumption.") },
@@ -194,6 +202,9 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
ServiceType ContainerUtils::containerService(DockerContainer c)
{
if (isUnsupportedContainer(c)) {
return ServiceType::Vpn;
}
return ProtocolUtils::protocolService(defaultProtocol(c));
}
@@ -202,6 +213,8 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
switch (c) {
case DockerContainer::None: return Proto::Unknown;
case DockerContainer::OpenVpn: return Proto::OpenVpn;
case DockerContainer::Cloak:
case DockerContainer::ShadowSocks: return Proto::Unknown;
case DockerContainer::WireGuard: return Proto::WireGuard;
case DockerContainer::Awg2: return Proto::Awg;
case DockerContainer::Awg: return Proto::Awg;
@@ -252,6 +265,8 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
// macOS build using Network Extension – allow OpenVPN for parity with iOS.
switch (c) {
case DockerContainer::OpenVpn: return true;
case DockerContainer::Cloak: return false;
case DockerContainer::ShadowSocks: return false;
case DockerContainer::WireGuard: return true;
case DockerContainer::Awg2: return true;
case DockerContainer::Awg: return true;
@@ -336,6 +351,10 @@ int ContainerUtils::easySetupOrder(DockerContainer container)
bool ContainerUtils::isShareable(DockerContainer container)
{
if (isUnsupportedContainer(container)) {
return false;
}
switch (container) {
case DockerContainer::TorWebSite: return false;
case DockerContainer::Dns: return false;
@@ -352,6 +371,11 @@ bool ContainerUtils::isAwgContainer(DockerContainer container)
return container == DockerContainer::Awg || container == DockerContainer::Awg2;
}
bool ContainerUtils::isUnsupportedContainer(DockerContainer container)
{
return container == DockerContainer::Cloak || container == DockerContainer::ShadowSocks;
}
QJsonObject ContainerUtils::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig)
{
QString protocolConfigString = containerConfig.value(ProtocolUtils::protoToString(protocol))
@@ -45,6 +45,8 @@ namespace amnezia
bool isAwgContainer(DockerContainer container);
bool isUnsupportedContainer(DockerContainer container);
QJsonObject getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig);
int installPageOrder(DockerContainer container);
+10 -11
View File
@@ -35,6 +35,11 @@ namespace amnezia
ServerCgroupMountpoint = 212,
DockerPullRateLimit = 213,
ServerLinuxKernelTooOld = 214,
XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217,
ServerContainerRuntimeNotSupported = 218,
ContainerRuntimeServiceNotRunning = 219,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -76,6 +81,7 @@ namespace amnezia
ImportBackupFileUseRestoreInstead = 903,
RestoreBackupInvalidError = 904,
LegacyApiV1NotSupportedError = 905,
LegacyContainerNotSupportedError = 906,
// Android errors
AndroidError = 1000,
@@ -98,15 +104,10 @@ namespace amnezia
ApiSubscriptionNotActiveError = 1114,
ApiNoPurchasedSubscriptionsError = 1115,
ApiTrialAlreadyUsedError = 1116,
// QR pairing (gateway /v1/generate_qr, /v1/scan_qr)
ApiPairingForbiddenError = 1117,
ApiPairingConflictError = 1118,
ApiPairingRateLimitedError = 1119,
ApiPairingServiceUnavailableError = 1120,
ApiPairingPayloadTooLargeError = 1121,
ApiPairingMissingMetadataError = 1122,
ApiPairingSessionExpiredError = 1123,
ApiCaptchaRequiredError = 1117,
ApiCaptchaInvalidError = 1118,
ApiCaptchaRefreshError = 1119,
ApiRateLimitError = 1120,
// QFile errors
OpenError = 1200,
@@ -125,5 +126,3 @@ namespace amnezia
Q_DECLARE_METATYPE(amnezia::ErrorCode)
#endif // ERRORCODES_H
+16 -7
View File
@@ -30,6 +30,17 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
case(ErrorCode::XrayServerConfigInvalid):
errorMessage = QObject::tr("Server error: invalid or unreadable XRay server configuration");
break;
case(ErrorCode::XrayServerNoVlessClients):
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
break;
case(ErrorCode::XrayRealityKeysReadFailed):
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
break;
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -60,6 +71,7 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); 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;
@@ -84,13 +96,10 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
case (ErrorCode::ApiPairingForbiddenError): errorMessage = QObject::tr("QR pairing was rejected (forbidden)"); break;
case (ErrorCode::ApiPairingConflictError): errorMessage = QObject::tr("This QR code has already been used"); break;
case (ErrorCode::ApiPairingRateLimitedError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
case (ErrorCode::ApiPairingServiceUnavailableError): errorMessage = QObject::tr("Service temporarily unavailable. Please try again later"); break;
case (ErrorCode::ApiPairingPayloadTooLargeError): errorMessage = QObject::tr("QR pairing data is too large to send"); break;
case (ErrorCode::ApiPairingMissingMetadataError): errorMessage = QObject::tr("This subscription is missing data required to transfer via QR (service type or country). Refresh the subscription or pick another server."); break;
case (ErrorCode::ApiPairingSessionExpiredError): errorMessage = QObject::tr("The QR code session has ended. Show a new QR code on the other device and scan again."); break;
case (ErrorCode::ApiCaptchaRequiredError): errorMessage = QObject::tr("CAPTCHA verification is required"); break;
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
// QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
-8
View File
@@ -3,14 +3,6 @@
#include <QIODevice>
#include <QList>
QList<QString> qrCodeUtils::generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text)
{
const QString text = QString::fromUtf8(utf8Text);
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(text.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
const QString svg = QString::fromStdString(toSvgString(qr, 1));
return { svgToBase64(svg) };
}
QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
{
double k = 850;
-1
View File
@@ -10,7 +10,6 @@ namespace qrCodeUtils
constexpr const qint16 qrMagicCode = 1984;
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
QList<QString> generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text);
qrcodegen::QrCode generateQrCode(const QByteArray &data);
QString svgToBase64(const QString &image);
};
@@ -295,6 +295,8 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
vars.append({{"$MTPROXY_SECRET", c.secret}});
vars.append({{"$MTPROXY_REGENERATE_SECRET",
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0")}});
vars.append({{"$MTPROXY_TAG", c.tag}});
vars.append({{"$MTPROXY_TRANSPORT_MODE",
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
@@ -350,6 +352,8 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
vars.append({ { "$TELEMT_SECRET", c.secret } });
vars.append({ { "$TELEMT_REGENERATE_SECRET",
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0") } });
vars.append({ { "$TELEMT_TAG", c.tag } });
QString tlsDomain = c.tlsDomain;
if (tlsDomain.isEmpty()) {
@@ -26,6 +26,8 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
)
if(DEPLOY)
@@ -114,10 +116,20 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
find_package(openvpnadapter REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
find_package(awg-apple REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
find_package(hev-socks5-tunnel REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)

Some files were not shown because too many files have changed in this diff Show More