Compare commits

..

45 Commits

Author SHA1 Message Date
dranik 67dcadcd42 add rus translate 2026-05-20 12:21:31 +03:00
dranik a4b2b3e3ad reset files 2026-05-20 12:17:32 +03:00
dranik 3d540b22bf remove doc 2026-05-20 12:11:21 +03:00
dranik d670bca9d4 chore(qr-pairing): drop unrelated PageStart churn and trim overlay logs
Remove tab bar/PageStart changes that were not needed for QR pairing (restore
dev baseline). Quiet iOS pairing scanner console output while keeping a few
failure logs. Record the dead-code audit status in docs/plans.
2026-05-20 12:09:10 +03:00
dranik b09a4ecd8d reset files 2026-05-20 12:01:22 +03:00
dranik c327d3e3c8 add error 1123 & fix update qr code & fix hide qr code 2026-05-20 11:46:21 +03:00
dranik 9c9e1700af fix 1103 error (update qr code) 2026-05-20 11:19:41 +03:00
dranik eba2097d1d remove temp code 2026-05-20 10:09:15 +03:00
dranik 22de0c2a16 remove comment 2026-05-19 23:44:55 +03:00
dranik 614973a4ce remove old code 2026-05-19 23:29:41 +03:00
dranik e554e9b8b4 remove log & remove temp code 2026-05-19 23:11:34 +03:00
dranik d4833454ef fixed serviceType|userCountryCode 2026-05-19 16:13:52 +03:00
dranik 9851b4bacb remove old code 2026-05-18 19:25:56 +03:00
dranik 29ad1f0c02 merge dev & fix conf 2026-05-18 19:10:00 +03:00
dranik d6c34b3f60 remove comment 2026-05-18 18:05:06 +03:00
dranik d8668742b4 remove mock & temp var AMNEZIA_QR_PAIRING_ALLOW 2026-05-18 17:57:57 +03:00
dranik 5eab5fc18b fixed 404, 1100, 1109 - fixed crash app (add server) 2026-05-18 16:02:51 +03:00
dranik b46a9e389f remove old file 2026-05-13 14:28:07 +03:00
dranik 81b8cd05c2 fix build 2026-05-13 14:18:48 +03:00
dranik d0a9f6e4d5 add ui Configuration Files 2026-05-13 13:17:37 +03:00
dranik 8a29b49fd7 update updated_spec.yaml 2026-05-13 12:48:06 +03:00
dranik 1baa2d85bd remove dead code 2026-05-13 11:56:58 +03:00
dranik e226fadb07 fixed PR code 2026-05-12 12:02:13 +03:00
dranik bf4bf9972d fixed line box 2026-05-09 17:18:15 +03:00
dranik f781bf6a23 fixed scaner QR Android 2026-05-09 17:11:29 +03:00
dranik 2fa0ec81ad remove qml limit device 2026-05-08 23:47:42 +03:00
dranik 1ee0a6c9c7 fixed addArc scanner 2026-05-08 23:42:21 +03:00
dranik 14c7aab0fb fixed icon back 2026-05-08 23:07:36 +03:00
dranik d3347e6007 fixed AVCaptureMetadataOutput rectOfInterest 2026-05-08 22:57:03 +03:00
dranik 026826970c fixed iOS UI scanner 2026-05-08 22:50:21 +03:00
dranik d2d3545961 add test scaner ios 2026-05-08 22:36:53 +03:00
dranik b7e2847393 fixed UI scanner iOS 2026-05-08 21:35:08 +03:00
dranik bb56008c3d add check access camera 2026-05-08 16:57:35 +03:00
dranik a53db6eafe fixed open QR code screen & fix iOS scanner 2026-05-08 10:21:24 +03:00
dranik 433ecb448f fixed scanner phone & fix UI/UX 2026-05-08 09:56:04 +03:00
dranik ab12a0b3f0 update screen QR Code 2026-05-08 08:37:18 +03:00
dranik 5a192cec15 fixed QR scaner 2026-05-07 23:37:48 +03:00
dranik 6fc65dba8a fixed iOS QRCodeReader 2026-05-07 22:50:14 +03:00
dranik f65fd4a8c5 fixed server go 2026-05-07 22:30:18 +03:00
dranik c877e1e5cb fix build iOS 2026-05-07 22:17:17 +03:00
dranik 2cb12c596c add qml QR Code 2026-05-07 21:51:39 +03:00
dranik 5beae954c7 add test macros AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY & disable ApiConfigAlreadyAdded 2026-05-07 20:44:35 +03:00
dranik 5583c0a2a9 fixed open Qr QML & add check error code & add test 2026-05-07 19:15:28 +03:00
dranik 2cb7b30d8a add test server 2026-05-07 14:35:53 +03:00
dranik 2f6714e278 feat/Implement QR code generation and scanning 2026-05-07 14:34:40 +03:00
162 changed files with 5848 additions and 2839 deletions
+26 -49
View File
@@ -18,11 +18,11 @@ 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'
Bake-Prebuilts-Linux: Bake-Prebuilts-Linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -40,7 +40,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- name: 'Build dependencies' - name: 'Build dependencies'
shell: bash shell: bash
@@ -50,11 +50,9 @@ 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
# ------------------------------------------------------ # ------------------------------------------------------
@@ -100,7 +98,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- 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
@@ -120,7 +118,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_x64.run path: deploy/build/AmneziaVPN-*-Linux.run
archive: false archive: false
retention-days: 7 retention-days: 7
@@ -151,17 +149,15 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1 - uses: ilammy/msvc-dev-cmd@v1
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- 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
# ------------------------------------------------------ # ------------------------------------------------------
@@ -233,7 +229,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- name: 'Build project' - name: 'Build project'
shell: cmd shell: cmd
@@ -246,31 +242,27 @@ 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_*_windows_x64.msi path: deploy/build/AmneziaVPN-*-win64.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_*_windows_x64.exe path: deploy/build/AmneziaVPN-*-win64.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, 26.4] xcode-version: [26.0]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -287,17 +279,15 @@ jobs:
xcode-version: ${{ matrix.xcode-version }} xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- 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
# ------------------------------------------------------ # ------------------------------------------------------
@@ -354,7 +344,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.0' xcode-version: '26.1'
- name: 'Install desktop Qt' - name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
@@ -386,7 +376,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install deps' - name: 'Install deps'
run: pip install "conan==2.28.0" jsonschema jinja2 run: pip install "conan==2.26.2" jsonschema jinja2
- name: 'Build project' - name: 'Build project'
env: env:
@@ -404,17 +394,14 @@ 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, 26.4] xcode-version: [16.2, 16.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
@@ -431,17 +418,15 @@ jobs:
xcode-version: ${{ matrix.xcode-version }} xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- 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
# ------------------------------------------------------ # ------------------------------------------------------
@@ -517,7 +502,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- name: 'Build project' - name: 'Build project'
env: env:
@@ -533,24 +518,20 @@ 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_*_macos_x64.pkg path: deploy/build/AmneziaVPN-*-Darwin.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, 26.4] xcode-version: [16.2, 16.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
@@ -567,17 +548,15 @@ jobs:
xcode-version: ${{ matrix.xcode-version }} xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- 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
# ------------------------------------------------------ # ------------------------------------------------------
@@ -656,7 +635,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- name: 'Build project' - name: 'Build project'
run: | run: |
@@ -692,7 +671,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- name: 'Setup Android SDK' - name: 'Setup Android SDK'
uses: android-actions/setup-android@v4 uses: android-actions/setup-android@v4
@@ -717,11 +696,9 @@ 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
# ------------------------------------------------------ # ------------------------------------------------------
@@ -735,7 +712,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.3 QT_VERSION: 6.10.1
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 }}
@@ -829,7 +806,7 @@ jobs:
python-version: 3.14 python-version: 3.14
- name: 'Install conan' - name: 'Install conan'
run: pip install "conan==2.28.0" run: pip install "conan==2.26.2"
- name: 'Decode keystore secret to file' - name: 'Decode keystore secret to file'
env: env:
+2 -2
View File
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.0) set(AMNEZIAVPN_VERSION 4.9.0.2)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE) set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -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 2122) set(APP_ANDROID_VERSION_CODE 2120)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
+3
View File
@@ -109,6 +109,9 @@ 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) {
@@ -0,0 +1,10 @@
<?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>
@@ -0,0 +1,5 @@
<?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,4 +8,75 @@
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,5 +24,13 @@
<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,5 +24,13 @@
<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,6 +42,9 @@ 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
@@ -73,12 +76,18 @@ 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() { class AmneziaActivity : QtActivity(), LifecycleOwner {
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>()
@@ -99,6 +108,8 @@ class AmneziaActivity : QtActivity() {
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) {
@@ -205,6 +216,7 @@ class AmneziaActivity : QtActivity() {
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) {
@@ -262,6 +274,7 @@ class AmneziaActivity : QtActivity() {
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()
@@ -285,6 +298,7 @@ class AmneziaActivity : QtActivity() {
qtInitialized.await() qtInitialized.await()
QtAndroidController.onServiceDisconnected() QtAndroidController.onServiceDisconnected()
} }
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
super.onStop() super.onStop()
} }
@@ -357,6 +371,7 @@ class AmneziaActivity : QtActivity() {
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
@@ -367,6 +382,7 @@ class AmneziaActivity : QtActivity() {
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) {
@@ -483,6 +499,7 @@ class AmneziaActivity : QtActivity() {
unregisterBroadcastReceiver(notificationStateReceiver) unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null notificationStateReceiver = null
mainScope.cancel() mainScope.cancel()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
super.onDestroy() super.onDestroy()
} }
@@ -880,6 +897,66 @@ class AmneziaActivity : QtActivity() {
@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)
@@ -928,6 +1005,19 @@ class AmneziaActivity : QtActivity() {
} }
} }
@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")
@@ -1179,6 +1269,7 @@ class AmneziaActivity : QtActivity() {
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,47 +2,384 @@ 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 lateinit var cameraProvider: ProcessCameraProvider private var cameraProvider: ProcessCameraProvider? = null
private var boundCamera: Camera? = null
private var boundImageAnalysis: ImageAnalysis? = null
private var torchOn: Boolean = false
private var imageAnalysisExecutor: ExecutorService? = null
private val qrHandledOrClosing = AtomicBoolean(false)
private var pairingQrDeliveredToQt = false
private var pairingQrUserDismissedCamera = false
private var barcodeScanner: BarcodeScanner? = null
private val cachedPreviewOutputTransform = AtomicReference<OutputTransform?>(null)
private var previewTransformLayoutListener: View.OnLayoutChangeListener? = null
private var previewStreamStateObserver: Observer<PreviewView.StreamState>? = null
@Volatile
private var pairingGeomHeaderBottomPx = 0f
@Volatile
private var pairingGeomStatusBarTopPx = 0f
@Volatile
private var pairingGeomDensity = 1f
@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()
@@ -67,26 +404,41 @@ class CameraActivity : ComponentActivity() {
cameraProviderFuture.addListener({ cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get() cameraProvider = cameraProviderFuture.get()
bindPreview() bindCameraUseCases()
bindImageAnalysis()
}, ContextCompat.getMainExecutor(this)) }, ContextCompat.getMainExecutor(this))
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun bindPreview() { @ExperimentalGetImage
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 camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview) val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val camera = provider.bindToLifecycle(
this,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageAnalysis
)
boundCamera = camera
boundImageAnalysis = imageAnalysis
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.x) .meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
val action = FocusMeteringAction val action = FocusMeteringAction
.Builder(point, FLAG_AF or FLAG_AE).build() .Builder(point, FLAG_AF or FLAG_AE).build()
@@ -98,58 +450,121 @@ class CameraActivity : ComponentActivity() {
else -> false else -> false
} }
} }
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
previewTransformLayoutListener?.let { viewFinder.removeOnLayoutChangeListener(it) }
val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
viewFinder.post {
scheduleCachedPreviewOutputTransformRefresh()
onPairingLayoutGeometryChanged()
}
}
previewTransformLayoutListener = layoutListener
viewFinder.addOnLayoutChangeListener(layoutListener)
previewStreamStateObserver?.let { viewFinder.previewStreamState.removeObserver(it) }
val streamObserver = Observer<PreviewView.StreamState> { state ->
if (state == PreviewView.StreamState.STREAMING) {
viewFinder.post {
scheduleCachedPreviewOutputTransformRefresh()
onPairingLayoutGeometryChanged()
}
}
}
previewStreamStateObserver = streamObserver
viewFinder.previewStreamState.observe(this, streamObserver)
scheduleCachedPreviewOutputTransformRefresh()
} }
@ExperimentalGetImage try {
private fun bindImageAnalysis() { barcodeScanner?.close()
val imageAnalysis = ImageAnalysis.Builder().build() } catch (_: Exception) {
}
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis) barcodeScanner = BarcodeScanning.getClient(
val barcodeScanner = BarcodeScanning.getClient(
Builder() Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE) .setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.setZoomSuggestionOptions( .build()
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(ContextCompat.getMainExecutor(this)) { imageProxy -> imageAnalysis.setAnalyzer(analysisExecutor) { imageProxy ->
imageProxy.image?.let { InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees) } if (qrHandledOrClosing.get()) {
?.let { image -> imageProxy.close()
barcodeScanner.process(image).addOnSuccessListener { barcodes -> return@setAnalyzer
barcodes.firstOrNull()?.let { barcode -> }
barcode.displayValue?.let { code -> val mediaImage = imageProxy.image
if (mediaImage == null) {
imageProxy.close()
return@setAnalyzer
}
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val viewW = viewFinder.width
val viewH = viewFinder.height
val pairingRoi = if (pairingQrMode) {
pairingHoleRectInImageSpace(viewFinder, imageProxy, image.width, image.height)
} else {
null
}
val scanner = barcodeScanner ?: run {
imageProxy.close()
return@setAnalyzer
}
scanner.process(image)
.addOnSuccessListener(mainExecutor) { barcodes ->
if (qrHandledOrClosing.get()) {
return@addOnSuccessListener
}
val barcode = if (pairingQrMode) {
val roi = pairingRoi
?: PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
viewW,
viewH,
pairingGeomHeaderBottomPx,
pairingGeomStatusBarTopPx,
pairingGeomDensity,
image.width,
image.height
)
barcodes.firstOrNull {
PairingQrScanGeometry.barcodeMatchesPairingHole(
roi,
image.width,
image.height,
it
)
}
} else {
barcodes.firstOrNull()
}
barcode?.displayValue?.let { code ->
if (code.isNotEmpty() && code !in checkedBarcodes) { if (code.isNotEmpty() && code !in checkedBarcodes) {
checkedBarcodes.add(code)
if (QtAndroidController.decodeQrCode(code)) { if (QtAndroidController.decodeQrCode(code)) {
barcodeScanner.close() if (qrHandledOrClosing.compareAndSet(false, true)) {
if (pairingQrMode) {
pairingQrDeliveredToQt = true
}
stopCamera() stopCamera()
} }
checkedBarcodes.add(code)
} }
} }
} }
}.addOnFailureListener { }
.addOnFailureListener(mainExecutor) {
Log.e(TAG, "Processing QR code image failed: ${it.message}") Log.e(TAG, "Processing QR code image failed: ${it.message}")
}.addOnCompleteListener {
imageProxy.close()
} }
.addOnCompleteListener(mainExecutor) {
imageProxy.close()
} }
} }
} }
private fun stopCamera() { private fun stopCamera() {
cameraProvider.unbindAll() cleanupCameraResources()
finish() finish()
} }
} }
@@ -0,0 +1,101 @@
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
}
}
@@ -0,0 +1,152 @@
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)
}
}
@@ -0,0 +1,115 @@
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,4 +34,10 @@ 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()
} }
+3
View File
@@ -28,6 +28,7 @@ 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
@@ -44,6 +45,8 @@ 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
+1
View File
@@ -49,6 +49,7 @@ 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,6 +45,7 @@ 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
@@ -65,6 +66,8 @@ 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
@@ -122,6 +125,7 @@ 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
@@ -157,6 +161,7 @@ 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()
+176 -305
View File
@@ -4,7 +4,6 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <QThread>
#include <QUuid> #include <QUuid>
#include "logger.h" #include "logger.h"
@@ -138,324 +137,115 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
return protocolConfig; return protocolConfig;
} }
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
{
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return errorCode;
}
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript,
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
dnsSettings.secondaryDns)));
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
}
return errorCode;
}
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
QString &outPublicKey, QString &outShortId) const
{
outPublicKey.clear();
outShortId.clear();
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
out.replace(QLatin1Char('\n'), QString());
out.replace(QLatin1Char('\r'), QString());
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
};
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
}
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
const QJsonObject &existingStreamSettings) const
{
QJsonObject streamSettings = buildStreamSettings(srv, QString());
if (srv.security != QLatin1String("reality")) {
return streamSettings;
}
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
}
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
if (!merged.contains(QStringLiteral("dest"))) {
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
}
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
return streamSettings;
}
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
bool appendNewClient, QString *outClientId)
{
ErrorCode errorCode = ErrorCode::NoError;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
if (srv.isThirdPartyConfig) {
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
if (outClientId && xrayCfg->hasClientConfig()) {
*outClientId = xrayCfg->clientConfig->id;
}
return ErrorCode::NoError;
}
logger.info() << "Xray applyServerSettings: start"
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
<< "appendClient=" << appendNewClient;
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;
}
}
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
return errorCode;
}
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject serverConfig = doc.object();
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
return ErrorCode::XrayServerConfigInvalid;
}
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
if (!srv.port.isEmpty()) {
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
settings[amnezia::protocols::xray::clients] = QJsonArray {};
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
QString clientId;
if (appendNewClient) {
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QJsonObject clientEntry;
clientEntry[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientEntry[amnezia::protocols::xray::flow] = flowValue;
}
clients.append(clientEntry);
} else {
if (clients.isEmpty()) {
logger.error() << "Server config has no VLESS clients";
return ErrorCode::XrayServerNoVlessClients;
}
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
if (clientId.isEmpty()) {
logger.error() << "Server config VLESS client has empty id";
return ErrorCode::XrayServerNoVlessClients;
}
QJsonArray updatedClients;
for (const QJsonValue &v : clients) {
QJsonObject c = v.toObject();
if (flowValue.isEmpty()) {
c.remove(amnezia::protocols::xray::flow);
} else {
c[amnezia::protocols::xray::flow] = flowValue;
}
updatedClients.append(c);
}
clients = updatedClients;
}
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
if (outClientId) {
*outClientId = clientId;
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
containerConfig.protocolConfig = updated;
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
return ErrorCode::NoError;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig, const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings, const DnsSettings &dnsSettings,
ErrorCode &errorCode) ErrorCode &errorCode)
{ {
ContainerConfig mutableConfig = containerConfig; // Generate new UUID for client
QString clientId; QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
const ErrorCode applyError =
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId); // Get flow value from settings (default xtls-rprx-vision)
errorCode = applyError; QString flowValue = "xtls-rprx-vision";
if (applyError != ErrorCode::NoError || clientId.isEmpty()) { if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
return QString(); if (!xrayCfg->serverConfig.flow.isEmpty()) {
flowValue = xrayCfg->serverConfig.flow;
}
} }
return clientId;
}
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials, // Get current server config
DockerContainer container, QString currentConfig = m_sshSession->getTextFileFromContainer(
const XrayServerConfig &srv, const QString &clientId, container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
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) { if (errorCode != ErrorCode::NoError) {
return {}; logger.error() << "Failed to get server config file";
} return "";
}
} }
QJsonObject userObj; // Parse current config as JSON
userObj[amnezia::protocols::xray::id] = clientId; QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none"); if (doc.isNull() || !doc.isObject()) {
if (!srv.flow.isEmpty()) { logger.error() << "Failed to parse server config JSON";
userObj[amnezia::protocols::xray::flow] = srv.flow; errorCode = ErrorCode::InternalError;
return "";
} }
QJsonObject vnextEntry; QJsonObject serverConfig = doc.object();
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; // Validate server config structure
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry }; if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
QJsonObject outbound; errorCode = ErrorCode::InternalError;
outbound[QStringLiteral("protocol")] = QStringLiteral("vless"); return "";
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; QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject inboundObj; QJsonObject inbound = inbounds[0].toObject();
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr; if (!inbound.contains(amnezia::protocols::xray::settings)) {
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort; logger.error() << "Inbound missing 'settings' field";
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks"); errorCode = ErrorCode::InternalError;
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } }; return "";
}
QJsonObject clientJson; QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } }; if (!settings.contains(amnezia::protocols::xray::clients)) {
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj }; logger.error() << "Settings missing 'clients' field";
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound }; errorCode = ErrorCode::InternalError;
return "";
}
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact)); QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
XrayProtocolConfig protocolConfig; // Create configuration for new client
protocolConfig.serverConfig = srv; QJsonObject clientConfig {
{amnezia::protocols::xray::id, clientId},
};
clientConfig[amnezia::protocols::xray::id] = clientId;
if (!flowValue.isEmpty()) {
clientConfig[amnezia::protocols::xray::flow] = flowValue;
}
XrayClientConfig clientConfig; clients.append(clientConfig);
clientConfig.nativeConfig = config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = clientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig; // Update config
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_sshSession->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
}
return clientId;
} }
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
@@ -629,13 +419,6 @@ 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;
@@ -658,5 +441,93 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
return XrayProtocolConfig{}; return XrayProtocolConfig{};
} }
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode); // Fetch server keys (Reality only)
QString xrayPublicKey;
QString xrayShortId;
if (srv.security == "reality") {
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
xrayPublicKey.replace("\n", "");
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
return XrayProtocolConfig{};
}
xrayShortId.replace("\n", "");
}
// Build outbound
QJsonObject userObj;
userObj[amnezia::protocols::xray::id] = xrayClientId;
userObj[amnezia::protocols::xray::encryption] = "none";
if (!srv.flow.isEmpty()) {
userObj[amnezia::protocols::xray::flow] = srv.flow;
}
QJsonObject vnextEntry;
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
QJsonObject outboundSettings;
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
QJsonObject outbound;
outbound["protocol"] = "vless";
outbound[amnezia::protocols::xray::settings] = outboundSettings;
// Build streamSettings
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
// Inject Reality keys
if (srv.security == "reality") {
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
rs[amnezia::protocols::xray::shortId] = xrayShortId;
rs[amnezia::protocols::xray::spiderX] = "";
streamObj[amnezia::protocols::xray::realitySettings] = rs;
}
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
// Build full client config
QJsonObject inboundObj;
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
inboundObj["protocol"] = "socks";
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
QJsonObject clientJson;
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
// Return
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
qDebug() << "config:" << config;
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
} }
+1 -26
View File
@@ -23,37 +23,12 @@ 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);
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, // Builds the native xray "streamSettings" JSON object from XrayServerConfig
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); auto future = gatewayController->postAsync(QString("%1v1/news"), payload, nullptr, gatewayController);
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) {
@@ -0,0 +1,204 @@
#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;
}
@@ -0,0 +1,41 @@
#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
@@ -312,6 +312,71 @@ 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,
@@ -460,7 +525,6 @@ 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;
} }
@@ -935,7 +999,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); auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload, nullptr, gatewayController);
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]() {
@@ -1,6 +1,7 @@
#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>
@@ -53,6 +54,9 @@ public:
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,
@@ -6,7 +6,9 @@
#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"
@@ -65,15 +67,13 @@ 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->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns); dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
break; break;
@@ -83,7 +83,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->getDnsPair(primaryDns, secondaryDns); dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
break; break;
@@ -93,7 +93,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->getDnsPair(primaryDns, secondaryDns); dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
break; break;
@@ -105,7 +105,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->getDnsPair(primaryDns, secondaryDns); dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName; hostName = cfg->hostName;
description = cfg->description; description = cfg->description;
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway; configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
@@ -123,6 +123,16 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!isContainerSupported(container)) { if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform; 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);
+9 -4
View File
@@ -153,6 +153,7 @@ 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);
@@ -178,8 +179,7 @@ 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, m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
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);
@@ -221,10 +221,12 @@ void CoreController::initControllers()
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, m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
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);
@@ -344,6 +346,9 @@ 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);
} }
+4
View File
@@ -10,6 +10,8 @@
#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"
@@ -168,6 +170,7 @@ 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;
@@ -179,6 +182,7 @@ 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;
@@ -21,6 +21,7 @@
#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"
@@ -98,6 +99,9 @@ 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));
} }
@@ -125,9 +129,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::processedServerIdChanged, connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
m_coreController->m_installUiController, [this](const QString &serverId) { m_coreController->m_installUiController, [this](int serverIndex) {
if (!serverId.isEmpty()) { if (serverIndex >= 0) {
m_coreController->m_installUiController->clearProcessedServerCredentials(); m_coreController->m_installUiController->clearProcessedServerCredentials();
} }
}); });
+193 -46
View File
@@ -10,6 +10,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QPromise> #include <QPromise>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include "QBlockCipher.h" #include "QBlockCipher.h"
@@ -21,12 +22,25 @@
#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.");
@@ -42,12 +56,24 @@ namespace
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(gatewayEndpoint), m_gatewayEndpoint(normalizedGatewayBase(gatewayEndpoint)),
m_isDevEnvironment(isDevEnvironment), m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs), m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
@@ -135,6 +161,8 @@ 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;
@@ -151,6 +179,29 @@ 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);
@@ -165,7 +216,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; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
@@ -174,8 +225,18 @@ 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 =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); resolveResponseBody(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) {
@@ -191,7 +252,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult = decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); resolveResponseBody(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)) {
@@ -221,11 +282,15 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
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()));
@@ -234,12 +299,22 @@ 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, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable { connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, life]() mutable {
if (!life) {
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
return;
}
GatewayController *const ctl = life.data();
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
auto replyError = reply->error(); auto replyError = reply->error();
@@ -247,8 +322,20 @@ 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 =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); ctl->resolveResponseBody(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,
@@ -273,13 +360,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
promise->finish(); promise->finish();
}; };
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) { if (sslErrors->isEmpty() && ctl->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 (m_isDevEnvironment) { if (ctl->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);
@@ -306,16 +393,24 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
appendStorageUrls(primaryBaseUrls, proxyStorageUrls); appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { life->getProxyUrlsAsync(life, proxyStorageUrls, 0,
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) { [life, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData, life->getProxyUrlAsync(life, proxyUrls, 0,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful, [life, encRequestData, endpoint, processResponse](
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError, const QString &proxyUrl) {
const QString &replyErrorString, int httpStatusCode) { life->bypassProxyAsync(
life, endpoint, proxyUrl, encRequestData,
[processResponse](const QByteArray &decryptedBody,
bool isDecryptionSuccessful,
const QList<QSslError> &sslErrors,
QNetworkReply::NetworkError replyError,
const QString &replyErrorString,
int httpStatusCode) {
GatewayController::DecryptionResult result; GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody; result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful; result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode); processResponse(result, sslErrors, replyError,
replyErrorString, httpStatusCode);
}); });
}); });
}); });
@@ -381,7 +476,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; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
if (reply->error() == QNetworkReply::NetworkError::NoError) { if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll(); auto encryptedResponseBody = reply->readAll();
@@ -434,6 +529,10 @@ 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;
@@ -514,7 +613,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; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
auto result = replyProcessingFunction(reply, sslErrors); auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater(); reply->deleteLater();
@@ -536,7 +635,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; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
if (reply->error() == QNetworkReply::NetworkError::NoError) { if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater(); reply->deleteLater();
@@ -565,9 +664,14 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
} }
} }
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex, void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls,
std::function<void(const QStringList &)> onComplete) const int currentProxyStorageIndex, const std::function<void(const QStringList &)> &onComplete)
{ {
if (!life) {
onComplete({});
return;
}
if (currentProxyStorageIndex >= proxyStorageUrls.size()) { if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({}); onComplete({});
return; return;
@@ -580,17 +684,23 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
QNetworkReply *reply = amnApp->networkManager()->get(request); QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; }); connect(reply, &QNetworkReply::finished, reply, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
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 = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QByteArray key = ctl->m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) { if (!ctl->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();
@@ -607,15 +717,21 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
} catch (...) { } catch (...) {
Utils::logException(); Utils::logException();
qCritical() << "error decrypting payload"; qCritical() << "error decrypting payload";
QMetaObject::invokeMethod( QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); if (life) {
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;
@@ -630,16 +746,26 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
qDebug() << httpStatusCode; qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint"; qDebug() << "go to the next storage endpoint";
reply->deleteLater(); reply->deleteLater();
QMetaObject::invokeMethod( QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection); if (life) {
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
} else {
onComplete({});
}
});
}); });
} }
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls,
std::function<void(const QString &)> onComplete) const int currentProxyIndex, const std::function<void(const QString &)> &onComplete)
{ {
if (!life) {
onComplete(QString());
return;
}
if (currentProxyIndex >= proxyUrls.size()) { if (currentProxyIndex >= proxyUrls.size()) {
onComplete(""); onComplete(QString());
return; return;
} }
@@ -650,13 +776,16 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
QNetworkReply *reply = amnApp->networkManager()->get(request); QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { connect(reply, &QNetworkReply::finished, reply, [life, proxyUrls, currentProxyIndex, onComplete, reply]() {
// *(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);
@@ -664,15 +793,28 @@ void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int
} }
qDebug() << "go to the next proxy endpoint"; qDebug() << "go to the next proxy endpoint";
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection); QTimer::singleShot(0, ctl, [life, proxyUrls, currentProxyIndex, onComplete]() {
if (life) {
life->getProxyUrlAsync(life, proxyUrls, currentProxyIndex + 1, onComplete);
} else {
onComplete(QString());
}
});
}); });
} }
void GatewayController::bypassProxyAsync( void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete) const EncryptedRequestData &encRequestData,
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;
@@ -683,9 +825,9 @@ void GatewayController::bypassProxyAsync(
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody); QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, reply, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() { connect(reply, &QNetworkReply::finished, reply, [life, sslErrors, onComplete, encRequestData, reply]() {
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
auto replyError = reply->error(); auto replyError = reply->error();
@@ -693,8 +835,13 @@ void GatewayController::bypassProxyAsync(
reply->deleteLater(); reply->deleteLater();
auto decryptionResult = if (!life) {
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt); onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
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);
+14 -6
View File
@@ -1,6 +1,8 @@
#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>
@@ -25,7 +27,9 @@ 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
@@ -36,6 +40,7 @@ private:
QByteArray iv; QByteArray iv;
QByteArray salt; QByteArray salt;
amnezia::ErrorCode errorCode; amnezia::ErrorCode errorCode;
bool isPlaintextLocalGateway = false;
}; };
struct DecryptionResult struct DecryptionResult
@@ -47,6 +52,8 @@ 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);
@@ -54,12 +61,13 @@ 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 QStringList proxyStorageUrls, const int currentProxyStorageIndex, void getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls, int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete); const std::function<void(const QStringList &)> &onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete); void getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, int currentProxyIndex,
const std::function<void(const QString &)> &onComplete);
void bypassProxyAsync( void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData, const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, 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);
int m_requestTimeoutMsecs; int m_requestTimeoutMsecs;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
@@ -20,7 +20,6 @@
#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"
@@ -120,14 +119,9 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
return e; return e;
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished"; qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
if (!isUpdate) {
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
}
sshSession.runScript(credentials, sshSession.runScript(credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
removeContainerVars)); amnezia::genBaseVars(credentials, container, QString(), QString())));
qDebug().noquote() << "InstallController::setupContainer removeContainer finished"; qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
qDebug().noquote() << "buildContainerWorker start"; qDebug().noquote() << "buildContainerWorker start";
@@ -187,16 +181,6 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig); bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
qDebug() << "InstallController::updateContainer for container" << container << "reinstall required is" << reinstallRequired; qDebug() << "InstallController::updateContainer 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) {
errorCode = setupContainer(credentials, container, newConfig, true); errorCode = setupContainer(credentials, container, newConfig, true);
@@ -207,21 +191,6 @@ 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::updateContainer applying Xray server inbound sync, reinstall="
<< reinstallRequired;
errorCode = xrayConfigurator.applyServerSettingsToRemote(credentials, container, newConfig, dnsSettings, false);
if (errorCode != ErrorCode::NoError) {
qDebug() << "InstallController::updateContainer 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);
@@ -247,9 +216,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);
@@ -257,74 +226,37 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId) ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
{ {
const auto kind = m_serversRepository->serverKind(serverId);
DockerContainer container = DockerContainer::None;
ContainerConfig containerConfig;
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
containerConfig = cfg->containerConfig(container);
break;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
containerConfig = cfg->containerConfig(container);
break;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
containerConfig = cfg->containerConfig(container);
break;
}
default:
return ErrorCode::InternalError;
}
if (container == DockerContainer::None) {
return ErrorCode::NoInstalledContainersError;
}
if (containerConfig.protocolConfig.hasClientConfig()) {
return ErrorCode::NoError;
}
if (kind != serverConfigUtils::ConfigType::SelfHostedAdmin) {
return ErrorCode::InternalError;
}
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId); auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) { if (!adminConfig.has_value()) {
return ErrorCode::InternalError; return ErrorCode::InternalError;
} }
DockerContainer container = adminConfig->defaultContainer;
if (container == DockerContainer::None) {
return ErrorCode::NoInstalledContainersError;
}
ContainerConfig containerConfig = adminConfig->containerConfig(container);
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());
const ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName); auto isProtocolConfigExists = [](const ContainerConfig &cfg) {
return cfg.protocolConfig.hasClientConfig();
};
if (!isProtocolConfigExists(containerConfig)) {
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
if (errorCode != ErrorCode::NoError) { if (errorCode != ErrorCode::NoError) {
return errorCode; return errorCode;
} }
adminConfig->updateContainerConfig(container, containerConfig); adminConfig->updateContainerConfig(container, containerConfig);
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
}
return ErrorCode::NoError; return ErrorCode::NoError;
} }
@@ -669,21 +601,14 @@ 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) {
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty() if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
? 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;
} }
} }
}
if (container == DockerContainer::MtProxy) { if (container == DockerContainer::MtProxy) {
const auto *oldMt = oldConfig.getMtProxyProtocolConfig(); const auto *oldMt = oldConfig.getMtProxyProtocolConfig();
@@ -980,12 +905,10 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
return ErrorCode::InternalError; return ErrorCode::InternalError;
} }
SshSession sshSession(this); SshSession sshSession(this);
amnezia::ScriptVars removeContainerVars =
amnezia::genBaseVars(credentials, container, QString(), QString());
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
ErrorCode errorCode = sshSession.runScript( ErrorCode errorCode = sshSession.runScript(
credentials, credentials,
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), removeContainerVars)); sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
amnezia::genBaseVars(credentials, container, QString(), QString())));
if (errorCode == ErrorCode::NoError) { if (errorCode == ErrorCode::NoError) {
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers; QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
@@ -44,7 +44,6 @@ 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;
} }
@@ -52,7 +51,6 @@ 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;
} }
@@ -60,7 +58,6 @@ 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;
} }
@@ -70,7 +67,6 @@ 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,11 +217,6 @@ 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();
} }
+35 -10
View File
@@ -21,13 +21,13 @@ namespace
Logger logger("UpdateController"); Logger logger("UpdateController");
#if defined(Q_OS_WINDOWS) #if defined(Q_OS_WINDOWS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe"); const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.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) && !defined(MACOS_NE) #elif defined(Q_OS_MACOS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg"); const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.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_x64.run"); const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run"; const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
#endif #endif
} }
@@ -57,6 +57,10 @@ 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();
@@ -93,6 +97,11 @@ 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,
@@ -105,11 +114,19 @@ 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]() {
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload) if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) { finishUpdateCheck();
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.debug() << "Update check: updater_endpoint not found on gateway";
} else {
logger.error() << errorString(err); logger.error() << errorString(err);
}
finishUpdateCheck(); finishUpdateCheck();
return; return;
} }
@@ -196,13 +213,21 @@ 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
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error())); logger.error() << "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() << "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) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
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
@@ -212,7 +237,7 @@ QString UpdateController::composeDownloadUrl() const
void UpdateController::runInstaller() void UpdateController::runInstaller()
{ {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (m_downloadUrl.isEmpty()) { if (m_downloadUrl.isEmpty()) {
logger.error() << "Download URL is empty"; logger.error() << "Download URL is empty";
return; return;
@@ -244,7 +269,7 @@ void UpdateController::runInstaller()
#if defined(Q_OS_WINDOWS) #if defined(Q_OS_WINDOWS)
runWindowsInstaller(kInstallerLocalPath); runWindowsInstaller(kInstallerLocalPath);
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE) #elif defined(Q_OS_MACOS)
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);
@@ -284,7 +309,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
} }
#endif #endif
#if defined(Q_OS_MACOS) && !defined(MACOS_NE) #if defined(Q_OS_MACOS)
int UpdateController::runMacInstaller(const QString &installerPath) int UpdateController::runMacInstaller(const QString &installerPath)
{ {
// Create temporary directory for extraction // Create temporary directory for extraction
+6 -27
View File
@@ -1,17 +1,15 @@
#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;
@@ -35,29 +33,10 @@ 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)
{ {
if (container != DockerContainer::Socks5Proxy || !sshSession) { Q_UNUSED(container);
return ErrorCode::NoError; Q_UNUSED(credentials);
} 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;
} }
@@ -13,7 +13,6 @@
#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
{ {
@@ -68,20 +67,6 @@ 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;
@@ -3,7 +3,6 @@
#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"
@@ -44,9 +43,6 @@ 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);
}; };
@@ -108,114 +108,35 @@ 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)
{ {
if (json.isEmpty()) { XrayXhttpConfig c;
return clearedXhttpConfig(); c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
} c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
XrayXhttpConfig c = clearedXhttpConfig();
if (json.contains(configKey::xhttpMode)) {
c.mode = json.value(configKey::xhttpMode).toString();
}
if (json.contains(configKey::xhttpHost)) {
c.host = json.value(configKey::xhttpHost).toString();
}
if (json.contains(configKey::xhttpPath)) {
c.path = json.value(configKey::xhttpPath).toString(); c.path = json.value(configKey::xhttpPath).toString();
} c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
if (json.contains(configKey::xhttpHeadersTemplate)) { c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(); c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
} c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
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();
}
if (json.contains(QLatin1String("xPadding"))) { c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject()); c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
} c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
if (json.contains(QLatin1String("xmux"))) { c.seqKey = json.value(configKey::xhttpSeqKey).toString();
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("xmux")).toObject()); 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");
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
return c; return c;
} }
@@ -235,27 +156,12 @@ QJsonObject XrayMkcpConfig::toJson() const
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json) XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
{ {
XrayMkcpConfig c; XrayMkcpConfig c;
if (json.isEmpty()) {
return c;
}
if (json.contains(configKey::mkcpTti)) {
c.tti = json.value(configKey::mkcpTti).toString(); c.tti = json.value(configKey::mkcpTti).toString();
}
if (json.contains(configKey::mkcpUplinkCapacity)) {
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString(); c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
}
if (json.contains(configKey::mkcpDownlinkCapacity)) {
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString(); c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
}
if (json.contains(configKey::mkcpReadBufferSize)) {
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString(); c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
}
if (json.contains(configKey::mkcpWriteBufferSize)) {
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString(); c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
} c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
if (json.contains(configKey::mkcpCongestion)) {
c.congestion = json.value(configKey::mkcpCongestion).toBool();
}
return c; return c;
} }
@@ -302,14 +208,8 @@ QJsonObject XrayServerConfig::toJson() const
if (!transport.isEmpty()) { if (!transport.isEmpty()) {
obj[configKey::xrayTransport] = transport; obj[configKey::xrayTransport] = transport;
} }
const QJsonObject xhttpObj = xhttp.toJson(); obj["xhttp"] = xhttp.toJson();
if (!xhttpObj.isEmpty()) { obj["mkcp"] = mkcp.toJson();
obj[QStringLiteral("xhttp")] = xhttpObj;
}
const QJsonObject mkcpObj = mkcp.toJson();
if (!mkcpObj.isEmpty()) {
obj[QStringLiteral("mkcp")] = mkcpObj;
}
return obj; return obj;
} }
@@ -325,39 +225,20 @@ 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);
if (json.contains(configKey::xraySecurity)) { // New: Security
c.security = json.value(configKey::xraySecurity).toString(); c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
} c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
if (json.contains(configKey::xrayFlow)) { c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
c.flow = json.value(configKey::xrayFlow).toString();
}
if (json.contains(configKey::xrayFingerprint)) {
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
} }
} c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
if (json.contains(configKey::xraySni)) { c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
c.sni = json.value(configKey::xraySni).toString();
} // New: Transport
if (json.contains(configKey::xrayAlpn)) { c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
c.alpn = json.value(configKey::xrayAlpn).toString(); c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
} c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
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);
}
}
return c; return c;
} }
@@ -370,10 +251,7 @@ 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
@@ -473,154 +351,9 @@ 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,7 +75,6 @@ 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);
}; };
@@ -100,13 +99,15 @@ struct XrayServerConfig {
QString site; QString site;
bool isThirdPartyConfig = false; bool isThirdPartyConfig = false;
QString security; // New: Security
QString flow; QString security = protocols::xray::defaultSecurity;
QString fingerprint; QString flow = protocols::xray::defaultFlow;
QString sni; QString fingerprint = protocols::xray::defaultFingerprint;
QString alpn; QString sni = protocols::xray::defaultSni;
QString alpn = protocols::xray::defaultAlpn;
QString transport; // New: Transport
QString transport = protocols::xray::defaultTransport;
XrayXhttpConfig xhttp; XrayXhttpConfig xhttp;
XrayMkcpConfig mkcp; XrayMkcpConfig mkcp;
@@ -138,10 +139,6 @@ 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,7 +9,6 @@
#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
{ {
@@ -29,20 +28,6 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container); return containers.value(container);
} }
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;
@@ -3,7 +3,6 @@
#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"
@@ -26,9 +25,6 @@ struct NativeServerConfig {
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 NativeServerConfig fromJson(const QJsonObject& json); static NativeServerConfig fromJson(const QJsonObject& json);
}; };
@@ -8,7 +8,6 @@
#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
{ {
@@ -43,21 +42,6 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
return containers.value(container); return containers.value(container);
} }
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;
@@ -3,7 +3,6 @@
#include <QJsonObject> #include <QJsonObject>
#include <QMap> #include <QMap>
#include <QPair>
#include <optional> #include <optional>
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
@@ -31,9 +30,6 @@ 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;
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);
}; };
+59
View File
@@ -2,6 +2,7 @@
#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>
@@ -76,6 +77,26 @@ 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)
@@ -103,6 +124,10 @@ 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();
@@ -128,9 +153,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (httpStatusFromBody == httpStatusCodePaymentRequired) { if (httpStatusFromBody == httpStatusCodePaymentRequired) {
return amnezia::ErrorCode::ApiSubscriptionNotActiveError; return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
} }
const QString msg = apiErrorMessageFromJson(jsonObj);
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
}
if (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; return amnezia::ErrorCode::ApiConfigDownloadError;
} }
if (httpStatusCode == httpStatusCodeNotFound) {
return amnezia::ErrorCode::ApiNotFoundError;
}
qDebug() << "something went wrong"; qDebug() << "something went wrong";
return amnezia::ErrorCode::ApiConfigDownloadError; return amnezia::ErrorCode::ApiConfigDownloadError;
} }
@@ -228,3 +272,18 @@ 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,8 +23,13 @@ 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,6 +22,7 @@ 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");
+9 -3
View File
@@ -35,9 +35,6 @@ 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,
@@ -102,6 +99,15 @@ namespace amnezia
ApiNoPurchasedSubscriptionsError = 1115, ApiNoPurchasedSubscriptionsError = 1115,
ApiTrialAlreadyUsedError = 1116, ApiTrialAlreadyUsedError = 1116,
// QR pairing (gateway /v1/generate_qr, /v1/scan_qr)
ApiPairingForbiddenError = 1117,
ApiPairingConflictError = 1118,
ApiPairingRateLimitedError = 1119,
ApiPairingServiceUnavailableError = 1120,
ApiPairingPayloadTooLargeError = 1121,
ApiPairingMissingMetadataError = 1122,
ApiPairingSessionExpiredError = 1123,
// QFile errors // QFile errors
OpenError = 1200, OpenError = 1200,
ReadError = 1201, ReadError = 1201,
+7 -9
View File
@@ -30,15 +30,6 @@ 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;
@@ -93,6 +84,13 @@ 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::ApiPairingConflictError): errorMessage = QObject::tr("This QR code has already been used"); break;
case (ErrorCode::ApiPairingRateLimitedError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
case (ErrorCode::ApiPairingServiceUnavailableError): errorMessage = QObject::tr("Service temporarily unavailable. Please try again later"); break;
case (ErrorCode::ApiPairingPayloadTooLargeError): errorMessage = QObject::tr("QR pairing data is too large to send"); break;
case (ErrorCode::ApiPairingMissingMetadataError): errorMessage = QObject::tr("This subscription is missing data required to transfer via QR (service type or country). Refresh the subscription or pick another server."); break;
case (ErrorCode::ApiPairingSessionExpiredError): errorMessage = QObject::tr("The QR code session has ended. Show a new QR code on the other device and scan again."); break;
// 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,6 +3,14 @@
#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,6 +10,7 @@ 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,8 +295,6 @@ 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)
@@ -352,8 +350,6 @@ 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()) {
+2 -6
View File
@@ -1,12 +1,13 @@
#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
@@ -46,11 +47,6 @@ 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,6 +9,7 @@
#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
{ {
@@ -103,7 +104,10 @@ 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;
@@ -201,6 +205,21 @@ 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");
@@ -226,6 +245,11 @@ 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);
@@ -538,7 +562,11 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
{ {
Q_UNUSED(thiz); Q_UNUSED(thiz);
return ImportUiController::decodeQrCode(AndroidUtils::convertJString(env, data)); const QString code = 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)
@@ -578,4 +606,31 @@ 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,11 +38,15 @@ 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();
@@ -77,6 +81,7 @@ 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;
@@ -109,6 +114,9 @@ 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,3 +12,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 &) {}
+1
View File
@@ -16,6 +16,7 @@ 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);
+143 -36
View File
@@ -1,16 +1,56 @@
#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) QRCodeReader* qrCodeReader; @property (nonatomic, assign) QRCodeReader *qrCodeReader;
@property (nonatomic, strong) AVCaptureSession *captureSession; @property (nonatomic, retain) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer; @property (nonatomic, retain) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
@property (nonatomic) dispatch_queue_t sessionQueue;
@end @end
@@ -19,61 +59,115 @@
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
_captureSession = nil; self.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)startReading { - (BOOL)startReadingOnMainThread {
NSError *error; [self stopReadingOnMainThread];
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; NSError *error = nil;
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error];
if(!deviceInput) { AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSLog(@"Error %@", error.localizedDescription); if (!captureDevice) {
return NO; return NO;
} }
_captureSession = [[AVCaptureSession alloc]init]; AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
[_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];
[_captureSession addOutput:capturedMetadataOutput]; [session addOutput:capturedMetadataOutput];
dispatch_queue_t dispatchQueue; if (!_sessionQueue) {
dispatchQueue = dispatch_queue_create("myQueue", NULL); _sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
[capturedMetadataOutput setMetadataObjectsDelegate: self queue: dispatchQueue]; }
[capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]]; [capturedMetadataOutput setMetadataObjectsDelegate:self queue:_sessionQueue];
[capturedMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
_videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession]; self.captureSession = session;
[session release];
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height; AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
[preview setVideoGravity:AVLayerVideoGravityResizeAspectFill];
self.videoPreviewPlayer = preview;
[preview release];
QRect cameraRect = _qrCodeReader->cameraSize(); UIWindow *keyWindow = amneziaKeyWindowForQrCamera();
CGRect cameraCGRect = CGRectMake(cameraRect.x(), if (!keyWindow) {
cameraRect.y() + statusBarHeight, [self stopReadingOnMainThread];
cameraRect.width(), return NO;
cameraRect.height()); }
[_videoPreviewPlayer setVideoGravity: AVLayerVideoGravityResizeAspectFill]; CGRect bounds = keyWindow.bounds;
[_videoPreviewPlayer setFrame: cameraCGRect]; [self.videoPreviewPlayer setFrame:bounds];
self.videoPreviewPlayer.zPosition = -1000.f;
[keyWindow.layer insertSublayer:self.videoPreviewPlayer atIndex:0];
CALayer* layer = [UIApplication sharedApplication].keyWindow.layer; AVCaptureSession *runningSession = self.captureSession;
[layer addSublayer: _videoPreviewPlayer]; dispatch_async(_sessionQueue, ^{
[runningSession startRunning];
[_captureSession startRunning]; });
return YES; return YES;
} }
- (void)stopReading { - (BOOL)startReading {
[_captureSession stopRunning]; if ([NSThread isMainThread]) {
_captureSession = nil; return [self startReadingOnMainThread];
}
__block BOOL ok = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
ok = [self startReadingOnMainThread];
});
return ok;
}
[_videoPreviewPlayer removeFromSuperlayer]; - (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 {
if ([NSThread isMainThread]) {
[self stopReadingOnMainThread];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[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 {
@@ -82,7 +176,15 @@
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0]; AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) { if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) {
_qrCodeReader->emit codeReaded([metadataObject stringValue].UTF8String); NSString *value = [metadataObject stringValue];
if (value.length == 0) {
return;
}
QRCodeReader *cpp = _qrCodeReader;
const QByteArray utf8([value UTF8String]);
dispatch_async(dispatch_get_main_queue(), ^{
cpp->notifyCodeRead(QString::fromUtf8(utf8));
});
} }
} }
} }
@@ -109,6 +211,10 @@ 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"
@@ -124,4 +230,5 @@ 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
@@ -0,0 +1,10 @@
#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
@@ -0,0 +1,37 @@
#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];
}
}
@@ -0,0 +1,13 @@
#include "platforms/ios/iosPairingCameraAccess.h"
bool amneziaIosPairingCameraAccessGranted()
{
return true;
}
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
{
onDone(true);
}
void amneziaIosOpenApplicationSettings() {}
@@ -0,0 +1,16 @@
#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
@@ -0,0 +1,836 @@
#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,10 +4,8 @@
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: regenerate (fresh install) -> env var -> saved file -> generate new # Determine secret: env var -> saved file -> generate new
if [ "$MTPROXY_REGENERATE_SECRET" = "1" ]; then if [ -n "$MTPROXY_SECRET" ]; 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 -2
View File
@@ -1,4 +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
test "$REMOVE_CONTAINER_DATA" = "1" && sudo docker volume rm -f ${CONTAINER_NAME}-data 2>/dev/null || true
@@ -4,10 +4,8 @@
echo "[*] Amnezia Telemt: configure script start" echo "[*] Amnezia Telemt: configure script start"
mkdir -p /data/tlsfront mkdir -p /data/tlsfront
# Secret: regenerate (fresh install) -> env var -> saved file -> openssl # Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure)
if [ "$TELEMT_REGENERATE_SECRET" = "1" ]; then if [ -n "$TELEMT_SECRET" ]; 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)
+10
View File
@@ -131,6 +131,15 @@ target_link_libraries(test_self_hosted_server_setup PRIVATE
test_common test_common
) )
add_executable(test_pairing_parsers
testPairingParsers.cpp
)
target_link_libraries(test_pairing_parsers PRIVATE
Qt6::Test
test_common
)
enable_testing() enable_testing()
add_test(NAME ImportExportTest COMMAND test_import_export) add_test(NAME ImportExportTest COMMAND test_import_export)
add_test(NAME MultipleImportsTest COMMAND test_multiple_imports) add_test(NAME MultipleImportsTest COMMAND test_multiple_imports)
@@ -143,3 +152,4 @@ add_test(NAME ComplexOperationsTest COMMAND test_complex_operations)
add_test(NAME SettingsSignalsTest COMMAND test_settings_signals) add_test(NAME SettingsSignalsTest COMMAND test_settings_signals)
add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller) add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller)
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup) add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
add_test(NAME PairingParsersTest COMMAND test_pairing_parsers)
+1 -1
View File
@@ -38,7 +38,7 @@ private slots:
void init() { void init() {
m_settings->clearSettings(); m_settings->clearSettings();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
+1 -1
View File
@@ -40,7 +40,7 @@ private slots:
m_settings->clearSettings(); m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache(); m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
+1 -1
View File
@@ -41,7 +41,7 @@ private slots:
m_settings->clearSettings(); m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache(); m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
+165
View File
@@ -0,0 +1,165 @@
#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"
+1 -1
View File
@@ -146,7 +146,7 @@ private slots:
void init() { void init() {
m_settings->clearSettings(); m_settings->clearSettings();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
+1 -1
View File
@@ -40,7 +40,7 @@ private slots:
m_settings->clearSettings(); m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache(); m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
+1 -1
View File
@@ -40,7 +40,7 @@ private slots:
m_settings->clearSettings(); m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache(); m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
+1 -1
View File
@@ -39,7 +39,7 @@ private slots:
void init() { void init() {
m_settings->clearSettings(); m_settings->clearSettings();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
-17
View File
@@ -133,8 +133,6 @@ private slots:
void testStartMinimizedSignals() { void testStartMinimizedSignals() {
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged); QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
m_coreController->m_settingsUiController->toggleAutoStart(true);
bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled(); bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled();
m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized); m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized);
@@ -142,21 +140,6 @@ private slots:
QVERIFY2(m_coreController->m_settingsController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be updated in SettingsController"); 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_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"); QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAutoStart(false);
}
void testAutoStartDisablesStartMinimizedUi() {
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
m_coreController->m_settingsUiController->toggleAutoStart(true);
m_coreController->m_settingsUiController->toggleStartMinimized(true);
QVERIFY2(m_coreController->m_settingsUiController->isStartMinimizedEnabled(), "Start minimized should be enabled with autostart");
m_coreController->m_settingsUiController->toggleAutoStart(false);
QVERIFY2(startMinimizedChangedSpy.count() >= 1, "startMinimizedChanged signal should be emitted when autostart is disabled");
QVERIFY2(!m_coreController->m_settingsUiController->isStartMinimizedEnabled(), "Start minimized should be disabled when autostart is disabled");
QVERIFY2(!m_coreController->m_appSettingsRepository->isStartMinimized(), "Start minimized setting should be cleared when autostart is disabled");
} }
void testAutoConnectSignals() { void testAutoConnectSignals() {
+1 -1
View File
@@ -38,7 +38,7 @@ private slots:
m_settings->clearSettings(); m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache(); m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
@@ -23,6 +23,18 @@ using namespace amnezia;
using namespace amnezia; 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 class TestUiServersModelAndController : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -119,7 +131,7 @@ private slots:
void init() { void init() {
m_settings->clearSettings(); m_settings->clearSettings();
if (m_coreController->m_serversModel) { if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString()); m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
} }
} }
@@ -262,7 +274,7 @@ private slots:
QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions( QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns()); m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId(); const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsNoDns, defIdNoDns); m_coreController->m_serversModel->updateModel(descriptionsNoDns, defaultServerRow(descriptionsNoDns, defIdNoDns));
QString descNoDns = m_coreController->m_serversModel->data( QString descNoDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString(); m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
@@ -281,7 +293,7 @@ private slots:
QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions( QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns()); m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId(); const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsWithDns, defIdWithDns); m_coreController->m_serversModel->updateModel(descriptionsWithDns, defaultServerRow(descriptionsWithDns, defIdWithDns));
QString descWithDns = m_coreController->m_serversModel->data( QString descWithDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString(); m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
+10
View File
@@ -1822,6 +1822,16 @@ 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>
@@ -0,0 +1,739 @@
#include "pairingUiController.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QIODevice>
#include <QJsonArray>
#include <QJsonObject>
#include <QMetaObject>
#include <QPointer>
#include <QRegularExpression>
#include <QSet>
#include <QTimer>
#include <QUuid>
#include <string>
#include "platforms/ios/iosPairingCameraAccess.h"
#if defined(Q_OS_IOS)
#include "platforms/ios/iosPairingQrOverlayWindow.h"
#endif
#if defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#endif
#include "core/controllers/gatewayController.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/qrCodeUtils.h"
using namespace amnezia;
namespace
{
constexpr auto kGenerateQrPath = "%1v1/generate_qr";
constexpr auto kScanQrPath = "%1v1/scan_qr";
constexpr auto kGatewayProbePath = "%1v1/news";
constexpr int kPairingRetryMaxAttempts = 3;
constexpr int kGatewayProbeTimeoutMsecs = 3000;
QJsonObject apiGatewayServicesFromServers(const ServersController *serversController)
{
if (!serversController || serversController->getServersCount() == 0) {
return {};
}
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
for (int i = 0; i < serversController->getServersCount(); ++i) {
const QString serverId = serversController->getServerId(i);
const auto apiV2 = serversController->apiV2Config(serverId);
if (!apiV2.has_value()) {
continue;
}
if (!apiV2->apiConfig.userCountryCode.isEmpty()) {
userCountryCodes.insert(apiV2->apiConfig.userCountryCode);
}
const QString serviceType = apiV2->serviceType();
if (!serviceType.isEmpty()) {
serviceTypes.insert(serviceType);
}
}
if (userCountryCodes.isEmpty() && serviceTypes.isEmpty()) {
return {};
}
QJsonObject json;
QJsonArray userCountryCodesArray;
for (const QString &code : userCountryCodes) {
userCountryCodesArray.append(code);
}
json.insert(apiDefs::key::userCountryCode, userCountryCodesArray);
QJsonArray serviceTypesArray;
for (const QString &type : serviceTypes) {
serviceTypesArray.append(type);
}
json.insert(apiDefs::key::serviceType, serviceTypesArray);
return json;
}
bool isPairingRetriableError(ErrorCode code)
{
switch (code) {
case ErrorCode::ApiPairingRateLimitedError:
case ErrorCode::ApiPairingServiceUnavailableError:
case ErrorCode::ApiConfigDownloadError:
return true;
default:
return false;
}
}
int pairingRetryDelayMs(int zeroBasedAttempt)
{
constexpr int baseMs = 500;
return baseMs * (1 << zeroBasedAttempt);
}
QString extractPairingSessionUuidFromScanText(const QString &raw)
{
const QString t = raw.trimmed();
if (t.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) {
return {};
}
static const QRegularExpression reV4(QStringLiteral(
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"));
const QRegularExpressionMatch m = reV4.match(t);
if (m.hasMatch()) {
return m.captured(0);
}
const QUuid parsed = QUuid::fromString(t);
if (!parsed.isNull()) {
return parsed.toString(QUuid::WithoutBraces);
}
return {};
}
} // namespace
#if defined(Q_OS_ANDROID)
namespace {
PairingUiController *g_pairingUiForAndroidQr = nullptr;
}
#endif
PairingUiController::PairingUiController(PairingController *pairingController, ServersController *serversController,
SubscriptionController *subscriptionController,
SecureAppSettingsRepository *appSettingsRepository, QObject *parent)
: QObject(parent),
m_pairingController(pairingController),
m_serversController(serversController),
m_subscriptionController(subscriptionController),
m_appSettingsRepository(appSettingsRepository)
{
#if defined(Q_OS_ANDROID)
g_pairingUiForAndroidQr = this;
connect(AndroidController::instance(), &AndroidController::cameraPermissionResult, this,
[this](bool granted) { emit pairingCameraAccessFinished(granted); });
#endif
}
PairingUiController::~PairingUiController()
{
#if defined(Q_OS_ANDROID)
if (g_pairingUiForAndroidQr == this) {
g_pairingUiForAndroidQr = nullptr;
}
#endif
#if defined(Q_OS_IOS)
amneziaIosPairingQrOverlayDismiss();
#endif
}
void PairingUiController::setPendingPhonePairingUuid(const QString &uuid)
{
const QString trimmed = uuid.trimmed();
if (m_pendingPhonePairingUuid == trimmed) {
return;
}
m_pendingPhonePairingUuid = trimmed;
emit pendingPhonePairingUuidChanged();
}
void PairingUiController::clearPendingPhonePairingUuid()
{
if (m_pendingPhonePairingUuid.isEmpty()) {
return;
}
m_pendingPhonePairingUuid.clear();
emit pendingPhonePairingUuidChanged();
}
void PairingUiController::openPairingQrScanner()
{
#if defined(Q_OS_ANDROID)
AndroidController::instance()->startPairingQrReaderActivity();
#endif
}
bool PairingUiController::isPairingCameraAccessGranted() const
{
#if defined(Q_OS_ANDROID)
return AndroidController::instance()->isCameraPermissionGranted();
#elif defined(Q_OS_IOS)
return amneziaIosPairingCameraAccessGranted();
#else
return true;
#endif
}
void PairingUiController::requestPairingCameraAccess()
{
#if defined(Q_OS_ANDROID)
AndroidController::instance()->requestCameraPermissionForQrPairing();
#elif defined(Q_OS_IOS)
amneziaIosRequestPairingCameraAccess([this](bool granted) {
QMetaObject::invokeMethod(
this, [this, granted]() { emit pairingCameraAccessFinished(granted); }, Qt::QueuedConnection);
});
#else
emit pairingCameraAccessFinished(true);
#endif
}
void PairingUiController::openPairingCameraAppSettings()
{
#if defined(Q_OS_ANDROID)
AndroidController::instance()->openApplicationDetailsSettings();
#elif defined(Q_OS_IOS)
amneziaIosOpenApplicationSettings();
#endif
}
void PairingUiController::setPairingQrTorchEnabled(bool enabled)
{
#if defined(Q_OS_ANDROID)
Q_UNUSED(enabled);
#elif defined(Q_OS_IOS)
amneziaIosPairingQrOverlaySetTorchEnabled(enabled);
#else
Q_UNUSED(enabled);
#endif
}
void PairingUiController::presentIosPairingQrNativeOverlayScanner(const QString &title, const QString &subtitle)
{
#if defined(Q_OS_IOS)
const std::string titleUtf8 = title.isEmpty() ? std::string() : title.toStdString();
const std::string subtitleUtf8 = subtitle.isEmpty() ? std::string() : subtitle.toStdString();
amneziaIosPairingQrOverlayPresent(
[this](const char *utf8) {
const QString code = QString::fromUtf8(utf8);
QMetaObject::invokeMethod(
this,
[this, code]() {
if (!applyScannedTextAsPairingUuid(code)) {
emit pairingSendQrScanRejectedInvalidPayload();
}
},
Qt::QueuedConnection);
},
[this]() {
QMetaObject::invokeMethod(
this,
[this]() { emit pairingIosNativeQrOverlayBackRequested(); },
Qt::QueuedConnection);
},
titleUtf8, subtitleUtf8);
#else
Q_UNUSED(title);
Q_UNUSED(subtitle);
#endif
}
void PairingUiController::dismissIosPairingQrNativeOverlayScanner()
{
#if defined(Q_OS_IOS)
amneziaIosPairingQrOverlayDismiss();
#endif
}
void PairingUiController::restartIosPairingQrNativeOverlayCapture()
{
#if defined(Q_OS_IOS)
amneziaIosPairingQrOverlayRestartCapture();
#endif
}
bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
{
const QString uuid = extractPairingSessionUuidFromScanText(raw);
if (uuid.isEmpty()) {
return false;
}
emit pairingUuidFromScan(uuid);
return true;
}
#if defined(Q_OS_ANDROID)
bool PairingUiController::tryConsumeAndroidQrScan(const QString &code)
{
if (!g_pairingUiForAndroidQr) {
return false;
}
const QString codeCopy = code;
// Parse on this thread: while CameraActivity is foreground, AmneziaActivity is stopped and the Qt
// event loop may not process BlockingQueuedConnection until the user returns — UI would lag behind.
if (extractPairingSessionUuidFromScanText(codeCopy).isEmpty()) {
return false;
}
PairingUiController *const ctl = g_pairingUiForAndroidQr;
QPointer<PairingUiController> ctlPtr(ctl);
QTimer::singleShot(0, ctl, [ctlPtr, codeCopy]() {
if (!ctlPtr) {
return;
}
ctlPtr->applyScannedTextAsPairingUuid(codeCopy);
});
return true;
}
void PairingUiController::notifyAndroidPairingQrCameraClosed()
{
if (g_pairingUiForAndroidQr) {
g_pairingUiForAndroidQr->suppressAndroidNativePairingReaderStarts(2000);
}
}
void PairingUiController::notifyAndroidPairingQrCameraUserDismissed()
{
if (!g_pairingUiForAndroidQr) {
return;
}
PairingUiController *const ctl = g_pairingUiForAndroidQr;
QPointer<PairingUiController> ptr(ctl);
QTimer::singleShot(0, ctl, [ptr]() {
if (!ptr) {
return;
}
emit ptr->pairingAndroidNativeQrScannerUserDismissed();
});
}
#endif
void PairingUiController::suppressAndroidNativePairingReaderStarts(int ms)
{
if (ms <= 0) {
return;
}
#if defined(Q_OS_ANDROID)
const qint64 now = QDateTime::currentMSecsSinceEpoch();
const qint64 until = now + ms;
if (until <= m_androidPairingReaderCooldownUntilEpochMs) {
return;
}
m_androidPairingReaderCooldownUntilEpochMs = until;
emit androidPairingReaderCooldownUntilEpochMsChanged();
#else
Q_UNUSED(ms);
#endif
}
QVariantList PairingUiController::tvQrCodes() const
{
QVariantList list;
list.reserve(m_tvQrCodes.size());
for (const QString &s : m_tvQrCodes) {
list.append(s);
}
return list;
}
int PairingUiController::tvQrCodesCount() const
{
return m_tvQrCodes.size();
}
int PairingUiController::tvPairingWaitWindowSeconds() const
{
if (!m_pairingController) {
return 30;
}
const int msec = m_pairingController->pairingLongPollTimeoutMsecs();
return qMax(1, (msec + 999) / 1000);
}
bool PairingUiController::phonePairingBusy() const
{
return m_phonePairingBusy;
}
void PairingUiController::setTvBusy(bool busy)
{
m_tvPairingBusy = busy;
}
void PairingUiController::setPhoneBusy(bool busy)
{
if (m_phonePairingBusy == busy) {
return;
}
m_phonePairingBusy = busy;
emit phonePairingBusyChanged();
}
bool PairingUiController::canOpenTvQrPairingPage()
{
if (!m_appSettingsRepository) {
emit errorOccurred(ErrorCode::InternalError);
return false;
}
const QJsonObject gatewayServices = apiGatewayServicesFromServers(m_serversController);
if (gatewayServices.isEmpty()) {
return true;
}
QJsonObject payload;
payload.insert(QStringLiteral("locale"), m_appSettingsRepository->getAppLanguage().name().split(QLatin1Char('_')).first());
if (gatewayServices.contains(apiDefs::key::userCountryCode)) {
payload.insert(apiDefs::key::userCountryCode, gatewayServices.value(apiDefs::key::userCountryCode));
}
if (gatewayServices.contains(apiDefs::key::serviceType)) {
payload.insert(apiDefs::key::serviceType, gatewayServices.value(apiDefs::key::serviceType));
}
const bool isTestPurchase = false;
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), kGatewayProbeTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const ErrorCode err = gatewayController.post(QString::fromLatin1(kGatewayProbePath), payload, responseBody);
if (err != ErrorCode::NoError) {
emit errorOccurred(err);
return false;
}
return true;
}
void PairingUiController::resetTvQrDisplay()
{
m_tvQrCodes.clear();
m_tvSessionUuid.clear();
emit tvQrCodesChanged();
}
void PairingUiController::startTvQrSession()
{
if (!m_pairingController || !m_appSettingsRepository) {
return;
}
if (m_tvPairingBusy) {
return;
}
rotateTvQrSession();
}
void PairingUiController::rotateTvQrSession()
{
if (!m_pairingController || !m_appSettingsRepository) {
return;
}
if (m_tvWatcher) {
m_tvWatcher->disconnect();
m_tvWatcher->deleteLater();
m_tvWatcher.clear();
}
if (m_tvNetworkReply) {
m_tvNetworkReply->abort();
m_tvNetworkReply.clear();
}
++m_tvSessionGeneration;
const quint64 generation = m_tvSessionGeneration;
m_tvSessionUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QByteArray qrPayload = m_tvSessionUuid.toUtf8();
m_tvQrCodes = qrCodeUtils::generateQrCodeImageSeriesPlainText(qrPayload);
emit tvQrCodesChanged();
setTvBusy(true);
dispatchTvGenerateQrAttempt(generation, 0);
}
void PairingUiController::dispatchTvGenerateQrAttempt(quint64 generation, int retryAttempt)
{
if (!m_pairingController || !m_appSettingsRepository) {
return;
}
if (generation != m_tvSessionGeneration) {
return;
}
const bool isTestPurchase = false;
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
m_pairingController->pairingLongPollTimeoutMsecs(),
m_appSettingsRepository->isStrictKillSwitchEnabled());
const QJsonObject payload = m_pairingController->buildGenerateQrPayload(m_tvSessionUuid);
QNetworkReply *replyRaw = nullptr;
const QFuture<QPair<ErrorCode, QByteArray>> future =
gatewayController->postAsync(QString::fromLatin1(kGenerateQrPath), payload, &replyRaw, gatewayController);
m_tvNetworkReply = replyRaw;
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>(this);
m_tvWatcher = watcher;
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, this,
[this, gatewayController, watcher, generation, retryAttempt]() {
Q_UNUSED(gatewayController);
const auto result = watcher->result();
watcher->deleteLater();
if (m_tvWatcher == watcher) {
m_tvWatcher.clear();
}
if (generation != m_tvSessionGeneration) {
return;
}
m_tvNetworkReply.clear();
PairingController::QrPairingConfigPayload out;
ErrorCode logicalErr = result.first;
if (logicalErr == ErrorCode::NoError) {
logicalErr = PairingController::parseGenerateQrResponseBody(result.second, out);
}
if (logicalErr == ErrorCode::NoError) {
const ErrorCode impErr = m_subscriptionController->importServerFromQrPairingResponse(
out.config, out.serviceInfo, out.supportedProtocols);
setTvBusy(false);
if (impErr != ErrorCode::NoError) {
emit errorOccurred(impErr);
if (impErr == ErrorCode::ApiConfigAlreadyAdded) {
emit tvPairingConfigAlreadyAdded();
QTimer::singleShot(0, this, [this]() { rotateTvQrSession(); });
return;
}
resetTvQrDisplay();
return;
}
resetTvQrDisplay();
emit tvPairingConfigReceived();
return;
}
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
const int delayMs = pairingRetryDelayMs(retryAttempt);
QTimer::singleShot(delayMs, this, [this, generation, retryAttempt]() {
if (generation != m_tvSessionGeneration) {
return;
}
dispatchTvGenerateQrAttempt(generation, retryAttempt + 1);
});
return;
}
if (logicalErr == ErrorCode::ApiConfigTimeoutError) {
setTvBusy(false);
QTimer::singleShot(0, this, [this]() { rotateTvQrSession(); });
return;
}
setTvBusy(false);
emit errorOccurred(logicalErr);
});
watcher->setFuture(future);
}
void PairingUiController::cancelTvQrSession()
{
++m_tvSessionGeneration;
if (m_tvNetworkReply) {
m_tvNetworkReply->abort();
}
m_tvNetworkReply.clear();
if (m_tvWatcher) {
m_tvWatcher->disconnect();
m_tvWatcher->deleteLater();
m_tvWatcher.clear();
}
setTvBusy(false);
resetTvQrDisplay();
}
void PairingUiController::cancelAllPairingActivity()
{
++m_phoneSessionGeneration;
if (m_phoneNetworkReply) {
m_phoneNetworkReply->abort();
}
m_phoneNetworkReply.clear();
if (m_phoneWatcher) {
m_phoneWatcher->disconnect();
m_phoneWatcher->deleteLater();
m_phoneWatcher.clear();
}
setPhoneBusy(false);
clearPendingPhonePairingUuid();
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
m_lastSuccessfulPhonePairingDisplayName.clear();
emit lastSuccessfulPhonePairingDisplayNameChanged();
}
cancelTvQrSession();
}
void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIndex)
{
if (!m_pairingController || !m_serversController || !m_subscriptionController || !m_appSettingsRepository) {
return;
}
if (m_phonePairingBusy) {
return;
}
const QString trimmedUuid = qrUuid.trimmed();
if (trimmedUuid.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return;
}
if (serverIndex < 0 || serverIndex >= m_serversController->getServersCount()) {
emit errorOccurred(ErrorCode::InternalError);
return;
}
const QString serverId = m_serversController->getServerId(serverIndex);
const auto apiV2Opt = m_serversController->apiV2Config(serverId);
if (!apiV2Opt.has_value()) {
emit errorOccurred(ErrorCode::InternalError);
return;
}
const ApiV2ServerConfig &apiV2 = *apiV2Opt;
QString vpnKey;
const ErrorCode keyErr = m_subscriptionController->prepareVpnKeyExport(serverId, vpnKey);
if (keyErr != ErrorCode::NoError) {
emit errorOccurred(keyErr);
return;
}
const QJsonObject serviceInfo = apiV2.apiConfig.serviceInfo.toJson();
const QJsonArray supportedProtocols = apiV2.apiConfig.supportedProtocols;
const QString apiKey = apiV2.authData.apiKey;
if (apiKey.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return;
}
const QString serviceType = apiV2.apiConfig.serviceType.trimmed();
const QString userCountryCode = apiV2.apiConfig.userCountryCode.trimmed();
const ErrorCode fieldErr =
PairingController::validatePairingScanFields(trimmedUuid, vpnKey, apiKey, serviceType, userCountryCode);
if (fieldErr != ErrorCode::NoError) {
emit errorOccurred(fieldErr);
return;
}
++m_phoneSessionGeneration;
const quint64 phoneGeneration = m_phoneSessionGeneration;
if (!m_lastSuccessfulPhonePairingDisplayName.isEmpty()) {
m_lastSuccessfulPhonePairingDisplayName.clear();
emit lastSuccessfulPhonePairingDisplayNameChanged();
}
setPhoneBusy(true);
dispatchPhoneScanQrAttempt(trimmedUuid, apiV2.apiConfig.isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey,
serviceType, userCountryCode, phoneGeneration, 0);
}
void PairingUiController::dispatchPhoneScanQrAttempt(const QString &qrUuid, const bool isTestPurchase, const QString &vpnKey,
const QJsonObject &serviceInfo, const QJsonArray &supportedProtocols,
const QString &apiKey, const QString &serviceType, const QString &userCountryCode,
quint64 generation, int retryAttempt)
{
if (!m_pairingController || !m_appSettingsRepository) {
return;
}
if (generation != m_phoneSessionGeneration) {
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled());
const QJsonObject payload = m_pairingController->buildScanQrPayload(qrUuid, vpnKey, serviceInfo, supportedProtocols, apiKey,
serviceType, userCountryCode);
QNetworkReply *replyRaw = nullptr;
const QFuture<QPair<ErrorCode, QByteArray>> future =
gatewayController->postAsync(QString::fromLatin1(kScanQrPath), payload, &replyRaw, gatewayController);
m_phoneNetworkReply = replyRaw;
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>(this);
m_phoneWatcher = watcher;
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished, this,
[this, gatewayController, watcher, generation, retryAttempt, qrUuid, isTestPurchase, vpnKey, serviceInfo,
supportedProtocols, apiKey, serviceType, userCountryCode]() {
Q_UNUSED(gatewayController);
const auto result = watcher->result();
watcher->deleteLater();
if (m_phoneWatcher == watcher) {
m_phoneWatcher.clear();
}
if (generation != m_phoneSessionGeneration) {
return;
}
m_phoneNetworkReply.clear();
ErrorCode logicalErr = result.first;
QString scanDisplayName;
if (logicalErr == ErrorCode::NoError) {
logicalErr = PairingController::parseScanQrResponseBody(result.second, &scanDisplayName);
}
if (logicalErr == ErrorCode::NoError) {
setPhoneBusy(false);
if (m_lastSuccessfulPhonePairingDisplayName != scanDisplayName) {
m_lastSuccessfulPhonePairingDisplayName = scanDisplayName;
emit lastSuccessfulPhonePairingDisplayNameChanged();
}
clearPendingPhonePairingUuid();
emit phonePairingSucceeded();
return;
}
if (isPairingRetriableError(logicalErr) && retryAttempt + 1 < kPairingRetryMaxAttempts) {
const int delayMs = pairingRetryDelayMs(retryAttempt);
QTimer::singleShot(delayMs, this, [this, qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols,
apiKey, serviceType, userCountryCode, generation, retryAttempt]() {
if (generation != m_phoneSessionGeneration) {
return;
}
dispatchPhoneScanQrAttempt(qrUuid, isTestPurchase, vpnKey, serviceInfo, supportedProtocols, apiKey,
serviceType, userCountryCode, generation, retryAttempt + 1);
});
return;
}
setPhoneBusy(false);
emit errorOccurred(logicalErr);
});
watcher->setFuture(future);
}
@@ -0,0 +1,131 @@
#ifndef PAIRINGUICONTROLLER_H
#define PAIRINGUICONTROLLER_H
#include <QFutureWatcher>
#include <QNetworkReply>
#include <QObject>
#include <QVariantList>
#include <QPointer>
#include <QStringList>
#include "core/controllers/api/pairingController.h"
#include "core/controllers/api/subscriptionController.h"
#include "core/controllers/serversController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/errorCodes.h"
class PairingUiController : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariantList tvQrCodes READ tvQrCodes NOTIFY tvQrCodesChanged)
Q_PROPERTY(int tvQrCodesCount READ tvQrCodesCount NOTIFY tvQrCodesChanged)
Q_PROPERTY(int tvPairingWaitWindowSeconds READ tvPairingWaitWindowSeconds NOTIFY tvQrCodesChanged)
Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged)
Q_PROPERTY(QString pendingPhonePairingUuid READ pendingPhonePairingUuid WRITE setPendingPhonePairingUuid NOTIFY
pendingPhonePairingUuidChanged)
Q_PROPERTY(QString lastSuccessfulPhonePairingDisplayName READ lastSuccessfulPhonePairingDisplayName NOTIFY
lastSuccessfulPhonePairingDisplayNameChanged)
Q_PROPERTY(qint64 androidPairingReaderCooldownUntilEpochMs READ androidPairingReaderCooldownUntilEpochMs NOTIFY
androidPairingReaderCooldownUntilEpochMsChanged)
public:
PairingUiController(PairingController *pairingController, ServersController *serversController,
SubscriptionController *subscriptionController, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
~PairingUiController() override;
QVariantList tvQrCodes() const;
int tvQrCodesCount() const;
int tvPairingWaitWindowSeconds() const;
bool phonePairingBusy() const;
QString pendingPhonePairingUuid() const { return m_pendingPhonePairingUuid; }
void setPendingPhonePairingUuid(const QString &uuid);
QString lastSuccessfulPhonePairingDisplayName() const { return m_lastSuccessfulPhonePairingDisplayName; }
qint64 androidPairingReaderCooldownUntilEpochMs() const { return m_androidPairingReaderCooldownUntilEpochMs; }
Q_INVOKABLE void presentIosPairingQrNativeOverlayScanner(const QString &title = QString(),
const QString &subtitle = QString());
Q_INVOKABLE void dismissIosPairingQrNativeOverlayScanner();
Q_INVOKABLE void restartIosPairingQrNativeOverlayCapture();
#if defined(Q_OS_ANDROID)
static bool tryConsumeAndroidQrScan(const QString &code);
static void notifyAndroidPairingQrCameraClosed();
static void notifyAndroidPairingQrCameraUserDismissed();
#endif
public slots:
bool canOpenTvQrPairingPage();
void startTvQrSession();
void rotateTvQrSession();
void cancelTvQrSession();
void cancelAllPairingActivity();
void submitPhonePairing(const QString &qrUuid, int serverIndex);
void openPairingQrScanner();
Q_INVOKABLE bool isPairingCameraAccessGranted() const;
Q_INVOKABLE void requestPairingCameraAccess();
Q_INVOKABLE void openPairingCameraAppSettings();
Q_INVOKABLE void setPairingQrTorchEnabled(bool enabled);
bool applyScannedTextAsPairingUuid(const QString &raw);
signals:
void errorOccurred(amnezia::ErrorCode errorCode);
void tvQrCodesChanged();
void phonePairingBusyChanged();
void pendingPhonePairingUuidChanged();
void lastSuccessfulPhonePairingDisplayNameChanged();
void tvPairingConfigReceived();
void tvPairingConfigAlreadyAdded();
void phonePairingSucceeded();
void pairingUuidFromScan(const QString &uuid);
void pairingCameraAccessFinished(bool granted);
void androidPairingReaderCooldownUntilEpochMsChanged();
void pairingSendQrScanRejectedInvalidPayload();
void pairingIosNativeQrOverlayBackRequested();
void pairingAndroidNativeQrScannerUserDismissed();
private:
void setTvBusy(bool busy);
void setPhoneBusy(bool busy);
void resetTvQrDisplay();
void clearPendingPhonePairingUuid();
void suppressAndroidNativePairingReaderStarts(int ms);
void dispatchTvGenerateQrAttempt(quint64 generation, int retryAttempt);
void dispatchPhoneScanQrAttempt(const QString &qrUuid, bool isTestPurchase, const QString &vpnKey, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
const QString &userCountryCode, quint64 generation, int retryAttempt);
PairingController *m_pairingController {};
ServersController *m_serversController {};
SubscriptionController *m_subscriptionController {};
SecureAppSettingsRepository *m_appSettingsRepository {};
QList<QString> m_tvQrCodes;
QString m_tvSessionUuid;
bool m_tvPairingBusy = false;
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_tvWatcher;
QPointer<QNetworkReply> m_tvNetworkReply;
quint64 m_tvSessionGeneration { 0 };
bool m_phonePairingBusy = false;
QString m_pendingPhonePairingUuid;
QString m_lastSuccessfulPhonePairingDisplayName;
QPointer<QFutureWatcher<QPair<amnezia::ErrorCode, QByteArray>>> m_phoneWatcher;
QPointer<QNetworkReply> m_phoneNetworkReply;
quint64 m_phoneSessionGeneration { 0 };
qint64 m_androidPairingReaderCooldownUntilEpochMs = 0;
};
#endif // PAIRINGUICONTROLLER_H
@@ -16,6 +16,10 @@
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QTimer> #include <QTimer>
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
namespace namespace
{ {
namespace configKey namespace configKey
@@ -65,7 +69,6 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
ApiCountryModel* apiCountryModel, ApiCountryModel* apiCountryModel,
ApiDevicesModel* apiDevicesModel, ApiDevicesModel* apiDevicesModel,
SettingsController* settingsController, SettingsController* settingsController,
ConnectionController* connectionController,
QObject *parent) QObject *parent)
: QObject(parent), : QObject(parent),
m_serversController(serversController), m_serversController(serversController),
@@ -77,29 +80,13 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
m_apiAccountInfoModel(apiAccountInfoModel), m_apiAccountInfoModel(apiAccountInfoModel),
m_apiCountryModel(apiCountryModel), m_apiCountryModel(apiCountryModel),
m_apiDevicesModel(apiDevicesModel), m_apiDevicesModel(apiDevicesModel),
m_settingsController(settingsController), m_settingsController(settingsController)
m_connectionController(connectionController)
{ {
connect(m_apiServicesModel, &ApiServicesModel::serviceSelectionChanged, this, [this]() { connect(m_apiServicesModel, &ApiServicesModel::serviceSelectionChanged, this, [this]() {
ApiServicesModel::ApiServicesData selectedServiceData = m_apiServicesModel->selectedServiceData(); ApiServicesModel::ApiServicesData selectedServiceData = m_apiServicesModel->selectedServiceData();
m_apiSubscriptionPlansModel->updateModel(selectedServiceData.subscriptionPlansJson); m_apiSubscriptionPlansModel->updateModel(selectedServiceData.subscriptionPlansJson);
m_apiBenefitsModel->updateModel(selectedServiceData.benefits); m_apiBenefitsModel->updateModel(selectedServiceData.benefits);
}); });
connect(this, &SubscriptionUiController::installServerFromApiFinished, this,
[this](const QString &, int preferredDefaultServerIndex) {
if (m_connectionController->isConnected()) {
return;
}
const int selectedServerIndex = preferredDefaultServerIndex >= 0
? preferredDefaultServerIndex
: (m_serversController->getServersCount() - 1);
const QString serverId = m_serversController->getServerId(selectedServerIndex);
if (!serverId.isEmpty()) {
m_serversController->setDefaultServer(serverId);
}
});
} }
bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName) bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName)
@@ -453,7 +440,11 @@ bool SubscriptionUiController::getAccountInfo(const QString &serverId, bool relo
if (reload) { if (reload) {
QEventLoop wait; QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit); QTimer::singleShot(1000, &wait, &QEventLoop::quit);
#ifdef Q_OS_IOS
wait.exec();
#else
wait.exec(QEventLoop::ExcludeUserInputEvents); wait.exec(QEventLoop::ExcludeUserInputEvents);
#endif
} }
QJsonObject accountInfo; QJsonObject accountInfo;
ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverId, accountInfo); ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverId, accountInfo);
@@ -5,7 +5,6 @@
#include "core/controllers/serversController.h" #include "core/controllers/serversController.h"
#include "core/controllers/settingsController.h" #include "core/controllers/settingsController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/api/servicesCatalogController.h" #include "core/controllers/api/servicesCatalogController.h"
#include "core/controllers/api/subscriptionController.h" #include "core/controllers/api/subscriptionController.h"
#include "ui/models/api/apiSubscriptionPlansModel.h" #include "ui/models/api/apiSubscriptionPlansModel.h"
@@ -29,7 +28,6 @@ public:
ApiCountryModel* apiCountryModel, ApiCountryModel* apiCountryModel,
ApiDevicesModel* apiDevicesModel, ApiDevicesModel* apiDevicesModel,
SettingsController* settingsController, SettingsController* settingsController,
ConnectionController* connectionController,
QObject *parent = nullptr); QObject *parent = nullptr);
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady) Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
@@ -106,7 +104,6 @@ private:
ApiCountryModel* m_apiCountryModel; ApiCountryModel* m_apiCountryModel;
ApiDevicesModel* m_apiDevicesModel; ApiDevicesModel* m_apiDevicesModel;
SettingsController* m_settingsController; SettingsController* m_settingsController;
ConnectionController* m_connectionController;
}; };
#endif // SUBSCRIPTIONUICONTROLLER_H #endif // SUBSCRIPTIONUICONTROLLER_H
@@ -44,6 +44,7 @@ signals:
void connectionStateChanged(); void connectionStateChanged();
void connectionErrorOccurred(ErrorCode errorCode); void connectionErrorOccurred(ErrorCode errorCode);
void reconnectWithUpdatedContainer(const QString &message);
void connectButtonClicked(); void connectButtonClicked();
void preparingConfig(); void preparingConfig();
@@ -82,6 +82,9 @@ namespace PageLoader
PageSetupWizardApiPremiumInfo, PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail, PageSetupWizardApiTrialEmail,
PageSettingsApiQrPairingSend,
PageSetupWizardApiQrPairingReceive,
PageDevMenu, PageDevMenu,
PageProtocolXraySnapshots, PageProtocolXraySnapshots,
@@ -12,7 +12,6 @@
#include "core/utils/api/apiUtils.h" #include "core/utils/api/apiUtils.h"
#include "core/controllers/selfhosted/installController.h" #include "core/controllers/selfhosted/installController.h"
#include "core/controllers/connectionController.h"
#include "core/utils/networkUtilities.h" #include "core/utils/networkUtilities.h"
#include "core/utils/protocolEnum.h" #include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h" #include "core/protocols/protocolUtils.h"
@@ -52,7 +51,6 @@ InstallUiController::InstallUiController(InstallController *installController,
Socks5ProxyConfigModel *socks5ConfigModel, Socks5ProxyConfigModel *socks5ConfigModel,
MtProxyConfigModel* mtConfigModel, MtProxyConfigModel* mtConfigModel,
TelemtConfigModel *telemtConfigModel, TelemtConfigModel *telemtConfigModel,
ConnectionController *connectionController,
QObject *parent) QObject *parent)
: QObject(parent), : QObject(parent),
m_installController(installController), m_installController(installController),
@@ -71,8 +69,7 @@ InstallUiController::InstallUiController(InstallController *installController,
m_sftpConfigModel(sftpConfigModel), m_sftpConfigModel(sftpConfigModel),
m_socks5ConfigModel(socks5ConfigModel), m_socks5ConfigModel(socks5ConfigModel),
m_mtProxyConfigModel(mtConfigModel), m_mtProxyConfigModel(mtConfigModel),
m_telemtConfigModel(telemtConfigModel), m_telemtConfigModel(telemtConfigModel)
m_connectionController(connectionController)
{ {
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated); connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) { connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
@@ -136,10 +133,6 @@ void InstallUiController::install(DockerContainer container, int port, Transport
finishMessage += tr("\nAdded containers that were already installed on the server"); finishMessage += tr("\nAdded containers that were already installed on the server");
} }
if (!m_connectionController->isConnected()) {
m_serversController->setDefaultServer(newServerId);
}
emit installServerFinished(finishMessage); emit installServerFinished(finishMessage);
} else { } else {
const auto adminBefore = m_serversController->selfHostedAdminConfig(serverId); const auto adminBefore = m_serversController->selfHostedAdminConfig(serverId);
@@ -179,12 +172,7 @@ void InstallUiController::install(DockerContainer container, int port, Transport
"All installed containers have been added to the application"); "All installed containers have been added to the application");
} }
const bool isServiceInstall = ContainerUtils::containerService(container) == ServiceType::Other; emit installContainerFinished(finishMessage, ContainerUtils::containerService(container) == ServiceType::Other);
if (!m_connectionController->isConnected() && !isServiceInstall) {
m_serversController->setDefaultContainer(serverId, container);
}
emit installContainerFinished(finishMessage, isServiceInstall);
} }
} }
@@ -275,15 +263,11 @@ void InstallUiController::updateContainer(const QString &serverId, int container
} }
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container); ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
const bool asyncUpdate = container == DockerContainer::MtProxy || container == DockerContainer::Telemt if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncUpdate) {
emit serverIsBusy(true); emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this); auto *watcher = new QFutureWatcher<ErrorCode>(this);
const Proto protocolTypeCopy = protocolType;
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this, QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, closePage, protocolTypeCopy]() { [this, watcher, serverId, container, closePage]() {
const ErrorCode errorCode = watcher->result(); const ErrorCode errorCode = watcher->result();
watcher->deleteLater(); watcher->deleteLater();
emit serverIsBusy(false); emit serverIsBusy(false);
@@ -292,8 +276,15 @@ void InstallUiController::updateContainer(const QString &serverId, int container
const ContainerConfig updatedConfig = const ContainerConfig updatedConfig =
m_serversController->getContainerConfig(serverId, container); m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig); m_protocolModel->updateModel(updatedConfig);
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolTypeCopy));
const auto defaultContainer =
m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId())
&& (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"), closePage); emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
} else { } else {
emit installationErrorOccurred(errorCode); emit installationErrorOccurred(errorCode);
} }
@@ -316,8 +307,13 @@ void InstallUiController::updateContainer(const QString &serverId, int container
if (errorCode == ErrorCode::NoError) { if (errorCode == ErrorCode::NoError) {
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container); ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig); m_protocolModel->updateModel(updatedConfig);
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolType));
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"), closePage); emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
return; return;
} }
@@ -431,34 +427,6 @@ void InstallUiController::removeContainer(const QString &serverId, int container
DockerContainer container = static_cast<DockerContainer>(containerIndex); DockerContainer container = static_cast<DockerContainer>(containerIndex);
QString containerName = ContainerUtils::containerHumanNames().value(container); QString containerName = ContainerUtils::containerHumanNames().value(container);
const bool asyncRemove = container == DockerContainer::Xray || container == DockerContainer::SSXray;
if (asyncRemove) {
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, containerName, serverName]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
emit removeContainerFinished(
tr("%1 has been removed from the server '%2'").arg(containerName, serverName));
} else {
emit installationErrorOccurred(errorCode);
}
});
InstallController *installController = m_installController;
QFuture<ErrorCode> future = QtConcurrent::run(
[installController, serverId, container]() -> ErrorCode {
return installController->removeContainer(serverId, container);
});
watcher->setFuture(future);
return;
}
ErrorCode errorCode = m_installController->removeContainer(serverId, container); ErrorCode errorCode = m_installController->removeContainer(serverId, container);
if (errorCode == ErrorCode::NoError) { if (errorCode == ErrorCode::NoError) {
@@ -549,12 +517,6 @@ void InstallUiController::setEncryptedPassphrase(QString passphrase)
void InstallUiController::addEmptyServer() void InstallUiController::addEmptyServer()
{ {
m_installController->addEmptyServer(m_processedServerCredentials); m_installController->addEmptyServer(m_processedServerCredentials);
if (!m_connectionController->isConnected()) {
const QString newServerId = m_serversController->getServerId(m_serversController->getServersCount() - 1);
if (!newServerId.isEmpty()) {
m_serversController->setDefaultServer(newServerId);
}
}
emit installServerFinished(tr("Server added successfully")); emit installServerFinished(tr("Server added successfully"));
} }
@@ -9,7 +9,6 @@
#include "core/utils/protocolEnum.h" #include "core/utils/protocolEnum.h"
#include "core/controllers/serversController.h" #include "core/controllers/serversController.h"
#include "core/controllers/settingsController.h" #include "core/controllers/settingsController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/selfhosted/usersController.h" #include "core/controllers/selfhosted/usersController.h"
#include "core/controllers/selfhosted/installController.h" #include "core/controllers/selfhosted/installController.h"
#include "core/utils/errorCodes.h" #include "core/utils/errorCodes.h"
@@ -53,7 +52,6 @@ public:
Socks5ProxyConfigModel* socks5ConfigModel, Socks5ProxyConfigModel* socks5ConfigModel,
MtProxyConfigModel* mtConfigModel, MtProxyConfigModel* mtConfigModel,
TelemtConfigModel* telemtConfigModel, TelemtConfigModel* telemtConfigModel,
ConnectionController* connectionController,
QObject *parent = nullptr); QObject *parent = nullptr);
~InstallUiController(); ~InstallUiController();
@@ -129,6 +127,8 @@ signals:
void serverIsBusy(const bool isBusy); void serverIsBusy(const bool isBusy);
void cancelInstallation(); void cancelInstallation();
void currentContainerUpdated();
void cachedProfileCleared(const QString &message); void cachedProfileCleared(const QString &message);
void apiConfigRemoved(const QString &message); void apiConfigRemoved(const QString &message);
@@ -155,7 +155,6 @@ private:
Socks5ProxyConfigModel* m_socks5ConfigModel; Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel; MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel; TelemtConfigModel* m_telemtConfigModel;
ConnectionController* m_connectionController;
ServerCredentials m_processedServerCredentials; ServerCredentials m_processedServerCredentials;
+115 -108
View File
@@ -1,5 +1,6 @@
#include "serversUiController.h" #include "serversUiController.h"
#include "core/utils/api/apiUtils.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"
@@ -31,12 +32,6 @@ bool descriptionsHaveGatewayServers(const QVector<ServerDescription> &list)
} }
return false; return false;
} }
const ServerDescription &emptyServerDescription()
{
static const ServerDescription s_emptyDescription;
return s_emptyDescription;
}
} // namespace } // namespace
ServersUiController::ServersUiController(ServersController* serversController, ServersUiController::ServersUiController(ServersController* serversController,
SettingsController* settingsController, SettingsController* settingsController,
@@ -106,6 +101,8 @@ void ServersUiController::setDefaultServer(const QString &serverId)
return; return;
} }
m_serversController->setDefaultServer(serverId); m_serversController->setDefaultServer(serverId);
updateModel();
emit defaultServerIdChanged(serverId);
} }
void ServersUiController::setDefaultContainer(const QString &serverId, int containerIndex) void ServersUiController::setDefaultContainer(const QString &serverId, int containerIndex)
@@ -124,12 +121,12 @@ void ServersUiController::toggleAmneziaDns(bool enabled)
updateModel(); updateModel();
} }
void ServersUiController::onDefaultServerChanged(const QString &defaultServerId) void ServersUiController::onDefaultServerChanged(const QString &/*defaultServerId*/)
{ {
m_serversModel->setDefaultServerId(defaultServerId); updateModel();
setProcessedServerId(m_serversController->getDefaultServerId());
updateDefaultServerContainersModel(); updateDefaultServerContainersModel();
emit defaultServerIdChanged(m_serversController->getDefaultServerId());
emit defaultServerIdChanged(defaultServerId);
} }
void ServersUiController::updateModel() void ServersUiController::updateModel()
@@ -140,21 +137,27 @@ void ServersUiController::updateModel()
const QString defaultServerId = m_serversController->getDefaultServerId(); const QString defaultServerId = m_serversController->getDefaultServerId();
const bool hadServersFromGatewayBefore = descriptionsHaveGatewayServers(m_orderedServerDescriptions); const bool hadServersFromGatewayBefore = descriptionsHaveGatewayServers(m_orderedServerDescriptions);
const bool hasServersFromGatewayNow = descriptionsHaveGatewayServers(descriptions); const bool hasServersFromGatewayNow = descriptionsHaveGatewayServers(descriptions);
const int listCount = descriptions.size();
const int defaultRowInDescriptions = rowForServerId(descriptions, defaultServerId);
m_orderedServerDescriptions = descriptions; m_orderedServerDescriptions = descriptions;
if (m_orderedServerDescriptions.isEmpty()) { if (listCount == 0) {
if (!m_processedServerId.isEmpty()) {
setProcessedServerId(QString()); setProcessedServerId(QString());
} } else if (m_processedServerIndex >= listCount) {
setProcessedServerId(defaultServerId);
} else if (!m_processedServerId.isEmpty()) { } else if (!m_processedServerId.isEmpty()) {
const int row = rowForServerId(m_orderedServerDescriptions, m_processedServerId); const int row = rowForServerId(m_orderedServerDescriptions, m_processedServerId);
if (row < 0) { if (row < 0) {
setProcessedServerId(QString()); setProcessedServerId(defaultServerId);
} else {
setProcessedServerId(m_processedServerId);
} }
} else if (defaultRowInDescriptions >= 0) {
setProcessedServerId(defaultServerId);
} }
m_serversModel->updateModel(m_orderedServerDescriptions, defaultServerId); m_serversModel->updateModel(m_orderedServerDescriptions, defaultRowInDescriptions);
updateContainersModel(); updateContainersModel();
updateDefaultServerContainersModel(); updateDefaultServerContainersModel();
@@ -164,6 +167,7 @@ void ServersUiController::updateModel()
} }
emit defaultServerIdChanged(defaultServerId); emit defaultServerIdChanged(defaultServerId);
emit defaultServerIndexChanged(defaultServerIndex());
} }
QString ServersUiController::getDefaultServerId() const QString ServersUiController::getDefaultServerId() const
@@ -173,35 +177,64 @@ QString ServersUiController::getDefaultServerId() const
QString ServersUiController::getDefaultServerName() const QString ServersUiController::getDefaultServerName() const
{ {
return serverName(getDefaultServerId()); const QString defaultServerId = m_serversController->getDefaultServerId();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == defaultServerId) {
return description.serverName;
}
}
return QString();
} }
QString ServersUiController::getDefaultServerDefaultContainerName() const QString ServersUiController::getDefaultServerDefaultContainerName() const
{ {
const auto &description = serverDescriptionById(getDefaultServerId()); const QString defaultServerId = m_serversController->getDefaultServerId();
if (description.serverId.isEmpty()) { for (const auto &description : m_orderedServerDescriptions) {
return QString(); if (description.serverId == defaultServerId) {
}
return ContainerUtils::containerHumanNames().value(description.defaultContainer); return ContainerUtils::containerHumanNames().value(description.defaultContainer);
}
}
return QString();
} }
QString ServersUiController::getDefaultServerDescriptionCollapsed() const QString ServersUiController::getDefaultServerDescriptionCollapsed() const
{ {
return serverDescriptionById(getDefaultServerId()).collapsedServerDescription; const QString defaultServerId = m_serversController->getDefaultServerId();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == defaultServerId) {
return description.collapsedServerDescription;
}
}
return QString();
} }
QString ServersUiController::getDefaultServerImagePathCollapsed() const QString ServersUiController::getDefaultServerImagePathCollapsed() const
{ {
const auto &description = serverDescriptionById(getDefaultServerId()); const QString defaultServerId = m_serversController->getDefaultServerId();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == defaultServerId) {
if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) { if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) {
return ""; return "";
} }
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(description.apiServerCountryCode.toUpper()); const QString imageCode = apiUtils::countryCodeBaseForFlag(description.apiServerCountryCode.toUpper());
if (imageCode.isEmpty()) {
return QString();
}
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(imageCode);
}
}
return "";
} }
QString ServersUiController::getDefaultServerDescriptionExpanded() const QString ServersUiController::getDefaultServerDescriptionExpanded() const
{ {
return serverDescriptionById(getDefaultServerId()).expandedServerDescription; const QString defaultServerId = m_serversController->getDefaultServerId();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == defaultServerId) {
return description.expandedServerDescription;
}
}
return QString();
} }
bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() const bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() const
@@ -253,75 +286,15 @@ bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() con
bool ServersUiController::isDefaultServerFromApi() const bool ServersUiController::isDefaultServerFromApi() const
{ {
return isServerFromApi(getDefaultServerId()); const QString defaultServerId = m_serversController->getDefaultServerId();
}
bool ServersUiController::hasServerWithWriteAccess() const
{
for (const auto &description : m_orderedServerDescriptions) { for (const auto &description : m_orderedServerDescriptions) {
if (description.hasWriteAccess) { if (description.serverId == defaultServerId) {
return true; return description.isApiV2;
} }
} }
return false; return false;
} }
QString ServersUiController::serverName(const QString &serverId) const
{
return serverDescriptionById(serverId).serverName;
}
QString ServersUiController::serverHostName(const QString &serverId) const
{
return serverDescriptionById(serverId).hostName;
}
int ServersUiController::serverDefaultContainer(const QString &serverId) const
{
const auto &description = serverDescriptionById(serverId);
return description.serverId.isEmpty() ? -1 : static_cast<int>(description.defaultContainer);
}
bool ServersUiController::isServerFromApi(const QString &serverId) const
{
return serverDescriptionById(serverId).isServerFromGatewayApi;
}
bool ServersUiController::isServerCountrySelectionAvailable(const QString &serverId) const
{
return serverDescriptionById(serverId).isCountrySelectionAvailable;
}
bool ServersUiController::isServerHasWriteAccess(const QString &serverId) const
{
return serverDescriptionById(serverId).hasWriteAccess;
}
bool ServersUiController::serverHasInstalledContainers(const QString &serverId) const
{
return serverDescriptionById(serverId).hasInstalledVpnContainers;
}
QString ServersUiController::serverAdEndpoint(const QString &serverId) const
{
return serverDescriptionById(serverId).adEndpoint;
}
bool ServersUiController::isServerRenewalAvailable(const QString &serverId) const
{
return serverDescriptionById(serverId).isRenewalAvailable;
}
bool ServersUiController::isServerSubscriptionExpired(const QString &serverId) const
{
return serverDescriptionById(serverId).isSubscriptionExpired;
}
bool ServersUiController::isServerSubscriptionExpiringSoon(const QString &serverId) const
{
return serverDescriptionById(serverId).isSubscriptionExpiringSoon;
}
int ServersUiController::getProcessedContainerIndex() const int ServersUiController::getProcessedContainerIndex() const
{ {
return m_processedContainerIndex; return m_processedContainerIndex;
@@ -343,17 +316,27 @@ QString ServersUiController::getProcessedServerId() const
void ServersUiController::setProcessedServerId(const QString &serverId) void ServersUiController::setProcessedServerId(const QString &serverId)
{ {
const int newIndex = serverId.isEmpty() ? -1 : serverIndexForId(serverId); const int index = serverId.isEmpty() ? -1 : serverIndexForId(serverId);
const QString normalizedServerId = newIndex >= 0 ? serverId : QString(); if (!serverId.isEmpty() && index < 0) {
return;
}
if (m_processedServerId != normalizedServerId) { if (m_processedServerIndex != index || m_processedServerId != serverId) {
m_processedServerId = normalizedServerId; m_processedServerIndex = index;
m_processedServerId = serverId;
m_serversModel->setProcessedServerIndex(index);
if (newIndex >= 0) { if (index >= 0) {
updateContainersModel(); updateContainersModel();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == serverId) {
setProcessedContainerIndex(static_cast<int>(description.defaultContainer));
break;
}
}
for (const auto &description : m_orderedServerDescriptions) { for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId != normalizedServerId) { if (description.serverId != serverId) {
continue; continue;
} }
if (description.isApiV2) { if (description.isApiV2) {
@@ -367,12 +350,45 @@ void ServersUiController::setProcessedServerId(const QString &serverId)
} }
emit processedServerIdChanged(m_processedServerId); emit processedServerIdChanged(m_processedServerId);
emit processedServerIndexChanged(m_processedServerIndex);
} }
} }
int ServersUiController::getProcessedServerIndex() const
{
return m_processedServerIndex;
}
void ServersUiController::setProcessedServerIndex(int index)
{
if (index < 0) {
setProcessedServerId(QString());
return;
}
const QString id = getServerId(index);
if (!id.isEmpty()) {
setProcessedServerId(id);
}
}
int ServersUiController::defaultServerIndex() const
{
return rowForServerId(m_orderedServerDescriptions, getDefaultServerId());
}
bool ServersUiController::processedServerIsPremium() const bool ServersUiController::processedServerIsPremium() const
{ {
return processedServerDescription().isPremium; for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == m_processedServerId) {
return description.isPremium;
}
}
return false;
}
const ServerCredentials ServersUiController::getProcessedServerCredentials() const
{
return m_serversController->getServerCredentials(m_processedServerId);
} }
bool ServersUiController::isDefaultServerCurrentlyProcessed() const bool ServersUiController::isDefaultServerCurrentlyProcessed() const
@@ -382,22 +398,18 @@ bool ServersUiController::isDefaultServerCurrentlyProcessed() const
bool ServersUiController::isProcessedServerHasWriteAccess() const bool ServersUiController::isProcessedServerHasWriteAccess() const
{ {
return isServerHasWriteAccess(m_processedServerId); ServerCredentials credentials = m_serversController->getServerCredentials(m_processedServerId);
return (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty());
} }
const ServerDescription &ServersUiController::processedServerDescription() const QString ServersUiController::getDefaultServerDescription(const QString &serverId) const
{
return serverDescriptionById(m_processedServerId);
}
const ServerDescription &ServersUiController::serverDescriptionById(const QString &serverId) const
{ {
for (const auto &description : m_orderedServerDescriptions) { for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == serverId) { if (description.serverId == serverId) {
return description; return description.baseDescription;
} }
} }
return emptyServerDescription(); return QString();
} }
bool ServersUiController::hasServersFromGatewayApi() const bool ServersUiController::hasServersFromGatewayApi() const
@@ -460,11 +472,6 @@ int ServersUiController::getServerIndexById(const QString &serverId) const
return rowForServerId(m_orderedServerDescriptions, serverId); return rowForServerId(m_orderedServerDescriptions, serverId);
} }
int ServersUiController::getServersCount() const
{
return m_orderedServerDescriptions.size();
}
void ServersUiController::updateContainersModel() void ServersUiController::updateContainersModel()
{ {
if (m_processedServerId.isEmpty()) { if (m_processedServerId.isEmpty()) {
+13 -17
View File
@@ -19,6 +19,7 @@ class ServersUiController : public QObject
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString defaultServerId READ getDefaultServerId NOTIFY defaultServerIdChanged) Q_PROPERTY(QString defaultServerId READ getDefaultServerId NOTIFY defaultServerIdChanged)
Q_PROPERTY(int defaultServerIndex READ defaultServerIndex NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerIdChanged) Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerIdChanged)
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerIdChanged) Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerIdChanged)
@@ -29,8 +30,9 @@ class ServersUiController : public QObject
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged) Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged)
Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged) Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged)
Q_PROPERTY(int processedServerIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged) Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIdChanged) Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged) Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged)
@@ -70,27 +72,20 @@ public slots:
QString getDefaultServerDescriptionExpanded() const; QString getDefaultServerDescriptionExpanded() const;
bool isDefaultServerDefaultContainerHasSplitTunneling() const; bool isDefaultServerDefaultContainerHasSplitTunneling() const;
bool isDefaultServerFromApi() const; bool isDefaultServerFromApi() const;
bool hasServerWithWriteAccess() const;
QString serverName(const QString &serverId) const;
QString serverHostName(const QString &serverId) const;
int serverDefaultContainer(const QString &serverId) const;
bool isServerFromApi(const QString &serverId) const;
bool isServerCountrySelectionAvailable(const QString &serverId) const;
bool isServerHasWriteAccess(const QString &serverId) const;
bool serverHasInstalledContainers(const QString &serverId) const;
QString serverAdEndpoint(const QString &serverId) const;
bool isServerRenewalAvailable(const QString &serverId) const;
bool isServerSubscriptionExpired(const QString &serverId) const;
bool isServerSubscriptionExpiringSoon(const QString &serverId) const;
QString getProcessedServerId() const; QString getProcessedServerId() const;
void setProcessedServerId(const QString &serverId); void setProcessedServerId(const QString &serverId);
int getProcessedServerIndex() const;
void setProcessedServerIndex(int index);
int defaultServerIndex() const;
int getProcessedContainerIndex() const; int getProcessedContainerIndex() const;
void setProcessedContainerIndex(int index); void setProcessedContainerIndex(int index);
bool processedServerIsPremium() const; bool processedServerIsPremium() const;
const ServerCredentials getProcessedServerCredentials() const;
bool isDefaultServerCurrentlyProcessed() const; bool isDefaultServerCurrentlyProcessed() const;
bool isProcessedServerHasWriteAccess() const; bool isProcessedServerHasWriteAccess() const;
@@ -102,14 +97,15 @@ public slots:
QString getServerId(int index) const; QString getServerId(int index) const;
int getServerIndexById(const QString &serverId) const; int getServerIndexById(const QString &serverId) const;
int getServersCount() const;
QStringList getAllInstalledServicesName(int serverIndex) const; QStringList getAllInstalledServicesName(int serverIndex) const;
signals: signals:
void errorOccurred(const QString &errorMessage); void errorOccurred(const QString &errorMessage);
void finished(const QString &message); void finished(const QString &message);
void defaultServerIdChanged(const QString &serverId); void defaultServerIdChanged(const QString &serverId);
void defaultServerIndexChanged(int index);
void processedServerIdChanged(const QString &serverId); void processedServerIdChanged(const QString &serverId);
void processedServerIndexChanged(int index);
void processedContainerIndexChanged(int index); void processedContainerIndexChanged(int index);
void hasServersFromGatewayApiChanged(); void hasServersFromGatewayApiChanged();
void updateApiCountryModel(); void updateApiCountryModel();
@@ -119,8 +115,7 @@ public:
void updateModel(); void updateModel();
private: private:
const ServerDescription &serverDescriptionById(const QString &serverId) const; QString getDefaultServerDescription(const QString &serverId) const;
const ServerDescription &processedServerDescription() const;
int serverIndexForId(const QString &serverId) const; int serverIndexForId(const QString &serverId) const;
bool listHasServersFromGatewayApi() const; bool listHasServersFromGatewayApi() const;
@@ -135,6 +130,7 @@ private:
QVector<amnezia::ServerDescription> m_orderedServerDescriptions; QVector<amnezia::ServerDescription> m_orderedServerDescriptions;
int m_processedServerIndex = -1;
QString m_processedServerId; QString m_processedServerId;
int m_processedContainerIndex = -1; int m_processedContainerIndex = -1;
}; };
@@ -164,7 +164,6 @@ void SettingsUiController::restoreAppConfigFromData(const QByteArray &data)
emit amneziaDnsToggled(amneziaDnsEnabled); emit amneziaDnsToggled(amneziaDnsEnabled);
emit restoreBackupFinished(); emit restoreBackupFinished();
emit startMinimizedChanged();
} else { } else {
emit errorOccurred(errorCode); emit errorOccurred(errorCode);
} }
@@ -178,7 +177,6 @@ QString SettingsUiController::getAppVersion()
void SettingsUiController::clearSettings() void SettingsUiController::clearSettings()
{ {
m_settingsController->clearSettings(); m_settingsController->clearSettings();
emit startMinimizedChanged();
emit resetLanguageToSystem(); emit resetLanguageToSystem();
emit changeSettingsFinished(tr("All settings have been reset to default values")); emit changeSettingsFinished(tr("All settings have been reset to default values"));
@@ -206,9 +204,6 @@ bool SettingsUiController::isAutoStartEnabled()
void SettingsUiController::toggleAutoStart(bool enable) void SettingsUiController::toggleAutoStart(bool enable)
{ {
m_settingsController->toggleAutoStart(enable); m_settingsController->toggleAutoStart(enable);
if (!enable) {
emit startMinimizedChanged();
}
} }
bool SettingsUiController::isStartMinimizedEnabled() bool SettingsUiController::isStartMinimizedEnabled()
@@ -1,5 +1,7 @@
#include "apiAccountInfoModel.h" #include "apiAccountInfoModel.h"
#include <QtGlobal>
#include <QDateTime> #include <QDateTime>
#include <QJsonObject> #include <QJsonObject>
@@ -10,6 +12,8 @@
namespace namespace
{ {
Logger logger("AccountInfoModel"); Logger logger("AccountInfoModel");
constexpr QLatin1String kCountryConfigSourceType("country_config");
} }
ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent) ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent)
@@ -106,6 +110,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
case IsInAppPurchaseRole: { case IsInAppPurchaseRole: {
return m_accountInfoData.isInAppPurchase; return m_accountInfoData.isInAppPurchase;
} }
case ConfigurationFilesCountRole: {
return m_accountInfoData.configurationFilesCount;
}
} }
return QVariant(); return QVariant();
@@ -120,6 +127,15 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
m_availableCountries = accountInfoObject.value(apiDefs::key::availableCountries).toArray(); m_availableCountries = accountInfoObject.value(apiDefs::key::availableCountries).toArray();
m_issuedConfigsInfo = accountInfoObject.value(apiDefs::key::issuedConfigs).toArray(); m_issuedConfigsInfo = accountInfoObject.value(apiDefs::key::issuedConfigs).toArray();
int configurationFilesCount = 0;
for (int i = 0; i < m_issuedConfigsInfo.size(); ++i) {
const QJsonObject issued = m_issuedConfigsInfo.at(i).toObject();
if (issued.value(apiDefs::key::sourceType).toString() == kCountryConfigSourceType) {
++configurationFilesCount;
}
}
accountInfoData.configurationFilesCount = configurationFilesCount;
accountInfoData.activeDeviceCount = accountInfoObject.value(apiDefs::key::activeDeviceCount).toInt(); accountInfoData.activeDeviceCount = accountInfoObject.value(apiDefs::key::activeDeviceCount).toInt();
accountInfoData.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt(); accountInfoData.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt();
accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString(); accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString();
@@ -205,6 +221,7 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired"; roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon"; roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[IsInAppPurchaseRole] = "isInAppPurchase"; roles[IsInAppPurchaseRole] = "isInAppPurchase";
roles[ConfigurationFilesCountRole] = "configurationFilesCount";
return roles; return roles;
} }
+3 -1
View File
@@ -25,7 +25,8 @@ public:
IsProtocolSelectionSupportedRole, IsProtocolSelectionSupportedRole,
IsSubscriptionExpiredRole, IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole, IsSubscriptionExpiringSoonRole,
IsInAppPurchaseRole IsInAppPurchaseRole,
ConfigurationFilesCountRole
}; };
explicit ApiAccountInfoModel(QObject *parent = nullptr); explicit ApiAccountInfoModel(QObject *parent = nullptr);
@@ -64,6 +65,7 @@ private:
bool isInAppPurchase = false; bool isInAppPurchase = false;
bool isRenewalAvailable = false; bool isRenewalAvailable = false;
int configurationFilesCount = 0;
}; };
AccountInfoData m_accountInfoData; AccountInfoData m_accountInfoData;
+2 -1
View File
@@ -5,6 +5,7 @@
#include "core/utils/serverConfigUtils.h" #include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h" #include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h" #include "core/utils/constants/apiConstants.h"
#include "core/utils/api/apiUtils.h"
#include "logger.h" #include "logger.h"
namespace namespace
@@ -41,7 +42,7 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
return countryInfo.countryName; return countryInfo.countryName;
} }
case CountryImageCodeRole: { case CountryImageCodeRole: {
return countryInfo.countryCode.toUpper(); return apiUtils::countryCodeBaseForFlag(countryInfo.countryCode);
} }
case IsIssuedRole: { case IsIssuedRole: {
return isIssued; return isIssued;
@@ -267,13 +267,8 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
m_container = container; m_container = container;
m_protocolConfig = protocolConfig; m_protocolConfig = protocolConfig;
if (m_protocolConfig.needsClientHydration) {
m_protocolConfig.hydrateServerConfigFromClientNative();
}
if (!m_protocolConfig.serverConfig.isThirdPartyConfig) {
applyDefaultsToServerConfig(m_protocolConfig.serverConfig); applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
}
m_originalProtocolConfig = m_protocolConfig; m_originalProtocolConfig = m_protocolConfig;
+153 -36
View File
@@ -20,25 +20,20 @@
using namespace amnezia; using namespace amnezia;
namespace {
int rowForServerId(const QVector<ServerDescription> &descriptions, const QString &serverId)
{
if (serverId.isEmpty()) {
return -1;
}
for (int i = 0; i < descriptions.size(); ++i) {
if (descriptions.at(i).serverId == serverId) {
return i;
}
}
return -1;
}
} // namespace
ServersModel::ServersModel(QObject *parent) : QAbstractListModel(parent) ServersModel::ServersModel(QObject *parent) : QAbstractListModel(parent)
{ {
connect(this, &ServersModel::defaultServerIndexChanged, this, &ServersModel::defaultServerNameChanged);
connect(this, &ServersModel::defaultServerIndexChanged, this, [this](const int serverIndex) {
if (serverIndex < 0 || serverIndex >= m_descriptions.size()) {
return;
}
auto defaultContainer = m_descriptions.at(serverIndex).defaultContainer;
emit ServersModel::defaultServerDefaultContainerChanged(defaultContainer);
emit ServersModel::defaultServerNameChanged();
});
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
} }
int ServersModel::rowCount(const QModelIndex &parent) const int ServersModel::rowCount(const QModelIndex &parent) const
@@ -61,22 +56,52 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
return row.serverName; return row.serverName;
case ServerDescriptionRole: case ServerDescriptionRole:
return configVersion ? row.baseDescription : (row.baseDescription + row.hostName); return configVersion ? row.baseDescription : (row.baseDescription + row.hostName);
case CollapsedServerDescriptionRole:
return row.collapsedServerDescription;
case ExpandedServerDescriptionRole:
return row.expandedServerDescription;
case HostNameRole: case HostNameRole:
return row.hostName; return row.hostName;
case ServerIdRole: case CredentialsRole:
return row.serverId; return QVariant::fromValue(serverCredentials(index.row()));
case CredentialsLoginRole: case CredentialsLoginRole:
return serverCredentials(index.row()).userName; return serverCredentials(index.row()).userName;
case IsDefaultRole: case IsDefaultRole:
return row.serverId == m_defaultServerId; return index.row() == m_defaultServerIndex;
case IsCurrentlyProcessedRole:
return index.row() == m_processedServerIndex;
case HasWriteAccessRole: case HasWriteAccessRole:
return row.hasWriteAccess; return row.hasWriteAccess;
case ContainsAmneziaDnsRole:
return row.primaryDnsIsAmnezia;
case DefaultContainerRole: case DefaultContainerRole:
return QVariant::fromValue(row.defaultContainer); return QVariant::fromValue(row.defaultContainer);
case HasInstalledContainers: case HasInstalledContainers:
return row.hasInstalledVpnContainers; return row.hasInstalledVpnContainers;
case IsServerFromTelegramApiRole:
return false;
case IsServerFromGatewayApiRole: case IsServerFromGatewayApiRole:
return row.isServerFromGatewayApi; return row.isServerFromGatewayApi;
case ApiConfigRole:
return QVariant();
case IsCountrySelectionAvailableRole:
return row.isCountrySelectionAvailable;
case ApiAvailableCountriesRole:
return row.apiAvailableCountries;
case ApiServerCountryCodeRole:
return row.apiServerCountryCode;
case HasAmneziaDns:
return row.primaryDnsIsAmnezia;
case IsAdVisibleRole:
return row.isAdVisible;
case AdHeaderRole:
return row.adHeader;
case AdDescriptionRole:
return row.adDescription;
case AdEndpointRole:
return row.adEndpoint;
case IsRenewalAvailableRole:
return row.isRenewalAvailable;
case IsSubscriptionExpiredRole: case IsSubscriptionExpiredRole:
return row.isSubscriptionExpired; return row.isSubscriptionExpired;
case IsSubscriptionExpiringSoonRole: case IsSubscriptionExpiringSoonRole:
@@ -92,32 +117,68 @@ QVariant ServersModel::data(const int index, int role) const
return data(modelIndex, role); return data(modelIndex, role);
} }
void ServersModel::updateModel(const QVector<ServerDescription> &descriptions, void ServersModel::updateModel(const QVector<ServerDescription> &descriptions, int defaultServerIndex)
const QString &defaultServerId)
{ {
beginResetModel(); beginResetModel();
m_descriptions = descriptions; m_descriptions = descriptions;
m_defaultServerId = defaultServerId; m_defaultServerIndex = defaultServerIndex;
endResetModel(); endResetModel();
emit defaultServerIndexChanged(m_defaultServerIndex);
emit processedServerChanged();
} }
void ServersModel::setDefaultServerId(const QString &serverId) const int ServersModel::getDefaultServerIndex()
{ {
if (m_defaultServerId == serverId) { return m_defaultServerIndex;
return; }
}
const int oldIndex = rowForServerId(m_descriptions, m_defaultServerId); const int ServersModel::getServersCount()
const int newIndex = rowForServerId(m_descriptions, serverId); {
m_defaultServerId = serverId; return m_descriptions.size();
}
const QVector<int> roles = { IsDefaultRole }; bool ServersModel::hasServerWithWriteAccess()
if (oldIndex >= 0 && oldIndex < m_descriptions.size()) { {
emit dataChanged(this->index(oldIndex), this->index(oldIndex), roles); for (size_t i = 0; i < getServersCount(); i++) {
if (qvariant_cast<bool>(data(static_cast<int>(i), HasWriteAccessRole))) {
return true;
} }
if (newIndex >= 0 && newIndex < m_descriptions.size()) {
emit dataChanged(this->index(newIndex), this->index(newIndex), roles);
} }
return false;
}
void ServersModel::setProcessedServerIndex(const int index)
{
if (m_processedServerIndex != index) {
m_processedServerIndex = index;
emit processedServerIndexChanged(m_processedServerIndex);
}
}
const ServerCredentials ServersModel::getProcessedServerCredentials()
{
return serverCredentials(m_processedServerIndex);
}
bool ServersModel::isDefaultServerCurrentlyProcessed()
{
return m_defaultServerIndex == m_processedServerIndex;
}
bool ServersModel::isDefaultServerFromApi()
{
return data(m_defaultServerIndex, IsServerFromTelegramApiRole).toBool()
|| data(m_defaultServerIndex, IsServerFromGatewayApiRole).toBool();
}
bool ServersModel::isProcessedServerHasWriteAccess()
{
return qvariant_cast<bool>(data(m_processedServerIndex, HasWriteAccessRole));
}
bool ServersModel::isDefaultServerHasWriteAccess()
{
return qvariant_cast<bool>(data(m_defaultServerIndex, HasWriteAccessRole));
} }
QHash<int, QByteArray> ServersModel::roleNames() const QHash<int, QByteArray> ServersModel::roleNames() const
@@ -126,22 +187,41 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[ServerDescriptionRole] = "serverDescription"; roles[ServerDescriptionRole] = "serverDescription";
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
roles[ExpandedServerDescriptionRole] = "expandedServerDescription";
roles[HostNameRole] = "hostName"; roles[HostNameRole] = "hostName";
roles[ServerIdRole] = "serverId";
roles[CredentialsRole] = "credentials";
roles[CredentialsLoginRole] = "credentialsLogin"; roles[CredentialsLoginRole] = "credentialsLogin";
roles[IsDefaultRole] = "isDefault"; roles[IsDefaultRole] = "isDefault";
roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed";
roles[HasWriteAccessRole] = "hasWriteAccess"; roles[HasWriteAccessRole] = "hasWriteAccess";
roles[ContainsAmneziaDnsRole] = "containsAmneziaDns";
roles[DefaultContainerRole] = "defaultContainer"; roles[DefaultContainerRole] = "defaultContainer";
roles[HasInstalledContainers] = "hasInstalledContainers"; roles[HasInstalledContainers] = "hasInstalledContainers";
roles[IsServerFromTelegramApiRole] = "isServerFromTelegramApi";
roles[IsServerFromGatewayApiRole] = "isServerFromGatewayApi"; roles[IsServerFromGatewayApiRole] = "isServerFromGatewayApi";
roles[ApiConfigRole] = "apiConfig";
roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable";
roles[ApiAvailableCountriesRole] = "apiAvailableCountries";
roles[ApiServerCountryCodeRole] = "apiServerCountryCode";
roles[IsAdVisibleRole] = "isAdVisible";
roles[AdHeaderRole] = "adHeader";
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
roles[IsRenewalAvailableRole] = "isRenewalAvailable";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired"; roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon"; roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[HasAmneziaDns] = "hasAmneziaDns";
return roles; return roles;
} }
@@ -153,3 +233,40 @@ ServerCredentials ServersModel::serverCredentials(int index) const
return m_descriptions.at(index).selfHostedSshCredentials; return m_descriptions.at(index).selfHostedSshCredentials;
} }
bool ServersModel::isServerFromApi(const int serverIndex)
{
return data(serverIndex, IsServerFromTelegramApiRole).toBool()
|| data(serverIndex, IsServerFromGatewayApiRole).toBool();
}
QVariant ServersModel::getDefaultServerData(const QString roleString)
{
auto roles = roleNames();
for (auto it = roles.begin(); it != roles.end(); it++) {
if (QString(it.value()) == roleString) {
return data(m_defaultServerIndex, it.key());
}
}
return {};
}
QVariant ServersModel::getProcessedServerData(const QString &roleString)
{
auto roles = roleNames();
for (auto it = roles.begin(); it != roles.end(); it++) {
if (QString(it.value()) == roleString) {
return data(m_processedServerIndex, it.key());
}
}
return {};
}
bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
{
if (serverIndex < 0 || serverIndex >= m_descriptions.size()) {
return false;
}
return m_descriptions.at(serverIndex).hasInstalledVpnContainers;
}
+55 -5
View File
@@ -14,22 +14,39 @@ public:
enum Roles { enum Roles {
NameRole = Qt::UserRole + 1, NameRole = Qt::UserRole + 1,
ServerDescriptionRole, ServerDescriptionRole,
CollapsedServerDescriptionRole,
ExpandedServerDescriptionRole,
HostNameRole, HostNameRole,
ServerIdRole,
CredentialsRole,
CredentialsLoginRole, CredentialsLoginRole,
IsDefaultRole, IsDefaultRole,
IsCurrentlyProcessedRole,
HasWriteAccessRole, HasWriteAccessRole,
ContainsAmneziaDnsRole,
DefaultContainerRole, DefaultContainerRole,
HasInstalledContainers, HasInstalledContainers,
IsServerFromTelegramApiRole,
IsServerFromGatewayApiRole, IsServerFromGatewayApiRole,
ApiConfigRole,
IsCountrySelectionAvailableRole,
ApiAvailableCountriesRole,
ApiServerCountryCodeRole,
IsAdVisibleRole,
AdHeaderRole,
AdDescriptionRole,
AdEndpointRole,
IsRenewalAvailableRole,
IsSubscriptionExpiredRole, IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole, IsSubscriptionExpiringSoonRole,
HasAmneziaDns
}; };
ServersModel(QObject *parent = nullptr); ServersModel(QObject *parent = nullptr);
@@ -39,19 +56,52 @@ public:
QVariant data(const int index, int role = Qt::DisplayRole) const; QVariant data(const int index, int role = Qt::DisplayRole) const;
public slots: public slots:
void updateModel(const QVector<amnezia::ServerDescription> &descriptions, const int getDefaultServerIndex();
const QString &defaultServerId); bool isDefaultServerCurrentlyProcessed();
void setDefaultServerId(const QString &serverId); bool isDefaultServerFromApi();
bool isProcessedServerHasWriteAccess();
bool isDefaultServerHasWriteAccess();
bool hasServerWithWriteAccess();
const int getServersCount();
void setProcessedServerIndex(const int index);
const ServerCredentials getProcessedServerCredentials();
QVariant getProcessedServerData(const QString &roleString);
QVariant getDefaultServerData(const QString roleString);
bool isServerFromApi(const int serverIndex);
void updateModel(const QVector<amnezia::ServerDescription> &descriptions, int defaultServerIndex);
protected: protected:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
signals:
void processedServerIndexChanged(const int index);
void processedServerChanged();
void defaultServerIndexChanged(const int index);
void defaultServerNameChanged();
void defaultServerDescriptionChanged();
void defaultServerDefaultContainerChanged(const int containerIndex);
void updateApiCountryModel();
void updateApiServicesModel();
private: private:
ServerCredentials serverCredentials(int index) const; ServerCredentials serverCredentials(int index) const;
bool serverHasInstalledContainers(const int serverIndex) const;
QVector<amnezia::ServerDescription> m_descriptions; QVector<amnezia::ServerDescription> m_descriptions;
QString m_defaultServerId; int m_defaultServerIndex = -1;
int m_processedServerIndex = -1;
}; };
#endif // SERVERSMODEL_H #endif // SERVERSMODEL_H
+3 -3
View File
@@ -51,11 +51,11 @@ Rectangle {
} }
Keys.onEnterPressed: { Keys.onEnterPressed: {
Qt.openUrlExternally(ServersUiController.serverAdEndpoint(ServersUiController.defaultServerId)) Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
} }
Keys.onReturnPressed: { Keys.onReturnPressed: {
Qt.openUrlExternally(ServersUiController.serverAdEndpoint(ServersUiController.defaultServerId)) Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
} }
RowLayout { RowLayout {
@@ -144,7 +144,7 @@ Rectangle {
onClicked: function() { onClicked: function() {
root.forceActiveFocus() root.forceActiveFocus()
Qt.openUrlExternally(ServersUiController.serverAdEndpoint(ServersUiController.defaultServerId)) Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
} }
} }
} }
@@ -182,6 +182,7 @@ Button {
} }
onClicked: { onClicked: {
ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex)
ConnectionController.connectButtonClicked() ConnectionController.connectButtonClicked()
} }
@@ -13,6 +13,7 @@ Item {
onButtonStartChanged: { onButtonStartChanged: {
if (buttonStart) { if (buttonStart) {
ServersUiController.setProcessedServerIndex(ServersUiController.defaultServerIndex)
ConnectionController.connectButtonClicked() ConnectionController.connectButtonClicked()
} }
} }
@@ -48,7 +48,7 @@ ListViewType {
showImage: !isInstalled showImage: !isInstalled
checkable: isInstalled && !ConnectionController.isConnected checkable: isInstalled && !ConnectionController.isConnected
checked: proxyDefaultServerContainersModel.mapToSource(index) === ServersUiController.serverDefaultContainer(ServersUiController.defaultServerId) checked: proxyDefaultServerContainersModel.mapToSource(index) === ServersModel.getDefaultServerData("defaultContainer")
onClicked: { onClicked: {
if (ConnectionController.isConnected && isInstalled) { if (ConnectionController.isConnected && isInstalled) {
@@ -58,7 +58,7 @@ ListViewType {
if (checked) { if (checked) {
containersDropDown.closeTriggered() containersDropDown.closeTriggered()
ServersUiController.setDefaultContainer(ServersUiController.defaultServerId, proxyDefaultServerContainersModel.mapToSource(index)) ServersUiController.setDefaultContainer(ServersUiController.getServerId(ServersUiController.defaultServerIndex), proxyDefaultServerContainersModel.mapToSource(index))
} else { } else {
ServersUiController.processedContainerIndex = proxyDefaultServerContainersModel.mapToSource(index) ServersUiController.processedContainerIndex = proxyDefaultServerContainersModel.mapToSource(index)
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
@@ -46,7 +46,7 @@ DrawerType2 {
} }
if (serverName.textField.text !== root.serverNameText) { if (serverName.textField.text !== root.serverNameText) {
ServersUiController.editServerName(ServersUiController.processedServerId, serverName.textField.text); ServersUiController.editServerName(ServersUiController.getServerId(ServersUiController.processedServerIndex), serverName.textField.text);
} }
root.closeTriggered() root.closeTriggered()
} }

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