mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-28 13:56:16 +03:00
Compare commits
23 Commits
feat/imple
...
server_scr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
201e4063ed | ||
|
|
211bf51f1d | ||
|
|
7e0c35ba29 | ||
|
|
bcee58b08b | ||
|
|
52de1acebf | ||
|
|
027a12a1df | ||
|
|
0a659a2d74 | ||
|
|
6f119cd083 | ||
|
|
1753aed3fc | ||
|
|
c714d98bd1 | ||
|
|
4787f3915b | ||
|
|
7a383116b2 | ||
|
|
d3de5f0f48 | ||
|
|
8749d683e3 | ||
|
|
9de9d082bc | ||
|
|
a4233fef41 | ||
|
|
4890dd1d74 | ||
|
|
564630827e | ||
|
|
fbe15d965b | ||
|
|
b29515c380 | ||
|
|
0658a8f565 | ||
|
|
482ec04b4a | ||
|
|
d40d24fcf9 |
76
.github/workflows/deploy.yml
vendored
76
.github/workflows/deploy.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.event.before }}
|
||||
filters: |
|
||||
recipes:
|
||||
- 'recipes/**'
|
||||
- 'conanfile.py'
|
||||
- '.github/workflows/deploy.yml'
|
||||
|
||||
Bake-Prebuilts-Linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
shell: bash
|
||||
@@ -50,9 +50,11 @@ jobs:
|
||||
done
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -98,7 +100,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Install system packages'
|
||||
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||
@@ -118,7 +120,7 @@ jobs:
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-Linux.run
|
||||
path: deploy/build/AmneziaVPN_*_linux_x64.run
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
@@ -149,15 +151,17 @@ jobs:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -229,7 +233,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
shell: cmd
|
||||
@@ -242,27 +246,31 @@ jobs:
|
||||
- name: 'Upload WIX installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-win64.msi
|
||||
path: deploy/build/AmneziaVPN_*_windows_x64.msi
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload IFW installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-win64.exe
|
||||
path: deploy/build/AmneziaVPN_*_windows_x64.exe
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-iOS:
|
||||
runs-on: macos-latest
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [26.0]
|
||||
xcode-version: [26.0, 26.4]
|
||||
include:
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -279,15 +287,17 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -344,7 +354,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '26.1'
|
||||
xcode-version: '26.0'
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -376,7 +386,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install deps'
|
||||
run: pip install "conan==2.26.2" jsonschema jinja2
|
||||
run: pip install "conan==2.28.0" jsonschema jinja2
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
@@ -394,14 +404,17 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-MacOS:
|
||||
runs-on: macos-latest
|
||||
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4]
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -418,15 +431,17 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -502,7 +517,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
env:
|
||||
@@ -518,20 +533,24 @@ jobs:
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/AmneziaVPN-*-Darwin.pkg
|
||||
path: deploy/build/AmneziaVPN_*_macos_x64.pkg
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Bake-Prebuilts-MacOS-NE:
|
||||
runs-on: macos-latest
|
||||
needs: Detect-Changes
|
||||
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
xcode-version: [16.2, 16.4]
|
||||
xcode-version: [16.2, 16.4, 26.4]
|
||||
include:
|
||||
- xcode-version: 26.4
|
||||
os: macos-26
|
||||
|
||||
runs-on: ${{ matrix.os || 'macos-latest' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -548,15 +567,17 @@ jobs:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -635,7 +656,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -671,7 +692,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Setup Android SDK'
|
||||
uses: android-actions/setup-android@v4
|
||||
@@ -696,9 +717,11 @@ jobs:
|
||||
done
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
|
||||
|
||||
- name: 'Upload baked prebuilts'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: conan upload -r amnezia "*" -c
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -712,7 +735,7 @@ jobs:
|
||||
env:
|
||||
ANDROID_PLATFORM: android-28
|
||||
NDK_VERSION: 27.0.11718014
|
||||
QT_VERSION: 6.10.1
|
||||
QT_VERSION: 6.10.3
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -806,7 +829,7 @@ jobs:
|
||||
python-version: 3.14
|
||||
|
||||
- name: 'Install conan'
|
||||
run: pip install "conan==2.26.2"
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Decode keystore secret to file'
|
||||
env:
|
||||
@@ -894,3 +917,4 @@ jobs:
|
||||
run: |
|
||||
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.2)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.1)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2120)
|
||||
set(APP_ANDROID_VERSION_CODE 2122)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -193,10 +193,6 @@ elseif(APPLE)
|
||||
include(cmake/macos.cmake)
|
||||
endif()
|
||||
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
|
||||
@@ -109,9 +109,6 @@ void AmneziaApplication::init()
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
win->installEventFilter(this);
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->setDefaultAlphaBuffer(true);
|
||||
#endif
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFE8E8EC"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#38FFFFFF" />
|
||||
</shape>
|
||||
@@ -8,75 +8,4 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<org.amnezia.vpn.PairingQrScanOverlayView
|
||||
android:id="@+id/pairingScanOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pairingChrome"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="28dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/pairingBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="top"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pairing_qr_camera_back"
|
||||
android:padding="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_pairing_back" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairingTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pairing_qr_camera_title"
|
||||
android:textColor="#FFE8E8EC"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairingSubtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/pairing_qr_camera_subtitle"
|
||||
android:textColor="#FFB8B8C0"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/torchButton"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/torch_fab_bg"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:text="🔦"
|
||||
android:textSize="26sp"
|
||||
android:contentDescription="@string/camera_torch" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -24,13 +24,5 @@
|
||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||
|
||||
<string name="cameraPermissionDialogTitle">Доступ к камере</string>
|
||||
<string name="cameraPermissionDialogMessage">Чтобы отсканировать QR-код для добавления устройства, Amnezia VPN нужен доступ к камере.</string>
|
||||
<string name="cameraPermissionContinue">Продолжить</string>
|
||||
<string name="camera_torch">Фонарик</string>
|
||||
<string name="pairing_qr_camera_title">Добавить устройство по QR</string>
|
||||
<string name="pairing_qr_camera_subtitle">Отсканируйте QR сессии на устройстве, которое хотите добавить. Перед отправкой подписки будет подтверждение.</string>
|
||||
<string name="pairing_qr_camera_back">Назад</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||
</resources>
|
||||
@@ -24,13 +24,5 @@
|
||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
|
||||
<string name="openNotificationSettings">Open notification settings</string>
|
||||
|
||||
<string name="cameraPermissionDialogTitle">Camera access</string>
|
||||
<string name="cameraPermissionDialogMessage">To scan a QR code for device pairing, Amnezia VPN needs access to the camera.</string>
|
||||
<string name="cameraPermissionContinue">Continue</string>
|
||||
<string name="camera_torch">Flashlight</string>
|
||||
<string name="pairing_qr_camera_title">Add device via QR</string>
|
||||
<string name="pairing_qr_camera_subtitle">Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent.</string>
|
||||
<string name="pairing_qr_camera_back">Back</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
||||
</resources>
|
||||
@@ -42,9 +42,6 @@ import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -76,18 +73,12 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
||||
private const val CREATE_FILE_ACTION_CODE = 2
|
||||
private const val OPEN_FILE_ACTION_CODE = 3
|
||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||
private const val CHECK_CAMERA_PERMISSION_ACTION_CODE = 5
|
||||
|
||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||
|
||||
class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
|
||||
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private val qtInitialized = CompletableDeferred<Unit>()
|
||||
@@ -108,8 +99,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private var lastPairingQrReaderStartUptimeMs: Long = 0L
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
@@ -216,7 +205,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -274,7 +262,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Log.d(TAG, "Start Amnezia activity")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
@@ -298,7 +285,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -371,7 +357,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
if (qtInitialized.isCompleted) {
|
||||
QtAndroidController.onActivityPaused()
|
||||
}
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
super.onPause()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
@@ -382,7 +367,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
if (qtInitialized.isCompleted) {
|
||||
@@ -499,7 +483,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
notificationStateReceiver = null
|
||||
mainScope.cancel()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -897,66 +880,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
|
||||
@Suppress("unused")
|
||||
fun isCameraPermissionGranted(): Boolean =
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
@Suppress("unused")
|
||||
fun requestCameraPermissionForQrPairing() {
|
||||
if (isCameraPermissionGranted()) {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
runOnUiThread {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.cameraPermissionDialogTitle)
|
||||
.setMessage(R.string.cameraPermissionDialogMessage)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(false)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.cameraPermissionContinue) { _, _ ->
|
||||
requestPermission(
|
||||
Manifest.permission.CAMERA,
|
||||
CHECK_CAMERA_PERMISSION_ACTION_CODE,
|
||||
PermissionRequestHandler(
|
||||
onSuccess = {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(true)
|
||||
}
|
||||
},
|
||||
onFail = {
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onCameraPermissionResult(false)
|
||||
}
|
||||
},
|
||||
onAny = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun openApplicationDetailsSettings() {
|
||||
try {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
startActivity(this)
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(TAG, "openApplicationDetailsSettings: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
@@ -1005,19 +928,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun startPairingQrCodeReader() {
|
||||
val now = SystemClock.uptimeMillis()
|
||||
if (now - lastPairingQrReaderStartUptimeMs < 1200L) {
|
||||
return
|
||||
}
|
||||
lastPairingQrReaderStartUptimeMs = now
|
||||
Intent(this, CameraActivity::class.java).also {
|
||||
it.putExtra(CameraActivity.EXTRA_PAIRING_QR_CAMERA, true)
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setSaveLogs(enabled: Boolean) {
|
||||
Log.v(TAG, "Set save logs: $enabled")
|
||||
@@ -1269,7 +1179,6 @@ class AmneziaActivity : QtActivity(), LifecycleOwner {
|
||||
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
|
||||
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
|
||||
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
|
||||
CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION"
|
||||
else -> actionCode.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,384 +2,47 @@ package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionEvent.ACTION_UP
|
||||
import android.graphics.RectF
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.FocusMeteringAction.FLAG_AE
|
||||
import androidx.camera.core.FocusMeteringAction.FLAG_AF
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.camera.view.TransformExperimental
|
||||
import androidx.camera.view.transform.CoordinateTransform
|
||||
import androidx.camera.view.transform.ImageProxyTransformFactory
|
||||
import androidx.camera.view.transform.OutputTransform
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions.Builder
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import org.amnezia.vpn.databinding.CameraPreviewBinding
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
import org.amnezia.vpn.util.Log
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "CameraActivity"
|
||||
|
||||
@OptIn(TransformExperimental::class)
|
||||
class CameraActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PAIRING_QR_CAMERA = "org.amnezia.vpn.extra.PAIRING_QR_CAMERA"
|
||||
}
|
||||
|
||||
private lateinit var viewBinding: CameraPreviewBinding
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var boundCamera: Camera? = null
|
||||
private var boundImageAnalysis: ImageAnalysis? = null
|
||||
private var torchOn: Boolean = false
|
||||
|
||||
private var imageAnalysisExecutor: ExecutorService? = null
|
||||
|
||||
private val qrHandledOrClosing = AtomicBoolean(false)
|
||||
|
||||
private var pairingQrDeliveredToQt = false
|
||||
|
||||
private var pairingQrUserDismissedCamera = false
|
||||
|
||||
private var barcodeScanner: BarcodeScanner? = null
|
||||
|
||||
private val cachedPreviewOutputTransform = AtomicReference<OutputTransform?>(null)
|
||||
|
||||
private var previewTransformLayoutListener: View.OnLayoutChangeListener? = null
|
||||
|
||||
private var previewStreamStateObserver: Observer<PreviewView.StreamState>? = null
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomHeaderBottomPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomStatusBarTopPx = 0f
|
||||
|
||||
@Volatile
|
||||
private var pairingGeomDensity = 1f
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
|
||||
@ExperimentalGetImage
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewBinding = CameraPreviewBinding.inflate(layoutInflater)
|
||||
setContentView(viewBinding.root)
|
||||
viewBinding.viewFinder.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val density = resources.displayMetrics.density
|
||||
val padH = (8 * density).toInt()
|
||||
val padTopBase = (28 * density).toInt()
|
||||
val padBottom = (12 * density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.pairingChrome) { v, windowInsets ->
|
||||
val bars = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||
v.setPadding(padH, padTopBase + bars.top, (16 * density).toInt(), padBottom)
|
||||
v.post { onPairingLayoutGeometryChanged() }
|
||||
windowInsets
|
||||
}
|
||||
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
|
||||
viewBinding.pairingChrome.visibility = View.VISIBLE
|
||||
viewBinding.root.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
viewBinding.root.post { onPairingLayoutGeometryChanged() }
|
||||
}
|
||||
viewBinding.root.post {
|
||||
onPairingLayoutGeometryChanged()
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.pairingBack.setOnClickListener { releaseCameraAndFinish() }
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
releaseCameraAndFinish()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
viewBinding.torchButton.setOnClickListener {
|
||||
torchOn = !torchOn
|
||||
try {
|
||||
boundCamera?.cameraControl?.enableTorch(torchOn)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Torch: $e")
|
||||
}
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
if (!intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
cleanupCameraResources()
|
||||
qrHandledOrClosing.set(false)
|
||||
pairingQrDeliveredToQt = false
|
||||
pairingQrUserDismissedCamera = false
|
||||
torchOn = false
|
||||
viewBinding.pairingScanOverlay.visibility = View.VISIBLE
|
||||
viewBinding.pairingChrome.visibility = View.VISIBLE
|
||||
viewBinding.root.post {
|
||||
onPairingLayoutGeometryChanged()
|
||||
applyPairingTorchButtonChrome()
|
||||
}
|
||||
checkPermissions(onSuccess = ::startCamera, onFail = ::finish)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cleanupCameraResources()
|
||||
val pairing = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
|
||||
if (pairing && !pairingQrDeliveredToQt && !pairingQrUserDismissedCamera) {
|
||||
try {
|
||||
QtAndroidController.onPairingQrCameraClosed()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onPairingQrCameraClosed: $t")
|
||||
}
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/** Idempotent: safe from back, successful decode, or process death. */
|
||||
private fun cleanupCameraResources() {
|
||||
qrHandledOrClosing.set(true)
|
||||
try {
|
||||
boundImageAnalysis?.clearAnalyzer()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
boundImageAnalysis = null
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
barcodeScanner = null
|
||||
try {
|
||||
boundCamera?.cameraControl?.enableTorch(false)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
boundCamera = null
|
||||
try {
|
||||
cameraProvider?.unbindAll()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
imageAnalysisExecutor?.let { ex ->
|
||||
try {
|
||||
ex.shutdown()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
imageAnalysisExecutor = null
|
||||
previewTransformLayoutListener?.let { listener ->
|
||||
if (::viewBinding.isInitialized) {
|
||||
viewBinding.viewFinder.removeOnLayoutChangeListener(listener)
|
||||
}
|
||||
}
|
||||
previewTransformLayoutListener = null
|
||||
previewStreamStateObserver?.let { obs ->
|
||||
if (::viewBinding.isInitialized) {
|
||||
viewBinding.viewFinder.previewStreamState.removeObserver(obs)
|
||||
}
|
||||
}
|
||||
previewStreamStateObserver = null
|
||||
cachedPreviewOutputTransform.set(null)
|
||||
}
|
||||
|
||||
private fun refreshCachedPreviewOutputTransform() {
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
val vf = viewBinding.viewFinder
|
||||
try {
|
||||
val out = vf.outputTransform
|
||||
cachedPreviewOutputTransform.set(out)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "refreshCachedPreviewOutputTransform: $t")
|
||||
cachedPreviewOutputTransform.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleCachedPreviewOutputTransformRefresh() {
|
||||
if (!::viewBinding.isInitialized) {
|
||||
return
|
||||
}
|
||||
viewBinding.viewFinder.post { refreshCachedPreviewOutputTransform() }
|
||||
}
|
||||
|
||||
private fun onPairingLayoutGeometryChanged() {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
val root = viewBinding.root
|
||||
val chrome = viewBinding.pairingChrome
|
||||
val w = root.width
|
||||
val h = root.height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val density = resources.displayMetrics.density
|
||||
val headerBottom = if (chrome.visibility == View.VISIBLE) chrome.bottom.toFloat() else 0f
|
||||
val insets = ViewCompat.getRootWindowInsets(root)
|
||||
val statusTop = insets?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val safeBottom = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom?.toFloat() ?: 0f
|
||||
|
||||
pairingGeomHeaderBottomPx = headerBottom
|
||||
pairingGeomStatusBarTopPx = statusTop
|
||||
pairingGeomDensity = density
|
||||
|
||||
viewBinding.pairingScanOverlay.setPairingHeaderBottomPx(headerBottom)
|
||||
|
||||
val hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, headerBottom, statusTop, density)
|
||||
val torchCy = PairingQrScanGeometry.pairingIosStyleTorchCenterYPx(
|
||||
hole.bottom,
|
||||
h.toFloat(),
|
||||
headerBottom,
|
||||
safeBottom,
|
||||
density
|
||||
)
|
||||
val torchSizePx = (56f * density).roundToInt().coerceAtLeast(1)
|
||||
val topMargin = (torchCy - torchSizePx / 2f).roundToInt().coerceAtLeast(0)
|
||||
val wantGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
viewBinding.torchButton.post {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return@post
|
||||
}
|
||||
val btn = viewBinding.torchButton
|
||||
val lp = btn.layoutParams as FrameLayout.LayoutParams
|
||||
if (lp.gravity == wantGravity && lp.topMargin == topMargin && lp.bottomMargin == 0) {
|
||||
return@post
|
||||
}
|
||||
lp.gravity = wantGravity
|
||||
lp.topMargin = topMargin
|
||||
lp.bottomMargin = 0
|
||||
btn.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPairingTorchButtonChrome() {
|
||||
if (!::viewBinding.isInitialized || !intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
return
|
||||
}
|
||||
val btn = viewBinding.torchButton
|
||||
val d = resources.displayMetrics.density
|
||||
val alpha = if (torchOn) (0.42f * 255f).toInt() else (0.22f * 255f).toInt()
|
||||
val bg = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.argb(alpha, 255, 255, 255))
|
||||
if (torchOn) {
|
||||
setStroke((2f * d).roundToInt(), Color.rgb(255, 191, 115))
|
||||
} else {
|
||||
setStroke(0, 0)
|
||||
}
|
||||
}
|
||||
btn.background = bg
|
||||
}
|
||||
|
||||
private fun pairingHoleRectInImageSpace(
|
||||
viewFinder: PreviewView,
|
||||
imageProxy: ImageProxy,
|
||||
imageWidth: Int,
|
||||
imageHeight: Int
|
||||
): RectF {
|
||||
val vw = viewFinder.width
|
||||
val vh = viewFinder.height
|
||||
fun geomFallback(): RectF =
|
||||
PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
|
||||
vw,
|
||||
vh,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
)
|
||||
if (vw <= 0 || vh <= 0 || imageWidth <= 0 || imageHeight <= 0) {
|
||||
return geomFallback()
|
||||
}
|
||||
return try {
|
||||
val previewOut = cachedPreviewOutputTransform.get()
|
||||
if (previewOut == null) {
|
||||
geomFallback()
|
||||
} else {
|
||||
val imageFactory = ImageProxyTransformFactory().apply {
|
||||
setUsingRotationDegrees(true)
|
||||
}
|
||||
val imageOut = imageFactory.getOutputTransform(imageProxy)
|
||||
val holeView = PairingQrScanGeometry.pairingIosStyleHoleRectF(
|
||||
vw,
|
||||
vh,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity
|
||||
)
|
||||
if (holeView.width() <= 0f || holeView.height() <= 0f) {
|
||||
return geomFallback()
|
||||
}
|
||||
val hole = RectF(holeView)
|
||||
CoordinateTransform(previewOut, imageOut).mapRect(hole)
|
||||
hole
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "pairingHoleRectInImageSpace: $t")
|
||||
geomFallback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseCameraAndFinish() {
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
pairingQrUserDismissedCamera = true
|
||||
try {
|
||||
QtAndroidController.onPairingQrCameraUserDismissed()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onPairingQrCameraUserDismissed: $t")
|
||||
}
|
||||
}
|
||||
cleanupCameraResources()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun checkPermissions(onSuccess: () -> Unit, onFail: () -> Unit) {
|
||||
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
onSuccess()
|
||||
@@ -404,41 +67,26 @@ class CameraActivity : ComponentActivity() {
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
bindCameraUseCases()
|
||||
bindPreview()
|
||||
bindImageAnalysis()
|
||||
}, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@ExperimentalGetImage
|
||||
private fun bindCameraUseCases() {
|
||||
val provider = cameraProvider ?: return
|
||||
imageAnalysisExecutor?.shutdown()
|
||||
imageAnalysisExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
private fun bindPreview() {
|
||||
val viewFinder = viewBinding.viewFinder
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(viewFinder.surfaceProvider)
|
||||
}
|
||||
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
|
||||
val camera = provider.bindToLifecycle(
|
||||
this,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
imageAnalysis
|
||||
)
|
||||
boundCamera = camera
|
||||
boundImageAnalysis = imageAnalysis
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview)
|
||||
|
||||
viewFinder.setOnTouchListener { _, motionEvent ->
|
||||
when (motionEvent.action) {
|
||||
ACTION_DOWN -> true
|
||||
ACTION_UP -> {
|
||||
val point = viewFinder
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
|
||||
.meteringPointFactory.createPoint(motionEvent.x, motionEvent.x)
|
||||
|
||||
val action = FocusMeteringAction
|
||||
.Builder(point, FLAG_AF or FLAG_AE).build()
|
||||
@@ -450,121 +98,58 @@ class CameraActivity : ComponentActivity() {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)) {
|
||||
previewTransformLayoutListener?.let { viewFinder.removeOnLayoutChangeListener(it) }
|
||||
val layoutListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
viewFinder.post {
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
onPairingLayoutGeometryChanged()
|
||||
}
|
||||
}
|
||||
previewTransformLayoutListener = layoutListener
|
||||
viewFinder.addOnLayoutChangeListener(layoutListener)
|
||||
previewStreamStateObserver?.let { viewFinder.previewStreamState.removeObserver(it) }
|
||||
val streamObserver = Observer<PreviewView.StreamState> { state ->
|
||||
if (state == PreviewView.StreamState.STREAMING) {
|
||||
viewFinder.post {
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
onPairingLayoutGeometryChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
previewStreamStateObserver = streamObserver
|
||||
viewFinder.previewStreamState.observe(this, streamObserver)
|
||||
scheduleCachedPreviewOutputTransformRefresh()
|
||||
}
|
||||
@ExperimentalGetImage
|
||||
private fun bindImageAnalysis() {
|
||||
val imageAnalysis = ImageAnalysis.Builder().build()
|
||||
|
||||
try {
|
||||
barcodeScanner?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
val camera = cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, imageAnalysis)
|
||||
|
||||
barcodeScanner = BarcodeScanning.getClient(
|
||||
val barcodeScanner = BarcodeScanning.getClient(
|
||||
Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
.setZoomSuggestionOptions(
|
||||
ZoomSuggestionOptions.Builder { zoomLevel ->
|
||||
camera.cameraControl.setZoomRatio(zoomLevel)
|
||||
true
|
||||
}.apply {
|
||||
camera.cameraInfo.zoomState.value?.maxZoomRatio?.let { maxZoomRation ->
|
||||
setMaxSupportedZoomRatio(maxZoomRation)
|
||||
}
|
||||
}.build()
|
||||
).build()
|
||||
)
|
||||
|
||||
// optimization
|
||||
val checkedBarcodes = hashSetOf<String>()
|
||||
val analysisExecutor = imageAnalysisExecutor!!
|
||||
val mainExecutor = ContextCompat.getMainExecutor(this)
|
||||
val pairingQrMode = intent.getBooleanExtra(EXTRA_PAIRING_QR_CAMERA, false)
|
||||
|
||||
imageAnalysis.setAnalyzer(analysisExecutor) { imageProxy ->
|
||||
if (qrHandledOrClosing.get()) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage == null) {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
val viewW = viewFinder.width
|
||||
val viewH = viewFinder.height
|
||||
val pairingRoi = if (pairingQrMode) {
|
||||
pairingHoleRectInImageSpace(viewFinder, imageProxy, image.width, image.height)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val scanner = barcodeScanner ?: run {
|
||||
imageProxy.close()
|
||||
return@setAnalyzer
|
||||
}
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener(mainExecutor) { barcodes ->
|
||||
if (qrHandledOrClosing.get()) {
|
||||
return@addOnSuccessListener
|
||||
}
|
||||
val barcode = if (pairingQrMode) {
|
||||
val roi = pairingRoi
|
||||
?: PairingQrScanGeometry.pairingIosStyleHoleInImageCoords(
|
||||
viewW,
|
||||
viewH,
|
||||
pairingGeomHeaderBottomPx,
|
||||
pairingGeomStatusBarTopPx,
|
||||
pairingGeomDensity,
|
||||
image.width,
|
||||
image.height
|
||||
)
|
||||
barcodes.firstOrNull {
|
||||
PairingQrScanGeometry.barcodeMatchesPairingHole(
|
||||
roi,
|
||||
image.width,
|
||||
image.height,
|
||||
it
|
||||
)
|
||||
}
|
||||
} else {
|
||||
barcodes.firstOrNull()
|
||||
}
|
||||
barcode?.displayValue?.let { code ->
|
||||
if (code.isNotEmpty() && code !in checkedBarcodes) {
|
||||
checkedBarcodes.add(code)
|
||||
if (QtAndroidController.decodeQrCode(code)) {
|
||||
if (qrHandledOrClosing.compareAndSet(false, true)) {
|
||||
if (pairingQrMode) {
|
||||
pairingQrDeliveredToQt = true
|
||||
}
|
||||
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)) { imageProxy ->
|
||||
imageProxy.image?.let { InputImage.fromMediaImage(it, imageProxy.imageInfo.rotationDegrees) }
|
||||
?.let { image ->
|
||||
barcodeScanner.process(image).addOnSuccessListener { barcodes ->
|
||||
barcodes.firstOrNull()?.let { barcode ->
|
||||
barcode.displayValue?.let { code ->
|
||||
if (code.isNotEmpty() && code !in checkedBarcodes) {
|
||||
if (QtAndroidController.decodeQrCode(code)) {
|
||||
barcodeScanner.close()
|
||||
stopCamera()
|
||||
}
|
||||
checkedBarcodes.add(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
.addOnFailureListener(mainExecutor) {
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}
|
||||
.addOnCompleteListener(mainExecutor) {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopCamera() {
|
||||
cleanupCameraResources()
|
||||
cameraProvider.unbindAll()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanBracketPaths {
|
||||
|
||||
private fun Path.addCornerMinorArc(
|
||||
cx: Float,
|
||||
cy: Float,
|
||||
r: Float,
|
||||
sx: Float,
|
||||
sy: Float,
|
||||
ex: Float,
|
||||
ey: Float
|
||||
) {
|
||||
var asRad = atan2((sy - cy).toDouble(), (sx - cx).toDouble())
|
||||
var aeRad = atan2((ey - cy).toDouble(), (ex - cx).toDouble())
|
||||
while (aeRad - asRad > PI) {
|
||||
aeRad -= 2.0 * PI
|
||||
}
|
||||
while (aeRad - asRad < -PI) {
|
||||
aeRad += 2.0 * PI
|
||||
}
|
||||
val minor = aeRad - asRad
|
||||
val startDeg = Math.toDegrees(asRad).toFloat()
|
||||
val sweepDeg = Math.toDegrees(minor).toFloat()
|
||||
addArc(RectF(cx - r, cy - r, cx + r, cy + r), startDeg, sweepDeg)
|
||||
}
|
||||
|
||||
fun bracketStrokePath(corner: Int, x0: Float, y0: Float, s: Float, R: Float, L: Float, t: Float): Path {
|
||||
val r = max(1.5f, R - t * 0.5f)
|
||||
val p = Path()
|
||||
val yy = y0 + t * 0.5f
|
||||
val yyb = y0 + s - t * 0.5f
|
||||
val xx = x0 + t * 0.5f
|
||||
val xxb = x0 + s - t * 0.5f
|
||||
|
||||
when (corner) {
|
||||
0 -> {
|
||||
val cTLx = x0 + R
|
||||
val cTLy = y0 + R
|
||||
val sTLx = x0 + R
|
||||
val sTLy = yy
|
||||
val eTLx = xx
|
||||
val eTLy = y0 + R
|
||||
p.moveTo(x0 + R + L, yy)
|
||||
p.lineTo(sTLx, sTLy)
|
||||
p.addCornerMinorArc(cTLx, cTLy, r, sTLx, sTLy, eTLx, eTLy)
|
||||
val yEndTL = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xx, max(yEndTL, y0 + R + 2f))
|
||||
}
|
||||
1 -> {
|
||||
val cTRx = x0 + s - R
|
||||
val cTRy = y0 + R
|
||||
val sTRx = x0 + s - R
|
||||
val sTRy = yy
|
||||
val eTRx = xxb
|
||||
val eTRy = y0 + R
|
||||
p.moveTo(x0 + s - R - L, yy)
|
||||
p.lineTo(sTRx, sTRy)
|
||||
p.addCornerMinorArc(cTRx, cTRy, r, sTRx, sTRy, eTRx, eTRy)
|
||||
val yEndTR = min(y0 + R + L, y0 + s - R - t * 0.5f)
|
||||
p.lineTo(xxb, max(yEndTR, y0 + R + 2f))
|
||||
}
|
||||
2 -> {
|
||||
val cBLx = x0 + R
|
||||
val cBLy = y0 + s - R
|
||||
val sBLx = x0 + R
|
||||
val sBLy = yyb
|
||||
val eBLx = xx
|
||||
val eBLy = y0 + s - R
|
||||
p.moveTo(x0 + R + L, yyb)
|
||||
p.lineTo(sBLx, sBLy)
|
||||
p.addCornerMinorArc(cBLx, cBLy, r, sBLx, sBLy, eBLx, eBLy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBL = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xx, yLegBL)
|
||||
}
|
||||
3 -> {
|
||||
val cBRx = x0 + s - R
|
||||
val cBRy = y0 + s - R
|
||||
val sBRx = x0 + s - R
|
||||
val sBRy = yyb
|
||||
val eBRx = xxb
|
||||
val eBRy = y0 + s - R
|
||||
p.moveTo(x0 + s - R - L, yyb)
|
||||
p.lineTo(sBRx, sBRy)
|
||||
p.addCornerMinorArc(cBRx, cBRy, r, sBRx, sBRy, eBRx, eBRy)
|
||||
val yEndTopRef = max(min(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2f)
|
||||
val yLegBR = y0 + s + y0 - yEndTopRef
|
||||
p.lineTo(xxb, yLegBR)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object PairingQrScanGeometry {
|
||||
fun viewRectToInputImageRectFillCenter(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
viewRect: RectF
|
||||
): RectF {
|
||||
val scale = max(viewW / imageW.toFloat(), viewH / imageH.toFloat())
|
||||
val drawLeft = (viewW - imageW * scale) / 2f
|
||||
val drawTop = (viewH - imageH * scale) / 2f
|
||||
return RectF(
|
||||
(viewRect.left - drawLeft) / scale,
|
||||
(viewRect.top - drawTop) / scale,
|
||||
(viewRect.right - drawLeft) / scale,
|
||||
(viewRect.bottom - drawTop) / scale
|
||||
)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float {
|
||||
val d = density
|
||||
var holeR = min(28f * d, max(10f * d, sidePx * 0.056f))
|
||||
val half = 0.5f * sidePx
|
||||
holeR = min(holeR, max(6f * d, half - 2f * d))
|
||||
return max(holeR, 1f)
|
||||
}
|
||||
|
||||
fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float {
|
||||
val bf = RectF(box)
|
||||
val inter = RectF(roi)
|
||||
if (!inter.intersect(bf)) return 0f
|
||||
val interArea = inter.width() * inter.height()
|
||||
val boxArea = bf.width() * bf.height()
|
||||
return if (boxArea <= 0f) 0f else interArea / boxArea
|
||||
}
|
||||
|
||||
fun barcodeMatchesPairingHole(
|
||||
roiInImageSpace: RectF,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
barcode: Barcode,
|
||||
minOverlapFraction: Float = PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK
|
||||
): Boolean {
|
||||
if (imageW <= 0 || imageH <= 0) {
|
||||
return false
|
||||
}
|
||||
val roi = RectF(roiInImageSpace)
|
||||
val iw = imageW.toFloat()
|
||||
val ih = imageH.toFloat()
|
||||
roi.left = max(0f, roi.left)
|
||||
roi.top = max(0f, roi.top)
|
||||
roi.right = min(iw, roi.right)
|
||||
roi.bottom = min(ih, roi.bottom)
|
||||
if (roi.width() <= 0f || roi.height() <= 0f) {
|
||||
return false
|
||||
}
|
||||
|
||||
val corners = barcode.cornerPoints
|
||||
if (corners != null && corners.size >= 4) {
|
||||
for (p in corners) {
|
||||
if (!roi.contains(p.x.toFloat(), p.y.toFloat())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val box = barcode.boundingBox ?: return false
|
||||
val cx = box.centerX().toFloat()
|
||||
val cy = box.centerY().toFloat()
|
||||
if (!roi.contains(cx, cy)) {
|
||||
return false
|
||||
}
|
||||
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
|
||||
}
|
||||
|
||||
private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f
|
||||
|
||||
fun pairingIosStyleHoleRectF(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float
|
||||
): RectF {
|
||||
val w = viewW.toFloat()
|
||||
val h = viewH.toFloat()
|
||||
val d = density
|
||||
if (w < 32f || h < 32f) {
|
||||
return RectF()
|
||||
}
|
||||
var hdrBottom = headerBottomPx
|
||||
if (hdrBottom < 8f * d) {
|
||||
hdrBottom = 132f * d + statusBarTopPx
|
||||
}
|
||||
val sqSz = floor(min(w, h) * 0.72).toFloat()
|
||||
var sqX = (w - sqSz) / 2f
|
||||
var sqY = (h - sqSz) / 2f
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
val kBottomBand = 80f * d
|
||||
val maxHoleBottom = h - kBottomBand
|
||||
if (sqY + sqSz > maxHoleBottom) {
|
||||
sqY = maxHoleBottom - sqSz
|
||||
sqY = max(sqY, hdrBottom + 8f * d)
|
||||
}
|
||||
sqX = max(8f * d, min(sqX, w - sqSz - 8f * d))
|
||||
sqY = max(hdrBottom + 4f * d, min(sqY, h - sqSz - 8f * d))
|
||||
return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz)
|
||||
}
|
||||
|
||||
fun pairingIosStyleTorchCenterYPx(
|
||||
holeBottomPx: Float,
|
||||
bandBottomPx: Float,
|
||||
headerBottomPx: Float,
|
||||
safeBottomPx: Float,
|
||||
density: Float
|
||||
): Float {
|
||||
val torchH = 56f * density
|
||||
val d = density
|
||||
var torchCy = (holeBottomPx + bandBottomPx) * 0.5f
|
||||
val minC = holeBottomPx + torchH * 0.5f + 6f * d
|
||||
val maxC = bandBottomPx - torchH * 0.5f - max(6f * d, safeBottomPx)
|
||||
torchCy = max(minC, min(maxC, torchCy))
|
||||
if (minC > maxC) {
|
||||
torchCy = (minC + maxC) * 0.5f
|
||||
}
|
||||
val hdr = headerBottomPx + torchH * 0.5f + 10f * d
|
||||
return max(torchCy, hdr)
|
||||
}
|
||||
|
||||
fun pairingIosStyleHoleInImageCoords(
|
||||
viewW: Int,
|
||||
viewH: Int,
|
||||
headerBottomPx: Float,
|
||||
statusBarTopPx: Float,
|
||||
density: Float,
|
||||
imageW: Int,
|
||||
imageH: Int
|
||||
): RectF {
|
||||
val hv = pairingIosStyleHoleRectF(viewW, viewH, headerBottomPx, statusBarTopPx, density)
|
||||
return viewRectToInputImageRectFillCenter(viewW, viewH, imageW, imageH, hv)
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import kotlin.math.max
|
||||
|
||||
class PairingQrScanOverlayView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
init {
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean = false
|
||||
|
||||
private val dimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0x8C000000.toInt()
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val bracketPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = 0xFFE8E8EC.toInt()
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private var hole = RectF()
|
||||
|
||||
private val bracketPaths = arrayOfNulls<Path>(4)
|
||||
|
||||
private val dimPath = Path()
|
||||
|
||||
private var pairingHeaderBottomPx = 0f
|
||||
|
||||
fun setPairingHeaderBottomPx(px: Float) {
|
||||
if (pairingHeaderBottomPx == px) {
|
||||
return
|
||||
}
|
||||
pairingHeaderBottomPx = px
|
||||
recomputePairingHole()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun recomputePairingHole() {
|
||||
val w = width
|
||||
val h = height
|
||||
if (w <= 0 || h <= 0) {
|
||||
return
|
||||
}
|
||||
val topInset = ViewCompat.getRootWindowInsets(this)
|
||||
?.getInsets(WindowInsetsCompat.Type.statusBars())?.top?.toFloat() ?: 0f
|
||||
val d = resources.displayMetrics.density
|
||||
hole = PairingQrScanGeometry.pairingIosStyleHoleRectF(w, h, pairingHeaderBottomPx, topInset, d)
|
||||
rebuildBracketPaths()
|
||||
}
|
||||
|
||||
private fun rebuildBracketPaths() {
|
||||
val s = hole.width()
|
||||
if (s <= 0f) {
|
||||
bracketPaths.fill(null)
|
||||
return
|
||||
}
|
||||
val x0 = hole.left
|
||||
val y0 = hole.top
|
||||
val t = bracketPaint.strokeWidth
|
||||
val d = resources.displayMetrics.density
|
||||
val l = max(28f * d, s * 0.13f)
|
||||
val r = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(s, d)
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i] = PairingQrScanBracketPaths.bracketStrokePath(i, x0, y0, s, r, l, t)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
bracketPaint.strokeWidth = max(3f, 5f * resources.displayMetrics.density)
|
||||
recomputePairingHole()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
val side = hole.width()
|
||||
if (side > 0f) {
|
||||
val d = resources.displayMetrics.density
|
||||
val rx = PairingQrScanGeometry.pairingIosStyleHoleCornerRadiusPx(side, d)
|
||||
dimPath.rewind()
|
||||
dimPath.fillType = Path.FillType.EVEN_ODD
|
||||
dimPath.addRect(0f, 0f, w, h, Path.Direction.CW)
|
||||
dimPath.addRoundRect(hole, rx, rx, Path.Direction.CW)
|
||||
canvas.drawPath(dimPath, dimPaint)
|
||||
} else {
|
||||
canvas.drawRect(0f, 0f, w, h, dimPaint)
|
||||
}
|
||||
|
||||
for (i in 0..3) {
|
||||
bracketPaths[i]?.let { canvas.drawPath(it, bracketPaint) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,4 @@ object QtAndroidController {
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
|
||||
external fun onCameraPermissionResult(granted: Boolean)
|
||||
|
||||
external fun onPairingQrCameraClosed()
|
||||
|
||||
external fun onPairingQrCameraUserDismissed()
|
||||
}
|
||||
@@ -28,7 +28,6 @@ set(LIBS ${LIBS}
|
||||
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
@@ -45,8 +44,6 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingQrOverlayWindow.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||
|
||||
@@ -49,7 +49,6 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||
)
|
||||
|
||||
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||
|
||||
@@ -45,7 +45,6 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.h
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.h
|
||||
@@ -66,8 +65,6 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/constants.h
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingQrOverlayWindow.h
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -125,7 +122,6 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/subscriptionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/pairingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/newsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/updateController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/repositories/secureServersRepository.cpp
|
||||
@@ -161,7 +157,6 @@ set(SOURCES ${SOURCES}
|
||||
if(NOT IOS AND NOT MACOS_NE)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
@@ -137,117 +138,326 @@ amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const a
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
|
||||
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
|
||||
{
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
// Get flow value from settings (default xtls-rprx-vision)
|
||||
QString flowValue = "xtls-rprx-vision";
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (!xrayCfg->serverConfig.flow.isEmpty()) {
|
||||
flowValue = xrayCfg->serverConfig.flow;
|
||||
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript,
|
||||
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
|
||||
dnsSettings.secondaryDns)));
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
|
||||
QString &outPublicKey, QString &outShortId) const
|
||||
{
|
||||
outPublicKey.clear();
|
||||
outShortId.clear();
|
||||
|
||||
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
ErrorCode fileError = ErrorCode::NoError;
|
||||
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
|
||||
out.replace(QLatin1Char('\n'), QString());
|
||||
out.replace(QLatin1Char('\r'), QString());
|
||||
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
if (attempt < 2) {
|
||||
QThread::msleep(500);
|
||||
}
|
||||
}
|
||||
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
|
||||
return ErrorCode::XrayRealityKeysReadFailed;
|
||||
};
|
||||
|
||||
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::mergeStreamSettingsForServerInbound(const XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const
|
||||
{
|
||||
QJsonObject streamSettings = buildStreamSettings(srv, QString());
|
||||
|
||||
if (srv.security != QLatin1String("reality")) {
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
const QJsonObject newRs = streamSettings[amnezia::protocols::xray::realitySettings].toObject();
|
||||
QJsonObject oldRs = existingStreamSettings[amnezia::protocols::xray::realitySettings].toObject();
|
||||
QJsonObject merged = oldRs.isEmpty() ? newRs : oldRs;
|
||||
|
||||
const QString siteEff = srv.site.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultSite) : srv.site;
|
||||
const QString sniEff = srv.sni.isEmpty() ? siteEff : srv.sni;
|
||||
|
||||
if (newRs.contains(amnezia::protocols::xray::fingerprint)) {
|
||||
merged[amnezia::protocols::xray::fingerprint] = newRs[amnezia::protocols::xray::fingerprint];
|
||||
}
|
||||
merged[amnezia::protocols::xray::serverNames] = QJsonArray { sniEff };
|
||||
if (!merged.contains(QStringLiteral("dest"))) {
|
||||
merged[QStringLiteral("dest")] = siteEff + QStringLiteral(":443");
|
||||
}
|
||||
|
||||
streamSettings[amnezia::protocols::xray::realitySettings] = merged;
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
|
||||
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
|
||||
bool appendNewClient, QString *outClientId)
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
|
||||
if (!xrayCfg) {
|
||||
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = xrayCfg->serverConfig;
|
||||
if (srv.isThirdPartyConfig) {
|
||||
logger.info() << "Xray applyServerSettings: skipped (third-party/native profile)";
|
||||
if (outClientId && xrayCfg->hasClientConfig()) {
|
||||
*outClientId = xrayCfg->clientConfig->id;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
logger.info() << "Xray applyServerSettings: start"
|
||||
<< "container=" << static_cast<int>(container) << "host=" << credentials.hostName
|
||||
<< "transport=" << srv.transport << "security=" << srv.security << "port=" << srv.port
|
||||
<< "appendClient=" << appendNewClient;
|
||||
QString flowValue = srv.flow;
|
||||
if (flowValue.isEmpty() && srv.security == QLatin1String("reality")) {
|
||||
flowValue = QStringLiteral("xtls-rprx-vision");
|
||||
}
|
||||
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
|
||||
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: read server config, bytes=" << currentConfig.size();
|
||||
|
||||
// Parse current config as JSON
|
||||
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
return ErrorCode::XrayServerConfigInvalid;
|
||||
}
|
||||
|
||||
const QJsonObject existingStream = inbound[amnezia::protocols::xray::streamSettings].toObject();
|
||||
inbound[amnezia::protocols::xray::streamSettings] = mergeStreamSettingsForServerInbound(srv, existingStream);
|
||||
|
||||
if (!srv.port.isEmpty()) {
|
||||
inbound[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
|
||||
if (!settings.contains(amnezia::protocols::xray::clients)) {
|
||||
logger.error() << "Settings missing 'clients' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
settings[amnezia::protocols::xray::clients] = QJsonArray {};
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
QString clientId;
|
||||
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{amnezia::protocols::xray::id, clientId},
|
||||
};
|
||||
clientConfig[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientConfig[amnezia::protocols::xray::flow] = flowValue;
|
||||
if (appendNewClient) {
|
||||
clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QJsonObject clientEntry;
|
||||
clientEntry[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientEntry[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
clients.append(clientEntry);
|
||||
} else {
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Server config has no VLESS clients";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
clientId = clients[0].toObject()[amnezia::protocols::xray::id].toString();
|
||||
if (clientId.isEmpty()) {
|
||||
logger.error() << "Server config VLESS client has empty id";
|
||||
return ErrorCode::XrayServerNoVlessClients;
|
||||
}
|
||||
QJsonArray updatedClients;
|
||||
for (const QJsonValue &v : clients) {
|
||||
QJsonObject c = v.toObject();
|
||||
if (flowValue.isEmpty()) {
|
||||
c.remove(amnezia::protocols::xray::flow);
|
||||
} else {
|
||||
c[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
updatedClients.append(c);
|
||||
}
|
||||
clients = updatedClients;
|
||||
}
|
||||
|
||||
clients.append(clientConfig);
|
||||
|
||||
// Update config
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
// Save updated config to server
|
||||
QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container,
|
||||
credentials,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
);
|
||||
errorCode = uploadServerConfigJson(credentials, container, dnsSettings, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return "";
|
||||
logger.error() << "Xray applyServerSettings: upload/restart failed, error=" << static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
logger.info() << "Xray applyServerSettings: server config uploaded and container restarted";
|
||||
|
||||
if (outClientId) {
|
||||
*outClientId = clientId;
|
||||
}
|
||||
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
|
||||
);
|
||||
|
||||
XrayProtocolConfig updated =
|
||||
buildClientProtocolConfig(credentials, container, srv, clientId, errorCode, realityPublicKey, realityShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
return "";
|
||||
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
|
||||
<< static_cast<int>(errorCode);
|
||||
return errorCode;
|
||||
}
|
||||
containerConfig.protocolConfig = updated;
|
||||
logger.info() << "Xray applyServerSettings: done, clientId=" << clientId;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
ContainerConfig mutableConfig = containerConfig;
|
||||
QString clientId;
|
||||
const ErrorCode applyError =
|
||||
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId);
|
||||
errorCode = applyError;
|
||||
if (applyError != ErrorCode::NoError || clientId.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
|
||||
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
|
||||
DockerContainer container,
|
||||
const XrayServerConfig &srv, const QString &clientId,
|
||||
ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey,
|
||||
const QString &prefetchedRealityShortId) const
|
||||
{
|
||||
QString xrayPublicKey = prefetchedRealityPublicKey;
|
||||
QString xrayShortId = prefetchedRealityShortId;
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
|
||||
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = clientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = QStringLiteral("none");
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] =
|
||||
srv.port.isEmpty() ? QString(amnezia::protocols::xray::defaultPort).toInt() : srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound[QStringLiteral("protocol")] = QStringLiteral("vless");
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
QJsonObject streamObj = buildStreamSettings(srv, clientId);
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = QString();
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
QJsonObject inboundObj;
|
||||
inboundObj[QStringLiteral("listen")] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj[QStringLiteral("protocol")] = QStringLiteral("socks");
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { QStringLiteral("udp"), true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson[QStringLiteral("log")] = QJsonObject { { QStringLiteral("loglevel"), QStringLiteral("error") } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
const QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = clientId;
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
@@ -419,6 +629,13 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (xrayCfg->serverConfig.isThirdPartyConfig && xrayCfg->hasClientConfig()) {
|
||||
logger.info() << "Xray createConfig: returning existing third-party client config without server SSH";
|
||||
return *xrayCfg;
|
||||
}
|
||||
}
|
||||
|
||||
const XrayServerConfig *serverConfig = nullptr;
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayCfg->serverConfig;
|
||||
@@ -441,93 +658,5 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
|
||||
// Fetch server keys (Reality only)
|
||||
QString xrayPublicKey;
|
||||
QString xrayShortId;
|
||||
|
||||
if (srv.security == "reality") {
|
||||
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
errorCode = ErrorCode::InternalError;
|
||||
}
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
}
|
||||
|
||||
// Build outbound
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = xrayClientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = "none";
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound["protocol"] = "vless";
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
// Build streamSettings
|
||||
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
|
||||
|
||||
// Inject Reality keys
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = "";
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
// Build full client config
|
||||
QJsonObject inboundObj;
|
||||
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj["protocol"] = "socks";
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
// Return
|
||||
XrayProtocolConfig protocolConfig;
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
qDebug() << "config:" << config;
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
return protocolConfig;
|
||||
return buildClientProtocolConfig(credentials, container, srv, xrayClientId, errorCode);
|
||||
}
|
||||
@@ -23,12 +23,37 @@ public:
|
||||
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
|
||||
amnezia::ProtocolConfig protocolConfig) override;
|
||||
|
||||
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
bool appendNewClient,
|
||||
QString *outClientId = nullptr);
|
||||
|
||||
private:
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
|
||||
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
|
||||
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
|
||||
|
||||
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId,
|
||||
amnezia::ErrorCode &errorCode,
|
||||
const QString &prefetchedRealityPublicKey = {},
|
||||
const QString &prefetchedRealityShortId = {}) const;
|
||||
|
||||
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
|
||||
const amnezia::ServerCredentials &credentials,
|
||||
QString &outPublicKey,
|
||||
QString &outShortId) const;
|
||||
|
||||
QJsonObject mergeStreamSettingsForServerInbound(const amnezia::XrayServerConfig &srv,
|
||||
const QJsonObject &existingStreamSettings) const;
|
||||
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
|
||||
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType));
|
||||
}
|
||||
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload, nullptr, gatewayController);
|
||||
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
|
||||
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {
|
||||
auto [errorCode, responseBody] = result;
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
#include "pairingController.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QSysInfo>
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qsizetype kPairingMaxQrUuidChars = 128;
|
||||
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
|
||||
constexpr qsizetype kPairingMaxApiKeyChars = 8192;
|
||||
constexpr qsizetype kPairingMaxServiceTypeChars = 64;
|
||||
constexpr qsizetype kPairingMaxUserCountryCodeChars = 32;
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiGenerateError(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
const QString config = obj.value(apiDefs::key::config).toString();
|
||||
if (!config.isEmpty()) {
|
||||
outPayload.config = config;
|
||||
outPayload.serviceInfo = obj.value(apiDefs::key::serviceInfo).toObject();
|
||||
outPayload.supportedProtocols = obj.value(apiDefs::key::supportedProtocols).toArray();
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("timeout"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode applyGatewayOrOpenApiScanError(const QJsonObject &obj)
|
||||
{
|
||||
const QString msgProbe = obj.value(QStringLiteral("message")).toString();
|
||||
if (msgProbe.contains(QStringLiteral("limit"), Qt::CaseInsensitive)
|
||||
&& (msgProbe.contains(QStringLiteral("device"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("maximum"), Qt::CaseInsensitive)
|
||||
|| msgProbe.contains(QStringLiteral("max"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiConfigLimitError;
|
||||
}
|
||||
|
||||
ErrorCode apiStatus = apiUtils::errorCodeFromGatewayJsonHttpStatus(obj);
|
||||
if (apiStatus != ErrorCode::NoError) {
|
||||
return apiStatus;
|
||||
}
|
||||
|
||||
if (obj.value(QStringLiteral("message")).toString() == QLatin1String("OK")) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (obj.contains(QStringLiteral("detail"))) {
|
||||
return ErrorCode::ApiPairingForbiddenError;
|
||||
}
|
||||
|
||||
const QString msg = obj.value(QStringLiteral("message")).toString();
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return ErrorCode::ApiPairingSessionExpiredError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Conflict"), Qt::CaseInsensitive) || msg.contains(QStringLiteral("already"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingConflictError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Too Many"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingRateLimitedError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("Unavailable"), Qt::CaseInsensitive)) {
|
||||
return ErrorCode::ApiPairingServiceUnavailableError;
|
||||
}
|
||||
if (!msg.isEmpty()) {
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
ErrorCode interpretGenerateQrJson(const QJsonObject &obj, PairingController::QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
return applyGatewayOrOpenApiGenerateError(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode interpretScanQrJson(const QJsonObject &obj)
|
||||
{
|
||||
return applyGatewayOrOpenApiScanError(obj);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ErrorCode PairingController::parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload)
|
||||
{
|
||||
outPayload = QrPairingConfigPayload {};
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
return interpretGenerateQrJson(obj, outPayload);
|
||||
}
|
||||
|
||||
ErrorCode PairingController::parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName)
|
||||
{
|
||||
if (outOptionalDisplayName) {
|
||||
outOptionalDisplayName->clear();
|
||||
}
|
||||
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
|
||||
const ErrorCode err = interpretScanQrJson(obj);
|
||||
if (err != ErrorCode::NoError) {
|
||||
return err;
|
||||
}
|
||||
if (outOptionalDisplayName) {
|
||||
const QString deviceName = obj.value(QStringLiteral("device_name")).toString().trimmed();
|
||||
if (!deviceName.isEmpty()) {
|
||||
*outOptionalDisplayName = deviceName;
|
||||
}
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode PairingController::validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
if (qrUuid.size() > kPairingMaxQrUuidChars) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
if (vpnConfig.size() > kPairingMaxVpnConfigChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
if (apiKey.size() > kPairingMaxApiKeyChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
const QString st = serviceType.trimmed();
|
||||
const QString cc = userCountryCode.trimmed();
|
||||
if (st.isEmpty() || cc.isEmpty()) {
|
||||
return ErrorCode::ApiPairingMissingMetadataError;
|
||||
}
|
||||
if (st.size() > kPairingMaxServiceTypeChars || cc.size() > kPairingMaxUserCountryCodeChars) {
|
||||
return ErrorCode::ApiPairingPayloadTooLargeError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
PairingController::PairingController(SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
}
|
||||
|
||||
int PairingController::pairingLongPollTimeoutMsecs() const
|
||||
{
|
||||
return 60 * 1000;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildGenerateQrPayload(const QString &qrUuid) const
|
||||
{
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
return o;
|
||||
}
|
||||
|
||||
QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode) const
|
||||
{
|
||||
QJsonObject auth;
|
||||
auth[apiDefs::key::apiKey] = apiKey;
|
||||
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::qrUuid] = qrUuid;
|
||||
o[apiDefs::key::config] = vpnConfig;
|
||||
o[apiDefs::key::serviceInfo] = serviceInfo;
|
||||
o[apiDefs::key::supportedProtocols] = supportedProtocols;
|
||||
o[apiDefs::key::authData] = auth;
|
||||
o[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
o[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
o[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
o[apiDefs::key::serviceType] = serviceType.trimmed();
|
||||
o[apiDefs::key::userCountryCode] = userCountryCode.trimmed();
|
||||
return o;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#ifndef PAIRINGCONTROLLER_H
|
||||
#define PAIRINGCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class PairingController
|
||||
{
|
||||
public:
|
||||
struct QrPairingConfigPayload
|
||||
{
|
||||
QString config;
|
||||
QJsonObject serviceInfo;
|
||||
QJsonArray supportedProtocols;
|
||||
};
|
||||
|
||||
explicit PairingController(SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
int pairingLongPollTimeoutMsecs() const;
|
||||
|
||||
QJsonObject buildGenerateQrPayload(const QString &qrUuid) const;
|
||||
QJsonObject buildScanQrPayload(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, const QString &apiKey, const QString &serviceType,
|
||||
const QString &userCountryCode) const;
|
||||
|
||||
static amnezia::ErrorCode parseGenerateQrResponseBody(const QByteArray &responseBody, QrPairingConfigPayload &outPayload);
|
||||
static amnezia::ErrorCode parseScanQrResponseBody(const QByteArray &responseBody, QString *outOptionalDisplayName = nullptr);
|
||||
|
||||
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey,
|
||||
const QString &serviceType, const QString &userCountryCode);
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
};
|
||||
|
||||
#endif // PAIRINGCONTROLLER_H
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QPromise>
|
||||
#include <QSet>
|
||||
#include <QSysInfo>
|
||||
@@ -216,7 +217,8 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData)
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -233,6 +235,19 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
|
||||
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
|
||||
captchaInfo.hint = jsonObj.value("hint").toString();
|
||||
captchaInfo.isRequired = true;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -242,9 +257,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -312,71 +327,6 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols,
|
||||
int *duplicateServerIndex)
|
||||
{
|
||||
if (vpnConfigKey.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
QString normalizedKey = vpnConfigKey;
|
||||
normalizedKey.replace(QStringLiteral("vpn://"), QString());
|
||||
|
||||
for (int i = 0; i < m_serversRepository->serversCount(); ++i) {
|
||||
const auto apiV2 = m_serversRepository->apiV2Config(m_serversRepository->serverIdAt(i));
|
||||
QString existingVpnKey = apiV2.has_value() ? apiV2->vpnKey() : QString();
|
||||
existingVpnKey.replace(QStringLiteral("vpn://"), QString());
|
||||
if (!existingVpnKey.isEmpty() && existingVpnKey == normalizedKey) {
|
||||
if (duplicateServerIndex) {
|
||||
*duplicateServerIndex = i;
|
||||
}
|
||||
return ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray configString =
|
||||
QByteArray::fromBase64(normalizedKey.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QByteArray configUncompressed = qUncompress(configString);
|
||||
if (!configUncompressed.isEmpty()) {
|
||||
configString = configUncompressed;
|
||||
}
|
||||
if (configString.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
QJsonObject serverJson = QJsonDocument::fromJson(configString).object();
|
||||
if (serverJson.isEmpty()) {
|
||||
return ErrorCode::ApiConfigEmptyError;
|
||||
}
|
||||
|
||||
if (serverJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject apiConfig = serverJson.value(apiDefs::key::apiConfig).toObject();
|
||||
if (!serviceInfo.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, serviceInfo);
|
||||
}
|
||||
if (!supportedProtocols.isEmpty()) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols, supportedProtocols);
|
||||
}
|
||||
serverJson[apiDefs::key::apiConfig] = apiConfig;
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverJson);
|
||||
if (apiV2ServerConfig.apiConfig.vpnKey.isEmpty()) {
|
||||
QString fullKey = vpnConfigKey.trimmed();
|
||||
if (!fullKey.startsWith(QStringLiteral("vpn://"))) {
|
||||
fullKey = QStringLiteral("vpn://") + fullKey;
|
||||
}
|
||||
apiV2ServerConfig.apiConfig.vpnKey = fullKey;
|
||||
}
|
||||
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
@@ -525,6 +475,7 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
|
||||
if (apiV2->nameOverriddenByUser) {
|
||||
newApiV2->name = apiV2->name;
|
||||
newApiV2->displayName = apiV2->displayName;
|
||||
newApiV2->nameOverriddenByUser = true;
|
||||
}
|
||||
|
||||
@@ -999,7 +950,7 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload, nullptr, gatewayController);
|
||||
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
|
||||
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
|
||||
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
|
||||
[promise, watcher, gatewayController]() {
|
||||
@@ -1020,3 +971,74 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
|
||||
const QString &serviceType,
|
||||
const QString &serviceProtocol,
|
||||
const ProtocolData &protocolData,
|
||||
const QString &captchaId,
|
||||
const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
userCountryCode,
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject()};
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
apiPayload["captcha_id"] = captchaId;
|
||||
QString normalizedSolution;
|
||||
normalizedSolution.reserve(captchaSolution.size());
|
||||
for (const QChar &ch : captchaSolution) {
|
||||
const ushort u = ch.unicode();
|
||||
if (u >= '0' && u <= '9') {
|
||||
normalizedSolution += ch;
|
||||
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||
}
|
||||
}
|
||||
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (retryCaptchaOut
|
||||
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
const QJsonObject jsonObj = jsonDoc.object();
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||
retryCaptchaOut->isRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QJsonObject serverConfigJson;
|
||||
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
|
||||
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef SUBSCRIPTIONCONTROLLER_H
|
||||
#define SUBSCRIPTIONCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QByteArray>
|
||||
#include <QFuture>
|
||||
@@ -43,6 +42,13 @@ public:
|
||||
QJsonObject toJsonObject() const;
|
||||
};
|
||||
|
||||
struct CaptchaInfo {
|
||||
QString captchaId;
|
||||
QString captchaImageBase64;
|
||||
QString hint;
|
||||
bool isRequired = false;
|
||||
};
|
||||
|
||||
explicit SubscriptionController(SecureServersRepository* serversRepository,
|
||||
SecureAppSettingsRepository* appSettingsRepository);
|
||||
|
||||
@@ -50,13 +56,11 @@ public:
|
||||
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
|
||||
|
||||
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData);
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo);
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode importServerFromQrPairingResponse(const QString &vpnConfigKey, const QJsonObject &serviceInfo,
|
||||
const QJsonArray &supportedProtocols, int *duplicateServerIndex = nullptr);
|
||||
|
||||
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
@@ -102,6 +106,11 @@ public:
|
||||
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/utilities.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "version.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -67,13 +65,15 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
bool isApiConfig = false;
|
||||
|
||||
const auto kind = m_serversRepository->serverKind(serverId);
|
||||
const QString primaryDns = m_appSettingsRepository->primaryDns();
|
||||
const QString secondaryDns = m_appSettingsRepository->secondaryDns();
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -83,7 +83,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -93,7 +93,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
break;
|
||||
@@ -105,7 +105,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!cfg.has_value()) return ErrorCode::InternalError;
|
||||
container = cfg->defaultContainer;
|
||||
containerConfigModel = cfg->containerConfig(container);
|
||||
dns = { cfg->dns1, cfg->dns2 };
|
||||
dns = cfg->getDnsPair(primaryDns, secondaryDns);
|
||||
hostName = cfg->hostName;
|
||||
description = cfg->description;
|
||||
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
|
||||
@@ -123,16 +123,6 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
|
||||
if (!isContainerSupported(container)) {
|
||||
return ErrorCode::NotSupportedOnThisPlatform;
|
||||
}
|
||||
if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) {
|
||||
if (m_appSettingsRepository->useAmneziaDns()) {
|
||||
dns.first = protocols::dns::amneziaDnsIp;
|
||||
} else {
|
||||
dns.first = m_appSettingsRepository->primaryDns();
|
||||
}
|
||||
}
|
||||
if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) {
|
||||
dns.second = m_appSettingsRepository->secondaryDns();
|
||||
}
|
||||
|
||||
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
|
||||
containerConfigModel, container);
|
||||
|
||||
@@ -153,7 +153,6 @@ void CoreController::initCoreControllers()
|
||||
m_allowedDnsController = new AllowedDnsController(m_appSettingsRepository);
|
||||
m_servicesCatalogController = new ServicesCatalogController(m_appSettingsRepository);
|
||||
m_subscriptionController = new SubscriptionController(m_serversRepository, m_appSettingsRepository);
|
||||
m_pairingController = new PairingController(m_appSettingsRepository);
|
||||
m_newsController = new NewsController(m_appSettingsRepository, m_serversRepository);
|
||||
m_updateController = new UpdateController(m_appSettingsRepository, this);
|
||||
|
||||
@@ -179,7 +178,8 @@ void CoreController::initControllers()
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel, this);
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
m_importController = new ImportUiController(m_importCoreController, this);
|
||||
@@ -213,20 +213,18 @@ void CoreController::initControllers()
|
||||
setQmlContextProperty("SystemController", m_systemController);
|
||||
|
||||
m_networkReachabilityController = new NetworkReachabilityController(this);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
m_engine->rootContext()->setContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
|
||||
setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
|
||||
|
||||
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
|
||||
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
|
||||
|
||||
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
|
||||
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel,
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
|
||||
m_apiCountryModel, m_apiDevicesModel, m_settingsController,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
|
||||
|
||||
m_pairingUiController = new PairingUiController(m_pairingController, m_serversController, m_subscriptionController, m_appSettingsRepository, this);
|
||||
setQmlContextProperty("PairingUiController", m_pairingUiController);
|
||||
|
||||
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
|
||||
setQmlContextProperty("ApiNewsController", m_apiNewsUiController);
|
||||
|
||||
@@ -346,9 +344,6 @@ void CoreController::openConnectionByIndex(int serverIndex)
|
||||
if (serverId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (m_serversModel) {
|
||||
m_serversModel->setProcessedServerIndex(serverIndex);
|
||||
}
|
||||
if (m_serversController) {
|
||||
m_serversController->setDefaultServer(serverId);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#endif
|
||||
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "core/controllers/api/pairingController.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/controllers/appSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/allowedDnsUiController.h"
|
||||
@@ -84,33 +82,11 @@
|
||||
#endif
|
||||
|
||||
class CoreSignalHandlers;
|
||||
class TestMultipleImports;
|
||||
class TestAdminSelfHostedExport;
|
||||
class TestServerEdit;
|
||||
class TestDefaultServerChange;
|
||||
class TestServerEdgeCases;
|
||||
class TestSignalOrder;
|
||||
class TestServersModelSync;
|
||||
class TestComplexOperations;
|
||||
class TestSettingsSignals;
|
||||
class TestUiServersModelAndController;
|
||||
class TestSelfHostedServerSetup;
|
||||
|
||||
class CoreController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
friend class CoreSignalHandlers;
|
||||
friend class TestMultipleImports;
|
||||
friend class TestAdminSelfHostedExport;
|
||||
friend class TestServerEdit;
|
||||
friend class TestDefaultServerChange;
|
||||
friend class TestServerEdgeCases;
|
||||
friend class TestSignalOrder;
|
||||
friend class TestServersModelSync;
|
||||
friend class TestComplexOperations;
|
||||
friend class TestSettingsSignals;
|
||||
friend class TestUiServersModelAndController;
|
||||
friend class TestSelfHostedServerSetup;
|
||||
|
||||
public:
|
||||
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
|
||||
@@ -127,6 +103,36 @@ signals:
|
||||
void translationsUpdated();
|
||||
void websiteUrlChanged(const QString &newUrl);
|
||||
|
||||
protected:
|
||||
SecureServersRepository* serversRepositoryProtected() const { return m_serversRepository; }
|
||||
SecureAppSettingsRepository* appSettingsRepositoryProtected() const { return m_appSettingsRepository; }
|
||||
ServersModel* serversModelProtected() const { return m_serversModel; }
|
||||
ContainersModel* containersModelProtected() const { return m_containersModel; }
|
||||
ApiServicesModel* apiServicesModelProtected() const { return m_apiServicesModel; }
|
||||
NewsModel* newsModelProtected() const { return m_newsModel; }
|
||||
AllowedDnsModel* allowedDnsModelProtected() const { return m_allowedDnsModel; }
|
||||
AppSplitTunnelingModel* appSplitTunnelingModelProtected() const { return m_appSplitTunnelingModel; }
|
||||
IpSplitTunnelingModel* ipSplitTunnelingModelProtected() const { return m_ipSplitTunnelingModel; }
|
||||
LanguageModel* languageModelProtected() const { return m_languageModel; }
|
||||
|
||||
InstallUiController* installUiControllerProtected() const { return m_installUiController; }
|
||||
ImportController* importCoreControllerProtected() const { return m_importCoreController; }
|
||||
ExportController* exportControllerProtected() const { return m_exportController; }
|
||||
InstallController* installControllerProtected() const { return m_installController; }
|
||||
ServersController* serversControllerProtected() const { return m_serversController; }
|
||||
SettingsUiController* settingsUiControllerProtected() const { return m_settingsUiController; }
|
||||
SettingsController* settingsControllerProtected() const { return m_settingsController; }
|
||||
AllowedDnsUiController* allowedDnsUiControllerProtected() const { return m_allowedDnsUiController; }
|
||||
AllowedDnsController* allowedDnsControllerProtected() const { return m_allowedDnsController; }
|
||||
LanguageUiController* languageUiControllerProtected() const { return m_languageUiController; }
|
||||
IpSplitTunnelingController* ipSplitTunnelingControllerProtected() const { return m_ipSplitTunnelingController; }
|
||||
IpSplitTunnelingUiController* ipSplitTunnelingUiControllerProtected() const { return m_ipSplitTunnelingUiController; }
|
||||
AppSplitTunnelingController* appSplitTunnelingControllerProtected() const { return m_appSplitTunnelingController; }
|
||||
AppSplitTunnelingUiController* appSplitTunnelingUiControllerProtected() const { return m_appSplitTunnelingUiController; }
|
||||
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
|
||||
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
|
||||
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
|
||||
|
||||
private:
|
||||
void initRepositories();
|
||||
void initCoreControllers();
|
||||
@@ -170,7 +176,6 @@ private:
|
||||
UpdateUiController* m_updateUiController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
PairingUiController* m_pairingUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
|
||||
ServicesCatalogUiController* m_servicesCatalogUiController;
|
||||
@@ -182,7 +187,6 @@ private:
|
||||
AllowedDnsController* m_allowedDnsController;
|
||||
ServicesCatalogController* m_servicesCatalogController;
|
||||
SubscriptionController* m_subscriptionController;
|
||||
PairingController* m_pairingController;
|
||||
NewsController* m_newsController;
|
||||
UpdateController* m_updateController;
|
||||
InstallController* m_installController;
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
@@ -99,9 +98,6 @@ void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
|
||||
connect(m_coreController->m_pairingUiController, &PairingUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
|
||||
connect(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred, m_coreController->m_pageController,
|
||||
qOverload<ErrorCode>(&PageController::showErrorMessage));
|
||||
}
|
||||
@@ -129,9 +125,9 @@ void CoreSignalHandlers::initInstallControllerHandler()
|
||||
{
|
||||
connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy);
|
||||
connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation);
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
|
||||
m_coreController->m_installUiController, [this](int serverIndex) {
|
||||
if (serverIndex >= 0) {
|
||||
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged,
|
||||
m_coreController->m_installUiController, [this](const QString &serverId) {
|
||||
if (!serverId.isEmpty()) {
|
||||
m_coreController->m_installUiController->clearProcessedServerCredentials();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPromise>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
@@ -22,28 +21,17 @@
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/utilities.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
void execNetworkWaitLoop(QEventLoop &wait)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
wait.exec();
|
||||
#else
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
|
||||
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
|
||||
|
||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||
|
||||
@@ -51,29 +39,18 @@ namespace
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
constexpr int httpStatusCodePaymentRequired = 402;
|
||||
constexpr int httpStatusCodeRequestTimeout = 408;
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||
|
||||
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||
|
||||
QString normalizedGatewayBase(const QString &endpoint)
|
||||
{
|
||||
QString e = endpoint.trimmed();
|
||||
if (e.isEmpty()) {
|
||||
return e;
|
||||
}
|
||||
if (!e.endsWith(QLatin1Char('/'))) {
|
||||
e.append(QLatin1Char('/'));
|
||||
}
|
||||
return e;
|
||||
}
|
||||
} // namespace
|
||||
}
|
||||
|
||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(normalizedGatewayBase(gatewayEndpoint)),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isDevEnvironment(isDevEnvironment),
|
||||
m_requestTimeoutMsecs(requestTimeoutMsecs),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
|
||||
@@ -161,8 +138,6 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
{
|
||||
Q_UNUSED(replyError);
|
||||
|
||||
DecryptionResult result;
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
@@ -179,29 +154,6 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
return result;
|
||||
}
|
||||
|
||||
GatewayController::DecryptionResult GatewayController::resolveResponseBody(const QByteArray &responseBody,
|
||||
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
{
|
||||
DecryptionResult result = tryDecryptResponseBody(responseBody, replyError, key, iv, salt);
|
||||
if (result.isDecryptionSuccessful || !m_isDevEnvironment) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const QByteArray trimmed = responseBody.trimmed();
|
||||
if (trimmed.isEmpty() || trimmed.front() != '{') {
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(trimmed, &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
result.decryptedBody = trimmed;
|
||||
result.isDecryptionSuccessful = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
@@ -216,7 +168,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
execNetworkWaitLoop(wait);
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
@@ -225,18 +177,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (encRequestData.isPlaintextLocalGateway) {
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
responseBody = encryptedResponseBody;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
auto decryptionResult =
|
||||
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||
@@ -252,7 +194,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
decryptionResult =
|
||||
resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
if (!sslErrors.isEmpty()
|
||||
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
@@ -267,8 +209,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
const auto errorCode =
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -278,19 +221,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
responseBody = decryptionResult.decryptedBody;
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut,
|
||||
const QSharedPointer<GatewayController> &keepAlive)
|
||||
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||
promise->start();
|
||||
|
||||
const QSharedPointer<GatewayController> life = keepAlive;
|
||||
|
||||
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||
@@ -299,22 +237,12 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
}
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||
if (activeReplyOut) {
|
||||
*activeReplyOut = reply;
|
||||
}
|
||||
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, life]() mutable {
|
||||
if (!life) {
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
connect(reply, &QNetworkReply::finished, this, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
@@ -322,20 +250,8 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (encRequestData.isPlaintextLocalGateway) {
|
||||
const auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
encryptedResponseBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, QByteArray()));
|
||||
} else {
|
||||
promise->addResult(qMakePair(ErrorCode::NoError, encryptedResponseBody));
|
||||
}
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
auto decryptionResult =
|
||||
ctl->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
@@ -343,7 +259,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||
decryptionResult.decryptedBody);
|
||||
if (errorCode) {
|
||||
promise->addResult(qMakePair(errorCode, QByteArray()));
|
||||
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
|
||||
promise->finish();
|
||||
return;
|
||||
}
|
||||
@@ -360,13 +276,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
promise->finish();
|
||||
};
|
||||
|
||||
if (sslErrors->isEmpty() && ctl->shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
|
||||
QStringList primaryBaseUrls;
|
||||
QStringList fallbackBaseUrls;
|
||||
if (ctl->m_isDevEnvironment) {
|
||||
if (m_isDevEnvironment) {
|
||||
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
} else {
|
||||
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||
@@ -393,27 +309,19 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, 0,
|
||||
[life, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
life->getProxyUrlAsync(life, proxyUrls, 0,
|
||||
[life, encRequestData, endpoint, processResponse](
|
||||
const QString &proxyUrl) {
|
||||
life->bypassProxyAsync(
|
||||
life, endpoint, proxyUrl, encRequestData,
|
||||
[processResponse](const QByteArray &decryptedBody,
|
||||
bool isDecryptionSuccessful,
|
||||
const QList<QSslError> &sslErrors,
|
||||
QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString,
|
||||
int httpStatusCode) {
|
||||
GatewayController::DecryptionResult result;
|
||||
result.decryptedBody = decryptedBody;
|
||||
result.isDecryptionSuccessful = isDecryptionSuccessful;
|
||||
processResponse(result, sslErrors, replyError,
|
||||
replyErrorString, httpStatusCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||
const QString &replyErrorString, int httpStatusCode) {
|
||||
GatewayController::DecryptionResult result;
|
||||
result.decryptedBody = decryptedBody;
|
||||
result.isDecryptionSuccessful = isDecryptionSuccessful;
|
||||
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||
@@ -476,7 +384,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
execNetworkWaitLoop(wait);
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
@@ -529,10 +437,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||
bool isDecryptionSuccessful)
|
||||
{
|
||||
if (m_isDevEnvironment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray &responseBody = decryptedResponseBody;
|
||||
|
||||
int apiHttpStatus = -1;
|
||||
@@ -558,15 +462,19 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
|
||||
return false;
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|
||||
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
@@ -613,7 +521,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
execNetworkWaitLoop(wait);
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
@@ -635,7 +543,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
execNetworkWaitLoop(wait);
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
reply->deleteLater();
|
||||
@@ -664,14 +572,9 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls,
|
||||
const int currentProxyStorageIndex, const std::function<void(const QStringList &)> &onComplete)
|
||||
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete)
|
||||
{
|
||||
if (!life) {
|
||||
onComplete({});
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||
onComplete({});
|
||||
return;
|
||||
@@ -684,23 +587,17 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (!life) {
|
||||
onComplete({});
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray encrypted = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
QByteArray key = ctl->m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!ctl->m_isDevEnvironment) {
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
@@ -717,21 +614,15 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error decrypting payload";
|
||||
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete({});
|
||||
}
|
||||
});
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||
QStringList endpoints;
|
||||
for (const QJsonValue &endpoint : endpointsArray) {
|
||||
for (const QJsonValue &endpoint : endpointsArray)
|
||||
endpoints.push_back(endpoint.toString());
|
||||
}
|
||||
|
||||
QStringList shuffled = endpoints;
|
||||
std::random_device randomDevice;
|
||||
@@ -746,26 +637,16 @@ void GatewayController::getProxyUrlsAsync(const QSharedPointer<GatewayController
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
reply->deleteLater();
|
||||
QTimer::singleShot(0, ctl, [life, proxyStorageUrls, currentProxyStorageIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlsAsync(life, proxyStorageUrls, currentProxyStorageIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete({});
|
||||
}
|
||||
});
|
||||
QMetaObject::invokeMethod(
|
||||
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls,
|
||||
const int currentProxyIndex, const std::function<void(const QString &)> &onComplete)
|
||||
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||
std::function<void(const QString &)> onComplete)
|
||||
{
|
||||
if (!life) {
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProxyIndex >= proxyUrls.size()) {
|
||||
onComplete(QString());
|
||||
onComplete("");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -776,16 +657,13 @@ void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController>
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
|
||||
// *(state->sslErrors) = e;
|
||||
// });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (!life) {
|
||||
onComplete(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
GatewayController *const ctl = life.data();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
m_proxyUrl = proxyUrls[currentProxyIndex];
|
||||
onComplete(m_proxyUrl);
|
||||
@@ -793,28 +671,15 @@ void GatewayController::getProxyUrlAsync(const QSharedPointer<GatewayController>
|
||||
}
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
QTimer::singleShot(0, ctl, [life, proxyUrls, currentProxyIndex, onComplete]() {
|
||||
if (life) {
|
||||
life->getProxyUrlAsync(life, proxyUrls, currentProxyIndex + 1, onComplete);
|
||||
} else {
|
||||
onComplete(QString());
|
||||
}
|
||||
});
|
||||
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxyAsync(
|
||||
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl,
|
||||
const EncryptedRequestData &encRequestData,
|
||||
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)>
|
||||
&onComplete)
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
|
||||
{
|
||||
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||
if (!life) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proxyUrl.isEmpty()) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
|
||||
return;
|
||||
@@ -825,9 +690,9 @@ void GatewayController::bypassProxyAsync(
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||
|
||||
connect(reply, &QNetworkReply::sslErrors, reply, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, reply, [life, sslErrors, onComplete, encRequestData, reply]() {
|
||||
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
@@ -835,13 +700,8 @@ void GatewayController::bypassProxyAsync(
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!life) {
|
||||
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, QStringLiteral("gateway gone"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
auto decryptionResult = life->resolveResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv,
|
||||
encRequestData.salt);
|
||||
auto decryptionResult =
|
||||
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||
|
||||
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||
httpStatusCode);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
@@ -27,9 +25,7 @@ public:
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject &apiPayload,
|
||||
QNetworkReply **activeReplyOut = nullptr,
|
||||
const QSharedPointer<GatewayController> &keepAlive = {});
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
|
||||
private:
|
||||
struct EncryptedRequestData
|
||||
@@ -40,7 +36,6 @@ private:
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode;
|
||||
bool isPlaintextLocalGateway = false;
|
||||
};
|
||||
|
||||
struct DecryptionResult
|
||||
@@ -52,8 +47,6 @@ private:
|
||||
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
|
||||
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
|
||||
DecryptionResult resolveResponseBody(const QByteArray &responseBody, QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt);
|
||||
|
||||
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
|
||||
@@ -61,13 +54,12 @@ private:
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
void getProxyUrlsAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyStorageUrls, int currentProxyStorageIndex,
|
||||
const std::function<void(const QStringList &)> &onComplete);
|
||||
void getProxyUrlAsync(const QSharedPointer<GatewayController> &life, const QStringList &proxyUrls, int currentProxyIndex,
|
||||
const std::function<void(const QString &)> &onComplete);
|
||||
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||
std::function<void(const QStringList &)> onComplete);
|
||||
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||
void bypassProxyAsync(
|
||||
const QSharedPointer<GatewayController> &life, const QString &endpoint, const QString &proxyUrl, const EncryptedRequestData &encRequestData,
|
||||
const std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> &onComplete);
|
||||
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
QString m_gatewayEndpoint;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "core/installers/sftpInstaller.h"
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/configurators/xrayConfigurator.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
@@ -119,9 +120,14 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
return e;
|
||||
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.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
removeContainerVars));
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
@@ -181,6 +187,16 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig);
|
||||
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;
|
||||
if (reinstallRequired) {
|
||||
errorCode = setupContainer(credentials, container, newConfig, true);
|
||||
@@ -191,6 +207,21 @@ ErrorCode InstallController::updateContainer(const QString &serverId, DockerCont
|
||||
}
|
||||
}
|
||||
|
||||
const bool skipXrayInboundSync =
|
||||
newConfig.getXrayProtocolConfig() && newConfig.getXrayProtocolConfig()->serverConfig.isThirdPartyConfig;
|
||||
|
||||
if (errorCode == ErrorCode::NoError && xrayServerSettingsChanged && !skipXrayInboundSync) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
XrayConfigurator xrayConfigurator(&sshSession);
|
||||
qDebug() << "InstallController::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 (container == DockerContainer::MtProxy) {
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
@@ -216,9 +247,9 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
|
||||
return;
|
||||
}
|
||||
|
||||
adminConfig->clearCachedClientProfile(container);
|
||||
const ContainerConfig containerConfigModel = adminConfig->containerConfig(container);
|
||||
|
||||
adminConfig->clearCachedClientProfile(container);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
emit clientRevocationRequested(serverId, containerConfigModel, container);
|
||||
@@ -226,38 +257,75 @@ void InstallController::clearCachedProfile(const QString &serverId, DockerContai
|
||||
|
||||
ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
|
||||
{
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
const auto kind = m_serversRepository->serverKind(serverId);
|
||||
|
||||
DockerContainer container = DockerContainer::None;
|
||||
ContainerConfig containerConfig;
|
||||
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
const auto cfg = m_serversRepository->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
container = cfg->defaultContainer;
|
||||
containerConfig = cfg->containerConfig(container);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
DockerContainer container = adminConfig->defaultContainer;
|
||||
|
||||
if (container == DockerContainer::None) {
|
||||
return ErrorCode::NoInstalledContainersError;
|
||||
}
|
||||
|
||||
ContainerConfig containerConfig = adminConfig->containerConfig(container);
|
||||
if (containerConfig.protocolConfig.hasClientConfig()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
if (kind != serverConfigUtils::ConfigType::SelfHostedAdmin) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!adminConfig.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
if (!credentials.isValid()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
SshSession sshSession;
|
||||
|
||||
auto isProtocolConfigExists = [](const ContainerConfig &cfg) {
|
||||
return cfg.protocolConfig.hasClientConfig();
|
||||
};
|
||||
|
||||
if (!isProtocolConfigExists(containerConfig)) {
|
||||
QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
|
||||
ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, containerConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
const QString clientName = QString("Admin [%1]").arg(QSysInfo::prettyProductName());
|
||||
const ErrorCode errorCode = processContainerForAdmin(container, containerConfig, credentials, sshSession, serverId, clientName);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
adminConfig->updateContainerConfig(container, containerConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -601,12 +669,19 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
const auto* oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto* newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
const auto *oldXrayConfig = oldConfig.getXrayProtocolConfig();
|
||||
const auto *newXrayConfig = newConfig.getXrayProtocolConfig();
|
||||
|
||||
if (oldXrayConfig && newXrayConfig) {
|
||||
if (oldXrayConfig->serverConfig.port != newXrayConfig->serverConfig.port)
|
||||
const QString oldPort = oldXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: oldXrayConfig->serverConfig.port;
|
||||
const QString newPort = newXrayConfig->serverConfig.port.isEmpty()
|
||||
? QString(protocols::xray::defaultPort)
|
||||
: newXrayConfig->serverConfig.port;
|
||||
if (oldPort != newPort) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,10 +980,12 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
SshSession sshSession(this);
|
||||
amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
removeContainerVars.append({ { "$REMOVE_CONTAINER_DATA", QStringLiteral("1") } });
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
credentials,
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container),
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString())));
|
||||
sshSession.replaceVars(amnezia::scriptData(SharedScriptType::remove_container), removeContainerVars));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QMap<DockerContainer, ContainerConfig> containers = adminConfig->containers;
|
||||
|
||||
@@ -44,6 +44,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->description = name;
|
||||
cfg->displayName = name;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
}
|
||||
@@ -51,6 +52,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->description = name;
|
||||
cfg->displayName = name;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
}
|
||||
@@ -58,6 +60,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->description = name;
|
||||
cfg->displayName = name;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
}
|
||||
@@ -67,6 +70,7 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
|
||||
auto cfg = m_serversRepository->apiV2Config(serverId);
|
||||
if (!cfg.has_value()) return false;
|
||||
cfg->name = name;
|
||||
cfg->displayName = name;
|
||||
cfg->nameOverriddenByUser = true;
|
||||
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
|
||||
return true;
|
||||
|
||||
@@ -217,6 +217,11 @@ void SettingsController::toggleAutoStart(bool enable)
|
||||
|
||||
bool SettingsController::isStartMinimizedEnabled() const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (!isAutoStartEnabled()) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return m_appSettingsRepository->isStartMinimized();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ namespace
|
||||
Logger logger("UpdateController");
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.exe");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg");
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run");
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
|
||||
#endif
|
||||
}
|
||||
@@ -57,10 +57,6 @@ void UpdateController::checkForUpdates()
|
||||
if (m_updateCheckRunning || !m_appSettingsRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
return;
|
||||
}
|
||||
m_updateCheckRunning = true;
|
||||
|
||||
fetchGatewayUrl();
|
||||
@@ -97,11 +93,6 @@ void UpdateController::doGetAsync(const QString &endpoint, std::function<void(bo
|
||||
|
||||
void UpdateController::fetchGatewayUrl()
|
||||
{
|
||||
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
7000,
|
||||
@@ -114,19 +105,11 @@ void UpdateController::fetchGatewayUrl()
|
||||
|
||||
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
|
||||
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
|
||||
if (!m_appSettingsRepository || m_appSettingsRepository->isDevGatewayEnv()) {
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload, nullptr, gatewayController)
|
||||
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
|
||||
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
|
||||
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
|
||||
auto [err, gatewayResponse] = result;
|
||||
if (err != ErrorCode::NoError) {
|
||||
if (err == ErrorCode::ApiNotFoundError) {
|
||||
logger.debug() << "Update check: updater_endpoint not found on gateway";
|
||||
} else {
|
||||
logger.error() << errorString(err);
|
||||
}
|
||||
logger.error() << errorString(err);
|
||||
finishUpdateCheck();
|
||||
return;
|
||||
}
|
||||
@@ -201,7 +184,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
|
||||
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
|
||||
.arg(operation, reply->errorString(), QString::number(error));
|
||||
});
|
||||
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
|
||||
QStringList errorStrings;
|
||||
for (const QSslError &err : errors) {
|
||||
@@ -213,21 +196,13 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
|
||||
|
||||
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "Error message:" << err;
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
|
||||
}
|
||||
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
|
||||
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
}
|
||||
|
||||
QString UpdateController::composeDownloadUrl() const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
|
||||
return m_baseUrl + "/" + fileName;
|
||||
#else
|
||||
@@ -237,7 +212,7 @@ QString UpdateController::composeDownloadUrl() const
|
||||
|
||||
void UpdateController::runInstaller()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
if (m_downloadUrl.isEmpty()) {
|
||||
logger.error() << "Download URL is empty";
|
||||
return;
|
||||
@@ -269,7 +244,7 @@ void UpdateController::runInstaller()
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
runWindowsInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_MACOS)
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
runMacInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
runLinuxInstaller(kInstallerLocalPath);
|
||||
@@ -309,7 +284,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MACOS)
|
||||
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
int UpdateController::runMacInstaller(const QString &installerPath)
|
||||
{
|
||||
// Create temporary directory for extraction
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
#include "socks5Installer.h"
|
||||
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/utilities.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
@@ -33,10 +35,29 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
|
||||
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession* sshSession, ContainerConfig &config)
|
||||
{
|
||||
Q_UNUSED(container);
|
||||
Q_UNUSED(credentials);
|
||||
Q_UNUSED(sshSession);
|
||||
Q_UNUSED(config);
|
||||
if (container != DockerContainer::Socks5Proxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
Socks5ProxyProtocolConfig *socks5Config = config.getSocks5ProxyProtocolConfig();
|
||||
if (!socks5Config) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode readError = ErrorCode::NoError;
|
||||
const QByteArray configRaw = sshSession->getTextFileFromContainer(
|
||||
container, credentials, QString::fromUtf8(protocols::socks5Proxy::proxyConfigPath), readError);
|
||||
if (readError != ErrorCode::NoError || configRaw.trimmed().isEmpty()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
const QString proxyConfig = QString::fromUtf8(configRaw);
|
||||
static const QRegularExpression usernameAndPasswordRegExp(QStringLiteral("users (\\w+):CL:(\\w+)"));
|
||||
const QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
|
||||
if (usernameAndPasswordMatch.hasMatch()) {
|
||||
socks5Config->userName = usernameAndPasswordMatch.captured(1);
|
||||
socks5Config->password = usernameAndPasswordMatch.captured(2);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/models/api/apiConfig.h"
|
||||
#include "core/models/api/authData.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -67,6 +68,20 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
|
||||
return containers.value(container);
|
||||
}
|
||||
|
||||
QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
|
||||
{
|
||||
QString d1 = dns1;
|
||||
QString d2 = dns2;
|
||||
|
||||
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
|
||||
d1 = primaryDns;
|
||||
}
|
||||
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
|
||||
d2 = secondaryDns;
|
||||
}
|
||||
return { d1, d2 };
|
||||
}
|
||||
|
||||
QJsonObject ApiV2ServerConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
@@ -80,9 +95,6 @@ QJsonObject ApiV2ServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
|
||||
obj[configKey::configVersion] = configVersion;
|
||||
|
||||
@@ -134,7 +146,6 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
|
||||
config.name = json.value(configKey::name).toString();
|
||||
config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false);
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.configVersion = json.value(configKey::configVersion).toInt(2);
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -43,6 +44,9 @@ struct ApiV2ServerConfig {
|
||||
bool isExternalPremium() const;
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static ApiV2ServerConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -23,9 +23,7 @@ LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
LegacyApiServerConfig config;
|
||||
|
||||
config.name = json.value(configKey::name).toString();
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
config.crc = json.value(configKey::crc).toInt(0);
|
||||
|
||||
@@ -108,35 +108,114 @@ QJsonObject XrayXhttpConfig::toJson() const
|
||||
return obj;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
XrayXhttpConfig clearedXhttpConfig()
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = QString();
|
||||
c.host = QString();
|
||||
c.path = QString();
|
||||
c.headersTemplate = QString();
|
||||
c.uplinkMethod = QString();
|
||||
c.disableGrpc = false;
|
||||
c.disableSse = false;
|
||||
c.sessionPlacement = QString();
|
||||
c.sessionKey = QString();
|
||||
c.seqPlacement = QString();
|
||||
c.seqKey = QString();
|
||||
c.uplinkDataPlacement = QString();
|
||||
c.uplinkDataKey = QString();
|
||||
c.uplinkChunkSize = QString();
|
||||
c.scMaxBufferedPosts = QString();
|
||||
c.scMaxEachPostBytesMin = QString();
|
||||
c.scMaxEachPostBytesMax = QString();
|
||||
c.scMinPostsIntervalMsMin = QString();
|
||||
c.scMinPostsIntervalMsMax = QString();
|
||||
c.scStreamUpServerSecsMin = QString();
|
||||
c.scStreamUpServerSecsMax = QString();
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayXhttpConfig c;
|
||||
c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode);
|
||||
c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite);
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate);
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod);
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true);
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true);
|
||||
if (json.isEmpty()) {
|
||||
return clearedXhttpConfig();
|
||||
}
|
||||
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement);
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement);
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
XrayXhttpConfig c = clearedXhttpConfig();
|
||||
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0");
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1");
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100");
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100");
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800");
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1");
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100");
|
||||
if (json.contains(configKey::xhttpMode)) {
|
||||
c.mode = json.value(configKey::xhttpMode).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHost)) {
|
||||
c.host = json.value(configKey::xhttpHost).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpPath)) {
|
||||
c.path = json.value(configKey::xhttpPath).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpHeadersTemplate)) {
|
||||
c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkMethod)) {
|
||||
c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableGrpc)) {
|
||||
c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpDisableSse)) {
|
||||
c.disableSse = json.value(configKey::xhttpDisableSse).toBool();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionPlacement)) {
|
||||
c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSessionKey)) {
|
||||
c.sessionKey = json.value(configKey::xhttpSessionKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqPlacement)) {
|
||||
c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpSeqKey)) {
|
||||
c.seqKey = json.value(configKey::xhttpSeqKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataPlacement)) {
|
||||
c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkDataKey)) {
|
||||
c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpUplinkChunkSize)) {
|
||||
c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxBufferedPosts)) {
|
||||
c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMin)) {
|
||||
c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMaxEachPostBytesMax)) {
|
||||
c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMin)) {
|
||||
c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScMinPostsIntervalMsMax)) {
|
||||
c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMin)) {
|
||||
c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString();
|
||||
}
|
||||
if (json.contains(configKey::xhttpScStreamUpServerSecsMax)) {
|
||||
c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString();
|
||||
}
|
||||
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject());
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject());
|
||||
if (json.contains(QLatin1String("xPadding"))) {
|
||||
c.xPadding = XrayXPaddingConfig::fromJson(json.value(QLatin1String("xPadding")).toObject());
|
||||
}
|
||||
if (json.contains(QLatin1String("xmux"))) {
|
||||
c.xmux = XrayXmuxConfig::fromJson(json.value(QLatin1String("xmux")).toObject());
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -156,12 +235,27 @@ QJsonObject XrayMkcpConfig::toJson() const
|
||||
XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayMkcpConfig c;
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool(true);
|
||||
if (json.isEmpty()) {
|
||||
return c;
|
||||
}
|
||||
if (json.contains(configKey::mkcpTti)) {
|
||||
c.tti = json.value(configKey::mkcpTti).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpUplinkCapacity)) {
|
||||
c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpDownlinkCapacity)) {
|
||||
c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpReadBufferSize)) {
|
||||
c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpWriteBufferSize)) {
|
||||
c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString();
|
||||
}
|
||||
if (json.contains(configKey::mkcpCongestion)) {
|
||||
c.congestion = json.value(configKey::mkcpCongestion).toBool();
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -208,8 +302,14 @@ QJsonObject XrayServerConfig::toJson() const
|
||||
if (!transport.isEmpty()) {
|
||||
obj[configKey::xrayTransport] = transport;
|
||||
}
|
||||
obj["xhttp"] = xhttp.toJson();
|
||||
obj["mkcp"] = mkcp.toJson();
|
||||
const QJsonObject xhttpObj = xhttp.toJson();
|
||||
if (!xhttpObj.isEmpty()) {
|
||||
obj[QStringLiteral("xhttp")] = xhttpObj;
|
||||
}
|
||||
const QJsonObject mkcpObj = mkcp.toJson();
|
||||
if (!mkcpObj.isEmpty()) {
|
||||
obj[QStringLiteral("mkcp")] = mkcpObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -225,20 +325,39 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
c.site = json.value(configKey::site).toString();
|
||||
c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
// New: Security
|
||||
c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity);
|
||||
c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow);
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint);
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
if (json.contains(configKey::xraySecurity)) {
|
||||
c.security = json.value(configKey::xraySecurity).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFlow)) {
|
||||
c.flow = json.value(configKey::xrayFlow).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayFingerprint)) {
|
||||
c.fingerprint = json.value(configKey::xrayFingerprint).toString();
|
||||
if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
}
|
||||
if (json.contains(configKey::xraySni)) {
|
||||
c.sni = json.value(configKey::xraySni).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayAlpn)) {
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString();
|
||||
}
|
||||
if (json.contains(configKey::xrayTransport)) {
|
||||
c.transport = json.value(configKey::xrayTransport).toString();
|
||||
}
|
||||
if (json.contains(QLatin1String("xhttp"))) {
|
||||
const QJsonObject xhttpJson = json.value(QLatin1String("xhttp")).toObject();
|
||||
if (!xhttpJson.isEmpty()) {
|
||||
c.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
}
|
||||
if (json.contains(QLatin1String("mkcp"))) {
|
||||
const QJsonObject mkcpJson = json.value(QLatin1String("mkcp")).toObject();
|
||||
if (!mkcpJson.isEmpty()) {
|
||||
c.mkcp = XrayMkcpConfig::fromJson(mkcpJson);
|
||||
}
|
||||
}
|
||||
c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni);
|
||||
c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn);
|
||||
|
||||
// New: Transport
|
||||
c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport);
|
||||
c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject());
|
||||
c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject());
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -251,7 +370,10 @@ bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) con
|
||||
&& flow == other.flow
|
||||
&& transport == other.transport
|
||||
&& fingerprint == other.fingerprint
|
||||
&& sni == other.sni;
|
||||
&& sni == other.sni
|
||||
&& alpn == other.alpn
|
||||
&& xhttp.toJson() == other.xhttp.toJson()
|
||||
&& mkcp.toJson() == other.mkcp.toJson();
|
||||
}
|
||||
|
||||
QJsonObject XrayClientConfig::toJson() const
|
||||
@@ -351,9 +473,154 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
}
|
||||
}
|
||||
|
||||
c.needsClientHydration =
|
||||
c.hasClientConfig()
|
||||
&& (!json.contains(configKey::xrayTransport) || c.serverConfig.isThirdPartyConfig);
|
||||
if (c.needsClientHydration) {
|
||||
c.hydrateServerConfigFromClientNative();
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hydrateServerConfigFromClientNative()
|
||||
{
|
||||
if (!clientConfig.has_value() || clientConfig->nativeConfig.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
const QJsonArray outbounds = root.value(protocols::xray::outbounds).toArray();
|
||||
if (outbounds.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject outbound = outbounds[0].toObject();
|
||||
const QJsonObject streamSettings = outbound.value(protocols::xray::streamSettings).toObject();
|
||||
if (streamSettings.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XrayServerConfig &srv = serverConfig;
|
||||
|
||||
const QJsonObject settings = outbound.value(protocols::xray::settings).toObject();
|
||||
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
|
||||
if (!vnext.isEmpty()) {
|
||||
const QJsonObject vnextEntry = vnext[0].toObject();
|
||||
if (vnextEntry.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(vnextEntry.value(protocols::xray::port).toInt());
|
||||
}
|
||||
const QJsonArray users = vnextEntry.value(protocols::xray::users).toArray();
|
||||
if (!users.isEmpty()) {
|
||||
srv.flow = users[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
const QString networkVal = streamSettings.value(protocols::xray::network).toString(QStringLiteral("tcp"));
|
||||
if (networkVal == QLatin1String("xhttp")) {
|
||||
srv.transport = QStringLiteral("xhttp");
|
||||
} else if (networkVal == QLatin1String("kcp")) {
|
||||
srv.transport = QStringLiteral("mkcp");
|
||||
} else {
|
||||
srv.transport = QStringLiteral("raw");
|
||||
}
|
||||
|
||||
if (streamSettings.contains(protocols::xray::security)) {
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString();
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
const QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
srv.sni = rs.value(protocols::xray::serverName).toString();
|
||||
srv.site = srv.sni.isEmpty() ? srv.site : srv.sni;
|
||||
const QString fp = rs.value(protocols::xray::fingerprint).toString();
|
||||
if (!fp.isEmpty()) {
|
||||
srv.fingerprint = fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)
|
||||
? QString::fromLatin1(protocols::xray::defaultFingerprint)
|
||||
: fp;
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("tls")) {
|
||||
const QJsonObject tls = streamSettings.value(QStringLiteral("tlsSettings")).toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
const QString fp = tls.value(protocols::xray::fingerprint).toString();
|
||||
if (!fp.isEmpty()) {
|
||||
srv.fingerprint = fp;
|
||||
}
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : tls.value(QStringLiteral("alpn")).toArray()) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
if (!alpnList.isEmpty()) {
|
||||
srv.alpn = alpnList.join(QLatin1Char(','));
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
const QJsonObject xhttpObj = streamSettings.value(QStringLiteral("xhttpSettings")).toObject();
|
||||
QJsonObject xhttpJson;
|
||||
const QString mode = xhttpObj.value(QStringLiteral("mode")).toString();
|
||||
if (!mode.isEmpty()) {
|
||||
if (mode == QLatin1String("auto")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Auto");
|
||||
} else if (mode == QLatin1String("packet-up")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Packet-up");
|
||||
} else if (mode == QLatin1String("stream-up")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-up");
|
||||
} else if (mode == QLatin1String("stream-one")) {
|
||||
xhttpJson[configKey::xhttpMode] = QStringLiteral("Stream-one");
|
||||
} else {
|
||||
xhttpJson[configKey::xhttpMode] = mode;
|
||||
}
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("host"))) {
|
||||
xhttpJson[configKey::xhttpHost] = xhttpObj.value(QStringLiteral("host")).toString();
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("path"))) {
|
||||
xhttpJson[configKey::xhttpPath] = xhttpObj.value(QStringLiteral("path")).toString();
|
||||
}
|
||||
if (xhttpObj.contains(QStringLiteral("uplinkHTTPMethod"))) {
|
||||
xhttpJson[configKey::xhttpUplinkMethod] = xhttpObj.value(QStringLiteral("uplinkHTTPMethod")).toString();
|
||||
}
|
||||
xhttpJson[configKey::xhttpDisableGrpc] = xhttpObj.value(QStringLiteral("noGRPCHeader")).toBool(true);
|
||||
xhttpJson[configKey::xhttpDisableSse] = xhttpObj.value(QStringLiteral("noSSEHeader")).toBool(true);
|
||||
srv.xhttp = XrayXhttpConfig::fromJson(xhttpJson);
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("mkcp")) {
|
||||
const QJsonObject kcpObj = streamSettings.value(QStringLiteral("kcpSettings")).toObject();
|
||||
XrayMkcpConfig mk;
|
||||
if (kcpObj.contains(QStringLiteral("tti"))) {
|
||||
mk.tti = QString::number(kcpObj.value(QStringLiteral("tti")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("uplinkCapacity"))) {
|
||||
mk.uplinkCapacity = QString::number(kcpObj.value(QStringLiteral("uplinkCapacity")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("downlinkCapacity"))) {
|
||||
mk.downlinkCapacity = QString::number(kcpObj.value(QStringLiteral("downlinkCapacity")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("readBufferSize"))) {
|
||||
mk.readBufferSize = QString::number(kcpObj.value(QStringLiteral("readBufferSize")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("writeBufferSize"))) {
|
||||
mk.writeBufferSize = QString::number(kcpObj.value(QStringLiteral("writeBufferSize")).toInt());
|
||||
}
|
||||
if (kcpObj.contains(QStringLiteral("congestion"))) {
|
||||
mk.congestion = kcpObj.value(QStringLiteral("congestion")).toBool(true);
|
||||
}
|
||||
srv.mkcp = mk;
|
||||
}
|
||||
|
||||
needsClientHydration = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayProtocolConfig::hasClientConfig() const
|
||||
{
|
||||
return clientConfig.has_value();
|
||||
|
||||
@@ -75,6 +75,7 @@ struct XrayXhttpConfig {
|
||||
XrayXmuxConfig xmux;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
|
||||
static XrayXhttpConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
@@ -99,15 +100,13 @@ struct XrayServerConfig {
|
||||
QString site;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
// New: Security
|
||||
QString security = protocols::xray::defaultSecurity;
|
||||
QString flow = protocols::xray::defaultFlow;
|
||||
QString fingerprint = protocols::xray::defaultFingerprint;
|
||||
QString sni = protocols::xray::defaultSni;
|
||||
QString alpn = protocols::xray::defaultAlpn;
|
||||
QString security;
|
||||
QString flow;
|
||||
QString fingerprint;
|
||||
QString sni;
|
||||
QString alpn;
|
||||
|
||||
// New: Transport
|
||||
QString transport = protocols::xray::defaultTransport;
|
||||
QString transport;
|
||||
XrayXhttpConfig xhttp;
|
||||
XrayMkcpConfig mkcp;
|
||||
|
||||
@@ -139,6 +138,10 @@ struct XrayProtocolConfig {
|
||||
bool hasClientConfig() const;
|
||||
void setClientConfig(const XrayClientConfig &config);
|
||||
void clearClientConfig();
|
||||
|
||||
bool needsClientHydration = false;
|
||||
|
||||
bool hydrateServerConfigFromClientNative();
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -28,6 +29,20 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
|
||||
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 obj;
|
||||
@@ -35,9 +50,6 @@ QJsonObject NativeServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -70,7 +82,6 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
|
||||
NativeServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -25,6 +26,9 @@ struct NativeServerConfig {
|
||||
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static NativeServerConfig fromJson(const QJsonObject& json);
|
||||
};
|
||||
|
||||
@@ -87,9 +87,6 @@ QJsonObject SelfHostedAdminServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -132,7 +129,6 @@ SelfHostedAdminServerConfig SelfHostedAdminServerConfig::fromJson(const QJsonObj
|
||||
SelfHostedAdminServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -42,6 +43,21 @@ ContainerConfig SelfHostedUserServerConfig::containerConfig(DockerContainer cont
|
||||
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 obj;
|
||||
@@ -49,9 +65,6 @@ QJsonObject SelfHostedUserServerConfig::toJson() const
|
||||
if (!description.isEmpty()) {
|
||||
obj[configKey::description] = this->description;
|
||||
}
|
||||
if (!displayName.isEmpty()) {
|
||||
obj[configKey::displayName] = displayName;
|
||||
}
|
||||
if (!hostName.isEmpty()) {
|
||||
obj[configKey::hostName] = hostName;
|
||||
}
|
||||
@@ -84,7 +97,6 @@ SelfHostedUserServerConfig SelfHostedUserServerConfig::fromJson(const QJsonObjec
|
||||
SelfHostedUserServerConfig config;
|
||||
|
||||
config.description = json.value(configKey::description).toString();
|
||||
config.displayName = json.value(configKey::displayName).toString();
|
||||
config.hostName = json.value(configKey::hostName).toString();
|
||||
|
||||
QJsonArray containersArray = json.value(configKey::containers).toArray();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
#include <optional>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
@@ -30,6 +31,9 @@ struct SelfHostedUserServerConfig {
|
||||
std::optional<ServerCredentials> credentials() const;
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static SelfHostedUserServerConfig fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include <QLatin1Char>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
@@ -77,26 +76,6 @@ bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, in
|
||||
return endDate <= nowUtc.addDays(withinDays);
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj)
|
||||
{
|
||||
if (!jsonObj.contains(QStringLiteral("http_status"))) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
const int st = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
switch (st) {
|
||||
case 200: return amnezia::ErrorCode::NoError;
|
||||
case 400: return amnezia::ErrorCode::ApiConfigEmptyError;
|
||||
case 403: return amnezia::ErrorCode::ApiPairingForbiddenError;
|
||||
case 404: return amnezia::ErrorCode::ApiNotFoundError;
|
||||
case 408: return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
case 409: return amnezia::ErrorCode::ApiPairingConflictError;
|
||||
case 429: return amnezia::ErrorCode::ApiPairingRateLimitedError;
|
||||
case 500: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
case 503: return amnezia::ErrorCode::ApiPairingServiceUnavailableError;
|
||||
default: return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
@@ -105,15 +84,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
const int httpStatusCodeNotImplemented = 501;
|
||||
const int httpStatusCodePaymentRequired = 402;
|
||||
const int httpStatusCodeTooManyRequests = 429;
|
||||
const int httpStatusCodeRequestTimeout = 408;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
@@ -124,14 +102,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||
return amnezia::ErrorCode::ApiRateLimitError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||
@@ -141,6 +119,9 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
@@ -151,28 +132,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||
}
|
||||
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||
}
|
||||
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|
||||
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|
||||
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||
}
|
||||
|
||||
const QString msg = apiErrorMessageFromJson(jsonObj);
|
||||
if (msg.contains(QStringLiteral("QR session"), Qt::CaseInsensitive)
|
||||
&& (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive))) {
|
||||
return amnezia::ErrorCode::ApiPairingSessionExpiredError;
|
||||
if (httpStatusFromBody >= 300) {
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
if (msg.contains(QStringLiteral("not found"), Qt::CaseInsensitive)
|
||||
|| msg.contains(QStringLiteral("expired"), Qt::CaseInsensitive)) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
}
|
||||
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
|
||||
if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
}
|
||||
|
||||
qDebug() << "something went wrong";
|
||||
@@ -272,18 +253,3 @@ QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
return vpnKeyText;
|
||||
}
|
||||
|
||||
QString apiUtils::countryCodeBaseForFlag(const QString &fullCountryCode)
|
||||
{
|
||||
const QString trimmed = fullCountryCode.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
const int dashIdx = trimmed.indexOf(QLatin1Char('-'));
|
||||
const QString base = dashIdx < 0 ? trimmed : trimmed.left(dashIdx);
|
||||
const QString normalized = base.trimmed();
|
||||
if (normalized.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return normalized.toUpper();
|
||||
}
|
||||
|
||||
@@ -23,13 +23,8 @@ namespace apiUtils
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
|
||||
amnezia::ErrorCode errorCodeFromGatewayJsonHttpStatus(const QJsonObject &jsonObj);
|
||||
|
||||
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
|
||||
// ISO2-style segment for flagKit assets (e.g. US-WEST -> US). Do not use in API request bodies.
|
||||
QString countryCodeBaseForFlag(const QString &fullCountryCode);
|
||||
}
|
||||
|
||||
#endif // APIUTILS_H
|
||||
|
||||
@@ -22,7 +22,6 @@ namespace apiDefs
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String installationUuid("installation_uuid");
|
||||
constexpr QLatin1String uuid("installation_uuid");
|
||||
constexpr QLatin1String qrUuid("qr_uuid");
|
||||
constexpr QLatin1String osVersion("os_version");
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
constexpr QLatin1String serverCountryCode("server_country_code");
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace amnezia
|
||||
ServerCgroupMountpoint = 212,
|
||||
DockerPullRateLimit = 213,
|
||||
ServerLinuxKernelTooOld = 214,
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -98,15 +101,10 @@ namespace amnezia
|
||||
ApiSubscriptionNotActiveError = 1114,
|
||||
ApiNoPurchasedSubscriptionsError = 1115,
|
||||
ApiTrialAlreadyUsedError = 1116,
|
||||
|
||||
// QR pairing (gateway /v1/generate_qr, /v1/scan_qr)
|
||||
ApiPairingForbiddenError = 1117,
|
||||
ApiPairingConflictError = 1118,
|
||||
ApiPairingRateLimitedError = 1119,
|
||||
ApiPairingServiceUnavailableError = 1120,
|
||||
ApiPairingPayloadTooLargeError = 1121,
|
||||
ApiPairingMissingMetadataError = 1122,
|
||||
ApiPairingSessionExpiredError = 1123,
|
||||
ApiCaptchaRequiredError = 1117,
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiCaptchaRefreshError = 1119,
|
||||
ApiRateLimitError = 1120,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -30,6 +30,15 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||
case(ErrorCode::XrayServerConfigInvalid):
|
||||
errorMessage = QObject::tr("Server error: invalid or unreadable XRay server configuration");
|
||||
break;
|
||||
case(ErrorCode::XrayServerNoVlessClients):
|
||||
errorMessage = QObject::tr("Server error: XRay server has no VLESS clients");
|
||||
break;
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
@@ -84,13 +93,10 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
|
||||
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
||||
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
||||
case (ErrorCode::ApiPairingForbiddenError): errorMessage = QObject::tr("QR pairing was rejected (forbidden)"); break;
|
||||
case (ErrorCode::ApiPairingConflictError): errorMessage = QObject::tr("This QR code has already been used"); break;
|
||||
case (ErrorCode::ApiPairingRateLimitedError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingServiceUnavailableError): errorMessage = QObject::tr("Service temporarily unavailable. Please try again later"); break;
|
||||
case (ErrorCode::ApiPairingPayloadTooLargeError): errorMessage = QObject::tr("QR pairing data is too large to send"); break;
|
||||
case (ErrorCode::ApiPairingMissingMetadataError): errorMessage = QObject::tr("This subscription is missing data required to transfer via QR (service type or country). Refresh the subscription or pick another server."); break;
|
||||
case (ErrorCode::ApiPairingSessionExpiredError): errorMessage = QObject::tr("The QR code session has ended. Show a new QR code on the other device and scan again."); break;
|
||||
case (ErrorCode::ApiCaptchaRequiredError): errorMessage = QObject::tr("CAPTCHA verification is required"); break;
|
||||
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
|
||||
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
|
||||
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
#include <QIODevice>
|
||||
#include <QList>
|
||||
|
||||
QList<QString> qrCodeUtils::generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text)
|
||||
{
|
||||
const QString text = QString::fromUtf8(utf8Text);
|
||||
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(text.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
|
||||
const QString svg = QString::fromStdString(toSvgString(qr, 1));
|
||||
return { svgToBase64(svg) };
|
||||
}
|
||||
|
||||
QList<QString> qrCodeUtils::generateQrCodeImageSeries(const QByteArray &data)
|
||||
{
|
||||
double k = 850;
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace qrCodeUtils
|
||||
constexpr const qint16 qrMagicCode = 1984;
|
||||
|
||||
QList<QString> generateQrCodeImageSeries(const QByteArray &data);
|
||||
QList<QString> generateQrCodeImageSeriesPlainText(const QByteArray &utf8Text);
|
||||
qrcodegen::QrCode generateQrCode(const QByteArray &data);
|
||||
QString svgToBase64(const QString &image);
|
||||
};
|
||||
|
||||
@@ -295,6 +295,8 @@ amnezia::ScriptVars amnezia::genMtProxyVars(const ContainerConfig &containerConf
|
||||
|
||||
vars.append({{"$MTPROXY_PORT", c.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : c.port}});
|
||||
vars.append({{"$MTPROXY_SECRET", c.secret}});
|
||||
vars.append({{"$MTPROXY_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
vars.append({{"$MTPROXY_TAG", c.tag}});
|
||||
vars.append({{"$MTPROXY_TRANSPORT_MODE",
|
||||
c.transportMode.isEmpty() ? QString(protocols::mtProxy::transportModeStandard)
|
||||
@@ -350,6 +352,8 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
vars.append({ { "$TELEMT_TOML_TLS", faketls ? QLatin1String("true") : QLatin1String("false") } });
|
||||
vars.append({ { "$TELEMT_PORT", c.port.isEmpty() ? QString(protocols::telemt::defaultPort) : c.port } });
|
||||
vars.append({ { "$TELEMT_SECRET", c.secret } });
|
||||
vars.append({ { "$TELEMT_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0") } });
|
||||
vars.append({ { "$TELEMT_TAG", c.tag } });
|
||||
QString tlsDomain = c.tlsDomain;
|
||||
if (tlsDomain.isEmpty()) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/utils/osSignalHandler.h"
|
||||
#include "core/utils/migrations.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "Windows.h"
|
||||
#endif
|
||||
@@ -47,6 +46,11 @@ int main(int argc, char *argv[])
|
||||
AmneziaApplication app(argc, argv);
|
||||
OsSignalHandler::setup();
|
||||
|
||||
ssh_init();
|
||||
QObject::connect(&app, &QCoreApplication::aboutToQuit, []() {
|
||||
ssh_finalize();
|
||||
});
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
if (isAnotherInstanceRunning()) {
|
||||
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "android_controller.h"
|
||||
#include "android_utils.h"
|
||||
#include "ui/controllers/importUiController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -104,10 +103,7 @@ bool AndroidController::initialize()
|
||||
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
|
||||
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
|
||||
{"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)}
|
||||
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
|
||||
};
|
||||
|
||||
QJniEnvironment env;
|
||||
@@ -205,21 +201,6 @@ bool AndroidController::isCameraPresent()
|
||||
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()
|
||||
{
|
||||
return callActivityMethod<jboolean>("isOnTv", "()Z");
|
||||
@@ -245,11 +226,6 @@ void AndroidController::startQrReaderActivity()
|
||||
callActivityMethod("startQrCodeReader", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::startPairingQrReaderActivity()
|
||||
{
|
||||
callActivityMethod("startPairingQrCodeReader", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::setSaveLogs(bool enabled)
|
||||
{
|
||||
callActivityMethod("setSaveLogs", "(Z)V", enabled);
|
||||
@@ -562,11 +538,7 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
|
||||
{
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
const QString code = AndroidUtils::convertJString(env, data);
|
||||
if (PairingUiController::tryConsumeAndroidQrScan(code)) {
|
||||
return true;
|
||||
}
|
||||
return ImportUiController::decodeQrCode(code);
|
||||
return ImportUiController::decodeQrCode(AndroidUtils::convertJString(env, data));
|
||||
}
|
||||
// static
|
||||
void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp)
|
||||
@@ -606,31 +578,4 @@ void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
|
||||
emit AndroidController::instance()->activityResumed();
|
||||
}
|
||||
|
||||
// static
|
||||
void AndroidController::onCameraPermissionResult(JNIEnv *env, jobject thiz, jboolean granted)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
emit AndroidController::instance()->cameraPermissionResult(static_cast<bool>(granted));
|
||||
}
|
||||
|
||||
// static
|
||||
void AndroidController::onPairingQrCameraClosed(JNIEnv *env, jobject thiz)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
PairingUiController::notifyAndroidPairingQrCameraClosed();
|
||||
}
|
||||
|
||||
// static
|
||||
void AndroidController::onPairingQrCameraUserDismissed(JNIEnv *env, jobject thiz)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
PairingUiController::notifyAndroidPairingQrCameraUserDismissed();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,15 +38,11 @@ public:
|
||||
void closeFd();
|
||||
QString getFileName(const QString &uri);
|
||||
bool isCameraPresent();
|
||||
bool isCameraPermissionGranted();
|
||||
void requestCameraPermissionForQrPairing();
|
||||
void openApplicationDetailsSettings();
|
||||
bool isOnTv();
|
||||
bool isEdgeToEdgeEnabled();
|
||||
int getStatusBarHeight();
|
||||
int getNavigationBarHeight();
|
||||
void startQrReaderActivity();
|
||||
void startPairingQrReaderActivity();
|
||||
void setSaveLogs(bool enabled);
|
||||
void exportLogsFile(const QString &fileName);
|
||||
void clearLogs();
|
||||
@@ -81,7 +77,6 @@ signals:
|
||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||
void activityPaused();
|
||||
void activityResumed();
|
||||
void cameraPermissionResult(bool granted);
|
||||
|
||||
private:
|
||||
bool isWaitingStatus = true;
|
||||
@@ -114,9 +109,6 @@ private:
|
||||
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
|
||||
static void onActivityPaused(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>
|
||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||
|
||||
@@ -12,4 +12,3 @@ QRect QRCodeReader::cameraSize() {
|
||||
void QRCodeReader::startReading() {}
|
||||
void QRCodeReader::stopReading() {}
|
||||
void QRCodeReader::setCameraSize(QRect) {}
|
||||
void QRCodeReader::notifyCodeRead(const QString &) {}
|
||||
|
||||
@@ -16,7 +16,6 @@ public slots:
|
||||
void startReading();
|
||||
void stopReading();
|
||||
void setCameraSize(QRect value);
|
||||
void notifyCodeRead(const QString &code);
|
||||
|
||||
signals:
|
||||
void codeReaded(QString code);
|
||||
|
||||
@@ -1,56 +1,16 @@
|
||||
#if !MACOS_NE
|
||||
#include "QRCodeReaderBase.h"
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
#import <UIKit/UIKit.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
|
||||
@end
|
||||
|
||||
@interface QRCodeReaderImpl () <AVCaptureMetadataOutputObjectsDelegate>
|
||||
@property (nonatomic, assign) QRCodeReader *qrCodeReader;
|
||||
@property (nonatomic, retain) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, retain) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
|
||||
@property (nonatomic) dispatch_queue_t sessionQueue;
|
||||
@property (nonatomic) QRCodeReader* qrCodeReader;
|
||||
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
|
||||
@end
|
||||
|
||||
|
||||
@@ -59,115 +19,61 @@ static UIWindow *amneziaKeyWindowForQrCamera(void)
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.captureSession = nil;
|
||||
if (!_sessionQueue) {
|
||||
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
_captureSession = nil;
|
||||
}
|
||||
|
||||
- (void)setQrCodeReader:(QRCodeReader *)value {
|
||||
- (void)setQrCodeReader: (QRCodeReader*)value {
|
||||
_qrCodeReader = value;
|
||||
}
|
||||
|
||||
- (BOOL)startReadingOnMainThread {
|
||||
[self stopReadingOnMainThread];
|
||||
- (BOOL)startReading {
|
||||
NSError *error;
|
||||
|
||||
NSError *error = nil;
|
||||
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
|
||||
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error];
|
||||
|
||||
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!captureDevice) {
|
||||
if(!deviceInput) {
|
||||
NSLog(@"Error %@", error.localizedDescription);
|
||||
return NO;
|
||||
}
|
||||
|
||||
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
|
||||
|
||||
if (!deviceInput) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
AVCaptureSession *session = [[AVCaptureSession alloc] init];
|
||||
[session addInput:deviceInput];
|
||||
_captureSession = [[AVCaptureSession alloc]init];
|
||||
[_captureSession addInput:deviceInput];
|
||||
|
||||
AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
|
||||
[session addOutput:capturedMetadataOutput];
|
||||
[_captureSession addOutput:capturedMetadataOutput];
|
||||
|
||||
if (!_sessionQueue) {
|
||||
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
[capturedMetadataOutput setMetadataObjectsDelegate:self queue:_sessionQueue];
|
||||
[capturedMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
|
||||
dispatch_queue_t dispatchQueue;
|
||||
dispatchQueue = dispatch_queue_create("myQueue", NULL);
|
||||
[capturedMetadataOutput setMetadataObjectsDelegate: self queue: dispatchQueue];
|
||||
[capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
|
||||
|
||||
self.captureSession = session;
|
||||
[session release];
|
||||
_videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession];
|
||||
|
||||
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();
|
||||
CGRect cameraCGRect = CGRectMake(cameraRect.x(),
|
||||
cameraRect.y() + statusBarHeight,
|
||||
cameraRect.width(),
|
||||
cameraRect.height());
|
||||
|
||||
UIWindow *keyWindow = amneziaKeyWindowForQrCamera();
|
||||
if (!keyWindow) {
|
||||
[self stopReadingOnMainThread];
|
||||
return NO;
|
||||
}
|
||||
[_videoPreviewPlayer setVideoGravity: AVLayerVideoGravityResizeAspectFill];
|
||||
[_videoPreviewPlayer setFrame: cameraCGRect];
|
||||
|
||||
CGRect bounds = keyWindow.bounds;
|
||||
[self.videoPreviewPlayer setFrame:bounds];
|
||||
self.videoPreviewPlayer.zPosition = -1000.f;
|
||||
[keyWindow.layer insertSublayer:self.videoPreviewPlayer atIndex:0];
|
||||
CALayer* layer = [UIApplication sharedApplication].keyWindow.layer;
|
||||
[layer addSublayer: _videoPreviewPlayer];
|
||||
|
||||
AVCaptureSession *runningSession = self.captureSession;
|
||||
dispatch_async(_sessionQueue, ^{
|
||||
[runningSession startRunning];
|
||||
});
|
||||
[_captureSession startRunning];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)startReading {
|
||||
if ([NSThread isMainThread]) {
|
||||
return [self startReadingOnMainThread];
|
||||
}
|
||||
__block BOOL ok = NO;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
ok = [self startReadingOnMainThread];
|
||||
});
|
||||
return ok;
|
||||
}
|
||||
|
||||
- (void)stopReadingOnMainThread {
|
||||
AVCaptureSession *session = self.captureSession;
|
||||
self.captureSession = nil;
|
||||
|
||||
if (session) {
|
||||
if (!_sessionQueue) {
|
||||
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
dispatch_sync(_sessionQueue, ^{
|
||||
@try {
|
||||
if ([session isRunning]) {
|
||||
[session stopRunning];
|
||||
}
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"Session stopRunning exception: %@", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (self.videoPreviewPlayer) {
|
||||
[self.videoPreviewPlayer removeFromSuperlayer];
|
||||
self.videoPreviewPlayer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopReading {
|
||||
if ([NSThread isMainThread]) {
|
||||
[self stopReadingOnMainThread];
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[self stopReadingOnMainThread];
|
||||
});
|
||||
}
|
||||
[_captureSession stopRunning];
|
||||
_captureSession = nil;
|
||||
|
||||
[_videoPreviewPlayer removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
|
||||
@@ -176,15 +82,7 @@ static UIWindow *amneziaKeyWindowForQrCamera(void)
|
||||
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
|
||||
|
||||
if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) {
|
||||
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));
|
||||
});
|
||||
_qrCodeReader->emit codeReaded([metadataObject stringValue].UTF8String);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,10 +109,6 @@ void QRCodeReader::startReading() {
|
||||
void QRCodeReader::stopReading() {
|
||||
[m_qrCodeReader stopReading];
|
||||
}
|
||||
|
||||
void QRCodeReader::notifyCodeRead(const QString &code) {
|
||||
emit codeReaded(code);
|
||||
}
|
||||
#else
|
||||
#include "QRCodeReaderBase.h"
|
||||
|
||||
@@ -230,5 +124,4 @@ QRect QRCodeReader::cameraSize() {
|
||||
void QRCodeReader::startReading() {}
|
||||
void QRCodeReader::stopReading() {}
|
||||
void QRCodeReader::setCameraSize(QRect) {}
|
||||
void QRCodeReader::notifyCodeRead(const QString &) {}
|
||||
#endif
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#ifndef IOS_PAIRING_CAMERA_ACCESS_H
|
||||
#define IOS_PAIRING_CAMERA_ACCESS_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
bool amneziaIosPairingCameraAccessGranted();
|
||||
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone);
|
||||
void amneziaIosOpenApplicationSettings();
|
||||
|
||||
#endif
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
bool amneziaIosPairingCameraAccessGranted()
|
||||
{
|
||||
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
return status == AVAuthorizationStatusAuthorized;
|
||||
}
|
||||
|
||||
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
|
||||
{
|
||||
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
if (status == AVAuthorizationStatusAuthorized) {
|
||||
onDone(true);
|
||||
return;
|
||||
}
|
||||
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
|
||||
onDone(false);
|
||||
return;
|
||||
}
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
|
||||
completionHandler:^(BOOL granted) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
onDone(static_cast<bool>(granted));
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
void amneziaIosOpenApplicationSettings()
|
||||
{
|
||||
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
|
||||
if (url != nil) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||
|
||||
bool amneziaIosPairingCameraAccessGranted()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
|
||||
{
|
||||
onDone(true);
|
||||
}
|
||||
|
||||
void amneziaIosOpenApplicationSettings() {}
|
||||
@@ -1,16 +0,0 @@
|
||||
#ifndef IOS_PAIRING_QR_OVERLAY_WINDOW_H
|
||||
#define IOS_PAIRING_QR_OVERLAY_WINDOW_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
using AmneziaPairingQrScannedUtf8Handler = std::function<void(const char *)>;
|
||||
using AmneziaPairingQrOverlayBackHandler = std::function<void()>;
|
||||
|
||||
void amneziaIosPairingQrOverlayPresent(AmneziaPairingQrScannedUtf8Handler onScanned, AmneziaPairingQrOverlayBackHandler onBack,
|
||||
const std::string &titleUtf8, const std::string &subtitleUtf8);
|
||||
void amneziaIosPairingQrOverlayDismiss();
|
||||
void amneziaIosPairingQrOverlaySetTorchEnabled(bool on);
|
||||
void amneziaIosPairingQrOverlayRestartCapture();
|
||||
|
||||
#endif
|
||||
@@ -1,836 +0,0 @@
|
||||
#include "platforms/ios/iosPairingQrOverlayWindow.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <math.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
static const CGFloat kAmneziaPairingQrOverlayWindowLevel = (CGFloat)UIWindowLevelAlert + 1000.f;
|
||||
|
||||
static AmneziaPairingQrScannedUtf8Handler gOnScanned;
|
||||
static AmneziaPairingQrOverlayBackHandler gOnBack;
|
||||
static UIWindow *gPairingQrOverlayWindow = nil;
|
||||
static bool gTorchRequested = false;
|
||||
static CFAbsoluteTime gPairingQrOverlayKeySince = -1.0;
|
||||
|
||||
static UIWindowScene *amneziaForegroundWindowScene(void)
|
||||
{
|
||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive
|
||||
&& [scene isKindOfClass:[UIWindowScene class]]) {
|
||||
return (UIWindowScene *)scene;
|
||||
}
|
||||
}
|
||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
if ([scene isKindOfClass:[UIWindowScene class]]) {
|
||||
return (UIWindowScene *)scene;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static UIWindow *amneziaPickQtAppWindowToRestore(void)
|
||||
{
|
||||
UIWindow *best = nil;
|
||||
for (UIWindow *cw in UIApplication.sharedApplication.windows) {
|
||||
if (cw == gPairingQrOverlayWindow || cw.hidden) {
|
||||
continue;
|
||||
}
|
||||
if (cw.windowScene && cw.windowLevel <= UIWindowLevelNormal + 1) {
|
||||
if (!best || cw.isKeyWindow) {
|
||||
best = cw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static CGFloat amneziaPairingQrBottomTabStripReserve(UIWindowScene *scene)
|
||||
{
|
||||
Class qios = NSClassFromString(@"QIOSViewController");
|
||||
if (!qios) {
|
||||
return 83.f;
|
||||
}
|
||||
for (UIWindow *cw in scene.windows) {
|
||||
if (!cw.rootViewController) {
|
||||
continue;
|
||||
}
|
||||
if ([cw.rootViewController isKindOfClass:qios]) {
|
||||
const CGFloat inset = cw.safeAreaInsets.bottom;
|
||||
const CGFloat reserve = inset + 49.f;
|
||||
return MIN(MAX(reserve, 72.f), 140.f);
|
||||
}
|
||||
}
|
||||
return 83.f;
|
||||
}
|
||||
|
||||
static void amneziaApplyReadableOverCameraShadow(UIView *v)
|
||||
{
|
||||
v.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
v.layer.shadowOffset = CGSizeMake(0, 1);
|
||||
v.layer.shadowRadius = 4;
|
||||
v.layer.shadowOpacity = 0.9;
|
||||
v.layer.masksToBounds = NO;
|
||||
}
|
||||
|
||||
static UIColor *amneziaPaleGray(void)
|
||||
{
|
||||
return [UIColor colorWithRed:(CGFloat)0xD7 / 255.0 green:(CGFloat)0xD8 / 255.0 blue:(CGFloat)0xDB / 255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
static void amneziaAddCornerMinorArc(UIBezierPath *p, CGPoint C, CGFloat r, CGPoint S, CGPoint E)
|
||||
{
|
||||
const CGFloat as = atan2f((float)(S.y - C.y), (float)(S.x - C.x));
|
||||
CGFloat ae = atan2f((float)(E.y - C.y), (float)(E.x - C.x));
|
||||
while (ae - as > (CGFloat)M_PI) {
|
||||
ae -= (CGFloat)(2.0 * M_PI);
|
||||
}
|
||||
while (ae - as < (CGFloat)(-M_PI)) {
|
||||
ae += (CGFloat)(2.0 * M_PI);
|
||||
}
|
||||
const CGFloat minor = ae - as;
|
||||
const BOOL cw = minor > 0;
|
||||
[p addArcWithCenter:C radius:r startAngle:as endAngle:ae clockwise:cw];
|
||||
}
|
||||
|
||||
static UIBezierPath *amneziaScanBracketStrokePath(int corner, CGFloat x0, CGFloat y0, CGFloat s, CGFloat R, CGFloat L, CGFloat t)
|
||||
{
|
||||
const CGFloat r = MAX(1.5, R - t * 0.5);
|
||||
UIBezierPath *p = [UIBezierPath bezierPath];
|
||||
const CGFloat yy = y0 + t * 0.5f;
|
||||
const CGFloat yyb = y0 + s - t * 0.5f;
|
||||
const CGFloat xx = x0 + t * 0.5f;
|
||||
const CGFloat xxb = x0 + s - t * 0.5f;
|
||||
|
||||
switch (corner) {
|
||||
case 0: {
|
||||
const CGPoint cTL = CGPointMake(x0 + R, y0 + R);
|
||||
const CGPoint sTL = CGPointMake(x0 + R, yy);
|
||||
const CGPoint eTL = CGPointMake(xx, y0 + R);
|
||||
[p moveToPoint:CGPointMake(x0 + R + L, yy)];
|
||||
[p addLineToPoint:sTL];
|
||||
amneziaAddCornerMinorArc(p, cTL, r, sTL, eTL);
|
||||
const CGFloat yEndTL = MIN(y0 + R + L, y0 + s - R - t * 0.5f);
|
||||
[p addLineToPoint:CGPointMake(xx, MAX(yEndTL, y0 + R + 2.f))];
|
||||
} break;
|
||||
case 1: {
|
||||
const CGPoint cTR = CGPointMake(x0 + s - R, y0 + R);
|
||||
const CGPoint sTR = CGPointMake(x0 + s - R, yy);
|
||||
const CGPoint eTR = CGPointMake(xxb, y0 + R);
|
||||
[p moveToPoint:CGPointMake(x0 + s - R - L, yy)];
|
||||
[p addLineToPoint:sTR];
|
||||
amneziaAddCornerMinorArc(p, cTR, r, sTR, eTR);
|
||||
const CGFloat yEndTR = MIN(y0 + R + L, y0 + s - R - t * 0.5f);
|
||||
[p addLineToPoint:CGPointMake(xxb, MAX(yEndTR, y0 + R + 2.f))];
|
||||
} break;
|
||||
case 2: {
|
||||
const CGPoint cBL = CGPointMake(x0 + R, y0 + s - R);
|
||||
const CGPoint sBL = CGPointMake(x0 + R, yyb);
|
||||
const CGPoint eBL = CGPointMake(xx, y0 + s - R);
|
||||
[p moveToPoint:CGPointMake(x0 + R + L, yyb)];
|
||||
[p addLineToPoint:sBL];
|
||||
amneziaAddCornerMinorArc(p, cBL, r, sBL, eBL);
|
||||
const CGFloat yEndTopRef = MAX(MIN(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2.f);
|
||||
const CGFloat yLegBL = y0 + s + y0 - yEndTopRef;
|
||||
[p addLineToPoint:CGPointMake(xx, yLegBL)];
|
||||
} break;
|
||||
case 3: {
|
||||
const CGPoint cBR = CGPointMake(x0 + s - R, y0 + s - R);
|
||||
const CGPoint sBR = CGPointMake(x0 + s - R, yyb);
|
||||
const CGPoint eBR = CGPointMake(xxb, y0 + s - R);
|
||||
[p moveToPoint:CGPointMake(x0 + s - R - L, yyb)];
|
||||
[p addLineToPoint:sBR];
|
||||
amneziaAddCornerMinorArc(p, cBR, r, sBR, eBR);
|
||||
const CGFloat yEndTopRef = MAX(MIN(y0 + R + L, y0 + s - R - t * 0.5f), y0 + R + 2.f);
|
||||
const CGFloat yLegBR = y0 + s + y0 - yEndTopRef;
|
||||
[p addLineToPoint:CGPointMake(xxb, yLegBR)];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@interface AmneziaPairingQrOverlayViewController : UIViewController
|
||||
@end
|
||||
|
||||
@interface AmneziaPairingQrOverlayViewController () <AVCaptureMetadataOutputObjectsDelegate>
|
||||
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput;
|
||||
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
|
||||
@property (nonatomic, strong) AVCaptureDevice *videoDevice;
|
||||
@property (nonatomic, strong) dispatch_queue_t sessionQueue;
|
||||
@property (nonatomic, strong) UIView *cameraContainer;
|
||||
@property (nonatomic, strong) UIView *headerContainer;
|
||||
@property (nonatomic, strong) UIButton *backButton;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *subtitleLabel;
|
||||
@property (nonatomic, strong) UIButton *torchButton;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *torchCenterYConstraint;
|
||||
@property (nonatomic, copy) NSString *chromeTitleText;
|
||||
@property (nonatomic, copy) NSString *chromeSubtitleText;
|
||||
@property (nonatomic, strong) UIView *scanDimView;
|
||||
@property (nonatomic, strong) CAShapeLayer *scanDimMaskLayer;
|
||||
@property (nonatomic, strong) UIView *scanHoleFillView;
|
||||
@property (nonatomic, strong) CAShapeLayer *scanHoleHighlightLayer;
|
||||
@property (nonatomic, strong) UIView *bracketContainer;
|
||||
@property (nonatomic, strong) NSMutableArray<CAShapeLayer *> *bracketCornerLayers;
|
||||
@end
|
||||
|
||||
@implementation AmneziaPairingQrOverlayViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor clearColor];
|
||||
if (!self.sessionQueue) {
|
||||
self.sessionQueue = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
[self buildChromeUi];
|
||||
}
|
||||
|
||||
- (void)buildChromeUi
|
||||
{
|
||||
if (self.headerContainer) {
|
||||
return;
|
||||
}
|
||||
UIView *cam = [[UIView alloc] init];
|
||||
cam.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
cam.backgroundColor = [UIColor clearColor];
|
||||
cam.clipsToBounds = YES;
|
||||
self.cameraContainer = cam;
|
||||
[self.view addSubview:cam];
|
||||
|
||||
UIView *holeFill = [[UIView alloc] init];
|
||||
holeFill.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
holeFill.backgroundColor = [UIColor clearColor];
|
||||
holeFill.opaque = NO;
|
||||
holeFill.userInteractionEnabled = NO;
|
||||
self.scanHoleFillView = holeFill;
|
||||
CAShapeLayer *hi = [CAShapeLayer layer];
|
||||
hi.fillColor = [UIColor colorWithWhite:1.0 alpha:0.14].CGColor;
|
||||
hi.strokeColor = nil;
|
||||
[holeFill.layer addSublayer:hi];
|
||||
self.scanHoleHighlightLayer = hi;
|
||||
[self.view addSubview:holeFill];
|
||||
|
||||
UIView *dim = [[UIView alloc] init];
|
||||
dim.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
dim.backgroundColor = [UIColor colorWithWhite:0.02 alpha:0.55];
|
||||
dim.userInteractionEnabled = NO;
|
||||
dim.opaque = NO;
|
||||
self.scanDimView = dim;
|
||||
[self.view addSubview:dim];
|
||||
|
||||
CAShapeLayer *dimMask = [CAShapeLayer layer];
|
||||
dimMask.fillRule = kCAFillRuleEvenOdd;
|
||||
dimMask.fillColor = [UIColor blackColor].CGColor;
|
||||
dim.layer.mask = dimMask;
|
||||
self.scanDimMaskLayer = dimMask;
|
||||
|
||||
UIView *bracketHost = [[UIView alloc] init];
|
||||
bracketHost.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
bracketHost.backgroundColor = [UIColor clearColor];
|
||||
bracketHost.opaque = NO;
|
||||
bracketHost.userInteractionEnabled = NO;
|
||||
self.bracketContainer = bracketHost;
|
||||
[self.view addSubview:bracketHost];
|
||||
|
||||
self.bracketCornerLayers = [NSMutableArray arrayWithCapacity:4];
|
||||
for (NSInteger i = 0; i < 4; i++) {
|
||||
CAShapeLayer *sl = [CAShapeLayer layer];
|
||||
sl.fillColor = nil;
|
||||
sl.strokeColor = [UIColor colorWithWhite:0.94 alpha:1].CGColor;
|
||||
sl.lineWidth = 5.0;
|
||||
sl.lineCap = kCALineCapRound;
|
||||
sl.lineJoin = kCALineJoinRound;
|
||||
[bracketHost.layer addSublayer:sl];
|
||||
[self.bracketCornerLayers addObject:sl];
|
||||
}
|
||||
|
||||
UIView *header = [[UIView alloc] init];
|
||||
header.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
header.backgroundColor = [UIColor clearColor];
|
||||
header.opaque = NO;
|
||||
header.userInteractionEnabled = YES;
|
||||
self.headerContainer = header;
|
||||
[self.view addSubview:header];
|
||||
|
||||
UIButton *back = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
back.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
back.tintColor = amneziaPaleGray();
|
||||
if (@available(iOS 13.0, *)) {
|
||||
const CGFloat kBackArrowPt = 20.0;
|
||||
UIImageSymbolConfiguration *sym =
|
||||
[UIImageSymbolConfiguration configurationWithPointSize:kBackArrowPt weight:UIImageSymbolWeightMedium
|
||||
scale:UIImageSymbolScaleDefault];
|
||||
UIImage *img = [UIImage systemImageNamed:@"arrow.left" withConfiguration:sym];
|
||||
[back setImage:[img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
|
||||
} else {
|
||||
[back setTitle:@"<" forState:UIControlStateNormal];
|
||||
}
|
||||
[back addTarget:self action:@selector(backTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
self.backButton = back;
|
||||
[header addSubview:back];
|
||||
|
||||
UILabel *title = [[UILabel alloc] init];
|
||||
title.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
title.textColor = [UIColor colorWithWhite:0.96 alpha:1];
|
||||
title.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
|
||||
title.numberOfLines = 0;
|
||||
title.text = self.chromeTitleText.length ? self.chromeTitleText : @"Add device via QR";
|
||||
self.titleLabel = title;
|
||||
[header addSubview:title];
|
||||
amneziaApplyReadableOverCameraShadow(title);
|
||||
|
||||
UILabel *sub = [[UILabel alloc] init];
|
||||
sub.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
sub.textColor = [UIColor colorWithWhite:0.88 alpha:0.95];
|
||||
sub.font = [UIFont systemFontOfSize:14 weight:UIFontWeightRegular];
|
||||
sub.numberOfLines = 0;
|
||||
sub.text = self.chromeSubtitleText.length
|
||||
? self.chromeSubtitleText
|
||||
: @"Scan the session QR shown on the device you want to add.";
|
||||
self.subtitleLabel = sub;
|
||||
[header addSubview:sub];
|
||||
amneziaApplyReadableOverCameraShadow(sub);
|
||||
|
||||
UIButton *torch = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
torch.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[torch setTitle:@"🔦" forState:UIControlStateNormal];
|
||||
torch.titleLabel.font = [UIFont systemFontOfSize:26];
|
||||
torch.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.22];
|
||||
torch.layer.cornerRadius = 28;
|
||||
torch.clipsToBounds = YES;
|
||||
[torch addTarget:self action:@selector(torchTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
self.torchButton = torch;
|
||||
[self.view addSubview:torch];
|
||||
|
||||
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[cam.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[cam.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[cam.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[cam.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[holeFill.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[holeFill.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[holeFill.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[holeFill.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[dim.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[dim.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[dim.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[dim.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[bracketHost.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||
[bracketHost.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[bracketHost.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[bracketHost.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||||
|
||||
[header.topAnchor constraintEqualToAnchor:safe.topAnchor],
|
||||
[header.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||
[header.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||
[header.heightAnchor constraintGreaterThanOrEqualToConstant:120],
|
||||
|
||||
[back.leadingAnchor constraintEqualToAnchor:header.leadingAnchor constant:8],
|
||||
[back.topAnchor constraintEqualToAnchor:header.topAnchor constant:20],
|
||||
[back.widthAnchor constraintEqualToConstant:40],
|
||||
[back.heightAnchor constraintEqualToConstant:40],
|
||||
|
||||
[title.leadingAnchor constraintEqualToAnchor:header.leadingAnchor constant:16],
|
||||
[title.trailingAnchor constraintEqualToAnchor:header.trailingAnchor constant:-16],
|
||||
[title.topAnchor constraintEqualToAnchor:back.bottomAnchor],
|
||||
|
||||
[sub.leadingAnchor constraintEqualToAnchor:title.leadingAnchor],
|
||||
[sub.trailingAnchor constraintEqualToAnchor:title.trailingAnchor],
|
||||
[sub.topAnchor constraintEqualToAnchor:title.bottomAnchor constant:8],
|
||||
[sub.bottomAnchor constraintEqualToAnchor:header.bottomAnchor constant:-10],
|
||||
|
||||
[torch.topAnchor constraintGreaterThanOrEqualToAnchor:header.bottomAnchor constant:8],
|
||||
[torch.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
|
||||
[torch.widthAnchor constraintEqualToConstant:56],
|
||||
[torch.heightAnchor constraintEqualToConstant:56],
|
||||
]];
|
||||
NSLayoutConstraint *torchCy = [torch.centerYAnchor constraintEqualToAnchor:self.view.topAnchor constant:200.0];
|
||||
self.torchCenterYConstraint = torchCy;
|
||||
torchCy.active = YES;
|
||||
[header setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
|
||||
[header setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
|
||||
}
|
||||
|
||||
- (void)applyMetadataRectOfInterestForScanHole:(CGRect)holeInScanDimBounds
|
||||
{
|
||||
if (!self.previewLayer || !self.metadataOutput || !self.scanDimView || !self.cameraContainer) {
|
||||
return;
|
||||
}
|
||||
if (CGRectIsEmpty(holeInScanDimBounds) || holeInScanDimBounds.size.width < 24.0 || holeInScanDimBounds.size.height < 24.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect holeInCam = [self.scanDimView convertRect:holeInScanDimBounds toView:self.cameraContainer];
|
||||
holeInCam = CGRectIntersection(holeInCam, self.cameraContainer.bounds);
|
||||
if (CGRectIsEmpty(holeInCam)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CGRect plFrame = self.previewLayer.frame;
|
||||
CGRect holeInPreview = CGRectOffset(holeInCam, -plFrame.origin.x, -plFrame.origin.y);
|
||||
holeInPreview = CGRectIntersection(holeInPreview, self.previewLayer.bounds);
|
||||
if (CGRectIsEmpty(holeInPreview)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect roi = [self.previewLayer metadataOutputRectOfInterestForRect:holeInPreview];
|
||||
roi.origin.x = MAX(0.0, MIN(1.0, roi.origin.x));
|
||||
roi.origin.y = MAX(0.0, MIN(1.0, roi.origin.y));
|
||||
roi.size.width = MAX(0.02, MIN(1.0 - roi.origin.x, roi.size.width));
|
||||
roi.size.height = MAX(0.02, MIN(1.0 - roi.origin.y, roi.size.height));
|
||||
|
||||
AVCaptureMetadataOutput *mo = self.metadataOutput;
|
||||
dispatch_queue_t sq = self.sessionQueue;
|
||||
if (!mo || !sq) {
|
||||
return;
|
||||
}
|
||||
dispatch_async(sq, ^{
|
||||
mo.rectOfInterest = roi;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)layoutScanOverlayGeometry
|
||||
{
|
||||
if (!self.scanDimView || !self.scanDimMaskLayer || !self.scanHoleHighlightLayer || self.bracketCornerLayers.count != 4) {
|
||||
return;
|
||||
}
|
||||
const CGRect vb = self.scanDimView.bounds;
|
||||
if (vb.size.width < 32 || vb.size.height < 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat sqSz = floor(MIN(vb.size.width, vb.size.height) * 0.72);
|
||||
CGFloat sqX = (vb.size.width - sqSz) / 2.0;
|
||||
CGFloat sqY = (vb.size.height - sqSz) / 2.0;
|
||||
|
||||
CGFloat headerBottom = CGRectGetMaxY(self.headerContainer.frame);
|
||||
if (headerBottom < 8.0) {
|
||||
headerBottom = 132.0 + self.view.safeAreaInsets.top;
|
||||
}
|
||||
sqY = MAX(sqY, headerBottom + 8.0);
|
||||
|
||||
const CGFloat kBottomBandForTorch = 80.0;
|
||||
const CGFloat maxHoleBottom = vb.size.height - kBottomBandForTorch;
|
||||
if (sqY + sqSz > maxHoleBottom) {
|
||||
sqY = maxHoleBottom - sqSz;
|
||||
sqY = MAX(sqY, headerBottom + 8.0);
|
||||
}
|
||||
|
||||
sqX = MAX(8.0, MIN(sqX, vb.size.width - sqSz - 8.0));
|
||||
sqY = MAX(headerBottom + 4.0, MIN(sqY, vb.size.height - sqSz - 8.0));
|
||||
|
||||
const CGRect hole = CGRectMake(sqX, sqY, sqSz, sqSz);
|
||||
CGFloat holeR = MIN(28.0, MAX(10.0, sqSz * 0.056));
|
||||
{
|
||||
const CGFloat half = 0.5 * MIN(hole.size.width, hole.size.height);
|
||||
holeR = MIN(holeR, MAX(6.0, half - 2.0));
|
||||
}
|
||||
UIBezierPath *holeRoundPath = [UIBezierPath bezierPathWithRoundedRect:hole cornerRadius:holeR];
|
||||
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithRect:vb];
|
||||
[path appendPath:holeRoundPath];
|
||||
self.scanDimMaskLayer.frame = vb;
|
||||
self.scanDimMaskLayer.path = path.CGPath;
|
||||
|
||||
self.scanHoleHighlightLayer.frame = CGRectMake(0, 0, vb.size.width, vb.size.height);
|
||||
self.scanHoleHighlightLayer.path = holeRoundPath.CGPath;
|
||||
|
||||
const CGFloat bracketThick = 5.0;
|
||||
const CGFloat bracketLen = (CGFloat)MAX(28, (NSInteger)floor(sqSz * 0.13));
|
||||
const CGFloat x0 = hole.origin.x;
|
||||
const CGFloat y0 = hole.origin.y;
|
||||
const CGFloat s = hole.size.width;
|
||||
|
||||
const CGFloat t = bracketThick;
|
||||
const CGFloat L = bracketLen;
|
||||
|
||||
for (NSUInteger i = 0; i < 4; i++) {
|
||||
CAShapeLayer *layer = self.bracketCornerLayers[i];
|
||||
layer.lineWidth = t;
|
||||
layer.path = amneziaScanBracketStrokePath((int)i, x0, y0, s, holeR, L, t).CGPath;
|
||||
}
|
||||
|
||||
if (self.torchCenterYConstraint && self.torchButton) {
|
||||
const CGFloat holeBottom = CGRectGetMaxY(hole);
|
||||
const CGFloat bandBottom = vb.size.height;
|
||||
const CGFloat torchH = 56.0;
|
||||
CGFloat torchCenterY = (holeBottom + bandBottom) * 0.5;
|
||||
const CGFloat minC = holeBottom + torchH * 0.5 + 6.0;
|
||||
const CGFloat maxC = bandBottom - torchH * 0.5 - MAX(6.0, self.view.safeAreaInsets.bottom);
|
||||
torchCenterY = MAX(minC, MIN(maxC, torchCenterY));
|
||||
if (minC > maxC) {
|
||||
torchCenterY = (minC + maxC) * 0.5;
|
||||
}
|
||||
const CGFloat hdr = headerBottom + torchH * 0.5 + 10.0;
|
||||
torchCenterY = MAX(torchCenterY, hdr);
|
||||
self.torchCenterYConstraint.constant = torchCenterY;
|
||||
}
|
||||
|
||||
[self applyMetadataRectOfInterestForScanHole:hole];
|
||||
}
|
||||
|
||||
- (void)backTapped
|
||||
{
|
||||
if (gOnBack) {
|
||||
gOnBack();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)torchTapped
|
||||
{
|
||||
gTorchRequested = !gTorchRequested;
|
||||
[self applyTorchFromGlobalFlag];
|
||||
if (gTorchRequested) {
|
||||
self.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.42];
|
||||
self.torchButton.layer.borderWidth = 2;
|
||||
self.torchButton.layer.borderColor = [UIColor colorWithRed:1 green:0.75 blue:0.45 alpha:1].CGColor;
|
||||
} else {
|
||||
self.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.22];
|
||||
self.torchButton.layer.borderWidth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
{
|
||||
[super viewDidLayoutSubviews];
|
||||
if (self.previewLayer && self.cameraContainer) {
|
||||
self.previewLayer.frame = self.cameraContainer.bounds;
|
||||
}
|
||||
[self layoutScanOverlayGeometry];
|
||||
if (self.scanHoleFillView) {
|
||||
[self.view bringSubviewToFront:self.scanHoleFillView];
|
||||
}
|
||||
if (self.scanDimView) {
|
||||
[self.view bringSubviewToFront:self.scanDimView];
|
||||
}
|
||||
if (self.bracketContainer) {
|
||||
[self.view bringSubviewToFront:self.bracketContainer];
|
||||
}
|
||||
if (self.headerContainer) {
|
||||
[self.view bringSubviewToFront:self.headerContainer];
|
||||
}
|
||||
if (self.torchButton) {
|
||||
[self.view bringSubviewToFront:self.torchButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applyTorchOnMainThread:(BOOL)on
|
||||
{
|
||||
AVCaptureDevice *device = self.videoDevice;
|
||||
if (!device || ![device hasTorch]) {
|
||||
if (on && gTorchRequested) {
|
||||
__unsafe_unretained AmneziaPairingQrOverlayViewController *unsafeSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.12 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
AmneziaPairingQrOverlayViewController *strongSelf = unsafeSelf;
|
||||
if (strongSelf && gTorchRequested) {
|
||||
[strongSelf applyTorchOnMainThread:YES];
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
AVCaptureSession *session = self.captureSession;
|
||||
if (on && session && ![session isRunning]) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (gTorchRequested) {
|
||||
[self applyTorchOnMainThread:YES];
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
NSError *err = nil;
|
||||
if (![device lockForConfiguration:&err]) {
|
||||
return;
|
||||
}
|
||||
if (on) {
|
||||
err = nil;
|
||||
if (![device setTorchModeOnWithLevel:AVCaptureMaxAvailableTorchLevel error:&err]) {
|
||||
if ([device isTorchModeSupported:AVCaptureTorchModeOn]) {
|
||||
device.torchMode = AVCaptureTorchModeOn;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.torchMode = AVCaptureTorchModeOff;
|
||||
}
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
|
||||
- (void)applyTorchFromGlobalFlag
|
||||
{
|
||||
[self applyTorchOnMainThread:gTorchRequested ? YES : NO];
|
||||
}
|
||||
|
||||
- (void)stopCapturePipelineOnMainThread
|
||||
{
|
||||
[self applyTorchOnMainThread:NO];
|
||||
self.videoDevice = nil;
|
||||
|
||||
AVCaptureSession *session = self.captureSession;
|
||||
self.captureSession = nil;
|
||||
self.metadataOutput = nil;
|
||||
|
||||
if (self.previewLayer) {
|
||||
[self.previewLayer removeFromSuperlayer];
|
||||
self.previewLayer = nil;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
dispatch_queue_t q = self.sessionQueue;
|
||||
if (!q) {
|
||||
q = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
|
||||
self.sessionQueue = q;
|
||||
}
|
||||
dispatch_sync(q, ^{
|
||||
@try {
|
||||
if ([session isRunning]) {
|
||||
[session stopRunning];
|
||||
}
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"Stop running exception: %@", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)startCapturePipelineOnMainThread
|
||||
{
|
||||
|
||||
[self stopCapturePipelineOnMainThread];
|
||||
|
||||
if (!self.cameraContainer) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!device) {
|
||||
return NO;
|
||||
}
|
||||
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
|
||||
if (!input) {
|
||||
return NO;
|
||||
}
|
||||
self.videoDevice = device;
|
||||
|
||||
AVCaptureSession *session = [[AVCaptureSession alloc] init];
|
||||
if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
|
||||
session.sessionPreset = AVCaptureSessionPresetHigh;
|
||||
}
|
||||
|
||||
[session addInput:input];
|
||||
|
||||
AVCaptureMetadataOutput *meta = [[AVCaptureMetadataOutput alloc] init];
|
||||
if (![session canAddOutput:meta]) {
|
||||
return NO;
|
||||
}
|
||||
[session addOutput:meta];
|
||||
dispatch_queue_t q = self.sessionQueue;
|
||||
if (!q) {
|
||||
q = dispatch_queue_create("org.amnezia.pairingqr.overlay", DISPATCH_QUEUE_SERIAL);
|
||||
self.sessionQueue = q;
|
||||
}
|
||||
[meta setMetadataObjectsDelegate:self queue:q];
|
||||
meta.metadataObjectTypes = @[ AVMetadataObjectTypeQRCode ];
|
||||
|
||||
self.captureSession = session;
|
||||
self.metadataOutput = meta;
|
||||
|
||||
AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
|
||||
preview.videoGravity = AVLayerVideoGravityResizeAspectFill;
|
||||
self.previewLayer = preview;
|
||||
[self.cameraContainer.layer insertSublayer:preview atIndex:0];
|
||||
preview.frame = self.cameraContainer.bounds;
|
||||
|
||||
[self.view layoutIfNeeded];
|
||||
[self layoutScanOverlayGeometry];
|
||||
|
||||
AVCaptureSession *runningSession = session;
|
||||
__unsafe_unretained AmneziaPairingQrOverlayViewController *weakSelf = self;
|
||||
dispatch_async(q, ^{
|
||||
@try {
|
||||
[runningSession startRunning];
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"Start running exception: %@", ex);
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
AmneziaPairingQrOverlayViewController *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
[strongSelf applyTorchFromGlobalFlag];
|
||||
});
|
||||
});
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)output
|
||||
didOutputMetadataObjects:(NSArray<__kindof AVMetadataMachineReadableCodeObject *> *)metadataObjects
|
||||
fromConnection:(AVCaptureConnection *)connection
|
||||
{
|
||||
(void)output;
|
||||
(void)connection;
|
||||
for (AVMetadataMachineReadableCodeObject *obj in metadataObjects) {
|
||||
NSString *value = obj.stringValue;
|
||||
if (value.length == 0) {
|
||||
continue;
|
||||
}
|
||||
const char *utf8 = value.UTF8String;
|
||||
std::string copy(utf8 ? utf8 : "");
|
||||
if (copy.empty()) {
|
||||
continue;
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (gOnScanned) {
|
||||
gOnScanned(copy.c_str());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static void amneziaPairingQrOverlayTeardownOnMain(void)
|
||||
{
|
||||
UIWindow *w = gPairingQrOverlayWindow;
|
||||
gPairingQrOverlayWindow = nil;
|
||||
gOnScanned = nullptr;
|
||||
gOnBack = nullptr;
|
||||
gTorchRequested = false;
|
||||
gPairingQrOverlayKeySince = -1.0;
|
||||
|
||||
if (w) {
|
||||
UIViewController *root = w.rootViewController;
|
||||
w.rootViewController = nil;
|
||||
w.hidden = YES;
|
||||
if ([root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
|
||||
[(AmneziaPairingQrOverlayViewController *)root stopCapturePipelineOnMainThread];
|
||||
}
|
||||
}
|
||||
|
||||
UIWindow *restore = amneziaPickQtAppWindowToRestore();
|
||||
if (restore) {
|
||||
[restore makeKeyWindow];
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlayPresent(AmneziaPairingQrScannedUtf8Handler onScanned, AmneziaPairingQrOverlayBackHandler onBack,
|
||||
const std::string &titleUtf8, const std::string &subtitleUtf8)
|
||||
{
|
||||
const bool hasScan = static_cast<bool>(onScanned);
|
||||
const bool hasBack = static_cast<bool>(onBack);
|
||||
AmneziaPairingQrScannedUtf8Handler scanH = std::move(onScanned);
|
||||
AmneziaPairingQrOverlayBackHandler backH = std::move(onBack);
|
||||
const std::string titleCopy = titleUtf8;
|
||||
const std::string subCopy = subtitleUtf8;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
amneziaPairingQrOverlayTeardownOnMain();
|
||||
gOnScanned = std::move(scanH);
|
||||
gOnBack = std::move(backH);
|
||||
|
||||
UIWindowScene *scene = amneziaForegroundWindowScene();
|
||||
if (!scene) {
|
||||
gOnScanned = nullptr;
|
||||
gOnBack = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
const CGFloat bottomReserve = amneziaPairingQrBottomTabStripReserve(scene);
|
||||
const CGRect sceneBounds = scene.coordinateSpace.bounds;
|
||||
const CGRect overlayFrame = CGRectMake(0, 0, sceneBounds.size.width, sceneBounds.size.height - bottomReserve);
|
||||
|
||||
AmneziaPairingQrOverlayViewController *vc = [[AmneziaPairingQrOverlayViewController alloc] init];
|
||||
NSString *nsTitle = titleCopy.empty() ? nil : [NSString stringWithUTF8String:titleCopy.c_str()];
|
||||
NSString *nsSub = subCopy.empty() ? nil : [NSString stringWithUTF8String:subCopy.c_str()];
|
||||
vc.chromeTitleText = nsTitle;
|
||||
vc.chromeSubtitleText = nsSub;
|
||||
|
||||
UIWindow *w = [[UIWindow alloc] initWithWindowScene:scene];
|
||||
w.frame = overlayFrame;
|
||||
w.windowLevel = kAmneziaPairingQrOverlayWindowLevel;
|
||||
w.backgroundColor = [UIColor blackColor];
|
||||
w.rootViewController = vc;
|
||||
gPairingQrOverlayWindow = w;
|
||||
|
||||
[w makeKeyAndVisible];
|
||||
[w layoutIfNeeded];
|
||||
[vc.view setNeedsLayout];
|
||||
[vc.view layoutIfNeeded];
|
||||
|
||||
gPairingQrOverlayKeySince = CFAbsoluteTimeGetCurrent();
|
||||
|
||||
if (![vc startCapturePipelineOnMainThread]) {
|
||||
NSLog(@"Start capture failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlayDismiss()
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
amneziaPairingQrOverlayTeardownOnMain();
|
||||
});
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlaySetTorchEnabled(bool on)
|
||||
{
|
||||
gTorchRequested = on;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIWindow *win = gPairingQrOverlayWindow;
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
UIViewController *root = win.rootViewController;
|
||||
if ([root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
|
||||
AmneziaPairingQrOverlayViewController *vc = (AmneziaPairingQrOverlayViewController *)root;
|
||||
[vc applyTorchFromGlobalFlag];
|
||||
if (vc.torchButton) {
|
||||
if (on) {
|
||||
vc.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.42];
|
||||
vc.torchButton.layer.borderWidth = 2;
|
||||
vc.torchButton.layer.borderColor = [UIColor colorWithRed:1 green:0.75 blue:0.45 alpha:1].CGColor;
|
||||
} else {
|
||||
vc.torchButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.18];
|
||||
vc.torchButton.layer.borderWidth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void amneziaIosPairingQrOverlayRestartCapture()
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
const CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
|
||||
if (gPairingQrOverlayKeySince > 0 && (now - gPairingQrOverlayKeySince) < 1.0) {
|
||||
return;
|
||||
}
|
||||
UIWindow *w = gPairingQrOverlayWindow;
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
UIViewController *root = w.rootViewController;
|
||||
if (![root isKindOfClass:[AmneziaPairingQrOverlayViewController class]]) {
|
||||
return;
|
||||
}
|
||||
AmneziaPairingQrOverlayViewController *vc = (AmneziaPairingQrOverlayViewController *)root;
|
||||
[vc stopCapturePipelineOnMainThread];
|
||||
if (![vc startCapturePipelineOnMainThread]) {
|
||||
NSLog(@"Restart startCapture failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
if which apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
|
||||
elif which dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
|
||||
elif which yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
|
||||
elif which zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
|
||||
elif which pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
|
||||
else echo "Packet manager not found"; echo "Internal error"; exit 1; fi;\
|
||||
if command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
|
||||
if which apt-get > /dev/null 2>&1 || command -v apt-get > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/dpkg/lock-frontend";\
|
||||
elif which dnf > /dev/null 2>&1 || command -v dnf > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/cache/dnf/* /var/run/dnf/* /var/lib/dnf/* /var/lib/rpm/*";\
|
||||
elif which yum > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/yum.pid";\
|
||||
elif which zypper > /dev/null 2>&1 || command -v zypper > /dev/null 2>&1; then LOCK_CMD="cat"; LOCK_FILE="/var/run/zypp.pid";\
|
||||
elif which pacman > /dev/null 2>&1 || command -v pacman > /dev/null 2>&1; then LOCK_CMD="fuser"; LOCK_FILE="/var/lib/pacman/db.lck";\
|
||||
else echo "Packet manager not found"; echo "Internal error"; exit 1;\
|
||||
fi;\
|
||||
if which $LOCK_CMD > /dev/null 2>&1 || command -v $LOCK_CMD > /dev/null 2>&1; then sudo $LOCK_CMD $LOCK_FILE 2>/dev/null; else echo "$LOCK_CMD not installed"; fi
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); opt="--version";\
|
||||
elif which dnf > /dev/null 2>&1; then pm=$(which dnf); opt="--version";\
|
||||
elif which yum > /dev/null 2>&1; then pm=$(which yum); opt="--version";\
|
||||
elif which zypper > /dev/null 2>&1; then pm=$(which zypper); opt="--version";\
|
||||
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); opt="--version";\
|
||||
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then opt="--version";\
|
||||
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then opt="--version";\
|
||||
else pm="uname"; opt="-a";\
|
||||
fi;\
|
||||
CUR_USER=$(whoami 2>/dev/null || echo $HOME | sed 's/.*\///');\
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
curl -s https://core.telegram.org/getProxySecret -o /data/proxy-secret
|
||||
curl -s https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
|
||||
|
||||
# Determine secret: env var -> saved file -> generate new
|
||||
if [ -n "$MTPROXY_SECRET" ]; then
|
||||
# Determine secret: regenerate (fresh install) -> env var -> saved file -> generate new
|
||||
if [ "$MTPROXY_REGENERATE_SECRET" = "1" ]; then
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
elif [ -n "$MTPROXY_SECRET" ]; then
|
||||
SECRET="$MTPROXY_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
sudo docker stop $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,8 +4,10 @@
|
||||
echo "[*] Amnezia Telemt: configure script start"
|
||||
mkdir -p /data/tlsfront
|
||||
|
||||
# Secret: substituted $TELEMT_SECRET -> saved file -> openssl (same rules as MTProxy configure)
|
||||
if [ -n "$TELEMT_SECRET" ]; then
|
||||
# Secret: regenerate (fresh install) -> env var -> saved file -> openssl
|
||||
if [ "$TELEMT_REGENERATE_SECRET" = "1" ]; then
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
elif [ -n "$TELEMT_SECRET" ]; then
|
||||
SECRET="$TELEMT_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.25.0)
|
||||
|
||||
project(AmneziaVPN_Tests)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||
|
||||
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
qt6_add_resources(TEST_QRC
|
||||
${CLIENT_ROOT_DIR}/server_scripts/serverScripts.qrc
|
||||
)
|
||||
|
||||
add_library(test_common OBJECT
|
||||
${SOURCES}
|
||||
${HEADERS}
|
||||
${TEST_QRC}
|
||||
)
|
||||
|
||||
qt_add_repc_replicas(test_common
|
||||
${CLIENT_ROOT_DIR}/../ipc/ipc_interface.rep
|
||||
${CLIENT_ROOT_DIR}/../ipc/ipc_process_interface.rep
|
||||
)
|
||||
|
||||
target_link_libraries(test_common PUBLIC
|
||||
${LIBS}
|
||||
)
|
||||
|
||||
target_include_directories(test_common PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
${CMAKE_CURRENT_BINARY_DIR}/..
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
add_executable(test_import_export
|
||||
testAdminSelfHostedExport.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_import_export PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_multiple_imports
|
||||
testMultipleImports.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_multiple_imports PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_server_edit
|
||||
testServerEdit.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_server_edit PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_default_server_change
|
||||
testDefaultServerChange.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_default_server_change PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_server_edge_cases
|
||||
testServerEdgeCases.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_server_edge_cases PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_signal_order
|
||||
testSignalOrder.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_signal_order PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_servers_model_sync
|
||||
testServersModelSync.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_servers_model_sync PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_complex_operations
|
||||
testComplexOperations.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_complex_operations PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_settings_signals
|
||||
testSettingsSignals.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_settings_signals PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_ui_servers_model_and_controller
|
||||
testUiServersModelAndController.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_servers_model_and_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_self_hosted_server_setup
|
||||
testSelfHostedServerSetup.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_self_hosted_server_setup PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_pairing_parsers
|
||||
testPairingParsers.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_pairing_parsers PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME ImportExportTest COMMAND test_import_export)
|
||||
add_test(NAME MultipleImportsTest COMMAND test_multiple_imports)
|
||||
add_test(NAME ServerEditTest COMMAND test_server_edit)
|
||||
add_test(NAME DefaultServerChangeTest COMMAND test_default_server_change)
|
||||
add_test(NAME ServerEdgeCasesTest COMMAND test_server_edge_cases)
|
||||
add_test(NAME SignalOrderTest COMMAND test_signal_order)
|
||||
add_test(NAME ServersModelSyncTest COMMAND test_servers_model_sync)
|
||||
add_test(NAME ComplexOperationsTest COMMAND test_complex_operations)
|
||||
add_test(NAME SettingsSignalsTest COMMAND test_settings_signals)
|
||||
add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller)
|
||||
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
|
||||
add_test(NAME PairingParsersTest COMMAND test_pairing_parsers)
|
||||
@@ -1,147 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
class TestAdminSelfHostedExport : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QJsonObject decodeVpnKey(const QString &vpnKey) {
|
||||
QString key = vpnKey;
|
||||
key.replace("vpn://", "");
|
||||
|
||||
QByteArray ba = QByteArray::fromBase64(
|
||||
key.toUtf8(),
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals
|
||||
);
|
||||
|
||||
qDebug() << "Base64 decoded size:" << ba.size();
|
||||
|
||||
QJsonDocument testDoc = QJsonDocument::fromJson(ba);
|
||||
if (!testDoc.isNull()) {
|
||||
qDebug() << "Data is not compressed, using as-is";
|
||||
return testDoc.object();
|
||||
}
|
||||
|
||||
QByteArray baUncompressed = qUncompress(ba);
|
||||
if (!baUncompressed.isEmpty()) {
|
||||
qDebug() << "Data was compressed, uncompressed size:" << baUncompressed.size();
|
||||
ba = baUncompressed;
|
||||
} else {
|
||||
qDebug() << "qUncompress failed or data is not compressed";
|
||||
}
|
||||
|
||||
return QJsonDocument::fromJson(ba).object();
|
||||
}
|
||||
|
||||
QJsonObject sortContainers(const QJsonObject &config) {
|
||||
QJsonObject sorted = config;
|
||||
|
||||
if (!config.contains("containers")) {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
QJsonArray containers = config["containers"].toArray();
|
||||
QVector<QJsonObject> containerVec;
|
||||
|
||||
for (const QJsonValue &val : containers) {
|
||||
containerVec.append(val.toObject());
|
||||
}
|
||||
|
||||
std::sort(containerVec.begin(), containerVec.end(), [](const QJsonObject &a, const QJsonObject &b) {
|
||||
return a["container"].toString() < b["container"].toString();
|
||||
});
|
||||
|
||||
QJsonArray sortedContainers;
|
||||
for (const QJsonObject &obj : containerVec) {
|
||||
sortedContainers.append(obj);
|
||||
}
|
||||
|
||||
sorted["containers"] = sortedContainers;
|
||||
return sorted;
|
||||
}
|
||||
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
}
|
||||
|
||||
void testAdminSelfHostedExport() {
|
||||
QString vpnKey = "vpn://AAABTXjarZIxT8MwEIX_Cro5jbDjQunKUhhYyoZQZZKjRGpsy3baQtT_zp2bJh3oACLLPfvz3bOe00FpTdS1QR9g_tKB3q1h3sFCwBzEdf9N5ElBBgtJqBiQOkcFoemAbs6RInQ7oNkZemAvrrKvRV9VX6fH-lhSVSwavU9GSdcmXZX0UqSbseJRMqlioDxuSsJZH1mKWTrhvI22tJvVljKoLU-TtB3aN4NxpavKYwhpSD7LRc4t0WsTeMwqNRNsKweHbAyTtnRj8KvWE0pUEut-hNah2TpDM0-Kwu8vKMSd-ttFLrntao_rVvuKWkc9OnIk4n8t915_Ulcqo5FSxa9tYsk2rxlU-K7bTby_lDWfCKWvXTy-5jOGeLVET-9L7MOG-KQbJEBx57jXjdtgXtqG_wUdws5yJhCpa1iefhopM2gD-n4An-ElHL4BvzD6nw";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
qDebug() << "IMPORTED KEY:" << vpnKey;
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(vpnKey);
|
||||
|
||||
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import should succeed");
|
||||
QVERIFY2(!importResult.config.isEmpty(), "Config should not be empty");
|
||||
|
||||
QJsonObject importedConfig = importResult.config;
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importedConfig);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
|
||||
|
||||
const QString serverId = m_coreController->m_serversRepository->defaultServerId();
|
||||
auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverId);
|
||||
|
||||
QVERIFY2(exportResult.errorCode == ErrorCode::NoError, "Export should succeed");
|
||||
QVERIFY2(!exportResult.config.isEmpty(), "Exported config should not be empty");
|
||||
|
||||
qDebug() << "EXPORTED KEY:" << exportResult.config;
|
||||
|
||||
QJsonObject exportedConfig = decodeVpnKey(exportResult.config);
|
||||
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(exportResult.config);
|
||||
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Re-import should succeed");
|
||||
|
||||
QJsonObject sortedImported = sortContainers(importedConfig);
|
||||
QJsonObject sortedExported = sortContainers(importResult2.config);
|
||||
|
||||
QString importedJson = QJsonDocument(sortedImported).toJson(QJsonDocument::Compact);
|
||||
QString exportedJson = QJsonDocument(sortedExported).toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "IMPORTED JSON:" << importedJson;
|
||||
qDebug() << "EXPORTED JSON:" << exportedJson;
|
||||
|
||||
QCOMPARE(exportedJson, importedJson);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestAdminSelfHostedExport)
|
||||
#include "testAdminSelfHostedExport.moc"
|
||||
@@ -1,111 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestComplexOperations : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testComplexOperationSequence() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 3, "importFinished should be emitted 3 times");
|
||||
QVERIFY2(serverAddedSpy.count() == 3, "serverAdded should be emitted 3 times");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged should be emitted 2 times (0->1, 1->2, first import doesn't emit as default is already 0)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited First Server"));
|
||||
|
||||
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited should be emitted");
|
||||
QString editedDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(editedDesc0 == "Edited First Server", "First server should be edited");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 1)");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 4, "defaultServerChanged should be emitted again");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Final Edited Server"));
|
||||
|
||||
QVERIFY2(serverEditedSpy.count() == 2, "serverEdited should be emitted again");
|
||||
QString finalDesc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(finalDesc0 == "Final Edited Server", "First server should be edited again");
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Final servers count should be 2");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Final default index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 2, "Model should have 2 rows");
|
||||
QString modelDesc0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc0 == "Final Edited Server", "Model should reflect final edited name");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestComplexOperations)
|
||||
#include "testComplexOperations.moc"
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestDefaultServerChange : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSetDefaultServerIndex() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversController->getServerId(0),
|
||||
"defaultServerChanged should emit new default server id");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
int modelDefaultIndex = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool() ? 0 : -1;
|
||||
QVERIFY2(modelDefaultIndex == 0, "Model should reflect default server");
|
||||
}
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(2));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted again");
|
||||
QVERIFY2(defaultServerChangedSpy.at(1).at(0).toString() == m_coreController->m_serversController->getServerId(2),
|
||||
"defaultServerChanged should emit new default server id");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default server index should be 2");
|
||||
}
|
||||
|
||||
void testDefaultServerChangeOnRemoveEdgeCases() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 0)");
|
||||
|
||||
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(1));
|
||||
QVERIFY2(desc1 == "Xray Server", "First remaining server should be Xray");
|
||||
QVERIFY2(desc2 == "WireGuard Server", "Second remaining server should be WireGuard");
|
||||
|
||||
defaultServerChangedSpy.clear();
|
||||
serverRemovedSpy.clear();
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0 (was 1, removed 0)");
|
||||
|
||||
QString lastDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(lastDesc == "WireGuard Server", "Last server should be WireGuard");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestDefaultServerChange)
|
||||
#include "testDefaultServerChange.moc"
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestMultipleImports : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testMultipleImports() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
|
||||
}
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted once");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After first import servers count should be 1");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After first import model row count should be 1");
|
||||
}
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "First server should be default");
|
||||
|
||||
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(desc1 == "AWG Server", "First server description should match");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc1 == "AWG Server", "First server description in model should match");
|
||||
}
|
||||
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After second import servers count should be 2");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 2, "After second import model row count should be 2");
|
||||
}
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
|
||||
|
||||
QString desc2 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(1));
|
||||
QVERIFY2(desc2 == "Xray Server", "Second server description should match");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc2 == "Xray Server", "Second server description in model should match");
|
||||
}
|
||||
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
QVERIFY2(importResult3.errorCode == ErrorCode::NoError, "Third import should succeed");
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 3, "importFinished signal should be emitted three times");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted twice (0->1, 1->2, first import doesn't emit)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "After third import servers count should be 3");
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "After third import model row count should be 3");
|
||||
}
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Third server should be default");
|
||||
|
||||
QString desc3 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(2));
|
||||
QVERIFY2(desc3 == "WireGuard Server", "Third server description should match");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc3 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc3 == "WireGuard Server", "Third server description in model should match");
|
||||
}
|
||||
}
|
||||
|
||||
void testMultipleImportsRemoval() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After two imports servers count should be 2");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
|
||||
|
||||
QString desc0 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QString desc1 = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(1));
|
||||
QVERIFY2(desc0 == "AWG Server", "First server description should match");
|
||||
QVERIFY2(desc1 == "Xray Server", "Second server description should match");
|
||||
|
||||
defaultServerChangedSpy.clear();
|
||||
serverRemovedSpy.clear();
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After removing first server, servers count should be 1");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing first server, default index should be 0");
|
||||
|
||||
QString remainingDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(remainingDesc == "Xray Server", "Remaining server should be Xray Server");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After removing first server, model row count should be 1");
|
||||
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc == "Xray Server", "Remaining server description in model should match");
|
||||
}
|
||||
|
||||
defaultServerChangedSpy.clear();
|
||||
serverRemovedSpy.clear();
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "After removing last server, servers count should be 0");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing last server, default index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "After removing last server, model row count should be 0");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestMultipleImports)
|
||||
#include "testMultipleImports.moc"
|
||||
@@ -1,165 +0,0 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "core/controllers/api/pairingController.h"
|
||||
#include "ui/controllers/api/pairingUiController.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestPairingParsers : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void generateQr_success_extractsConfigAndMeta()
|
||||
{
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
QJsonObject o;
|
||||
o[apiDefs::key::config] = QStringLiteral("vpn://dummy");
|
||||
o[apiDefs::key::serviceInfo] = QJsonObject { { QStringLiteral("is_ad_visible"), false } };
|
||||
o[apiDefs::key::supportedProtocols] = QJsonArray { QStringLiteral("awg") };
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::NoError);
|
||||
QCOMPARE(out.config, QStringLiteral("vpn://dummy"));
|
||||
QCOMPARE(out.supportedProtocols.size(), 1);
|
||||
}
|
||||
|
||||
void generateQr_http408()
|
||||
{
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 408;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Request Timeout");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::ApiConfigTimeoutError);
|
||||
QVERIFY(out.config.isEmpty());
|
||||
}
|
||||
|
||||
void generateQr_http429()
|
||||
{
|
||||
PairingController::QrPairingConfigPayload out;
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 429;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Too Many Requests");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseGenerateQrResponseBody(body, out), ErrorCode::ApiPairingRateLimitedError);
|
||||
}
|
||||
|
||||
void scanQr_messageOk()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("OK");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::NoError);
|
||||
}
|
||||
|
||||
void scanQr_messageOk_extractsDeviceName()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("OK");
|
||||
o[QStringLiteral("device_name")] = QStringLiteral("TestPhone");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
QString name;
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body, &name), ErrorCode::NoError);
|
||||
QCOMPARE(name, QStringLiteral("TestPhone"));
|
||||
}
|
||||
|
||||
void scanQr_deviceLimitMessage()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Device limit reached for subscription");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiConfigLimitError);
|
||||
}
|
||||
|
||||
void scanQr_http403()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 403;
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingForbiddenError);
|
||||
}
|
||||
|
||||
void scanQr_http409()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("http_status")] = 409;
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingConflictError);
|
||||
}
|
||||
|
||||
void scanQr_notFoundMessage()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("Session not found");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiNotFoundError);
|
||||
}
|
||||
|
||||
void scanQr_qrSessionExpiredMessage()
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("message")] = QStringLiteral("QR session not found or expired");
|
||||
const QByteArray body = QJsonDocument(o).toJson();
|
||||
|
||||
QCOMPARE(PairingController::parseScanQrResponseBody(body), ErrorCode::ApiPairingSessionExpiredError);
|
||||
}
|
||||
|
||||
void validateScanFields_oversizedVpnKey()
|
||||
{
|
||||
QString vpnKey;
|
||||
vpnKey.fill(QLatin1Char('x'), 256 * 1024 + 1);
|
||||
QCOMPARE(PairingController::validatePairingScanFields(QStringLiteral("ab"), vpnKey, QStringLiteral("k"),
|
||||
QStringLiteral("amnezia-premium"), QStringLiteral("ru")),
|
||||
ErrorCode::ApiPairingPayloadTooLargeError);
|
||||
}
|
||||
|
||||
void validateScanFields_uuidTooLong()
|
||||
{
|
||||
QString uuid(200, QLatin1Char('a'));
|
||||
QCOMPARE(PairingController::validatePairingScanFields(uuid, QStringLiteral("vpn://a"), QStringLiteral("k"),
|
||||
QStringLiteral("amnezia-premium"), QStringLiteral("ru")),
|
||||
ErrorCode::ApiConfigEmptyError);
|
||||
}
|
||||
|
||||
void validateScanFields_missingServiceType()
|
||||
{
|
||||
QCOMPARE(PairingController::validatePairingScanFields(QStringLiteral("ab"), QStringLiteral("vpn://x"),
|
||||
QStringLiteral("k"), QString(),
|
||||
QStringLiteral("ru")),
|
||||
ErrorCode::ApiPairingMissingMetadataError);
|
||||
}
|
||||
|
||||
void pairingUi_applyScanned_extractsUuid_emitsSignal()
|
||||
{
|
||||
PairingUiController ctl(nullptr, nullptr, nullptr, nullptr);
|
||||
QSignalSpy spy(&ctl, &PairingUiController::pairingUuidFromScan);
|
||||
const QString u = QStringLiteral("123e4567-e89b-12d3-a456-426614174000");
|
||||
QVERIFY(ctl.applyScannedTextAsPairingUuid(QStringLiteral("prefix ") + u + QStringLiteral(" suffix")));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(spy.first().first().toString(), u);
|
||||
}
|
||||
|
||||
void pairingUi_applyScanned_rejectsVpnKey()
|
||||
{
|
||||
PairingUiController ctl(nullptr, nullptr, nullptr, nullptr);
|
||||
QSignalSpy spy(&ctl, &PairingUiController::pairingUuidFromScan);
|
||||
QVERIFY(!ctl.applyScannedTextAsPairingUuid(QStringLiteral("vpn://AAAA")));
|
||||
QCOMPARE(spy.count(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestPairingParsers)
|
||||
#include "testPairingParsers.moc"
|
||||
@@ -1,385 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QDebug>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/awgProtocolConfig.h"
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestSelfHostedServerSetup : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
ServerCredentials getCredentialsFromEnv() {
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
|
||||
QString hostName = env.value("TEST_SERVER_HOST");
|
||||
QString userName = env.value("TEST_SERVER_USER");
|
||||
QString password = env.value("TEST_SERVER_PASSWORD");
|
||||
QString portStr = env.value("TEST_SERVER_PORT", "22");
|
||||
int port = portStr.toInt();
|
||||
|
||||
ServerCredentials credentials;
|
||||
credentials.hostName = hostName;
|
||||
credentials.userName = userName;
|
||||
credentials.secretData = password;
|
||||
credentials.port = port;
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
void verifySshConnection(const ServerCredentials& credentials) {
|
||||
QString sshOutput;
|
||||
ErrorCode sshError = m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
|
||||
QVERIFY2(sshError == ErrorCode::NoError,
|
||||
QString("SSH connection should succeed. Error: %1, Output: %2")
|
||||
.arg(static_cast<int>(sshError))
|
||||
.arg(sshOutput)
|
||||
.toUtf8().constData());
|
||||
qDebug() << "SSH connection successful. Output:" << sshOutput;
|
||||
}
|
||||
|
||||
void verifyAdminAccess(int serverIndex)
|
||||
{
|
||||
const QString serverId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
const auto adminCfg = m_coreController->m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
QVERIFY2(adminCfg.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
|
||||
const SelfHostedAdminServerConfig &selfHosted = *adminCfg;
|
||||
|
||||
QVERIFY2(selfHosted.hasCredentials(),
|
||||
"Server should have credentials (admin access)");
|
||||
|
||||
QVERIFY2(!selfHosted.userName.isEmpty(),
|
||||
"Server should have userName for admin access");
|
||||
|
||||
QVERIFY2(!selfHosted.password.isEmpty(),
|
||||
"Server should have password for admin access");
|
||||
|
||||
QVERIFY2(!selfHosted.isReadOnly(),
|
||||
"Server should not be read-only (should have admin access)");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
bool hasWriteAccess = m_coreController->m_serversModel->data(
|
||||
m_coreController->m_serversModel->index(serverIndex, 0),
|
||||
ServersModel::HasWriteAccessRole
|
||||
).toBool();
|
||||
|
||||
QVERIFY2(hasWriteAccess,
|
||||
"Server should have write access (admin access) according to ServersModel");
|
||||
}
|
||||
|
||||
qDebug() << "Admin access verified for server at index:" << serverIndex;
|
||||
}
|
||||
|
||||
void verifyClientConfig(const ContainerConfig& containerConfig, DockerContainer container) {
|
||||
QString containerName = ContainerUtils::containerToString(container);
|
||||
qDebug() << "Checking container:" << containerName;
|
||||
|
||||
if (ContainerUtils::containerService(container) != ServiceType::Other) {
|
||||
bool hasClientConfig = containerConfig.protocolConfig.hasClientConfig();
|
||||
|
||||
QVERIFY2(hasClientConfig,
|
||||
QString("Container %1 should have client config initialized")
|
||||
.arg(containerName)
|
||||
.toUtf8().constData());
|
||||
|
||||
if (container == DockerContainer::Awg) {
|
||||
const AwgProtocolConfig* awgProtocolConfig = containerConfig.protocolConfig.as<AwgProtocolConfig>();
|
||||
QVERIFY2(awgProtocolConfig != nullptr, "Protocol config should be AwgProtocolConfig");
|
||||
QVERIFY2(awgProtocolConfig->hasClientConfig(), "AwgProtocolConfig should have client config");
|
||||
const std::optional<AwgClientConfig>& clientCfgOpt = awgProtocolConfig->clientConfig;
|
||||
QVERIFY2(clientCfgOpt.has_value(), "Awg client config should exist");
|
||||
|
||||
const AwgClientConfig& awgClientConfig = *clientCfgOpt;
|
||||
QVERIFY2(!awgClientConfig.hostName.isEmpty(), "Awg client config should have hostName");
|
||||
QVERIFY2(awgClientConfig.port > 0, "Awg client config should have valid port");
|
||||
QVERIFY2(!awgClientConfig.clientPrivateKey.isEmpty(), "Awg client config should have clientPrivateKey");
|
||||
QVERIFY2(!awgClientConfig.clientPublicKey.isEmpty(), "Awg client config should have clientPublicKey");
|
||||
QVERIFY2(!awgClientConfig.serverPublicKey.isEmpty(), "Awg client config should have serverPublicKey");
|
||||
QVERIFY2(!awgClientConfig.clientId.isEmpty(), "Awg client config should have clientId");
|
||||
QVERIFY2(!awgClientConfig.nativeConfig.isEmpty(), "Awg client config should have nativeConfig");
|
||||
}
|
||||
|
||||
qDebug() << "Container" << containerName << "has valid client config initialized";
|
||||
} else {
|
||||
qDebug() << "Container" << containerName << "is service type Other, skipping client config check";
|
||||
}
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSelfHostedServerSetup() {
|
||||
ServerCredentials credentials = getCredentialsFromEnv();
|
||||
|
||||
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
|
||||
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
|
||||
}
|
||||
|
||||
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
|
||||
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
|
||||
|
||||
verifySshConnection(credentials);
|
||||
|
||||
int awgPort = 55424;
|
||||
TransportProto awgTransportProto = TransportProto::Udp;
|
||||
bool wasAwgInstalled = false;
|
||||
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
ErrorCode installServerError = m_coreController->m_installController->installServer(
|
||||
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
|
||||
|
||||
QVERIFY2(installServerError == ErrorCode::NoError,
|
||||
QString("installServer for Awg should succeed. Error: %1")
|
||||
.arg(static_cast<int>(installServerError))
|
||||
.toUtf8().constData());
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
|
||||
qDebug() << "Server with Awg container added at index:" << serverIndex;
|
||||
|
||||
const auto adminAfterAwg = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminAfterAwg.has_value(), "Server should be self-hosted (admin)");
|
||||
const SelfHostedAdminServerConfig *selfHostedAfterAwg = &(*adminAfterAwg);
|
||||
QVERIFY2(selfHostedAfterAwg->defaultContainer == DockerContainer::Awg, "Default container should be Awg");
|
||||
QVERIFY2(selfHostedAfterAwg->containers.contains(DockerContainer::Awg), "Server should have Awg container");
|
||||
|
||||
ContainerConfig awgConfig = selfHostedAfterAwg->containers.value(DockerContainer::Awg);
|
||||
QVERIFY2(awgConfig.container == DockerContainer::Awg, "Awg container config should be valid");
|
||||
QVERIFY2(selfHostedAfterAwg->containers.size() == 1,
|
||||
QString("Server should have exactly 1 container after Awg installation, but has %1")
|
||||
.arg(selfHostedAfterAwg->containers.size())
|
||||
.toUtf8().constData());
|
||||
verifyClientConfig(awgConfig, DockerContainer::Awg);
|
||||
|
||||
qDebug() << "Awg container installed and configured successfully with valid client config";
|
||||
|
||||
int dnsPort = 53;
|
||||
TransportProto dnsTransportProto = TransportProto::Udp;
|
||||
bool wasDnsInstalled = false;
|
||||
|
||||
const QString serverIdForOps = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
ErrorCode installContainerError = m_coreController->m_installController->installContainer(
|
||||
serverIdForOps, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled);
|
||||
|
||||
QVERIFY2(installContainerError == ErrorCode::NoError,
|
||||
QString("installContainer for Dns should succeed. Error: %1")
|
||||
.arg(static_cast<int>(installContainerError))
|
||||
.toUtf8().constData());
|
||||
qDebug() << "Dns container installed:" << wasDnsInstalled;
|
||||
|
||||
const auto adminAfterDns = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminAfterDns.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *selfHostedAfterDns = &(*adminAfterDns);
|
||||
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Awg), "Server should still have Awg container");
|
||||
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Dns), "Server should have Dns container");
|
||||
QVERIFY2(selfHostedAfterDns->containers.size() == 2,
|
||||
QString("Server should have exactly 2 containers after Dns installation, but has %1")
|
||||
.arg(selfHostedAfterDns->containers.size())
|
||||
.toUtf8().constData());
|
||||
|
||||
ContainerConfig dnsConfig = selfHostedAfterDns->containers.value(DockerContainer::Dns);
|
||||
QVERIFY2(dnsConfig.container == DockerContainer::Dns, "Dns container config should be valid");
|
||||
|
||||
const DnsProtocolConfig* dnsProtocolConfig = dnsConfig.protocolConfig.as<DnsProtocolConfig>();
|
||||
QVERIFY2(dnsProtocolConfig != nullptr, "Protocol config should be DnsProtocolConfig");
|
||||
|
||||
qDebug() << "Dns container installed and configured successfully";
|
||||
|
||||
verifyAdminAccess(serverIndex);
|
||||
|
||||
qDebug() << "Test completed successfully. Server setup with Awg and Dns containers is complete.";
|
||||
}
|
||||
|
||||
void testSelfHostedServerEmptyRecover() {
|
||||
ServerCredentials credentials = getCredentialsFromEnv();
|
||||
|
||||
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
|
||||
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
|
||||
}
|
||||
|
||||
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
|
||||
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
|
||||
|
||||
verifySshConnection(credentials);
|
||||
|
||||
SelfHostedAdminServerConfig serverConfig;
|
||||
serverConfig.hostName = credentials.hostName;
|
||||
serverConfig.userName = credentials.userName;
|
||||
serverConfig.password = credentials.secretData;
|
||||
serverConfig.port = credentials.port;
|
||||
serverConfig.description = m_coreController->m_appSettingsRepository->nextAvailableServerName();
|
||||
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
|
||||
serverConfig.defaultContainer = DockerContainer::None;
|
||||
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
m_coreController->m_serversRepository->addServer(QString(), serverConfig.toJson(),
|
||||
serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
|
||||
qDebug() << "Empty server added at index:" << serverIndex;
|
||||
|
||||
const auto addedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(addedAdmin.has_value(), "Added server should be self-hosted admin");
|
||||
const SelfHostedAdminServerConfig *selfHosted = &(*addedAdmin);
|
||||
QVERIFY2(selfHosted->containers.isEmpty(), "Server should have no containers initially");
|
||||
QVERIFY2(selfHosted->defaultContainer == DockerContainer::None, "Default container should be None");
|
||||
|
||||
const QString scanServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(scanServerId);
|
||||
QVERIFY2(scanError == ErrorCode::NoError,
|
||||
QString("Server scan should succeed. Error: %1")
|
||||
.arg(static_cast<int>(scanError))
|
||||
.toUtf8().constData());
|
||||
qDebug() << "Server scan completed successfully";
|
||||
|
||||
const auto scannedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(scannedAdmin.has_value(), "Scanned server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *scannedSelfHosted = &(*scannedAdmin);
|
||||
|
||||
QMap<DockerContainer, ContainerConfig> containers = scannedSelfHosted->containers;
|
||||
int containersCount = containers.size();
|
||||
qDebug() << "Found containers count:" << containersCount;
|
||||
|
||||
QVERIFY2(containersCount >= 0,
|
||||
QString("Containers count should be non-negative, but got %1")
|
||||
.arg(containersCount)
|
||||
.toUtf8().constData());
|
||||
|
||||
if (containersCount > 0) {
|
||||
qDebug() << "Server has" << containersCount << "installed container(s)";
|
||||
} else {
|
||||
qDebug() << "Server has no installed containers";
|
||||
}
|
||||
|
||||
for (auto it = containers.begin(); it != containers.end(); ++it) {
|
||||
verifyClientConfig(it.value(), it.key());
|
||||
}
|
||||
|
||||
QVERIFY2(scannedSelfHosted->containers.size() == containersCount,
|
||||
QString("Scanned containers count should match. Expected: %1, Actual: %2")
|
||||
.arg(containersCount)
|
||||
.arg(scannedSelfHosted->containers.size())
|
||||
.toUtf8().constData());
|
||||
|
||||
verifyAdminAccess(serverIndex);
|
||||
|
||||
qDebug() << "Test completed successfully. Server has admin access and all containers are initialized.";
|
||||
}
|
||||
|
||||
void testRemoveAllContainers() {
|
||||
ServerCredentials credentials = getCredentialsFromEnv();
|
||||
|
||||
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
|
||||
QSKIP("Test requires TEST_SERVER_HOST, TEST_SERVER_USER, TEST_SERVER_PASSWORD environment variables");
|
||||
}
|
||||
|
||||
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
|
||||
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
|
||||
|
||||
verifySshConnection(credentials);
|
||||
|
||||
int awgPort = 55424;
|
||||
TransportProto awgTransportProto = TransportProto::Udp;
|
||||
bool wasAwgInstalled = false;
|
||||
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
ErrorCode installServerError = m_coreController->m_installController->installServer(
|
||||
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
|
||||
|
||||
QVERIFY2(installServerError == ErrorCode::NoError,
|
||||
QString("installServer for Awg should succeed. Error: %1")
|
||||
.arg(static_cast<int>(installServerError))
|
||||
.toUtf8().constData());
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
|
||||
qDebug() << "Server with Awg container added at index:" << serverIndex;
|
||||
|
||||
const auto adminBeforeRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminBeforeRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *selfHostedBeforeRemoval = &(*adminBeforeRemoval);
|
||||
QVERIFY2(!selfHostedBeforeRemoval->containers.isEmpty(), "Server should have containers before removal");
|
||||
QVERIFY2(selfHostedBeforeRemoval->defaultContainer != DockerContainer::None, "Server should have default container before removal");
|
||||
|
||||
qDebug() << "Containers before removal:" << selfHostedBeforeRemoval->containers.size();
|
||||
|
||||
const QString removeServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
|
||||
ErrorCode removeError = m_coreController->m_installController->removeAllContainers(removeServerId);
|
||||
QVERIFY2(removeError == ErrorCode::NoError,
|
||||
QString("removeAllContainers should succeed. Error: %1")
|
||||
.arg(static_cast<int>(removeError))
|
||||
.toUtf8().constData());
|
||||
qDebug() << "All containers removed successfully";
|
||||
|
||||
const auto adminAfterRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
|
||||
m_coreController->m_serversRepository->serverIdAt(serverIndex));
|
||||
QVERIFY2(adminAfterRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
|
||||
const SelfHostedAdminServerConfig *selfHostedAfterRemoval = &(*adminAfterRemoval);
|
||||
|
||||
QVERIFY2(selfHostedAfterRemoval->containers.isEmpty(),
|
||||
"Server should have no containers after removal");
|
||||
QVERIFY2(selfHostedAfterRemoval->defaultContainer == DockerContainer::None,
|
||||
"Default container should be None after removal");
|
||||
|
||||
qDebug() << "Containers after removal:" << selfHostedAfterRemoval->containers.size();
|
||||
|
||||
verifyAdminAccess(serverIndex);
|
||||
|
||||
qDebug() << "Test completed successfully. All containers removed and server is empty.";
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSelfHostedServerSetup)
|
||||
#include "testSelfHostedServerSetup.moc"
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestServerEdgeCases : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testInvalidIndexOperations() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
|
||||
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(-1));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(10));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(100));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server count should remain 1");
|
||||
|
||||
const QString validServerId = m_coreController->m_serversController->getServerId(0);
|
||||
const serverConfigUtils::ConfigType editKind =
|
||||
m_coreController->m_serversRepository->serverKind(validServerId);
|
||||
|
||||
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(-1),
|
||||
QJsonObject(), editKind);
|
||||
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(10),
|
||||
QJsonObject(), editKind);
|
||||
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(-1));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(10));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should remain 0");
|
||||
}
|
||||
|
||||
void testEmptyRepositoryOperations() {
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Should start with 0 servers");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for empty repository");
|
||||
|
||||
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(0),
|
||||
SelfHostedAdminServerConfig {}.toJson(),
|
||||
serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for empty repository");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for empty repository");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0 for empty repository");
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Server count should remain 0");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestServerEdgeCases)
|
||||
#include "testServerEdgeCases.moc"
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestServerEdit : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testServerEditTriggersHandlers() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "Import should succeed");
|
||||
|
||||
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited AWG Server"));
|
||||
|
||||
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted");
|
||||
QVERIFY2(serverEditedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->serverIdAt(0),
|
||||
"serverEdited should emit edited server id");
|
||||
|
||||
const QString editedDesc = amnezia::test::serverDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversRepository->serverIdAt(0));
|
||||
QVERIFY2(editedDesc == "Edited AWG Server", "Server description should be updated");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc == "Edited AWG Server", "Server description in model should be updated");
|
||||
}
|
||||
}
|
||||
|
||||
void testServerEditPreservesDefault() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server should be index 1");
|
||||
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(1),
|
||||
QStringLiteral("Edited Default Server"));
|
||||
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing default server");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited Non-Default Server"));
|
||||
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing non-default server");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestServerEdit)
|
||||
#include "testServerEdit.moc"
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
#ifndef TESTSERVERREPOSITORYHELPERS_H
|
||||
#define TESTSERVERREPOSITORYHELPERS_H
|
||||
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
namespace amnezia::test
|
||||
{
|
||||
|
||||
inline QString serverDescription(SecureServersRepository *repo, const QString &serverId)
|
||||
{
|
||||
switch (repo->serverKind(serverId)) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
const auto cfg = repo->selfHostedAdminConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
const auto cfg = repo->selfHostedUserConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
const auto cfg = repo->nativeConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV3:
|
||||
case serverConfigUtils::ConfigType::ExternalPremium: {
|
||||
const auto cfg = repo->apiV2Config(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV2: {
|
||||
const auto cfg = repo->legacyApiConfig(serverId);
|
||||
return cfg.has_value() ? cfg->description : QString();
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Invalid:
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
inline void setServerDescription(SecureServersRepository *repo, const QString &serverId, const QString &description)
|
||||
{
|
||||
const serverConfigUtils::ConfigType kind = repo->serverKind(serverId);
|
||||
switch (kind) {
|
||||
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
|
||||
auto cfg = repo->selfHostedAdminConfig(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::SelfHostedUser: {
|
||||
auto cfg = repo->selfHostedUserConfig(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::Native: {
|
||||
auto cfg = repo->nativeConfig(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV3:
|
||||
case serverConfigUtils::ConfigType::ExternalPremium: {
|
||||
auto cfg = repo->apiV2Config(serverId);
|
||||
if (!cfg.has_value()) return;
|
||||
cfg->description = description;
|
||||
cfg->displayName = description;
|
||||
repo->editServer(serverId, cfg->toJson(), kind);
|
||||
return;
|
||||
}
|
||||
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
|
||||
case serverConfigUtils::ConfigType::AmneziaFreeV2:
|
||||
case serverConfigUtils::ConfigType::Invalid:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace amnezia::test
|
||||
|
||||
#endif
|
||||
@@ -1,112 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "tests/testServerRepositoryHelpers.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestServersModelSync : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testServersModelSyncOnOperations() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
if (!m_coreController->m_serversModel) {
|
||||
QSKIP("ServersModel not available");
|
||||
}
|
||||
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "Model should have 1 row after import");
|
||||
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc1 == "AWG Server", "Model should have correct server name");
|
||||
|
||||
amnezia::test::setServerDescription(m_coreController->m_serversRepository,
|
||||
m_coreController->m_serversController->getServerId(0),
|
||||
QStringLiteral("Edited AWG Server"));
|
||||
|
||||
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
|
||||
QVERIFY2(modelDesc2 == "Edited AWG Server", "Model should be updated after edit");
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Model should have 0 rows after removal");
|
||||
}
|
||||
|
||||
void testServersModelDefaultIndexSync() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
QString wgKey = "vpn://AAAAwXjahY89a8NADIb_StDsHLFDIHjt0C1LhgwlBNWnpgfx3SHp6hDj_15dacnYTS_Po68ZhhQVQyQW6N_mZ4QecIz0CLieAtO1IHto4Fn3M-TEat6u3XetMSnvkfSC3jOJjYN24_audRtjyhil-pfMSZPB4jMsy7kBTx9Ybvryz2ZPMnDIGlI042TktZLVkfjLmhr4TKIHHMnodHV0xzHfyA1pNJZRZEr1alAS_Yvbin6e6LoGihD_DqhSjbB8AyB_ZI8";
|
||||
|
||||
if (!m_coreController->m_serversModel) {
|
||||
QSKIP("ServersModel not available");
|
||||
}
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult3.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "Model should have 3 rows");
|
||||
|
||||
bool isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
|
||||
bool isDefault1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::IsDefaultRole).toBool();
|
||||
bool isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
|
||||
|
||||
QVERIFY2(!isDefault0, "Server 0 should not be default");
|
||||
QVERIFY2(!isDefault1, "Server 1 should not be default");
|
||||
QVERIFY2(isDefault2, "Server 2 should be default");
|
||||
|
||||
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
|
||||
|
||||
isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
|
||||
isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
|
||||
|
||||
QVERIFY2(isDefault0, "Server 0 should be default after change");
|
||||
QVERIFY2(!isDefault2, "Server 2 should not be default after change");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestServersModelSync)
|
||||
#include "testServersModelSync.moc"
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
#include <QLocale>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/models/allowedDnsModel.h"
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/appSplitTunnelingModel.h"
|
||||
#include "ui/models/languageModel.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
class TestSettingsSignals : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
}
|
||||
|
||||
void testDnsSettingsSignals() {
|
||||
QSignalSpy primaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::primaryDnsChanged);
|
||||
QSignalSpy secondaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::secondaryDnsChanged);
|
||||
QSignalSpy allowedDnsServersChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::allowedDnsServersChanged);
|
||||
|
||||
QString primaryDns = "8.8.8.8";
|
||||
QString secondaryDns = "8.8.4.4";
|
||||
|
||||
m_coreController->m_settingsUiController->setPrimaryDns(primaryDns);
|
||||
QVERIFY2(primaryDnsChangedSpy.count() == 1, "primaryDnsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->getPrimaryDns() == primaryDns, "Primary DNS should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->getPrimaryDns() == primaryDns, "Primary DNS should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->primaryDns() == primaryDns, "Primary DNS should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->setSecondaryDns(secondaryDns);
|
||||
QVERIFY2(secondaryDnsChangedSpy.count() == 1, "secondaryDnsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->getSecondaryDns() == secondaryDns, "Secondary DNS should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->getSecondaryDns() == secondaryDns, "Secondary DNS should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->secondaryDns() == secondaryDns, "Secondary DNS should be available in SecureAppSettingsRepository");
|
||||
|
||||
QStringList dnsList = {"1.1.1.1", "1.0.0.1"};
|
||||
m_coreController->m_allowedDnsController->addDnsList(dnsList, true);
|
||||
QVERIFY2(allowedDnsServersChangedSpy.count() == 1, "allowedDnsServersChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->getAllowedDnsServers() == dnsList, "Allowed DNS servers should be updated in SecureAppSettingsRepository");
|
||||
QVERIFY2(m_coreController->m_allowedDnsController->getCurrentDnsServers() == dnsList, "Allowed DNS servers should be available in AllowedDnsController");
|
||||
|
||||
QVERIFY2(m_coreController->m_allowedDnsUiController != nullptr, "AllowedDnsUiController should exist");
|
||||
QVERIFY2(m_coreController->m_allowedDnsModel != nullptr, "AllowedDnsModel should exist");
|
||||
|
||||
QStringList modelDnsList;
|
||||
for (int i = 0; i < m_coreController->m_allowedDnsModel->rowCount(); ++i) {
|
||||
modelDnsList.append(m_coreController->m_allowedDnsModel->data(m_coreController->m_allowedDnsModel->index(i, 0), AllowedDnsModel::IpRole).toString());
|
||||
}
|
||||
QVERIFY2(modelDnsList == dnsList, "Allowed DNS servers should be available in AllowedDnsModel");
|
||||
}
|
||||
|
||||
void testAmneziaDnsToggleSignal() {
|
||||
QSignalSpy amneziaDnsToggledSpy(m_coreController->m_settingsUiController, &SettingsUiController::amneziaDnsToggled);
|
||||
QSignalSpy useAmneziaDnsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::useAmneziaDnsChanged);
|
||||
|
||||
bool initialValue = m_coreController->m_settingsController->isAmneziaDnsEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAmneziaDns(!initialValue);
|
||||
QVERIFY2(amneziaDnsToggledSpy.count() == 1, "amneziaDnsToggled signal should be emitted");
|
||||
QVERIFY2(amneziaDnsToggledSpy.at(0).at(0).toBool() == !initialValue, "amneziaDnsToggled should emit correct value");
|
||||
QVERIFY2(useAmneziaDnsChangedSpy.count() == 1, "useAmneziaDnsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->useAmneziaDns() == !initialValue, "Amnezia DNS state should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAmneziaDns(initialValue);
|
||||
QVERIFY2(amneziaDnsToggledSpy.count() == 2, "amneziaDnsToggled signal should be emitted again");
|
||||
QVERIFY2(useAmneziaDnsChangedSpy.count() == 2, "useAmneziaDnsChanged signal should be emitted again");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == initialValue, "Amnezia DNS state should be restored in SettingsUiController");
|
||||
}
|
||||
|
||||
void testLoggingSignals() {
|
||||
QSignalSpy loggingStateChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::loggingStateChanged);
|
||||
QSignalSpy saveLogsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged);
|
||||
|
||||
bool initialLogging = m_coreController->m_settingsController->isLoggingEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleLogging(!initialLogging);
|
||||
QVERIFY2(loggingStateChangedSpy.count() == 1, "loggingStateChanged signal should be emitted");
|
||||
QVERIFY2(saveLogsChangedSpy.count() == 1, "saveLogsChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->isLoggingEnabled() == !initialLogging, "Logging state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == !initialLogging, "Logging state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isSaveLogs() == !initialLogging, "Logging state should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->toggleLogging(initialLogging);
|
||||
QVERIFY2(loggingStateChangedSpy.count() == 2, "loggingStateChanged signal should be emitted again");
|
||||
QVERIFY2(saveLogsChangedSpy.count() == 2, "saveLogsChanged signal should be emitted again");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == initialLogging, "Logging state should be restored in SettingsUiController");
|
||||
}
|
||||
|
||||
void testScreenshotsSignals() {
|
||||
QSignalSpy screenshotsEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged);
|
||||
|
||||
bool initialScreenshots = m_coreController->m_settingsController->isScreenshotsEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleScreenshotsEnabled(!initialScreenshots);
|
||||
QVERIFY2(screenshotsEnabledChangedSpy.count() == 1, "screenshotsEnabledChanged signal should be emitted");
|
||||
QVERIFY2(screenshotsEnabledChangedSpy.at(0).at(0).toBool() == !initialScreenshots, "screenshotsEnabledChanged should emit correct value");
|
||||
QVERIFY2(m_coreController->m_settingsController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testStartMinimizedSignals() {
|
||||
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
|
||||
|
||||
bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized);
|
||||
QVERIFY2(startMinimizedChangedSpy.count() == 1, "startMinimizedChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_settingsController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testAutoConnectSignals() {
|
||||
bool initialAutoConnect = m_coreController->m_settingsController->isAutoConnectEnabled();
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAutoConnect(!initialAutoConnect);
|
||||
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isAutoConnect() == !initialAutoConnect, "Auto connect state should be available in SecureAppSettingsRepository");
|
||||
|
||||
m_coreController->m_settingsUiController->toggleAutoConnect(initialAutoConnect);
|
||||
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsUiController");
|
||||
}
|
||||
|
||||
void testLanguageChangeSignals() {
|
||||
QSignalSpy appLanguageChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appLanguageChanged);
|
||||
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
|
||||
|
||||
QLocale initialLocale = m_coreController->m_settingsController->getAppLanguage();
|
||||
QLocale newLocale = (initialLocale.language() == QLocale::English) ? QLocale::Russian : QLocale::English;
|
||||
|
||||
m_coreController->m_settingsController->setAppLanguage(newLocale);
|
||||
QVERIFY2(appLanguageChangedSpy.count() == 1, "appLanguageChanged signal should be emitted");
|
||||
QVERIFY2(appLanguageChangedSpy.at(0).at(0).value<QLocale>() == newLocale, "appLanguageChanged should emit correct locale");
|
||||
QVERIFY2(m_coreController->m_settingsController->getAppLanguage() == newLocale, "App language should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->getAppLanguage() == newLocale, "App language should be available in SecureAppSettingsRepository");
|
||||
|
||||
if (m_coreController->m_languageModel) {
|
||||
QString newLanguageName = m_coreController->m_languageUiController->getCurrentLanguageName();
|
||||
QVERIFY2(!newLanguageName.isEmpty(), "Language name should be available in LanguageUiController");
|
||||
}
|
||||
}
|
||||
|
||||
void testGatewayEndpointSignals() {
|
||||
QSignalSpy gatewayEndpointChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::gatewayEndpointChanged);
|
||||
QSignalSpy devGatewayEnvChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::devGatewayEnvChanged);
|
||||
|
||||
QString initialEndpoint = m_coreController->m_settingsController->getGatewayEndpoint();
|
||||
QString newEndpoint = "https://test-gateway.example.com";
|
||||
|
||||
m_coreController->m_settingsUiController->setGatewayEndpoint(newEndpoint);
|
||||
QVERIFY2(gatewayEndpointChangedSpy.count() == 1, "gatewayEndpointChanged signal should be emitted");
|
||||
QVERIFY2(gatewayEndpointChangedSpy.at(0).at(0).toString() == newEndpoint, "gatewayEndpointChanged should emit correct endpoint");
|
||||
QVERIFY2(m_coreController->m_settingsController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SecureAppSettingsRepository");
|
||||
|
||||
bool initialDevEnv = m_coreController->m_settingsController->isDevGatewayEnv();
|
||||
m_coreController->m_settingsUiController->toggleDevGatewayEnv(!initialDevEnv);
|
||||
QVERIFY2(devGatewayEnvChangedSpy.count() == 1, "devGatewayEnvChanged signal should be emitted");
|
||||
QVERIFY2(devGatewayEnvChangedSpy.at(0).at(0).toBool() == !initialDevEnv, "devGatewayEnvChanged should emit correct value");
|
||||
QVERIFY2(m_coreController->m_settingsController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be updated in SettingsController");
|
||||
QVERIFY2(m_coreController->m_settingsUiController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SettingsUiController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SecureAppSettingsRepository");
|
||||
}
|
||||
|
||||
void testSettingsClearedSignal() {
|
||||
QSignalSpy settingsClearedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared);
|
||||
|
||||
m_coreController->m_settingsController->clearSettings();
|
||||
QVERIFY2(settingsClearedSpy.count() == 1, "settingsCleared signal should be emitted");
|
||||
}
|
||||
|
||||
void testSplitTunnelingSignals() {
|
||||
QSignalSpy siteSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::siteSplitTunnelingToggled);
|
||||
QSignalSpy appSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::appSplitTunnelingToggled);
|
||||
QSignalSpy sitesSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesSplitTunnelingEnabledChanged);
|
||||
QSignalSpy appsSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsSplitTunnelingEnabledChanged);
|
||||
QSignalSpy routeModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::routeModeChanged);
|
||||
QSignalSpy appsRouteModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsRouteModeChanged);
|
||||
QSignalSpy sitesChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesChanged);
|
||||
QSignalSpy appsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsChanged);
|
||||
|
||||
bool initialSitesSplitTunneling = m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled();
|
||||
m_coreController->m_ipSplitTunnelingController->toggleSplitTunneling(!initialSitesSplitTunneling);
|
||||
QVERIFY2(sitesSplitTunnelingEnabledChangedSpy.count() == 1, "sitesSplitTunnelingEnabledChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be updated in IpSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isSitesSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be available in SecureAppSettingsRepository");
|
||||
|
||||
bool initialAppsSplitTunneling = m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled();
|
||||
m_coreController->m_appSplitTunnelingController->toggleSplitTunneling(!initialAppsSplitTunneling);
|
||||
QVERIFY2(appsSplitTunnelingEnabledChangedSpy.count() == 1, "appsSplitTunnelingEnabledChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be updated in AppSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->isAppsSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be available in SecureAppSettingsRepository");
|
||||
|
||||
RouteMode initialRouteMode = m_coreController->m_ipSplitTunnelingController->getRouteMode();
|
||||
RouteMode newRouteMode = (initialRouteMode == RouteMode::VpnOnlyForwardSites)
|
||||
? RouteMode::VpnAllExceptSites
|
||||
: RouteMode::VpnOnlyForwardSites;
|
||||
m_coreController->m_ipSplitTunnelingController->setRouteMode(newRouteMode);
|
||||
QVERIFY2(routeModeChangedSpy.count() == 1, "routeModeChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingController->getRouteMode() == newRouteMode, "Route mode should be updated in IpSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->routeMode() == newRouteMode, "Route mode should be available in SecureAppSettingsRepository");
|
||||
|
||||
AppsRouteMode initialAppsRouteMode = m_coreController->m_appSplitTunnelingController->getRouteMode();
|
||||
AppsRouteMode newAppsRouteMode = (initialAppsRouteMode == AppsRouteMode::VpnAllExceptApps)
|
||||
? AppsRouteMode::VpnAllApps
|
||||
: AppsRouteMode::VpnAllExceptApps;
|
||||
m_coreController->m_appSplitTunnelingController->setRouteMode(newAppsRouteMode);
|
||||
QVERIFY2(appsRouteModeChangedSpy.count() == 1, "appsRouteModeChanged signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_appSplitTunnelingController->getRouteMode() == newAppsRouteMode, "Apps route mode should be updated in AppSplitTunnelingController");
|
||||
QVERIFY2(m_coreController->m_appSettingsRepository->appsRouteMode() == newAppsRouteMode, "Apps route mode should be available in SecureAppSettingsRepository");
|
||||
|
||||
QMap<QString, QString> sitesMap{{"example.com", "1.2.3.4"}};
|
||||
m_coreController->m_ipSplitTunnelingController->addSites(sitesMap, true);
|
||||
QVERIFY2(sitesChangedSpy.count() >= 1, "sitesChanged signal should be emitted");
|
||||
QVector<QPair<QString, QString>> currentSites = m_coreController->m_ipSplitTunnelingController->getCurrentSites();
|
||||
QVERIFY2(currentSites.size() >= 1, "Sites should be available in IpSplitTunnelingController");
|
||||
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController != nullptr, "IpSplitTunnelingUiController should exist");
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingModel != nullptr, "IpSplitTunnelingModel should exist");
|
||||
|
||||
m_coreController->m_ipSplitTunnelingUiController->updateModel();
|
||||
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() >= 1, "Sites should be available in IpSplitTunnelingModel");
|
||||
QString modelUrl = m_coreController->m_ipSplitTunnelingModel->data(m_coreController->m_ipSplitTunnelingModel->index(0, 0), IpSplitTunnelingModel::UrlRole).toString();
|
||||
QVERIFY2(modelUrl == "example.com", "Site URL should be available in IpSplitTunnelingModel");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSettingsSignals)
|
||||
#include "testSettingsSignals.moc"
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestSignalOrder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testSignalOrderOnImport() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult.config);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
|
||||
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
|
||||
|
||||
QVERIFY2(serverAddedSpy.at(0).count() > 0, "serverAdded should have arguments");
|
||||
}
|
||||
|
||||
void testSignalOrderOnRemoveDefault() {
|
||||
QString awgKey = "vpn://AAABFHjadZBBT4QwEIX_ipkzS2wBJdyMB1cPXvbgwRgyQnclgZa0RTYS_rszXRa52Mt77TfzOu0EldEeG62sg-J9AhxPUEywF1CAuF3WTl4dRLCXhJIVpVuUEMpWdLdFKaH7FeUb9Mx3scpFk0XTRbOLvlSkKZsOz-Gi4BsdRiV_EGEydhwlg0tWynEZmd5Yz1bkoaK3xpvKtOU3_UFjOE3SsRs-tfIl1rVVzoWQOI9FzC3eonYcU4ZmgkPdwxz9fSYdYafVT4M7-lEJ80cEtTri0PrH_2q4wlW26f1lioe3p5uDsjQWoS_j_Ct2ipvGU6zO2PWtiivT8RPQudHYmqBXzl-3Yn2slBEMTtklgYt4C_Mv3ROMwA";
|
||||
QString xrayKey = "vpn://AAAAtXjadY7NCsJADIRfRXKui1YP0qt3L14EkRK7EQt2d0lS_0rf3awonjyFmW-YyQBNDIptIBao9sNPQgXYBXq2OL0zPqCA96kGSJHV6HK5MFP6YyCt0XsmsQqYz9zKzd3MmDIGyek6cdRoUJsE43gowNMJ-4uu_695kobbpG0MBndmTrbEV4sWcI6iG-zIQE47umOXLuSa2BlNKHKL7PMeiX5lmdH79bIsoBfiT0UOZQnjCw_AXRQ";
|
||||
|
||||
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult1.config);
|
||||
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
|
||||
m_coreController->m_importCoreController->importConfig(importResult2.config);
|
||||
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1");
|
||||
|
||||
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
|
||||
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
|
||||
|
||||
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
|
||||
|
||||
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
|
||||
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted when removing default server");
|
||||
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->defaultServerId(),
|
||||
"defaultServerChanged should emit new default server id");
|
||||
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSignalOrder)
|
||||
#include "testSignalOrder.moc"
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
#include <QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSignalSpy>
|
||||
#include <QModelIndex>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/controllers/selfhosted/importController.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
#include "ui/models/containersModel.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
|
||||
using namespace amnezia;
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "secureQSettings.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
int defaultServerRow(const QVector<ServerDescription> &descriptions, const QString &defaultServerId)
|
||||
{
|
||||
for (int i = 0; i < descriptions.size(); ++i) {
|
||||
if (descriptions.at(i).serverId == defaultServerId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class TestUiServersModelAndController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
CoreController* m_coreController;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QJsonObject createAwg2Config()
|
||||
{
|
||||
QJsonObject clientConfig;
|
||||
clientConfig[configKey::mtu] = protocols::awg::defaultMtu;
|
||||
clientConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
|
||||
clientConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
|
||||
clientConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
|
||||
clientConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
|
||||
clientConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
|
||||
clientConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
|
||||
clientConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
|
||||
clientConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
|
||||
clientConfig[configKey::clientPrivKey] = "test_client_private_key";
|
||||
clientConfig[configKey::clientPubKey] = "test_client_public_key";
|
||||
clientConfig[configKey::serverPubKey] = "test_server_public_key";
|
||||
clientConfig[configKey::pskKey] = "test_psk_key";
|
||||
clientConfig[configKey::clientIp] = "10.8.1.2";
|
||||
clientConfig[configKey::allowedIps] = QJsonArray::fromStringList({"0.0.0.0/0"});
|
||||
|
||||
QJsonObject awgConfig;
|
||||
awgConfig[configKey::lastConfig] = QString(QJsonDocument(clientConfig).toJson());
|
||||
awgConfig[configKey::port] = protocols::awg::defaultPort;
|
||||
awgConfig[configKey::transportProto] = "udp";
|
||||
awgConfig[configKey::protocolVersion] = protocols::awg::awgV2;
|
||||
awgConfig[configKey::subnetAddress] = protocols::wireguard::defaultSubnetAddress;
|
||||
awgConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
|
||||
awgConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
|
||||
awgConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
|
||||
awgConfig[configKey::initPacketJunkSize] = protocols::awg::defaultInitPacketJunkSize;
|
||||
awgConfig[configKey::responsePacketJunkSize] = protocols::awg::defaultResponsePacketJunkSize;
|
||||
awgConfig[configKey::cookieReplyPacketJunkSize] = protocols::awg::defaultCookieReplyPacketJunkSize;
|
||||
awgConfig[configKey::transportPacketJunkSize] = protocols::awg::defaultTransportPacketJunkSize;
|
||||
awgConfig[configKey::initPacketMagicHeader] = protocols::awg::defaultInitPacketMagicHeader;
|
||||
awgConfig[configKey::responsePacketMagicHeader] = protocols::awg::defaultResponsePacketMagicHeader;
|
||||
awgConfig[configKey::underloadPacketMagicHeader] = protocols::awg::defaultUnderloadPacketMagicHeader;
|
||||
awgConfig[configKey::transportPacketMagicHeader] = protocols::awg::defaultTransportPacketMagicHeader;
|
||||
awgConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
|
||||
awgConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
|
||||
awgConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
|
||||
awgConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
|
||||
awgConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
|
||||
awgConfig[configKey::isThirdPartyConfig] = true;
|
||||
|
||||
QJsonObject container;
|
||||
container[configKey::container] = "amnezia-awg";
|
||||
container[configKey::awg] = awgConfig;
|
||||
|
||||
QJsonArray containers;
|
||||
containers.append(container);
|
||||
|
||||
QJsonObject config;
|
||||
config[configKey::containers] = containers;
|
||||
config[configKey::defaultContainer] = "amnezia-awg";
|
||||
config[configKey::description] = "AWG2 Test Server";
|
||||
config[configKey::hostName] = "test.example.com";
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
QJsonObject createServerDescriptionTestConfig(bool withAmneziaDns)
|
||||
{
|
||||
QJsonObject config = createAwg2Config();
|
||||
config[configKey::description] = "Server 1";
|
||||
if (withAmneziaDns) {
|
||||
config[configKey::dns1] = protocols::dns::amneziaDnsIp;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase() {
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new CoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase() {
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init() {
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void testUiServersModelAndControllerRoles() {
|
||||
QJsonObject testConfig = createAwg2Config();
|
||||
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
|
||||
m_coreController->m_importCoreController->importConfig(testConfig);
|
||||
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
|
||||
|
||||
int serverIndex = m_coreController->m_serversRepository->defaultServerIndex();
|
||||
QVERIFY2(serverIndex == 0, "Default server index should be 0");
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "ServersModel should have 1 row");
|
||||
|
||||
QModelIndex serverModelIndex = m_coreController->m_serversModel->index(0, 0);
|
||||
QVERIFY2(serverModelIndex.isValid(), "Server model index should be valid");
|
||||
|
||||
QString serverName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::NameRole).toString();
|
||||
QVERIFY2(serverName == "AWG2 Test Server", QString("Server name should be 'AWG2 Test Server', got '%1'").arg(serverName).toUtf8().constData());
|
||||
|
||||
QString serverDescription = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::ServerDescriptionRole).toString();
|
||||
QVERIFY2(serverDescription.contains("test.example.com"), QString("Server description should contain hostname, got '%1'").arg(serverDescription).toUtf8().constData());
|
||||
|
||||
QString hostName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HostNameRole).toString();
|
||||
QVERIFY2(hostName == "test.example.com", "Host name should match");
|
||||
|
||||
bool isDefault = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::IsDefaultRole).toBool();
|
||||
QVERIFY2(isDefault == true, "Server should be default");
|
||||
|
||||
bool hasInstalledContainers = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasInstalledContainers).toBool();
|
||||
QVERIFY2(hasInstalledContainers == true, "Server should have installed containers");
|
||||
|
||||
bool hasWriteAccess = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasWriteAccessRole).toBool();
|
||||
QVERIFY2(hasWriteAccess == false, "Server should not have write access for imported config");
|
||||
|
||||
int defaultContainerRole = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::DefaultContainerRole).toInt();
|
||||
DockerContainer expectedContainer = DockerContainer::Awg;
|
||||
QVERIFY2(defaultContainerRole == static_cast<int>(expectedContainer), "Default container should be Awg");
|
||||
}
|
||||
|
||||
if (m_coreController->m_serversUiController) {
|
||||
m_coreController->m_serversUiController->setProcessedServerId(
|
||||
m_coreController->m_serversUiController->getServerId(0));
|
||||
|
||||
QString hostName = "test.example.com";
|
||||
|
||||
QString collapsedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionCollapsed();
|
||||
QString expectedCollapsed = "AmneziaWG (version 2) | " + hostName;
|
||||
QVERIFY2(collapsedDescription == expectedCollapsed,
|
||||
QString("Collapsed description should be '%1', got '%2'").arg(expectedCollapsed, collapsedDescription).toUtf8().constData());
|
||||
|
||||
QString expandedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionExpanded();
|
||||
QString expectedExpanded = hostName;
|
||||
QVERIFY2(expandedDescription == expectedExpanded,
|
||||
QString("Expanded description should be '%1', got '%2'").arg(expectedExpanded, expandedDescription).toUtf8().constData());
|
||||
}
|
||||
|
||||
if (m_coreController->m_containersModel) {
|
||||
|
||||
int awgContainerIndex = -1;
|
||||
for (int i = 0; i < ContainerUtils::allContainers().size(); ++i) {
|
||||
DockerContainer container = ContainerUtils::allContainers().at(i);
|
||||
if (container == DockerContainer::Awg) {
|
||||
awgContainerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QVERIFY2(awgContainerIndex >= 0, "Awg container index should be found");
|
||||
|
||||
QModelIndex containerModelIndex = m_coreController->m_containersModel->index(awgContainerIndex, 0);
|
||||
QVERIFY2(containerModelIndex.isValid(), "Container model index should be valid");
|
||||
|
||||
bool isInstalled = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsInstalledRole).toBool();
|
||||
QVERIFY2(isInstalled == true, "Awg container should be installed");
|
||||
|
||||
bool isVpnContainer = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsVpnContainerRole).toBool();
|
||||
QVERIFY2(isVpnContainer == true, "Awg container should be VPN container");
|
||||
|
||||
QString containerName = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::NameRole).toString();
|
||||
QString expectedContainerName = ContainerUtils::containerHumanNames().value(DockerContainer::Awg);
|
||||
QVERIFY2(containerName == expectedContainerName, QString("Container name should be '%1', got '%2'").arg(expectedContainerName, containerName).toUtf8().constData());
|
||||
|
||||
QString containerDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DescriptionRole).toString();
|
||||
QString expectedDescription = ContainerUtils::containerDescriptions().value(DockerContainer::Awg);
|
||||
QVERIFY2(containerDescription == expectedDescription, QString("Container description should match, got '%1'").arg(containerDescription).toUtf8().constData());
|
||||
|
||||
QString detailedDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DetailedDescriptionRole).toString();
|
||||
QString expectedDetailedDescription = ContainerUtils::containerDetailedDescriptions().value(DockerContainer::Awg);
|
||||
QVERIFY2(detailedDescription == expectedDetailedDescription, QString("Container detailed description should match, got '%1'").arg(detailedDescription).toUtf8().constData());
|
||||
|
||||
int serviceType = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ServiceTypeRole).toInt();
|
||||
QVERIFY2(serviceType == static_cast<int>(ProtocolEnumNS::ServiceType::Vpn), "Service type should be Vpn");
|
||||
|
||||
bool isSupported = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsSupportedRole).toBool();
|
||||
QVERIFY2(isSupported == true, "Container should be supported");
|
||||
|
||||
bool isShareable = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsShareableRole).toBool();
|
||||
QVERIFY2(isShareable == true, "Container should be shareable");
|
||||
|
||||
QJsonObject containerConfig = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ConfigRole).toJsonObject();
|
||||
QVERIFY2(!containerConfig.isEmpty(), "Container config should not be empty");
|
||||
QVERIFY2(containerConfig.value(configKey::container).toString() == "amnezia-awg", "Container config should have correct container type");
|
||||
|
||||
QJsonObject awgProtocolConfig = containerConfig.value(configKey::awg).toObject();
|
||||
QVERIFY2(!awgProtocolConfig.isEmpty(), "AWG protocol config should not be empty");
|
||||
|
||||
QString protocolVersion = awgProtocolConfig.value(configKey::protocolVersion).toString();
|
||||
QVERIFY2(protocolVersion == protocols::awg::awgV2, QString("Protocol version should be '%1', got '%2'").arg(protocols::awg::awgV2, protocolVersion).toUtf8().constData());
|
||||
|
||||
QString port = awgProtocolConfig.value(configKey::port).toString();
|
||||
QVERIFY2(port == protocols::awg::defaultPort, QString("Port should be '%1', got '%2'").arg(protocols::awg::defaultPort, port).toUtf8().constData());
|
||||
|
||||
QString subnetAddress = awgProtocolConfig.value(configKey::subnetAddress).toString();
|
||||
QVERIFY2(subnetAddress == protocols::wireguard::defaultSubnetAddress, QString("Subnet address should be '%1', got '%2'").arg(protocols::wireguard::defaultSubnetAddress, subnetAddress).toUtf8().constData());
|
||||
|
||||
bool isThirdParty = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsThirdPartyConfigRole).toBool();
|
||||
QVERIFY2(isThirdParty == true, "Imported config should be third party config");
|
||||
|
||||
DockerContainer dockerContainer = static_cast<DockerContainer>(m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DockerContainerRole).toInt());
|
||||
QVERIFY2(dockerContainer == DockerContainer::Awg, "Docker container should be Awg");
|
||||
|
||||
QString containerString = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ContainerStringRole).toString();
|
||||
QVERIFY2(containerString == "amnezia-awg", "Container string should be amnezia-awg");
|
||||
}
|
||||
}
|
||||
|
||||
void testServerDescriptionFormat() {
|
||||
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
|
||||
|
||||
QJsonObject configNoDns = createServerDescriptionTestConfig(false);
|
||||
m_coreController->m_importCoreController->importConfig(configNoDns);
|
||||
QVERIFY2(importFinishedSpy.count() == 1, "importFinished should be emitted");
|
||||
m_coreController->m_appSettingsRepository->setUseAmneziaDns(false);
|
||||
QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions(
|
||||
m_coreController->m_appSettingsRepository->useAmneziaDns());
|
||||
const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId();
|
||||
m_coreController->m_serversModel->updateModel(descriptionsNoDns, defaultServerRow(descriptionsNoDns, defIdNoDns));
|
||||
|
||||
QString descNoDns = m_coreController->m_serversModel->data(
|
||||
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
|
||||
QVERIFY2(descNoDns == "test.example.com",
|
||||
QString("Without Amnezia DNS expected 'test.example.com', got '%1'").arg(descNoDns).toUtf8().constData());
|
||||
|
||||
m_coreController->m_serversRepository->clearServers();
|
||||
if (m_coreController->m_serversRepository->serversCount() > 0) {
|
||||
m_coreController->m_serversRepository->setDefaultServer(m_coreController->m_serversRepository->serverIdAt(0));
|
||||
}
|
||||
|
||||
QJsonObject configWithDns = createServerDescriptionTestConfig(true);
|
||||
m_coreController->m_importCoreController->importConfig(configWithDns);
|
||||
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
|
||||
m_coreController->m_appSettingsRepository->setUseAmneziaDns(true);
|
||||
QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions(
|
||||
m_coreController->m_appSettingsRepository->useAmneziaDns());
|
||||
const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId();
|
||||
m_coreController->m_serversModel->updateModel(descriptionsWithDns, defaultServerRow(descriptionsWithDns, defIdWithDns));
|
||||
|
||||
QString descWithDns = m_coreController->m_serversModel->data(
|
||||
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
|
||||
QVERIFY2(descWithDns == "Amnezia DNS | test.example.com",
|
||||
QString("With Amnezia DNS expected 'Amnezia DNS | test.example.com', got '%1'").arg(descWithDns).toUtf8().constData());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestUiServersModelAndController)
|
||||
#include "testUiServersModelAndController.moc"
|
||||
@@ -1822,16 +1822,6 @@ Thank you for staying with us!</source>
|
||||
<source>Cancel</source>
|
||||
<translation>Отменить</translation>
|
||||
</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>
|
||||
<name>PageSettingsApiInstructions</name>
|
||||
|
||||
@@ -1,739 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#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,10 +16,6 @@
|
||||
#include <QFutureWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace configKey
|
||||
@@ -69,6 +65,7 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
|
||||
ApiCountryModel* apiCountryModel,
|
||||
ApiDevicesModel* apiDevicesModel,
|
||||
SettingsController* settingsController,
|
||||
ConnectionController* connectionController,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_serversController(serversController),
|
||||
@@ -80,13 +77,34 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
|
||||
m_apiAccountInfoModel(apiAccountInfoModel),
|
||||
m_apiCountryModel(apiCountryModel),
|
||||
m_apiDevicesModel(apiDevicesModel),
|
||||
m_settingsController(settingsController)
|
||||
m_settingsController(settingsController),
|
||||
m_connectionController(connectionController)
|
||||
{
|
||||
connect(m_apiServicesModel, &ApiServicesModel::serviceSelectionChanged, this, [this]() {
|
||||
ApiServicesModel::ApiServicesData selectedServiceData = m_apiServicesModel->selectedServiceData();
|
||||
m_apiSubscriptionPlansModel->updateModel(selectedServiceData.subscriptionPlansJson);
|
||||
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::isCaptchaAwaitingUser() const
|
||||
{
|
||||
return m_captchaState.isPending;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName)
|
||||
@@ -275,18 +293,105 @@ bool SubscriptionUiController::importFreeFromGateway()
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData = m_subscriptionController->generateProtocolData(serviceProtocol);
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(userCountryCode, serviceType,
|
||||
serviceProtocol, protocolData);
|
||||
serviceProtocol, protocolData,
|
||||
captchaInfo);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return true;
|
||||
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
m_captchaState.userCountryCode = userCountryCode;
|
||||
m_captchaState.serviceType = serviceType;
|
||||
m_captchaState.serviceProtocol = serviceProtocol;
|
||||
m_captchaState.openvpnPrivKey = protocolData.certPrivKey;
|
||||
m_captchaState.wireguardClientPrivKey = protocolData.wireGuardClientPrivKey;
|
||||
m_captchaState.wireguardClientPubKey = protocolData.wireGuardClientPubKey;
|
||||
m_captchaState.xrayUuid = protocolData.xrayUuid;
|
||||
m_captchaState.isPending = true;
|
||||
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
return false;
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const QString &solution)
|
||||
{
|
||||
if (!m_captchaState.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
|
||||
protocolData.xrayUuid = m_captchaState.xrayUuid;
|
||||
|
||||
SubscriptionController::CaptchaInfo retryCaptcha;
|
||||
ErrorCode errorCode = m_subscriptionController->resolveImportServiceCaptcha(
|
||||
m_captchaState.userCountryCode,
|
||||
m_captchaState.serviceType,
|
||||
m_captchaState.serviceProtocol,
|
||||
protocolData,
|
||||
captchaId,
|
||||
solution,
|
||||
&retryCaptcha);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
m_captchaState.isPending = false;
|
||||
emit captchaFlowDismissRequested();
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||
|| errorCode == ErrorCode::ApiCaptchaRequiredError)
|
||||
&& retryCaptcha.isRequired) {
|
||||
emit captchaRequired(retryCaptcha.captchaId, retryCaptcha.captchaImageBase64,
|
||||
retryCaptcha.hint.isEmpty() ? tr("Enter the digits from the image to continue") : retryCaptcha.hint);
|
||||
return;
|
||||
}
|
||||
|
||||
m_captchaState.isPending = false;
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
|
||||
void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||
{
|
||||
if (!m_captchaState.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionController::ProtocolData protocolData;
|
||||
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
|
||||
protocolData.xrayUuid = m_captchaState.xrayUuid;
|
||||
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
|
||||
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(
|
||||
m_captchaState.userCountryCode,
|
||||
m_captchaState.serviceType,
|
||||
m_captchaState.serviceProtocol,
|
||||
protocolData,
|
||||
captchaInfo);
|
||||
|
||||
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
m_captchaState.isPending = false;
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importTrialFromGateway(const QString &email)
|
||||
{
|
||||
emit trialEmailError(QString());
|
||||
@@ -440,11 +545,7 @@ bool SubscriptionUiController::getAccountInfo(const QString &serverId, bool relo
|
||||
if (reload) {
|
||||
QEventLoop wait;
|
||||
QTimer::singleShot(1000, &wait, &QEventLoop::quit);
|
||||
#ifdef Q_OS_IOS
|
||||
wait.exec();
|
||||
#else
|
||||
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
#endif
|
||||
}
|
||||
QJsonObject accountInfo;
|
||||
ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverId, accountInfo);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/api/servicesCatalogController.h"
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||
@@ -28,6 +29,7 @@ public:
|
||||
ApiCountryModel* apiCountryModel,
|
||||
ApiDevicesModel* apiDevicesModel,
|
||||
SettingsController* settingsController,
|
||||
ConnectionController* connectionController,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
|
||||
@@ -56,6 +58,10 @@ public slots:
|
||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||
bool isVlessProtocol(const QString &serverId);
|
||||
|
||||
bool isCaptchaAwaitingUser() const;
|
||||
void onCaptchaSolved(const QString &captchaId, const QString &solution);
|
||||
void onRefreshCaptchaRequested();
|
||||
|
||||
void removeApiConfig(const QString &serverId);
|
||||
|
||||
void removeServer(const QString &serverId);
|
||||
@@ -83,9 +89,23 @@ signals:
|
||||
void apiServerRemoved(const QString &message);
|
||||
|
||||
void vpnKeyExportReady();
|
||||
void captchaRequired(const QString &captchaId, const QString &captchaImageBase64, const QString &hint);
|
||||
void captchaFlowDismissRequested();
|
||||
|
||||
void unsupportedConnectDrawerRequested();
|
||||
|
||||
private:
|
||||
struct CaptchaState {
|
||||
QString userCountryCode;
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
QString openvpnPrivKey;
|
||||
QString wireguardClientPrivKey;
|
||||
QString wireguardClientPubKey;
|
||||
QString xrayUuid;
|
||||
bool isPending = false;
|
||||
} m_captchaState;
|
||||
|
||||
private:
|
||||
QList<QString> getQrCodes();
|
||||
int getQrCodesCount();
|
||||
@@ -104,6 +124,7 @@ private:
|
||||
ApiCountryModel* m_apiCountryModel;
|
||||
ApiDevicesModel* m_apiDevicesModel;
|
||||
SettingsController* m_settingsController;
|
||||
ConnectionController* m_connectionController;
|
||||
};
|
||||
|
||||
#endif // SUBSCRIPTIONUICONTROLLER_H
|
||||
|
||||
@@ -44,7 +44,6 @@ signals:
|
||||
void connectionStateChanged();
|
||||
|
||||
void connectionErrorOccurred(ErrorCode errorCode);
|
||||
void reconnectWithUpdatedContainer(const QString &message);
|
||||
|
||||
void connectButtonClicked();
|
||||
void preparingConfig();
|
||||
|
||||
@@ -12,7 +12,6 @@ LanguageUiController::LanguageUiController(SettingsController* settingsControlle
|
||||
void LanguageUiController::onAppLanguageChanged(const QLocale &locale)
|
||||
{
|
||||
emit updateTranslations(locale);
|
||||
emit translationsUpdated();
|
||||
}
|
||||
|
||||
void LanguageUiController::changeLanguage(const LanguageSettings::AvailableLanguageEnum language)
|
||||
|
||||
@@ -82,9 +82,6 @@ namespace PageLoader
|
||||
PageSetupWizardApiPremiumInfo,
|
||||
PageSetupWizardApiTrialEmail,
|
||||
|
||||
PageSettingsApiQrPairingSend,
|
||||
PageSetupWizardApiQrPairingReceive,
|
||||
|
||||
PageDevMenu,
|
||||
|
||||
PageProtocolXraySnapshots,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
@@ -51,6 +52,7 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
Socks5ProxyConfigModel *socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel *telemtConfigModel,
|
||||
ConnectionController *connectionController,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_installController(installController),
|
||||
@@ -69,7 +71,8 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
m_sftpConfigModel(sftpConfigModel),
|
||||
m_socks5ConfigModel(socks5ConfigModel),
|
||||
m_mtProxyConfigModel(mtConfigModel),
|
||||
m_telemtConfigModel(telemtConfigModel)
|
||||
m_telemtConfigModel(telemtConfigModel),
|
||||
m_connectionController(connectionController)
|
||||
{
|
||||
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
|
||||
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
|
||||
@@ -133,6 +136,10 @@ void InstallUiController::install(DockerContainer container, int port, Transport
|
||||
finishMessage += tr("\nAdded containers that were already installed on the server");
|
||||
}
|
||||
|
||||
if (!m_connectionController->isConnected()) {
|
||||
m_serversController->setDefaultServer(newServerId);
|
||||
}
|
||||
|
||||
emit installServerFinished(finishMessage);
|
||||
} else {
|
||||
const auto adminBefore = m_serversController->selfHostedAdminConfig(serverId);
|
||||
@@ -172,7 +179,12 @@ void InstallUiController::install(DockerContainer container, int port, Transport
|
||||
"All installed containers have been added to the application");
|
||||
}
|
||||
|
||||
emit installContainerFinished(finishMessage, ContainerUtils::containerService(container) == ServiceType::Other);
|
||||
const bool isServiceInstall = ContainerUtils::containerService(container) == ServiceType::Other;
|
||||
if (!m_connectionController->isConnected() && !isServiceInstall) {
|
||||
m_serversController->setDefaultContainer(serverId, container);
|
||||
}
|
||||
|
||||
emit installContainerFinished(finishMessage, isServiceInstall);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,11 +275,15 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
}
|
||||
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
const bool asyncUpdate = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
|
||||
|
||||
if (asyncUpdate) {
|
||||
emit serverIsBusy(true);
|
||||
auto *watcher = new QFutureWatcher<ErrorCode>(this);
|
||||
const Proto protocolTypeCopy = protocolType;
|
||||
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
|
||||
[this, watcher, serverId, container, closePage]() {
|
||||
[this, watcher, serverId, container, closePage, protocolTypeCopy]() {
|
||||
const ErrorCode errorCode = watcher->result();
|
||||
watcher->deleteLater();
|
||||
emit serverIsBusy(false);
|
||||
@@ -276,15 +292,8 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
const ContainerConfig updatedConfig =
|
||||
m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
|
||||
const auto defaultContainer =
|
||||
m_serversController->getDefaultContainer(serverId);
|
||||
if ((serverId == m_serversController->getDefaultServerId())
|
||||
&& (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
}
|
||||
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolTypeCopy));
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
} else {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
}
|
||||
@@ -295,7 +304,7 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
InstallController *installController = m_installController;
|
||||
QFuture<ErrorCode> future =
|
||||
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
|
||||
newConfigCopy]() mutable -> ErrorCode {
|
||||
newConfigCopy]() mutable -> ErrorCode {
|
||||
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
@@ -307,13 +316,8 @@ void InstallUiController::updateContainer(const QString &serverId, int container
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
m_protocolModel->updateModel(updatedConfig);
|
||||
|
||||
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
|
||||
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
}
|
||||
updateProtocolConfigModel(serverId, static_cast<int>(container), static_cast<int>(protocolType));
|
||||
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -427,6 +431,34 @@ void InstallUiController::removeContainer(const QString &serverId, int container
|
||||
DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
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);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
|
||||
@@ -517,6 +549,12 @@ void InstallUiController::setEncryptedPassphrase(QString passphrase)
|
||||
void InstallUiController::addEmptyServer()
|
||||
{
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/controllers/serversController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/selfhosted/usersController.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
@@ -52,6 +53,7 @@ public:
|
||||
Socks5ProxyConfigModel* socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel* telemtConfigModel,
|
||||
ConnectionController* connectionController,
|
||||
QObject *parent = nullptr);
|
||||
~InstallUiController();
|
||||
|
||||
@@ -127,8 +129,6 @@ signals:
|
||||
void serverIsBusy(const bool isBusy);
|
||||
void cancelInstallation();
|
||||
|
||||
void currentContainerUpdated();
|
||||
|
||||
void cachedProfileCleared(const QString &message);
|
||||
void apiConfigRemoved(const QString &message);
|
||||
|
||||
@@ -155,6 +155,7 @@ private:
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
ConnectionController* m_connectionController;
|
||||
|
||||
ServerCredentials m_processedServerCredentials;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "serversUiController.h"
|
||||
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -32,6 +31,12 @@ bool descriptionsHaveGatewayServers(const QVector<ServerDescription> &list)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ServerDescription &emptyServerDescription()
|
||||
{
|
||||
static const ServerDescription s_emptyDescription;
|
||||
return s_emptyDescription;
|
||||
}
|
||||
} // namespace
|
||||
ServersUiController::ServersUiController(ServersController* serversController,
|
||||
SettingsController* settingsController,
|
||||
@@ -101,8 +106,6 @@ void ServersUiController::setDefaultServer(const QString &serverId)
|
||||
return;
|
||||
}
|
||||
m_serversController->setDefaultServer(serverId);
|
||||
updateModel();
|
||||
emit defaultServerIdChanged(serverId);
|
||||
}
|
||||
|
||||
void ServersUiController::setDefaultContainer(const QString &serverId, int containerIndex)
|
||||
@@ -121,12 +124,12 @@ void ServersUiController::toggleAmneziaDns(bool enabled)
|
||||
updateModel();
|
||||
}
|
||||
|
||||
void ServersUiController::onDefaultServerChanged(const QString &/*defaultServerId*/)
|
||||
void ServersUiController::onDefaultServerChanged(const QString &defaultServerId)
|
||||
{
|
||||
updateModel();
|
||||
setProcessedServerId(m_serversController->getDefaultServerId());
|
||||
m_serversModel->setDefaultServerId(defaultServerId);
|
||||
updateDefaultServerContainersModel();
|
||||
emit defaultServerIdChanged(m_serversController->getDefaultServerId());
|
||||
|
||||
emit defaultServerIdChanged(defaultServerId);
|
||||
}
|
||||
|
||||
void ServersUiController::updateModel()
|
||||
@@ -137,27 +140,21 @@ void ServersUiController::updateModel()
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
const bool hadServersFromGatewayBefore = descriptionsHaveGatewayServers(m_orderedServerDescriptions);
|
||||
const bool hasServersFromGatewayNow = descriptionsHaveGatewayServers(descriptions);
|
||||
const int listCount = descriptions.size();
|
||||
const int defaultRowInDescriptions = rowForServerId(descriptions, defaultServerId);
|
||||
|
||||
m_orderedServerDescriptions = descriptions;
|
||||
|
||||
if (listCount == 0) {
|
||||
setProcessedServerId(QString());
|
||||
} else if (m_processedServerIndex >= listCount) {
|
||||
setProcessedServerId(defaultServerId);
|
||||
if (m_orderedServerDescriptions.isEmpty()) {
|
||||
if (!m_processedServerId.isEmpty()) {
|
||||
setProcessedServerId(QString());
|
||||
}
|
||||
} else if (!m_processedServerId.isEmpty()) {
|
||||
const int row = rowForServerId(m_orderedServerDescriptions, m_processedServerId);
|
||||
if (row < 0) {
|
||||
setProcessedServerId(defaultServerId);
|
||||
} else {
|
||||
setProcessedServerId(m_processedServerId);
|
||||
setProcessedServerId(QString());
|
||||
}
|
||||
} else if (defaultRowInDescriptions >= 0) {
|
||||
setProcessedServerId(defaultServerId);
|
||||
}
|
||||
|
||||
m_serversModel->updateModel(m_orderedServerDescriptions, defaultRowInDescriptions);
|
||||
m_serversModel->updateModel(m_orderedServerDescriptions, defaultServerId);
|
||||
|
||||
updateContainersModel();
|
||||
updateDefaultServerContainersModel();
|
||||
@@ -167,7 +164,6 @@ void ServersUiController::updateModel()
|
||||
}
|
||||
|
||||
emit defaultServerIdChanged(defaultServerId);
|
||||
emit defaultServerIndexChanged(defaultServerIndex());
|
||||
}
|
||||
|
||||
QString ServersUiController::getDefaultServerId() const
|
||||
@@ -177,64 +173,35 @@ QString ServersUiController::getDefaultServerId() const
|
||||
|
||||
QString ServersUiController::getDefaultServerName() const
|
||||
{
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == defaultServerId) {
|
||||
return description.serverName;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
return serverName(getDefaultServerId());
|
||||
}
|
||||
|
||||
QString ServersUiController::getDefaultServerDefaultContainerName() const
|
||||
{
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == defaultServerId) {
|
||||
return ContainerUtils::containerHumanNames().value(description.defaultContainer);
|
||||
}
|
||||
const auto &description = serverDescriptionById(getDefaultServerId());
|
||||
if (description.serverId.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return QString();
|
||||
return ContainerUtils::containerHumanNames().value(description.defaultContainer);
|
||||
}
|
||||
|
||||
QString ServersUiController::getDefaultServerDescriptionCollapsed() const
|
||||
{
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == defaultServerId) {
|
||||
return description.collapsedServerDescription;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
return serverDescriptionById(getDefaultServerId()).collapsedServerDescription;
|
||||
}
|
||||
|
||||
QString ServersUiController::getDefaultServerImagePathCollapsed() const
|
||||
{
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == defaultServerId) {
|
||||
if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
const QString imageCode = apiUtils::countryCodeBaseForFlag(description.apiServerCountryCode.toUpper());
|
||||
if (imageCode.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(imageCode);
|
||||
}
|
||||
const auto &description = serverDescriptionById(getDefaultServerId());
|
||||
if (!description.isApiV2 || description.apiServerCountryCode.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(description.apiServerCountryCode.toUpper());
|
||||
}
|
||||
|
||||
QString ServersUiController::getDefaultServerDescriptionExpanded() const
|
||||
{
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == defaultServerId) {
|
||||
return description.expandedServerDescription;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
return serverDescriptionById(getDefaultServerId()).expandedServerDescription;
|
||||
}
|
||||
|
||||
bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() const
|
||||
@@ -286,15 +253,75 @@ bool ServersUiController::isDefaultServerDefaultContainerHasSplitTunneling() con
|
||||
|
||||
bool ServersUiController::isDefaultServerFromApi() const
|
||||
{
|
||||
const QString defaultServerId = m_serversController->getDefaultServerId();
|
||||
return isServerFromApi(getDefaultServerId());
|
||||
}
|
||||
|
||||
bool ServersUiController::hasServerWithWriteAccess() const
|
||||
{
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == defaultServerId) {
|
||||
return description.isApiV2;
|
||||
if (description.hasWriteAccess) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
return m_processedContainerIndex;
|
||||
@@ -316,27 +343,17 @@ QString ServersUiController::getProcessedServerId() const
|
||||
|
||||
void ServersUiController::setProcessedServerId(const QString &serverId)
|
||||
{
|
||||
const int index = serverId.isEmpty() ? -1 : serverIndexForId(serverId);
|
||||
if (!serverId.isEmpty() && index < 0) {
|
||||
return;
|
||||
}
|
||||
const int newIndex = serverId.isEmpty() ? -1 : serverIndexForId(serverId);
|
||||
const QString normalizedServerId = newIndex >= 0 ? serverId : QString();
|
||||
|
||||
if (m_processedServerIndex != index || m_processedServerId != serverId) {
|
||||
m_processedServerIndex = index;
|
||||
m_processedServerId = serverId;
|
||||
m_serversModel->setProcessedServerIndex(index);
|
||||
if (m_processedServerId != normalizedServerId) {
|
||||
m_processedServerId = normalizedServerId;
|
||||
|
||||
if (index >= 0) {
|
||||
if (newIndex >= 0) {
|
||||
updateContainersModel();
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == serverId) {
|
||||
setProcessedContainerIndex(static_cast<int>(description.defaultContainer));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId != serverId) {
|
||||
if (description.serverId != normalizedServerId) {
|
||||
continue;
|
||||
}
|
||||
if (description.isApiV2) {
|
||||
@@ -350,45 +367,12 @@ void ServersUiController::setProcessedServerId(const QString &serverId)
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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);
|
||||
return processedServerDescription().isPremium;
|
||||
}
|
||||
|
||||
bool ServersUiController::isDefaultServerCurrentlyProcessed() const
|
||||
@@ -398,18 +382,22 @@ bool ServersUiController::isDefaultServerCurrentlyProcessed() const
|
||||
|
||||
bool ServersUiController::isProcessedServerHasWriteAccess() const
|
||||
{
|
||||
ServerCredentials credentials = m_serversController->getServerCredentials(m_processedServerId);
|
||||
return (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty());
|
||||
return isServerHasWriteAccess(m_processedServerId);
|
||||
}
|
||||
|
||||
QString ServersUiController::getDefaultServerDescription(const QString &serverId) const
|
||||
const ServerDescription &ServersUiController::processedServerDescription() const
|
||||
{
|
||||
return serverDescriptionById(m_processedServerId);
|
||||
}
|
||||
|
||||
const ServerDescription &ServersUiController::serverDescriptionById(const QString &serverId) const
|
||||
{
|
||||
for (const auto &description : m_orderedServerDescriptions) {
|
||||
if (description.serverId == serverId) {
|
||||
return description.baseDescription;
|
||||
return description;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
return emptyServerDescription();
|
||||
}
|
||||
|
||||
bool ServersUiController::hasServersFromGatewayApi() const
|
||||
@@ -472,6 +460,11 @@ int ServersUiController::getServerIndexById(const QString &serverId) const
|
||||
return rowForServerId(m_orderedServerDescriptions, serverId);
|
||||
}
|
||||
|
||||
int ServersUiController::getServersCount() const
|
||||
{
|
||||
return m_orderedServerDescriptions.size();
|
||||
}
|
||||
|
||||
void ServersUiController::updateContainersModel()
|
||||
{
|
||||
if (m_processedServerId.isEmpty()) {
|
||||
|
||||
@@ -19,7 +19,6 @@ class ServersUiController : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
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 defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerIdChanged)
|
||||
@@ -30,9 +29,8 @@ class ServersUiController : public QObject
|
||||
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged)
|
||||
|
||||
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(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIndexChanged)
|
||||
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIdChanged)
|
||||
|
||||
Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged)
|
||||
|
||||
@@ -72,20 +70,27 @@ public slots:
|
||||
QString getDefaultServerDescriptionExpanded() const;
|
||||
bool isDefaultServerDefaultContainerHasSplitTunneling() 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;
|
||||
void setProcessedServerId(const QString &serverId);
|
||||
|
||||
int getProcessedServerIndex() const;
|
||||
void setProcessedServerIndex(int index);
|
||||
|
||||
int defaultServerIndex() const;
|
||||
|
||||
int getProcessedContainerIndex() const;
|
||||
void setProcessedContainerIndex(int index);
|
||||
bool processedServerIsPremium() const;
|
||||
|
||||
const ServerCredentials getProcessedServerCredentials() const;
|
||||
bool isDefaultServerCurrentlyProcessed() const;
|
||||
bool isProcessedServerHasWriteAccess() const;
|
||||
|
||||
@@ -97,15 +102,14 @@ public slots:
|
||||
|
||||
QString getServerId(int index) const;
|
||||
int getServerIndexById(const QString &serverId) const;
|
||||
int getServersCount() const;
|
||||
QStringList getAllInstalledServicesName(int serverIndex) const;
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString &errorMessage);
|
||||
void finished(const QString &message);
|
||||
void defaultServerIdChanged(const QString &serverId);
|
||||
void defaultServerIndexChanged(int index);
|
||||
void processedServerIdChanged(const QString &serverId);
|
||||
void processedServerIndexChanged(int index);
|
||||
void processedContainerIndexChanged(int index);
|
||||
void hasServersFromGatewayApiChanged();
|
||||
void updateApiCountryModel();
|
||||
@@ -115,7 +119,8 @@ public:
|
||||
void updateModel();
|
||||
|
||||
private:
|
||||
QString getDefaultServerDescription(const QString &serverId) const;
|
||||
const ServerDescription &serverDescriptionById(const QString &serverId) const;
|
||||
const ServerDescription &processedServerDescription() const;
|
||||
int serverIndexForId(const QString &serverId) const;
|
||||
bool listHasServersFromGatewayApi() const;
|
||||
|
||||
@@ -130,7 +135,6 @@ private:
|
||||
|
||||
QVector<amnezia::ServerDescription> m_orderedServerDescriptions;
|
||||
|
||||
int m_processedServerIndex = -1;
|
||||
QString m_processedServerId;
|
||||
int m_processedContainerIndex = -1;
|
||||
};
|
||||
|
||||
@@ -164,6 +164,7 @@ void SettingsUiController::restoreAppConfigFromData(const QByteArray &data)
|
||||
emit amneziaDnsToggled(amneziaDnsEnabled);
|
||||
|
||||
emit restoreBackupFinished();
|
||||
emit startMinimizedChanged();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
@@ -177,6 +178,7 @@ QString SettingsUiController::getAppVersion()
|
||||
void SettingsUiController::clearSettings()
|
||||
{
|
||||
m_settingsController->clearSettings();
|
||||
emit startMinimizedChanged();
|
||||
emit resetLanguageToSystem();
|
||||
|
||||
emit changeSettingsFinished(tr("All settings have been reset to default values"));
|
||||
@@ -204,6 +206,9 @@ bool SettingsUiController::isAutoStartEnabled()
|
||||
void SettingsUiController::toggleAutoStart(bool enable)
|
||||
{
|
||||
m_settingsController->toggleAutoStart(enable);
|
||||
if (!enable) {
|
||||
emit startMinimizedChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool SettingsUiController::isStartMinimizedEnabled()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "apiAccountInfoModel.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
|
||||
@@ -12,8 +10,6 @@
|
||||
namespace
|
||||
{
|
||||
Logger logger("AccountInfoModel");
|
||||
|
||||
constexpr QLatin1String kCountryConfigSourceType("country_config");
|
||||
}
|
||||
|
||||
ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent)
|
||||
@@ -110,9 +106,6 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
case IsInAppPurchaseRole: {
|
||||
return m_accountInfoData.isInAppPurchase;
|
||||
}
|
||||
case ConfigurationFilesCountRole: {
|
||||
return m_accountInfoData.configurationFilesCount;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@@ -127,15 +120,6 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
|
||||
m_availableCountries = accountInfoObject.value(apiDefs::key::availableCountries).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.maxDeviceCount = accountInfoObject.value(apiDefs::key::maxDeviceCount).toInt();
|
||||
accountInfoData.subscriptionEndDate = accountInfoObject.value(apiDefs::key::subscriptionEndDate).toString();
|
||||
@@ -221,7 +205,6 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
roles[IsInAppPurchaseRole] = "isInAppPurchase";
|
||||
roles[ConfigurationFilesCountRole] = "configurationFilesCount";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ public:
|
||||
IsProtocolSelectionSupportedRole,
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
IsInAppPurchaseRole,
|
||||
ConfigurationFilesCountRole
|
||||
IsInAppPurchaseRole
|
||||
};
|
||||
|
||||
explicit ApiAccountInfoModel(QObject *parent = nullptr);
|
||||
@@ -65,7 +64,6 @@ private:
|
||||
|
||||
bool isInAppPurchase = false;
|
||||
bool isRenewalAvailable = false;
|
||||
int configurationFilesCount = 0;
|
||||
};
|
||||
|
||||
AccountInfoData m_accountInfoData;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "logger.h"
|
||||
|
||||
namespace
|
||||
@@ -42,7 +41,7 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
|
||||
return countryInfo.countryName;
|
||||
}
|
||||
case CountryImageCodeRole: {
|
||||
return apiUtils::countryCodeBaseForFlag(countryInfo.countryCode);
|
||||
return countryInfo.countryCode.toUpper();
|
||||
}
|
||||
case IsIssuedRole: {
|
||||
return isIssued;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user