Compare commits

..

31 Commits

Author SHA1 Message Date
dranik 7b96cfec8c remove comment & reset file 2026-06-10 10:44:55 +03:00
dranik 657b0fd60c add close proc & fix check inet 2026-06-10 10:42:36 +03:00
dranik 1be22a3051 Fix: Add check inet and liveness monitor Xray 2026-06-09 16:06:56 +03: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
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
204 changed files with 4348 additions and 8300 deletions
+53 -26
View File
@@ -18,11 +18,14 @@ jobs:
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v3
id: filter id: filter
with: with:
base: ${{ github.event.before }}
filters: | filters: |
recipes: recipes:
- 'recipes/**' - 'recipes/**'
- 'conanfile.py' - 'conanfile.py'
- '.github/workflows/deploy.yml'
- 'cmake/conan_provider.cmake'
- 'cmake/platform_settings.cmake'
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux: Bake-Prebuilts-Linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -40,7 +43,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build dependencies' - name: 'Build dependencies'
shell: bash shell: bash
@@ -50,9 +53,11 @@ jobs:
done done
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}" run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts' - name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c run: conan upload -r amnezia "*" -c
# ------------------------------------------------------ # ------------------------------------------------------
@@ -98,7 +103,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Install system packages' - name: 'Install system packages'
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
@@ -118,7 +123,7 @@ jobs:
- name: 'Upload installer artifact' - name: 'Upload installer artifact'
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
path: deploy/build/AmneziaVPN-*-Linux.run path: deploy/build/AmneziaVPN_*_linux_x64.run
archive: false archive: false
retention-days: 7 retention-days: 7
@@ -149,15 +154,17 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1 - uses: ilammy/msvc-dev-cmd@v1
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build dependencies' - name: 'Build dependencies'
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1 run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}" run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts' - name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c run: conan upload -r amnezia "*" -c
# ------------------------------------------------------ # ------------------------------------------------------
@@ -229,7 +236,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build project' - name: 'Build project'
shell: cmd shell: cmd
@@ -242,27 +249,31 @@ jobs:
- name: 'Upload WIX installer artifact' - name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
path: deploy/build/AmneziaVPN-*-win64.msi path: deploy/build/AmneziaVPN_*_windows_x64.msi
archive: false archive: false
retention-days: 7 retention-days: 7
- name: 'Upload IFW installer artifact' - name: 'Upload IFW installer artifact'
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
path: deploy/build/AmneziaVPN-*-win64.exe path: deploy/build/AmneziaVPN_*_windows_x64.exe
archive: false archive: false
retention-days: 7 retention-days: 7
# ------------------------------------------------------ # ------------------------------------------------------
Bake-Prebuilts-iOS: Bake-Prebuilts-iOS:
runs-on: macos-latest
needs: Detect-Changes needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true' if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy: strategy:
matrix: 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -279,15 +290,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }} xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build dependencies' - name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}" run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts' - name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c run: conan upload -r amnezia "*" -c
# ------------------------------------------------------ # ------------------------------------------------------
@@ -344,7 +357,7 @@ jobs:
- name: 'Setup xcode' - name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1 uses: maxim-lobanov/setup-xcode@v1
with: with:
xcode-version: '26.1' xcode-version: '26.0'
- name: 'Install desktop Qt' - name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
@@ -376,7 +389,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install deps' - name: 'Install deps'
run: pip install "conan==2.26.2" jsonschema jinja2 run: pip install "conan==2.28.0" jsonschema jinja2
- name: 'Build project' - name: 'Build project'
env: env:
@@ -394,14 +407,17 @@ jobs:
# ------------------------------------------------------ # ------------------------------------------------------
Bake-Prebuilts-MacOS: Bake-Prebuilts-MacOS:
runs-on: macos-latest
needs: Detect-Changes needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true' if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy: strategy:
matrix: 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -418,15 +434,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }} xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build dependencies' - name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}" run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts' - name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c run: conan upload -r amnezia "*" -c
# ------------------------------------------------------ # ------------------------------------------------------
@@ -502,7 +520,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build project' - name: 'Build project'
env: env:
@@ -518,20 +536,24 @@ jobs:
- name: 'Upload installer artifact' - name: 'Upload installer artifact'
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
path: deploy/build/AmneziaVPN-*-Darwin.pkg path: deploy/build/AmneziaVPN_*_macos_x64.pkg
archive: false archive: false
retention-days: 7 retention-days: 7
# ------------------------------------------------------ # ------------------------------------------------------
Bake-Prebuilts-MacOS-NE: Bake-Prebuilts-MacOS-NE:
runs-on: macos-latest
needs: Detect-Changes needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true' if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy: strategy:
matrix: 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -548,15 +570,17 @@ jobs:
xcode-version: ${{ matrix.xcode-version }} xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build dependencies' - name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}" run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts' - name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c run: conan upload -r amnezia "*" -c
# ------------------------------------------------------ # ------------------------------------------------------
@@ -635,7 +659,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Build project' - name: 'Build project'
run: | run: |
@@ -671,7 +695,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Setup Android SDK' - name: 'Setup Android SDK'
uses: android-actions/setup-android@v4 uses: android-actions/setup-android@v4
@@ -696,9 +720,11 @@ jobs:
done done
- name: 'Authorize in remote' - name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}" run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts' - name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c run: conan upload -r amnezia "*" -c
# ------------------------------------------------------ # ------------------------------------------------------
@@ -712,7 +738,7 @@ jobs:
env: env:
ANDROID_PLATFORM: android-28 ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014 NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.1 QT_VERSION: 6.10.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -806,7 +832,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.26.2" run: pip install "conan==2.28.0"
- name: 'Decode keystore secret to file' - name: 'Decode keystore secret to file'
env: env:
@@ -894,3 +920,4 @@ jobs:
run: | run: |
echo "Pull request:" >> $GITHUB_STEP_SUMMARY 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 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 -3
View File
@@ -18,9 +18,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/" HOMEPAGE_URL "https://amnezia.org/"
) )
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY) if (PREBUILTS_ONLY)
# trigger conan to kick off `conan install`
find_package(OpenSSL REQUIRED)
return() return()
endif() endif()
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) 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 2123)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
+23 -6
View File
@@ -193,10 +193,6 @@ elseif(APPLE)
include(cmake/macos.cmake) include(cmake/macos.cmake)
endif() endif()
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
add_subdirectory(tests)
endif()
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp) list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
target_link_libraries(${PROJECT} PRIVATE ${LIBS}) target_link_libraries(${PROJECT} PRIVATE ${LIBS})
@@ -216,11 +212,32 @@ endif()
install(TARGETS ${PROJECT} install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR} DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET client_deps
COMPONENT AmneziaVPN 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 COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
) )
set(deploy_tool_options "") set(deploy_tool_options "")
-3
View File
@@ -109,9 +109,6 @@ void AmneziaApplication::init()
// install filter on main window // install filter on main window
if (auto win = qobject_cast<QQuickWindow*>(obj)) { if (auto win = qobject_cast<QQuickWindow*>(obj)) {
win->installEventFilter(this); win->installEventFilter(this);
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->setDefaultAlphaBuffer(true);
#endif
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
QObject::connect(win, &QQuickWindow::sceneGraphError, QObject::connect(win, &QQuickWindow::sceneGraphError,
[](QQuickWindow::SceneGraphError, const QString &msg) { [](QQuickWindow::SceneGraphError, const QString &msg) {
@@ -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_width="match_parent"
android:layout_height="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> </FrameLayout>
-8
View File
@@ -24,13 +24,5 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string> <string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</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> <string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources> </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="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification 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> <string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources> </resources>
@@ -42,9 +42,6 @@ import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import java.io.IOException import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext 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 CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3 private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4 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 PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri" private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
class AmneziaActivity : QtActivity(), LifecycleOwner { class AmneziaActivity : QtActivity() {
private val lifecycleRegistry = LifecycleRegistry(this)
override val lifecycle: Lifecycle
get() = lifecycleRegistry
private lateinit var mainScope: CoroutineScope private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>() private val qtInitialized = CompletableDeferred<Unit>()
@@ -108,8 +99,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
private var pendingOpenFileUri: String? = null private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false private var openFileDeliveryScheduled = false
private var lastPairingQrReaderStartUptimeMs: Long = 0L
private val vpnServiceEventHandler: Handler by lazy(NONE) { private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) { object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) { override fun handleMessage(msg: Message) {
@@ -216,7 +205,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
registerBroadcastReceivers() registerBroadcastReceivers()
intent?.let(::processIntent) intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() } runBlocking { vpnProto = proto.await() }
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -274,7 +262,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Log.d(TAG, "Start Amnezia activity") Log.d(TAG, "Start Amnezia activity")
mainScope.launch { mainScope.launch {
qtInitialized.await() qtInitialized.await()
@@ -298,7 +285,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
qtInitialized.await() qtInitialized.await()
QtAndroidController.onServiceDisconnected() QtAndroidController.onServiceDisconnected()
} }
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
super.onStop() super.onStop()
} }
@@ -371,7 +357,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
if (qtInitialized.isCompleted) { if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused() QtAndroidController.onActivityPaused()
} }
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
super.onPause() super.onPause()
isActivityResumed = false isActivityResumed = false
// Cancel all pending operations when activity pauses // Cancel all pending operations when activity pauses
@@ -382,7 +367,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
isActivityResumed = true isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity") Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) { if (qtInitialized.isCompleted) {
@@ -499,7 +483,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
unregisterBroadcastReceiver(notificationStateReceiver) unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null notificationStateReceiver = null
mainScope.cancel() mainScope.cancel()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
super.onDestroy() super.onDestroy()
} }
@@ -897,66 +880,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
@SuppressLint("UnsupportedChromeOsCameraSystemFeature") @SuppressLint("UnsupportedChromeOsCameraSystemFeature")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) 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") @Suppress("unused")
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 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") @Suppress("unused")
fun setSaveLogs(enabled: Boolean) { fun setSaveLogs(enabled: Boolean) {
Log.v(TAG, "Set save logs: $enabled") Log.v(TAG, "Set save logs: $enabled")
@@ -1269,7 +1179,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
CREATE_FILE_ACTION_CODE -> "CREATE_FILE" CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE" OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION" CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION"
else -> actionCode.toString() else -> actionCode.toString()
} }
} }
@@ -2,384 +2,47 @@ package org.amnezia.vpn
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle import android.os.Bundle
import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP 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 android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.FocusMeteringAction import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringAction.FLAG_AE import androidx.camera.core.FocusMeteringAction.FLAG_AE
import androidx.camera.core.FocusMeteringAction.FLAG_AF import androidx.camera.core.FocusMeteringAction.FLAG_AF
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider 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.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.BarcodeScannerOptions.Builder
import com.google.mlkit.vision.barcode.BarcodeScanning 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.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.common.InputImage
import org.amnezia.vpn.databinding.CameraPreviewBinding import org.amnezia.vpn.databinding.CameraPreviewBinding
import org.amnezia.vpn.qt.QtAndroidController import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log 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" private const val TAG = "CameraActivity"
@OptIn(TransformExperimental::class)
class CameraActivity : ComponentActivity() { class CameraActivity : ComponentActivity() {
companion object {
const val EXTRA_PAIRING_QR_CAMERA = "org.amnezia.vpn.extra.PAIRING_QR_CAMERA"
}
private lateinit var viewBinding: CameraPreviewBinding private lateinit var viewBinding: CameraPreviewBinding
private var cameraProvider: ProcessCameraProvider? = null private lateinit var cameraProvider: ProcessCameraProvider
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
@ExperimentalGetImage @ExperimentalGetImage
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewBinding = CameraPreviewBinding.inflate(layoutInflater) viewBinding = CameraPreviewBinding.inflate(layoutInflater)
setContentView(viewBinding.root) 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) 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) { private fun checkPermissions(onSuccess: () -> Unit, onFail: () -> Unit) {
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
onSuccess() onSuccess()
@@ -404,41 +67,26 @@ class CameraActivity : ComponentActivity() {
cameraProviderFuture.addListener({ cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get() cameraProvider = cameraProviderFuture.get()
bindCameraUseCases() bindPreview()
bindImageAnalysis()
}, ContextCompat.getMainExecutor(this)) }, ContextCompat.getMainExecutor(this))
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ExperimentalGetImage private fun bindPreview() {
private fun bindCameraUseCases() {
val provider = cameraProvider ?: return
imageAnalysisExecutor?.shutdown()
imageAnalysisExecutor = Executors.newSingleThreadExecutor()
val viewFinder = viewBinding.viewFinder val viewFinder = viewBinding.viewFinder
val preview = Preview.Builder().build().also { val preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewFinder.surfaceProvider) it.setSurfaceProvider(viewFinder.surfaceProvider)
} }
val imageAnalysis = ImageAnalysis.Builder() val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val camera = provider.bindToLifecycle(
this,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
boundCamera = camera
boundImageAnalysis = imageAnalysis
viewFinder.setOnTouchListener { _, motionEvent -> viewFinder.setOnTouchListener { _, motionEvent ->
when (motionEvent.action) { when (motionEvent.action) {
ACTION_DOWN -> true ACTION_DOWN -> true
ACTION_UP -> { ACTION_UP -> {
val point = viewFinder val point = viewFinder
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.y) .meteringPointFactory.createPoint(motionEvent.x, motionEvent.x)
val action = FocusMeteringAction val action = FocusMeteringAction
.Builder(point, FLAG_AF or FLAG_AE).build() .Builder(point, FLAG_AF or FLAG_AE).build()
@@ -450,121 +98,58 @@ class CameraActivity : ComponentActivity() {
else -> false else -> false
} }
} }
}
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) { @ExperimentalGetImage
previewTransformLayoutListener?.let { viewFinder.removeOnLayoutChangeListener(it) } private fun bindImageAnalysis() {
val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> val imageAnalysis = ImageAnalysis.Builder().build()
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()
}
try { val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis)
barcodeScanner?.close()
} catch (_: Exception) {
}
barcodeScanner = BarcodeScanning.getClient( val barcodeScanner = BarcodeScanning.getClient(
Builder() Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE) .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 checkedBarcodes = hashSetOf<String>()
val analysisExecutor = imageAnalysisExecutor!!
val mainExecutor = ContextCompat.getMainExecutor(this)
val pairingQrMode = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
imageAnalysis.setAnalyzer(analysisExecutor) { imageProxy -> imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)) { imageProxy ->
if (qrHandledOrClosing.get()) { imageProxy.image?.let { InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees) }
imageProxy.close() ?.let { image ->
return@setAnalyzer barcodeScanner.process(image).addOnSuccessListener { barcodes ->
} barcodes.firstOrNull()?.let { barcode ->
val mediaImage = imageProxy.image barcode.displayValue?.let { code ->
if (mediaImage == null) { if (code.isNotEmpty() && code !in checkedBarcodes) {
imageProxy.close() if (QtAndroidController.decodeQrCode(code)) {
return@setAnalyzer barcodeScanner.close()
}
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
}
stopCamera() 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() { private fun stopCamera() {
cleanupCameraResources() cameraProvider.unbindAll()
finish() 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 onActivityPaused()
external fun onActivityResumed() 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} 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.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.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/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.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/QtAppDelegate.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.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 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_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon" MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN" MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
-1
View File
@@ -49,7 +49,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.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) set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
-5
View File
@@ -45,7 +45,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h ${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h ${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.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/api/newsController.h
${CLIENT_ROOT_DIR}/core/controllers/updateController.h ${CLIENT_ROOT_DIR}/core/controllers/updateController.h
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.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/utilities.h
${CLIENT_ROOT_DIR}/core/utils/managementServer.h ${CLIENT_ROOT_DIR}/core/utils/managementServer.h
${CLIENT_ROOT_DIR}/core/utils/constants.h ${CLIENT_ROOT_DIR}/core/utils/constants.h
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
) )
# Mozilla headres # Mozilla headres
@@ -125,7 +122,6 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp ${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp ${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.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/api/newsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp ${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp ${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
@@ -161,7 +157,6 @@ set(SOURCES ${SOURCES}
if(NOT IOS AND NOT MACOS_NE) if(NOT IOS AND NOT MACOS_NE)
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
) )
endif() endif()
+278 -149
View File
@@ -4,6 +4,7 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <QThread>
#include <QUuid> #include <QUuid>
#include "logger.h" #include "logger.h"
@@ -137,117 +138,326 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
return protocolConfig; return protocolConfig;
} }
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig, const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{ {
// Generate new UUID for client const QString updatedConfig = QJsonDocument(serverConfig).toJson();
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); 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) const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
QString flowValue = "xtls-rprx-vision"; errorCode = m_sshSession->runScript(
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) { credentials,
if (!xrayCfg->serverConfig.flow.isEmpty()) { m_sshSession->replaceVars(restartScript,
flowValue = xrayCfg->serverConfig.flow; 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;
QString flowValue = srv.flow;
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
flowValue = QStringLiteral("xtls-rprx-vision");
}
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( QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file"; logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
return ""; << 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()); QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) { if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON"; logger.error() << "Failed to parse server config JSON";
errorCode = ErrorCode::InternalError; return ErrorCode::XrayServerConfigInvalid;
return "";
} }
QJsonObject serverConfig = doc.object(); QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) { if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field"; logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError; return ErrorCode::XrayServerConfigInvalid;
return "";
} }
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray(); QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) { if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array"; logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError; return ErrorCode::XrayServerConfigInvalid;
return "";
} }
QJsonObject inbound = inbounds[0].toObject(); QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) { if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field"; logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError; return ErrorCode::XrayServerConfigInvalid;
return ""; }
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(); QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) { if (!settings.contains(amnezia::protocols::xray::clients)) {
logger.error() << "Settings missing 'clients' field"; settings[amnezia::protocols::xray::clients] = QJsonArray {};
errorCode = ErrorCode::InternalError;
return "";
} }
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray(); QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
QString clientId;
// Create configuration for new client if (appendNewClient) {
QJsonObject clientConfig { clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
{amnezia::protocols::xray::id, clientId}, QJsonObject clientEntry;
}; clientEntry[amnezia::protocols::xray::id] = clientId;
clientConfig[amnezia::protocols::xray::id] = clientId; if (!flowValue.isEmpty()) {
if (!flowValue.isEmpty()) { clientEntry[amnezia::protocols::xray::flow] = flowValue;
clientConfig[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; settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings; inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound; inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds; serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
// Save updated config to server errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_sshSession->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config"; logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
return ""; return errorCode;
}
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
if (outClientId) {
*outClientId = clientId;
} }
// Restart container XrayProtocolConfig updated =
QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container"; logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
return ""; << 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; 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 XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
{ {
QJsonObject streamSettings; QJsonObject streamSettings;
@@ -419,6 +629,13 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
const DnsSettings &dnsSettings, const DnsSettings &dnsSettings,
ErrorCode &errorCode) 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; const XrayServerConfig *serverConfig = nullptr;
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) { if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayCfg->serverConfig; serverConfig = &xrayCfg->serverConfig;
@@ -441,93 +658,5 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
return XrayProtocolConfig{}; return XrayProtocolConfig{};
} }
// Fetch server keys (Reality only) return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
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;
} }
+26 -1
View File
@@ -23,12 +23,37 @@ public:
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig) override; 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: private:
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig, QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings, const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode); 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, QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
const QString &clientId) const; const QString &clientId) const;
}; };
@@ -90,7 +90,7 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType)); 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> { return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
auto [errorCode, responseBody] = result; auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) { 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 <QEventLoop>
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject>
#include <QPromise> #include <QPromise>
#include <QSet> #include <QSet>
#include <QSysInfo> #include <QSysInfo>
@@ -216,7 +217,8 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
} }
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType, 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(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
@@ -233,6 +235,19 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, 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) { if (errorCode != ErrorCode::NoError) {
return errorCode; return errorCode;
} }
@@ -242,9 +257,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
return errorCode; return errorCode;
} }
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody); updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) { if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError; return ErrorCode::InternalError;
} }
@@ -312,71 +327,6 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::NoError; 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, ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData, const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase, const QString &transactionId, bool isTestPurchase,
@@ -525,6 +475,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
if (apiV2->nameOverriddenByUser) { if (apiV2->nameOverriddenByUser) {
newApiV2->name = apiV2->name; newApiV2->name = apiV2->name;
newApiV2->displayName = apiV2->displayName;
newApiV2->nameOverriddenByUser = true; newApiV2->nameOverriddenByUser = true;
} }
@@ -999,7 +950,7 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled()); 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>>(); auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
[promise, watcher, gatewayController]() { [promise, watcher, gatewayController]() {
@@ -1020,3 +971,74 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
return promise->future(); 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 #ifndef SUBSCRIPTIONCONTROLLER_H
#define SUBSCRIPTIONCONTROLLER_H #define SUBSCRIPTIONCONTROLLER_H
#include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QByteArray> #include <QByteArray>
#include <QFuture> #include <QFuture>
@@ -43,6 +42,13 @@ public:
QJsonObject toJsonObject() const; QJsonObject toJsonObject() const;
}; };
struct CaptchaInfo {
QString captchaId;
QString captchaImageBase64;
QString hint;
bool isRequired = false;
};
explicit SubscriptionController(SecureServersRepository* serversRepository, explicit SubscriptionController(SecureServersRepository* serversRepository,
SecureAppSettingsRepository* appSettingsRepository); SecureAppSettingsRepository* appSettingsRepository);
@@ -50,13 +56,11 @@ public:
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload); void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType, 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, ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email); 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, ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData, const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &transactionId, bool isTestPurchase, const QString &transactionId, bool isTestPurchase,
@@ -102,6 +106,11 @@ public:
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType, AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol); 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: private:
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false); ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
bool isApiKeyExpired(const QString &serverId) const; bool isApiKeyExpired(const QString &serverId) const;
@@ -6,9 +6,7 @@
#include "core/utils/protocolEnum.h" #include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h" #include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h" #include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/utilities.h" #include "core/utils/utilities.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h" #include "core/utils/serverConfigUtils.h"
#include "version.h" #include "version.h"
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
@@ -51,14 +49,92 @@ void ConnectionController::setConnectionState(Vpn::ConnectionState state)
} }
} }
ErrorCode ConnectionController::prepareConnection(const QString &serverId, ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
QJsonObject& vpnConfiguration,
DockerContainer& container)
{ {
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()) { if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning; return ErrorCode::AmneziaServiceNotRunning;
} }
if (serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId))) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
DockerContainer container = DockerContainer::None;
const ErrorCode errorCode = defaultContainerForServer(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (container == DockerContainer::None) {
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; ContainerConfig containerConfigModel;
QPair<QString, QString> dns; QPair<QString, QString> dns;
QString hostName; QString hostName;
@@ -67,13 +143,15 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
bool isApiConfig = false; bool isApiConfig = false;
const auto kind = m_serversRepository->serverKind(serverId); const auto kind = m_serversRepository->serverKind(serverId);
const QString primaryDns = m_appSettingsRepository->primaryDns();
const QString secondaryDns = m_appSettingsRepository->secondaryDns();
switch (kind) { switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: { case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError; if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer; container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container); containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 }; dns = cfg->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns);
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
break; break;
@@ -83,7 +161,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError; if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer; container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container); containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 }; dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
break; break;
@@ -93,7 +171,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError; if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer; container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container); containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 }; dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
break; break;
@@ -105,7 +183,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError; if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer; container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container); containerConfigModel = cfg->containerConfig(container);
dns = { cfg->dns1, cfg->dns2 }; dns = cfg->getDnsPair(primaryDns, secondaryDns);
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway; configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
@@ -120,20 +198,6 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
return ErrorCode::InternalError; 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, vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
containerConfigModel, container); containerConfigModel, container);
@@ -34,6 +34,8 @@ public:
QJsonObject& vpnConfiguration, QJsonObject& vpnConfiguration,
DockerContainer& container); DockerContainer& container);
ErrorCode isConnectionSupported(const QString &serverId) const;
ErrorCode openConnection(const QString &serverId); ErrorCode openConnection(const QString &serverId);
void closeConnection(); void closeConnection();
@@ -73,6 +75,8 @@ signals:
#endif #endif
private: private:
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
SecureServersRepository* m_serversRepository; SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository; SecureAppSettingsRepository* m_appSettingsRepository;
VpnConnection* m_vpnConnection; VpnConnection* m_vpnConnection;
+7 -12
View File
@@ -153,7 +153,6 @@ void CoreController::initCoreControllers()
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository); m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository); m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
m_subscriptionController = new SubscriptionController(m_serversRepository, 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_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
m_updateController = new UpdateController(m_appSettingsRepository, this); m_updateController = new UpdateController(m_appSettingsRepository, this);
@@ -179,7 +178,8 @@ void CoreController::initControllers()
#ifdef Q_OS_WINDOWS #ifdef Q_OS_WINDOWS
m_ikev2ConfigModel, m_ikev2ConfigModel,
#endif #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); setQmlContextProperty("InstallController", m_installUiController);
m_importController = new ImportUiController(m_importCoreController, this); m_importController = new ImportUiController(m_importCoreController, this);
@@ -191,7 +191,7 @@ void CoreController::initControllers()
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this); m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
setQmlContextProperty("LanguageUiController", m_languageUiController); 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); setQmlContextProperty("SettingsController", m_settingsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this); m_pageController = new PageController(m_serversController, m_settingsController, this);
@@ -213,20 +213,18 @@ void CoreController::initControllers()
setQmlContextProperty("SystemController", m_systemController); setQmlContextProperty("SystemController", m_systemController);
m_networkReachabilityController = new NetworkReachabilityController(this); m_networkReachabilityController = new NetworkReachabilityController(this);
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController); setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController); setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this); m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController); setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController, m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel, 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); 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); m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
setQmlContextProperty("ApiNewsController", m_apiNewsUiController); setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
@@ -346,9 +344,6 @@ void CoreController::openConnectionByIndex(int serverIndex)
if (serverId.isEmpty()) { if (serverId.isEmpty()) {
return; return;
} }
if (m_serversModel) {
m_serversModel->setProcessedServerIndex(serverIndex);
}
if (m_serversController) { if (m_serversController) {
m_serversController->setDefaultServer(serverId); m_serversController->setDefaultServer(serverId);
} }
+30 -26
View File
@@ -10,8 +10,6 @@
#endif #endif
#include "ui/controllers/api/subscriptionUiController.h" #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/api/apiNewsUiController.h"
#include "ui/controllers/appSplitTunnelingUiController.h" #include "ui/controllers/appSplitTunnelingUiController.h"
#include "ui/controllers/allowedDnsUiController.h" #include "ui/controllers/allowedDnsUiController.h"
@@ -84,33 +82,11 @@
#endif #endif
class CoreSignalHandlers; 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 class CoreController : public QObject
{ {
Q_OBJECT Q_OBJECT
friend class CoreSignalHandlers; 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: public:
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings, explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
@@ -127,6 +103,36 @@ signals:
void translationsUpdated(); void translationsUpdated();
void websiteUrlChanged(const QString &newUrl); 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: private:
void initRepositories(); void initRepositories();
void initCoreControllers(); void initCoreControllers();
@@ -170,7 +176,6 @@ private:
UpdateUiController* m_updateUiController; UpdateUiController* m_updateUiController;
SubscriptionUiController* m_subscriptionUiController; SubscriptionUiController* m_subscriptionUiController;
PairingUiController* m_pairingUiController;
ApiNewsUiController* m_apiNewsUiController; ApiNewsUiController* m_apiNewsUiController;
ServicesCatalogUiController* m_servicesCatalogUiController; ServicesCatalogUiController* m_servicesCatalogUiController;
@@ -182,7 +187,6 @@ private:
AllowedDnsController* m_allowedDnsController; AllowedDnsController* m_allowedDnsController;
ServicesCatalogController* m_servicesCatalogController; ServicesCatalogController* m_servicesCatalogController;
SubscriptionController* m_subscriptionController; SubscriptionController* m_subscriptionController;
PairingController* m_pairingController;
NewsController* m_newsController; NewsController* m_newsController;
UpdateController* m_updateController; UpdateController* m_updateController;
InstallController* m_installController; InstallController* m_installController;
+26 -26
View File
@@ -21,7 +21,6 @@
#include "ui/controllers/selfhosted/installUiController.h" #include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/importUiController.h" #include "ui/controllers/importUiController.h"
#include "ui/controllers/api/subscriptionUiController.h" #include "ui/controllers/api/subscriptionUiController.h"
#include "ui/controllers/api/pairingUiController.h"
#include "ui/controllers/updateUiController.h" #include "ui/controllers/updateUiController.h"
#include "ui/models/serversModel.h" #include "ui/models/serversModel.h"
#include "core/controllers/serversController.h" #include "core/controllers/serversController.h"
@@ -34,7 +33,6 @@
#include "core/controllers/connectionController.h" #include "core/controllers/connectionController.h"
#include "ui/models/clientManagementModel.h" #include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h" #include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/containersModel.h" #include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
@@ -99,9 +97,6 @@ void CoreSignalHandlers::initErrorMessagesHandler()
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController, connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage)); 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, connect(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred, m_coreController->m_pageController,
qOverload<ErrorCode>(&PageController::showErrorMessage)); qOverload<ErrorCode>(&PageController::showErrorMessage));
} }
@@ -129,9 +124,9 @@ void CoreSignalHandlers::initInstallControllerHandler()
{ {
connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy); 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_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation);
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged, connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged,
m_coreController->m_installUiController, [this](int serverIndex) { m_coreController->m_installUiController, [this](const QString &serverId) {
if (serverIndex >= 0) { if (!serverId.isEmpty()) {
m_coreController->m_installUiController->clearProcessedServerCredentials(); m_coreController->m_installUiController->clearProcessedServerCredentials();
} }
}); });
@@ -160,15 +155,17 @@ void CoreSignalHandlers::initExportControllerHandler()
void CoreSignalHandlers::initImportControllerHandler() void CoreSignalHandlers::initImportControllerHandler()
{ {
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() { connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
if (!m_coreController->m_connectionController->isConnected()) { if (m_coreController->m_connectionUiController->isConnected()) {
int newServerIndex = m_coreController->m_serversController->getServersCount() - 1; return;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex); }
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId); const int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
} const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (m_coreController->m_serversUiController) { if (!serverId.isEmpty()) {
m_coreController->m_serversUiController->setProcessedServerId(serverId); m_coreController->m_serversController->setDefaultServer(serverId);
} }
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
} }
}); });
} }
@@ -180,17 +177,14 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
if (processedServerId.isEmpty()) { if (processedServerId.isEmpty()) {
return; return;
} }
QJsonArray availableCountries;
QString serverCountryCode;
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId); const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
if (apiV2.has_value()) { if (!apiV2.has_value()) {
availableCountries = apiV2->apiConfig.availableCountries; return;
serverCountryCode = apiV2->apiConfig.serverCountryCode;
} }
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode); m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
}); });
} }
@@ -241,13 +235,16 @@ void CoreSignalHandlers::initLanguageHandler()
connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() { connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->changeLanguage(m_coreController->m_languageUiController->getSystemLanguageEnum()); 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() void CoreSignalHandlers::initAutoConnectHandler()
{ {
if (m_coreController->m_settingsUiController->isAutoConnectEnabled() if (m_coreController->m_settingsUiController->isAutoConnectEnabled()
&& !m_coreController->m_serversController->getDefaultServerId().isEmpty()) { && !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(); });
} }
} }
@@ -352,6 +349,9 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
{ {
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested, connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested); m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
connect(m_coreController->m_connectionUiController, &ConnectionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
} }
void CoreSignalHandlers::initStrictKillSwitchHandler() void CoreSignalHandlers::initStrictKillSwitchHandler()
+65 -205
View File
@@ -10,7 +10,6 @@
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QPromise> #include <QPromise>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include "QBlockCipher.h" #include "QBlockCipher.h"
@@ -22,28 +21,17 @@
#include "core/utils/networkUtilities.h" #include "core/utils/networkUtilities.h"
#include "core/utils/utilities.h" #include "core/utils/utilities.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
#ifdef AMNEZIA_DESKTOP #ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h" #include "core/utils/ipcClient.h"
#endif #endif
namespace 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 errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for"); constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found."); 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"); constexpr QLatin1String updateRequestResponsePattern("client version update is required");
@@ -51,29 +39,18 @@ namespace
constexpr int httpStatusCodeConflict = 409; constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501; constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402; constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeRequestTimeout = 408;
constexpr int httpStatusCodeUnprocessableEntity = 422; constexpr int httpStatusCodeUnprocessableEntity = 422;
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000; 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, GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent) const bool isStrictKillSwitchEnabled, QObject *parent)
: QObject(parent), : QObject(parent),
m_gatewayEndpoint(normalizedGatewayBase(gatewayEndpoint)), m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment), m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs), m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
@@ -161,8 +138,6 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
QNetworkReply::NetworkError replyError, const QByteArray &key, QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt) const QByteArray &iv, const QByteArray &salt)
{ {
Q_UNUSED(replyError);
DecryptionResult result; DecryptionResult result;
result.decryptedBody = encryptedResponseBody; result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false; result.isDecryptionSuccessful = false;
@@ -179,29 +154,6 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
return result; 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) ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{ {
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
@@ -216,7 +168,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait); wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
@@ -225,18 +177,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater(); 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 = 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)) { if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) { 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(); httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult = decryptionResult =
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty() if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) { || 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); bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
} }
auto errorCode = responseBody = decryptionResult.decryptedBody;
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody); const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
if (errorCode) { if (errorCode) {
return errorCode; return errorCode;
} }
@@ -278,19 +221,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
return ErrorCode::ApiConfigDecryptionError; return ErrorCode::ApiConfigDecryptionError;
} }
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError; return ErrorCode::NoError;
} }
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject &apiPayload, QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
QNetworkReply **activeReplyOut,
const QSharedPointer<GatewayController> &keepAlive)
{ {
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create(); auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start(); promise->start();
const QSharedPointer<GatewayController> life = keepAlive;
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) { if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray())); 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); QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
if (activeReplyOut) {
*activeReplyOut = reply;
}
auto sslErrors = QSharedPointer<QList<QSslError>>::create(); auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, life]() mutable { connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
if (!life) {
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
GatewayController *const ctl = life.data();
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
auto replyError = reply->error(); auto replyError = reply->error();
@@ -322,20 +250,8 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
reply->deleteLater(); 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 = 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, auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError, 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, auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody); decryptionResult.decryptedBody);
if (errorCode) { if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray())); promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->finish(); promise->finish();
return; return;
} }
@@ -360,13 +276,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
promise->finish(); 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 serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList primaryBaseUrls; QStringList primaryBaseUrls;
QStringList fallbackBaseUrls; QStringList fallbackBaseUrls;
if (ctl->m_isDevEnvironment) { if (m_isDevEnvironment) {
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
} else { } else {
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
@@ -393,27 +309,19 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
appendStorageUrls(primaryBaseUrls, proxyStorageUrls); appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
life->getProxyUrlsAsync(life, proxyStorageUrls, 0, getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
[life, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
life->getProxyUrlAsync(life, proxyUrls, 0, bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[life, encRequestData, endpoint, processResponse]( [processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
const QString &proxyUrl) { const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
life->bypassProxyAsync( const QString &replyErrorString, int httpStatusCode) {
life, endpoint, proxyUrl, encRequestData, GatewayController::DecryptionResult result;
[processResponse](const QByteArray &decryptedBody, result.decryptedBody = decryptedBody;
bool isDecryptionSuccessful, result.isDecryptionSuccessful = isDecryptionSuccessful;
const QList<QSslError> &sslErrors, processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
QNetworkReply::NetworkError replyError, });
const QString &replyErrorString, });
int httpStatusCode) { });
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError,
replyErrorString, httpStatusCode);
});
});
});
} else { } else {
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode); 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::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait); wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) { if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll(); 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 GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful) bool isDecryptionSuccessful)
{ {
if (m_isDevEnvironment) {
return false;
}
const QByteArray &responseBody = decryptedResponseBody; const QByteArray &responseBody = decryptedResponseBody;
int apiHttpStatus = -1; int apiHttpStatus = -1;
@@ -558,15 +462,19 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << "the response contains an html tag"; qDebug() << "the response contains an html tag";
return true; return true;
} }
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
return false;
}
if (apiHttpStatus == httpStatusCodeNotFound) { if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) { || responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
return false; return false;
} else { } else {
qDebug() << replyError; qDebug() << replyError;
return true; return true;
} }
} }
if (apiHttpStatus == httpStatusCodeNotImplemented) { if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) { if (responseBody.contains(updateRequestResponsePattern)) {
return false; return false;
@@ -613,7 +521,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait); wait.exec(QEventLoop::ExcludeUserInputEvents);
auto result = replyProcessingFunction(reply, sslErrors); auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater(); 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::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
execNetworkWaitLoop(wait); wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) { if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater(); 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, void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const int currentProxyStorageIndex, const std::function<void(const QStringList &)> &onComplete) std::function<void(const QStringList &)> onComplete)
{ {
if (!life) {
onComplete({});
return;
}
if (currentProxyStorageIndex >= proxyStorageUrls.size()) { if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({}); onComplete({});
return; return;
@@ -684,23 +587,17 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
QNetworkReply *reply = amnApp->networkManager()->get(request); QNetworkReply *reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() { // connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
if (!life) {
onComplete({});
reply->deleteLater();
return;
}
GatewayController *const ctl = life.data();
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll(); QByteArray encrypted = reply->readAll();
reply->deleteLater(); reply->deleteLater();
QByteArray responseBody; QByteArray responseBody;
try { try {
QByteArray key = ctl->m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!ctl->m_isDevEnvironment) { if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512); QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key); hash.addData(key);
QByteArray h = hash.result().toHex(); QByteArray h = hash.result().toHex();
@@ -717,21 +614,15 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
} catch (...) { } catch (...) {
Utils::logException(); Utils::logException();
qCritical() << "error decrypting payload"; qCritical() << "error decrypting payload";
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() { QMetaObject::invokeMethod(
if (life) { this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
} else {
onComplete({});
}
});
return; return;
} }
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array(); QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints; QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray) { for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString()); endpoints.push_back(endpoint.toString());
}
QStringList shuffled = endpoints; QStringList shuffled = endpoints;
std::random_device randomDevice; std::random_device randomDevice;
@@ -746,26 +637,16 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
qDebug() << httpStatusCode; qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint"; qDebug() << "go to the next storage endpoint";
reply->deleteLater(); reply->deleteLater();
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() { QMetaObject::invokeMethod(
if (life) { this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
} else {
onComplete({});
}
});
}); });
} }
void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
const int currentProxyIndex, const std::function<void(const QString &)> &onComplete) std::function<void(const QString &)> onComplete)
{ {
if (!life) {
onComplete(QString());
return;
}
if (currentProxyIndex >= proxyUrls.size()) { if (currentProxyIndex >= proxyUrls.size()) {
onComplete(QString()); onComplete("");
return; return;
} }
@@ -776,16 +657,13 @@ void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController>
QNetworkReply *reply = amnApp->networkManager()->get(request); 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(); reply->deleteLater();
if (!life) {
onComplete(QString());
return;
}
GatewayController *const ctl = life.data();
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex]; m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl); onComplete(m_proxyUrl);
@@ -793,28 +671,15 @@ void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController>
} }
qDebug() << "go to the next proxy endpoint"; qDebug() << "go to the next proxy endpoint";
QTimer::singleShot(0, ctl, [life, proxyUrls, currentProxyIndex, onComplete]() { QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
if (life) {
life->getProxyUrlAsync(life, proxyUrls, currentProxyIndex + 1, onComplete);
} else {
onComplete(QString());
}
});
}); });
} }
void GatewayController::bypassProxyAsync( void GatewayController::bypassProxyAsync(
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
const EncryptedRequestData &encRequestData, std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)>
&onComplete)
{ {
auto sslErrors = QSharedPointer<QList<QSslError>>::create(); auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (!life) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
return;
}
if (proxyUrl.isEmpty()) { if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0); onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return; return;
@@ -825,9 +690,9 @@ void GatewayController::bypassProxyAsync(
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody); 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(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
auto replyError = reply->error(); auto replyError = reply->error();
@@ -835,13 +700,8 @@ void GatewayController::bypassProxyAsync(
reply->deleteLater(); reply->deleteLater();
if (!life) { auto decryptionResult =
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0); tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
return;
}
auto decryptionResult = life->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv,
encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString, onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode); httpStatusCode);
+6 -14
View File
@@ -1,8 +1,6 @@
#ifndef GATEWAYCONTROLLER_H #ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H #define GATEWAYCONTROLLER_H
#include <functional>
#include <QFuture> #include <QFuture>
#include <QNetworkReply> #include <QNetworkReply>
#include <QObject> #include <QObject>
@@ -27,9 +25,7 @@ public:
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload, QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
QNetworkReply **activeReplyOut = nullptr,
const QSharedPointer<GatewayController> &keepAlive = {});
private: private:
struct EncryptedRequestData struct EncryptedRequestData
@@ -40,7 +36,6 @@ private:
QByteArray iv; QByteArray iv;
QByteArray salt; QByteArray salt;
amnezia::ErrorCode errorCode; amnezia::ErrorCode errorCode;
bool isPlaintextLocalGateway = false;
}; };
struct DecryptionResult struct DecryptionResult
@@ -52,8 +47,6 @@ private:
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload); EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError, DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt); 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); QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful); 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<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction); std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls, int currentProxyStorageIndex, void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const std::function<void(const QStringList &)> &onComplete); std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, int currentProxyIndex, void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
const std::function<void(const QString &)> &onComplete);
void bypassProxyAsync( void bypassProxyAsync(
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, const EncryptedRequestData &encRequestData, const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> &onComplete); std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs; int m_requestTimeoutMsecs;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
@@ -486,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject config; QJsonObject config;
config[configKey::containers] = arr; config[configKey::containers] = arr;
config[configKey::defaultContainer] = configKey::amneziaOpenvpn; 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)"); 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); QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(data);
@@ -645,7 +645,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
QJsonObject config; QJsonObject config;
config[configKey::containers] = arr; config[configKey::containers] = arr;
config[configKey::defaultContainer] = containerName; config[configKey::defaultContainer] = containerName;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName(); config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp( const static QRegularExpression dnsRegExp(
"DNS = " "DNS = "
@@ -699,7 +699,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
? configKey::amneziaSsxray ? configKey::amneziaSsxray
: configKey::amneziaXray; : configKey::amneziaXray;
if (description.isEmpty()) { if (description.isEmpty()) {
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName(); config[configKey::description] = m_serversRepository->nextAvailableServerName();
} else { } else {
config[configKey::description] = description; config[configKey::description] = description;
} }
@@ -20,6 +20,7 @@
#include "core/installers/sftpInstaller.h" #include "core/installers/sftpInstaller.h"
#include "core/installers/socks5Installer.h" #include "core/installers/socks5Installer.h"
#include "core/installers/mtProxyInstaller.h" #include "core/installers/mtProxyInstaller.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/installers/telemtInstaller.h" #include "core/installers/telemtInstaller.h"
#include "core/installers/torInstaller.h" #include "core/installers/torInstaller.h"
#include "core/installers/wireguardInstaller.h" #include "core/installers/wireguardInstaller.h"
@@ -71,6 +72,16 @@ namespace
} }
return false; 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, InstallController::InstallController(SecureServersRepository *serversRepository,
@@ -119,9 +130,10 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e; return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished"; qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
sshSession.runScript(credentials, const amnezia::ScriptVars removeContainerVars =
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), amnezia::genBaseVars(credentials, container, QString(), QString());
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() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start"; qDebug().noquote() << "buildContainerWorker start";
@@ -146,8 +158,8 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return startupContainerWorker(credentials, container, config, sshSession); return startupContainerWorker(credentials, container, config, sshSession);
} }
ErrorCode InstallController::updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig,
ContainerConfig &newConfig) ContainerConfig &newConfig)
{ {
if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) { if (!isUpdateDockerContainerRequired(container, oldConfig, newConfig)) {
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
@@ -179,7 +191,17 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
SshSession sshSession(this); SshSession sshSession(this);
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig); 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; ErrorCode errorCode = ErrorCode::NoError;
if (reinstallRequired) { if (reinstallRequired) {
@@ -191,6 +213,21 @@ 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 (errorCode == ErrorCode::NoError) {
if (container == DockerContainer::MtProxy) { if (container == DockerContainer::MtProxy) {
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
@@ -205,6 +242,41 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
return errorCode; 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) void InstallController::clearCachedProfile(const QString &serverId, DockerContainer container)
{ {
if (ContainerUtils::containerService(container) == ServiceType::Other) { if (ContainerUtils::containerService(container) == ServiceType::Other) {
@@ -216,9 +288,9 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
return; return;
} }
adminConfig->clearCachedClientProfile(container);
const ContainerConfig containerConfigModel = adminConfig->containerConfig(container); const ContainerConfig containerConfigModel = adminConfig->containerConfig(container);
adminConfig->clearCachedClientProfile(container);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
emit clientRevocationRequested(serverId, containerConfigModel, container); emit clientRevocationRequested(serverId, containerConfigModel, container);
@@ -226,38 +298,75 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId) ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
{ {
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); const auto kind = m_serversRepository->serverKind(serverId);
if (!adminConfig.has_value()) {
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; return ErrorCode::InternalError;
} }
DockerContainer container = adminConfig->defaultContainer;
if (container == DockerContainer::None) { if (container == DockerContainer::None) {
return ErrorCode::NoInstalledContainersError; 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(); ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) { if (!credentials.isValid()) {
return ErrorCode::InternalError; return ErrorCode::InternalError;
} }
SshSession sshSession; SshSession sshSession;
const QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
auto isProtocolConfigExists = [](const ContainerConfig &cfg) { const ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
return cfg.protocolConfig.hasClientConfig(); if (errorCode != ErrorCode::NoError) {
}; return errorCode;
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);
} }
adminConfig->updateContainerConfig(container, containerConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
return ErrorCode::NoError; return ErrorCode::NoError;
} }
@@ -290,7 +399,7 @@ void InstallController::addEmptyServer(const ServerCredentials &credentials)
serverConfig.userName = credentials.userName; serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData; serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port; serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName(); serverConfig.description = m_serversRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description; serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None; serverConfig.defaultContainer = DockerContainer::None;
@@ -601,12 +710,19 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
} }
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) { if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig(); const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
const auto* newXrayConfig = newConfig.getXrayProtocolConfig(); const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
if (oldXrayConfig && newXrayConfig) { 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; return true;
}
} }
} }
@@ -905,10 +1021,11 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
return ErrorCode::InternalError; return ErrorCode::InternalError;
} }
SshSession sshSession(this); SshSession sshSession(this);
ErrorCode errorCode = sshSession.runScript( const amnezia::ScriptVars removeContainerVars =
credentials, amnezia::genBaseVars(credentials, container, QString(), QString());
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
amnezia::genBaseVars(credentials, container, QString(), QString()))); ErrorCode errorCode =
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
if (errorCode == ErrorCode::NoError) { if (errorCode == ErrorCode::NoError) {
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers; QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
@@ -1093,7 +1210,7 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials,
serverConfig.userName = credentials.userName; serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData; serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port; serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName(); serverConfig.description = m_serversRepository->nextAvailableServerName();
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) { for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
serverConfig.containers.insert(iterator.key(), iterator.value()); serverConfig.containers.insert(iterator.key(), iterator.value());
@@ -1163,28 +1280,26 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
return ErrorCode::NoError; return ErrorCode::NoError;
} }
ErrorCode InstallController::checkSshConnection(const ServerCredentials &credentials, QString &output, ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback) std::function<QString()> passphraseCallback)
{ {
SshSession sshSession(this); SshSession sshSession(this);
ErrorCode errorCode = ErrorCode::NoError; ErrorCode errorCode = ErrorCode::NoError;
ServerCredentials processedCredentials = credentials; if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
if (processedCredentials.secretData.contains("BEGIN") && processedCredentials.secretData.contains("PRIVATE KEY")) {
if (!passphraseCallback) { if (!passphraseCallback) {
return ErrorCode::SshPrivateKeyError; return ErrorCode::SshPrivateKeyError;
} }
QString decryptedPrivateKey; QString decryptedPrivateKey;
errorCode = sshSession.getDecryptedPrivateKey(processedCredentials, decryptedPrivateKey, passphraseCallback); errorCode = sshSession.getDecryptedPrivateKey(credentials, decryptedPrivateKey, passphraseCallback);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
return errorCode; return errorCode;
} }
processedCredentials.secretData = decryptedPrivateKey; credentials.secretData = decryptedPrivateKey;
} }
output = sshSession.checkSshConnection(processedCredentials, errorCode); output = sshSession.checkSshConnection(credentials, errorCode);
return errorCode; return errorCode;
} }
@@ -1388,7 +1503,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = containerAndPortMatch.captured(3); QString transportProtoStr = containerAndPortMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name); DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) { if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
continue; continue;
} }
@@ -1413,7 +1528,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
QString transportProtoStr = torOrDnsRegMatch.captured(3); QString transportProtoStr = torOrDnsRegMatch.captured(3);
DockerContainer container = ContainerUtils::containerFromString(name); DockerContainer container = ContainerUtils::containerFromString(name);
if (container == DockerContainer::None) { if (container == DockerContainer::None || ContainerUtils::isUnsupportedContainer(container)) {
continue; continue;
} }
@@ -34,7 +34,12 @@ public:
~InstallController(); ~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false); 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 rebootServer(const QString &serverId);
ErrorCode removeAllContainers(const QString &serverId); ErrorCode removeAllContainers(const QString &serverId);
@@ -64,7 +69,8 @@ public:
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig); 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); bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);
@@ -44,6 +44,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId); auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return false; if (!cfg.has_value()) return false;
cfg->description = name; cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind); m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true; return true;
} }
@@ -51,6 +52,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->selfHostedUserConfig(serverId); auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return false; if (!cfg.has_value()) return false;
cfg->description = name; cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind); m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true; return true;
} }
@@ -58,6 +60,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->nativeConfig(serverId); auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) return false; if (!cfg.has_value()) return false;
cfg->description = name; cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind); m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true; return true;
} }
@@ -67,6 +70,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->apiV2Config(serverId); auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) return false; if (!cfg.has_value()) return false;
cfg->name = name; cfg->name = name;
cfg->displayName = name;
cfg->nameOverriddenByUser = true; cfg->nameOverriddenByUser = true;
m_serversRepository->editServer(serverId, cfg->toJson(), kind); m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true; return true;
@@ -217,6 +217,11 @@ void SettingsController::toggleAutoStart(bool enable)
bool SettingsController::isStartMinimizedEnabled() const bool SettingsController::isStartMinimizedEnabled() const
{ {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!isAutoStartEnabled()) {
return false;
}
#endif
return m_appSettingsRepository->isStartMinimized(); return m_appSettingsRepository->isStartMinimized();
} }
@@ -358,6 +363,6 @@ void SettingsController::disablePremV1MigrationReminder()
QString SettingsController::nextAvailableServerName() const QString SettingsController::nextAvailableServerName() const
{ {
return m_appSettingsRepository->nextAvailableServerName(); return m_serversRepository->nextAvailableServerName();
} }
+17 -51
View File
@@ -13,7 +13,6 @@
#include "version.h" #include "version.h"
#include "core/controllers/gatewayController.h" #include "core/controllers/gatewayController.h"
#include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiKeys.h"
#include "core/utils/errorStrings.h"
#include "core/utils/selfhosted/scriptsRegistry.h" #include "core/utils/selfhosted/scriptsRegistry.h"
namespace namespace
@@ -21,13 +20,13 @@ namespace
Logger logger("UpdateController"); Logger logger("UpdateController");
#if defined(Q_OS_WINDOWS) #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"; const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
#elif defined(Q_OS_MACOS) #elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg"); const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg"; const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) #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"; const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
#endif #endif
} }
@@ -57,10 +56,6 @@ void UpdateController::checkForUpdates()
if (m_updateCheckRunning || !m_appSettingsRepository) { if (m_updateCheckRunning || !m_appSettingsRepository) {
return; return;
} }
if (m_appSettingsRepository->isDevGatewayEnv()) {
return;
}
m_updateCheckRunning = true; m_updateCheckRunning = true;
fetchGatewayUrl(); fetchGatewayUrl();
@@ -97,11 +92,6 @@ void UpdateController::doGetAsync(const QString &endpoint, std::function<void(bo
void UpdateController::fetchGatewayUrl() void UpdateController::fetchGatewayUrl()
{ {
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
finishUpdateCheck();
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(), auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(), m_appSettingsRepository->isDevGatewayEnv(),
7000, 7000,
@@ -114,19 +104,11 @@ void UpdateController::fetchGatewayUrl()
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.) // Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() { QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) { gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
finishUpdateCheck(); .then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
return;
}
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload, nullptr, gatewayController)
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
auto [err, gatewayResponse] = result; auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) { if (err != ErrorCode::NoError) {
if (err == ErrorCode::ApiNotFoundError) { logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
logger.debug() << "Update check: updater_endpoint not found on gateway";
} else {
logger.error() << errorString(err);
}
finishUpdateCheck(); finishUpdateCheck();
return; return;
} }
@@ -201,7 +183,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
logger.error() << QString("Network error occurred while fetching %1: %2 %3") logger.error() << QString("Network error occurred while fetching %1: %2 %3")
.arg(operation, reply->errorString(), QString::number(error)); .arg(operation, reply->errorString(), QString::number(error));
}); });
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) { QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
QStringList errorStrings; QStringList errorStrings;
for (const QSslError &err : errors) { for (const QSslError &err : errors) {
@@ -213,21 +195,13 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation) void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
{ {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) { logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
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);
}
} }
QString UpdateController::composeDownloadUrl() const 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); const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
return m_baseUrl + "/" + fileName; return m_baseUrl + "/" + fileName;
#else #else
@@ -237,7 +211,7 @@ QString UpdateController::composeDownloadUrl() const
void UpdateController::runInstaller() 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()) { if (m_downloadUrl.isEmpty()) {
logger.error() << "Download URL is empty"; logger.error() << "Download URL is empty";
return; return;
@@ -269,23 +243,15 @@ void UpdateController::runInstaller()
#if defined(Q_OS_WINDOWS) #if defined(Q_OS_WINDOWS)
runWindowsInstaller(kInstallerLocalPath); runWindowsInstaller(kInstallerLocalPath);
#elif defined(Q_OS_MACOS) #elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
runMacInstaller(kInstallerLocalPath); runMacInstaller(kInstallerLocalPath);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
runLinuxInstaller(kInstallerLocalPath); runLinuxInstaller(kInstallerLocalPath);
#endif #endif
} else { } else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) { << reply->errorString();
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
} 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);
}
} }
reply->deleteLater(); reply->deleteLater();
}); });
@@ -309,7 +275,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
} }
#endif #endif
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS) && !defined(MACOS_NE)
int UpdateController::runMacInstaller(const QString &installerPath) int UpdateController::runMacInstaller(const QString &installerPath)
{ {
// Create temporary directory for extraction // Create temporary directory for extraction
+27 -6
View File
@@ -1,15 +1,17 @@
#include "socks5Installer.h" #include "socks5Installer.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h" #include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h" #include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h" #include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h" #include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h" #include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h" #include "core/utils/selfhosted/sshSession.h"
#include "core/utils/utilities.h" #include "core/utils/utilities.h"
#include <QRegularExpression>
using namespace amnezia; using namespace amnezia;
using namespace ProtocolUtils; using namespace ProtocolUtils;
@@ -33,10 +35,29 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials, ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, ContainerConfig &config) SshSession* sshSession, ContainerConfig &config)
{ {
Q_UNUSED(container); if (container != DockerContainer::Socks5Proxy || !sshSession) {
Q_UNUSED(credentials); return ErrorCode::NoError;
Q_UNUSED(sshSession); }
Q_UNUSED(config);
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; return ErrorCode::NoError;
} }
+15 -4
View File
@@ -13,6 +13,7 @@
#include "core/utils/api/apiUtils.h" #include "core/utils/api/apiUtils.h"
#include "core/models/api/apiConfig.h" #include "core/models/api/apiConfig.h"
#include "core/models/api/authData.h" #include "core/models/api/authData.h"
#include "core/utils/networkUtilities.h"
namespace amnezia namespace amnezia
{ {
@@ -67,6 +68,20 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
return containers.value(container); 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 ApiV2ServerConfig::toJson() const
{ {
QJsonObject obj; QJsonObject obj;
@@ -80,9 +95,6 @@ QJsonObject ApiV2ServerConfig::toJson() const
if (!description.isEmpty()) { if (!description.isEmpty()) {
obj[configKey::description] = description; obj[configKey::description] = description;
} }
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
obj[configKey::configVersion] = configVersion; obj[configKey::configVersion] = configVersion;
@@ -134,7 +146,6 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
config.name = json.value(configKey::name).toString(); config.name = json.value(configKey::name).toString();
config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false); config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false);
config.description = json.value(configKey::description).toString(); config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.configVersion = json.value(configKey::configVersion).toInt(2); config.configVersion = json.value(configKey::configVersion).toInt(2);
config.hostName = json.value(configKey::hostName).toString(); config.hostName = json.value(configKey::hostName).toString();
@@ -3,6 +3,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QMap> #include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h" #include "core/utils/containers/containerUtils.h"
@@ -43,6 +44,9 @@ struct ApiV2ServerConfig {
bool isExternalPremium() const; bool isExternalPremium() const;
bool hasContainers() const; bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const; ContainerConfig containerConfig(DockerContainer container) const;
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const; QJsonObject toJson() const;
static ApiV2ServerConfig fromJson(const QJsonObject& json); static ApiV2ServerConfig fromJson(const QJsonObject& json);
}; };
@@ -23,9 +23,7 @@ LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
{ {
LegacyApiServerConfig config; LegacyApiServerConfig config;
config.name = json.value(configKey::name).toString();
config.description = json.value(configKey::description).toString(); config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString(); config.hostName = json.value(configKey::hostName).toString();
config.crc = json.value(configKey::crc).toInt(0); config.crc = json.value(configKey::crc).toInt(0);
@@ -108,35 +108,114 @@ QJsonObject XrayXhttpConfig::toJson() const
return obj; 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 XrayXhttpConfig::fromJson(const QJsonObject &json)
{ {
XrayXhttpConfig c; if (json.isEmpty()) {
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode); return clearedXhttpConfig();
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);
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); XrayXhttpConfig c = clearedXhttpConfig();
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();
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0"); if (json.contains(configKey::xhttpMode)) {
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString(); c.mode = json.value(configKey::xhttpMode).toString();
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1"); }
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100"); if (json.contains(configKey::xhttpHost)) {
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100"); c.host = json.value(configKey::xhttpHost).toString();
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800"); }
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1"); if (json.contains(configKey::xhttpPath)) {
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100"); 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()); if (json.contains(QLatin1String("xPadding"))) {
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject()); 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; return c;
} }
@@ -156,12 +235,27 @@ QJsonObject XrayMkcpConfig::toJson() const
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json) XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
{ {
XrayMkcpConfig c; XrayMkcpConfig c;
c.tti = json.value(configKey::mkcpTti).toString(); if (json.isEmpty()) {
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString(); return c;
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString(); }
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString(); if (json.contains(configKey::mkcpTti)) {
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString(); c.tti = json.value(configKey::mkcpTti).toString();
c.congestion = json.value(configKey::mkcpCongestion).toBool(true); }
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; return c;
} }
@@ -208,8 +302,14 @@ QJsonObject XrayServerConfig::toJson() const
if (!transport.isEmpty()) { if (!transport.isEmpty()) {
obj[configKey::xrayTransport] = transport; obj[configKey::xrayTransport] = transport;
} }
obj["xhttp"] = xhttp.toJson(); const QJsonObject xhttpObj = xhttp.toJson();
obj["mkcp"] = mkcp.toJson(); if (!xhttpObj.isEmpty()) {
obj[QStringLiteral("xhttp")] = xhttpObj;
}
const QJsonObject mkcpObj = mkcp.toJson();
if (!mkcpObj.isEmpty()) {
obj[QStringLiteral("mkcp")] = mkcpObj;
}
return obj; return obj;
} }
@@ -225,20 +325,39 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
c.site = json.value(configKey::site).toString(); c.site = json.value(configKey::site).toString();
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
// New: Security if (json.contains(configKey::xraySecurity)) {
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity); c.security = json.value(configKey::xraySecurity).toString();
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow); }
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint); if (json.contains(configKey::xrayFlow)) {
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { c.flow = json.value(configKey::xrayFlow).toString();
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); }
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; return c;
} }
@@ -251,7 +370,10 @@ bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) con
&& flow == other.flow && flow == other.flow
&& transport == other.transport && transport == other.transport
&& fingerprint == other.fingerprint && 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 QJsonObject XrayClientConfig::toJson() const
@@ -351,9 +473,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; 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 bool XrayProtocolConfig::hasClientConfig() const
{ {
return clientConfig.has_value(); return clientConfig.has_value();
@@ -75,6 +75,7 @@ struct XrayXhttpConfig {
XrayXmuxConfig xmux; XrayXmuxConfig xmux;
QJsonObject toJson() const; QJsonObject toJson() const;
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
static XrayXhttpConfig fromJson(const QJsonObject &json); static XrayXhttpConfig fromJson(const QJsonObject &json);
}; };
@@ -99,15 +100,13 @@ struct XrayServerConfig {
QString site; QString site;
bool isThirdPartyConfig = false; bool isThirdPartyConfig = false;
// New: Security QString security;
QString security = protocols::xray::defaultSecurity; QString flow;
QString flow = protocols::xray::defaultFlow; QString fingerprint;
QString fingerprint = protocols::xray::defaultFingerprint; QString sni;
QString sni = protocols::xray::defaultSni; QString alpn;
QString alpn = protocols::xray::defaultAlpn;
// New: Transport QString transport;
QString transport = protocols::xray::defaultTransport;
XrayXhttpConfig xhttp; XrayXhttpConfig xhttp;
XrayMkcpConfig mkcp; XrayMkcpConfig mkcp;
@@ -139,6 +138,10 @@ struct XrayProtocolConfig {
bool hasClientConfig() const; bool hasClientConfig() const;
void setClientConfig(const XrayClientConfig &config); void setClientConfig(const XrayClientConfig &config);
void clearClientConfig(); void clearClientConfig();
bool needsClientHydration = false;
bool hydrateServerConfigFromClientNative();
}; };
} // namespace amnezia } // namespace amnezia
@@ -9,6 +9,7 @@
#include "core/protocols/protocolUtils.h" #include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h" #include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h" #include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
namespace amnezia namespace amnezia
{ {
@@ -28,6 +29,25 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container); 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 NativeServerConfig::toJson() const
{ {
QJsonObject obj; QJsonObject obj;
@@ -35,9 +55,6 @@ QJsonObject NativeServerConfig::toJson() const
if (!description.isEmpty()) { if (!description.isEmpty()) {
obj[configKey::description] = this->description; obj[configKey::description] = this->description;
} }
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) { if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName; obj[configKey::hostName] = hostName;
} }
@@ -70,7 +87,6 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
NativeServerConfig config; NativeServerConfig config;
config.description = json.value(configKey::description).toString(); config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString(); config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray(); QJsonArray containersArray = json.value(configKey::containers).toArray();
@@ -3,6 +3,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QMap> #include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h" #include "core/utils/containers/containerUtils.h"
@@ -25,6 +26,11 @@ struct NativeServerConfig {
bool hasContainers() const; bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) 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; QJsonObject toJson() const;
static NativeServerConfig fromJson(const QJsonObject& json); static NativeServerConfig fromJson(const QJsonObject& json);
}; };
@@ -87,9 +87,6 @@ QJsonObject SelfHostedAdminServerConfig::toJson() const
if (!description.isEmpty()) { if (!description.isEmpty()) {
obj[configKey::description] = this->description; obj[configKey::description] = this->description;
} }
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) { if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName; obj[configKey::hostName] = hostName;
} }
@@ -132,7 +129,6 @@ SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObj
SelfHostedAdminServerConfig config; SelfHostedAdminServerConfig config;
config.description = json.value(configKey::description).toString(); config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString(); config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray(); QJsonArray containersArray = json.value(configKey::containers).toArray();
@@ -8,6 +8,7 @@
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h" #include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h" #include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
namespace amnezia namespace amnezia
{ {
@@ -42,6 +43,26 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
return containers.value(container); 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 SelfHostedUserServerConfig::toJson() const
{ {
QJsonObject obj; QJsonObject obj;
@@ -49,9 +70,6 @@ QJsonObject SelfHostedUserServerConfig::toJson() const
if (!description.isEmpty()) { if (!description.isEmpty()) {
obj[configKey::description] = this->description; obj[configKey::description] = this->description;
} }
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) { if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName; obj[configKey::hostName] = hostName;
} }
@@ -84,7 +102,6 @@ SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObjec
SelfHostedUserServerConfig config; SelfHostedUserServerConfig config;
config.description = json.value(configKey::description).toString(); config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString(); config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray(); QJsonArray containersArray = json.value(configKey::containers).toArray();
@@ -3,6 +3,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QMap> #include <QMap>
#include <QPair>
#include <optional> #include <optional>
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
@@ -30,6 +31,11 @@ struct SelfHostedUserServerConfig {
std::optional<ServerCredentials> credentials() const; std::optional<ServerCredentials> credentials() const;
bool hasContainers() const; bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) 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; QJsonObject toJson() const;
static SelfHostedUserServerConfig fromJson(const QJsonObject &json); static SelfHostedUserServerConfig fromJson(const QJsonObject &json);
}; };
+24 -13
View File
@@ -39,33 +39,44 @@ QString OpenVpnProtocol::defaultConfigPath()
return p; return p;
} }
void OpenVpnProtocol::stop() void OpenVpnProtocol::cleanupResources()
{ {
qDebug() << "OpenVpnProtocol::stop()"; if (m_openVpnProcess || openVpnProcessIsRunning()) {
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 (!sendTermSignal()) { if (!sendTermSignal()) {
killOpenVpnProcess(); killOpenVpnProcess();
} }
QThread::msleep(10); QThread::msleep(10);
m_managementServer.stop();
} }
m_managementServer.stop();
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS) #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) { IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch(); QRemoteObjectPendingReply<bool> reply = iface->disableKillSwitch();
if (!reply.waitForFinished(1000) && !reply.returnValue()) { if (!reply.waitForFinished(1000) && !reply.returnValue()) {
qWarning() << "OpenVpnProtocol::stop(): Failed to disable killswitch"; qWarning() << "OpenVpnProtocol::cleanupResources(): Failed to disable killswitch";
} }
}); });
#endif #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() ErrorCode OpenVpnProtocol::prepare()
@@ -168,7 +179,7 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start() ErrorCode OpenVpnProtocol::start()
{ {
OpenVpnProtocol::stop(); cleanupResources();
if (!QFileInfo::exists(configPath())) { if (!QFileInfo::exists(configPath())) {
setLastError(ErrorCode::OpenVpnConfigMissing); setLastError(ErrorCode::OpenVpnConfigMissing);
+1
View File
@@ -29,6 +29,7 @@ protected slots:
void onReadyReadDataFromManagementServer(); void onReadyReadDataFromManagementServer();
private: private:
void cleanupResources();
QString configPath() const; QString configPath() const;
bool openVpnProcessIsRunning() const; bool openVpnProcessIsRunning() const;
bool sendTermSignal(); bool sendTermSignal();
+183 -38
View File
@@ -9,10 +9,13 @@
#include "ipc.h" #include "ipc.h"
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QTimer> #include <QTimer>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkInterface> #include <QNetworkInterface>
#include <QNetworkProxy>
#include <QTcpSocket>
#include <QtCore/qlogging.h> #include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h> #include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h> #include <QtCore/qprocess.h>
@@ -56,6 +59,28 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) :
qWarning() << "Xray config string is not a valid JSON object"; qWarning() << "Xray config string is not a valid JSON object";
m_xrayConfig = {}; m_xrayConfig = {};
} }
m_serverPort = extractServerPort();
}
int XrayProtocol::extractServerPort() const
{
const QJsonArray outbounds = m_xrayConfig.value(amnezia::protocols::xray::outbounds).toArray();
if (outbounds.isEmpty())
return 0;
const QJsonObject settings = outbounds.first().toObject().value(amnezia::protocols::xray::settings).toObject();
QJsonArray servers;
if (settings.contains(amnezia::protocols::xray::vnext))
servers = settings.value(amnezia::protocols::xray::vnext).toArray();
else if (settings.contains(amnezia::protocols::xray::servers))
servers = settings.value(amnezia::protocols::xray::servers).toArray();
if (servers.isEmpty())
return 0;
return servers.first().toObject().value(amnezia::protocols::xray::port).toInt();
} }
XrayProtocol::~XrayProtocol() XrayProtocol::~XrayProtocol()
@@ -68,6 +93,13 @@ ErrorCode XrayProtocol::start()
{ {
qDebug() << "XrayProtocol::start()"; qDebug() << "XrayProtocol::start()";
m_connectivityProbeStarted = false;
if (!probeServerReachable()) {
qCritical() << "XrayProtocol: VPN server" << m_remoteAddress << "is unreachable";
return ErrorCode::XrayServerUnreachable;
}
// Inject SOCKS5 auth into the inbound before starting xray. // Inject SOCKS5 auth into the inbound before starting xray.
// Re-uses existing credentials if the config already has them (e.g. imported config). // Re-uses existing credentials if the config already has them (e.g. imported config).
amnezia::serialization::inbounds::InboundCredentials creds; amnezia::serialization::inbounds::InboundCredentials creds;
@@ -104,22 +136,50 @@ ErrorCode XrayProtocol::start()
qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1"; qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1";
} }
startTimeoutTimer();
return IpcClient::withInterface( return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) { [&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr); auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) { if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray"; qCritical() << "Failed to start xray";
stopTimeoutTimer();
return ErrorCode::XrayExecutableCrashed; return ErrorCode::XrayExecutableCrashed;
} }
return startTun2Socks(); return startTun2Socks();
}, },
[]() { return ErrorCode::AmneziaServiceConnectionFailed; }); [this]() {
stopTimeoutTimer();
return ErrorCode::AmneziaServiceConnectionFailed;
});
} }
void XrayProtocol::stop() void XrayProtocol::stop()
{ {
qDebug() << "XrayProtocol::stop()"; qDebug() << "XrayProtocol::stop()";
stopTimeoutTimer();
stopLivenessMonitor();
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
m_tun2socksProcess->waitForFinished(1000);
}
#else
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) { IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch(); auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue()) if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
@@ -142,31 +202,13 @@ void XrayProtocol::stop()
qWarning() << "Failed to stop xray"; qWarning() << "Failed to stop xray";
}); });
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
}
#else
// terminate does not do anything useful on Windows
// so just kill the process
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
setConnectionState(Vpn::ConnectionState::Disconnected); setConnectionState(Vpn::ConnectionState::Disconnected);
} }
ErrorCode XrayProtocol::startTun2Socks() ErrorCode XrayProtocol::startTun2Socks()
{ {
m_tunResourceBusy = false;
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess(); m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) { if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed; return ErrorCode::AmneziaServiceConnectionFailed;
@@ -191,15 +233,31 @@ ErrorCode XrayProtocol::startTun2Socks()
if (!line.contains("[TCP]") && !line.contains("[UDP]")) if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line; qDebug() << "[tun2socks]:" << line;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://")) { if (line.contains("resource busy"))
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr); m_tunResourceBusy = true;
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) { if (line.contains("[STACK] tun://") && line.contains("<-> socks5://") && !m_connectivityProbeStarted) {
stop(); m_connectivityProbeStarted = true;
setLastError(res); disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
} else { disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this, nullptr);
setConnectionState(Vpn::ConnectionState::Connected);
} runConnectivityProbe([this](bool ok) {
if (!ok) {
qCritical() << "Xray connectivity probe failed: no traffic flows through the tunnel";
stop();
setLastError(ErrorCode::XrayConnectivityCheckFailed);
return;
}
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
stopTimeoutTimer();
setConnectionState(Vpn::ConnectionState::Connected);
startLivenessMonitor();
}
});
} }
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -207,15 +265,7 @@ ErrorCode XrayProtocol::startTun2Socks()
connect( connect(
m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[this](int exitCode, QProcess::ExitStatus exitStatus) { [this](int exitCode, QProcess::ExitStatus exitStatus) {
// Check stdout for "resource busy" — the TUN device was not yet released const bool resourceBusy = m_tunResourceBusy;
// by the previous tun2socks instance. Retry after a short delay.
bool resourceBusy = false;
if (m_tun2socksProcess) {
auto readOut = m_tun2socksProcess->readAllStandardOutput();
if (readOut.waitForFinished()) {
resourceBusy = readOut.returnValue().contains("resource busy");
}
}
if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) { if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) {
m_tun2socksRetryCount++; m_tun2socksRetryCount++;
@@ -331,3 +381,98 @@ ErrorCode XrayProtocol::setupRouting()
}, },
[]() { return ErrorCode::AmneziaServiceConnectionFailed; }); []() { return ErrorCode::AmneziaServiceConnectionFailed; });
} }
bool XrayProtocol::probeServerReachable()
{
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
qWarning() << "XrayProtocol: skipping server reachability probe (address/port unknown)";
return true;
}
QTcpSocket sock;
sock.connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
const bool ok = sock.waitForConnected(m_serverProbeTimeoutMs);
if (!ok) {
qWarning() << "XrayProtocol: server" << m_remoteAddress << ":" << m_serverPort
<< "unreachable:" << sock.errorString();
}
sock.abort();
return ok;
}
void XrayProtocol::runConnectivityProbe(std::function<void(bool)> onResult)
{
if (m_remoteAddress.isEmpty() || m_serverPort <= 0) {
qWarning() << "XrayProtocol: connectivity probe skipped (server address/port unknown)";
onResult(true);
return;
}
auto *sock = new QTcpSocket(this);
QNetworkProxy proxy(QNetworkProxy::Socks5Proxy, QStringLiteral("127.0.0.1"),
static_cast<quint16>(m_socksPort), m_socksUser, m_socksPassword);
proxy.setCapabilities(QNetworkProxy::TunnelingCapability | QNetworkProxy::HostNameLookupCapability);
sock->setProxy(proxy);
auto *timeout = new QTimer(this);
timeout->setSingleShot(true);
auto done = QSharedPointer<bool>::create(false);
auto finish = [=](bool ok) {
if (*done)
return;
*done = true;
timeout->stop();
timeout->deleteLater();
sock->abort();
sock->deleteLater();
onResult(ok);
};
connect(sock, &QTcpSocket::connected, this, [=]() { finish(true); });
connect(sock, &QAbstractSocket::errorOccurred, this, [=](QAbstractSocket::SocketError) { finish(false); });
connect(timeout, &QTimer::timeout, this, [=]() { finish(false); });
timeout->start(m_connectivityProbeTimeoutMs);
sock->connectToHost(m_remoteAddress, static_cast<quint16>(m_serverPort));
}
void XrayProtocol::startLivenessMonitor()
{
if (!m_livenessTimer) {
m_livenessTimer = new QTimer(this);
connect(m_livenessTimer, &QTimer::timeout, this, [this]() {
if (connectionState() != Vpn::ConnectionState::Connected)
return;
runConnectivityProbe([this](bool ok) {
if (connectionState() != Vpn::ConnectionState::Connected)
return;
if (ok) {
m_livenessFailures = 0;
} else if (++m_livenessFailures >= m_maxLivenessFailures) {
qCritical() << "XrayProtocol: liveness check failed" << m_livenessFailures
<< "times in a row, the tunnel is dead";
stop();
setLastError(ErrorCode::XrayConnectionLost);
} else {
qWarning() << "XrayProtocol: liveness check failed (" << m_livenessFailures << "/"
<< m_maxLivenessFailures << ")";
}
});
});
}
m_livenessFailures = 0;
m_livenessTimer->start(m_livenessIntervalMs);
}
void XrayProtocol::stopLivenessMonitor()
{
if (m_livenessTimer)
m_livenessTimer->stop();
m_livenessFailures = 0;
}
+27
View File
@@ -6,12 +6,16 @@
#include <QHostAddress> #include <QHostAddress>
#include <QList> #include <QList>
#include <functional>
#include "core/utils/errorCodes.h" #include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h" #include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h" #include "core/utils/commonStructs.h"
#include "core/utils/ipcClient.h" #include "core/utils/ipcClient.h"
#include "vpnProtocol.h" #include "vpnProtocol.h"
class QTimer;
class XrayProtocol : public VpnProtocol class XrayProtocol : public VpnProtocol
{ {
public: public:
@@ -25,10 +29,17 @@ private:
ErrorCode setupRouting(); ErrorCode setupRouting();
ErrorCode startTun2Socks(); ErrorCode startTun2Socks();
bool probeServerReachable();
void runConnectivityProbe(std::function<void(bool)> onResult);
void startLivenessMonitor();
void stopLivenessMonitor();
int extractServerPort() const;
QJsonObject m_xrayConfig; QJsonObject m_xrayConfig;
amnezia::RouteMode m_routeMode; amnezia::RouteMode m_routeMode;
QList<QHostAddress> m_dnsServers; QList<QHostAddress> m_dnsServers;
QString m_remoteAddress; QString m_remoteAddress;
int m_serverPort = 0;
QString m_socksUser; QString m_socksUser;
QString m_socksPassword; QString m_socksPassword;
@@ -38,6 +49,22 @@ private:
int m_tun2socksRetryCount = 0; int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5; static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400; static constexpr int tun2socksRetryDelayMs = 400;
bool m_connectivityProbeStarted = false;
bool m_tunResourceBusy = false;
QTimer *m_livenessTimer = nullptr;
int m_livenessFailures = 0;
static constexpr int defaultServerProbeTimeoutMs = 5000;
static constexpr int defaultConnectivityProbeTimeoutMs = 7000;
static constexpr int defaultLivenessIntervalMs = 15000;
static constexpr int defaultMaxLivenessFailures = 3;
int m_serverProbeTimeoutMs = defaultServerProbeTimeoutMs;
int m_connectivityProbeTimeoutMs = defaultConnectivityProbeTimeoutMs;
int m_livenessIntervalMs = defaultLivenessIntervalMs;
int m_maxLivenessFailures = defaultMaxLivenessFailures;
}; };
#endif // XRAYPROTOCOL_H #endif // XRAYPROTOCOL_H
@@ -426,26 +426,6 @@ void SecureAppSettingsRepository::clearSettings()
emit settingsCleared(); 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) void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
{ {
m_settings->setValue("Conf/installationUuid", uuid); m_settings->setValue("Conf/installationUuid", uuid);
@@ -90,8 +90,6 @@ public:
bool restoreAppConfig(const QByteArray &cfg); bool restoreAppConfig(const QByteArray &cfg);
void clearSettings(); void clearSettings();
QString nextAvailableServerName() const;
QByteArray xraySavedConfigs() const; QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data); void setXraySavedConfigs(const QByteArray &data);
@@ -3,6 +3,7 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonValue> #include <QJsonValue>
#include <QSet>
#include <QUuid> #include <QUuid>
#include "core/utils/serverConfigUtils.h" #include "core/utils/serverConfigUtils.h"
@@ -32,6 +33,45 @@ QJsonObject embedStorageServerId(const QString &serverId, const QJsonObject &pay
return o; 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 } // namespace
SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent) SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent)
@@ -153,6 +193,28 @@ void SecureServersRepository::clearServers()
syncToStorage(); 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 = QStringLiteral("Server %1").arg(i);
} while (usedNames.contains(candidate));
return candidate;
}
QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind) QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind)
{ {
const QString id = normalizedOrGeneratedServerId(serverId); const QString id = normalizedOrGeneratedServerId(serverId);
@@ -48,6 +48,8 @@ public:
void clearServers(); void clearServers();
QString nextAvailableServerName() const;
void invalidateCache(); void invalidateCache();
signals: signals:
+25 -59
View File
@@ -2,7 +2,6 @@
#include "core/utils/serverConfigUtils.h" #include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/configKeys.h" #include "core/utils/constants/configKeys.h"
#include <QLatin1Char>
#include <QDateTime> #include <QDateTime>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
@@ -77,26 +76,6 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
return endDate <= nowUtc.addDays(withinDays); 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, amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode, const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody) const QByteArray &responseBody)
@@ -105,15 +84,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeNotFound = 404; const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501; const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodePaymentRequired = 402; const int httpStatusCodePaymentRequired = 402;
const int httpStatusCodeTooManyRequests = 429;
const int httpStatusCodeRequestTimeout = 408;
const int httpStatusCodeUnprocessableEntity = 422; const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) { if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors; qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError; return amnezia::ErrorCode::ApiConfigSslError;
} }
if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| replyError == QNetworkReply::NetworkError::TimeoutError) { || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError; qDebug() << replyError;
@@ -124,14 +102,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} }
qDebug() << QString::fromUtf8(responseBody);
qDebug() << replyError;
qDebug() << httpStatusCode;
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody); QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) { if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object(); QJsonObject jsonObj = jsonDoc.object();
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1); const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
return amnezia::ErrorCode::ApiRateLimitError;
}
if (httpStatusFromBody == httpStatusCodeConflict) { if (httpStatusFromBody == httpStatusCodeConflict) {
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) { if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
return amnezia::ErrorCode::ApiTrialAlreadyUsedError; return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
@@ -141,6 +119,9 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (httpStatusFromBody == httpStatusCodeNotFound) { if (httpStatusFromBody == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError; return amnezia::ErrorCode::ApiNotFoundError;
} }
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
return amnezia::ErrorCode::ApiConfigTimeoutError;
}
if (httpStatusFromBody == httpStatusCodeNotImplemented) { if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} }
@@ -151,28 +132,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiConfigDownloadError; return amnezia::ErrorCode::ApiConfigDownloadError;
} }
if (httpStatusFromBody == httpStatusCodePaymentRequired) { 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; return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
} }
const QString msg = apiErrorMessageFromJson(jsonObj); if (httpStatusFromBody >= 300) {
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive) return amnezia::ErrorCode::ApiConfigDownloadError;
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
} }
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) { if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::ApiNotFoundError; return amnezia::ErrorCode::NoError;
} }
qDebug() << "something went wrong"; qDebug() << "something went wrong";
@@ -272,18 +253,3 @@ QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
return vpnKeyText; 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 QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody); const QByteArray &responseBody);
amnezia::ErrorCode errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj);
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject); QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
QString getPremiumV2VpnKey(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 #endif // APIUTILS_H
-1
View File
@@ -22,7 +22,6 @@ namespace apiDefs
constexpr QLatin1String availableCountries("available_countries"); constexpr QLatin1String availableCountries("available_countries");
constexpr QLatin1String installationUuid("installation_uuid"); constexpr QLatin1String installationUuid("installation_uuid");
constexpr QLatin1String uuid("installation_uuid"); constexpr QLatin1String uuid("installation_uuid");
constexpr QLatin1String qrUuid("qr_uuid");
constexpr QLatin1String osVersion("os_version"); constexpr QLatin1String osVersion("os_version");
constexpr QLatin1String userCountryCode("user_country_code"); constexpr QLatin1String userCountryCode("user_country_code");
constexpr QLatin1String serverCountryCode("server_country_code"); constexpr QLatin1String serverCountryCode("server_country_code");
+2
View File
@@ -15,6 +15,8 @@ namespace amnezia
Awg2, Awg2,
WireGuard, WireGuard,
OpenVpn, OpenVpn,
Cloak,
ShadowSocks,
Ipsec, Ipsec,
Xray, Xray,
SSXray, SSXray,
@@ -21,6 +21,8 @@ QString ContainerUtils::containerToString(DockerContainer c)
{ {
if (c == DockerContainer::None) if (c == DockerContainer::None)
return "none"; return "none";
if (c == DockerContainer::Cloak)
return "amnezia-openvpn-cloak";
if (c == DockerContainer::Awg) if (c == DockerContainer::Awg)
return "amnezia-awg"; return "amnezia-awg";
if (c == DockerContainer::Awg2) if (c == DockerContainer::Awg2)
@@ -62,6 +64,8 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
{ {
return { { DockerContainer::None, "Not installed" }, return { { DockerContainer::None, "Not installed" },
{ DockerContainer::OpenVpn, "OpenVPN" }, { DockerContainer::OpenVpn, "OpenVPN" },
{ DockerContainer::ShadowSocks, "OpenVPN over SS" },
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
{ DockerContainer::WireGuard, "WireGuard" }, { DockerContainer::WireGuard, "WireGuard" },
{ DockerContainer::Awg, "AmneziaWG" }, { DockerContainer::Awg, "AmneziaWG" },
{ DockerContainer::Awg2, "AmneziaWG" }, { DockerContainer::Awg2, "AmneziaWG" },
@@ -83,6 +87,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
return { { DockerContainer::OpenVpn, return { { DockerContainer::OpenVpn,
QObject::tr("OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its " 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.") }, "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, { DockerContainer::WireGuard,
QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power " QObject::tr("WireGuard - popular VPN protocol with high performance, high speed and low power "
"consumption.") }, "consumption.") },
@@ -194,6 +202,9 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
ServiceType ContainerUtils::containerService(DockerContainer c) ServiceType ContainerUtils::containerService(DockerContainer c)
{ {
if (isUnsupportedContainer(c)) {
return ServiceType::Vpn;
}
return ProtocolUtils::protocolService(defaultProtocol(c)); return ProtocolUtils::protocolService(defaultProtocol(c));
} }
@@ -202,6 +213,8 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
switch (c) { switch (c) {
case DockerContainer::None: return Proto::Unknown; case DockerContainer::None: return Proto::Unknown;
case DockerContainer::OpenVpn: return Proto::OpenVpn; case DockerContainer::OpenVpn: return Proto::OpenVpn;
case DockerContainer::Cloak:
case DockerContainer::ShadowSocks: return Proto::Unknown;
case DockerContainer::WireGuard: return Proto::WireGuard; case DockerContainer::WireGuard: return Proto::WireGuard;
case DockerContainer::Awg2: return Proto::Awg; case DockerContainer::Awg2: return Proto::Awg;
case DockerContainer::Awg: 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. // macOS build using Network Extension allow OpenVPN for parity with iOS.
switch (c) { switch (c) {
case DockerContainer::OpenVpn: return true; case DockerContainer::OpenVpn: return true;
case DockerContainer::Cloak: return false;
case DockerContainer::ShadowSocks: return false;
case DockerContainer::WireGuard: return true; case DockerContainer::WireGuard: return true;
case DockerContainer::Awg2: return true; case DockerContainer::Awg2: return true;
case DockerContainer::Awg: return true; case DockerContainer::Awg: return true;
@@ -336,6 +351,10 @@ int ContainerUtils::easySetupOrder(DockerContainer container)
bool ContainerUtils::isShareable(DockerContainer container) bool ContainerUtils::isShareable(DockerContainer container)
{ {
if (isUnsupportedContainer(container)) {
return false;
}
switch (container) { switch (container) {
case DockerContainer::TorWebSite: return false; case DockerContainer::TorWebSite: return false;
case DockerContainer::Dns: return false; case DockerContainer::Dns: return false;
@@ -352,6 +371,11 @@ bool ContainerUtils::isAwgContainer(DockerContainer container)
return container == DockerContainer::Awg || container == DockerContainer::Awg2; 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) QJsonObject ContainerUtils::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig)
{ {
QString protocolConfigString = containerConfig.value(ProtocolUtils::protoToString(protocol)) QString protocolConfigString = containerConfig.value(ProtocolUtils::protoToString(protocol))
@@ -45,6 +45,8 @@ namespace amnezia
bool isAwgContainer(DockerContainer container); bool isAwgContainer(DockerContainer container);
bool isUnsupportedContainer(DockerContainer container);
QJsonObject getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig); QJsonObject getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig);
int installPageOrder(DockerContainer container); int installPageOrder(DockerContainer container);
+11 -9
View File
@@ -35,6 +35,9 @@ namespace amnezia
ServerCgroupMountpoint = 212, ServerCgroupMountpoint = 212,
DockerPullRateLimit = 213, DockerPullRateLimit = 213,
ServerLinuxKernelTooOld = 214, ServerLinuxKernelTooOld = 214,
XrayServerConfigInvalid = 215,
XrayServerNoVlessClients = 216,
XrayRealityKeysReadFailed = 217,
// Ssh connection errors // Ssh connection errors
SshRequestDeniedError = 300, SshRequestDeniedError = 300,
@@ -68,6 +71,9 @@ namespace amnezia
OpenSslFailed = 800, OpenSslFailed = 800,
XrayExecutableCrashed = 803, XrayExecutableCrashed = 803,
Tun2SockExecutableCrashed = 804, Tun2SockExecutableCrashed = 804,
XrayServerUnreachable = 805,
XrayConnectivityCheckFailed = 806,
XrayConnectionLost = 807,
// import and install errors // import and install errors
ImportInvalidConfigError = 900, ImportInvalidConfigError = 900,
@@ -76,6 +82,7 @@ namespace amnezia
ImportBackupFileUseRestoreInstead = 903, ImportBackupFileUseRestoreInstead = 903,
RestoreBackupInvalidError = 904, RestoreBackupInvalidError = 904,
LegacyApiV1NotSupportedError = 905, LegacyApiV1NotSupportedError = 905,
LegacyContainerNotSupportedError = 906,
// Android errors // Android errors
AndroidError = 1000, AndroidError = 1000,
@@ -98,15 +105,10 @@ namespace amnezia
ApiSubscriptionNotActiveError = 1114, ApiSubscriptionNotActiveError = 1114,
ApiNoPurchasedSubscriptionsError = 1115, ApiNoPurchasedSubscriptionsError = 1115,
ApiTrialAlreadyUsedError = 1116, ApiTrialAlreadyUsedError = 1116,
ApiCaptchaRequiredError = 1117,
// QR pairing (gateway /v1/generate_qr, /v1/scan_qr) ApiCaptchaInvalidError = 1118,
ApiPairingForbiddenError = 1117, ApiCaptchaRefreshError = 1119,
ApiPairingConflictError = 1118, ApiRateLimitError = 1120,
ApiPairingRateLimitedError = 1119,
ApiPairingServiceUnavailableError = 1120,
ApiPairingPayloadTooLargeError = 1121,
ApiPairingMissingMetadataError = 1122,
ApiPairingSessionExpiredError = 1123,
// QFile errors // QFile errors
OpenError = 1200, OpenError = 1200,
+17 -7
View File
@@ -30,6 +30,15 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break; 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::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::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;
// Libssh errors // Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break; case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -50,6 +59,9 @@ QString errorString(ErrorCode code) {
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break; case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break; case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break; case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
case (ErrorCode::XrayServerUnreachable): errorMessage = QObject::tr("Can't connect: the VPN server is unreachable"); break;
case (ErrorCode::XrayConnectivityCheckFailed): errorMessage = QObject::tr("Can't connect: no internet traffic flows through the tunnel"); break;
case (ErrorCode::XrayConnectionLost): errorMessage = QObject::tr("Connection lost: traffic stopped flowing through the tunnel"); break;
// VPN errors // VPN errors
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break; case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
@@ -60,6 +72,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::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::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::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::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; case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;
@@ -84,13 +97,10 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break; 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::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::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::ApiCaptchaRequiredError): errorMessage = QObject::tr("CAPTCHA verification is required"); break;
case (ErrorCode::ApiPairingConflictError): errorMessage = QObject::tr("This QR code has already been used"); break; case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
case (ErrorCode::ApiPairingRateLimitedError): errorMessage = QObject::tr("Too many requests. Please try again later"); break; case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
case (ErrorCode::ApiPairingServiceUnavailableError): errorMessage = QObject::tr("Service temporarily unavailable. Please try again later"); break; case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. 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;
// QFile errors // QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; 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 <QIODevice>
#include <QList> #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) QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
{ {
double k = 850; double k = 850;
-1
View File
@@ -10,7 +10,6 @@ namespace qrCodeUtils
constexpr const qint16 qrMagicCode = 1984; constexpr const qint16 qrMagicCode = 1984;
QList<QString> generateQrCodeImageSeries(const QByteArray &data); QList<QString> generateQrCodeImageSeries(const QByteArray &data);
QList<QString> generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text);
qrcodegen::QrCode generateQrCode(const QByteArray &data); qrcodegen::QrCode generateQrCode(const QByteArray &data);
QString svgToBase64(const QString &image); 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_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
vars.append({{"$MTPROXY_SECRET", c.secret}}); 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_TAG", c.tag}});
vars.append({{"$MTPROXY_TRANSPORT_MODE", vars.append({{"$MTPROXY_TRANSPORT_MODE",
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) 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_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } }); vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
vars.append({ { "$TELEMT_SECRET", c.secret } }); vars.append({ { "$TELEMT_SECRET", c.secret } });
vars.append({ { "$TELEMT_REGENERATE_SECRET",
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0") } });
vars.append({ { "$TELEMT_TAG", c.tag } }); vars.append({ { "$TELEMT_TAG", c.tag } });
QString tlsDomain = c.tlsDomain; QString tlsDomain = c.tlsDomain;
if (tlsDomain.isEmpty()) { if (tlsDomain.isEmpty()) {
+6 -2
View File
@@ -8,6 +8,7 @@
#include <QFileInfo> #include <QFileInfo>
#include <QLocalSocket> #include <QLocalSocket>
#include "daemon.h"
#include "daemonlocalserverconnection.h" #include "daemonlocalserverconnection.h"
#include "leakdetector.h" #include "leakdetector.h"
#include "logger.h" #include "logger.h"
@@ -58,8 +59,11 @@ bool DaemonLocalServer::initialize() {
DaemonLocalServerConnection* connection = DaemonLocalServerConnection* connection =
new DaemonLocalServerConnection(&m_server, socket); new DaemonLocalServerConnection(&m_server, socket);
connect(socket, &QLocalSocket::disconnected, connection, connect(socket, &QLocalSocket::disconnected, connection, [connection]() {
&DaemonLocalServerConnection::deleteLater); logger.debug() << "Client connection dropped, deactivating daemon";
Daemon::instance()->deactivate(true);
connection->deleteLater();
});
}); });
return true; return true;
@@ -26,6 +26,8 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2" XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks" XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
) )
if(DEPLOY) if(DEPLOY)
@@ -114,10 +116,20 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
find_package(openvpnadapter REQUIRED) 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) target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
find_package(awg-apple REQUIRED) find_package(awg-apple REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::awg-apple) target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
find_package(hev-socks5-tunnel REQUIRED) 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) target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)
+6 -2
View File
@@ -1,13 +1,12 @@
#include <QDebug> #include <QDebug>
#include <QTimer> #include <QTimer>
#include <libssh/libssh.h>
#include "amneziaApplication.h" #include "amneziaApplication.h"
#include "core/utils/osSignalHandler.h" #include "core/utils/osSignalHandler.h"
#include "core/utils/migrations.h" #include "core/utils/migrations.h"
#include "version.h" #include "version.h"
#include <QTimer>
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
#include "Windows.h" #include "Windows.h"
#endif #endif
@@ -47,6 +46,11 @@ int main(int argc, char *argv[])
AmneziaApplication app(argc, argv); AmneziaApplication app(argc, argv);
OsSignalHandler::setup(); OsSignalHandler::setup();
ssh_init();
QObject::connect(&app, &QCoreApplication::aboutToQuit, []() {
ssh_finalize();
});
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
if (isAnotherInstanceRunning()) { if (isAnotherInstanceRunning()) {
QTimer::singleShot(1000, &app, [&]() { app.quit(); }); QTimer::singleShot(1000, &app, [&]() { app.quit(); });
@@ -9,7 +9,6 @@
#include "android_controller.h" #include "android_controller.h"
#include "android_utils.h" #include "android_utils.h"
#include "ui/controllers/importUiController.h" #include "ui/controllers/importUiController.h"
#include "ui/controllers/api/pairingUiController.h"
namespace namespace
{ {
@@ -104,10 +103,7 @@ bool AndroidController::initialize()
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)}, {"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}, {"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)}, {"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}, {"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
{"onCameraPermissionResult", "(Z)V", reinterpret_cast<void *>(onCameraPermissionResult)},
{"onPairingQrCameraClosed", "()V", reinterpret_cast<void *>(onPairingQrCameraClosed)},
{"onPairingQrCameraUserDismissed", "()V", reinterpret_cast<void *>(onPairingQrCameraUserDismissed)}
}; };
QJniEnvironment env; QJniEnvironment env;
@@ -205,21 +201,6 @@ bool AndroidController::isCameraPresent()
return callActivityMethod<jboolean>("isCameraPresent", "()Z"); return callActivityMethod<jboolean>("isCameraPresent", "()Z");
} }
bool AndroidController::isCameraPermissionGranted()
{
return callActivityMethod<jboolean>("isCameraPermissionGranted", "()Z");
}
void AndroidController::requestCameraPermissionForQrPairing()
{
callActivityMethod("requestCameraPermissionForQrPairing", "()V");
}
void AndroidController::openApplicationDetailsSettings()
{
callActivityMethod("openApplicationDetailsSettings", "()V");
}
bool AndroidController::isOnTv() bool AndroidController::isOnTv()
{ {
return callActivityMethod<jboolean>("isOnTv", "()Z"); return callActivityMethod<jboolean>("isOnTv", "()Z");
@@ -245,11 +226,6 @@ void AndroidController::startQrReaderActivity()
callActivityMethod("startQrCodeReader", "()V"); callActivityMethod("startQrCodeReader", "()V");
} }
void AndroidController::startPairingQrReaderActivity()
{
callActivityMethod("startPairingQrCodeReader", "()V");
}
void AndroidController::setSaveLogs(bool enabled) void AndroidController::setSaveLogs(bool enabled)
{ {
callActivityMethod("setSaveLogs", "(Z)V", enabled); callActivityMethod("setSaveLogs", "(Z)V", enabled);
@@ -562,11 +538,7 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
{ {
Q_UNUSED(thiz); Q_UNUSED(thiz);
const QString code = AndroidUtils::convertJString(env, data); return ImportUiController::decodeQrCode(AndroidUtils::convertJString(env, data));
if (PairingUiController::tryConsumeAndroidQrScan(code)) {
return true;
}
return ImportUiController::decodeQrCode(code);
} }
// static // static
void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp) void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp)
@@ -606,31 +578,4 @@ void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
emit AndroidController::instance()->activityResumed(); emit AndroidController::instance()->activityResumed();
} }
// static
void AndroidController::onCameraPermissionResult(JNIEnv *env, jobject thiz, jboolean granted)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->cameraPermissionResult(static_cast<bool>(granted));
}
// static
void AndroidController::onPairingQrCameraClosed(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
PairingUiController::notifyAndroidPairingQrCameraClosed();
}
// static
void AndroidController::onPairingQrCameraUserDismissed(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
PairingUiController::notifyAndroidPairingQrCameraUserDismissed();
}
@@ -38,15 +38,11 @@ public:
void closeFd(); void closeFd();
QString getFileName(const QString &uri); QString getFileName(const QString &uri);
bool isCameraPresent(); bool isCameraPresent();
bool isCameraPermissionGranted();
void requestCameraPermissionForQrPairing();
void openApplicationDetailsSettings();
bool isOnTv(); bool isOnTv();
bool isEdgeToEdgeEnabled(); bool isEdgeToEdgeEnabled();
int getStatusBarHeight(); int getStatusBarHeight();
int getNavigationBarHeight(); int getNavigationBarHeight();
void startQrReaderActivity(); void startQrReaderActivity();
void startPairingQrReaderActivity();
void setSaveLogs(bool enabled); void setSaveLogs(bool enabled);
void exportLogsFile(const QString &fileName); void exportLogsFile(const QString &fileName);
void clearLogs(); void clearLogs();
@@ -81,7 +77,6 @@ signals:
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp); void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused(); void activityPaused();
void activityResumed(); void activityResumed();
void cameraPermissionResult(bool granted);
private: private:
bool isWaitingStatus = true; bool isWaitingStatus = true;
@@ -114,9 +109,6 @@ private:
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp); static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz); static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz); static void onActivityResumed(JNIEnv *env, jobject thiz);
static void onCameraPermissionResult(JNIEnv *env, jobject thiz, jboolean granted);
static void onPairingQrCameraClosed(JNIEnv *env, jobject thiz);
static void onPairingQrCameraUserDismissed(JNIEnv *env, jobject thiz);
template <typename Ret, typename ...Args> template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
@@ -12,4 +12,3 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::startReading() {} void QRCodeReader::startReading() {}
void QRCodeReader::stopReading() {} void QRCodeReader::stopReading() {}
void QRCodeReader::setCameraSize(QRect) {} void QRCodeReader::setCameraSize(QRect) {}
void QRCodeReader::notifyCodeRead(const QString &) {}
-1
View File
@@ -16,7 +16,6 @@ public slots:
void startReading(); void startReading();
void stopReading(); void stopReading();
void setCameraSize(QRect value); void setCameraSize(QRect value);
void notifyCodeRead(const QString &code);
signals: signals:
void codeReaded(QString code); void codeReaded(QString code);
+36 -143
View File
@@ -1,56 +1,16 @@
#if !MACOS_NE #if !MACOS_NE
#include "QRCodeReaderBase.h" #include "QRCodeReaderBase.h"
#include <QByteArray>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
static UIWindow *amneziaKeyWindowForQrCamera(void)
{
UIApplication *app = [UIApplication sharedApplication];
if (@available(iOS 13.0, *)) {
for (UIScene *scene in app.connectedScenes) {
if (scene.activationState != UISceneActivationStateForegroundActive) {
continue;
}
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *window in windowScene.windows) {
if (window.isKeyWindow) {
return window;
}
}
for (UIWindow *window in windowScene.windows) {
if (!window.isHidden) {
return window;
}
}
}
}
if (app.keyWindow) {
return app.keyWindow;
}
for (UIWindow *window in app.windows) {
if (window.isKeyWindow) {
return window;
}
}
return app.windows.firstObject;
}
@interface QRCodeReaderImpl : UIViewController @interface QRCodeReaderImpl : UIViewController
@end @end
@interface QRCodeReaderImpl () <AVCaptureMetadataOutputObjectsDelegate> @interface QRCodeReaderImpl () <AVCaptureMetadataOutputObjectsDelegate>
@property (nonatomic, assign) QRCodeReader *qrCodeReader; @property (nonatomic) QRCodeReader* qrCodeReader;
@property (nonatomic, retain) AVCaptureSession *captureSession; @property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, retain) AVCaptureVideoPreviewLayer *videoPreviewPlayer; @property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
@property (nonatomic) dispatch_queue_t sessionQueue;
@end @end
@@ -59,115 +19,61 @@ static UIWindow *amneziaKeyWindowForQrCamera(void)
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
self.captureSession = nil; _captureSession = nil;
if (!_sessionQueue) {
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
}
} }
- (void)setQrCodeReader:(QRCodeReader *)value { - (void)setQrCodeReader: (QRCodeReader*)value {
_qrCodeReader = value; _qrCodeReader = value;
} }
- (BOOL)startReadingOnMainThread { - (BOOL)startReading {
[self stopReadingOnMainThread]; NSError *error;
NSError *error = nil; AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error];
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; if(!deviceInput) {
if (!captureDevice) { NSLog(@"Error %@", error.localizedDescription);
return NO; return NO;
} }
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error]; _captureSession = [[AVCaptureSession alloc]init];
[_captureSession addInput:deviceInput];
if (!deviceInput) {
return NO;
}
AVCaptureSession *session = [[AVCaptureSession alloc] init];
[session addInput:deviceInput];
AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init]; AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
[session addOutput:capturedMetadataOutput]; [_captureSession addOutput:capturedMetadataOutput];
if (!_sessionQueue) { dispatch_queue_t dispatchQueue;
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL); dispatchQueue = dispatch_queue_create("myQueue", NULL);
} [capturedMetadataOutput setMetadataObjectsDelegate: self queue: dispatchQueue];
[capturedMetadataOutput setMetadataObjectsDelegate:self queue:_sessionQueue]; [capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
[capturedMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
self.captureSession = session; _videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession];
[session release];
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession]; QRect cameraRect = _qrCodeReader->cameraSize();
[preview setVideoGravity:AVLayerVideoGravityResizeAspectFill]; CGRect cameraCGRect = CGRectMake(cameraRect.x(),
self.videoPreviewPlayer = preview; cameraRect.y() + statusBarHeight,
[preview release]; cameraRect.width(),
cameraRect.height());
UIWindow *keyWindow = amneziaKeyWindowForQrCamera(); [_videoPreviewPlayer setVideoGravity: AVLayerVideoGravityResizeAspectFill];
if (!keyWindow) { [_videoPreviewPlayer setFrame: cameraCGRect];
[self stopReadingOnMainThread];
return NO;
}
CGRect bounds = keyWindow.bounds; CALayer* layer = [UIApplication sharedApplication].keyWindow.layer;
[self.videoPreviewPlayer setFrame:bounds]; [layer addSublayer: _videoPreviewPlayer];
self.videoPreviewPlayer.zPosition = -1000.f;
[keyWindow.layer insertSublayer:self.videoPreviewPlayer atIndex:0];
AVCaptureSession *runningSession = self.captureSession; [_captureSession startRunning];
dispatch_async(_sessionQueue, ^{
[runningSession startRunning];
});
return YES; return YES;
} }
- (BOOL)startReading {
if ([NSThread isMainThread]) {
return [self startReadingOnMainThread];
}
__block BOOL ok = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
ok = [self startReadingOnMainThread];
});
return ok;
}
- (void)stopReadingOnMainThread {
AVCaptureSession *session = self.captureSession;
self.captureSession = nil;
if (session) {
if (!_sessionQueue) {
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
}
dispatch_sync(_sessionQueue, ^{
@try {
if ([session isRunning]) {
[session stopRunning];
}
} @catch (NSException *ex) {
NSLog(@"Session stopRunning exception: %@", ex);
}
});
}
if (self.videoPreviewPlayer) {
[self.videoPreviewPlayer removeFromSuperlayer];
self.videoPreviewPlayer = nil;
}
}
- (void)stopReading { - (void)stopReading {
if ([NSThread isMainThread]) { [_captureSession stopRunning];
[self stopReadingOnMainThread]; _captureSession = nil;
} else {
dispatch_sync(dispatch_get_main_queue(), ^{ [_videoPreviewPlayer removeFromSuperlayer];
[self stopReadingOnMainThread];
});
}
} }
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { - (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
@@ -176,15 +82,7 @@ static UIWindow *amneziaKeyWindowForQrCamera(void)
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0]; AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) { if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) {
NSString *value = [metadataObject stringValue]; _qrCodeReader->emit codeReaded([metadataObject stringValue].UTF8String);
if (value.length == 0) {
return;
}
QRCodeReader *cpp = _qrCodeReader;
const QByteArray utf8([value UTF8String]);
dispatch_async(dispatch_get_main_queue(), ^{
cpp->notifyCodeRead(QString::fromUtf8(utf8));
});
} }
} }
} }
@@ -211,10 +109,6 @@ void QRCodeReader::startReading() {
void QRCodeReader::stopReading() { void QRCodeReader::stopReading() {
[m_qrCodeReader stopReading]; [m_qrCodeReader stopReading];
} }
void QRCodeReader::notifyCodeRead(const QString &code) {
emit codeReaded(code);
}
#else #else
#include "QRCodeReaderBase.h" #include "QRCodeReaderBase.h"
@@ -230,5 +124,4 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::startReading() {} void QRCodeReader::startReading() {}
void QRCodeReader::stopReading() {} void QRCodeReader::stopReading() {}
void QRCodeReader::setCameraSize(QRect) {} void QRCodeReader::setCameraSize(QRect) {}
void QRCodeReader::notifyCodeRead(const QString &) {}
#endif #endif
@@ -1,10 +0,0 @@
#ifndef IOS_PAIRING_CAMERA_ACCESS_H
#define IOS_PAIRING_CAMERA_ACCESS_H
#include <functional>
bool amneziaIosPairingCameraAccessGranted();
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone);
void amneziaIosOpenApplicationSettings();
#endif
@@ -1,37 +0,0 @@
#include "platforms/ios/iosPairingCameraAccess.h"
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
bool amneziaIosPairingCameraAccessGranted()
{
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
return status == AVAuthorizationStatusAuthorized;
}
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
{
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (status == AVAuthorizationStatusAuthorized) {
onDone(true);
return;
}
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
onDone(false);
return;
}
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
onDone(static_cast<bool>(granted));
});
}];
}
void amneziaIosOpenApplicationSettings()
{
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
if (url != nil) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}
@@ -1,13 +0,0 @@
#include "platforms/ios/iosPairingCameraAccess.h"
bool amneziaIosPairingCameraAccessGranted()
{
return true;
}
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
{
onDone(true);
}
void amneziaIosOpenApplicationSettings() {}
@@ -1,16 +0,0 @@
#ifndef IOS_PAIRING_QR_OVERLAY_WINDOW_H
#define IOS_PAIRING_QR_OVERLAY_WINDOW_H
#include <functional>
#include <string>
using AmneziaPairingQrScannedUtf8Handler = std::function<void(const char *)>;
using AmneziaPairingQrOverlayBackHandler = std::function<void()>;
void amneziaIosPairingQrOverlayPresent(AmneziaPairingQrScannedUtf8Handler onScanned, AmneziaPairingQrOverlayBackHandler onBack,
const std::string &titleUtf8, const std::string &subtitleUtf8);
void amneziaIosPairingQrOverlayDismiss();
void amneziaIosPairingQrOverlaySetTorchEnabled(bool on);
void amneziaIosPairingQrOverlayRestartCapture();
#endif
@@ -1,836 +0,0 @@
#include "platforms/ios/iosPairingQrOverlayWindow.h"
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <QuartzCore/QuartzCore.h>
#import <math.h>
#include <string>
static const CGFloat kAmneziaPairingQrOverlayWindowLevel = (CGFloat)UIWindowLevelAlert + 1000.f;
static AmneziaPairingQrScannedUtf8Handler gOnScanned;
static AmneziaPairingQrOverlayBackHandler gOnBack;
static UIWindow *gPairingQrOverlayWindow = nil;
static bool gTorchRequested = false;
static CFAbsoluteTime gPairingQrOverlayKeySince = -1.0;
static UIWindowScene *amneziaForegroundWindowScene(void)
{
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive
&& [scene isKindOfClass:[UIWindowScene class]]) {
return (UIWindowScene *)scene;
}
}
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if ([scene isKindOfClass:[UIWindowScene class]]) {
return (UIWindowScene *)scene;
}
}
return nil;
}
static UIWindow *amneziaPickQtAppWindowToRestore(void)
{
UIWindow *best = nil;
for (UIWindow *cw in UIApplication.sharedApplication.windows) {
if (cw == gPairingQrOverlayWindow || cw.hidden) {
continue;
}
if (cw.windowScene && cw.windowLevel <= UIWindowLevelNormal + 1) {
if (!best || cw.isKeyWindow) {
best = cw;
}
}
}
return best;
}
static CGFloat amneziaPairingQrBottomTabStripReserve(UIWindowScene *scene)
{
Class qios = NSClassFromString(@"QIOSViewController");
if (!qios) {
return 83.f;
}
for (UIWindow *cw in scene.windows) {
if (!cw.rootViewController) {
continue;
}
if ([cw.rootViewController isKindOfClass:qios]) {
const CGFloat inset = cw.safeAreaInsets.bottom;
const CGFloat reserve = inset + 49.f;
return MIN(MAX(reserve, 72.f), 140.f);
}
}
return 83.f;
}
static void amneziaApplyReadableOverCameraShadow(UIView *v)
{
v.layer.shadowColor = [UIColor blackColor].CGColor;
v.layer.shadowOffset = CGSizeMake(0, 1);
v.layer.shadowRadius = 4;
v.layer.shadowOpacity = 0.9;
v.layer.masksToBounds = NO;
}
static UIColor *amneziaPaleGray(void)
{
return [UIColor colorWithRed:(CGFloat)0xD7 / 255.0 green:(CGFloat)0xD8 / 255.0 blue:(CGFloat)0xDB / 255.0 alpha:1.0];
}
static void amneziaAddCornerMinorArc(UIBezierPath *p, CGPoint C, CGFloat r, CGPoint S, CGPoint E)
{
const CGFloat as = atan2f((float)(S.y - C.y), (float)(S.x - C.x));
CGFloat ae = atan2f((float)(E.y - C.y), (float)(E.x - C.x));
while (ae - as > (CGFloat)M_PI) {
ae -= (CGFloat)(2.0 * M_PI);
}
while (ae - as < (CGFloat)(-M_PI)) {
ae += (CGFloat)(2.0 * M_PI);
}
const CGFloat minor = ae - as;
const BOOL cw = minor > 0;
[p addArcWithCenter:C radius:r startAngle:as endAngle:ae clockwise:cw];
}
static UIBezierPath *amneziaScanBracketStrokePath(int corner, CGFloat x0, CGFloat y0, CGFloat s, CGFloat R, CGFloat L, CGFloat t)
{
const CGFloat r = MAX(1.5, R - t * 0.5);
UIBezierPath *p = [UIBezierPath bezierPath];
const CGFloat yy = y0 + t * 0.5f;
const CGFloat yyb = y0 + s - t * 0.5f;
const CGFloat xx = x0 + t * 0.5f;
const CGFloat xxb = x0 + s - t * 0.5f;
switch (corner) {
case 0: {
const CGPoint cTL = CGPointMake(x0 + R, y0 + R);
const CGPoint sTL = CGPointMake(x0 + R, yy);
const CGPoint eTL = CGPointMake(xx, y0 + R);
[p moveToPoint:CGPointMake(x0 + R + L, yy)];
[p addLineToPoint:sTL];
amneziaAddCornerMinorArc(p, cTL, r, sTL, eTL);
const CGFloat yEndTL = MIN(y0 + R + L, y0 + s - R - t * 0.5f);
[p addLineToPoint:CGPointMake(xx, MAX(yEndTL, y0 + R + 2.f))];
} break;
case 1: {
const CGPoint cTR = CGPointMake(x0 + s - R, y0 + R);
const CGPoint sTR = CGPointMake(x0 + s - R, yy);
const CGPoint eTR = CGPointMake(xxb, y0 + R);
[p moveToPoint:CGPointMake(x0 + s - R - L, yy)];
[p addLineToPoint:sTR];
amneziaAddCornerMinorArc(p, cTR, r, sTR, eTR);
const CGFloat yEndTR = MIN(y0 + R + L, y0 + s - R - t * 0.5f);
[p addLineToPoint:CGPointMake(xxb, MAX(yEndTR, y0 + R + 2.f))];
} break;
case 2: {
const CGPoint cBL = CGPointMake(x0 + R, y0 + s - R);
const CGPoint sBL = CGPointMake(x0 + R, yyb);
const CGPoint eBL = CGPointMake(xx, y0 + s - R);
[p moveToPoint:CGPointMake(x0 + R + L, yyb)];
[p addLineToPoint:sBL];
amneziaAddCornerMinorArc(p, cBL, r, sBL, eBL);
const CGFloat yEndTopRef = MAX(MIN(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2.f);
const CGFloat yLegBL = y0 + s + y0 - yEndTopRef;
[p addLineToPoint:CGPointMake(xx, yLegBL)];
} break;
case 3: {
const CGPoint cBR = CGPointMake(x0 + s - R, y0 + s - R);
const CGPoint sBR = CGPointMake(x0 + s - R, yyb);
const CGPoint eBR = CGPointMake(xxb, y0 + s - R);
[p moveToPoint:CGPointMake(x0 + s - R - L, yyb)];
[p addLineToPoint:sBR];
amneziaAddCornerMinorArc(p, cBR, r, sBR, eBR);
const CGFloat yEndTopRef = MAX(MIN(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2.f);
const CGFloat yLegBR = y0 + s + y0 - yEndTopRef;
[p addLineToPoint:CGPointMake(xxb, yLegBR)];
} break;
default:
break;
}
return p;
}
@interface AmneziaPairingQrOverlayViewController : UIViewController
@end
@interface AmneziaPairingQrOverlayViewController () <AVCaptureMetadataOutputObjectsDelegate>
@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
@property (nonatomic, strong) AVCaptureDevice *videoDevice;
@property (nonatomic, strong) dispatch_queue_t sessionQueue;
@property (nonatomic, strong) UIView *cameraContainer;
@property (nonatomic, strong) UIView *headerContainer;
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *subtitleLabel;
@property (nonatomic, strong) UIButton *torchButton;
@property (nonatomic, strong) NSLayoutConstraint *torchCenterYConstraint;
@property (nonatomic, copy) NSString *chromeTitleText;
@property (nonatomic, copy) NSString *chromeSubtitleText;
@property (nonatomic, strong) UIView *scanDimView;
@property (nonatomic, strong) CAShapeLayer *scanDimMaskLayer;
@property (nonatomic, strong) UIView *scanHoleFillView;
@property (nonatomic, strong) CAShapeLayer *scanHoleHighlightLayer;
@property (nonatomic, strong) UIView *bracketContainer;
@property (nonatomic, strong) NSMutableArray<CAShapeLayer *> *bracketCornerLayers;
@end
@implementation AmneziaPairingQrOverlayViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
if (!self.sessionQueue) {
self.sessionQueue = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
}
[self buildChromeUi];
}
- (void)buildChromeUi
{
if (self.headerContainer) {
return;
}
UIView *cam = [[UIView alloc] init];
cam.translatesAutoresizingMaskIntoConstraints = NO;
cam.backgroundColor = [UIColor clearColor];
cam.clipsToBounds = YES;
self.cameraContainer = cam;
[self.view addSubview:cam];
UIView *holeFill = [[UIView alloc] init];
holeFill.translatesAutoresizingMaskIntoConstraints = NO;
holeFill.backgroundColor = [UIColor clearColor];
holeFill.opaque = NO;
holeFill.userInteractionEnabled = NO;
self.scanHoleFillView = holeFill;
CAShapeLayer *hi = [CAShapeLayer layer];
hi.fillColor = [UIColor colorWithWhite:1.0 alpha:0.14].CGColor;
hi.strokeColor = nil;
[holeFill.layer addSublayer:hi];
self.scanHoleHighlightLayer = hi;
[self.view addSubview:holeFill];
UIView *dim = [[UIView alloc] init];
dim.translatesAutoresizingMaskIntoConstraints = NO;
dim.backgroundColor = [UIColor colorWithWhite:0.02 alpha:0.55];
dim.userInteractionEnabled = NO;
dim.opaque = NO;
self.scanDimView = dim;
[self.view addSubview:dim];
CAShapeLayer *dimMask = [CAShapeLayer layer];
dimMask.fillRule = kCAFillRuleEvenOdd;
dimMask.fillColor = [UIColor blackColor].CGColor;
dim.layer.mask = dimMask;
self.scanDimMaskLayer = dimMask;
UIView *bracketHost = [[UIView alloc] init];
bracketHost.translatesAutoresizingMaskIntoConstraints = NO;
bracketHost.backgroundColor = [UIColor clearColor];
bracketHost.opaque = NO;
bracketHost.userInteractionEnabled = NO;
self.bracketContainer = bracketHost;
[self.view addSubview:bracketHost];
self.bracketCornerLayers = [NSMutableArray arrayWithCapacity:4];
for (NSInteger i = 0; i < 4; i++) {
CAShapeLayer *sl = [CAShapeLayer layer];
sl.fillColor = nil;
sl.strokeColor = [UIColor colorWithWhite:0.94 alpha:1].CGColor;
sl.lineWidth = 5.0;
sl.lineCap = kCALineCapRound;
sl.lineJoin = kCALineJoinRound;
[bracketHost.layer addSublayer:sl];
[self.bracketCornerLayers addObject:sl];
}
UIView *header = [[UIView alloc] init];
header.translatesAutoresizingMaskIntoConstraints = NO;
header.backgroundColor = [UIColor clearColor];
header.opaque = NO;
header.userInteractionEnabled = YES;
self.headerContainer = header;
[self.view addSubview:header];
UIButton *back = [UIButton buttonWithType:UIButtonTypeSystem];
back.translatesAutoresizingMaskIntoConstraints = NO;
back.tintColor = amneziaPaleGray();
if (@available(iOS 13.0, *)) {
const CGFloat kBackArrowPt = 20.0;
UIImageSymbolConfiguration *sym =
[UIImageSymbolConfiguration configurationWithPointSize:kBackArrowPt weight:UIImageSymbolWeightMedium
scale:UIImageSymbolScaleDefault];
UIImage *img = [UIImage systemImageNamed:@"arrow.left" withConfiguration:sym];
[back setImage:[img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
} else {
[back setTitle:@"<" forState:UIControlStateNormal];
}
[back addTarget:self action:@selector(backTapped) forControlEvents:UIControlEventTouchUpInside];
self.backButton = back;
[header addSubview:back];
UILabel *title = [[UILabel alloc] init];
title.translatesAutoresizingMaskIntoConstraints = NO;
title.textColor = [UIColor colorWithWhite:0.96 alpha:1];
title.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
title.numberOfLines = 0;
title.text = self.chromeTitleText.length ? self.chromeTitleText : @"Add device via QR";
self.titleLabel = title;
[header addSubview:title];
amneziaApplyReadableOverCameraShadow(title);
UILabel *sub = [[UILabel alloc] init];
sub.translatesAutoresizingMaskIntoConstraints = NO;
sub.textColor = [UIColor colorWithWhite:0.88 alpha:0.95];
sub.font = [UIFont systemFontOfSize:14 weight:UIFontWeightRegular];
sub.numberOfLines = 0;
sub.text = self.chromeSubtitleText.length
? self.chromeSubtitleText
: @"Scan the session QR shown on the device you want to add.";
self.subtitleLabel = sub;
[header addSubview:sub];
amneziaApplyReadableOverCameraShadow(sub);
UIButton *torch = [UIButton buttonWithType:UIButtonTypeSystem];
torch.translatesAutoresizingMaskIntoConstraints = NO;
[torch setTitle:@"🔦" forState:UIControlStateNormal];
torch.titleLabel.font = [UIFont systemFontOfSize:26];
torch.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.22];
torch.layer.cornerRadius = 28;
torch.clipsToBounds = YES;
[torch addTarget:self action:@selector(torchTapped) forControlEvents:UIControlEventTouchUpInside];
self.torchButton = torch;
[self.view addSubview:torch];
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[cam.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[cam.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[cam.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[cam.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[holeFill.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[holeFill.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[holeFill.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[holeFill.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[dim.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[dim.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[dim.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[dim.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[bracketHost.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[bracketHost.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[bracketHost.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[bracketHost.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[header.topAnchor constraintEqualToAnchor:safe.topAnchor],
[header.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[header.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[header.heightAnchor constraintGreaterThanOrEqualToConstant:120],
[back.leadingAnchor constraintEqualToAnchor:header.leadingAnchor constant:8],
[back.topAnchor constraintEqualToAnchor:header.topAnchor constant:20],
[back.widthAnchor constraintEqualToConstant:40],
[back.heightAnchor constraintEqualToConstant:40],
[title.leadingAnchor constraintEqualToAnchor:header.leadingAnchor constant:16],
[title.trailingAnchor constraintEqualToAnchor:header.trailingAnchor constant:-16],
[title.topAnchor constraintEqualToAnchor:back.bottomAnchor],
[sub.leadingAnchor constraintEqualToAnchor:title.leadingAnchor],
[sub.trailingAnchor constraintEqualToAnchor:title.trailingAnchor],
[sub.topAnchor constraintEqualToAnchor:title.bottomAnchor constant:8],
[sub.bottomAnchor constraintEqualToAnchor:header.bottomAnchor constant:-10],
[torch.topAnchor constraintGreaterThanOrEqualToAnchor:header.bottomAnchor constant:8],
[torch.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[torch.widthAnchor constraintEqualToConstant:56],
[torch.heightAnchor constraintEqualToConstant:56],
]];
NSLayoutConstraint *torchCy = [torch.centerYAnchor constraintEqualToAnchor:self.view.topAnchor constant:200.0];
self.torchCenterYConstraint = torchCy;
torchCy.active = YES;
[header setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[header setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
}
- (void)applyMetadataRectOfInterestForScanHole:(CGRect)holeInScanDimBounds
{
if (!self.previewLayer || !self.metadataOutput || !self.scanDimView || !self.cameraContainer) {
return;
}
if (CGRectIsEmpty(holeInScanDimBounds) || holeInScanDimBounds.size.width < 24.0 || holeInScanDimBounds.size.height < 24.0) {
return;
}
CGRect holeInCam = [self.scanDimView convertRect:holeInScanDimBounds toView:self.cameraContainer];
holeInCam = CGRectIntersection(holeInCam, self.cameraContainer.bounds);
if (CGRectIsEmpty(holeInCam)) {
return;
}
const CGRect plFrame = self.previewLayer.frame;
CGRect holeInPreview = CGRectOffset(holeInCam, -plFrame.origin.x, -plFrame.origin.y);
holeInPreview = CGRectIntersection(holeInPreview, self.previewLayer.bounds);
if (CGRectIsEmpty(holeInPreview)) {
return;
}
CGRect roi = [self.previewLayer metadataOutputRectOfInterestForRect:holeInPreview];
roi.origin.x = MAX(0.0, MIN(1.0, roi.origin.x));
roi.origin.y = MAX(0.0, MIN(1.0, roi.origin.y));
roi.size.width = MAX(0.02, MIN(1.0 - roi.origin.x, roi.size.width));
roi.size.height = MAX(0.02, MIN(1.0 - roi.origin.y, roi.size.height));
AVCaptureMetadataOutput *mo = self.metadataOutput;
dispatch_queue_t sq = self.sessionQueue;
if (!mo || !sq) {
return;
}
dispatch_async(sq, ^{
mo.rectOfInterest = roi;
});
}
- (void)layoutScanOverlayGeometry
{
if (!self.scanDimView || !self.scanDimMaskLayer || !self.scanHoleHighlightLayer || self.bracketCornerLayers.count != 4) {
return;
}
const CGRect vb = self.scanDimView.bounds;
if (vb.size.width < 32 || vb.size.height < 32) {
return;
}
CGFloat sqSz = floor(MIN(vb.size.width, vb.size.height) * 0.72);
CGFloat sqX = (vb.size.width - sqSz) / 2.0;
CGFloat sqY = (vb.size.height - sqSz) / 2.0;
CGFloat headerBottom = CGRectGetMaxY(self.headerContainer.frame);
if (headerBottom < 8.0) {
headerBottom = 132.0 + self.view.safeAreaInsets.top;
}
sqY = MAX(sqY, headerBottom + 8.0);
const CGFloat kBottomBandForTorch = 80.0;
const CGFloat maxHoleBottom = vb.size.height - kBottomBandForTorch;
if (sqY + sqSz > maxHoleBottom) {
sqY = maxHoleBottom - sqSz;
sqY = MAX(sqY, headerBottom + 8.0);
}
sqX = MAX(8.0, MIN(sqX, vb.size.width - sqSz - 8.0));
sqY = MAX(headerBottom + 4.0, MIN(sqY, vb.size.height - sqSz - 8.0));
const CGRect hole = CGRectMake(sqX, sqY, sqSz, sqSz);
CGFloat holeR = MIN(28.0, MAX(10.0, sqSz * 0.056));
{
const CGFloat half = 0.5 * MIN(hole.size.width, hole.size.height);
holeR = MIN(holeR, MAX(6.0, half - 2.0));
}
UIBezierPath *holeRoundPath = [UIBezierPath bezierPathWithRoundedRect:hole cornerRadius:holeR];
UIBezierPath *path = [UIBezierPath bezierPathWithRect:vb];
[path appendPath:holeRoundPath];
self.scanDimMaskLayer.frame = vb;
self.scanDimMaskLayer.path = path.CGPath;
self.scanHoleHighlightLayer.frame = CGRectMake(0, 0, vb.size.width, vb.size.height);
self.scanHoleHighlightLayer.path = holeRoundPath.CGPath;
const CGFloat bracketThick = 5.0;
const CGFloat bracketLen = (CGFloat)MAX(28, (NSInteger)floor(sqSz * 0.13));
const CGFloat x0 = hole.origin.x;
const CGFloat y0 = hole.origin.y;
const CGFloat s = hole.size.width;
const CGFloat t = bracketThick;
const CGFloat L = bracketLen;
for (NSUInteger i = 0; i < 4; i++) {
CAShapeLayer *layer = self.bracketCornerLayers[i];
layer.lineWidth = t;
layer.path = amneziaScanBracketStrokePath((int)i, x0, y0, s, holeR, L, t).CGPath;
}
if (self.torchCenterYConstraint && self.torchButton) {
const CGFloat holeBottom = CGRectGetMaxY(hole);
const CGFloat bandBottom = vb.size.height;
const CGFloat torchH = 56.0;
CGFloat torchCenterY = (holeBottom + bandBottom) * 0.5;
const CGFloat minC = holeBottom + torchH * 0.5 + 6.0;
const CGFloat maxC = bandBottom - torchH * 0.5 - MAX(6.0, self.view.safeAreaInsets.bottom);
torchCenterY = MAX(minC, MIN(maxC, torchCenterY));
if (minC > maxC) {
torchCenterY = (minC + maxC) * 0.5;
}
const CGFloat hdr = headerBottom + torchH * 0.5 + 10.0;
torchCenterY = MAX(torchCenterY, hdr);
self.torchCenterYConstraint.constant = torchCenterY;
}
[self applyMetadataRectOfInterestForScanHole:hole];
}
- (void)backTapped
{
if (gOnBack) {
gOnBack();
}
}
- (void)torchTapped
{
gTorchRequested = !gTorchRequested;
[self applyTorchFromGlobalFlag];
if (gTorchRequested) {
self.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.42];
self.torchButton.layer.borderWidth = 2;
self.torchButton.layer.borderColor = [UIColor colorWithRed:1 green:0.75 blue:0.45 alpha:1].CGColor;
} else {
self.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.22];
self.torchButton.layer.borderWidth = 0;
}
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (self.previewLayer && self.cameraContainer) {
self.previewLayer.frame = self.cameraContainer.bounds;
}
[self layoutScanOverlayGeometry];
if (self.scanHoleFillView) {
[self.view bringSubviewToFront:self.scanHoleFillView];
}
if (self.scanDimView) {
[self.view bringSubviewToFront:self.scanDimView];
}
if (self.bracketContainer) {
[self.view bringSubviewToFront:self.bracketContainer];
}
if (self.headerContainer) {
[self.view bringSubviewToFront:self.headerContainer];
}
if (self.torchButton) {
[self.view bringSubviewToFront:self.torchButton];
}
}
- (void)applyTorchOnMainThread:(BOOL)on
{
AVCaptureDevice *device = self.videoDevice;
if (!device || ![device hasTorch]) {
if (on && gTorchRequested) {
__unsafe_unretained AmneziaPairingQrOverlayViewController *unsafeSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.12 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
AmneziaPairingQrOverlayViewController *strongSelf = unsafeSelf;
if (strongSelf && gTorchRequested) {
[strongSelf applyTorchOnMainThread:YES];
}
});
}
return;
}
AVCaptureSession *session = self.captureSession;
if (on && session && ![session isRunning]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (gTorchRequested) {
[self applyTorchOnMainThread:YES];
}
});
return;
}
NSError *err = nil;
if (![device lockForConfiguration:&err]) {
return;
}
if (on) {
err = nil;
if (![device setTorchModeOnWithLevel:AVCaptureMaxAvailableTorchLevel error:&err]) {
if ([device isTorchModeSupported:AVCaptureTorchModeOn]) {
device.torchMode = AVCaptureTorchModeOn;
}
}
} else {
device.torchMode = AVCaptureTorchModeOff;
}
[device unlockForConfiguration];
}
- (void)applyTorchFromGlobalFlag
{
[self applyTorchOnMainThread:gTorchRequested ? YES : NO];
}
- (void)stopCapturePipelineOnMainThread
{
[self applyTorchOnMainThread:NO];
self.videoDevice = nil;
AVCaptureSession *session = self.captureSession;
self.captureSession = nil;
self.metadataOutput = nil;
if (self.previewLayer) {
[self.previewLayer removeFromSuperlayer];
self.previewLayer = nil;
}
if (session) {
dispatch_queue_t q = self.sessionQueue;
if (!q) {
q = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
self.sessionQueue = q;
}
dispatch_sync(q, ^{
@try {
if ([session isRunning]) {
[session stopRunning];
}
} @catch (NSException *ex) {
NSLog(@"Stop running exception: %@", ex);
}
});
}
}
- (BOOL)startCapturePipelineOnMainThread
{
[self stopCapturePipelineOnMainThread];
if (!self.cameraContainer) {
return NO;
}
NSError *error = nil;
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if (!device) {
return NO;
}
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input) {
return NO;
}
self.videoDevice = device;
AVCaptureSession *session = [[AVCaptureSession alloc] init];
if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
session.sessionPreset = AVCaptureSessionPresetHigh;
}
[session addInput:input];
AVCaptureMetadataOutput *meta = [[AVCaptureMetadataOutput alloc] init];
if (![session canAddOutput:meta]) {
return NO;
}
[session addOutput:meta];
dispatch_queue_t q = self.sessionQueue;
if (!q) {
q = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
self.sessionQueue = q;
}
[meta setMetadataObjectsDelegate:self queue:q];
meta.metadataObjectTypes = @[ AVMetadataObjectTypeQRCode ];
self.captureSession = session;
self.metadataOutput = meta;
AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
preview.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.previewLayer = preview;
[self.cameraContainer.layer insertSublayer:preview atIndex:0];
preview.frame = self.cameraContainer.bounds;
[self.view layoutIfNeeded];
[self layoutScanOverlayGeometry];
AVCaptureSession *runningSession = session;
__unsafe_unretained AmneziaPairingQrOverlayViewController *weakSelf = self;
dispatch_async(q, ^{
@try {
[runningSession startRunning];
} @catch (NSException *ex) {
NSLog(@"Start running exception: %@", ex);
}
dispatch_async(dispatch_get_main_queue(), ^{
AmneziaPairingQrOverlayViewController *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf applyTorchFromGlobalFlag];
});
});
return YES;
}
- (void)captureOutput:(AVCaptureOutput *)output
didOutputMetadataObjects:(NSArray<__kindof AVMetadataMachineReadableCodeObject *> *)metadataObjects
fromConnection:(AVCaptureConnection *)connection
{
(void)output;
(void)connection;
for (AVMetadataMachineReadableCodeObject *obj in metadataObjects) {
NSString *value = obj.stringValue;
if (value.length == 0) {
continue;
}
const char *utf8 = value.UTF8String;
std::string copy(utf8 ? utf8 : "");
if (copy.empty()) {
continue;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (gOnScanned) {
gOnScanned(copy.c_str());
}
});
break;
}
}
@end
static void amneziaPairingQrOverlayTeardownOnMain(void)
{
UIWindow *w = gPairingQrOverlayWindow;
gPairingQrOverlayWindow = nil;
gOnScanned = nullptr;
gOnBack = nullptr;
gTorchRequested = false;
gPairingQrOverlayKeySince = -1.0;
if (w) {
UIViewController *root = w.rootViewController;
w.rootViewController = nil;
w.hidden = YES;
if ([root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
[(AmneziaPairingQrOverlayViewController *)root stopCapturePipelineOnMainThread];
}
}
UIWindow *restore = amneziaPickQtAppWindowToRestore();
if (restore) {
[restore makeKeyWindow];
} else {
}
}
void amneziaIosPairingQrOverlayPresent(AmneziaPairingQrScannedUtf8Handler onScanned, AmneziaPairingQrOverlayBackHandler onBack,
const std::string &titleUtf8, const std::string &subtitleUtf8)
{
const bool hasScan = static_cast<bool>(onScanned);
const bool hasBack = static_cast<bool>(onBack);
AmneziaPairingQrScannedUtf8Handler scanH = std::move(onScanned);
AmneziaPairingQrOverlayBackHandler backH = std::move(onBack);
const std::string titleCopy = titleUtf8;
const std::string subCopy = subtitleUtf8;
dispatch_async(dispatch_get_main_queue(), ^{
amneziaPairingQrOverlayTeardownOnMain();
gOnScanned = std::move(scanH);
gOnBack = std::move(backH);
UIWindowScene *scene = amneziaForegroundWindowScene();
if (!scene) {
gOnScanned = nullptr;
gOnBack = nullptr;
return;
}
const CGFloat bottomReserve = amneziaPairingQrBottomTabStripReserve(scene);
const CGRect sceneBounds = scene.coordinateSpace.bounds;
const CGRect overlayFrame = CGRectMake(0, 0, sceneBounds.size.width, sceneBounds.size.height - bottomReserve);
AmneziaPairingQrOverlayViewController *vc = [[AmneziaPairingQrOverlayViewController alloc] init];
NSString *nsTitle = titleCopy.empty() ? nil : [NSString stringWithUTF8String:titleCopy.c_str()];
NSString *nsSub = subCopy.empty() ? nil : [NSString stringWithUTF8String:subCopy.c_str()];
vc.chromeTitleText = nsTitle;
vc.chromeSubtitleText = nsSub;
UIWindow *w = [[UIWindow alloc] initWithWindowScene:scene];
w.frame = overlayFrame;
w.windowLevel = kAmneziaPairingQrOverlayWindowLevel;
w.backgroundColor = [UIColor blackColor];
w.rootViewController = vc;
gPairingQrOverlayWindow = w;
[w makeKeyAndVisible];
[w layoutIfNeeded];
[vc.view setNeedsLayout];
[vc.view layoutIfNeeded];
gPairingQrOverlayKeySince = CFAbsoluteTimeGetCurrent();
if (![vc startCapturePipelineOnMainThread]) {
NSLog(@"Start capture failed");
}
});
}
void amneziaIosPairingQrOverlayDismiss()
{
dispatch_async(dispatch_get_main_queue(), ^{
amneziaPairingQrOverlayTeardownOnMain();
});
}
void amneziaIosPairingQrOverlaySetTorchEnabled(bool on)
{
gTorchRequested = on;
dispatch_async(dispatch_get_main_queue(), ^{
UIWindow *win = gPairingQrOverlayWindow;
if (!win) {
return;
}
UIViewController *root = win.rootViewController;
if ([root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
AmneziaPairingQrOverlayViewController *vc = (AmneziaPairingQrOverlayViewController *)root;
[vc applyTorchFromGlobalFlag];
if (vc.torchButton) {
if (on) {
vc.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.42];
vc.torchButton.layer.borderWidth = 2;
vc.torchButton.layer.borderColor = [UIColor colorWithRed:1 green:0.75 blue:0.45 alpha:1].CGColor;
} else {
vc.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.18];
vc.torchButton.layer.borderWidth = 0;
}
}
}
});
}
void amneziaIosPairingQrOverlayRestartCapture()
{
dispatch_async(dispatch_get_main_queue(), ^{
const CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
if (gPairingQrOverlayKeySince > 0 && (now - gPairingQrOverlayKeySince) < 1.0) {
return;
}
UIWindow *w = gPairingQrOverlayWindow;
if (!w) {
return;
}
UIViewController *root = w.rootViewController;
if (![root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
return;
}
AmneziaPairingQrOverlayViewController *vc = (AmneziaPairingQrOverlayViewController *)root;
[vc stopCapturePipelineOnMainThread];
if (![vc startCapturePipelineOnMainThread]) {
NSLog(@"Restart startCapture failed");
}
});
}
@@ -4,8 +4,10 @@
curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret
curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
# Determine secret: env var -> saved file -> generate new # Determine secret: regenerate (fresh install) -> env var -> saved file -> generate new
if [ -n "$MTPROXY_SECRET" ]; then if [ "$MTPROXY_REGENERATE_SECRET" = "1" ]; then
SECRET=$(openssl rand -hex 16)
elif [ -n "$MTPROXY_SECRET" ]; then
SECRET="$MTPROXY_SECRET" SECRET="$MTPROXY_SECRET"
elif [ -f /data/secret ]; then elif [ -f /data/secret ]; then
SECRET=$(cat /data/secret) SECRET=$(cat /data/secret)
@@ -1,5 +1,6 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\ sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\ sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\ sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\
sudo docker volume ls | grep amnezia | awk '{print $2}' | xargs sudo docker volume rm -f;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\ sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia sudo rm -frd /opt/amnezia
+1 -1
View File
@@ -1,3 +1,3 @@
sudo docker stop $CONTAINER_NAME;\ sudo docker stop $CONTAINER_NAME;\
sudo docker rm -fv $CONTAINER_NAME;\ sudo docker rm -fv $CONTAINER_NAME;\
sudo docker rmi $CONTAINER_NAME sudo docker rmi $CONTAINER_NAME;
@@ -4,8 +4,10 @@
echo "[*] Amnezia Telemt: configure script start" echo "[*] Amnezia Telemt: configure script start"
mkdir -p /data/tlsfront mkdir -p /data/tlsfront
# Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure) # Secret: regenerate (fresh install) -> env var -> saved file -> openssl
if [ -n "$TELEMT_SECRET" ]; then if [ "$TELEMT_REGENERATE_SECRET" = "1" ]; then
SECRET=$(openssl rand -hex 16)
elif [ -n "$TELEMT_SECRET" ]; then
SECRET="$TELEMT_SECRET" SECRET="$TELEMT_SECRET"
elif [ -f /data/secret ]; then elif [ -f /data/secret ]; then
SECRET=$(cat /data/secret) SECRET=$(cat /data/secret)
-155
View File
@@ -1,155 +0,0 @@
cmake_minimum_required(VERSION 3.25.0)
project(AmneziaVPN_Tests)
find_package(Qt6 REQUIRED COMPONENTS Test)
set(CMAKE_AUTORCC ON)
qt6_add_resources(TEST_QRC
${CLIENT_ROOT_DIR}/server_scripts/serverScripts.qrc
)
add_library(test_common OBJECT
${SOURCES}
${HEADERS}
${TEST_QRC}
)
qt_add_repc_replicas(test_common
${CLIENT_ROOT_DIR}/../ipc/ipc_interface.rep
${CLIENT_ROOT_DIR}/../ipc/ipc_process_interface.rep
)
target_link_libraries(test_common PUBLIC
${LIBS}
)
target_include_directories(test_common PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_BINARY_DIR}/..
${CMAKE_CURRENT_BINARY_DIR}
)
add_executable(test_import_export
testAdminSelfHostedExport.cpp
)
target_link_libraries(test_import_export PRIVATE
Qt6::Test
test_common
)
add_executable(test_multiple_imports
testMultipleImports.cpp
)
target_link_libraries(test_multiple_imports PRIVATE
Qt6::Test
test_common
)
add_executable(test_server_edit
testServerEdit.cpp
)
target_link_libraries(test_server_edit PRIVATE
Qt6::Test
test_common
)
add_executable(test_default_server_change
testDefaultServerChange.cpp
)
target_link_libraries(test_default_server_change PRIVATE
Qt6::Test
test_common
)
add_executable(test_server_edge_cases
testServerEdgeCases.cpp
)
target_link_libraries(test_server_edge_cases PRIVATE
Qt6::Test
test_common
)
add_executable(test_signal_order
testSignalOrder.cpp
)
target_link_libraries(test_signal_order PRIVATE
Qt6::Test
test_common
)
add_executable(test_servers_model_sync
testServersModelSync.cpp
)
target_link_libraries(test_servers_model_sync PRIVATE
Qt6::Test
test_common
)
add_executable(test_complex_operations
testComplexOperations.cpp
)
target_link_libraries(test_complex_operations PRIVATE
Qt6::Test
test_common
)
add_executable(test_settings_signals
testSettingsSignals.cpp
)
target_link_libraries(test_settings_signals PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_servers_model_and_controller
testUiServersModelAndController.cpp
)
target_link_libraries(test_ui_servers_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_self_hosted_server_setup
testSelfHostedServerSetup.cpp
)
target_link_libraries(test_self_hosted_server_setup PRIVATE
Qt6::Test
test_common
)
add_executable(test_pairing_parsers
testPairingParsers.cpp
)
target_link_libraries(test_pairing_parsers PRIVATE
Qt6::Test
test_common
)
enable_testing()
add_test(NAME ImportExportTest COMMAND test_import_export)
add_test(NAME MultipleImportsTest COMMAND test_multiple_imports)
add_test(NAME ServerEditTest COMMAND test_server_edit)
add_test(NAME DefaultServerChangeTest COMMAND test_default_server_change)
add_test(NAME ServerEdgeCasesTest COMMAND test_server_edge_cases)
add_test(NAME SignalOrderTest COMMAND test_signal_order)
add_test(NAME ServersModelSyncTest COMMAND test_servers_model_sync)
add_test(NAME ComplexOperationsTest COMMAND test_complex_operations)
add_test(NAME SettingsSignalsTest COMMAND test_settings_signals)
add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller)
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
add_test(NAME PairingParsersTest COMMAND test_pairing_parsers)
-147
View File
@@ -1,147 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/utils/constants/configKeys.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
class TestAdminSelfHostedExport : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
QJsonObject decodeVpnKey(const QString &vpnKey) {
QString key = vpnKey;
key.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(
key.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals
);
qDebug() << "Base64 decoded size:" << ba.size();
QJsonDocument testDoc = QJsonDocument::fromJson(ba);
if (!testDoc.isNull()) {
qDebug() << "Data is not compressed, using as-is";
return testDoc.object();
}
QByteArray baUncompressed = qUncompress(ba);
if (!baUncompressed.isEmpty()) {
qDebug() << "Data was compressed, uncompressed size:" << baUncompressed.size();
ba = baUncompressed;
} else {
qDebug() << "qUncompress failed or data is not compressed";
}
return QJsonDocument::fromJson(ba).object();
}
QJsonObject sortContainers(const QJsonObject &config) {
QJsonObject sorted = config;
if (!config.contains("containers")) {
return sorted;
}
QJsonArray containers = config["containers"].toArray();
QVector<QJsonObject> containerVec;
for (const QJsonValue &val : containers) {
containerVec.append(val.toObject());
}
std::sort(containerVec.begin(), containerVec.end(), [](const QJsonObject &a, const QJsonObject &b) {
return a["container"].toString() < b["container"].toString();
});
QJsonArray sortedContainers;
for (const QJsonObject &obj : containerVec) {
sortedContainers.append(obj);
}
sorted["containers"] = sortedContainers;
return sorted;
}
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
}
void testAdminSelfHostedExport() {
QString vpnKey = "vpn://AAABTXjarZIxT8MwEIX_Cro5jbDjQunKUhhYyoZQZZKjRGpsy3baQtT_zp2bJh3oACLLPfvz3bOe00FpTdS1QR9g_tKB3q1h3sFCwBzEdf9N5ElBBgtJqBiQOkcFoemAbs6RInQ7oNkZemAvrrKvRV9VX6fH-lhSVSwavU9GSdcmXZX0UqSbseJRMqlioDxuSsJZH1mKWTrhvI22tJvVljKoLU-TtB3aN4NxpavKYwhpSD7LRc4t0WsTeMwqNRNsKweHbAyTtnRj8KvWE0pUEut-hNah2TpDM0-Kwu8vKMSd-ttFLrntao_rVvuKWkc9OnIk4n8t915_Ulcqo5FSxa9tYsk2rxlU-K7bTby_lDWfCKWvXTy-5jOGeLVET-9L7MOG-KQbJEBx57jXjdtgXtqG_wUdws5yJhCpa1iefhopM2gD-n4An-ElHL4BvzD6nw";
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
qDebug() << "IMPORTED KEY:" << vpnKey;
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(vpnKey);
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import should succeed");
QVERIFY2(!importResult.config.isEmpty(), "Config should not be empty");
QJsonObject importedConfig = importResult.config;
m_coreController->m_importCoreController->importConfig(importedConfig);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
const QString serverId = m_coreController->m_serversRepository->defaultServerId();
auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverId);
QVERIFY2(exportResult.errorCode == ErrorCode::NoError, "Export should succeed");
QVERIFY2(!exportResult.config.isEmpty(), "Exported config should not be empty");
qDebug() << "EXPORTED KEY:" << exportResult.config;
QJsonObject exportedConfig = decodeVpnKey(exportResult.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(exportResult.config);
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Re-import should succeed");
QJsonObject sortedImported = sortContainers(importedConfig);
QJsonObject sortedExported = sortContainers(importResult2.config);
QString importedJson = QJsonDocument(sortedImported).toJson(QJsonDocument::Compact);
QString exportedJson = QJsonDocument(sortedExported).toJson(QJsonDocument::Compact);
qDebug() << "IMPORTED JSON:" << importedJson;
qDebug() << "EXPORTED JSON:" << exportedJson;
QCOMPARE(exportedJson, importedJson);
}
};
QTEST_MAIN(TestAdminSelfHostedExport)
#include "testAdminSelfHostedExport.moc"
-111
View File
@@ -1,111 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "tests/testServerRepositoryHelpers.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestComplexOperations : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testComplexOperationSequence() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(importFinishedSpy.count() == 3, "importFinished should be emitted 3 times");
QVERIFY2(serverAddedSpy.count() == 3, "serverAdded should be emitted 3 times");
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged should be emitted 2 times (0->1, 1->2, first import doesn't emit as default is already 0)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
m_coreController->m_serversController->getServerId(0),
QStringLiteral("Edited First Server"));
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited should be emitted");
QString editedDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(editedDesc0 == "Edited First Server", "First server should be edited");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 1)");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 4, "defaultServerChanged should be emitted again");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0");
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
m_coreController->m_serversController->getServerId(0),
QStringLiteral("Final Edited Server"));
QVERIFY2(serverEditedSpy.count() == 2, "serverEdited should be emitted again");
QString finalDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(finalDesc0 == "Final Edited Server", "First server should be edited again");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Final servers count should be 2");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Final default index should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 2, "Model should have 2 rows");
QString modelDesc0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc0 == "Final Edited Server", "Model should reflect final edited name");
}
}
};
QTEST_MAIN(TestComplexOperations)
#include "testComplexOperations.moc"
-128
View File
@@ -1,128 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "tests/testServerRepositoryHelpers.h"
#include "ui/models/serversModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestDefaultServerChange : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testSetDefaultServerIndex() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted");
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversController->getServerId(0),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
if (m_coreController->m_serversModel) {
int modelDefaultIndex = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool() ? 0 : -1;
QVERIFY2(modelDefaultIndex == 0, "Model should reflect default server");
}
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(2));
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted again");
QVERIFY2(defaultServerChangedSpy.at(1).at(0).toString() == m_coreController->m_serversController->getServerId(2),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default server index should be 2");
}
void testDefaultServerChangeOnRemoveEdgeCases() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 0)");
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(1));
QVERIFY2(desc1 == "Xray Server", "First remaining server should be Xray");
QVERIFY2(desc2 == "WireGuard Server", "Second remaining server should be WireGuard");
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0 (was 1, removed 0)");
QString lastDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(lastDesc == "WireGuard Server", "Last server should be WireGuard");
}
};
QTEST_MAIN(TestDefaultServerChange)
#include "testDefaultServerChange.moc"
-195
View File
@@ -1,195 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "tests/testServerRepositoryHelpers.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestMultipleImports : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testMultipleImports() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
m_coreController->m_importCoreController->importConfig(importResult1.config);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted once");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After first import servers count should be 1");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After first import model row count should be 1");
}
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "First server should be default");
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(desc1 == "AWG Server", "First server description should match");
if (m_coreController->m_serversModel) {
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc1 == "AWG Server", "First server description in model should match");
}
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After second import servers count should be 2");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 2, "After second import model row count should be 2");
}
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(1));
QVERIFY2(desc2 == "Xray Server", "Second server description should match");
if (m_coreController->m_serversModel) {
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc2 == "Xray Server", "Second server description in model should match");
}
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
QVERIFY2(importResult3.errorCode == ErrorCode::NoError, "Third import should succeed");
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(importFinishedSpy.count() == 3, "importFinished signal should be emitted three times");
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted twice (0->1, 1->2, first import doesn't emit)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "After third import servers count should be 3");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "After third import model row count should be 3");
}
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Third server should be default");
QString desc3 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(2));
QVERIFY2(desc3 == "WireGuard Server", "Third server description should match");
if (m_coreController->m_serversModel) {
QString modelDesc3 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc3 == "WireGuard Server", "Third server description in model should match");
}
}
void testMultipleImportsRemoval() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After two imports servers count should be 2");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
QString desc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(1));
QVERIFY2(desc0 == "AWG Server", "First server description should match");
QVERIFY2(desc1 == "Xray Server", "Second server description should match");
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After removing first server, servers count should be 1");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing first server, default index should be 0");
QString remainingDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(remainingDesc == "Xray Server", "Remaining server should be Xray Server");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After removing first server, model row count should be 1");
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc == "Xray Server", "Remaining server description in model should match");
}
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "After removing last server, servers count should be 0");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing last server, default index should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "After removing last server, model row count should be 0");
}
}
};
QTEST_MAIN(TestMultipleImports)
#include "testMultipleImports.moc"
-165
View File
@@ -1,165 +0,0 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "core/controllers/api/pairingController.h"
#include "ui/controllers/api/pairingUiController.h"
#include "core/utils/constants/apiKeys.h"
using namespace amnezia;
class TestPairingParsers : public QObject
{
Q_OBJECT
private slots:
void generateQr_success_extractsConfigAndMeta()
{
PairingController::QrPairingConfigPayload out;
QJsonObject o;
o[apiDefs::key::config] = QStringLiteral("vpn://dummy");
o[apiDefs::key::serviceInfo] = QJsonObject { { QStringLiteral("is_ad_visible"), false } };
o[apiDefs::key::supportedProtocols] = QJsonArray { QStringLiteral("awg") };
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::NoError);
QCOMPARE(out.config, QStringLiteral("vpn://dummy"));
QCOMPARE(out.supportedProtocols.size(), 1);
}
void generateQr_http408()
{
PairingController::QrPairingConfigPayload out;
QJsonObject o;
o[QStringLiteral("http_status")] = 408;
o[QStringLiteral("message")] = QStringLiteral("Request Timeout");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::ApiConfigTimeoutError);
QVERIFY(out.config.isEmpty());
}
void generateQr_http429()
{
PairingController::QrPairingConfigPayload out;
QJsonObject o;
o[QStringLiteral("http_status")] = 429;
o[QStringLiteral("message")] = QStringLiteral("Too Many Requests");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::ApiPairingRateLimitedError);
}
void scanQr_messageOk()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("OK");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::NoError);
}
void scanQr_messageOk_extractsDeviceName()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("OK");
o[QStringLiteral("device_name")] = QStringLiteral("TestPhone");
const QByteArray body = QJsonDocument(o).toJson();
QString name;
QCOMPARE(PairingController::parseScanQrResponseBody(body, &name), ErrorCode::NoError);
QCOMPARE(name, QStringLiteral("TestPhone"));
}
void scanQr_deviceLimitMessage()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("Device limit reached for subscription");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiConfigLimitError);
}
void scanQr_http403()
{
QJsonObject o;
o[QStringLiteral("http_status")] = 403;
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingForbiddenError);
}
void scanQr_http409()
{
QJsonObject o;
o[QStringLiteral("http_status")] = 409;
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingConflictError);
}
void scanQr_notFoundMessage()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("Session not found");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiNotFoundError);
}
void scanQr_qrSessionExpiredMessage()
{
QJsonObject o;
o[QStringLiteral("message")] = QStringLiteral("QR session not found or expired");
const QByteArray body = QJsonDocument(o).toJson();
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingSessionExpiredError);
}
void validateScanFields_oversizedVpnKey()
{
QString vpnKey;
vpnKey.fill(QLatin1Char('x'), 256 * 1024 + 1);
QCOMPARE(PairingController::validatePairingScanFields(QStringLiteral("ab"), vpnKey, QStringLiteral("k"),
QStringLiteral("amnezia-premium"), QStringLiteral("ru")),
ErrorCode::ApiPairingPayloadTooLargeError);
}
void validateScanFields_uuidTooLong()
{
QString uuid(200, QLatin1Char('a'));
QCOMPARE(PairingController::validatePairingScanFields(uuid, QStringLiteral("vpn://a"), QStringLiteral("k"),
QStringLiteral("amnezia-premium"), QStringLiteral("ru")),
ErrorCode::ApiConfigEmptyError);
}
void validateScanFields_missingServiceType()
{
QCOMPARE(PairingController::validatePairingScanFields(QStringLiteral("ab"), QStringLiteral("vpn://x"),
QStringLiteral("k"), QString(),
QStringLiteral("ru")),
ErrorCode::ApiPairingMissingMetadataError);
}
void pairingUi_applyScanned_extractsUuid_emitsSignal()
{
PairingUiController ctl(nullptr, nullptr, nullptr, nullptr);
QSignalSpy spy(&ctl, &PairingUiController::pairingUuidFromScan);
const QString u = QStringLiteral("123e4567-e89b-12d3-a456-426614174000");
QVERIFY(ctl.applyScannedTextAsPairingUuid(QStringLiteral("prefix ") + u + QStringLiteral(" suffix")));
QCOMPARE(spy.count(), 1);
QCOMPARE(spy.first().first().toString(), u);
}
void pairingUi_applyScanned_rejectsVpnKey()
{
PairingUiController ctl(nullptr, nullptr, nullptr, nullptr);
QSignalSpy spy(&ctl, &PairingUiController::pairingUuidFromScan);
QVERIFY(!ctl.applyScannedTextAsPairingUuid(QStringLiteral("vpn://AAAA")));
QCOMPARE(spy.count(), 0);
}
};
QTEST_MAIN(TestPairingParsers)
#include "testPairingParsers.moc"
-385
View File
@@ -1,385 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QProcessEnvironment>
#include <QDebug>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/utils/commonStructs.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/errorCodes.h"
#include "ui/models/serversModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestSelfHostedServerSetup : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
ServerCredentials getCredentialsFromEnv() {
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
QString hostName = env.value("TEST_SERVER_HOST");
QString userName = env.value("TEST_SERVER_USER");
QString password = env.value("TEST_SERVER_PASSWORD");
QString portStr = env.value("TEST_SERVER_PORT", "22");
int port = portStr.toInt();
ServerCredentials credentials;
credentials.hostName = hostName;
credentials.userName = userName;
credentials.secretData = password;
credentials.port = port;
return credentials;
}
void verifySshConnection(const ServerCredentials& credentials) {
QString sshOutput;
ErrorCode sshError = m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
QVERIFY2(sshError == ErrorCode::NoError,
QString("SSH connection should succeed. Error: %1, Output: %2")
.arg(static_cast<int>(sshError))
.arg(sshOutput)
.toUtf8().constData());
qDebug() << "SSH connection successful. Output:" << sshOutput;
}
void verifyAdminAccess(int serverIndex)
{
const QString serverId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
const auto adminCfg = m_coreController->m_serversRepository->selfHostedAdminConfig(serverId);
QVERIFY2(adminCfg.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig &selfHosted = *adminCfg;
QVERIFY2(selfHosted.hasCredentials(),
"Server should have credentials (admin access)");
QVERIFY2(!selfHosted.userName.isEmpty(),
"Server should have userName for admin access");
QVERIFY2(!selfHosted.password.isEmpty(),
"Server should have password for admin access");
QVERIFY2(!selfHosted.isReadOnly(),
"Server should not be read-only (should have admin access)");
if (m_coreController->m_serversModel) {
bool hasWriteAccess = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(serverIndex, 0),
ServersModel::HasWriteAccessRole
).toBool();
QVERIFY2(hasWriteAccess,
"Server should have write access (admin access) according to ServersModel");
}
qDebug() << "Admin access verified for server at index:" << serverIndex;
}
void verifyClientConfig(const ContainerConfig& containerConfig, DockerContainer container) {
QString containerName = ContainerUtils::containerToString(container);
qDebug() << "Checking container:" << containerName;
if (ContainerUtils::containerService(container) != ServiceType::Other) {
bool hasClientConfig = containerConfig.protocolConfig.hasClientConfig();
QVERIFY2(hasClientConfig,
QString("Container %1 should have client config initialized")
.arg(containerName)
.toUtf8().constData());
if (container == DockerContainer::Awg) {
const AwgProtocolConfig* awgProtocolConfig = containerConfig.protocolConfig.as<AwgProtocolConfig>();
QVERIFY2(awgProtocolConfig != nullptr, "Protocol config should be AwgProtocolConfig");
QVERIFY2(awgProtocolConfig->hasClientConfig(), "AwgProtocolConfig should have client config");
const std::optional<AwgClientConfig>& clientCfgOpt = awgProtocolConfig->clientConfig;
QVERIFY2(clientCfgOpt.has_value(), "Awg client config should exist");
const AwgClientConfig& awgClientConfig = *clientCfgOpt;
QVERIFY2(!awgClientConfig.hostName.isEmpty(), "Awg client config should have hostName");
QVERIFY2(awgClientConfig.port > 0, "Awg client config should have valid port");
QVERIFY2(!awgClientConfig.clientPrivateKey.isEmpty(), "Awg client config should have clientPrivateKey");
QVERIFY2(!awgClientConfig.clientPublicKey.isEmpty(), "Awg client config should have clientPublicKey");
QVERIFY2(!awgClientConfig.serverPublicKey.isEmpty(), "Awg client config should have serverPublicKey");
QVERIFY2(!awgClientConfig.clientId.isEmpty(), "Awg client config should have clientId");
QVERIFY2(!awgClientConfig.nativeConfig.isEmpty(), "Awg client config should have nativeConfig");
}
qDebug() << "Container" << containerName << "has valid client config initialized";
} else {
qDebug() << "Container" << containerName << "is service type Other, skipping client config check";
}
}
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testSelfHostedServerSetup() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
int awgPort = 55424;
TransportProto awgTransportProto = TransportProto::Udp;
bool wasAwgInstalled = false;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
ErrorCode installServerError = m_coreController->m_installController->installServer(
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
QVERIFY2(installServerError == ErrorCode::NoError,
QString("installServer for Awg should succeed. Error: %1")
.arg(static_cast<int>(installServerError))
.toUtf8().constData());
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Server with Awg container added at index:" << serverIndex;
const auto adminAfterAwg = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterAwg.has_value(), "Server should be self-hosted (admin)");
const SelfHostedAdminServerConfig *selfHostedAfterAwg = &(*adminAfterAwg);
QVERIFY2(selfHostedAfterAwg->defaultContainer == DockerContainer::Awg, "Default container should be Awg");
QVERIFY2(selfHostedAfterAwg->containers.contains(DockerContainer::Awg), "Server should have Awg container");
ContainerConfig awgConfig = selfHostedAfterAwg->containers.value(DockerContainer::Awg);
QVERIFY2(awgConfig.container == DockerContainer::Awg, "Awg container config should be valid");
QVERIFY2(selfHostedAfterAwg->containers.size() == 1,
QString("Server should have exactly 1 container after Awg installation, but has %1")
.arg(selfHostedAfterAwg->containers.size())
.toUtf8().constData());
verifyClientConfig(awgConfig, DockerContainer::Awg);
qDebug() << "Awg container installed and configured successfully with valid client config";
int dnsPort = 53;
TransportProto dnsTransportProto = TransportProto::Udp;
bool wasDnsInstalled = false;
const QString serverIdForOps = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode installContainerError = m_coreController->m_installController->installContainer(
serverIdForOps, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled);
QVERIFY2(installContainerError == ErrorCode::NoError,
QString("installContainer for Dns should succeed. Error: %1")
.arg(static_cast<int>(installContainerError))
.toUtf8().constData());
qDebug() << "Dns container installed:" << wasDnsInstalled;
const auto adminAfterDns = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterDns.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedAfterDns = &(*adminAfterDns);
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Awg), "Server should still have Awg container");
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Dns), "Server should have Dns container");
QVERIFY2(selfHostedAfterDns->containers.size() == 2,
QString("Server should have exactly 2 containers after Dns installation, but has %1")
.arg(selfHostedAfterDns->containers.size())
.toUtf8().constData());
ContainerConfig dnsConfig = selfHostedAfterDns->containers.value(DockerContainer::Dns);
QVERIFY2(dnsConfig.container == DockerContainer::Dns, "Dns container config should be valid");
const DnsProtocolConfig* dnsProtocolConfig = dnsConfig.protocolConfig.as<DnsProtocolConfig>();
QVERIFY2(dnsProtocolConfig != nullptr, "Protocol config should be DnsProtocolConfig");
qDebug() << "Dns container installed and configured successfully";
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. Server setup with Awg and Dns containers is complete.";
}
void testSelfHostedServerEmptyRecover() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
SelfHostedAdminServerConfig serverConfig;
serverConfig.hostName = credentials.hostName;
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_coreController->m_appSettingsRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
m_coreController->m_serversRepository->addServer(QString(), serverConfig.toJson(),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Empty server added at index:" << serverIndex;
const auto addedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(addedAdmin.has_value(), "Added server should be self-hosted admin");
const SelfHostedAdminServerConfig *selfHosted = &(*addedAdmin);
QVERIFY2(selfHosted->containers.isEmpty(), "Server should have no containers initially");
QVERIFY2(selfHosted->defaultContainer == DockerContainer::None, "Default container should be None");
const QString scanServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(scanServerId);
QVERIFY2(scanError == ErrorCode::NoError,
QString("Server scan should succeed. Error: %1")
.arg(static_cast<int>(scanError))
.toUtf8().constData());
qDebug() << "Server scan completed successfully";
const auto scannedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(scannedAdmin.has_value(), "Scanned server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *scannedSelfHosted = &(*scannedAdmin);
QMap<DockerContainer, ContainerConfig> containers = scannedSelfHosted->containers;
int containersCount = containers.size();
qDebug() << "Found containers count:" << containersCount;
QVERIFY2(containersCount >= 0,
QString("Containers count should be non-negative, but got %1")
.arg(containersCount)
.toUtf8().constData());
if (containersCount > 0) {
qDebug() << "Server has" << containersCount << "installed container(s)";
} else {
qDebug() << "Server has no installed containers";
}
for (auto it = containers.begin(); it != containers.end(); ++it) {
verifyClientConfig(it.value(), it.key());
}
QVERIFY2(scannedSelfHosted->containers.size() == containersCount,
QString("Scanned containers count should match. Expected: %1, Actual: %2")
.arg(containersCount)
.arg(scannedSelfHosted->containers.size())
.toUtf8().constData());
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. Server has admin access and all containers are initialized.";
}
void testRemoveAllContainers() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
int awgPort = 55424;
TransportProto awgTransportProto = TransportProto::Udp;
bool wasAwgInstalled = false;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
ErrorCode installServerError = m_coreController->m_installController->installServer(
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
QVERIFY2(installServerError == ErrorCode::NoError,
QString("installServer for Awg should succeed. Error: %1")
.arg(static_cast<int>(installServerError))
.toUtf8().constData());
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Server with Awg container added at index:" << serverIndex;
const auto adminBeforeRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminBeforeRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedBeforeRemoval = &(*adminBeforeRemoval);
QVERIFY2(!selfHostedBeforeRemoval->containers.isEmpty(), "Server should have containers before removal");
QVERIFY2(selfHostedBeforeRemoval->defaultContainer != DockerContainer::None, "Server should have default container before removal");
qDebug() << "Containers before removal:" << selfHostedBeforeRemoval->containers.size();
const QString removeServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode removeError = m_coreController->m_installController->removeAllContainers(removeServerId);
QVERIFY2(removeError == ErrorCode::NoError,
QString("removeAllContainers should succeed. Error: %1")
.arg(static_cast<int>(removeError))
.toUtf8().constData());
qDebug() << "All containers removed successfully";
const auto adminAfterRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedAfterRemoval = &(*adminAfterRemoval);
QVERIFY2(selfHostedAfterRemoval->containers.isEmpty(),
"Server should have no containers after removal");
QVERIFY2(selfHostedAfterRemoval->defaultContainer == DockerContainer::None,
"Default container should be None after removal");
qDebug() << "Containers after removal:" << selfHostedAfterRemoval->containers.size();
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. All containers removed and server is empty.";
}
};
QTEST_MAIN(TestSelfHostedServerSetup)
#include "testSelfHostedServerSetup.moc"
-114
View File
@@ -1,114 +0,0 @@
#include <QTest>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/models/serverDescription.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
#include "core/utils/serverConfigUtils.h"
using namespace amnezia;
class TestServerEdgeCases : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testInvalidIndexOperations() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(-1));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(10));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(100));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server count should remain 1");
const QString validServerId = m_coreController->m_serversController->getServerId(0);
const serverConfigUtils::ConfigType editKind =
m_coreController->m_serversRepository->serverKind(validServerId);
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(-1),
QJsonObject(), editKind);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(10),
QJsonObject(), editKind);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(-1));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(10));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should remain 0");
}
void testEmptyRepositoryOperations() {
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Should start with 0 servers");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for empty repository");
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(0),
SelfHostedAdminServerConfig {}.toJson(),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for empty repository");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for empty repository");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0 for empty repository");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Server count should remain 0");
}
};
QTEST_MAIN(TestServerEdgeCases)
#include "testServerEdgeCases.moc"
-106
View File
@@ -1,106 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "tests/testServerRepositoryHelpers.h"
#include "ui/models/serversModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestServerEdit : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testServerEditTriggersHandlers() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == 1, "Import should succeed");
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
m_coreController->m_serversController->getServerId(0),
QStringLiteral("Edited AWG Server"));
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted");
QVERIFY2(serverEditedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->serverIdAt(0),
"serverEdited should emit edited server id");
const QString editedDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(editedDesc == "Edited AWG Server", "Server description should be updated");
if (m_coreController->m_serversModel) {
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc == "Edited AWG Server", "Server description in model should be updated");
}
}
void testServerEditPreservesDefault() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server should be index 1");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
m_coreController->m_serversController->getServerId(1),
QStringLiteral("Edited Default Server"));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing default server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
m_coreController->m_serversController->getServerId(0),
QStringLiteral("Edited Non-Default Server"));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing non-default server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
}
};
QTEST_MAIN(TestServerEdit)
#include "testServerEdit.moc"
@@ -1,93 +0,0 @@
#ifndef TESTSERVERREPOSITORYHELPERS_H
#define TESTSERVERREPOSITORYHELPERS_H
#include <QString>
#include <QJsonObject>
#include "core/repositories/secureServersRepository.h"
#include "core/utils/serverConfigUtils.h"
namespace amnezia::test
{
inline QString serverDescription(SecureServersRepository *repo, const QString &serverId)
{
switch (repo->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = repo->selfHostedAdminConfig(serverId);
return cfg.has_value() ? cfg->description : QString();
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = repo->selfHostedUserConfig(serverId);
return cfg.has_value() ? cfg->description : QString();
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = repo->nativeConfig(serverId);
return cfg.has_value() ? cfg->description : QString();
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = repo->apiV2Config(serverId);
return cfg.has_value() ? cfg->description : QString();
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2: {
const auto cfg = repo->legacyApiConfig(serverId);
return cfg.has_value() ? cfg->description : QString();
}
case serverConfigUtils::ConfigType::Invalid:
default:
return {};
}
}
inline void setServerDescription(SecureServersRepository *repo, const QString &serverId, const QString &description)
{
const serverConfigUtils::ConfigType kind = repo->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
auto cfg = repo->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return;
cfg->description = description;
cfg->displayName = description;
repo->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
auto cfg = repo->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return;
cfg->description = description;
cfg->displayName = description;
repo->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::Native: {
auto cfg = repo->nativeConfig(serverId);
if (!cfg.has_value()) return;
cfg->description = description;
cfg->displayName = description;
repo->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
auto cfg = repo->apiV2Config(serverId);
if (!cfg.has_value()) return;
cfg->description = description;
cfg->displayName = description;
repo->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
case serverConfigUtils::ConfigType::Invalid:
default:
return;
}
}
} // namespace amnezia::test
#endif
-112
View File
@@ -1,112 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "tests/testServerRepositoryHelpers.h"
#include "ui/models/serversModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestServersModelSync : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testServersModelSyncOnOperations() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
if (!m_coreController->m_serversModel) {
QSKIP("ServersModel not available");
}
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "Model should have 1 row after import");
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc1 == "AWG Server", "Model should have correct server name");
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
m_coreController->m_serversController->getServerId(0),
QStringLiteral("Edited AWG Server"));
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc2 == "Edited AWG Server", "Model should be updated after edit");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Model should have 0 rows after removal");
}
void testServersModelDefaultIndexSync() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
if (!m_coreController->m_serversModel) {
QSKIP("ServersModel not available");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "Model should have 3 rows");
bool isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
bool isDefault1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::IsDefaultRole).toBool();
bool isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
QVERIFY2(!isDefault0, "Server 0 should not be default");
QVERIFY2(!isDefault1, "Server 1 should not be default");
QVERIFY2(isDefault2, "Server 2 should be default");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
QVERIFY2(isDefault0, "Server 0 should be default after change");
QVERIFY2(!isDefault2, "Server 2 should not be default after change");
}
};
QTEST_MAIN(TestServersModelSync)
#include "testServersModelSync.moc"
-265
View File
@@ -1,265 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QLocale>
#include "core/controllers/coreController.h"
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/languageUiController.h"
#include "ui/models/allowedDnsModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/appSplitTunnelingModel.h"
#include "ui/models/languageModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
class TestSettingsSignals : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
}
void testDnsSettingsSignals() {
QSignalSpy primaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::primaryDnsChanged);
QSignalSpy secondaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::secondaryDnsChanged);
QSignalSpy allowedDnsServersChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::allowedDnsServersChanged);
QString primaryDns = "8.8.8.8";
QString secondaryDns = "8.8.4.4";
m_coreController->m_settingsUiController->setPrimaryDns(primaryDns);
QVERIFY2(primaryDnsChangedSpy.count() == 1, "primaryDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->getPrimaryDns() == primaryDns, "Primary DNS should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getPrimaryDns() == primaryDns, "Primary DNS should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->primaryDns() == primaryDns, "Primary DNS should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->setSecondaryDns(secondaryDns);
QVERIFY2(secondaryDnsChangedSpy.count() == 1, "secondaryDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->getSecondaryDns() == secondaryDns, "Secondary DNS should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getSecondaryDns() == secondaryDns, "Secondary DNS should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->secondaryDns() == secondaryDns, "Secondary DNS should be available in SecureAppSettingsRepository");
QStringList dnsList = {"1.1.1.1", "1.0.0.1"};
m_coreController->m_allowedDnsController->addDnsList(dnsList, true);
QVERIFY2(allowedDnsServersChangedSpy.count() == 1, "allowedDnsServersChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSettingsRepository->getAllowedDnsServers() == dnsList, "Allowed DNS servers should be updated in SecureAppSettingsRepository");
QVERIFY2(m_coreController->m_allowedDnsController->getCurrentDnsServers() == dnsList, "Allowed DNS servers should be available in AllowedDnsController");
QVERIFY2(m_coreController->m_allowedDnsUiController != nullptr, "AllowedDnsUiController should exist");
QVERIFY2(m_coreController->m_allowedDnsModel != nullptr, "AllowedDnsModel should exist");
QStringList modelDnsList;
for (int i = 0; i < m_coreController->m_allowedDnsModel->rowCount(); ++i) {
modelDnsList.append(m_coreController->m_allowedDnsModel->data(m_coreController->m_allowedDnsModel->index(i, 0), AllowedDnsModel::IpRole).toString());
}
QVERIFY2(modelDnsList == dnsList, "Allowed DNS servers should be available in AllowedDnsModel");
}
void testAmneziaDnsToggleSignal() {
QSignalSpy amneziaDnsToggledSpy(m_coreController->m_settingsUiController, &SettingsUiController::amneziaDnsToggled);
QSignalSpy useAmneziaDnsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::useAmneziaDnsChanged);
bool initialValue = m_coreController->m_settingsController->isAmneziaDnsEnabled();
m_coreController->m_settingsUiController->toggleAmneziaDns(!initialValue);
QVERIFY2(amneziaDnsToggledSpy.count() == 1, "amneziaDnsToggled signal should be emitted");
QVERIFY2(amneziaDnsToggledSpy.at(0).at(0).toBool() == !initialValue, "amneziaDnsToggled should emit correct value");
QVERIFY2(useAmneziaDnsChangedSpy.count() == 1, "useAmneziaDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->useAmneziaDns() == !initialValue, "Amnezia DNS state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAmneziaDns(initialValue);
QVERIFY2(amneziaDnsToggledSpy.count() == 2, "amneziaDnsToggled signal should be emitted again");
QVERIFY2(useAmneziaDnsChangedSpy.count() == 2, "useAmneziaDnsChanged signal should be emitted again");
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == initialValue, "Amnezia DNS state should be restored in SettingsUiController");
}
void testLoggingSignals() {
QSignalSpy loggingStateChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::loggingStateChanged);
QSignalSpy saveLogsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged);
bool initialLogging = m_coreController->m_settingsController->isLoggingEnabled();
m_coreController->m_settingsUiController->toggleLogging(!initialLogging);
QVERIFY2(loggingStateChangedSpy.count() == 1, "loggingStateChanged signal should be emitted");
QVERIFY2(saveLogsChangedSpy.count() == 1, "saveLogsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isLoggingEnabled() == !initialLogging, "Logging state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == !initialLogging, "Logging state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isSaveLogs() == !initialLogging, "Logging state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleLogging(initialLogging);
QVERIFY2(loggingStateChangedSpy.count() == 2, "loggingStateChanged signal should be emitted again");
QVERIFY2(saveLogsChangedSpy.count() == 2, "saveLogsChanged signal should be emitted again");
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == initialLogging, "Logging state should be restored in SettingsUiController");
}
void testScreenshotsSignals() {
QSignalSpy screenshotsEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged);
bool initialScreenshots = m_coreController->m_settingsController->isScreenshotsEnabled();
m_coreController->m_settingsUiController->toggleScreenshotsEnabled(!initialScreenshots);
QVERIFY2(screenshotsEnabledChangedSpy.count() == 1, "screenshotsEnabledChanged signal should be emitted");
QVERIFY2(screenshotsEnabledChangedSpy.at(0).at(0).toBool() == !initialScreenshots, "screenshotsEnabledChanged should emit correct value");
QVERIFY2(m_coreController->m_settingsController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SecureAppSettingsRepository");
}
void testStartMinimizedSignals() {
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled();
m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized);
QVERIFY2(startMinimizedChangedSpy.count() == 1, "startMinimizedChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
}
void testAutoConnectSignals() {
bool initialAutoConnect = m_coreController->m_settingsController->isAutoConnectEnabled();
m_coreController->m_settingsUiController->toggleAutoConnect(!initialAutoConnect);
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isAutoConnect() == !initialAutoConnect, "Auto connect state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAutoConnect(initialAutoConnect);
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsUiController");
}
void testLanguageChangeSignals() {
QSignalSpy appLanguageChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appLanguageChanged);
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
QLocale initialLocale = m_coreController->m_settingsController->getAppLanguage();
QLocale newLocale = (initialLocale.language() == QLocale::English) ? QLocale::Russian : QLocale::English;
m_coreController->m_settingsController->setAppLanguage(newLocale);
QVERIFY2(appLanguageChangedSpy.count() == 1, "appLanguageChanged signal should be emitted");
QVERIFY2(appLanguageChangedSpy.at(0).at(0).value<QLocale>() == newLocale, "appLanguageChanged should emit correct locale");
QVERIFY2(m_coreController->m_settingsController->getAppLanguage() == newLocale, "App language should be updated in SettingsController");
QVERIFY2(m_coreController->m_appSettingsRepository->getAppLanguage() == newLocale, "App language should be available in SecureAppSettingsRepository");
if (m_coreController->m_languageModel) {
QString newLanguageName = m_coreController->m_languageUiController->getCurrentLanguageName();
QVERIFY2(!newLanguageName.isEmpty(), "Language name should be available in LanguageUiController");
}
}
void testGatewayEndpointSignals() {
QSignalSpy gatewayEndpointChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::gatewayEndpointChanged);
QSignalSpy devGatewayEnvChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::devGatewayEnvChanged);
QString initialEndpoint = m_coreController->m_settingsController->getGatewayEndpoint();
QString newEndpoint = "https://test-gateway.example.com";
m_coreController->m_settingsUiController->setGatewayEndpoint(newEndpoint);
QVERIFY2(gatewayEndpointChangedSpy.count() == 1, "gatewayEndpointChanged signal should be emitted");
QVERIFY2(gatewayEndpointChangedSpy.at(0).at(0).toString() == newEndpoint, "gatewayEndpointChanged should emit correct endpoint");
QVERIFY2(m_coreController->m_settingsController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SecureAppSettingsRepository");
bool initialDevEnv = m_coreController->m_settingsController->isDevGatewayEnv();
m_coreController->m_settingsUiController->toggleDevGatewayEnv(!initialDevEnv);
QVERIFY2(devGatewayEnvChangedSpy.count() == 1, "devGatewayEnvChanged signal should be emitted");
QVERIFY2(devGatewayEnvChangedSpy.at(0).at(0).toBool() == !initialDevEnv, "devGatewayEnvChanged should emit correct value");
QVERIFY2(m_coreController->m_settingsController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SecureAppSettingsRepository");
}
void testSettingsClearedSignal() {
QSignalSpy settingsClearedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared);
m_coreController->m_settingsController->clearSettings();
QVERIFY2(settingsClearedSpy.count() == 1, "settingsCleared signal should be emitted");
}
void testSplitTunnelingSignals() {
QSignalSpy siteSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::siteSplitTunnelingToggled);
QSignalSpy appSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::appSplitTunnelingToggled);
QSignalSpy sitesSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesSplitTunnelingEnabledChanged);
QSignalSpy appsSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsSplitTunnelingEnabledChanged);
QSignalSpy routeModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::routeModeChanged);
QSignalSpy appsRouteModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsRouteModeChanged);
QSignalSpy sitesChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesChanged);
QSignalSpy appsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsChanged);
bool initialSitesSplitTunneling = m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled();
m_coreController->m_ipSplitTunnelingController->toggleSplitTunneling(!initialSitesSplitTunneling);
QVERIFY2(sitesSplitTunnelingEnabledChangedSpy.count() == 1, "sitesSplitTunnelingEnabledChanged signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be updated in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->isSitesSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be available in SecureAppSettingsRepository");
bool initialAppsSplitTunneling = m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled();
m_coreController->m_appSplitTunnelingController->toggleSplitTunneling(!initialAppsSplitTunneling);
QVERIFY2(appsSplitTunnelingEnabledChangedSpy.count() == 1, "appsSplitTunnelingEnabledChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be updated in AppSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->isAppsSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be available in SecureAppSettingsRepository");
RouteMode initialRouteMode = m_coreController->m_ipSplitTunnelingController->getRouteMode();
RouteMode newRouteMode = (initialRouteMode == RouteMode::VpnOnlyForwardSites)
? RouteMode::VpnAllExceptSites
: RouteMode::VpnOnlyForwardSites;
m_coreController->m_ipSplitTunnelingController->setRouteMode(newRouteMode);
QVERIFY2(routeModeChangedSpy.count() == 1, "routeModeChanged signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingController->getRouteMode() == newRouteMode, "Route mode should be updated in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->routeMode() == newRouteMode, "Route mode should be available in SecureAppSettingsRepository");
AppsRouteMode initialAppsRouteMode = m_coreController->m_appSplitTunnelingController->getRouteMode();
AppsRouteMode newAppsRouteMode = (initialAppsRouteMode == AppsRouteMode::VpnAllExceptApps)
? AppsRouteMode::VpnAllApps
: AppsRouteMode::VpnAllExceptApps;
m_coreController->m_appSplitTunnelingController->setRouteMode(newAppsRouteMode);
QVERIFY2(appsRouteModeChangedSpy.count() == 1, "appsRouteModeChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingController->getRouteMode() == newAppsRouteMode, "Apps route mode should be updated in AppSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->appsRouteMode() == newAppsRouteMode, "Apps route mode should be available in SecureAppSettingsRepository");
QMap<QString, QString> sitesMap{{"example.com", "1.2.3.4"}};
m_coreController->m_ipSplitTunnelingController->addSites(sitesMap, true);
QVERIFY2(sitesChangedSpy.count() >= 1, "sitesChanged signal should be emitted");
QVector<QPair<QString, QString>> currentSites = m_coreController->m_ipSplitTunnelingController->getCurrentSites();
QVERIFY2(currentSites.size() >= 1, "Sites should be available in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController != nullptr, "IpSplitTunnelingUiController should exist");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel != nullptr, "IpSplitTunnelingModel should exist");
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() >= 1, "Sites should be available in IpSplitTunnelingModel");
QString modelUrl = m_coreController->m_ipSplitTunnelingModel->data(m_coreController->m_ipSplitTunnelingModel->index(0, 0), IpSplitTunnelingModel::UrlRole).toString();
QVERIFY2(modelUrl == "example.com", "Site URL should be available in IpSplitTunnelingModel");
}
};
QTEST_MAIN(TestSettingsSignals)
#include "testSettingsSignals.moc"
-88
View File
@@ -1,88 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
class TestSignalOrder : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testSignalOrderOnImport() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(serverAddedSpy.at(0).count() > 0, "serverAdded should have arguments");
}
void testSignalOrderOnRemoveDefault() {
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1");
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted when removing default server");
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->defaultServerId(),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
}
};
QTEST_MAIN(TestSignalOrder)
#include "testSignalOrder.moc"
@@ -1,306 +0,0 @@
#include <QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QModelIndex>
#include "core/controllers/coreController.h"
#include "core/models/serverDescription.h"
#include "core/controllers/selfhosted/importController.h"
#include "ui/models/serversModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/constants/configKeys.h"
using namespace amnezia;
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/protocolEnum.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
namespace {
int defaultServerRow(const QVector<ServerDescription> &descriptions, const QString &defaultServerId)
{
for (int i = 0; i < descriptions.size(); ++i) {
if (descriptions.at(i).serverId == defaultServerId) {
return i;
}
}
return -1;
}
} // namespace
class TestUiServersModelAndController : public QObject
{
Q_OBJECT
private:
CoreController* m_coreController;
SecureQSettings* m_settings;
QJsonObject createAwg2Config()
{
QJsonObject clientConfig;
clientConfig[configKey::mtu] = protocols::awg::defaultMtu;
clientConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
clientConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
clientConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
clientConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
clientConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
clientConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
clientConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
clientConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
clientConfig[configKey::clientPrivKey] = "test_client_private_key";
clientConfig[configKey::clientPubKey] = "test_client_public_key";
clientConfig[configKey::serverPubKey] = "test_server_public_key";
clientConfig[configKey::pskKey] = "test_psk_key";
clientConfig[configKey::clientIp] = "10.8.1.2";
clientConfig[configKey::allowedIps] = QJsonArray::fromStringList({"0.0.0.0/0"});
QJsonObject awgConfig;
awgConfig[configKey::lastConfig] = QString(QJsonDocument(clientConfig).toJson());
awgConfig[configKey::port] = protocols::awg::defaultPort;
awgConfig[configKey::transportProto] = "udp";
awgConfig[configKey::protocolVersion] = protocols::awg::awgV2;
awgConfig[configKey::subnetAddress] = protocols::wireguard::defaultSubnetAddress;
awgConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
awgConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
awgConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
awgConfig[configKey::initPacketJunkSize] = protocols::awg::defaultInitPacketJunkSize;
awgConfig[configKey::responsePacketJunkSize] = protocols::awg::defaultResponsePacketJunkSize;
awgConfig[configKey::cookieReplyPacketJunkSize] = protocols::awg::defaultCookieReplyPacketJunkSize;
awgConfig[configKey::transportPacketJunkSize] = protocols::awg::defaultTransportPacketJunkSize;
awgConfig[configKey::initPacketMagicHeader] = protocols::awg::defaultInitPacketMagicHeader;
awgConfig[configKey::responsePacketMagicHeader] = protocols::awg::defaultResponsePacketMagicHeader;
awgConfig[configKey::underloadPacketMagicHeader] = protocols::awg::defaultUnderloadPacketMagicHeader;
awgConfig[configKey::transportPacketMagicHeader] = protocols::awg::defaultTransportPacketMagicHeader;
awgConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
awgConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
awgConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
awgConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
awgConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
awgConfig[configKey::isThirdPartyConfig] = true;
QJsonObject container;
container[configKey::container] = "amnezia-awg";
container[configKey::awg] = awgConfig;
QJsonArray containers;
containers.append(container);
QJsonObject config;
config[configKey::containers] = containers;
config[configKey::defaultContainer] = "amnezia-awg";
config[configKey::description] = "AWG2 Test Server";
config[configKey::hostName] = "test.example.com";
return config;
}
QJsonObject createServerDescriptionTestConfig(bool withAmneziaDns)
{
QJsonObject config = createAwg2Config();
config[configKey::description] = "Server 1";
if (withAmneziaDns) {
config[configKey::dns1] = protocols::dns::amneziaDnsIp;
}
return config;
}
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
}
}
void testUiServersModelAndControllerRoles() {
QJsonObject testConfig = createAwg2Config();
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
m_coreController->m_importCoreController->importConfig(testConfig);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
int serverIndex = m_coreController->m_serversRepository->defaultServerIndex();
QVERIFY2(serverIndex == 0, "Default server index should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "ServersModel should have 1 row");
QModelIndex serverModelIndex = m_coreController->m_serversModel->index(0, 0);
QVERIFY2(serverModelIndex.isValid(), "Server model index should be valid");
QString serverName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::NameRole).toString();
QVERIFY2(serverName == "AWG2 Test Server", QString("Server name should be 'AWG2 Test Server', got '%1'").arg(serverName).toUtf8().constData());
QString serverDescription = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::ServerDescriptionRole).toString();
QVERIFY2(serverDescription.contains("test.example.com"), QString("Server description should contain hostname, got '%1'").arg(serverDescription).toUtf8().constData());
QString hostName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HostNameRole).toString();
QVERIFY2(hostName == "test.example.com", "Host name should match");
bool isDefault = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::IsDefaultRole).toBool();
QVERIFY2(isDefault == true, "Server should be default");
bool hasInstalledContainers = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasInstalledContainers).toBool();
QVERIFY2(hasInstalledContainers == true, "Server should have installed containers");
bool hasWriteAccess = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasWriteAccessRole).toBool();
QVERIFY2(hasWriteAccess == false, "Server should not have write access for imported config");
int defaultContainerRole = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::DefaultContainerRole).toInt();
DockerContainer expectedContainer = DockerContainer::Awg;
QVERIFY2(defaultContainerRole == static_cast<int>(expectedContainer), "Default container should be Awg");
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(
m_coreController->m_serversUiController->getServerId(0));
QString hostName = "test.example.com";
QString collapsedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionCollapsed();
QString expectedCollapsed = "AmneziaWG (version 2) | " + hostName;
QVERIFY2(collapsedDescription == expectedCollapsed,
QString("Collapsed description should be '%1', got '%2'").arg(expectedCollapsed, collapsedDescription).toUtf8().constData());
QString expandedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionExpanded();
QString expectedExpanded = hostName;
QVERIFY2(expandedDescription == expectedExpanded,
QString("Expanded description should be '%1', got '%2'").arg(expectedExpanded, expandedDescription).toUtf8().constData());
}
if (m_coreController->m_containersModel) {
int awgContainerIndex = -1;
for (int i = 0; i < ContainerUtils::allContainers().size(); ++i) {
DockerContainer container = ContainerUtils::allContainers().at(i);
if (container == DockerContainer::Awg) {
awgContainerIndex = i;
break;
}
}
QVERIFY2(awgContainerIndex >= 0, "Awg container index should be found");
QModelIndex containerModelIndex = m_coreController->m_containersModel->index(awgContainerIndex, 0);
QVERIFY2(containerModelIndex.isValid(), "Container model index should be valid");
bool isInstalled = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsInstalledRole).toBool();
QVERIFY2(isInstalled == true, "Awg container should be installed");
bool isVpnContainer = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsVpnContainerRole).toBool();
QVERIFY2(isVpnContainer == true, "Awg container should be VPN container");
QString containerName = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::NameRole).toString();
QString expectedContainerName = ContainerUtils::containerHumanNames().value(DockerContainer::Awg);
QVERIFY2(containerName == expectedContainerName, QString("Container name should be '%1', got '%2'").arg(expectedContainerName, containerName).toUtf8().constData());
QString containerDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DescriptionRole).toString();
QString expectedDescription = ContainerUtils::containerDescriptions().value(DockerContainer::Awg);
QVERIFY2(containerDescription == expectedDescription, QString("Container description should match, got '%1'").arg(containerDescription).toUtf8().constData());
QString detailedDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DetailedDescriptionRole).toString();
QString expectedDetailedDescription = ContainerUtils::containerDetailedDescriptions().value(DockerContainer::Awg);
QVERIFY2(detailedDescription == expectedDetailedDescription, QString("Container detailed description should match, got '%1'").arg(detailedDescription).toUtf8().constData());
int serviceType = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ServiceTypeRole).toInt();
QVERIFY2(serviceType == static_cast<int>(ProtocolEnumNS::ServiceType::Vpn), "Service type should be Vpn");
bool isSupported = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsSupportedRole).toBool();
QVERIFY2(isSupported == true, "Container should be supported");
bool isShareable = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsShareableRole).toBool();
QVERIFY2(isShareable == true, "Container should be shareable");
QJsonObject containerConfig = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ConfigRole).toJsonObject();
QVERIFY2(!containerConfig.isEmpty(), "Container config should not be empty");
QVERIFY2(containerConfig.value(configKey::container).toString() == "amnezia-awg", "Container config should have correct container type");
QJsonObject awgProtocolConfig = containerConfig.value(configKey::awg).toObject();
QVERIFY2(!awgProtocolConfig.isEmpty(), "AWG protocol config should not be empty");
QString protocolVersion = awgProtocolConfig.value(configKey::protocolVersion).toString();
QVERIFY2(protocolVersion == protocols::awg::awgV2, QString("Protocol version should be '%1', got '%2'").arg(protocols::awg::awgV2, protocolVersion).toUtf8().constData());
QString port = awgProtocolConfig.value(configKey::port).toString();
QVERIFY2(port == protocols::awg::defaultPort, QString("Port should be '%1', got '%2'").arg(protocols::awg::defaultPort, port).toUtf8().constData());
QString subnetAddress = awgProtocolConfig.value(configKey::subnetAddress).toString();
QVERIFY2(subnetAddress == protocols::wireguard::defaultSubnetAddress, QString("Subnet address should be '%1', got '%2'").arg(protocols::wireguard::defaultSubnetAddress, subnetAddress).toUtf8().constData());
bool isThirdParty = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsThirdPartyConfigRole).toBool();
QVERIFY2(isThirdParty == true, "Imported config should be third party config");
DockerContainer dockerContainer = static_cast<DockerContainer>(m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DockerContainerRole).toInt());
QVERIFY2(dockerContainer == DockerContainer::Awg, "Docker container should be Awg");
QString containerString = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ContainerStringRole).toString();
QVERIFY2(containerString == "amnezia-awg", "Container string should be amnezia-awg");
}
}
void testServerDescriptionFormat() {
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QJsonObject configNoDns = createServerDescriptionTestConfig(false);
m_coreController->m_importCoreController->importConfig(configNoDns);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished should be emitted");
m_coreController->m_appSettingsRepository->setUseAmneziaDns(false);
QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsNoDns, defaultServerRow(descriptionsNoDns, defIdNoDns));
QString descNoDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
QVERIFY2(descNoDns == "test.example.com",
QString("Without Amnezia DNS expected 'test.example.com', got '%1'").arg(descNoDns).toUtf8().constData());
m_coreController->m_serversRepository->clearServers();
if (m_coreController->m_serversRepository->serversCount() > 0) {
m_coreController->m_serversRepository->setDefaultServer(m_coreController->m_serversRepository->serverIdAt(0));
}
QJsonObject configWithDns = createServerDescriptionTestConfig(true);
m_coreController->m_importCoreController->importConfig(configWithDns);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
m_coreController->m_appSettingsRepository->setUseAmneziaDns(true);
QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsWithDns, defaultServerRow(descriptionsWithDns, defIdWithDns));
QString descWithDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
QVERIFY2(descWithDns == "Amnezia DNS | test.example.com",
QString("With Amnezia DNS expected 'Amnezia DNS | test.example.com', got '%1'").arg(descWithDns).toUtf8().constData());
}
};
QTEST_MAIN(TestUiServersModelAndController)
#include "testUiServersModelAndController.moc"
-10
View File
@@ -1822,16 +1822,6 @@ Thank you for staying with us!</source>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiDevices.qml" line="252"/>
<source>Configuration Files: %1</source>
<translation>Файлы конфигурации: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiDevices.qml" line="253"/>
<source>Generated configuration files also count towards the device limit</source>
<translation>Сгенерированные файлы конфигурации тоже учитываются в лимите устройств</translation>
</message>
</context> </context>
<context> <context>
<name>PageSettingsApiInstructions</name> <name>PageSettingsApiInstructions</name>

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